diff --git a/.editorconfig b/.editorconfig index c7a381b730..25e0135725 100644 --- a/.editorconfig +++ b/.editorconfig @@ -137,7 +137,7 @@ space_within_single_line_array_initializer_braces = true csharp_wrap_before_ternary_opsigns = false # Xaml files -[*.xaml] +[*.{xaml,axaml}] indent_size = 2 # Xml project files diff --git a/.gitignore b/.gitignore index 9b15011929..44fe5e4ba4 100644 --- a/.gitignore +++ b/.gitignore @@ -192,13 +192,12 @@ dirs.sln ################## -# XCode +# Xcode ################## Index/ Logs/ ModuleCache.noindex/ Build/Intermediates.noindex/ -info.plist build-intermediate obj-Direct2D1/ obj-Skia/ diff --git a/.ncrunch/Avalonia.IntegrationTests.Appium.v3.ncrunchproject b/.ncrunch/Avalonia.IntegrationTests.Appium.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Avalonia.IntegrationTests.Appium.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/Avalonia.IntegrationTests.Win32.v3.ncrunchproject b/.ncrunch/Avalonia.IntegrationTests.Win32.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Avalonia.IntegrationTests.Win32.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/IntegrationTestApp.v3.ncrunchproject b/.ncrunch/IntegrationTestApp.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/IntegrationTestApp.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index 0354e20d4f..1e2a3c6027 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -5,48 +5,26 @@ VisualStudioVersion = 17.0.31903.59 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 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Visuals", "src\Avalonia.Visuals\Avalonia.Visuals.csproj", "{EB582467-6ABB-43A1-B052-E981BA910E3A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Layout", "src\Avalonia.Layout\Avalonia.Layout.csproj", "{42472427-4774-4C81-8AFF-9F27B8E31721}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Windows", "Windows", "{B39A8919-9F95-48FE-AD7B-76E08B509888}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32", "src\Windows\Avalonia.Win32\Avalonia.Win32.csproj", "{811A76CF-1CF6-440F-963B-BBE31BD72A82}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1", "src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj", "{3E908F67-5543-4879-A1DC-08EACE79B3CD}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Input", "src\Avalonia.Input\Avalonia.Input.csproj", "{62024B2D-53EB-4638-B26B-85EEAA54866E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Interactivity", "src\Avalonia.Interactivity\Avalonia.Interactivity.csproj", "{6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls", "src\Avalonia.Controls\Avalonia.Controls.csproj", "{D2221C82-4A25-4583-9B43-D791E3F6820C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Styling", "src\Avalonia.Styling\Avalonia.Styling.csproj", "{F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Default", "src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Diagnostics", "src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj", "{7062AE20-5DCC-4442-9645-8195BDECE63E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Animation", "src\Avalonia.Animation\Avalonia.Animation.csproj", "{D211E587-D8BC-45B9-95A4-F297C8FA5200}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Styling.UnitTests", "tests\Avalonia.Styling.UnitTests\Avalonia.Styling.UnitTests.csproj", "{47ECDF59-DEF8-4C53-87B1-2098A3429059}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.UnitTests", "tests\Avalonia.Controls.UnitTests\Avalonia.Controls.UnitTests.csproj", "{5CCB5571-7C30-4E7D-967D-0E2158EBD91F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Visuals.UnitTests", "tests\Avalonia.Visuals.UnitTests\Avalonia.Visuals.UnitTests.csproj", "{76716382-3159-460E-BDA6-C5715CF606D7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base.UnitTests", "tests\Avalonia.Base.UnitTests\Avalonia.Base.UnitTests.csproj", "{2905FF23-53FB-45E6-AA49-6AF47A172056}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Layout.UnitTests", "tests\Avalonia.Layout.UnitTests\Avalonia.Layout.UnitTests.csproj", "{DB070A10-BF39-4752-8456-86E9D5928478}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Interactivity.UnitTests", "tests\Avalonia.Interactivity.UnitTests\Avalonia.Interactivity.UnitTests.csproj", "{08478EF5-44E8-42E9-92D6-15E00EC038D8}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1.RenderTests", "tests\Avalonia.Direct2D1.RenderTests\Avalonia.Direct2D1.RenderTests.csproj", "{DABFD304-D6A4-4752-8123-C2CCF7AC7831}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Input.UnitTests", "tests\Avalonia.Input.UnitTests\Avalonia.Input.UnitTests.csproj", "{AC18926A-E784-40FE-B09D-BB0FE2B599F0}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1.UnitTests", "tests\Avalonia.Direct2D1.UnitTests\Avalonia.Direct2D1.UnitTests.csproj", "{EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.UnitTests", "tests\Avalonia.Markup.Xaml.UnitTests\Avalonia.Markup.Xaml.UnitTests.csproj", "{99135EAB-653D-47E4-A378-C96E1278CA44}" @@ -61,6 +39,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs + src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}" @@ -77,8 +56,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Android", "src\Android\Avalonia.Android\Avalonia.Android.csproj", "{7B92AF71-6287-4693-9DCB-BD5B6E927E23}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.AndroidTestApplication", "src\Android\Avalonia.AndroidTestApplication\Avalonia.AndroidTestApplication.csproj", "{FF69B927-C545-49AE-8E16-3D14D621AA12}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "iOS", "iOS", "{0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.iOS", "src\iOS\Avalonia.iOS\Avalonia.iOS.csproj", "{4488AD85-1495-4809-9AA4-DDFE0A48527E}" @@ -95,8 +72,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog", "samples\C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Desktop", "samples\ControlCatalog.Desktop\ControlCatalog.Desktop.csproj", "{2B888490-D14A-4BCA-AB4B-48676FA93C9B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{57E0455D-D565-44BB-B069-EE1AA20F8337}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{52F55355-D120-42AC-8116-8410A7D602FA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" @@ -107,7 +82,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interop", "Interop", "{A0CC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderDemo", "samples\RenderDemo\RenderDemo.csproj", "{F1FDC5B0-4654-416F-AE69-E3E9BBD87801}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia", "src\Skia\Avalonia.Skia\Avalonia.Skia.csproj", "{7D2D3083-71DD-4CC9-8907-39A0D86FB322}" EndProject @@ -115,14 +90,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}" ProjectSection(SolutionItems) = preProject - build\AndroidWorkarounds.props = build\AndroidWorkarounds.props build\ApiDiff.props = build\ApiDiff.props build\Base.props = build\Base.props build\Binding.props = build\Binding.props build\CoreLibraries.props = build\CoreLibraries.props + build\DevAnalyzers.props = build\DevAnalyzers.props build\EmbedXaml.props = build\EmbedXaml.props build\HarfBuzzSharp.props = build\HarfBuzzSharp.props - build\iOSWorkarounds.props = build\iOSWorkarounds.props build\JetBrains.Annotations.props = build\JetBrains.Annotations.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props @@ -138,6 +112,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\SharedVersion.props = build\SharedVersion.props build\SharpDX.props = build\SharpDX.props build\SkiaSharp.props = build\SkiaSharp.props + build\SourceGenerators.props = build\SourceGenerators.props build\SourceLink.props = build\SourceLink.props build\System.Drawing.Common.props = build\System.Drawing.Common.props build\System.Memory.props = build\System.Memory.props @@ -178,8 +153,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.OpenGL", "src\Aval EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Native", "src\Avalonia.Native\Avalonia.Native.csproj", "{12A91A62-C064-42CA-9A8C-A1272F354388}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesktopRuntime", "src\Avalonia.DesktopRuntime\Avalonia.DesktopRuntime.csproj", "{878FEFE0-CD14-41CB-90B0-DBCB163E8F15}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Packages", "Packages", "{E870DCD7-F46A-498D-83FC-D0FD13E0A11C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia", "packages\Avalonia\Avalonia.csproj", "{D49233F8-F29C-47DD-9975-C4C9E4502720}" @@ -190,8 +163,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Build.Tasks", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "nukebuild\_build.csproj", "{3F00BC43-5095-477F-93D8-E65B08179A00}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Animation.UnitTests", "tests\Avalonia.Animation.UnitTests\Avalonia.Animation.UnitTests.csproj", "{AF227847-E65C-4BE9-BCE9-B551357788E0}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}" @@ -222,6 +193,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MicroCom", "src\Av EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestApp", "samples\IntegrationTestApp\IntegrationTestApp.csproj", "{676D6BFD-029D-4E43-BFC7-3892265CE251}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.IntegrationTests.Appium", "tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web.Blazor", "src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj", "{25831348-EB2A-483E-9576-E8F6528674A5}" @@ -234,6 +209,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -274,54 +257,6 @@ Global {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|iPhone.Build.0 = Release|Any CPU {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|Any CPU.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|iPhone.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|iPhone.Build.0 = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|Any CPU.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|iPhone.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|iPhone.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|Any CPU.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|iPhone.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|Any CPU.Build.0 = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|iPhone.Build.0 = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|Any CPU.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|Any CPU.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|iPhone.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|iPhone.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -370,54 +305,6 @@ Global {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhone.Build.0 = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|Any CPU.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|iPhone.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|iPhone.Build.0 = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|Any CPU.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|iPhone.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|iPhone.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|Any CPU.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|iPhone.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|iPhone.Build.0 = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|Any CPU.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|iPhone.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|iPhone.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -442,30 +329,6 @@ Global {D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|iPhone.Build.0 = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|Any CPU.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|iPhone.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|iPhone.Build.0 = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|Any CPU.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|iPhone.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|iPhone.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -514,54 +377,6 @@ Global {7062AE20-5DCC-4442-9645-8195BDECE63E}.Release|iPhone.Build.0 = Release|Any CPU {7062AE20-5DCC-4442-9645-8195BDECE63E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {7062AE20-5DCC-4442-9645-8195BDECE63E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|Any CPU.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|iPhone.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|iPhone.Build.0 = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|Any CPU.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|iPhone.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|iPhone.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|Any CPU.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|iPhone.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|Any CPU.Build.0 = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|iPhone.Build.0 = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|Any CPU.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|Any CPU.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|iPhone.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|iPhone.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -586,30 +401,6 @@ Global {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|iPhone.Build.0 = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|Any CPU.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|iPhone.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|iPhone.Build.0 = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|Any CPU.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|iPhone.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|iPhone.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -634,54 +425,6 @@ Global {2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|iPhone.Build.0 = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|Any CPU.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|iPhone.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|iPhone.Build.0 = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|Any CPU.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|iPhone.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|iPhone.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|Any CPU.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|iPhone.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|iPhone.Build.0 = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|Any CPU.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|iPhone.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|iPhone.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -706,30 +449,6 @@ Global {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|iPhone.Build.0 = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|Any CPU.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|iPhone.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|iPhone.Build.0 = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|Any CPU.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|iPhone.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|iPhone.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -912,22 +631,6 @@ Global {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|Any CPU.Build.0 = Release|Any CPU {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|iPhone.ActiveCfg = Release|Any CPU {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.Build.0 = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.Deploy.0 = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|iPhone.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -1096,26 +799,6 @@ Global {2B888490-D14A-4BCA-AB4B-48676FA93C9B}.Release|iPhone.Build.0 = Release|Any CPU {2B888490-D14A-4BCA-AB4B-48676FA93C9B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {2B888490-D14A-4BCA-AB4B-48676FA93C9B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator - {57E0455D-D565-44BB-B069-EE1AA20F8337}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone - {57E0455D-D565-44BB-B069-EE1AA20F8337}.AppStore|iPhone.ActiveCfg = AppStore|iPhone - {57E0455D-D565-44BB-B069-EE1AA20F8337}.AppStore|iPhone.Build.0 = AppStore|iPhone - {57E0455D-D565-44BB-B069-EE1AA20F8337}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator - {57E0455D-D565-44BB-B069-EE1AA20F8337}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Debug|Any CPU.ActiveCfg = Debug|iPhone - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Debug|iPhone.ActiveCfg = Debug|iPhone - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Debug|iPhone.Build.0 = Debug|iPhone - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Release|Any CPU.ActiveCfg = Release|iPhone - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Release|iPhone.ActiveCfg = Release|iPhone - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Release|iPhone.Build.0 = Release|iPhone - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {57E0455D-D565-44BB-B069-EE1AA20F8337}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator {52F55355-D120-42AC-8116-8410A7D602FA}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {52F55355-D120-42AC-8116-8410A7D602FA}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {52F55355-D120-42AC-8116-8410A7D602FA}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -1560,30 +1243,6 @@ Global {12A91A62-C064-42CA-9A8C-A1272F354388}.Release|iPhone.Build.0 = Release|Any CPU {12A91A62-C064-42CA-9A8C-A1272F354388}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {12A91A62-C064-42CA-9A8C-A1272F354388}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.AppStore|iPhone.Build.0 = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Debug|Any CPU.Build.0 = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Debug|iPhone.Build.0 = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Release|Any CPU.ActiveCfg = Release|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Release|Any CPU.Build.0 = Release|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Release|iPhone.ActiveCfg = Release|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Release|iPhone.Build.0 = Release|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {878FEFE0-CD14-41CB-90B0-DBCB163E8F15}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {D49233F8-F29C-47DD-9975-C4C9E4502720}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {D49233F8-F29C-47DD-9975-C4C9E4502720}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {D49233F8-F29C-47DD-9975-C4C9E4502720}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU @@ -1680,30 +1339,6 @@ Global {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|iPhone.Build.0 = Release|Any CPU {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhone.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhone.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|Any CPU.Build.0 = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhone.ActiveCfg = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhone.Build.0 = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU @@ -2064,6 +1699,54 @@ Global {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhone.Build.0 = Release|Any CPU {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|iPhone.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|Any CPU.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|iPhone.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.ActiveCfg = Release|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.Build.0 = Release|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|iPhone.ActiveCfg = Release|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|iPhone.Build.0 = Release|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|iPhone.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|iPhone.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.Build.0 = Release|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|iPhone.ActiveCfg = Release|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|iPhone.Build.0 = Release|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU @@ -2184,6 +1867,102 @@ Global {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhone.Build.0 = Release|Any CPU {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.AppStore|iPhone.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|iPhone.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.Build.0 = Release|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhone.ActiveCfg = Release|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhone.Build.0 = Release|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhone.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhone.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|Any CPU.Build.0 = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.ActiveCfg = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.Build.0 = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhone.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhone.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.Build.0 = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhone.ActiveCfg = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhone.Build.0 = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhone.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhone.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.Build.0 = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.ActiveCfg = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.Build.0 = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2191,14 +1970,9 @@ Global GlobalSection(NestedProjects) = preSolution {811A76CF-1CF6-440F-963B-BBE31BD72A82} = {B39A8919-9F95-48FE-AD7B-76E08B509888} {3E908F67-5543-4879-A1DC-08EACE79B3CD} = {B39A8919-9F95-48FE-AD7B-76E08B509888} - {47ECDF59-DEF8-4C53-87B1-2098A3429059} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {5CCB5571-7C30-4E7D-967D-0E2158EBD91F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {76716382-3159-460E-BDA6-C5715CF606D7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {2905FF23-53FB-45E6-AA49-6AF47A172056} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {DB070A10-BF39-4752-8456-86E9D5928478} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {08478EF5-44E8-42E9-92D6-15E00EC038D8} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {DABFD304-D6A4-4752-8123-C2CCF7AC7831} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {AC18926A-E784-40FE-B09D-BB0FE2B599F0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {99135EAB-653D-47E4-A378-C96E1278CA44} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {3E53A01A-B331-47F3-B828-4A5717E77A24} = {8B6A8209-894F-4BA1-B880-965FD453982C} @@ -2206,14 +1980,12 @@ Global {8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098} {7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} - {FF69B927-C545-49AE-8E16-3D14D621AA12} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} {4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1} {E1AA3DBF-9056-4530-9376-18119A7A3FFE} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {88060192-33D5-4932-B0F9-8BD2763E857D} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {D0A739B9-3C68-4BA6-A328-41606954B6BD} = {9B9E3891-2366-4253-A952-D08BCEB71098} {2B888490-D14A-4BCA-AB4B-48676FA93C9B} = {9B9E3891-2366-4253-A952-D08BCEB71098} - {57E0455D-D565-44BB-B069-EE1AA20F8337} = {9B9E3891-2366-4253-A952-D08BCEB71098} {52F55355-D120-42AC-8116-8410A7D602FA} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {F1381F98-4D24-409A-A6C5-1C5B1E08BB08} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {FBCAF3D0-2808-4934-8E96-3F607594517B} = {9B9E3891-2366-4253-A952-D08BCEB71098} @@ -2222,6 +1994,7 @@ Global {29132311-1848-4FD6-AE0C-4FF841151BD3} = {9B9E3891-2366-4253-A952-D08BCEB71098} {7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E} {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} = {A689DEF5-D50F-4975-8B72-124C9EB54066} {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {638580B0-7910-40EF-B674-DCB34DA308CD} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} = {B39A8919-9F95-48FE-AD7B-76E08B509888} @@ -2232,7 +2005,6 @@ Global {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {D49233F8-F29C-47DD-9975-C4C9E4502720} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C} {3C471044-3640-45E3-B1B2-16D2FF8399EE} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C} - {AF227847-E65C-4BE9-BCE9-B551357788E0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} @@ -2241,10 +2013,15 @@ Global {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} {BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {676D6BFD-029D-4E43-BFC7-3892265CE251} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {25831348-EB2A-483E-9576-E8F6528674A5} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} {C08E9894-AA92-426E-BF56-033E262CAD3E} = {9B9E3891-2366-4253-A952-D08BCEB71098} {26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {CE910927-CE5A-456F-BC92-E4C757354A5C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/Directory.Build.props b/Directory.Build.props index c6610695c4..97781b7517 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,5 +4,7 @@ $(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netcoreapp2.0\Avalonia.Designer.HostApp.dll false + false + False diff --git a/NuGet.Config b/NuGet.Config index 7a1f28bea7..7d2bd8abd2 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -5,5 +5,6 @@ + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 40669f4f53..edf3c3d819 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,6 +1,3 @@ -variables: - MSBuildEnableWorkloadResolver: 'false' - jobs: - job: GetPRNumber @@ -34,14 +31,14 @@ jobs: vmImage: 'ubuntu-20.04' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.414' + displayName: 'Use .NET Core SDK 3.1.418' inputs: - version: 3.1.414 + version: 3.1.418 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.100' + displayName: 'Use .NET Core SDK 6.0.202' inputs: - version: 6.0.100 + version: 6.0.202 - task: CmdLine@2 displayName: 'Run Build' @@ -65,14 +62,14 @@ jobs: vmImage: 'macOS-10.15' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.414' + displayName: 'Use .NET Core SDK 3.1.418' inputs: - version: 3.1.414 + version: 3.1.418 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.100' + displayName: 'Use .NET Core SDK 6.0.202' inputs: - version: 6.0.100 + version: 6.0.202 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -137,14 +134,20 @@ jobs: SolutionDir: '$(Build.SourcesDirectory)' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.414' + displayName: 'Use .NET Core SDK 3.1.418' inputs: - version: 3.1.414 + version: 3.1.418 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.100' + displayName: 'Use .NET Core SDK 6.0.202' inputs: - version: 6.0.100 + version: 6.0.202 + + - task: CmdLine@2 + displayName: 'Install Workloads' + inputs: + script: | + dotnet workload install android ios - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/build/AndroidWorkarounds.props b/build/AndroidWorkarounds.props deleted file mode 100644 index de86acc6de..0000000000 --- a/build/AndroidWorkarounds.props +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - false - - diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index 3fccad2641..314d38190a 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -1,14 +1,8 @@ - - - - - - @@ -16,7 +10,6 @@ - diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props new file mode 100644 index 0000000000..28959dbd47 --- /dev/null +++ b/build/DevAnalyzers.props @@ -0,0 +1,9 @@ + + + + + diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 1d84d5289a..e10de93530 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 7f24ef35bc..3d9548ab9d 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -11,7 +11,7 @@ latest MIT Icon.png - Avalonia is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), MacOS and with experimental support for Android and iOS. + Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly. avalonia;avaloniaui;mvvm;rx;reactive extensions;android;ios;mac;forms;wpf;net;netstandard;net461;uwp;xamarin https://github.com/AvaloniaUI/Avalonia/releases git diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index bb370256f9..a217a8272d 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props new file mode 100644 index 0000000000..d000af1bf6 --- /dev/null +++ b/build/SourceGenerators.props @@ -0,0 +1,10 @@ + + + + + + diff --git a/build/System.Drawing.Common.props b/build/System.Drawing.Common.props index 2b3707d38a..108a0f41e0 100644 --- a/build/System.Drawing.Common.props +++ b/build/System.Drawing.Common.props @@ -1,5 +1,6 @@  - + + diff --git a/build/XUnit.props b/build/XUnit.props index a75e1bac86..17ead91aa3 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -1,14 +1,14 @@  - - - - - - - - - + + + + + + + + + diff --git a/dirs.proj b/dirs.proj index 594f2c22d3..396e0c915c 100644 --- a/dirs.proj +++ b/dirs.proj @@ -1,5 +1,7 @@ + + @@ -8,21 +10,21 @@ - - - - - - - - + + + + + + + + diff --git a/global.json b/global.json index b160e4561d..a6792b05c7 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,7 @@ { "sdk": { - "version": "6.0.100" + "version": "6.0.202", + "rollForward": "latestFeature" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 85fcf20034..7571d51c9f 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -30,6 +30,8 @@ AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; }; AB661C202148286E00291242 /* window.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB661C1F2148286E00291242 /* window.mm */; }; AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; }; + BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; }; + BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -64,6 +66,8 @@ AB661C212148288600291242 /* common.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = common.h; sourceTree = ""; }; AB7A61EF2147C815003C5833 /* libAvalonia.Native.OSX.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libAvalonia.Native.OSX.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platformthreading.mm; sourceTree = ""; }; + BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = ""; }; + BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -97,6 +101,8 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + BC11A5BC2608D58F0017BAD0 /* automation.h */, + BC11A5BD2608D58F0017BAD0 /* automation.mm */, 1A1852DB23E05814008F0DED /* deadlock.mm */, 1A002B9D232135EE00021753 /* app.mm */, 37DDA9B121933371002E132B /* AvnString.h */, @@ -143,6 +149,7 @@ buildActionMask = 2147483647; files = ( 37155CE4233C00EB0034DCE9 /* menu.h in Headers */, + BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -213,6 +220,7 @@ AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */, 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */, + BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h index 3ce83d370a..3b750b11db 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.h +++ b/native/Avalonia.Native/src/OSX/AvnString.h @@ -14,4 +14,5 @@ extern IAvnStringArray* CreateAvnStringArray(NSArray* array); extern IAvnStringArray* CreateAvnStringArray(NSArray* array); extern IAvnStringArray* CreateAvnStringArray(NSString* string); extern IAvnString* CreateByteArray(void* data, int len); +extern NSString* GetNSStringAndRelease(IAvnString* s); #endif /* AvnString_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index cd0e2cdf94..5e50068c51 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -153,3 +153,19 @@ IAvnString* CreateByteArray(void* data, int len) { return new AvnStringImpl(data, len); } + +NSString* GetNSStringAndRelease(IAvnString* s) +{ + NSString* result = nil; + + if (s != nullptr) + { + char* p; + if (s->Pointer((void**)&p) == S_OK && p != nullptr) + result = [NSString stringWithUTF8String:p]; + + s->Release(); + } + + return result; +} diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 79175d9ff1..05b129baca 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -73,6 +73,11 @@ ComPtr _events; _isHandlingSendEvent = true; @try { [super sendEvent: event]; + if ([event type] == NSEventTypeKeyUp && ([event modifierFlags] & NSEventModifierFlagCommand)) + { + [[self keyWindow] sendEvent:event]; + } + } @finally { _isHandlingSendEvent = oldHandling; } diff --git a/native/Avalonia.Native/src/OSX/automation.h b/native/Avalonia.Native/src/OSX/automation.h new file mode 100644 index 0000000000..4a12a965fd --- /dev/null +++ b/native/Avalonia.Native/src/OSX/automation.h @@ -0,0 +1,12 @@ +#import +#include "window.h" + +NS_ASSUME_NONNULL_BEGIN + +class IAvnAutomationPeer; + +@interface AvnAccessibilityElement : NSAccessibilityElement ++ (AvnAccessibilityElement *) acquire:(IAvnAutomationPeer *) peer; +@end + +NS_ASSUME_NONNULL_END diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm new file mode 100644 index 0000000000..7d697140c2 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -0,0 +1,496 @@ +#include "common.h" +#include "automation.h" +#include "AvnString.h" +#include "window.h" + +@interface AvnAccessibilityElement (Events) +- (void) raiseChildrenChanged; +@end + +@interface AvnRootAccessibilityElement : AvnAccessibilityElement +- (AvnView *) ownerView; +- (AvnRootAccessibilityElement *) initWithPeer:(IAvnAutomationPeer *) peer owner:(AvnView*) owner; +- (void) raiseFocusChanged; +@end + +class AutomationNode : public ComSingleObject +{ +public: + FORWARD_IUNKNOWN() + + AutomationNode(AvnAccessibilityElement* owner) + { + _owner = owner; + } + + AvnAccessibilityElement* GetOwner() + { + return _owner; + } + + virtual void Dispose() override + { + _owner = nil; + } + + virtual void ChildrenChanged () override + { + [_owner raiseChildrenChanged]; + } + + virtual void PropertyChanged (AvnAutomationProperty property) override + { + + } + + virtual void FocusChanged () override + { + [(AvnRootAccessibilityElement*)_owner raiseFocusChanged]; + } + +private: + __strong AvnAccessibilityElement* _owner; +}; + +@implementation AvnAccessibilityElement +{ + IAvnAutomationPeer* _peer; + AutomationNode* _node; + NSMutableArray* _children; +} + ++ (AvnAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer +{ + if (peer == nullptr) + return nil; + + auto instance = peer->GetNode(); + + if (instance != nullptr) + return dynamic_cast(instance)->GetOwner(); + + if (peer->IsRootProvider()) + { + auto window = peer->RootProvider_GetWindow(); + auto holder = dynamic_cast(window); + auto view = holder->GetNSView(); + return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view]; + } + else + { + return [[AvnAccessibilityElement alloc] initWithPeer:peer]; + } +} + +- (AvnAccessibilityElement *)initWithPeer:(IAvnAutomationPeer *)peer +{ + self = [super init]; + _peer = peer; + _node = new AutomationNode(self); + _peer->SetNode(_node); + return self; +} + +- (void)dealloc +{ + if (_node) + delete _node; + _node = nullptr; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"%@ '%@' (%p)", + GetNSStringAndRelease(_peer->GetClassName()), + GetNSStringAndRelease(_peer->GetName()), + _peer]; +} + +- (IAvnAutomationPeer *)peer +{ + return _peer; +} + +- (BOOL)isAccessibilityElement +{ + return _peer->IsControlElement(); +} + +- (NSAccessibilityRole)accessibilityRole +{ + auto controlType = _peer->GetAutomationControlType(); + + switch (controlType) { + case AutomationButton: return NSAccessibilityButtonRole; + case AutomationCalendar: return NSAccessibilityGridRole; + case AutomationCheckBox: return NSAccessibilityCheckBoxRole; + case AutomationComboBox: return NSAccessibilityPopUpButtonRole; + case AutomationComboBoxItem: return NSAccessibilityMenuItemRole; + case AutomationEdit: return NSAccessibilityTextFieldRole; + case AutomationHyperlink: return NSAccessibilityLinkRole; + case AutomationImage: return NSAccessibilityImageRole; + case AutomationListItem: return NSAccessibilityRowRole; + case AutomationList: return NSAccessibilityTableRole; + case AutomationMenu: return NSAccessibilityMenuBarRole; + case AutomationMenuBar: return NSAccessibilityMenuBarRole; + case AutomationMenuItem: return NSAccessibilityMenuItemRole; + case AutomationProgressBar: return NSAccessibilityProgressIndicatorRole; + case AutomationRadioButton: return NSAccessibilityRadioButtonRole; + case AutomationScrollBar: return NSAccessibilityScrollBarRole; + case AutomationSlider: return NSAccessibilitySliderRole; + case AutomationSpinner: return NSAccessibilityIncrementorRole; + case AutomationStatusBar: return NSAccessibilityTableRole; + case AutomationTab: return NSAccessibilityTabGroupRole; + case AutomationTabItem: return NSAccessibilityRadioButtonRole; + case AutomationText: return NSAccessibilityStaticTextRole; + case AutomationToolBar: return NSAccessibilityToolbarRole; + case AutomationToolTip: return NSAccessibilityPopoverRole; + case AutomationTree: return NSAccessibilityOutlineRole; + case AutomationTreeItem: return NSAccessibilityCellRole; + case AutomationCustom: return NSAccessibilityUnknownRole; + case AutomationGroup: return NSAccessibilityGroupRole; + case AutomationThumb: return NSAccessibilityHandleRole; + case AutomationDataGrid: return NSAccessibilityGridRole; + case AutomationDataItem: return NSAccessibilityCellRole; + case AutomationDocument: return NSAccessibilityStaticTextRole; + case AutomationSplitButton: return NSAccessibilityPopUpButtonRole; + case AutomationWindow: return NSAccessibilityWindowRole; + case AutomationPane: return NSAccessibilityGroupRole; + case AutomationHeader: return NSAccessibilityGroupRole; + case AutomationHeaderItem: return NSAccessibilityButtonRole; + case AutomationTable: return NSAccessibilityTableRole; + case AutomationTitleBar: return NSAccessibilityGroupRole; + // Treat unknown roles as generic group container items. Returning + // NSAccessibilityUnknownRole is also possible but makes the screen + // reader focus on the item instead of passing focus to child items. + default: return NSAccessibilityGroupRole; + } +} + +- (NSString *)accessibilityIdentifier +{ + return GetNSStringAndRelease(_peer->GetAutomationId()); +} + +- (NSString *)accessibilityTitle +{ + // StaticText exposes its text via the value property. + if (_peer->GetAutomationControlType() != AutomationText) + { + return GetNSStringAndRelease(_peer->GetName()); + } + + return [super accessibilityTitle]; +} + +- (id)accessibilityValue +{ + if (_peer->IsRangeValueProvider()) + { + return [NSNumber numberWithDouble:_peer->RangeValueProvider_GetValue()]; + } + else if (_peer->IsToggleProvider()) + { + switch (_peer->ToggleProvider_GetToggleState()) { + case 0: return [NSNumber numberWithBool:NO]; + case 1: return [NSNumber numberWithBool:YES]; + default: return [NSNumber numberWithInt:2]; + } + } + else if (_peer->IsValueProvider()) + { + return GetNSStringAndRelease(_peer->ValueProvider_GetValue()); + } + else if (_peer->GetAutomationControlType() == AutomationText) + { + return GetNSStringAndRelease(_peer->GetName()); + } + + return [super accessibilityValue]; +} + +- (id)accessibilityMinValue +{ + if (_peer->IsRangeValueProvider()) + { + return [NSNumber numberWithDouble:_peer->RangeValueProvider_GetMinimum()]; + } + + return [super accessibilityMinValue]; +} + +- (id)accessibilityMaxValue +{ + if (_peer->IsRangeValueProvider()) + { + return [NSNumber numberWithDouble:_peer->RangeValueProvider_GetMaximum()]; + } + + return [super accessibilityMaxValue]; +} + +- (BOOL)isAccessibilityEnabled +{ + return _peer->IsEnabled(); +} + +- (BOOL)isAccessibilityFocused +{ + return _peer->HasKeyboardFocus(); +} + +- (NSArray *)accessibilityChildren +{ + if (_children == nullptr && _peer != nullptr) + [self recalculateChildren]; + return _children; +} + +- (NSRect)accessibilityFrame +{ + id topLevel = [self accessibilityTopLevelUIElement]; + auto result = NSZeroRect; + + if ([topLevel isKindOfClass:[AvnRootAccessibilityElement class]]) + { + auto root = (AvnRootAccessibilityElement*)topLevel; + auto view = [root ownerView]; + + if (view) + { + auto window = [view window]; + auto bounds = ToNSRect(_peer->GetBoundingRectangle()); + auto windowBounds = [view convertRect:bounds toView:nil]; + auto screenBounds = [window convertRectToScreen:windowBounds]; + result = screenBounds; + } + } + + return result; +} + +- (id)accessibilityParent +{ + auto parentPeer = _peer->GetParent(); + return parentPeer ? [AvnAccessibilityElement acquire:parentPeer] : [NSApplication sharedApplication]; +} + +- (id)accessibilityTopLevelUIElement +{ + auto rootPeer = _peer->GetRootPeer(); + return [AvnAccessibilityElement acquire:rootPeer]; +} + +- (id)accessibilityWindow +{ + id topLevel = [self accessibilityTopLevelUIElement]; + return [topLevel isKindOfClass:[NSWindow class]] ? topLevel : nil; +} + +- (BOOL)isAccessibilityExpanded +{ + if (!_peer->IsExpandCollapseProvider()) + return NO; + return _peer->ExpandCollapseProvider_GetIsExpanded(); +} + +- (void)setAccessibilityExpanded:(BOOL)accessibilityExpanded +{ + if (!_peer->IsExpandCollapseProvider()) + return; + if (accessibilityExpanded) + _peer->ExpandCollapseProvider_Expand(); + else + _peer->ExpandCollapseProvider_Collapse(); +} + +- (BOOL)accessibilityPerformPress +{ + if (_peer->IsInvokeProvider()) + { + _peer->InvokeProvider_Invoke(); + } + else if (_peer->IsExpandCollapseProvider()) + { + _peer->ExpandCollapseProvider_Expand(); + } + else if (_peer->IsToggleProvider()) + { + _peer->ToggleProvider_Toggle(); + } + return YES; +} + +- (BOOL)accessibilityPerformIncrement +{ + if (!_peer->IsRangeValueProvider()) + return NO; + auto value = _peer->RangeValueProvider_GetValue(); + value += _peer->RangeValueProvider_GetSmallChange(); + _peer->RangeValueProvider_SetValue(value); + return YES; +} + +- (BOOL)accessibilityPerformDecrement +{ + if (!_peer->IsRangeValueProvider()) + return NO; + auto value = _peer->RangeValueProvider_GetValue(); + value -= _peer->RangeValueProvider_GetSmallChange(); + _peer->RangeValueProvider_SetValue(value); + return YES; +} + +- (BOOL)accessibilityPerformShowMenu +{ + if (!_peer->IsExpandCollapseProvider()) + return NO; + _peer->ExpandCollapseProvider_Expand(); + return YES; +} + +- (BOOL)isAccessibilitySelected +{ + if (_peer->IsSelectionItemProvider()) + return _peer->SelectionItemProvider_IsSelected(); + return NO; +} + +- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector +{ + if (selector == @selector(accessibilityPerformShowMenu)) + { + return _peer->IsExpandCollapseProvider() && _peer->ExpandCollapseProvider_GetShowsMenu(); + } + else if (selector == @selector(isAccessibilityExpanded)) + { + return _peer->IsExpandCollapseProvider(); + } + else if (selector == @selector(accessibilityPerformPress)) + { + return _peer->IsInvokeProvider() || _peer->IsExpandCollapseProvider() || _peer->IsToggleProvider(); + } + else if (selector == @selector(accessibilityPerformIncrement) || + selector == @selector(accessibilityPerformDecrement) || + selector == @selector(accessibilityMinValue) || + selector == @selector(accessibilityMaxValue)) + { + return _peer->IsRangeValueProvider(); + } + + return [super isAccessibilitySelectorAllowed:selector]; +} + +- (void)raiseChildrenChanged +{ + auto changed = _children ? [NSMutableSet setWithArray:_children] : [NSMutableSet set]; + + [self recalculateChildren]; + + if (_children) + [changed addObjectsFromArray:_children]; + + NSAccessibilityPostNotificationWithUserInfo( + self, + NSAccessibilityLayoutChangedNotification, + @{ NSAccessibilityUIElementsKey: [changed allObjects]}); +} + +- (void)raisePropertyChanged +{ +} + +- (void)setAccessibilityFocused:(BOOL)accessibilityFocused +{ + if (accessibilityFocused) + _peer->SetFocus(); +} + +- (void)recalculateChildren +{ + auto childPeers = _peer->GetChildren(); + auto childCount = childPeers != nullptr ? childPeers->GetCount() : 0; + + if (childCount > 0) + { + _children = [[NSMutableArray alloc] initWithCapacity:childCount]; + + for (int i = 0; i < childCount; ++i) + { + IAvnAutomationPeer* child; + + if (childPeers->Get(i, &child) == S_OK) + { + auto element = [AvnAccessibilityElement acquire:child]; + [_children addObject:element]; + } + } + } + else + { + _children = nil; + } +} + +@end + +@implementation AvnRootAccessibilityElement +{ + AvnView* _owner; +} + +- (AvnRootAccessibilityElement *)initWithPeer:(IAvnAutomationPeer *)peer owner:(AvnView *)owner +{ + self = [super initWithPeer:peer]; + _owner = owner; + + // Seems we need to raise a focus changed notification here if we have focus + auto focusedPeer = [self peer]->RootProvider_GetFocus(); + id focused = [AvnAccessibilityElement acquire:focusedPeer]; + + if (focused) + NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification); + + return self; +} + +- (AvnView *)ownerView +{ + return _owner; +} + +- (id)accessibilityFocusedUIElement +{ + auto focusedPeer = [self peer]->RootProvider_GetFocus(); + return [AvnAccessibilityElement acquire:focusedPeer]; +} + +- (id)accessibilityHitTest:(NSPoint)point +{ + auto clientPoint = [[_owner window] convertPointFromScreen:point]; + auto localPoint = [_owner translateLocalPoint:ToAvnPoint(clientPoint)]; + auto hit = [self peer]->RootProvider_GetPeerFromPoint(localPoint); + return [AvnAccessibilityElement acquire:hit]; +} + +- (id)accessibilityParent +{ + return _owner; +} + +- (void)raiseFocusChanged +{ + id focused = [self accessibilityFocusedUIElement]; + NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification); +} + +// Although this method is marked as deprecated we get runtime warnings if we don't handle it. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)accessibilityPerformAction:(NSAccessibilityActionName)action +{ + [_owner accessibilityPerformAction:action]; +} +#pragma clang diagnostic pop + +@end diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 126c9aa87b..9186d9e15a 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -35,6 +35,7 @@ extern NSMenuItem* GetAppMenuItem (); extern void InitializeAvnApp(IAvnApplicationEvents* events); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); +extern NSRect ToNSRect (AvnRect r); extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ConvertPointY (AvnPoint p); extern CGFloat PrimaryDisplayHeight(); diff --git a/native/Avalonia.Native/src/OSX/controlhost.mm b/native/Avalonia.Native/src/OSX/controlhost.mm index f8e9a3b6d1..5683a5a975 100644 --- a/native/Avalonia.Native/src/OSX/controlhost.mm +++ b/native/Avalonia.Native/src/OSX/controlhost.mm @@ -36,7 +36,10 @@ public: virtual void DestroyDefaultChild(void* child) override { // ARC will release the object for us + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wunused-value" (__bridge_transfer NSView*) child; + #pragma clang diagnostic pop } }; diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 69f2995847..ea79c494d7 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -1,6 +1,7 @@ //This file will contain actual IID structures #define COM_GUIDS_MATERIALIZE #include "common.h" +#include "window.h" static NSString* s_appTitle = @"Avalonia"; @@ -335,7 +336,7 @@ public: return S_OK; } } - + virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override { START_COM_CALL; @@ -400,6 +401,15 @@ NSPoint ToNSPoint (AvnPoint p) return result; } +NSRect ToNSRect (AvnRect r) +{ + return NSRect + { + NSPoint { r.X, r.Y }, + NSSize { r.Width, r.Height } + }; +} + AvnPoint ToAvnPoint (NSPoint p) { AvnPoint result; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 1dc091a48d..1369ceaea0 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -43,6 +43,7 @@ class WindowBaseImpl; struct INSWindowHolder { virtual AvnWindow* _Nonnull GetNSWindow () = 0; + virtual AvnView* _Nonnull GetNSView () = 0; }; struct IWindowStateChanged diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 40180274e1..d16c466fe6 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -5,14 +5,22 @@ #include "menu.h" #include #include "rendertarget.h" +#include "AvnString.h" +#include "automation.h" -class WindowBaseImpl : public virtual ComSingleObject, public INSWindowHolder +class WindowBaseImpl : public virtual ComObject, + public virtual IAvnWindowBase, + public INSWindowHolder { private: NSCursor* cursor; public: FORWARD_IUNKNOWN() + BEGIN_INTERFACE_MAP() + INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase) + END_INTERFACE_MAP() + virtual ~WindowBaseImpl() { View = NULL; @@ -115,7 +123,12 @@ public: { return Window; } - + + virtual AvnView* GetNSView() override + { + return View; + } + virtual HRESULT Show(bool activate, bool isDialog) override { START_COM_CALL; @@ -444,7 +457,8 @@ public: } point = ConvertPointY(point); - auto viewPoint = [Window convertScreenToBase:ToNSPoint(point)]; + NSRect convertRect = [Window convertRectToScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; + auto viewPoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y); *ret = [View translateLocalPoint:ToAvnPoint(viewPoint)]; @@ -464,7 +478,8 @@ public: } auto cocoaViewPoint = ToNSPoint([View translateLocalPoint:point]); - auto cocoaScreenPoint = [Window convertBaseToScreen:cocoaViewPoint]; + NSRect convertRect = [Window convertRectToScreen:NSMakeRect(cocoaViewPoint.x, cocoaViewPoint.y, 0.0, 0.0)]; + auto cocoaScreenPoint = NSPointFromCGPoint(NSMakePoint(convertRect.origin.x, convertRect.origin.y)); *ret = ConvertPointY(ToAvnPoint(cocoaScreenPoint)); return S_OK; @@ -560,7 +575,8 @@ public: if(!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited) || (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged))) { - auto nspoint = [Window convertBaseToScreen: ToNSPoint(point)]; + NSRect convertRect = [Window convertRectToScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; + auto nspoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y); CGPoint cgpoint = NSPointToCGPoint(nspoint); auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft); nsevent = [NSEvent eventWithCGEvent: cgevent]; @@ -722,7 +738,7 @@ private: return E_INVALIDARG; // If one tries to show a child window with a minimized parent window, then the parent window will be - // restored but MacOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive + // restored but macOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. if (cparent->WindowState() == Minimized) cparent->SetWindowState(Normal); @@ -1396,6 +1412,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent AvnPixelSize _lastPixelSize; NSObject* _renderTarget; AvnPlatformResizeReason _resizeReason; + AvnAccessibilityElement* _accessibilityChild; } - (void)onClosed @@ -2050,6 +2067,37 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _resizeReason = reason; } +- (AvnAccessibilityElement *) accessibilityChild +{ + if (_accessibilityChild == nil) + { + auto peer = _parent->BaseEvents->GetAutomationPeer(); + + if (peer == nil) + return nil; + + _accessibilityChild = [AvnAccessibilityElement acquire:peer]; + } + + return _accessibilityChild; +} + +- (NSArray *)accessibilityChildren +{ + auto child = [self accessibilityChild]; + return NSAccessibilityUnignoredChildrenForOnlyChild(child); +} + +- (id)accessibilityHitTest:(NSPoint)point +{ + return [[self accessibilityChild] accessibilityHitTest:point]; +} + +- (id)accessibilityFocusedUIElement +{ + return [[self accessibilityChild] accessibilityFocusedUIElement]; +} + @end @@ -2062,6 +2110,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _isExtended; AvnMenu* _menu; double _lastScaling; + IAvnAutomationPeer* _automationPeer; + NSMutableArray* _automationChildren; } -(void) setIsExtended:(bool)value; @@ -2411,6 +2461,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent if(_parent != nullptr) { + auto cparent = dynamic_cast(_parent.getRaw()); + + if(cparent != nullptr) + { + if(cparent->WindowState() == Maximized) + { + cparent->SetWindowState(Normal); + } + } + _parent->GetPosition(&position); _parent->BaseEvents->PositionChanged(position); } @@ -2465,6 +2525,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } } + @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index f0b894b596..f0f677b844 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -87,7 +87,8 @@ partial class Build : NukeBuild Console.WriteLine(preamble); Process.Start(new ProcessStartInfo(command, args) {UseShellExecute = false}).WaitForExit(); } - ExecWait("dotnet version:", "dotnet", "--version"); + ExecWait("dotnet version:", "dotnet", "--info"); + ExecWait("dotnet workloads:", "dotnet", "workload list"); } IReadOnlyCollection MsBuildCommon( @@ -99,7 +100,7 @@ partial class Build : NukeBuild // This is required for VS2019 image on Azure Pipelines .When(Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure, _ => _ - .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_8_X64"))) + .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_11_X64"))) .AddProperty("PackageVersion", Parameters.Version) .AddProperty("iOSRoslynPathHackRequired", true) .SetProcessToolPath(MsBuildExe.Value) @@ -213,19 +214,14 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - RunCoreTest("Avalonia.Animation.UnitTests"); RunCoreTest("Avalonia.Base.UnitTests"); RunCoreTest("Avalonia.Controls.UnitTests"); RunCoreTest("Avalonia.Controls.DataGrid.UnitTests"); - RunCoreTest("Avalonia.Input.UnitTests"); - RunCoreTest("Avalonia.Interactivity.UnitTests"); - RunCoreTest("Avalonia.Layout.UnitTests"); RunCoreTest("Avalonia.Markup.UnitTests"); RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); - RunCoreTest("Avalonia.Styling.UnitTests"); - RunCoreTest("Avalonia.Visuals.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.PlatformSupport.UnitTests"); }); Target RunRenderTests => _ => _ diff --git a/nukebuild/numerge.config b/nukebuild/numerge.config index e4e15d693d..d1c0408241 100644 --- a/nukebuild/numerge.config +++ b/nukebuild/numerge.config @@ -11,11 +11,6 @@ "Id": "Avalonia.Build.Tasks", "IgnoreMissingFrameworkBinaries": true, "DoNotMergeDependencies": true - }, - { - "Id": "Avalonia.DesktopRuntime", - "IgnoreMissingFrameworkBinaries": true, - "IgnoreMissingFrameworkDependencies": true } ] } diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 4b28527465..4d0ed866a3 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -8,7 +8,9 @@ all - + true + TargetFramework=netstandard2.0 + diff --git a/readme.md b/readme.md index 5fbc5daaab..1cdaf3b8f8 100644 --- a/readme.md +++ b/readme.md @@ -1,11 +1,11 @@ [![Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
-[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg) +[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg) ## 📖 About -Avalonia is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, MacOs. Avalonia is mature and production ready. We also have in beta release support for iOS, Andriod and in early stages support for browser via WASM. +Avalonia is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS. Avalonia is mature and production ready. We also have in beta release support for iOS, Android and in early stages support for browser via WASM. ![image](https://user-images.githubusercontent.com/4672627/152126443-932966cf-57e7-4e77-9be6-62463a66b9f8.png) @@ -26,18 +26,15 @@ Install-Package Avalonia.Desktop ## Showcase Examples of UIs built with Avalonia -![image](https://user-images.githubusercontent.com/4672627/84707589-5b69a880-af35-11ea-87a6-7ad57a31d314.png) + +([Lunacy](https://icons8.com/lunacy)) -([Synfonia](https://github.com/jmacato/Synfonia)) +![image](https://user-images.githubusercontent.com/4672627/152325740-261c27a3-e6f0-4662-bff7-4796d4940e04.png) +([PlasticSCM](https://www.plasticscm.com/)) -![image](https://user-images.githubusercontent.com/4672627/85069644-d8419000-b18a-11ea-8732-be9055bb61fd.PNG) -([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery)) +![image](https://user-images.githubusercontent.com/4672627/152326453-14944c4d-33da-4d50-a268-b87f80927adb.png) +([WasabiWallet](https://www.wasabiwallet.io/)) -![image](https://user-images.githubusercontent.com/4672627/85069659-dc6dad80-b18a-11ea-8375-39ef95315b5c.PNG) -([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery)) - -![image](https://user-images.githubusercontent.com/4672627/84708947-c3b98980-af37-11ea-8c9d-503334615bbf.png) -([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery)) ## JetBrains Rider diff --git a/samples/ControlCatalog.Android/Assets/AboutAssets.txt b/samples/ControlCatalog.Android/Assets/AboutAssets.txt deleted file mode 100644 index a9b0638eb1..0000000000 --- a/samples/ControlCatalog.Android/Assets/AboutAssets.txt +++ /dev/null @@ -1,19 +0,0 @@ -Any raw assets you want to be deployed with your application can be placed in -this directory (and child directories) and given a Build Action of "AndroidAsset". - -These files will be deployed with your package and will be accessible using Android's -AssetManager, like this: - -public class ReadAsset : Activity -{ - protected override void OnCreate (Bundle bundle) - { - base.OnCreate (bundle); - - InputStream input = Assets.Open ("my_asset.txt"); - } -} - -Additionally, some Android functions will automatically load asset files: - -Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 617b6b6ab0..04c67e84e8 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -1,165 +1,47 @@ - - + - Debug - AnyCPU - 8.0.30703 - 2.0 - {29132311-1848-4FD6-AE0C-4FF841151BD3} - {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - Properties - ControlCatalog.Android - ControlCatalog.Android - 512 - true - Resources\Resource.Designer.cs - Off - False - v11.0 - Properties\AndroidManifest.xml + net6.0-android + 21 + Exe + enable + com.Avalonia.ControlCatalog + 1 + 1.0 + apk + true - - True - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - True - None - True - False - False - armeabi-v7a;x86;x86_64 - Xamarin - False - False - False - False - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - False - Full - True - False - False - armeabi-v7a,x86;x86_64 - Xamarin - False - False - False - False - False - - - - - - - - - - - - - - - - - - - - - - - - + Resources\drawable\Icon.png + + + True + True + True + True + + + + False + False + + + + True + + - + + + - - {7B92AF71-6287-4693-9DCB-BD5B6E927E23} - Avalonia.Android - - - {d211e587-d8bc-45b9-95a4-f297c8fa5200} - Avalonia.Animation - - - {b09b78d8-9b26-48b0-9149-d64a2f120f3f} - Avalonia.Base - - - {d2221c82-4a25-4583-9b43-d791e3f6820c} - Avalonia.Controls - - - {7062ae20-5dcc-4442-9645-8195bdece63e} - Avalonia.Diagnostics - - - {62024b2d-53eb-4638-b26b-85eeaa54866e} - Avalonia.Input - - - {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} - Avalonia.Interactivity - - - {42472427-4774-4c81-8aff-9f27b8e31721} - Avalonia.Layout - - - {c42d2fc1-a531-4ed4-84b9-89aec7c962fc} - Avalonia.Themes.Fluent - - - {eb582467-6abb-43a1-b052-e981ba910e3a} - Avalonia.Visuals - - - {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} - Avalonia.Styling - - - {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f} - Avalonia.Themes.Default - - - {3e53a01a-b331-47f3-b828-4a5717e77a24} - Avalonia.Markup.Xaml - - - {6417e941-21bc-467b-a771-0de389353ce6} - Avalonia.Markup - - - {7d2d3083-71dd-4cc9-8907-39a0d86fb322} - Avalonia.Skia - - - {d0a739b9-3c68-4ba6-a328-41606954b6bd} - ControlCatalog - + + - - - - - + \ No newline at end of file diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 2ab03551b6..44290d9816 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -1,19 +1,16 @@ using Android.App; -using Android.OS; using Android.Content.PM; +using Avalonia; using Avalonia.Android; namespace ControlCatalog.Android { - [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance)] - public class MainActivity : AvaloniaActivity + [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] + public class MainActivity : AvaloniaActivity { - protected override void OnCreate(Bundle savedInstanceState) + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) { - base.OnCreate(savedInstanceState); - - Content = new MainView(); + return base.CustomizeAppBuilder(builder); } } } - diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml index 9effda7e79..aa570ec504 100644 --- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml +++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml @@ -1,5 +1,4 @@  - - - - \ No newline at end of file + + + diff --git a/samples/ControlCatalog.Android/Properties/AssemblyInfo.cs b/samples/ControlCatalog.Android/Properties/AssemblyInfo.cs deleted file mode 100644 index baeec94648..0000000000 --- a/samples/ControlCatalog.Android/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Android.App; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("ControlCatalog.Android")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ControlCatalog.Android")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: ComVisible(false)] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs deleted file mode 100644 index dccc3f7159..0000000000 --- a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs +++ /dev/null @@ -1,101 +0,0 @@ -#pragma warning disable 1591 -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -[assembly: global::Android.Runtime.ResourceDesignerAttribute("ControlCatalog.Android.Resource", IsApplication=true)] - -namespace ControlCatalog.Android -{ - - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "12.1.99.62")] - public partial class Resource - { - - static Resource() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - public static void UpdateIdValues() - { - } - - public partial class Attribute - { - - static Attribute() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Attribute() - { - } - } - - public partial class Color - { - - // aapt resource value: 0x7F010000 - public const int splash_background = 2130771968; - - static Color() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Color() - { - } - } - - public partial class Drawable - { - - // aapt resource value: 0x7F020000 - public const int Icon = 2130837504; - - // aapt resource value: 0x7F020001 - public const int splash_screen = 2130837505; - - static Drawable() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Drawable() - { - } - } - - public partial class Style - { - - // aapt resource value: 0x7F030000 - public const int MyTheme = 2130903040; - - // aapt resource value: 0x7F030001 - public const int MyTheme_NoActionBar = 2130903041; - - // aapt resource value: 0x7F030002 - public const int MyTheme_Splash = 2130903042; - - static Style() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Style() - { - } - } - } -} -#pragma warning restore 1591 diff --git a/samples/ControlCatalog.Android/Resources/values/styles.xml b/samples/ControlCatalog.Android/Resources/values/styles.xml index e017b6facf..2759d2904a 100644 --- a/samples/ControlCatalog.Android/Resources/values/styles.xml +++ b/samples/ControlCatalog.Android/Resources/values/styles.xml @@ -4,7 +4,7 @@ - diff --git a/samples/ControlCatalog.Android/SplashActivity.cs b/samples/ControlCatalog.Android/SplashActivity.cs index 6d7c6bc116..dc292fd37b 100644 --- a/samples/ControlCatalog.Android/SplashActivity.cs +++ b/samples/ControlCatalog.Android/SplashActivity.cs @@ -1,16 +1,13 @@ using Android.App; using Android.Content; using Android.OS; -using Application = Android.App.Application; - -using Avalonia; namespace ControlCatalog.Android { [Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)] public class SplashActivity : Activity { - protected override void OnCreate(Bundle savedInstanceState) + protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); } @@ -19,13 +16,6 @@ namespace ControlCatalog.Android { base.OnResume(); - if (Avalonia.Application.Current == null) - { - AppBuilder.Configure() - .UseAndroid() - .SetupWithoutStarting(); - } - StartActivity(new Intent(Application.Context, typeof(MainActivity))); } } diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 2d4fc45171..d1b657722c 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -6,7 +6,14 @@ true
+ + true + https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json + 7.0.0-* + + + @@ -15,6 +22,14 @@ + + + + + + + + en diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 0c8fd9465c..4b81935452 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -118,6 +118,13 @@ namespace ControlCatalog.NetCore }) .UseSkia() .UseManagedSystemDialogs() + .AfterSetup(builder => + { + builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() + { + StartupScreenIndex = 1, + }); + }) .LogToTrace(); static void SilenceConsole() diff --git a/samples/ControlCatalog.NetCore/rd.xml b/samples/ControlCatalog.NetCore/rd.xml new file mode 100644 index 0000000000..27db7f34ca --- /dev/null +++ b/samples/ControlCatalog.NetCore/rd.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj index 199fa85ad2..b2c9ec72eb 100644 --- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj +++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj @@ -2,13 +2,13 @@ net6.0 enable - True + + true + 16777216 + false + false - - - - false @@ -22,19 +22,36 @@ -O3 -O3 false + false + false + false + false + false + true + false + true + true + true + link + true - - + + - - - - - + + + + + + + + +
+ diff --git a/samples/ControlCatalog.Web/LinkerConfig.xml b/samples/ControlCatalog.Web/LinkerConfig.xml deleted file mode 100644 index 5839a0fe03..0000000000 --- a/samples/ControlCatalog.Web/LinkerConfig.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index db1e16166a..513ac44f83 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -1,186 +1,16 @@ - - + - Debug - iPhoneSimulator - {57E0455D-D565-44BB-B069-EE1AA20F8337} - {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} Exe - ControlCatalog.iOS - Resources - ControlCatalogiOS - true - NSUrlSessionHandler - PackageReference - automatic + manual + net6.0-ios + 10.0 + + True + iossimulator-x64 + - - true - full - false - bin\iPhoneSimulator\Debug - DEBUG - prompt - 4 - false - x86_64 - None - True - 9.1 - False - False - False - False - False - False - False - False - True - Default - HttpClientHandler - False - - - none - true - bin\iPhoneSimulator\Release - prompt - 4 - None - x86_64 - false - - - true - full - false - bin\iPhone\Debug - DEBUG - prompt - 4 - false - ARMv7, ARM64 - Entitlements.plist - iPhone Developer - true - - - none - true - bin\iPhone\Release - prompt - 4 - Entitlements.plist - ARMv7, ARM64 - false - iPhone Developer - - - none - True - bin\iPhone\Ad-Hoc - prompt - 4 - False - ARMv7, ARM64 - Entitlements.plist - True - Automatic:AdHoc - iPhone Distribution - - - none - True - bin\iPhone\AppStore - prompt - 4 - False - ARMv7, ARM64 - Entitlements.plist - Automatic:AppStore - iPhone Distribution - - - - - - - - - - - - - - - - - {4488AD85-1495-4809-9AA4-DDFE0A48527E} - Avalonia.iOS - false - false - - - {3E53A01A-B331-47F3-B828-4A5717E77A24} - Avalonia.Markup.Xaml - - - {6417E941-21BC-467B-A771-0DE389353CE6} - Avalonia.Markup - - - {D211E587-D8BC-45B9-95A4-F297C8FA5200} - Avalonia.Animation - - - {B09B78D8-9B26-48B0-9149-D64A2F120F3F} - Avalonia.Base - - - {D2221C82-4A25-4583-9B43-D791E3F6820C} - Avalonia.Controls - - - {7062AE20-5DCC-4442-9645-8195BDECE63E} - Avalonia.Diagnostics - - - {62024B2D-53EB-4638-B26B-85EEAA54866E} - Avalonia.Input - - - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B} - Avalonia.Interactivity - - - {42472427-4774-4C81-8AFF-9F27B8E31721} - Avalonia.Layout - - - {EB582467-6ABB-43A1-B052-E981BA910E3A} - Avalonia.Visuals - - - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F} - Avalonia.Styling - - - {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F} - Avalonia.Themes.Default - - - {7d2d3083-71dd-4cc9-8907-39a0d86fb322} - Avalonia.Skia - - - {d0a739b9-3c68-4ba6-a328-41606954b6bd} - ControlCatalog - - + + - - - - - + \ No newline at end of file diff --git a/samples/ControlCatalog.iOS/Info.plist b/samples/ControlCatalog.iOS/Info.plist index 216fd9c333..6ffe3ba662 100644 --- a/samples/ControlCatalog.iOS/Info.plist +++ b/samples/ControlCatalog.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleDisplayName ControlCatalog.iOS CFBundleIdentifier - com.companyname.ControlCatalog.iOS + Avalonia.ControlCatalog CFBundleShortVersionString 1.0 CFBundleVersion @@ -13,7 +13,7 @@ LSRequiresIPhoneOS MinimumOSVersion - 8.0 + 10.0 UIDeviceFamily 1 @@ -28,6 +28,7 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight @@ -38,5 +39,9 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIStatusBarHidden + + UIViewControllerBasedStatusBarAppearance + diff --git a/samples/ControlCatalog.iOS/Main.cs b/samples/ControlCatalog.iOS/Main.cs index fe039ba69e..2400115041 100644 --- a/samples/ControlCatalog.iOS/Main.cs +++ b/samples/ControlCatalog.iOS/Main.cs @@ -9,7 +9,7 @@ namespace ControlCatalog.iOS { // if you want to use a different Application Delegate class from "AppDelegate" // you can specify it here. - UIApplication.Main(args, null, "AppDelegate"); + UIApplication.Main(args, null, typeof(AppDelegate)); } } -} \ No newline at end of file +} diff --git a/samples/ControlCatalog.iOS/Properties/AssemblyInfo.cs b/samples/ControlCatalog.iOS/Properties/AssemblyInfo.cs deleted file mode 100644 index 0a5a598651..0000000000 --- a/samples/ControlCatalog.iOS/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("ControlCatalog.iOS")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ControlCatalog.iOS")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("57e0455d-d565-44bb-b069-ee1aa20f8337")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/ControlCatalog.iOS/Resources/LaunchScreen.xib b/samples/ControlCatalog.iOS/Resources/LaunchScreen.xib index be4abb2b43..5d3ccc97db 100644 --- a/samples/ControlCatalog.iOS/Resources/LaunchScreen.xib +++ b/samples/ControlCatalog.iOS/Resources/LaunchScreen.xib @@ -11,7 +11,7 @@ -
- + + + TextFormatterPage.axaml + Code + diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml index 1aa7f8ea04..1d58c465a0 100644 --- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml +++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml @@ -22,7 +22,7 @@ 40 - 200 + 220 36 36 32 @@ -195,9 +195,9 @@ VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}" - TextBlock.FontFamily="{TemplateBinding FontFamily}" - TextBlock.FontSize="{TemplateBinding FontSize}" - TextBlock.FontWeight="{TemplateBinding FontWeight}" /> + TextElement.FontFamily="{TemplateBinding FontFamily}" + TextElement.FontSize="{TemplateBinding FontSize}" + TextElement.FontWeight="{TemplateBinding FontWeight}" /> @@ -216,25 +216,25 @@ Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" CornerRadius="{TemplateBinding CornerRadius}" - TextBlock.FontFamily="{TemplateBinding FontFamily}" - TextBlock.FontSize="{TemplateBinding FontSize}" - TextBlock.FontWeight="{TemplateBinding FontWeight}" /> + TextElement.FontFamily="{TemplateBinding FontFamily}" + TextElement.FontSize="{TemplateBinding FontSize}" + TextElement.FontWeight="{TemplateBinding FontWeight}" /> + @@ -102,22 +116,35 @@ + + + + + @@ -180,57 +188,58 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - + Fill="{TemplateBinding SeparatorBrush}" + IsVisible="{TemplateBinding AreSeparatorsVisible}" /> + + + + + - + @@ -271,38 +280,51 @@ - - - - - - - - + + + + + + + - + DataGridFrozenGrid.IsFrozen="True" /> + + - + + + + + @@ -430,9 +452,12 @@ Width="12" Height="12" Margin="12,0,0,0" + BorderBrush="{TemplateBinding BorderBrush}" + BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" - Foreground="{TemplateBinding Foreground}" - Focusable="False" /> + CornerRadius="{TemplateBinding CornerRadius}" + Focusable="False" + Foreground="{TemplateBinding Foreground}" /> - - + CornerRadius="{TemplateBinding CornerRadius}"> + diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 2c206b53f6..fe3ac31734 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -1,4 +1,6 @@ Compat issues with assembly Avalonia.Controls: +TypesMustExist : Type 'Avalonia.Controls.DropDown' does not exist in the implementation but it does exist in the contract. +TypesMustExist : Type 'Avalonia.Controls.DropDownItem' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.IMenuItem.StaysOpenOnClick.set(System.Boolean)' is present in the implementation but not in the contract. @@ -29,7 +31,28 @@ MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChang MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontFamilyProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontStyleProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontWeightProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.ForegroundProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontSizeProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.TextAlignmentProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.TextTrimmingProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.TextWrappingProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.LineHeightProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.MaxLinesProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.FontFamily Avalonia.Controls.TextBlock.GetFontFamily(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.TextBlock.GetFontSize(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.FontStyle Avalonia.Controls.TextBlock.GetFontStyle(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.FontWeight Avalonia.Controls.TextBlock.GetFontWeight(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.IBrush Avalonia.Controls.TextBlock.GetForeground(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontFamily(Avalonia.Controls.Control, Avalonia.Media.FontFamily)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontSize(Avalonia.Controls.Control, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontStyle(Avalonia.Controls.Control, Avalonia.Media.FontStyle)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontWeight(Avalonia.Controls.Control, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetForeground(Avalonia.Controls.Control, Avalonia.Media.IBrush)' does not exist in the implementation but it does exist in the contract. CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.TopLevel' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Viewbox' does not inherit from base type 'Avalonia.Controls.Decorator' in the implementation but it does in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Window' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. @@ -48,7 +71,11 @@ MembersMustExist : Member 'public Avalonia.Media.FormattedText Avalonia.Controls MembersMustExist : Member 'public System.Int32 Avalonia.Controls.Presenters.TextPresenter.GetCaretIndex(Avalonia.Point)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Controls.Presenters.TextPresenter.InvalidateFormattedText()' does not exist in the implementation but it does exist in the contract. CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Primitives.PopupRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. +TypesMustExist : Type 'Avalonia.Platform.ExportWindowingSubsystemAttribute' does not exist in the implementation but it does exist in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.Screen Avalonia.Platform.IScreenImpl.ScreenFromPoint(Avalonia.PixelPoint)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.Screen Avalonia.Platform.IScreenImpl.ScreenFromRect(Avalonia.PixelRect)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.Screen Avalonia.Platform.IScreenImpl.ScreenFromWindow(Avalonia.Platform.IWindowBaseImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the implementation but not in the contract. @@ -67,4 +94,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. -Total Issues: 68 +Total Issues: 95 diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 76e2d3a161..c59458311c 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -65,7 +65,7 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public Window? MainWindow { get; set; } - public IReadOnlyList Windows => _windows.ToList(); + public IReadOnlyList Windows => _windows.ToArray(); private void HandleWindowClosed(Window window) { diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 930e250334..3316c06bf5 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -252,6 +252,10 @@ namespace Avalonia.Controls /// drop-down that contains possible matches based on the input in the text /// box. /// + [TemplatePart(ElementPopup, typeof(Popup))] + [TemplatePart(ElementSelector, typeof(SelectingItemsControl))] + [TemplatePart(ElementSelectionAdapter, typeof(ISelectionAdapter))] + [TemplatePart(ElementTextBox, typeof(TextBox))] [PseudoClasses(":dropdownopen")] public class AutoCompleteBox : TemplatedControl { @@ -2180,7 +2184,7 @@ namespace Avalonia.Controls } // Store a local cached copy of the data - _items = newValue == null ? null : new List(newValue.Cast().ToList()); + _items = newValue == null ? null : new List(newValue.Cast()); // Clear and set the view on the selection adapter ClearView(); @@ -2239,7 +2243,7 @@ namespace Avalonia.Controls ClearView(); if (Items != null) { - _items = new List(Items.Cast().ToList()); + _items = new List(Items.Cast()); } } diff --git a/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs b/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs new file mode 100644 index 0000000000..4566cd9db5 --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs @@ -0,0 +1,28 @@ +using Avalonia.Automation.Peers; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as automation property identifiers by UI Automation providers. + /// + public static class AutomationElementIdentifiers + { + /// + /// Identifies the bounding rectangle automation property. The bounding rectangle property + /// value is returned by the method. + /// + public static AutomationProperty BoundingRectangleProperty { get; } = new AutomationProperty(); + + /// + /// Identifies the class name automation property. The class name property value is returned + /// by the method. + /// + public static AutomationProperty ClassNameProperty { get; } = new AutomationProperty(); + + /// + /// Identifies the name automation property. The class name property value is returned + /// by the method. + /// + public static AutomationProperty NameProperty { get; } = new AutomationProperty(); + } +} diff --git a/src/Avalonia.Controls/Automation/AutomationLiveSetting.cs b/src/Avalonia.Controls/Automation/AutomationLiveSetting.cs new file mode 100644 index 0000000000..55de657b32 --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationLiveSetting.cs @@ -0,0 +1,28 @@ +namespace Avalonia.Automation +{ + /// + /// Describes the notification characteristics of a particular live region + /// + public enum AutomationLiveSetting + { + /// + /// The element does not send notifications if the content of the live region has changed. + /// + Off = 0, + + /// + /// The element sends non-interruptive notifications if the content of the live region has + /// changed. With this setting, UI Automation clients and assistive technologies are expected + /// to not interrupt the user to inform of changes to the live region. + /// + Polite = 1, + + /// + /// The element sends interruptive notifications if the content of the live region has changed. + /// With this setting, UI Automation clients and assistive technologies are expected to interrupt + /// the user to inform of changes to the live region. + /// + Assertive = 2, + } +} + diff --git a/src/Avalonia.Controls/Automation/AutomationProperties.cs b/src/Avalonia.Controls/Automation/AutomationProperties.cs new file mode 100644 index 0000000000..c20af148b8 --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationProperties.cs @@ -0,0 +1,630 @@ +using System; +using Avalonia.Automation.Peers; +using Avalonia.Controls; + +namespace Avalonia.Automation +{ + /// + /// Declares how a control should included in different views of the automation tree. + /// + public enum AccessibilityView + { + /// + /// The control is included in the Raw view of the automation tree. + /// + Raw, + + /// + /// The control is included in the Control view of the automation tree. + /// + Control, + + /// + /// The control is included in the Content view of the automation tree. + /// + Content, + } + + public static class AutomationProperties + { + internal const int AutomationPositionInSetDefault = -1; + internal const int AutomationSizeOfSetDefault = -1; + + /// + /// Defines the AutomationProperties.AcceleratorKey attached property. + /// + public static readonly AttachedProperty AcceleratorKeyProperty = + AvaloniaProperty.RegisterAttached( + "AcceleratorKey", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.AccessibilityView attached property. + /// + public static readonly AttachedProperty AccessibilityViewProperty = + AvaloniaProperty.RegisterAttached( + "AccessibilityView", + typeof(AutomationProperties), + defaultValue: AccessibilityView.Content); + + /// + /// Defines the AutomationProperties.AccessKey attached property + /// + public static readonly AttachedProperty AccessKeyProperty = + AvaloniaProperty.RegisterAttached( + "AccessKey", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.AutomationId attached property. + /// + public static readonly AttachedProperty AutomationIdProperty = + AvaloniaProperty.RegisterAttached( + "AutomationId", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.ControlTypeOverride attached property. + /// + public static readonly AttachedProperty ControlTypeOverrideProperty = + AvaloniaProperty.RegisterAttached( + "ControlTypeOverride", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.HelpText attached property. + /// + public static readonly AttachedProperty HelpTextProperty = + AvaloniaProperty.RegisterAttached( + "HelpText", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.IsColumnHeader attached property. + /// + public static readonly AttachedProperty IsColumnHeaderProperty = + AvaloniaProperty.RegisterAttached( + "IsColumnHeader", + typeof(AutomationProperties), + false); + + /// + /// Defines the AutomationProperties.IsRequiredForForm attached property. + /// + public static readonly AttachedProperty IsRequiredForFormProperty = + AvaloniaProperty.RegisterAttached( + "IsRequiredForForm", + typeof(AutomationProperties), + false); + + /// + /// Defines the AutomationProperties.IsRowHeader attached property. + /// + public static readonly AttachedProperty IsRowHeaderProperty = + AvaloniaProperty.RegisterAttached( + "IsRowHeader", + typeof(AutomationProperties), + false); + + /// + /// Defines the AutomationProperties.IsOffscreenBehavior attached property. + /// + public static readonly AttachedProperty IsOffscreenBehaviorProperty = + AvaloniaProperty.RegisterAttached( + "IsOffscreenBehavior", + typeof(AutomationProperties), + IsOffscreenBehavior.Default); + + /// + /// Defines the AutomationProperties.ItemStatus attached property. + /// + public static readonly AttachedProperty ItemStatusProperty = + AvaloniaProperty.RegisterAttached( + "ItemStatus", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.ItemType attached property. + /// + public static readonly AttachedProperty ItemTypeProperty = + AvaloniaProperty.RegisterAttached( + "ItemType", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.LabeledBy attached property. + /// + public static readonly AttachedProperty LabeledByProperty = + AvaloniaProperty.RegisterAttached( + "LabeledBy", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.LiveSetting attached property. + /// + public static readonly AttachedProperty LiveSettingProperty = + AvaloniaProperty.RegisterAttached( + "LiveSetting", + typeof(AutomationProperties), + AutomationLiveSetting.Off); + + /// + /// Defines the AutomationProperties.Name attached attached property. + /// + public static readonly AttachedProperty NameProperty = + AvaloniaProperty.RegisterAttached( + "Name", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.PositionInSet attached property. + /// + /// + /// The PositionInSet property describes the ordinal location of the element within a set + /// of elements which are considered to be siblings. PositionInSet works in coordination + /// with the SizeOfSet property to describe the ordinal location in the set. + /// + public static readonly AttachedProperty PositionInSetProperty = + AvaloniaProperty.RegisterAttached( + "PositionInSet", + typeof(AutomationProperties), + AutomationPositionInSetDefault); + + /// + /// Defines the AutomationProperties.SizeOfSet attached property. + /// + /// + /// The SizeOfSet property describes the count of automation elements in a group or set + /// that are considered to be siblings. SizeOfSet works in coordination with the PositionInSet + /// property to describe the count of items in the set. + /// + public static readonly AttachedProperty SizeOfSetProperty = + AvaloniaProperty.RegisterAttached( + "SizeOfSet", + typeof(AutomationProperties), + AutomationSizeOfSetDefault); + + /// + /// Helper for setting AcceleratorKey property on a StyledElement. + /// + public static void SetAcceleratorKey(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(AcceleratorKeyProperty, value); + } + + /// + /// Helper for reading AcceleratorKey property from a StyledElement. + /// + public static string GetAcceleratorKey(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(AcceleratorKeyProperty)); + } + + /// + /// Helper for setting AccessibilityView property on a StyledElement. + /// + public static void SetAccessibilityView(StyledElement element, AccessibilityView value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(AccessibilityViewProperty, value); + } + + /// + /// Helper for reading AccessibilityView property from a StyledElement. + /// + public static AccessibilityView GetAccessibilityView(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return element.GetValue(AccessibilityViewProperty); + } + + /// + /// Helper for setting AccessKey property on a StyledElement. + /// + public static void SetAccessKey(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(AccessKeyProperty, value); + } + + /// + /// Helper for reading AccessKey property from a StyledElement. + /// + public static string GetAccessKey(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(AccessKeyProperty)); + } + + /// + /// Helper for setting AutomationId property on a StyledElement. + /// + public static void SetAutomationId(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(AutomationIdProperty, value); + } + + /// + /// Helper for reading AutomationId property from a StyledElement. + /// + public static string GetAutomationId(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return element.GetValue(AutomationIdProperty); + } + + /// + /// Helper for setting ControlTypeOverride property on a StyledElement. + /// + public static void SetControlTypeOverride(StyledElement element, AutomationControlType? value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(ControlTypeOverrideProperty, value); + } + + /// + /// Helper for reading ControlTypeOverride property from a StyledElement. + /// + public static AutomationControlType? GetControlTypeOverride(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return element.GetValue(ControlTypeOverrideProperty); + } + + /// + /// Helper for setting HelpText property on a StyledElement. + /// + public static void SetHelpText(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(HelpTextProperty, value); + } + + /// + /// Helper for reading HelpText property from a StyledElement. + /// + public static string GetHelpText(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(HelpTextProperty)); + } + + /// + /// Helper for setting IsColumnHeader property on a StyledElement. + /// + public static void SetIsColumnHeader(StyledElement element, bool value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(IsColumnHeaderProperty, value); + } + + /// + /// Helper for reading IsColumnHeader property from a StyledElement. + /// + public static bool GetIsColumnHeader(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((bool)element.GetValue(IsColumnHeaderProperty)); + } + + /// + /// Helper for setting IsRequiredForForm property on a StyledElement. + /// + public static void SetIsRequiredForForm(StyledElement element, bool value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(IsRequiredForFormProperty, value); + } + + /// + /// Helper for reading IsRequiredForForm property from a StyledElement. + /// + public static bool GetIsRequiredForForm(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((bool)element.GetValue(IsRequiredForFormProperty)); + } + + /// + /// Helper for reading IsRowHeader property from a StyledElement. + /// + public static bool GetIsRowHeader(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((bool)element.GetValue(IsRowHeaderProperty)); + } + + /// + /// Helper for setting IsRowHeader property on a StyledElement. + /// + public static void SetIsRowHeader(StyledElement element, bool value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(IsRowHeaderProperty, value); + } + + /// + /// Helper for setting IsOffscreenBehavior property on a StyledElement. + /// + public static void SetIsOffscreenBehavior(StyledElement element, IsOffscreenBehavior value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(IsOffscreenBehaviorProperty, value); + } + + /// + /// Helper for reading IsOffscreenBehavior property from a StyledElement. + /// + public static IsOffscreenBehavior GetIsOffscreenBehavior(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((IsOffscreenBehavior)element.GetValue(IsOffscreenBehaviorProperty)); + } + + /// + /// Helper for setting ItemStatus property on a StyledElement. + /// + public static void SetItemStatus(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(ItemStatusProperty, value); + } + + /// + /// Helper for reading ItemStatus property from a StyledElement. + /// + public static string GetItemStatus(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(ItemStatusProperty)); + } + + /// + /// Helper for setting ItemType property on a StyledElement. + /// + public static void SetItemType(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(ItemTypeProperty, value); + } + + /// + /// Helper for reading ItemType property from a StyledElement. + /// + public static string GetItemType(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(ItemTypeProperty)); + } + + /// + /// Helper for setting LabeledBy property on a StyledElement. + /// + public static void SetLabeledBy(StyledElement element, IControl value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(LabeledByProperty, value); + } + + /// + /// Helper for reading LabeledBy property from a StyledElement. + /// + public static IControl GetLabeledBy(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return element.GetValue(LabeledByProperty); + } + + /// + /// Helper for setting LiveSetting property on a StyledElement. + /// + public static void SetLiveSetting(StyledElement element, AutomationLiveSetting value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(LiveSettingProperty, value); + } + + /// + /// Helper for reading LiveSetting property from a StyledElement. + /// + public static AutomationLiveSetting GetLiveSetting(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((AutomationLiveSetting)element.GetValue(LiveSettingProperty)); + } + + /// + /// Helper for setting Name property on a StyledElement. + /// + public static void SetName(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(NameProperty, value); + } + + /// + /// Helper for reading Name property from a StyledElement. + /// + public static string GetName(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(NameProperty)); + } + + /// + /// Helper for setting PositionInSet property on a StyledElement. + /// + public static void SetPositionInSet(StyledElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(PositionInSetProperty, value); + } + + /// + /// Helper for reading PositionInSet property from a StyledElement. + /// + public static int GetPositionInSet(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((int)element.GetValue(PositionInSetProperty)); + } + + /// + /// Helper for setting SizeOfSet property on a StyledElement. + /// + public static void SetSizeOfSet(StyledElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(SizeOfSetProperty, value); + } + + /// + /// Helper for reading SizeOfSet property from a StyledElement. + /// + public static int GetSizeOfSet(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((int)element.GetValue(SizeOfSetProperty)); + } + } +} + diff --git a/src/Avalonia.Controls/Automation/AutomationProperty.cs b/src/Avalonia.Controls/Automation/AutomationProperty.cs new file mode 100644 index 0000000000..16968b271d --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationProperty.cs @@ -0,0 +1,11 @@ +namespace Avalonia.Automation +{ + /// + /// Identifies a property of or of a specific + /// control pattern. + /// + public sealed class AutomationProperty + { + internal AutomationProperty() { } + } +} diff --git a/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs b/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs new file mode 100644 index 0000000000..3b7eb70fcb --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs @@ -0,0 +1,21 @@ +using System; + +namespace Avalonia.Automation +{ + public class AutomationPropertyChangedEventArgs : EventArgs + { + public AutomationPropertyChangedEventArgs( + AutomationProperty property, + object? oldValue, + object? newValue) + { + Property = property; + OldValue = oldValue; + NewValue = newValue; + } + + public AutomationProperty Property { get; } + public object? OldValue { get; } + public object? NewValue { get; } + } +} diff --git a/src/Avalonia.Controls/Automation/ElementNotEnabledException.cs b/src/Avalonia.Controls/Automation/ElementNotEnabledException.cs new file mode 100644 index 0000000000..ac73d50603 --- /dev/null +++ b/src/Avalonia.Controls/Automation/ElementNotEnabledException.cs @@ -0,0 +1,10 @@ +using System; + +namespace Avalonia.Automation +{ + public class ElementNotEnabledException : Exception + { + public ElementNotEnabledException() : base("Element not enabled.") { } + public ElementNotEnabledException(string message) : base(message) { } + } +} diff --git a/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs b/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs new file mode 100644 index 0000000000..e2b6782162 --- /dev/null +++ b/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs @@ -0,0 +1,15 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class ExpandCollapsePatternIdentifiers + { + /// + /// Identifies automation property. + /// + public static AutomationProperty ExpandCollapseStateProperty { get; } = new AutomationProperty(); + } +} diff --git a/src/Avalonia.Controls/Automation/ExpandCollapseState.cs b/src/Avalonia.Controls/Automation/ExpandCollapseState.cs new file mode 100644 index 0000000000..c6b4feeb50 --- /dev/null +++ b/src/Avalonia.Controls/Automation/ExpandCollapseState.cs @@ -0,0 +1,29 @@ +namespace Avalonia.Automation +{ + /// + /// Contains values that specify the of a UI Automation element. + /// + public enum ExpandCollapseState + { + /// + /// No child nodes, controls, or content of the UI Automation element are displayed. + /// + Collapsed, + + /// + /// All child nodes, controls or content of the UI Automation element are displayed. + /// + Expanded, + + /// + /// The UI Automation element has no child nodes, controls, or content to display. + /// + LeafNode, + + /// + /// Some, but not all, child nodes, controls, or content of the UI Automation element are + /// displayed. + /// + PartiallyExpanded + } +} diff --git a/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs b/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs new file mode 100644 index 0000000000..128c1e1dcc --- /dev/null +++ b/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs @@ -0,0 +1,26 @@ +namespace Avalonia.Automation +{ + /// + /// This enum offers different ways of evaluating the IsOffscreen AutomationProperty + /// + public enum IsOffscreenBehavior + { + /// + /// The AutomationProperty IsOffscreen is calculated based on IsVisible. + /// + Default, + /// + /// The AutomationProperty IsOffscreen is false. + /// + Onscreen, + /// + /// The AutomationProperty IsOffscreen if true. + /// + Offscreen, + /// + /// The AutomationProperty IsOffscreen is calculated based on clip regions. + /// + FromClip, + } +} + diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs new file mode 100644 index 0000000000..71421ac136 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Automation.Peers +{ + public enum AutomationControlType + { + None, + Button, + Calendar, + CheckBox, + ComboBox, + ComboBoxItem, + Edit, + Hyperlink, + Image, + ListItem, + List, + Menu, + MenuBar, + MenuItem, + ProgressBar, + RadioButton, + ScrollBar, + Slider, + Spinner, + StatusBar, + Tab, + TabItem, + Text, + ToolBar, + ToolTip, + Tree, + TreeItem, + Custom, + Group, + Thumb, + DataGrid, + DataItem, + Document, + SplitButton, + Window, + Pane, + Header, + HeaderItem, + Table, + TitleBar, + Separator, + } + + /// + /// Provides a base class that exposes an element to UI Automation. + /// + public abstract class AutomationPeer + { + /// + /// Attempts to bring the element associated with the automation peer into view. + /// + public void BringIntoView() => BringIntoViewCore(); + + /// + /// Gets the accelerator key combinations for the element that is associated with the UI + /// Automation peer. + /// + public string? GetAcceleratorKey() => GetAcceleratorKeyCore(); + + /// + /// Gets the access key for the element that is associated with the automation peer. + /// + public string? GetAccessKey() => GetAccessKeyCore(); + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + public AutomationControlType GetAutomationControlType() => GetControlTypeOverrideCore(); + + /// + /// Gets the automation ID of the element that is associated with the UI Automation peer. + /// + public string? GetAutomationId() => GetAutomationIdCore(); + + /// + /// Gets the bounding rectangle of the element that is associated with the automation peer + /// in top-level coordinates. + /// + public Rect GetBoundingRectangle() => GetBoundingRectangleCore(); + + /// + /// Gets the child automation peers. + /// + public IReadOnlyList GetChildren() => GetOrCreateChildrenCore(); + + /// + /// Gets a string that describes the class of the element. + /// + public string GetClassName() => GetClassNameCore() ?? string.Empty; + + /// + /// Gets the automation peer for the label that is targeted to the element. + /// + /// + public AutomationPeer? GetLabeledBy() => GetLabeledByCore(); + + /// + /// Gets a human-readable localized string that represents the type of the control that is + /// associated with this automation peer. + /// + public string GetLocalizedControlType() => GetLocalizedControlTypeCore(); + + /// + /// Gets text that describes the element that is associated with this automation peer. + /// + public string GetName() => GetNameCore() ?? string.Empty; + + /// + /// Gets the that is the parent of this . + /// + /// + public AutomationPeer? GetParent() => GetParentCore(); + + /// + /// Gets a value that indicates whether the element that is associated with this automation + /// peer currently has keyboard focus. + /// + public bool HasKeyboardFocus() => HasKeyboardFocusCore(); + + /// + /// Gets a value that indicates whether the element that is associated with this automation + /// peer contains data that is presented to the user. + /// + public bool IsContentElement() => IsControlElement() && IsContentElementCore(); + + /// + /// Gets a value that indicates whether the element is understood by the user as + /// interactive or as contributing to the logical structure of the control in the GUI. + /// + public bool IsControlElement() => IsControlElementCore(); + + /// + /// Gets a value indicating whether the control is enabled for user interaction. + /// + public bool IsEnabled() => IsEnabledCore(); + + /// + /// Gets a value that indicates whether the element can accept keyboard focus. + /// + /// + public bool IsKeyboardFocusable() => IsKeyboardFocusableCore(); + + /// + /// Sets the keyboard focus on the element that is associated with this automation peer. + /// + public void SetFocus() => SetFocusCore(); + + /// + /// Shows the context menu for the element that is associated with this automation peer. + /// + /// true if a context menu is present for the element; otherwise false. + public bool ShowContextMenu() => ShowContextMenuCore(); + + /// + /// Tries to get a provider of the specified type from the peer. + /// + /// The provider type. + /// The provider, or null if not implemented on this peer. + public T? GetProvider() => (T?)GetProviderCore(typeof(T)); + + /// + /// Occurs when the children of the automation peer have changed. + /// + public event EventHandler? ChildrenChanged; + + /// + /// Occurs when a property value of the automation peer has changed. + /// + public event EventHandler? PropertyChanged; + + /// + /// Raises an event to notify the automation client the the children of the peer have changed. + /// + protected void RaiseChildrenChangedEvent() => ChildrenChanged?.Invoke(this, EventArgs.Empty); + + /// + /// Raises an event to notify the automation client of a changed property value. + /// + /// The property that changed. + /// The previous value of the property. + /// The new value of the property. + public void RaisePropertyChangedEvent( + AutomationProperty property, + object? oldValue, + object? newValue) + { + PropertyChanged?.Invoke(this, new AutomationPropertyChangedEventArgs(property, oldValue, newValue)); + } + + protected virtual string GetLocalizedControlTypeCore() + { + var controlType = GetAutomationControlType(); + + return controlType switch + { + AutomationControlType.CheckBox => "check box", + AutomationControlType.ComboBox => "combo box", + AutomationControlType.ListItem => "list item", + AutomationControlType.MenuBar => "menu bar", + AutomationControlType.MenuItem => "menu item", + AutomationControlType.ProgressBar => "progress bar", + AutomationControlType.RadioButton => "radio button", + AutomationControlType.ScrollBar => "scroll bar", + AutomationControlType.StatusBar => "status bar", + AutomationControlType.TabItem => "tab item", + AutomationControlType.ToolBar => "toolbar", + AutomationControlType.ToolTip => "tooltip", + AutomationControlType.TreeItem => "tree item", + AutomationControlType.Custom => "custom", + AutomationControlType.DataGrid => "data grid", + AutomationControlType.DataItem => "data item", + AutomationControlType.SplitButton => "split button", + AutomationControlType.HeaderItem => "header item", + AutomationControlType.TitleBar => "title bar", + _ => controlType.ToString().ToLowerInvariant(), + }; + } + + protected abstract void BringIntoViewCore(); + protected abstract string? GetAcceleratorKeyCore(); + protected abstract string? GetAccessKeyCore(); + protected abstract AutomationControlType GetAutomationControlTypeCore(); + protected abstract string? GetAutomationIdCore(); + protected abstract Rect GetBoundingRectangleCore(); + protected abstract IReadOnlyList GetOrCreateChildrenCore(); + protected abstract string GetClassNameCore(); + protected abstract AutomationPeer? GetLabeledByCore(); + protected abstract string? GetNameCore(); + protected abstract AutomationPeer? GetParentCore(); + protected abstract bool HasKeyboardFocusCore(); + protected abstract bool IsContentElementCore(); + protected abstract bool IsControlElementCore(); + protected abstract bool IsEnabledCore(); + protected abstract bool IsKeyboardFocusableCore(); + protected abstract void SetFocusCore(); + protected abstract bool ShowContextMenuCore(); + + protected virtual AutomationControlType GetControlTypeOverrideCore() + { + return GetAutomationControlTypeCore(); + } + + protected virtual object? GetProviderCore(Type providerType) + { + return providerType.IsAssignableFrom(this.GetType()) ? this : null; + } + + protected internal abstract bool TrySetParent(AutomationPeer? parent); + + protected void EnsureEnabled() + { + if (!IsEnabled()) + throw new ElementNotEnabledException(); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs new file mode 100644 index 0000000000..4ac07717da --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs @@ -0,0 +1,43 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class ButtonAutomationPeer : ContentControlAutomationPeer, + IInvokeProvider + { + public ButtonAutomationPeer(Button owner) + : base(owner) + { + } + + public new Button Owner => (Button)base.Owner; + + public void Invoke() + { + EnsureEnabled(); + (Owner as Button)?.PerformClick(); + } + + protected override string? GetAcceleratorKeyCore() + { + var result = base.GetAcceleratorKeyCore(); + + if (string.IsNullOrWhiteSpace(result)) + { + result = Owner.HotKey?.ToString(); + } + + return result; + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Button; + } + + protected override bool IsContentElementCore() => true; + protected override bool IsControlElementCore() => true; + } +} + diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs new file mode 100644 index 0000000000..5ff291d972 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class ComboBoxAutomationPeer : SelectingItemsControlAutomationPeer, + IExpandCollapseProvider, + IValueProvider + { + private UnrealizedSelectionPeer[]? _selection; + + public ComboBoxAutomationPeer(ComboBox owner) + : base(owner) + { + } + + public new ComboBox Owner => (ComboBox)base.Owner; + + public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsDropDownOpen); + public bool ShowsMenu => true; + public void Collapse() => Owner.IsDropDownOpen = false; + public void Expand() => Owner.IsDropDownOpen = true; + bool IValueProvider.IsReadOnly => true; + + string? IValueProvider.Value + { + get + { + var selection = GetSelection(); + return selection.Count == 1 ? selection[0].GetName() : null; + } + } + + void IValueProvider.SetValue(string? value) => throw new NotSupportedException(); + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.ComboBox; + } + + protected override IReadOnlyList? GetSelectionCore() + { + if (ExpandCollapseState == ExpandCollapseState.Expanded) + return base.GetSelectionCore(); + + // If the combo box is not open then we won't have an ItemsPresenter so the default + // GetSelectionCore implementation won't work. For this case we create a separate + // peer to represent the unrealized item. + if (Owner.SelectedItem is object selection) + { + _selection ??= new[] { new UnrealizedSelectionPeer(this) }; + _selection[0].Item = selection; + return _selection; + } + + return null; + } + + protected override void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + base.OwnerPropertyChanged(sender, e); + + if (e.Property == ComboBox.IsDropDownOpenProperty) + { + RaisePropertyChangedEvent( + ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, + ToState((bool)e.OldValue!), + ToState((bool)e.NewValue!)); + } + } + + private ExpandCollapseState ToState(bool value) + { + return value ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed; + } + + private class UnrealizedSelectionPeer : UnrealizedElementAutomationPeer + { + private readonly ComboBoxAutomationPeer _owner; + private object? _item; + + public UnrealizedSelectionPeer(ComboBoxAutomationPeer owner) + { + _owner = owner; + } + + public object? Item + { + get => _item; + set + { + if (_item != value) + { + var oldValue = GetNameCore(); + _item = value; + RaisePropertyChangedEvent( + AutomationElementIdentifiers.NameProperty, + oldValue, + GetNameCore()); + } + } + } + + protected override string? GetAcceleratorKeyCore() => null; + protected override string? GetAccessKeyCore() => null; + protected override string? GetAutomationIdCore() => null; + protected override string GetClassNameCore() => typeof(ComboBoxItem).Name; + protected override AutomationPeer? GetLabeledByCore() => null; + protected override AutomationPeer? GetParentCore() => _owner; + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.ListItem; + + protected override string? GetNameCore() + { + if (_item is Control c) + { + var result = AutomationProperties.GetName(c); + + if (result is null && c is ContentControl cc && cc.Presenter?.Child is TextBlock text) + { + result = text.Text; + } + + if (result is null) + { + result = c.GetValue(ContentControl.ContentProperty)?.ToString(); + } + + return result; + } + + return _item?.ToString(); + } + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs new file mode 100644 index 0000000000..df24222a0c --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class ContentControlAutomationPeer : ControlAutomationPeer + { + protected ContentControlAutomationPeer(ContentControl owner) + : base(owner) + { + } + + public new ContentControl Owner => (ContentControl)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Pane; + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (result is null && Owner.Presenter?.Child is TextBlock text) + { + result = text.Text; + } + + if (result is null) + { + result = Owner.Content?.ToString(); + } + + return result; + } + + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs new file mode 100644 index 0000000000..28cb3e34b2 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.VisualTree; + +namespace Avalonia.Automation.Peers +{ + /// + /// An automation peer which represents a element. + /// + public class ControlAutomationPeer : AutomationPeer + { + private IReadOnlyList? _children; + private bool _childrenValid; + private AutomationPeer? _parent; + private bool _parentValid; + + public ControlAutomationPeer(Control owner) + { + Owner = owner ?? throw new ArgumentNullException("owner"); + Initialize(); + } + + public Control Owner { get; } + + public AutomationPeer GetOrCreate(Control element) + { + if (element == Owner) + return this; + return CreatePeerForElement(element); + } + + public static AutomationPeer CreatePeerForElement(Control element) + { + return element.GetOrCreateAutomationPeer(); + } + + protected override void BringIntoViewCore() => Owner.BringIntoView(); + + protected override IReadOnlyList GetOrCreateChildrenCore() + { + var children = _children ?? Array.Empty(); + + if (_childrenValid) + return children; + + var newChildren = GetChildrenCore() ?? Array.Empty(); + + foreach (var peer in children.Except(newChildren)) + peer.TrySetParent(null); + foreach (var peer in newChildren) + peer.TrySetParent(this); + + _childrenValid = true; + return _children = newChildren; + } + + protected virtual IReadOnlyList? GetChildrenCore() + { + var children = ((IVisual)Owner).VisualChildren; + + if (children.Count == 0) + return null; + + var result = new List(); + + foreach (var child in children) + { + if (child is Control c && c.IsVisible) + { + result.Add(GetOrCreate(c)); + } + } + + return result; + } + + protected override AutomationPeer? GetLabeledByCore() + { + var label = AutomationProperties.GetLabeledBy(Owner); + return label is Control c ? GetOrCreate(c) : null; + } + + protected override string? GetNameCore() + { + var result = AutomationProperties.GetName(Owner); + + if (string.IsNullOrWhiteSpace(result) && GetLabeledBy() is AutomationPeer labeledBy) + { + return labeledBy.GetName(); + } + + return null; + } + + protected override AutomationPeer? GetParentCore() + { + EnsureConnected(); + return _parent; + } + + /// + /// Invalidates the peer's children and causes a re-read from . + /// + protected void InvalidateChildren() + { + _childrenValid = false; + RaiseChildrenChangedEvent(); + } + + /// + /// Invalidates the peer's parent. + /// + protected void InvalidateParent() + { + _parent = null; + _parentValid = false; + } + + protected override bool ShowContextMenuCore() + { + var c = Owner; + + while (c is object) + { + if (c.ContextMenu is object) + { + c.ContextMenu.Open(c); + return true; + } + + c = c.Parent as Control; + } + + return false; + } + + protected internal override bool TrySetParent(AutomationPeer? parent) + { + _parent = parent; + return true; + } + + protected override string? GetAcceleratorKeyCore() => AutomationProperties.GetAcceleratorKey(Owner); + protected override string? GetAccessKeyCore() => AutomationProperties.GetAccessKey(Owner); + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom; + protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name; + protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds); + protected override string GetClassNameCore() => Owner.GetType().Name; + protected override bool HasKeyboardFocusCore() => Owner.IsFocused; + protected override bool IsContentElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Content; + protected override bool IsControlElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Control; + protected override bool IsEnabledCore() => Owner.IsEnabled; + protected override bool IsKeyboardFocusableCore() => Owner.Focusable; + protected override void SetFocusCore() => Owner.Focus(); + + protected override AutomationControlType GetControlTypeOverrideCore() + { + return AutomationProperties.GetControlTypeOverride(Owner) ?? GetAutomationControlTypeCore(); + } + + private static Rect GetBounds(TransformedBounds? bounds) + { + return bounds?.Bounds.TransformToAABB(bounds!.Value.Transform) ?? default; + } + + private void Initialize() + { + Owner.PropertyChanged += OwnerPropertyChanged; + var visualChildren = ((IVisual)Owner).VisualChildren; + visualChildren.CollectionChanged += VisualChildrenChanged; + } + + private void VisualChildrenChanged(object? sender, EventArgs e) => InvalidateChildren(); + + private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == Visual.IsVisibleProperty) + { + var parent = Owner.GetVisualParent(); + if (parent is Control c) + (GetOrCreate(c) as ControlAutomationPeer)?.InvalidateChildren(); + } + else if (e.Property == Visual.TransformedBoundsProperty) + { + RaisePropertyChangedEvent( + AutomationElementIdentifiers.BoundingRectangleProperty, + GetBounds((TransformedBounds?)e.OldValue), + GetBounds((TransformedBounds?)e.NewValue)); + } + else if (e.Property == Visual.VisualParentProperty) + { + InvalidateParent(); + } + } + + + private void EnsureConnected() + { + if (!_parentValid) + { + var parent = Owner.GetVisualParent(); + + while (parent is object) + { + if (parent is Control c) + { + var parentPeer = GetOrCreate(c); + parentPeer.GetChildren(); + } + + parent = parent.GetVisualParent(); + } + + _parentValid = true; + } + } + } +} + diff --git a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs new file mode 100644 index 0000000000..db16bf0a53 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs @@ -0,0 +1,54 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class ItemsControlAutomationPeer : ControlAutomationPeer, IScrollProvider + { + private bool _searchedForScrollable; + private IScrollProvider? _scroller; + + public ItemsControlAutomationPeer(ItemsControl owner) + : base(owner) + { + } + + public new ItemsControl Owner => (ItemsControl)base.Owner; + public bool HorizontallyScrollable => _scroller?.HorizontallyScrollable ?? false; + public double HorizontalScrollPercent => _scroller?.HorizontalScrollPercent ?? -1; + public double HorizontalViewSize => _scroller?.HorizontalViewSize ?? 0; + public bool VerticallyScrollable => _scroller?.VerticallyScrollable ?? false; + public double VerticalScrollPercent => _scroller?.VerticalScrollPercent ?? -1; + public double VerticalViewSize => _scroller?.VerticalViewSize ?? 0; + + protected virtual IScrollProvider? Scroller + { + get + { + if (!_searchedForScrollable) + { + if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable) + _scroller = GetOrCreate(scrollable) as IScrollProvider; + _searchedForScrollable = true; + } + + return _scroller; + } + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.List; + } + + public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount) + { + _scroller?.Scroll(horizontalAmount, verticalAmount); + } + + public void SetScrollPercent(double horizontalPercent, double verticalPercent) + { + _scroller?.SetScrollPercent(horizontalPercent, verticalPercent); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs new file mode 100644 index 0000000000..ac23873e6a --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -0,0 +1,82 @@ +using System; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; + +namespace Avalonia.Automation.Peers +{ + public class ListItemAutomationPeer : ContentControlAutomationPeer, + ISelectionItemProvider + { + public ListItemAutomationPeer(ContentControl owner) + : base(owner) + { + } + + public bool IsSelected => Owner.GetValue(ListBoxItem.IsSelectedProperty); + + public ISelectionProvider? SelectionContainer + { + get + { + if (Owner.Parent is Control parent) + { + var parentPeer = GetOrCreate(parent); + return parentPeer as ISelectionProvider; + } + + return null; + } + } + + public void Select() + { + EnsureEnabled(); + + if (Owner.Parent is SelectingItemsControl parent) + { + var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + + if (index != -1) + parent.SelectedIndex = index; + } + } + + void ISelectionItemProvider.AddToSelection() + { + EnsureEnabled(); + + if (Owner.Parent is ItemsControl parent && + parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel) + { + var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + + if (index != -1) + selectionModel.Select(index); + } + } + + void ISelectionItemProvider.RemoveFromSelection() + { + EnsureEnabled(); + + if (Owner.Parent is ItemsControl parent && + parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel) + { + var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + + if (index != -1) + selectionModel.Deselect(index); + } + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.ListItem; + } + + protected override bool IsContentElementCore() => true; + protected override bool IsControlElementCore() => true; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs new file mode 100644 index 0000000000..c98c5c9a22 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs @@ -0,0 +1,59 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public class MenuItemAutomationPeer : ControlAutomationPeer + { + public MenuItemAutomationPeer(MenuItem owner) + : base(owner) + { + } + + public new MenuItem Owner => (MenuItem)base.Owner; + + protected override string? GetAccessKeyCore() + { + var result = base.GetAccessKeyCore(); + + if (string.IsNullOrWhiteSpace(result)) + { + if (Owner.HeaderPresenter?.Child is AccessText accessText) + { + result = accessText.AccessKey.ToString(); + } + } + + return result; + } + + protected override string? GetAcceleratorKeyCore() + { + var result = base.GetAcceleratorKeyCore(); + + if (string.IsNullOrWhiteSpace(result)) + { + result = Owner.InputGesture?.ToString(); + } + + return result; + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.MenuItem; + } + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (result is null && Owner.Header is string header) + { + result = AccessText.RemoveAccessKeyMarker(header); + } + + return result; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs new file mode 100644 index 0000000000..0f92fed6f3 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs @@ -0,0 +1,25 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + /// + /// An automation peer which represents an element that is exposed to automation as non- + /// interactive or as not contributing to the logical structure of the application. + /// + public class NoneAutomationPeer : ControlAutomationPeer + { + public NoneAutomationPeer(Control owner) + : base(owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.None; + } + + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + } +} + diff --git a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs new file mode 100644 index 0000000000..25f6ca6e2d --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public class PopupAutomationPeer : ControlAutomationPeer + { + public PopupAutomationPeer(Popup owner) + : base(owner) + { + owner.Opened += PopupOpenedClosed; + owner.Closed += PopupOpenedClosed; + } + + protected override IReadOnlyList? GetChildrenCore() + { + var host = (IPopupHostProvider)Owner; + return host.PopupHost is Control c ? new[] { GetOrCreate(c) } : null; + } + + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + + private void PopupOpenedClosed(object? sender, EventArgs e) + { + // This is golden. We're following WPF's automation peer API here where the + // parent of a peer is set when another peer returns it as a child. We want to + // add the popup root as a child of the popup, so we need to return it as a + // child right? Yeah except invalidating children doesn't automatically cause + // UIA to re-read the children meaning that the parent doesn't get set. So the + // MAIN MECHANISM FOR PARENTING CONTROLS IS BROKEN WITH THE ONLY AUTOMATION API + // IT WAS WRITTEN FOR. Luckily WPF provides an escape-hatch by exposing the + // TrySetParent API internally to work around this. We're exposing it publicly + // to shame whoever came up with this abomination of an API. + GetPopupRoot()?.TrySetParent(this); + InvalidateChildren(); + } + + private AutomationPeer? GetPopupRoot() + { + var popupRoot = ((IPopupHostProvider)Owner).PopupHost as Control; + return popupRoot is object ? GetOrCreate(popupRoot) : null; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs new file mode 100644 index 0000000000..cb65682c06 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs @@ -0,0 +1,40 @@ +using System; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public class PopupRootAutomationPeer : WindowBaseAutomationPeer + { + public PopupRootAutomationPeer(PopupRoot owner) + : base(owner) + { + if (owner.IsVisible) + StartTrackingFocus(); + else + owner.Opened += OnOpened; + owner.Closed += OnClosed; + } + + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + + + protected override AutomationPeer? GetParentCore() + { + var parent = base.GetParentCore(); + return parent; + } + + private void OnOpened(object? sender, EventArgs e) + { + ((PopupRoot)Owner).Opened -= OnOpened; + StartTrackingFocus(); + } + + private void OnClosed(object? sender, EventArgs e) + { + ((PopupRoot)Owner).Closed -= OnClosed; + StopTrackingFocus(); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs new file mode 100644 index 0000000000..39398933fa --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs @@ -0,0 +1,34 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public abstract class RangeBaseAutomationPeer : ControlAutomationPeer, IRangeValueProvider + { + public RangeBaseAutomationPeer(RangeBase owner) + : base(owner) + { + owner.PropertyChanged += OwnerPropertyChanged; + } + + public new RangeBase Owner => (RangeBase)base.Owner; + public virtual bool IsReadOnly => false; + public double Maximum => Owner.Maximum; + public double Minimum => Owner.Minimum; + public double Value => Owner.Value; + public double SmallChange => Owner.SmallChange; + public double LargeChange => Owner.LargeChange; + + public void SetValue(double value) => Owner.Value = value; + + protected virtual void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == RangeBase.MinimumProperty) + RaisePropertyChangedEvent(RangeValuePatternIdentifiers.MinimumProperty, e.OldValue, e.NewValue); + else if (e.Property == RangeBase.MaximumProperty) + RaisePropertyChangedEvent(RangeValuePatternIdentifiers.MaximumProperty, e.OldValue, e.NewValue); + else if (e.Property == RangeBase.ValueProperty) + RaisePropertyChangedEvent(RangeValuePatternIdentifiers.ValueProperty, e.OldValue, e.NewValue); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs new file mode 100644 index 0000000000..835ed1c4af --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs @@ -0,0 +1,172 @@ +using System; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Utilities; + +namespace Avalonia.Automation.Peers +{ + public class ScrollViewerAutomationPeer : ControlAutomationPeer, IScrollProvider + { + public ScrollViewerAutomationPeer(ScrollViewer owner) + : base(owner) + { + } + + public new ScrollViewer Owner => (ScrollViewer)base.Owner; + + public bool HorizontallyScrollable + { + get => MathUtilities.GreaterThan(Owner.Extent.Width, Owner.Viewport.Width); + } + + public double HorizontalScrollPercent + { + get + { + if (!HorizontallyScrollable) + return ScrollPatternIdentifiers.NoScroll; + return (double)(Owner.Offset.X * 100.0 / (Owner.Extent.Width - Owner.Viewport.Width)); + } + } + + public double HorizontalViewSize + { + get + { + if (MathUtilities.IsZero(Owner.Extent.Width)) + return 100; + return Math.Min(100, Owner.Viewport.Width * 100.0 / Owner.Extent.Width); + } + } + + public bool VerticallyScrollable + { + get => MathUtilities.GreaterThan(Owner.Extent.Height, Owner.Viewport.Height); + } + + public double VerticalScrollPercent + { + get + { + if (!VerticallyScrollable) + return ScrollPatternIdentifiers.NoScroll; + return (double)(Owner.Offset.Y * 100.0 / (Owner.Extent.Height - Owner.Viewport.Height)); + } + } + + public double VerticalViewSize + { + get + { + if (MathUtilities.IsZero(Owner.Extent.Height)) + return 100; + return Math.Min(100, Owner.Viewport.Height * 100.0 / Owner.Extent.Height); + } + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Pane; + } + + protected override bool IsContentElementCore() => false; + + protected override bool IsControlElementCore() + { + // Return false if the control is part of a control template. + return Owner.TemplatedParent is null && base.IsControlElementCore(); + } + + public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount) + { + if (!IsEnabled()) + throw new ElementNotEnabledException(); + + var scrollHorizontally = horizontalAmount != ScrollAmount.NoAmount; + var scrollVertically = verticalAmount != ScrollAmount.NoAmount; + + if (scrollHorizontally && !HorizontallyScrollable || scrollVertically && !VerticallyScrollable) + { + throw new InvalidOperationException("Operation cannot be performed"); + } + + switch (horizontalAmount) + { + case ScrollAmount.LargeDecrement: + Owner.PageLeft(); + break; + case ScrollAmount.SmallDecrement: + Owner.LineLeft(); + break; + case ScrollAmount.SmallIncrement: + Owner.LineRight(); + break; + case ScrollAmount.LargeIncrement: + Owner.PageRight(); + break; + case ScrollAmount.NoAmount: + break; + default: + throw new InvalidOperationException("Operation cannot be performed"); + } + + switch (verticalAmount) + { + case ScrollAmount.LargeDecrement: + Owner.PageUp(); + break; + case ScrollAmount.SmallDecrement: + Owner.LineUp(); + break; + case ScrollAmount.SmallIncrement: + Owner.LineDown(); + break; + case ScrollAmount.LargeIncrement: + Owner.PageDown(); + break; + case ScrollAmount.NoAmount: + break; + default: + throw new InvalidOperationException("Operation cannot be performed"); + } + } + + public void SetScrollPercent(double horizontalPercent, double verticalPercent) + { + if (!IsEnabled()) + throw new ElementNotEnabledException(); + + var scrollHorizontally = horizontalPercent != ScrollPatternIdentifiers.NoScroll; + var scrollVertically = verticalPercent != ScrollPatternIdentifiers.NoScroll; + + if (scrollHorizontally && !HorizontallyScrollable || scrollVertically && !VerticallyScrollable) + { + throw new InvalidOperationException("Operation cannot be performed"); + } + + if (scrollHorizontally && (horizontalPercent < 0.0) || (horizontalPercent > 100.0)) + { + throw new ArgumentOutOfRangeException("horizontalPercent"); + } + + if (scrollVertically && (verticalPercent < 0.0) || (verticalPercent > 100.0)) + { + throw new ArgumentOutOfRangeException("verticalPercent"); + } + + var offset = Owner.Offset; + + if (scrollHorizontally) + { + offset = offset.WithX((Owner.Extent.Width - Owner.Viewport.Width) * horizontalPercent * 0.01); + } + + if (scrollVertically) + { + offset = offset.WithY((Owner.Extent.Height - Owner.Viewport.Height) * verticalPercent * 0.01); + } + + Owner.Offset = offset; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs new file mode 100644 index 0000000000..4626e30ff1 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; +using Avalonia.VisualTree; + +namespace Avalonia.Automation.Peers +{ + public abstract class SelectingItemsControlAutomationPeer : ItemsControlAutomationPeer, + ISelectionProvider + { + private ISelectionModel _selection; + + protected SelectingItemsControlAutomationPeer(SelectingItemsControl owner) + : base(owner) + { + _selection = owner.GetValue(ListBox.SelectionProperty); + _selection.SelectionChanged += OwnerSelectionChanged; + owner.PropertyChanged += OwnerPropertyChanged; + } + + public bool CanSelectMultiple => GetSelectionModeCore().HasAllFlags(SelectionMode.Multiple); + public bool IsSelectionRequired => GetSelectionModeCore().HasAllFlags(SelectionMode.AlwaysSelected); + public IReadOnlyList GetSelection() => GetSelectionCore() ?? Array.Empty(); + + protected virtual IReadOnlyList? GetSelectionCore() + { + List? result = null; + + if (Owner is SelectingItemsControl owner) + { + var selection = Owner.GetValue(ListBox.SelectionProperty); + + foreach (var i in selection.SelectedIndexes) + { + var container = owner.ItemContainerGenerator.ContainerFromIndex(i); + + if (container is Control c && ((IVisual)c).IsAttachedToVisualTree) + { + var peer = GetOrCreate(c); + + if (peer is object) + { + result ??= new List(); + result.Add(peer); + } + } + } + + return result; + } + + return result; + } + + protected virtual SelectionMode GetSelectionModeCore() + { + return (Owner as SelectingItemsControl)?.GetValue(ListBox.SelectionModeProperty) ?? SelectionMode.Single; + } + + protected virtual void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == ListBox.SelectionProperty) + { + _selection.SelectionChanged -= OwnerSelectionChanged; + _selection = Owner.GetValue(ListBox.SelectionProperty); + _selection.SelectionChanged += OwnerSelectionChanged; + RaiseSelectionChanged(); + } + } + + protected virtual void OwnerSelectionChanged(object? sender, SelectionModelSelectionChangedEventArgs e) + { + RaiseSelectionChanged(); + } + + private void RaiseSelectionChanged() + { + RaisePropertyChangedEvent(SelectionPatternIdentifiers.SelectionProperty, null, null); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs new file mode 100644 index 0000000000..8a89e38f62 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class TextBlockAutomationPeer : ControlAutomationPeer + { + public TextBlockAutomationPeer(TextBlock owner) + : base(owner) + { + } + + public new TextBlock Owner => (TextBlock)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Text; + } + + protected override string? GetNameCore() => Owner.Text; + + protected override bool IsControlElementCore() + { + // Return false if the control is part of a control template. + return Owner.TemplatedParent is null && base.IsControlElementCore(); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs new file mode 100644 index 0000000000..9be17afa8c --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs @@ -0,0 +1,23 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class TextBoxAutomationPeer : ControlAutomationPeer, IValueProvider + { + public TextBoxAutomationPeer(TextBox owner) + : base(owner) + { + } + + public new TextBox Owner => (TextBox)base.Owner; + public bool IsReadOnly => Owner.IsReadOnly; + public string? Value => Owner.Text; + public void SetValue(string? value) => Owner.Text = value; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Edit; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs new file mode 100644 index 0000000000..979d54f48e --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs @@ -0,0 +1,39 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public class ToggleButtonAutomationPeer : ContentControlAutomationPeer, IToggleProvider + { + public ToggleButtonAutomationPeer(ToggleButton owner) + : base(owner) + { + } + + public new ToggleButton Owner => (ToggleButton)base.Owner; + + ToggleState IToggleProvider.ToggleState + { + get => Owner.IsChecked switch + { + true => ToggleState.On, + false => ToggleState.Off, + null => ToggleState.Indeterminate, + }; + } + + void IToggleProvider.Toggle() + { + EnsureEnabled(); + Owner.PerformClick(); + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Button; + } + + protected override bool IsContentElementCore() => true; + protected override bool IsControlElementCore() => true; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs new file mode 100644 index 0000000000..56d5aa79ae --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Automation.Peers +{ + /// + /// An automation peer which represents an unrealized element + /// + public abstract class UnrealizedElementAutomationPeer : AutomationPeer + { + public void SetParent(AutomationPeer? parent) => TrySetParent(parent); + protected override void BringIntoViewCore() => GetParent()?.BringIntoView(); + protected override Rect GetBoundingRectangleCore() => GetParent()?.GetBoundingRectangle() ?? default; + protected override IReadOnlyList GetOrCreateChildrenCore() => Array.Empty(); + protected override bool HasKeyboardFocusCore() => false; + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + protected override bool IsEnabledCore() => true; + protected override bool IsKeyboardFocusableCore() => false; + protected override void SetFocusCore() { } + protected override bool ShowContextMenuCore() => false; + protected internal override bool TrySetParent(AutomationPeer? parent) => false; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs new file mode 100644 index 0000000000..1162132d54 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class WindowAutomationPeer : WindowBaseAutomationPeer + { + public WindowAutomationPeer(Window owner) + : base(owner) + { + if (owner.IsVisible) + StartTrackingFocus(); + else + owner.Opened += OnOpened; + owner.Closed += OnClosed; + } + + public new Window Owner => (Window)base.Owner; + + protected override string? GetNameCore() => Owner.Title; + + private void OnOpened(object? sender, EventArgs e) + { + Owner.Opened -= OnOpened; + StartTrackingFocus(); + } + + private void OnClosed(object? sender, EventArgs e) + { + Owner.Closed -= OnClosed; + StopTrackingFocus(); + } + } +} + + diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs new file mode 100644 index 0000000000..30b56bbd96 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -0,0 +1,78 @@ +using System; +using System.ComponentModel; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Automation.Peers +{ + public class WindowBaseAutomationPeer : ControlAutomationPeer, IRootProvider + { + private Control? _focus; + + public WindowBaseAutomationPeer(WindowBase owner) + : base(owner) + { + } + + public new WindowBase Owner => (WindowBase)base.Owner; + public ITopLevelImpl? PlatformImpl => Owner.PlatformImpl; + + public event EventHandler? FocusChanged; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Window; + } + + public AutomationPeer? GetFocus() => _focus is object ? GetOrCreate(_focus) : null; + + public AutomationPeer? GetPeerFromPoint(Point p) + { + var hit = Owner.GetVisualAt(p)?.FindAncestorOfType(includeSelf: true); + return hit is object ? GetOrCreate(hit) : null; + } + + protected void StartTrackingFocus() + { + if (KeyboardDevice.Instance is not null) + { + KeyboardDevice.Instance.PropertyChanged += KeyboardDevicePropertyChanged; + OnFocusChanged(KeyboardDevice.Instance.FocusedElement); + } + } + + protected void StopTrackingFocus() + { + if (KeyboardDevice.Instance is not null) + KeyboardDevice.Instance.PropertyChanged -= KeyboardDevicePropertyChanged; + } + + private void OnFocusChanged(IInputElement? focus) + { + var oldFocus = _focus; + + _focus = focus?.VisualRoot == Owner ? focus as Control : null; + + if (_focus != oldFocus) + { + var peer = _focus is object ? + _focus == Owner ? this : + GetOrCreate(_focus) : null; + FocusChanged?.Invoke(this, EventArgs.Empty); + } + } + + private void KeyboardDevicePropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(KeyboardDevice.FocusedElement)) + { + OnFocusChanged(KeyboardDevice.Instance!.FocusedElement); + } + } + } +} + + diff --git a/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs b/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs new file mode 100644 index 0000000000..a4691180a3 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs @@ -0,0 +1,33 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support UI Automation client access to controls that + /// visually expand to display content and collapse to hide content. + /// + public interface IExpandCollapseProvider + { + /// + /// Gets the state, expanded or collapsed, of the control. + /// + ExpandCollapseState ExpandCollapseState { get; } + + /// + /// Gets a value indicating whether expanding the element shows a menu of items to the user, + /// such as drop-down list. + /// + /// + /// Used in OSX to enable the "Show Menu" action on the element. + /// + bool ShowsMenu { get; } + + /// + /// Displays all child nodes, controls, or content of the control. + /// + void Expand(); + + /// + /// Hides all nodes, controls, or content that are descendants of the control. + /// + void Collapse(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs b/src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs new file mode 100644 index 0000000000..47d7211c92 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support UI Automation client access to controls that + /// initiate or perform a single, unambiguous action and do not maintain state when + /// activated. + /// + public interface IInvokeProvider + { + /// + /// Sends a request to activate a control and initiate its single, unambiguous action. + /// + void Invoke(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs new file mode 100644 index 0000000000..43a877a21a --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs @@ -0,0 +1,47 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support access by a UI Automation client to controls + /// that can be set to a value within a range. + /// + public interface IRangeValueProvider + { + /// + /// Gets a value that indicates whether the value of a control is read-only. + /// + bool IsReadOnly { get; } + + /// + /// Gets the minimum range value that is supported by the control. + /// + double Minimum { get; } + + /// + /// Gets the maximum range value that is supported by the control. + /// + double Maximum { get; } + + /// + /// Gets the value of the control. + /// + double Value { get; } + + /// + /// Gets the value that is added to or subtracted from the Value property when a large + /// change is made, such as with the PAGE DOWN key. + /// + double LargeChange { get; } + + /// + /// Gets the value that is added to or subtracted from the Value property when a small + /// change is made, such as with an arrow key. + /// + double SmallChange { get; } + + /// + /// Sets the value of the control. + /// + /// The value to set. + public void SetValue(double value); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs new file mode 100644 index 0000000000..ce38059559 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Automation.Peers; +using Avalonia.Platform; + +namespace Avalonia.Automation.Provider +{ + public interface IRootProvider + { + ITopLevelImpl? PlatformImpl { get; } + AutomationPeer? GetFocus(); + AutomationPeer? GetPeerFromPoint(Point p); + event EventHandler? FocusChanged; + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs b/src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs new file mode 100644 index 0000000000..1055a2f1e1 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs @@ -0,0 +1,71 @@ +namespace Avalonia.Automation.Provider +{ + public enum ScrollAmount + { + LargeDecrement, + SmallDecrement, + NoAmount, + LargeIncrement, + SmallIncrement, + } + + /// + /// Exposes methods and properties to support access by a UI Automation client to a control + /// that acts as a scrollable container for a collection of child objects. + /// + public interface IScrollProvider + { + /// + /// Gets a value that indicates whether the control can scroll horizontally. + /// + bool HorizontallyScrollable { get; } + + /// + /// Gets the current horizontal scroll position. + /// + double HorizontalScrollPercent { get; } + + /// + /// Gets the current horizontal view size. + /// + double HorizontalViewSize { get; } + + /// + /// Gets a value that indicates whether the control can scroll vertically. + /// + bool VerticallyScrollable { get; } + + /// + /// Gets the current vertical scroll position. + /// + double VerticalScrollPercent { get; } + + /// + /// Gets the vertical view size. + /// + double VerticalViewSize { get; } + + /// + /// Scrolls the visible region of the content area horizontally and vertically. + /// + /// The horizontal increment specific to the control. + /// The vertical increment specific to the control. + void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount); + + /// + /// Sets the horizontal and vertical scroll position as a percentage of the total content + /// area within the control. + /// + /// + /// The horizontal position as a percentage of the content area's total range. + /// should be passed in if the control + /// cannot be scrolled in this direction. + /// + /// + /// The vertical position as a percentage of the content area's total range. + /// should be passed in if the control + /// cannot be scrolled in this direction. + /// + void SetScrollPercent(double horizontalPercent, double verticalPercent); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs b/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs new file mode 100644 index 0000000000..6cea1d1350 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs @@ -0,0 +1,35 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support access by a UI Automation client to individual, + /// selectable child controls of containers that implement . + /// + public interface ISelectionItemProvider + { + /// + /// Gets a value that indicates whether an item is selected. + /// + bool IsSelected { get; } + + /// + /// Gets the UI Automation provider that implements and + /// acts as the container for the calling object. + /// + ISelectionProvider? SelectionContainer { get; } + + /// + /// Adds the current element to the collection of selected items. + /// + void AddToSelection(); + + /// + /// Removes the current element from the collection of selected items. + /// + void RemoveFromSelection(); + + /// + /// Clears any existing selection and then selects the current element. + /// + void Select(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs b/src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs new file mode 100644 index 0000000000..bf21c0151f --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Avalonia.Automation.Peers; + +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support access by a UI Automation client to controls + /// that act as containers for a collection of individual, selectable child items. + /// + public interface ISelectionProvider + { + /// + /// Gets a value that indicates whether the provider allows more than one child element + /// to be selected concurrently. + /// + bool CanSelectMultiple { get; } + + /// + /// Gets a value that indicates whether the provider requires at least one child element + /// to be selected. + /// + bool IsSelectionRequired { get; } + + /// + /// Retrieves a provider for each child element that is selected. + /// + IReadOnlyList GetSelection(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs b/src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs new file mode 100644 index 0000000000..67913e3204 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs @@ -0,0 +1,40 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Contains values that specify the toggle state of a UI Automation element. + /// + public enum ToggleState + { + /// + /// The UI Automation element isn't selected, checked, marked, or otherwise activated. + /// + Off, + + /// + /// The UI Automation element is selected, checked, marked, or otherwise activated. + /// + On, + + /// + /// The UI Automation element is in an indeterminate state. + /// + Indeterminate, + } + + /// + /// Exposes methods and properties to support UI Automation client access to controls that can + /// cycle through a set of states and maintain a particular state. + /// + public interface IToggleProvider + { + /// + /// Gets the toggle state of the control. + /// + ToggleState ToggleState { get; } + + /// + /// Cycles through the toggle states of a control. + /// + void Toggle(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs b/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs new file mode 100644 index 0000000000..e025e28782 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs @@ -0,0 +1,29 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support access by a UI Automation client to controls + /// that have an intrinsic value that does not span a range and that can be represented as + /// a string. + /// + public interface IValueProvider + { + /// + /// Gets a value that indicates whether the value of a control is read-only. + /// + bool IsReadOnly { get; } + + /// + /// Gets the value of the control. + /// + public string? Value { get; } + + /// + /// Sets the value of a control. + /// + /// + /// The value to set. The provider is responsible for converting the value to the + /// appropriate data type. + /// + public void SetValue(string? value); + } +} diff --git a/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs b/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs new file mode 100644 index 0000000000..625b37d001 --- /dev/null +++ b/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs @@ -0,0 +1,30 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class RangeValuePatternIdentifiers + { + /// + /// Identifies automation property. + /// + public static AutomationProperty IsReadOnlyProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty MinimumProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty MaximumProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty ValueProperty { get; } = new AutomationProperty(); + } +} diff --git a/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs b/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs new file mode 100644 index 0000000000..d9e843e75a --- /dev/null +++ b/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs @@ -0,0 +1,45 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class ScrollPatternIdentifiers + { + /// + /// Specifies that scrolling should not be performed. + /// + public const double NoScroll = -1; + + /// + /// Identifies automation property. + /// + public static AutomationProperty HorizontallyScrollableProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty HorizontalScrollPercentProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty HorizontalViewSizeProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty VerticallyScrollableProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty VerticalScrollPercentProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty VerticalViewSizeProperty { get; } = new AutomationProperty(); + } +} diff --git a/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs b/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs new file mode 100644 index 0000000000..c3669528cd --- /dev/null +++ b/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs @@ -0,0 +1,25 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class SelectionPatternIdentifiers + { + /// + /// Identifies automation property. + /// + public static AutomationProperty CanSelectMultipleProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty IsSelectionRequiredProperty { get; } = new AutomationProperty(); + + /// + /// Identifies the property that gets the selected items in a container. + /// + public static AutomationProperty SelectionProperty { get; } = new AutomationProperty(); + } +} diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 543a513d57..4d239e69f4 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -6,17 +6,12 @@ - - - - - - + diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index a2efc7fba0..a4d15bab8d 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Windows.Input; +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; @@ -28,11 +29,14 @@ namespace Avalonia.Controls } /// - /// A button control. + /// A standard button control. /// - [PseudoClasses(":pressed")] - public class Button : ContentControl, ICommandSource + [PseudoClasses(pcFlyoutOpen, pcPressed)] + public class Button : ContentControl, ICommandSource, IClickableControl { + protected const string pcPressed = ":pressed"; + protected const string pcFlyoutOpen = ":flyout-open"; + /// /// Defines the property. /// @@ -85,12 +89,13 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly StyledProperty FlyoutProperty = - AvaloniaProperty.Register(nameof(Flyout)); + public static readonly StyledProperty FlyoutProperty = + AvaloniaProperty.Register(nameof(Flyout)); private ICommand? _command; private bool _commandCanExecute = true; private KeyGesture? _hotkey; + private bool _isFlyoutOpen = false; /// /// Initializes static members of the class. @@ -106,7 +111,6 @@ namespace Avalonia.Controls /// public Button() { - UpdatePseudoClasses(IsPressed); } /// @@ -186,7 +190,7 @@ namespace Avalonia.Controls /// /// Gets or sets the Flyout that should be shown with this button. /// - public FlyoutBase Flyout + public FlyoutBase? Flyout { get => GetValue(FlyoutProperty); set => SetValue(FlyoutProperty, value); @@ -237,7 +241,7 @@ namespace Avalonia.Controls { HotKey = _hotkey; } - + base.OnAttachedToLogicalTree(e); if (Command != null) @@ -327,11 +331,30 @@ namespace Avalonia.Controls } } + /// + /// Opens the button's flyout. + /// protected virtual void OpenFlyout() { Flyout?.ShowAt(this); } + /// + /// Invoked when the button's flyout is opened. + /// + protected virtual void OnFlyoutOpened() + { + // Available for derived types + } + + /// + /// Invoked when the button's flyout is closed. + /// + protected virtual void OnFlyoutClosed() + { + // Available for derived types + } + /// protected override void OnPointerPressed(PointerPressedEventArgs e) { @@ -381,6 +404,14 @@ namespace Avalonia.Controls IsPressed = false; } + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + UnregisterFlyoutEvents(Flyout); + RegisterFlyoutEvents(Flyout); + UpdatePseudoClasses(); + } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { @@ -441,20 +472,31 @@ namespace Avalonia.Controls } else if (change.Property == IsPressedProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(); } else if (change.Property == FlyoutProperty) { + var oldFlyout = change.OldValue.GetValueOrDefault() as FlyoutBase; + var newFlyout = change.NewValue.GetValueOrDefault() as FlyoutBase; + // If flyout is changed while one is already open, make sure we // close the old one first - if (change.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout && + if (oldFlyout != null && oldFlyout.IsOpen) { oldFlyout.Hide(); } + + // Must unregister events here while a reference to the old flyout still exists + UnregisterFlyoutEvents(oldFlyout); + + RegisterFlyoutEvents(newFlyout); + UpdatePseudoClasses(); } } + protected override AutomationPeer OnCreateAutomationPeer() => new ButtonAutomationPeer(this); + /// protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { @@ -472,6 +514,8 @@ namespace Avalonia.Controls } } + internal void PerformClick() => OnClick(); + /// /// Called when the event fires. /// @@ -488,6 +532,32 @@ namespace Avalonia.Controls } } + /// + /// Registers all flyout events. + /// + /// The flyout to connect events to. + private void RegisterFlyoutEvents(FlyoutBase? flyout) + { + if (flyout != null) + { + flyout.Opened += Flyout_Opened; + flyout.Closed += Flyout_Closed; + } + } + + /// + /// Explicitly unregisters all flyout events. + /// + /// The flyout to disconnect events from. + private void UnregisterFlyoutEvents(FlyoutBase? flyout) + { + if (flyout != null) + { + flyout.Opened -= Flyout_Opened; + flyout.Closed -= Flyout_Closed; + } + } + /// /// Starts listening for the Enter key when the button . /// @@ -534,6 +604,7 @@ namespace Avalonia.Controls if (e.Key == Key.Enter && IsVisible && IsEnabled) { OnClick(); + e.Handled = true; } } @@ -547,17 +618,60 @@ namespace Avalonia.Controls if (e.Key == Key.Escape && IsVisible && IsEnabled) { OnClick(); + e.Handled = true; } } /// /// Updates the visual state of the control by applying latest PseudoClasses. /// - private void UpdatePseudoClasses(bool isPressed) + private void UpdatePseudoClasses() { - PseudoClasses.Set(":pressed", isPressed); + PseudoClasses.Set(pcFlyoutOpen, _isFlyoutOpen); + PseudoClasses.Set(pcPressed, IsPressed); } void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + + void IClickableControl.RaiseClick() => OnClick(); + + /// + /// Event handler for when the button's flyout is opened. + /// + private void Flyout_Opened(object? sender, EventArgs e) + { + var flyout = sender as FlyoutBase; + + // It is possible to share flyouts among multiple controls including Button. + // This can cause a problem here since all controls that share a flyout receive + // the same Opened/Closed events at the same time. + // For Button that means they all would be updating their pseudoclasses accordingly. + // In other words, all Buttons with a shared Flyout would have the backgrounds changed together. + // To fix this, only continue here if the Flyout target matches this Button instance. + if (object.ReferenceEquals(flyout?.Target, this)) + { + _isFlyoutOpen = true; + UpdatePseudoClasses(); + + OnFlyoutOpened(); + } + } + + /// + /// Event handler for when the button's flyout is closed. + /// + private void Flyout_Closed(object? sender, EventArgs e) + { + var flyout = sender as FlyoutBase; + + // See comments in Flyout_Opened + if (object.ReferenceEquals(flyout?.Target, this)) + { + _isFlyoutOpen = false; + UpdatePseudoClasses(); + + OnFlyoutClosed(); + } + } } } diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 31aba024ae..29a954098f 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -16,6 +16,8 @@ namespace Avalonia.Controls /// /// Represents a spinner control that includes two Buttons. /// + [TemplatePart("PART_DecreaseButton", typeof(Button))] + [TemplatePart("PART_IncreaseButton", typeof(Button))] [PseudoClasses(":left", ":right")] public class ButtonSpinner : Spinner { diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 6c83308b39..2dbb5f02f9 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -6,6 +6,7 @@ using System; using System.Collections.ObjectModel; using System.Diagnostics; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; @@ -222,6 +223,8 @@ namespace Avalonia.Controls /// element in XAML. /// /// + [TemplatePart(PART_ElementMonth, typeof(CalendarItem))] + [TemplatePart(PART_ElementRoot, typeof(Panel))] public class Calendar : TemplatedControl { internal const int RowsPerMonth = 7; @@ -261,6 +264,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(FirstDayOfWeek), defaultValue: DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek); + /// /// Gets or sets the day that is considered the beginning of the week. /// @@ -273,6 +277,7 @@ namespace Avalonia.Controls get { return GetValue(FirstDayOfWeekProperty); } set { SetValue(FirstDayOfWeekProperty, value); } } + /// /// FirstDayOfWeekProperty property changed handler. /// @@ -289,6 +294,7 @@ namespace Avalonia.Controls throw new ArgumentOutOfRangeException("d", "Invalid DayOfWeek"); } } + /// /// Inherited code: Requires comment. /// @@ -311,6 +317,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(IsTodayHighlighted), defaultValue: true); + /// /// Gets or sets a value indicating whether the current date is /// highlighted. @@ -324,6 +331,7 @@ namespace Avalonia.Controls get { return GetValue(IsTodayHighlightedProperty); } set { SetValue(IsTodayHighlightedProperty, value); } } + /// /// IsTodayHighlightedProperty property changed handler. /// @@ -343,6 +351,7 @@ namespace Avalonia.Controls public static readonly StyledProperty HeaderBackgroundProperty = AvaloniaProperty.Register(nameof(HeaderBackground)); + public IBrush HeaderBackground { get { return GetValue(HeaderBackgroundProperty); } @@ -367,6 +376,7 @@ namespace Avalonia.Controls get { return GetValue(DisplayModeProperty); } set { SetValue(DisplayModeProperty, value); } } + /// /// DisplayModeProperty property changed handler. /// @@ -424,6 +434,7 @@ namespace Avalonia.Controls || mode == CalendarMode.Year || mode == CalendarMode.Decade; } + private void OnDisplayModeChanged(CalendarModeChangedEventArgs args) { DisplayModeChanged?.Invoke(this, args); @@ -433,6 +444,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(SelectionMode), defaultValue: CalendarSelectionMode.SingleDate); + /// /// Gets or sets a value that indicates what kind of selections are /// allowed. @@ -457,6 +469,7 @@ namespace Avalonia.Controls get { return GetValue(SelectionModeProperty); } set { SetValue(SelectionModeProperty, value); } } + private void OnSelectionModeChanged(AvaloniaPropertyChangedEventArgs e) { if (IsValidSelectionMode(e.NewValue!)) @@ -471,6 +484,7 @@ namespace Avalonia.Controls throw new ArgumentOutOfRangeException("d", "Invalid SelectionMode"); } } + /// /// Inherited code: Requires comment. /// @@ -492,6 +506,7 @@ namespace Avalonia.Controls o => o.SelectedDate, (o, v) => o.SelectedDate = v, defaultBindingMode: BindingMode.TwoWay); + /// /// Gets or sets the currently selected date. /// @@ -720,6 +735,7 @@ namespace Avalonia.Controls o => o.DisplayDate, (o, v) => o.DisplayDate = v, defaultBindingMode: BindingMode.TwoWay); + /// /// Gets or sets the date to display. /// @@ -1973,6 +1989,7 @@ namespace Avalonia.Controls } } } + private void Calendar_KeyUp(KeyEventArgs e) { if (!e.Handled && (e.Key == Key.LeftShift || e.Key == Key.RightShift)) @@ -1980,6 +1997,7 @@ namespace Avalonia.Controls ProcessShiftKeyUp(); } } + internal void ProcessShiftKeyUp() { if (_isShiftPressed && (SelectionMode == CalendarSelectionMode.SingleRange || SelectionMode == CalendarSelectionMode.MultipleRange)) @@ -2028,6 +2046,7 @@ namespace Avalonia.Controls } } } + protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); @@ -2054,6 +2073,7 @@ namespace Avalonia.Controls } } } + /// /// Called when the IsEnabled property changes. /// @@ -2098,6 +2118,7 @@ namespace Avalonia.Controls private const string PART_ElementRoot = "Root"; private const string PART_ElementMonth = "CalendarItem"; + /// /// Builds the visual tree for the /// when a new diff --git a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs index c1f487c32d..0ac2056ed1 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs @@ -7,6 +7,7 @@ using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; @@ -116,6 +117,10 @@ namespace Avalonia.Controls Custom = 2 } + [TemplatePart(ElementButton, typeof(Button))] + [TemplatePart(ElementCalendar, typeof(Calendar))] + [TemplatePart(ElementPopup, typeof(Popup))] + [TemplatePart(ElementTextBox, typeof(TextBox))] public class CalendarDatePicker : TemplatedControl { private const string ElementTextBox = "PART_TextBox"; @@ -186,7 +191,8 @@ namespace Avalonia.Controls nameof(SelectedDate), o => o.SelectedDate, (o, v) => o.SelectedDate = v, - enableDataValidation: true); + enableDataValidation: true, + defaultBindingMode:BindingMode.TwoWay); public static readonly StyledProperty SelectedDateFormatProperty = AvaloniaProperty.Register( diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 616b9083ff..c44994f92f 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -19,6 +19,11 @@ namespace Avalonia.Controls.Primitives /// Represents the currently displayed month or year on a /// . /// + [TemplatePart(PART_ElementHeaderButton, typeof(Button))] + [TemplatePart(PART_ElementMonthView, typeof(Grid))] + [TemplatePart(PART_ElementNextButton, typeof(Button))] + [TemplatePart(PART_ElementPreviousButton, typeof(Button))] + [TemplatePart(PART_ElementYearView, typeof(Grid))] [PseudoClasses(":calendardisabled")] public sealed class CalendarItem : TemplatedControl { diff --git a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs index eb90f6c399..7a5c74a51b 100644 --- a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs +++ b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs @@ -68,10 +68,7 @@ namespace Avalonia.Controls public static DateTime DiscardDayTime(DateTime d) { - int year = d.Year; - int month = d.Month; - DateTime newD = new DateTime(year, month, 1, 0, 0, 0); - return newD; + return new DateTime(d.Year, d.Month, 1, 0, 0, 0); } [return: NotNullIfNotNull("d")] diff --git a/src/Avalonia.Controls/CheckBox.cs b/src/Avalonia.Controls/CheckBox.cs index 05d49a44b1..238a21393f 100644 --- a/src/Avalonia.Controls/CheckBox.cs +++ b/src/Avalonia.Controls/CheckBox.cs @@ -1,3 +1,5 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; using Avalonia.Controls.Primitives; namespace Avalonia.Controls @@ -7,5 +9,9 @@ namespace Avalonia.Controls /// public class CheckBox : ToggleButton { + static CheckBox() + { + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.CheckBox); + } } } diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index 1cad1a4c69..d5923a8b37 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -8,6 +8,10 @@ namespace Avalonia.Controls.Chrome /// /// Draws window minimize / maximize / close buttons in a when managed client decorations are enabled. /// + [TemplatePart("PART_CloseButton", typeof(Panel))] + [TemplatePart("PART_RestoreButton", typeof(Panel))] + [TemplatePart("PART_MinimiseButton", typeof(Panel))] + [TemplatePart("PART_FullScreenButton", typeof(Panel))] [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class CaptionButtons : TemplatedControl { diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index 4da50e7220..b152a31587 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -8,6 +8,7 @@ namespace Avalonia.Controls.Chrome /// /// Draws a titlebar when managed client decorations are enabled. /// + [TemplatePart("PART_CaptionButtons", typeof(CaptionButtons))] [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class TitleBar : TemplatedControl { diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index d38cd3a0fd..cbf9b35a05 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Avalonia.Automation.Peers; using System.Reactive.Disposables; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; @@ -10,16 +11,16 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; -using Avalonia.LogicalTree; using Avalonia.Media; -using Avalonia.Threading; using Avalonia.VisualTree; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// A drop-down list control. /// + [TemplatePart("PART_Popup", typeof(Popup))] public class ComboBox : SelectingItemsControl { /// @@ -91,7 +92,7 @@ namespace Avalonia.Controls { ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); FocusableProperty.OverrideDefaultValue(true); - SelectedItemProperty.Changed.AddClassHandler((x,e) => x.SelectedItemChanged(e)); + SelectedItemProperty.Changed.AddClassHandler((x, e) => x.SelectedItemChanged(e)); KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); IsTextSearchEnabledProperty.OverrideDefaultValue(true); } @@ -183,6 +184,25 @@ namespace Avalonia.Controls this.UpdateSelectionBoxItem(SelectedItem); } + // Because the SelectedItem isn't connected to the visual tree + public override void InvalidateMirrorTransform() + { + base.InvalidateMirrorTransform(); + + if (SelectedItem is Control selectedControl) + { + selectedControl.InvalidateMirrorTransform(); + + foreach (var visual in selectedControl.GetVisualDescendants()) + { + if (visual is Control childControl) + { + childControl.InvalidateMirrorTransform(); + } + } + } + } + /// protected override void OnKeyDown(KeyEventArgs e) { @@ -221,8 +241,9 @@ namespace Avalonia.Controls e.Handled = true; } } + // This part of code is needed just to acquire initial focus, subsequent focus navigation will be done by ItemsControl. else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 && - (e.Key == Key.Up || e.Key == Key.Down)) + (e.Key == Key.Up || e.Key == Key.Down) && IsFocused == true) { var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c)); if (firstChild != null) @@ -296,6 +317,11 @@ namespace Avalonia.Controls _popup.Closed += PopupClosed; } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ComboBoxAutomationPeer(this); + } + internal void ItemFocused(ComboBoxItem dropDownItem) { if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) @@ -360,12 +386,12 @@ namespace Avalonia.Controls var selectedIndex = SelectedIndex; if (IsDropDownOpen && selectedIndex != -1) { - var container = ItemContainerGenerator!.ContainerFromIndex(selectedIndex); + var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); if (container == null && SelectedIndex != -1) { ScrollIntoView(Selection.SelectedIndex); - container = ItemContainerGenerator!.ContainerFromIndex(selectedIndex); + container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); } if (container != null && CanFocus(container)) @@ -415,7 +441,7 @@ namespace Avalonia.Controls private void SelectFocusedItem() { - foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator!.Containers) + foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers) { if (dropdownItem.ContainerControl.IsFocused) { @@ -427,22 +453,18 @@ namespace Avalonia.Controls private void SelectNext() { - int next = SelectedIndex + 1; - - if (next >= ItemCount) - next = 0; - - SelectedIndex = next; + if (ItemCount >= 1) + { + MoveSelection(NavigationDirection.Next, WrapSelection); + } } private void SelectPrev() { - int prev = SelectedIndex - 1; - - if (prev < 0) - prev = ItemCount - 1; - - SelectedIndex = prev; + if (ItemCount >= 1) + { + MoveSelection(NavigationDirection.Previous, WrapSelection); + } } } } diff --git a/src/Avalonia.Controls/ComboBoxItem.cs b/src/Avalonia.Controls/ComboBoxItem.cs index a0a1f2a4aa..83057d139f 100644 --- a/src/Avalonia.Controls/ComboBoxItem.cs +++ b/src/Avalonia.Controls/ComboBoxItem.cs @@ -1,5 +1,7 @@ using System; using System.Reactive.Linq; +using Avalonia.Automation; +using Avalonia.Automation.Peers; namespace Avalonia.Controls { @@ -13,5 +15,10 @@ namespace Avalonia.Controls this.GetObservable(ComboBoxItem.IsFocusedProperty).Where(focused => focused) .Subscribe(_ => (Parent as ComboBox)?.ItemFocused(this)); } + + static ComboBoxItem() + { + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.ComboBoxItem); + } } } diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index ac7d24be92..b8a45e102f 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -1,4 +1,5 @@ using Avalonia.Collections; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; @@ -12,6 +13,7 @@ namespace Avalonia.Controls /// /// Displays according to a . /// + [TemplatePart("PART_ContentPresenter", typeof(IContentPresenter))] public class ContentControl : TemplatedControl, IContentControl, IContentPresenterHost { /// diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index dff06a6369..bc5195ff6c 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using Avalonia.Automation.Peers; using System.Linq; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Generators; @@ -13,6 +14,7 @@ using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Styling; +using Avalonia.Automation; namespace Avalonia.Controls { @@ -107,6 +109,8 @@ namespace Avalonia.Controls ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); PlacementModeProperty.OverrideDefaultValue(PlacementMode.Pointer); ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); + AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue(AccessibilityView.Control); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Menu); } /// @@ -347,6 +351,11 @@ namespace Avalonia.Controls ? PlacementMode.Bottom : PlacementMode; + //Position of the line below is really important. + //All styles are being applied only when control has logical parent. + //Line below will add ContextMenu as child to the Popup and this will trigger styles and they would be applied. + //If you will move line below somewhere else it may cause that ContextMenu will behave differently from what you are expecting. + _popup.Child = this; _popup.PlacementTarget = placementTarget; _popup.HorizontalOffset = HorizontalOffset; _popup.VerticalOffset = VerticalOffset; @@ -355,7 +364,6 @@ namespace Avalonia.Controls _popup.PlacementGravity = PlacementGravity; _popup.PlacementRect = PlacementRect; _popup.WindowManagerAddShadowHint = WindowManagerAddShadowHint; - _popup.Child = this; IsOpen = true; _popup.IsOpen = true; diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index b9f3bd8890..2c696c8f74 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; -using System.Runtime.CompilerServices; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Documents; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; @@ -66,9 +67,10 @@ namespace Avalonia.Controls /// public static readonly AttachedProperty FlowDirectionProperty = AvaloniaProperty.RegisterAttached(nameof(FlowDirection), inherits: true); - + private DataTemplates? _dataTemplates; private IControl? _focusAdorner; + private AutomationPeer? _automationPeer; /// /// Gets or sets the control's focus adorner. @@ -135,9 +137,39 @@ namespace Avalonia.Controls public new IControl? Parent => (IControl?)base.Parent; + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The flow direction. + public static FlowDirection GetFlowDirection(Control control) + { + return control.GetValue(FlowDirectionProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFlowDirection(Control control, FlowDirection value) + { + control.SetValue(FlowDirectionProperty, value); + } + /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; + /// + /// Gets a value indicating whether control bypass FlowDirecton policies. + /// + /// + /// Related to FlowDirection system and returns false as default, so if + /// is RTL then control will get a mirror presentation. + /// For controls that want to avoid this behavior, override this property and return true. + /// + protected virtual bool BypassFlowDirectionPolicies => false; + /// void ISetterValue.Initialize(ISetter setter) { @@ -197,6 +229,14 @@ namespace Avalonia.Controls base.OnDetachedFromVisualTreeCore(e); } + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + InvalidateMirrorTransform(); + } + /// protected override void OnGotFocus(GotFocusEventArgs e) { @@ -242,6 +282,24 @@ namespace Avalonia.Controls } } + protected virtual AutomationPeer OnCreateAutomationPeer() + { + return new NoneAutomationPeer(this); + } + + internal AutomationPeer GetOrCreateAutomationPeer() + { + VerifyAccess(); + + if (_automationPeer is object) + { + return _automationPeer; + } + + _automationPeer = OnCreateAutomationPeer(); + return _automationPeer; + } + protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); @@ -289,5 +347,55 @@ namespace Avalonia.Controls } } } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == FlowDirectionProperty) + { + InvalidateMirrorTransform(); + + foreach (var visual in VisualChildren) + { + if (visual is Control child) + { + child.InvalidateMirrorTransform(); + } + } + } + } + + /// + /// Computes the value according to the + /// and + /// + public virtual void InvalidateMirrorTransform() + { + var flowDirection = this.FlowDirection; + var parentFlowDirection = FlowDirection.LeftToRight; + + bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies; + bool parentBypassFlowDirectionPolicies = false; + + var parent = this.FindAncestorOfType(); + if (parent != null) + { + parentFlowDirection = parent.FlowDirection; + parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies; + } + else if (Parent is Control logicalParent) + { + parentFlowDirection = logicalParent.FlowDirection; + parentBypassFlowDirectionPolicies = logicalParent.BypassFlowDirectionPolicies; + } + + bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies; + bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies; + + bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored; + + HasMirrorTransform = shouldApplyMirrorTransform; + } } } diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index 8c6ac17280..f2b808fe0d 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Interactivity; +using Avalonia.Layout; using System; using System.Collections.Generic; using System.Globalization; @@ -13,6 +14,15 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a date /// + [TemplatePart("ButtonContentGrid", typeof(Grid))] + [TemplatePart("DayText", typeof(TextBlock))] + [TemplatePart("FirstSpacer", typeof(Rectangle))] + [TemplatePart("FlyoutButton", typeof(Button))] + [TemplatePart("MonthText", typeof(TextBlock))] + [TemplatePart("PickerPresenter", typeof(DatePickerPresenter))] + [TemplatePart("Popup", typeof(Popup))] + [TemplatePart("SecondSpacer", typeof(Rectangle))] + [TemplatePart("YearText", typeof(TextBlock))] [PseudoClasses(":hasnodate")] public class DatePicker : TemplatedControl { @@ -398,18 +408,27 @@ namespace Avalonia.Controls private void OnFlyoutButtonClicked(object? sender, RoutedEventArgs e) { if (_presenter == null) - throw new InvalidOperationException("No DatePickerPresenter found"); + throw new InvalidOperationException("No DatePickerPresenter found."); + if (_popup == null) + throw new InvalidOperationException("No Popup found."); _presenter.Date = SelectedDate ?? DateTimeOffset.Now; - _popup!.IsOpen = true; + _popup.PlacementMode = PlacementMode.AnchorAndGravity; + _popup.PlacementAnchor = Primitives.PopupPositioning.PopupAnchor.Bottom; + _popup.PlacementGravity = Primitives.PopupPositioning.PopupGravity.Bottom; + _popup.PlacementConstraintAdjustment = Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; + _popup.IsOpen = true; + + // Overlay popup hosts won't get measured until the next layout pass, but we need the + // template to be applied to `_presenter` now. Detect this case and force a layout pass. + if (!_presenter.IsMeasureValid) + (VisualRoot as ILayoutRoot)?.LayoutManager?.ExecuteInitialLayoutPass(); var deltaY = _presenter.GetOffsetForPopup(); // The extra 5 px I think is related to default popup placement behavior - _popup!.Host!.ConfigurePosition(_popup.PlacementTarget!, PlacementMode.AnchorAndGravity, new Point(0, deltaY + 5), - Primitives.PopupPositioning.PopupAnchor.Bottom, Primitives.PopupPositioning.PopupGravity.Bottom, - Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY); + _popup.VerticalOffset = deltaY + 5; } protected virtual void OnSelectedDateChanged(object? sender, DatePickerSelectedValueChangedEventArgs e) diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs index eac6d83074..0612efe14d 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.Interactivity; @@ -12,6 +13,23 @@ namespace Avalonia.Controls /// Defines the presenter used for selecting a date for a /// /// + [TemplatePart("AcceptButton", typeof(Button))] + [TemplatePart("DayDownButton", typeof(RepeatButton))] + [TemplatePart("DayHost", typeof(Panel))] + [TemplatePart("DaySelector", typeof(DateTimePickerPanel))] + [TemplatePart("DayUpButton", typeof(RepeatButton))] + [TemplatePart("DismissButton", typeof(Button))] + [TemplatePart("FirstSpacer", typeof(Rectangle))] + [TemplatePart("MonthDownButton", typeof(RepeatButton))] + [TemplatePart("MonthHost", typeof(Panel))] + [TemplatePart("MonthSelector", typeof(DateTimePickerPanel))] + [TemplatePart("MonthUpButton", typeof(RepeatButton))] + [TemplatePart("PickerContainer", typeof(Grid))] + [TemplatePart("SecondSpacer", typeof(Rectangle))] + [TemplatePart("YearDownButton", typeof(RepeatButton))] + [TemplatePart("YearHost", typeof(Panel))] + [TemplatePart("YearSelector", typeof(DateTimePickerPanel))] + [TemplatePart("YearUpButton", typeof(RepeatButton))] public class DatePickerPresenter : PickerPresenterBase { /// @@ -537,8 +555,11 @@ namespace Avalonia.Controls internal double GetOffsetForPopup() { + if (_monthSelector is null) + return 0; + var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41; - return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_monthSelector!.ItemHeight / 2); + return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_monthSelector.ItemHeight / 2); } } } diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs index 10b7f9bdf9..667f994a1d 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs @@ -1,7 +1,11 @@ using System; using System.Globalization; using System.Linq; +using Avalonia.Controls.Presenters; using Avalonia.Input; +using Avalonia.Input.GestureRecognizers; +using Avalonia.Interactivity; +using Avalonia.Media; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -58,18 +62,18 @@ namespace Avalonia.Controls.Primitives private Vector _offset; private bool _hasInit; private bool _suppressUpdateOffset; - private ListBoxItem? _pressedItem; + private ScrollContentPresenter? _parentScroller; public DateTimePickerPanel() { FormatDate = DateTime.Now; - AddHandler(ListBoxItem.PointerPressedEvent, OnItemPointerDown, Avalonia.Interactivity.RoutingStrategies.Bubble); - AddHandler(ListBoxItem.PointerReleasedEvent, OnItemPointerUp, Avalonia.Interactivity.RoutingStrategies.Bubble); + AddHandler(TappedEvent, OnItemTapped, RoutingStrategies.Bubble); } static DateTimePickerPanel() { FocusableProperty.OverrideDefaultValue(true); + BackgroundProperty.OverrideDefaultValue(Brushes.Transparent); AffectsMeasure(ItemHeightProperty); } @@ -254,6 +258,8 @@ namespace Avalonia.Controls.Primitives _suppressUpdateOffset = true; SelectedValue = (int)newSel * Increment + MinimumValue; _suppressUpdateOffset = false; + + System.Diagnostics.Debug.WriteLine($"Offset: {_offset} ItemHeight: {ItemHeight}"); } } @@ -269,7 +275,7 @@ namespace Avalonia.Controls.Primitives public Size Extent => _extent; - public Size Viewport => new Size(0, ItemHeight); + public Size Viewport => Bounds.Size; public event EventHandler? ScrollInvalidated; @@ -340,6 +346,20 @@ namespace Avalonia.Controls.Primitives return finalSize; } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _parentScroller = this.GetVisualParent() as ScrollContentPresenter; + _parentScroller?.AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _parentScroller?.RemoveHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + _parentScroller = null; + } + protected override void OnKeyDown(KeyEventArgs e) { switch (e.Key) @@ -523,26 +543,13 @@ namespace Avalonia.Controls.Primitives return newValue; } - private void OnItemPointerDown(object? sender, PointerPressedEventArgs e) + private void OnItemTapped(object? sender, TappedEventArgs e) { - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && - e.Source is IVisual source) - { - _pressedItem = GetItemFromSource(source); - e.Handled = true; - } - } - - private void OnItemPointerUp(object? sender, PointerReleasedEventArgs e) - { - if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonReleased && - _pressedItem != null && - e.Source is IVisual source && - GetItemFromSource(source) is ListBoxItem item && - item.Tag is int tag) + if (e.Source is IVisual source && + GetItemFromSource(source) is ListBoxItem listBoxItem && + listBoxItem.Tag is int tag) { SelectedValue = tag; - _pressedItem = null; e.Handled = true; } } @@ -560,11 +567,21 @@ namespace Avalonia.Controls.Primitives public bool BringIntoView(IControl target, Rect targetRect) { return false; } - public IControl? GetControlInDirection(NavigationDirection direction, IControl from) { return null; } + public IControl? GetControlInDirection(NavigationDirection direction, IControl? from) { return null; } public void RaiseScrollInvalidated(EventArgs e) { ScrollInvalidated?.Invoke(this, e); } + + private void OnScrollGestureEnded(object? sender, ScrollGestureEndedEventArgs e) + { + var snapY = Math.Round(Offset.Y / ItemHeight) * ItemHeight; + + if (snapY != Offset.Y) + { + Offset = Offset.WithY(snapY); + } + } } } diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index a0a27bd4ed..f04c79505e 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Layout; using System; using System.Globalization; @@ -11,6 +12,18 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a time. /// + [TemplatePart("FirstColumnDivider", typeof(Rectangle))] + [TemplatePart("FirstPickerHost", typeof(Border))] + [TemplatePart("FlyoutButton", typeof(Button))] + [TemplatePart("FlyoutButtonContentGrid", typeof(Grid))] + [TemplatePart("HourTextBlock", typeof(TextBlock))] + [TemplatePart("MinuteTextBlock", typeof(TextBlock))] + [TemplatePart("PeriodTextBlock", typeof(TextBlock))] + [TemplatePart("PickerPresenter", typeof(TimePickerPresenter))] + [TemplatePart("Popup", typeof(Popup))] + [TemplatePart("SecondColumnDivider", typeof(Rectangle))] + [TemplatePart("SecondPickerHost", typeof(Border))] + [TemplatePart("ThirdPickerHost", typeof(Border))] [PseudoClasses(":hasnotime")] public class TimePicker : TemplatedControl { @@ -254,16 +267,28 @@ namespace Avalonia.Controls private void OnFlyoutButtonClicked(object? sender, Interactivity.RoutedEventArgs e) { - _presenter!.Time = SelectedTime ?? DateTime.Now.TimeOfDay; + if (_presenter == null) + throw new InvalidOperationException("No DatePickerPresenter found."); + if (_popup == null) + throw new InvalidOperationException("No Popup found."); - _popup!.IsOpen = true; + _presenter.Time = SelectedTime ?? DateTime.Now.TimeOfDay; + + _popup.PlacementMode = PlacementMode.AnchorAndGravity; + _popup.PlacementAnchor = Primitives.PopupPositioning.PopupAnchor.Bottom; + _popup.PlacementGravity = Primitives.PopupPositioning.PopupGravity.Bottom; + _popup.PlacementConstraintAdjustment = Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; + _popup.IsOpen = true; + + // Overlay popup hosts won't get measured until the next layout pass, but we need the + // template to be applied to `_presenter` now. Detect this case and force a layout pass. + if (!_presenter.IsMeasureValid) + (VisualRoot as ILayoutRoot)?.LayoutManager?.ExecuteInitialLayoutPass(); var deltaY = _presenter.GetOffsetForPopup(); // The extra 5 px I think is related to default popup placement behavior - _popup.Host!.ConfigurePosition(_popup.PlacementTarget!, PlacementMode.AnchorAndGravity, new Point(0, deltaY + 5), - Primitives.PopupPositioning.PopupAnchor.Bottom, Primitives.PopupPositioning.PopupGravity.Bottom, - Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY); + _popup.VerticalOffset = deltaY + 5; } private void OnDismissPicker(object? sender, EventArgs e) diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs index 55bc1af3a7..7f2abb7e98 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.Interactivity; @@ -10,6 +11,20 @@ namespace Avalonia.Controls /// Defines the presenter used for selecting a time. Intended for use with /// but can be used independently /// + [TemplatePart("AcceptButton", typeof(Button))] + [TemplatePart("DismissButton", typeof(Button))] + [TemplatePart("HourDownButton", typeof(RepeatButton))] + [TemplatePart("HourSelector", typeof(DateTimePickerPanel))] + [TemplatePart("HourUpButton", typeof(RepeatButton))] + [TemplatePart("MinuteDownButton", typeof(RepeatButton))] + [TemplatePart("MinuteSelector", typeof(DateTimePickerPanel))] + [TemplatePart("MinuteUpButton", typeof(RepeatButton))] + [TemplatePart("PeriodDownButton", typeof(RepeatButton))] + [TemplatePart("PeriodHost", typeof(Panel))] + [TemplatePart("PeriodSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PeriodUpButton", typeof(RepeatButton))] + [TemplatePart("PickerContainer", typeof(Grid))] + [TemplatePart("SecondSpacer", typeof(Rectangle))] public class TimePickerPresenter : PickerPresenterBase { /// @@ -256,8 +271,11 @@ namespace Avalonia.Controls internal double GetOffsetForPopup() { + if (_hourSelector is null) + return 0; + var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41; - return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_hourSelector!.ItemHeight / 2); + return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_hourSelector.ItemHeight / 2); } } } diff --git a/src/Avalonia.Controls/Documents/Bold.cs b/src/Avalonia.Controls/Documents/Bold.cs new file mode 100644 index 0000000000..7d0a9130ae --- /dev/null +++ b/src/Avalonia.Controls/Documents/Bold.cs @@ -0,0 +1,17 @@ +using Avalonia.Media; + +namespace Avalonia.Controls.Documents +{ + /// + /// Bold element - markup helper for indicating bolded content. + /// Equivalent to a Span with FontWeight property set to FontWeights.Bold. + /// Can contain other inline elements. + /// + public sealed class Bold : Span + { + static Bold() + { + FontWeightProperty.OverrideDefaultValue(FontWeight.Bold); + } + } +} diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs new file mode 100644 index 0000000000..5b63f95432 --- /dev/null +++ b/src/Avalonia.Controls/Documents/Inline.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Text; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Documents +{ + /// + /// Inline element. + /// + public abstract class Inline : TextElement + { + /// + /// AvaloniaProperty for property. + /// + public static readonly StyledProperty TextDecorationsProperty = + AvaloniaProperty.Register( + nameof(TextDecorations)); + + /// + /// AvaloniaProperty for property. + /// + public static readonly StyledProperty BaselineAlignmentProperty = + AvaloniaProperty.Register( + nameof(BaselineAlignment), + BaselineAlignment.Baseline); + + /// + /// The TextDecorations property specifies decorations that are added to the text of an element. + /// + public TextDecorationCollection TextDecorations + { + get { return GetValue(TextDecorationsProperty); } + set { SetValue(TextDecorationsProperty, value); } + } + + /// + /// Describes how the baseline for a text-based element is positioned on the vertical axis, + /// relative to the established baseline for text. + /// + public BaselineAlignment BaselineAlignment + { + get { return GetValue(BaselineAlignmentProperty); } + set { SetValue(BaselineAlignmentProperty, value); } + } + + internal abstract int BuildRun(StringBuilder stringBuilder, IList> textStyleOverrides, int firstCharacterIndex); + + internal abstract int AppendText(StringBuilder stringBuilder); + + protected TextRunProperties CreateTextRunProperties() + { + return new GenericTextRunProperties(new Typeface(FontFamily, FontStyle, FontWeight), FontSize, + TextDecorations, Foreground, Background, BaselineAlignment); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + switch (change.Property.Name) + { + case nameof(TextDecorations): + case nameof(BaselineAlignment): + Invalidate(); + break; + } + } + } +} diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs new file mode 100644 index 0000000000..45c715c13a --- /dev/null +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -0,0 +1,123 @@ +using System; +using System.Text; +using Avalonia.Collections; +using Avalonia.LogicalTree; +using Avalonia.Metadata; + +namespace Avalonia.Controls.Documents +{ + /// + /// A collection of s. + /// + [WhitespaceSignificantCollection] + public class InlineCollection : AvaloniaList + { + private string? _text = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + public InlineCollection(ILogical parent) : base(0) + { + ResetBehavior = ResetBehavior.Remove; + + this.ForEachItem( + x => + { + ((ISetLogicalParent)x).SetParent(parent); + x.Invalidated += Invalidate; + Invalidate(); + }, + x => + { + ((ISetLogicalParent)x).SetParent(null); + x.Invalidated -= Invalidate; + Invalidate(); + }, + () => throw new NotSupportedException()); + } + + public bool HasComplexContent => Count > 0; + + /// + /// Gets or adds the text held by the inlines collection. + /// + /// Can be null for complex content. + /// + /// + public string? Text + { + get + { + if (!HasComplexContent) + { + return _text; + } + + var builder = new StringBuilder(); + + foreach(var inline in this) + { + inline.AppendText(builder); + } + + return builder.ToString(); + } + set + { + if (HasComplexContent) + { + Add(new Run(value)); + } + else + { + _text = value; + } + } + } + + /// + /// Add a text segment to the collection. + /// + /// For non complex content this appends the text to the end of currently held text. + /// For complex content this adds a to the collection. + /// + /// + /// + public void Add(string text) + { + if (HasComplexContent) + { + Add(new Run(text)); + } + else + { + _text += text; + } + } + + public override void Add(Inline item) + { + if (!HasComplexContent) + { + base.Add(new Run(_text)); + + _text = string.Empty; + } + + base.Add(item); + } + + /// + /// Raised when an inline in the collection changes. + /// + public event EventHandler? Invalidated; + + /// + /// Raises the event. + /// + protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty); + + private void Invalidate(object? sender, EventArgs e) => Invalidate(); + } +} diff --git a/src/Avalonia.Controls/Documents/Italic.cs b/src/Avalonia.Controls/Documents/Italic.cs new file mode 100644 index 0000000000..e9f4698fc4 --- /dev/null +++ b/src/Avalonia.Controls/Documents/Italic.cs @@ -0,0 +1,17 @@ +using Avalonia.Media; + +namespace Avalonia.Controls.Documents +{ + /// + /// Italic element - markup helper for indicating italicized content. + /// Equivalent to a Span with FontStyle property set to FontStyles.Italic. + /// Can contain other inline elements. + /// + public sealed class Italic : Span + { + static Italic() + { + FontStyleProperty.OverrideDefaultValue(FontStyle.Italic); + } + } +} diff --git a/src/Avalonia.Controls/Documents/LineBreak.cs b/src/Avalonia.Controls/Documents/LineBreak.cs new file mode 100644 index 0000000000..5e0cd1d387 --- /dev/null +++ b/src/Avalonia.Controls/Documents/LineBreak.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Media.TextFormatting; +using Avalonia.Metadata; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Documents +{ + /// + /// LineBreak element that forces a line breaking. + /// + [TrimSurroundingWhitespace] + public class LineBreak : Inline + { + /// + /// Creates a new LineBreak instance. + /// + public LineBreak() + { + } + + internal override int BuildRun(StringBuilder stringBuilder, + IList> textStyleOverrides, int firstCharacterIndex) + { + var length = AppendText(stringBuilder); + + textStyleOverrides.Add(new ValueSpan(firstCharacterIndex, length, + CreateTextRunProperties())); + + return length; + } + + internal override int AppendText(StringBuilder stringBuilder) + { + var text = Environment.NewLine; + + stringBuilder.Append(text); + + return text.Length; + } + } +} + diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs new file mode 100644 index 0000000000..a7dd5fd94f --- /dev/null +++ b/src/Avalonia.Controls/Documents/Run.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Data; +using Avalonia.Media.TextFormatting; +using Avalonia.Metadata; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Documents +{ + /// + /// A terminal element in text flow hierarchy - contains a uniformatted run of unicode characters + /// + public class Run : Inline + { + /// + /// Initializes an instance of Run class. + /// + public Run() + { + } + + /// + /// Initializes an instance of Run class specifying its text content. + /// + /// + /// Text content assigned to the Run. + /// + public Run(string? text) + { + Text = text; + } + + /// + /// Dependency property backing Text. + /// + /// + /// Note that when a TextRange that intersects with this Run gets modified (e.g. by editing + /// a selection in RichTextBox), we will get two changes to this property since we delete + /// and then insert when setting the content of a TextRange. + /// + public static readonly StyledProperty TextProperty = AvaloniaProperty.Register ( + nameof (Text), defaultBindingMode: BindingMode.TwoWay); + + /// + /// The content spanned by this TextElement. + /// + [Content] + public string? Text { + get { return GetValue (TextProperty); } + set { SetValue (TextProperty, value); } + } + + internal override int BuildRun(StringBuilder stringBuilder, + IList> textStyleOverrides, int firstCharacterIndex) + { + var length = AppendText(stringBuilder); + + textStyleOverrides.Add(new ValueSpan(firstCharacterIndex, length, + CreateTextRunProperties())); + + return length; + } + + internal override int AppendText(StringBuilder stringBuilder) + { + var text = Text ?? ""; + + stringBuilder.Append(text); + + return text.Length; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + switch (change.Property.Name) + { + case nameof(Text): + Invalidate(); + break; + } + } + } +} diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs new file mode 100644 index 0000000000..c086997b07 --- /dev/null +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Text; +using Avalonia.Media.TextFormatting; +using Avalonia.Metadata; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Documents +{ + /// + /// Span element used for grouping other Inline elements. + /// + public class Span : Inline + { + /// + /// Defines the property. + /// + public static readonly DirectProperty InlinesProperty = + AvaloniaProperty.RegisterDirect( + nameof(Inlines), + o => o.Inlines); + + /// + /// Initializes a new instance of a Span element. + /// + public Span() + { + Inlines = new InlineCollection(this); + + Inlines.Invalidated += (s, e) => Invalidate(); + } + + /// + /// Gets or sets the inlines. + /// + [Content] + public InlineCollection Inlines { get; } + + internal override int BuildRun(StringBuilder stringBuilder, IList> textStyleOverrides, int firstCharacterIndex) + { + var length = 0; + + if (Inlines.HasComplexContent) + { + foreach (var inline in Inlines) + { + var inlineLength = inline.BuildRun(stringBuilder, textStyleOverrides, firstCharacterIndex); + + firstCharacterIndex += inlineLength; + + length += inlineLength; + } + } + else + { + if (Inlines.Text == null) + { + return length; + } + + stringBuilder.Append(Inlines.Text); + + length = Inlines.Text.Length; + + textStyleOverrides.Add(new ValueSpan(firstCharacterIndex, length, + CreateTextRunProperties())); + } + + return length; + } + + internal override int AppendText(StringBuilder stringBuilder) + { + if (Inlines.HasComplexContent) + { + var length = 0; + + foreach (var inline in Inlines) + { + length += inline.AppendText(stringBuilder); + } + + return length; + } + + if (Inlines.Text == null) + { + return 0; + } + + stringBuilder.Append(Inlines.Text); + + return Inlines.Text.Length; + } + } +} diff --git a/src/Avalonia.Controls/Documents/TextElement.cs b/src/Avalonia.Controls/Documents/TextElement.cs new file mode 100644 index 0000000000..d8e13554b5 --- /dev/null +++ b/src/Avalonia.Controls/Documents/TextElement.cs @@ -0,0 +1,282 @@ +using System; +using Avalonia.Media; + +namespace Avalonia.Controls.Documents +{ + /// + /// TextElement is an base class for content in text based controls. + /// TextElements span other content, applying property values or providing structural information. + /// + public abstract class TextElement : StyledElement + { + /// + /// Defines the property. + /// + public static readonly StyledProperty BackgroundProperty = + Border.BackgroundProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontFamilyProperty = + AvaloniaProperty.RegisterAttached( + nameof(FontFamily), + defaultValue: FontFamily.Default, + inherits: true); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontSizeProperty = + AvaloniaProperty.RegisterAttached( + nameof(FontSize), + defaultValue: 12, + inherits: true); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontStyleProperty = + AvaloniaProperty.RegisterAttached( + nameof(FontStyle), + inherits: true); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontWeightProperty = + AvaloniaProperty.RegisterAttached( + nameof(FontWeight), + inherits: true, + defaultValue: FontWeight.Normal); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontStretchProperty = + AvaloniaProperty.RegisterAttached( + nameof(FontStretch), + inherits: true, + defaultValue: FontStretch.Normal); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty ForegroundProperty = + AvaloniaProperty.RegisterAttached( + nameof(Foreground), + Brushes.Black, + inherits: true); + + /// + /// Gets or sets a brush used to paint the control's background. + /// + public IBrush? Background + { + get { return GetValue(BackgroundProperty); } + set { SetValue(BackgroundProperty, value); } + } + + /// + /// Gets or sets the font family. + /// + public FontFamily FontFamily + { + get { return GetValue(FontFamilyProperty); } + set { SetValue(FontFamilyProperty, value); } + } + + /// + /// Gets or sets the font size. + /// + public double FontSize + { + get { return GetValue(FontSizeProperty); } + set { SetValue(FontSizeProperty, value); } + } + + /// + /// Gets or sets the font style. + /// + public FontStyle FontStyle + { + get { return GetValue(FontStyleProperty); } + set { SetValue(FontStyleProperty, value); } + } + + /// + /// Gets or sets the font weight. + /// + public FontWeight FontWeight + { + get { return GetValue(FontWeightProperty); } + set { SetValue(FontWeightProperty, value); } + } + + /// + /// Gets or sets the font stretch. + /// + public FontStretch FontStretch + { + get { return GetValue(FontStretchProperty); } + set { SetValue(FontStretchProperty, value); } + } + + /// + /// Gets or sets a brush used to paint the text. + /// + public IBrush? Foreground + { + get { return GetValue(ForegroundProperty); } + set { SetValue(ForegroundProperty, value); } + } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font family. + public static FontFamily GetFontFamily(Control control) + { + return control.GetValue(FontFamilyProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFontFamily(Control control, FontFamily value) + { + control.SetValue(FontFamilyProperty, value); + } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font size. + public static double GetFontSize(Control control) + { + return control.GetValue(FontSizeProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFontSize(Control control, double value) + { + control.SetValue(FontSizeProperty, value); + } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font style. + public static FontStyle GetFontStyle(Control control) + { + return control.GetValue(FontStyleProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFontStyle(Control control, FontStyle value) + { + control.SetValue(FontStyleProperty, value); + } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font weight. + public static FontWeight GetFontWeight(Control control) + { + return control.GetValue(FontWeightProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFontWeight(Control control, FontWeight value) + { + control.SetValue(FontWeightProperty, value); + } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font stretch. + public static FontStretch GetFontStretch(Control control) + { + return control.GetValue(FontStretchProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFontStretch(Control control, FontStretch value) + { + control.SetValue(FontStretchProperty, value); + } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The foreground. + public static IBrush? GetForeground(Control control) + { + return control.GetValue(ForegroundProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetForeground(Control control, IBrush? value) + { + control.SetValue(ForegroundProperty, value); + } + + /// + /// Raised when the visual representation of the text element changes. + /// + public event EventHandler? Invalidated; + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + switch (change.Property.Name) + { + case nameof(Background): + case nameof(FontFamily): + case nameof(FontSize): + case nameof(FontStyle): + case nameof(FontWeight): + case nameof(FontStretch): + case nameof(Foreground): + Invalidate(); + break; + } + } + + /// + /// Raises the event. + /// + protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty); + } +} diff --git a/src/Avalonia.Controls/Documents/Underline.cs b/src/Avalonia.Controls/Documents/Underline.cs new file mode 100644 index 0000000000..fcd46c8439 --- /dev/null +++ b/src/Avalonia.Controls/Documents/Underline.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Controls.Documents +{ + /// + /// Underline element - markup helper for indicating superscript content. + /// Equivalent to a Span with TextDecorations property set to TextDecorations.Underlined. + /// Can contain other inline elements. + /// + public sealed class Underline : Span + { + static Underline() + { + TextDecorationsProperty.OverrideDefaultValue(Media.TextDecorations.Underline); + } + } +} diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs deleted file mode 100644 index 4e17f5bff5..0000000000 --- a/src/Avalonia.Controls/DropDown.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Avalonia.Logging; -using Avalonia.Styling; - -namespace Avalonia.Controls -{ - [Obsolete("Use ComboBox")] - public class DropDown : ComboBox, IStyleable - { - public DropDown() - { - Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "DropDown is deprecated: Use ComboBox"); - } - - Type IStyleable.StyleKey => typeof(ComboBox); - } - - [Obsolete("Use ComboBoxItem")] - public class DropDownItem : ComboBoxItem, IStyleable - { - public DropDownItem() - { - Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "DropDownItem is deprecated: Use ComboBoxItem"); - } - - Type IStyleable.StyleKey => typeof(ComboBoxItem); - } -} diff --git a/src/Avalonia.Controls/DropDownButton.cs b/src/Avalonia.Controls/DropDownButton.cs new file mode 100644 index 0000000000..8dc40f42af --- /dev/null +++ b/src/Avalonia.Controls/DropDownButton.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Controls +{ + /// + /// A button with an added drop-down chevron to visually indicate it has a flyout with additional actions. + /// + public class DropDownButton : Button + { + /// + /// Initializes a new instance of the class. + /// + public DropDownButton() + { + } + } +} diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs index bcd859100a..6a7da87387 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs @@ -8,15 +8,6 @@ namespace Avalonia.Controls { public class MenuFlyoutPresenter : MenuBase { - public static readonly StyledProperty CornerRadiusProperty = - Border.CornerRadiusProperty.AddOwner(); - - public CornerRadius CornerRadius - { - get => GetValue(CornerRadiusProperty); - set => SetValue(CornerRadiusProperty, value); - } - public MenuFlyoutPresenter() :base(new DefaultMenuInteractionHandler(true)) { diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index dd4c6561c2..70d9dbec08 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -97,6 +97,6 @@ namespace Avalonia.Controls.Generators /// /// The container. /// The index of the container, or -1 if not found. - int IndexFromContainer(IControl container); + int IndexFromContainer(IControl? container); } } diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index d02eaffeb2..a76dcbe9c8 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -82,7 +82,7 @@ namespace Avalonia.Controls.Generators { var toMove = _containers.Where(x => x.Key >= index) .OrderByDescending(x => x.Key) - .ToList(); + .ToArray(); foreach (var i in toMove) { @@ -111,7 +111,7 @@ namespace Avalonia.Controls.Generators } var toMove = _containers.Where(x => x.Key >= startingIndex) - .OrderBy(x => x.Key).ToList(); + .OrderBy(x => x.Key).ToArray(); foreach (var i in toMove) { @@ -122,9 +122,9 @@ namespace Avalonia.Controls.Generators Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); - if (toMove.Count > 0) + if (toMove.Length > 0) { - var containers = toMove.Select(x => x.Value).ToList(); + var containers = toMove.Select(x => x.Value).ToArray(); Recycled?.Invoke(this, new ItemContainerEventArgs(containers[0].Index, containers)); } } @@ -138,10 +138,10 @@ namespace Avalonia.Controls.Generators /// public virtual IEnumerable Clear() { - var result = Containers.ToList(); + var result = Containers.ToArray(); _containers.Clear(); - if (result.Count > 0) + if (result.Length > 0) { Dematerialized?.Invoke(this, new ItemContainerEventArgs(0, result)); } @@ -158,7 +158,7 @@ namespace Avalonia.Controls.Generators } /// - public int IndexFromContainer(IControl container) + public int IndexFromContainer(IControl? container) { foreach (var i in _containers) { diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index d83b996aa0..bde1da509b 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -12,21 +12,61 @@ namespace Avalonia.Controls class HotkeyCommandWrapper : ICommand { - public HotkeyCommandWrapper(ICommandSource? control) + readonly WeakReference reference; + + public HotkeyCommandWrapper(IControl control) { - CommandSource = control; + reference = new WeakReference(control); } - public readonly ICommandSource? CommandSource; + public ICommand? GetCommand() + { + if (reference.Target is { } target) + { + if (target is ICommandSource commandSource && commandSource.Command is { } command) + { + return command; + } + else if (target is IClickableControl { }) + { + return this; + } + } + return null; + } - private ICommand? GetCommand() => CommandSource?.Command; + public bool CanExecute(object? parameter) + { + if (reference.Target is { } target) + { + if (target is ICommandSource commandSource && commandSource.Command is { } command) + { + return commandSource.IsEffectivelyEnabled + && command.CanExecute(commandSource.CommandParameter) == true; + } + else if (target is IClickableControl clickable) + { + return clickable.IsEffectivelyEnabled; + } + } + return false; + } - public bool CanExecute(object? parameter) => - CommandSource?.Command?.CanExecute(CommandSource.CommandParameter) == true - && CommandSource.IsEffectivelyEnabled; + public void Execute(object? parameter) + { + if (reference.Target is { } target) + { + if (target is ICommandSource commandSource && commandSource.Command is { } command) + { + command.Execute(commandSource.CommandParameter); + } + else if (target is IClickableControl { IsEffectivelyEnabled: true } clickable) + { + clickable.RaiseClick(); + } + } + } - public void Execute(object? parameter) => - GetCommand()?.Execute(CommandSource?.CommandParameter); #pragma warning disable 67 // Event not used public event EventHandler? CanExecuteChanged; @@ -47,7 +87,7 @@ namespace Avalonia.Controls public Manager(IControl control) { _control = control; - _wrapper = new HotkeyCommandWrapper(_control as ICommandSource); + _wrapper = new HotkeyCommandWrapper(_control); } public void Init() @@ -104,13 +144,14 @@ namespace Avalonia.Controls { HotKeyProperty.Changed.Subscribe(args => { - if (args.NewValue.Value is null) return; + if (args.NewValue.Value is null) + return; var control = args.Sender as IControl; - if (control is not ICommandSource) + if (control is not IClickableControl) { Logging.Logger.TryGet(Logging.LogEventLevel.Warning, Logging.LogArea.Control)?.Log(control, - $"The element {args.Sender.GetType().Name} does not implement ICommandSource and does not support binding a HotKey ({args.NewValue})."); + $"The element {args.Sender.GetType().Name} does not implement IClickableControl and does not support binding a HotKey ({args.NewValue})."); return; } diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index c448729643..7408bff902 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -1,3 +1,5 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Metadata; @@ -33,6 +35,7 @@ namespace Avalonia.Controls { AffectsRender(SourceProperty, StretchProperty, StretchDirectionProperty); AffectsMeasure(SourceProperty, StretchProperty, StretchDirectionProperty); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Image); } /// @@ -63,6 +66,8 @@ namespace Avalonia.Controls set { SetValue(StretchDirectionProperty, value); } } + protected override bool BypassFlowDirectionPolicies => true; + /// /// Renders the control. /// diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 70f771ca00..ab236f703d 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; +using Avalonia.Automation.Peers; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; @@ -79,7 +80,7 @@ namespace Avalonia.Controls /// /// Gets the for the control. /// - public IItemContainerGenerator? ItemContainerGenerator + public IItemContainerGenerator ItemContainerGenerator { get { @@ -87,13 +88,10 @@ namespace Avalonia.Controls { _itemContainerGenerator = CreateItemContainerGenerator(); - if (_itemContainerGenerator != null) - { - _itemContainerGenerator.ItemTemplate = ItemTemplate; - _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); - _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); - _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e); - } + _itemContainerGenerator.ItemTemplate = ItemTemplate; + _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); + _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); + _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e); } return _itemContainerGenerator; @@ -146,6 +144,8 @@ namespace Avalonia.Controls protected set; } + private protected bool WrapFocus { get; set; } + event EventHandler? IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -166,7 +166,7 @@ namespace Avalonia.Controls if (Presenter is IChildIndexProvider innerProvider) { innerProvider.ChildIndexChanged += PresenterChildIndexChanged; - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); } } @@ -240,14 +240,8 @@ namespace Avalonia.Controls /// Creates the for the control. /// /// - /// An or null. + /// An . /// - /// - /// Certain controls such as don't actually create item - /// containers; however they want it to be ItemsControls so that they have an Items - /// property etc. In this case, a derived class can override this method to return null - /// in order to disable the creation of item containers. - /// protected virtual IItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator(this); @@ -324,7 +318,7 @@ namespace Avalonia.Controls { if (current.VisualParent == container && current is IInputElement inputElement) { - var next = GetNextControl(container, direction.Value, inputElement, false); + var next = GetNextControl(container, direction.Value, inputElement, WrapFocus); if (next != null) { @@ -342,6 +336,11 @@ namespace Avalonia.Controls base.OnKeyDown(e); } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ItemsControlAutomationPeer(this); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -509,7 +508,6 @@ namespace Avalonia.Controls do { result = container.GetControl(direction, c, wrap); - from = from ?? result; if (result != null && result.Focusable && diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 9b7ae0d324..79285bb86b 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -1,5 +1,6 @@ using System.Collections; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; @@ -12,6 +13,7 @@ namespace Avalonia.Controls /// /// An in which individual items can be selected. /// + [TemplatePart("PART_ScrollViewer", typeof(IScrollable))] public class ListBox : SelectingItemsControl { /// diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index 4fe5f4de40..66a46cab4a 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -1,6 +1,6 @@ +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; -using Avalonia.Input; namespace Avalonia.Controls { @@ -34,5 +34,10 @@ namespace Avalonia.Controls get { return GetValue(IsSelectedProperty); } set { SetValue(IsSelectedProperty, value); } } + + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ListItemAutomationPeer(this); + } } } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index cc89677f82..611811f170 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -1,3 +1,5 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -35,6 +37,8 @@ namespace Avalonia.Controls static Menu() { ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); + AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue(AccessibilityView.Control); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Menu); } /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index c36fbcc51f..bdcff5cd09 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -81,13 +81,13 @@ namespace Avalonia.Controls { var index = SelectedIndex; return (index != -1) ? - (IMenuItem?)ItemContainerGenerator!.ContainerFromIndex(index) : + (IMenuItem?)ItemContainerGenerator.ContainerFromIndex(index) : null; } set { SelectedIndex = value is not null ? - ItemContainerGenerator!.IndexFromContainer(value) : -1; + ItemContainerGenerator.IndexFromContainer(value) : -1; } } @@ -96,7 +96,7 @@ namespace Avalonia.Controls { get { - return ItemContainerGenerator!.Containers + return ItemContainerGenerator.Containers .Select(x => x.ContainerControl) .OfType(); } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 9540ab05f9..955af8888b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using System.Windows.Input; +using Avalonia.Automation.Peers; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; @@ -19,8 +20,9 @@ namespace Avalonia.Controls /// /// A menu item control. /// + [TemplatePart("PART_Popup", typeof(Popup))] [PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")] - public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource + public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource, IClickableControl { /// /// Defines the property. @@ -308,12 +310,12 @@ namespace Avalonia.Controls { var index = SelectedIndex; return (index != -1) ? - (IMenuItem?)ItemContainerGenerator!.ContainerFromIndex(index) : + (IMenuItem?)ItemContainerGenerator.ContainerFromIndex(index) : null; } set { - SelectedIndex = value is not null ? ItemContainerGenerator!.IndexFromContainer(value) : -1; + SelectedIndex = value is not null ? ItemContainerGenerator.IndexFromContainer(value) : -1; } } @@ -322,7 +324,7 @@ namespace Avalonia.Controls { get { - return ItemContainerGenerator!.Containers + return ItemContainerGenerator.Containers .Select(x => x.ContainerControl) .OfType(); } @@ -494,6 +496,11 @@ namespace Avalonia.Controls } } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new MenuItemAutomationPeer(this); + } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { base.UpdateDataValidation(property, value); @@ -637,7 +644,9 @@ namespace Avalonia.Controls /// The property change event. private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e) { - if ((bool)e.NewValue!) + var parentMenu = Parent as Menu; + + if ((bool)e.NewValue! && (parentMenu is null || parentMenu.IsOpen)) { Focus(); } @@ -698,6 +707,14 @@ namespace Avalonia.Controls void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + void IClickableControl.RaiseClick() + { + if (IsEffectivelyEnabled) + { + RaiseEvent(new RoutedEventArgs(ClickEvent)); + } + } + /// /// A dependency resolver which returns a . /// diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 9499995da3..d6b82a8f8a 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -14,6 +14,7 @@ namespace Avalonia.Controls.Notifications /// /// An that displays notifications in a . /// + [TemplatePart("PART_Items", typeof(Panel))] [PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright")] public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest { diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index f67377b310..fbbaab6182 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.IO; using System.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; @@ -15,6 +16,8 @@ namespace Avalonia.Controls /// /// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values. /// + [TemplatePart("PART_Spinner", typeof(Spinner))] + [TemplatePart("PART_TextBox", typeof(TextBox))] public class NumericUpDown : TemplatedControl { /// @@ -1051,7 +1054,7 @@ namespace Avalonia.Controls var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); var textSpecialCharacters = text.Where(c => !char.IsDigit(c)); // same non-digit characters on currentValueText and new text => remove them on new Text to parse it again. - if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0) + if (!currentValueTextSpecialCharacters.Except(textSpecialCharacters).Any()) { foreach (var character in textSpecialCharacters) { diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 482a7fab84..2230b4b0d2 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -147,7 +147,7 @@ namespace Avalonia.Controls throw new NotSupportedException(); } - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); InvalidateMeasureOnChildrenChanged(); } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 23ecdb2e7b..6e9ac537f1 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -149,13 +149,18 @@ namespace Avalonia.Controls.Platform case Key.Up: case Key.Down: { - if (item?.IsTopLevel == true) + if (item?.IsTopLevel == true && item.HasSubMenu) { - if (item.HasSubMenu && !item.IsSubMenuOpen) + if (!item.IsSubMenuOpen) { Open(item, true); - e.Handled = true; } + else + { + item.MoveSelection(NavigationDirection.First, true); + } + + e.Handled = true; } else { @@ -247,7 +252,8 @@ namespace Avalonia.Controls.Platform // new menu. if (item.IsSubMenuOpen && item.Parent is IMenu && - item.Parent.SelectedItem is object) + item.Parent.SelectedItem is object && + item.Parent.SelectedItem != item) { item.Close(); Open(item.Parent.SelectedItem, true); diff --git a/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs b/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs deleted file mode 100644 index 2a401ad912..0000000000 --- a/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace Avalonia.Platform -{ - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public class ExportWindowingSubsystemAttribute : Attribute - { - public ExportWindowingSubsystemAttribute(OperatingSystemType requiredRuntimePlatform, int priority, string name, Type initializationType, - string initializationMethod, Type? environmentChecker = null) - { - Name = name; - InitializationType = initializationType; - InitializationMethod = initializationMethod; - EnvironmentChecker = environmentChecker; - RequiredOS = requiredRuntimePlatform; - Priority = priority; - } - - public string InitializationMethod { get; private set; } - public Type? EnvironmentChecker { get; } - public Type InitializationType { get; private set; } - public string Name { get; private set; } - public int Priority { get; private set; } - public OperatingSystemType RequiredOS { get; private set; } - } -} diff --git a/src/Avalonia.Controls/Platform/IPlatformNativeSurfaceHandle.cs b/src/Avalonia.Controls/Platform/IPlatformNativeSurfaceHandle.cs new file mode 100644 index 0000000000..264f5e4667 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IPlatformNativeSurfaceHandle.cs @@ -0,0 +1,10 @@ +using System; + +namespace Avalonia.Platform +{ + public interface IPlatformNativeSurfaceHandle : IPlatformHandle + { + PixelSize Size { get; } + double Scaling { get; } + } +} diff --git a/src/Avalonia.Controls/Platform/IScreenImpl.cs b/src/Avalonia.Controls/Platform/IScreenImpl.cs index 5bd45057d9..b68391aa52 100644 --- a/src/Avalonia.Controls/Platform/IScreenImpl.cs +++ b/src/Avalonia.Controls/Platform/IScreenImpl.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +#nullable enable + namespace Avalonia.Platform { public interface IScreenImpl @@ -7,5 +9,11 @@ namespace Avalonia.Platform int ScreenCount { get; } IReadOnlyList AllScreens { get; } + + Screen? ScreenFromWindow(IWindowBaseImpl window); + + Screen? ScreenFromPoint(PixelPoint point); + + Screen? ScreenFromRect(PixelRect rect); } } diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index ff83e007b4..066f4579c0 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Automation.Peers; namespace Avalonia.Platform { diff --git a/src/Avalonia.Controls/Platform/ScreenHelper.cs b/src/Avalonia.Controls/Platform/ScreenHelper.cs new file mode 100644 index 0000000000..0bd2be69d0 --- /dev/null +++ b/src/Avalonia.Controls/Platform/ScreenHelper.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using Avalonia.Utilities; + +#nullable enable + +namespace Avalonia.Platform +{ + public static class ScreenHelper + { + public static Screen? ScreenFromPoint(PixelPoint point, IReadOnlyList screens) + { + foreach (Screen screen in screens) + { + if (screen.Bounds.ContainsExclusive(point)) + { + return screen; + } + } + + return null; + } + + public static Screen? ScreenFromRect(PixelRect bounds, IReadOnlyList screens) + { + Screen? currMaxScreen = null; + double maxAreaSize = 0; + + foreach (Screen screen in screens) + { + double left = MathUtilities.Clamp(bounds.X, screen.Bounds.X, screen.Bounds.X + screen.Bounds.Width); + double top = MathUtilities.Clamp(bounds.Y, screen.Bounds.Y, screen.Bounds.Y + screen.Bounds.Height); + double right = MathUtilities.Clamp(bounds.X + bounds.Width, screen.Bounds.X, screen.Bounds.X + screen.Bounds.Width); + double bottom = MathUtilities.Clamp(bounds.Y + bounds.Height, screen.Bounds.Y, screen.Bounds.Y + screen.Bounds.Height); + double area = (right - left) * (bottom - top); + if (area > maxAreaSize) + { + maxAreaSize = area; + currMaxScreen = screen; + } + } + + return currMaxScreen; + } + + public static Screen? ScreenFromWindow(IWindowBaseImpl window, IReadOnlyList screens) + { + var rect = new PixelRect( + window.Position, + PixelSize.FromSize(window.FrameSize ?? window.ClientSize, window.DesktopScaling)); + + return ScreenFromRect(rect, screens); + } + } +} diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 8883c9e8ce..3d1e7eb5a8 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -170,7 +170,7 @@ namespace Avalonia.Controls.Presenters { if (fromIndex != toIndex) { - var generator = ItemContainerGenerator!; + var generator = ItemContainerGenerator; IControl? from = null; IControl? to = null; diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 9886dd913a..8229f25a07 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -1,5 +1,6 @@ using System; +using Avalonia.Controls.Documents; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -46,7 +47,73 @@ namespace Avalonia.Controls.Presenters /// public static readonly StyledProperty BoxShadowProperty = Border.BoxShadowProperty.AddOwner(); - + + /// + /// Defines the property. + /// + public static readonly AttachedProperty ForegroundProperty = + TextElement.ForegroundProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontFamilyProperty = + TextElement.FontFamilyProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontSizeProperty = + TextElement.FontSizeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontStyleProperty = + TextElement.FontStyleProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontWeightProperty = + TextElement.FontWeightProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontStretchProperty = + TextElement.FontStretchProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty TextAlignmentProperty = + TextBlock.TextAlignmentProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty TextWrappingProperty = + TextBlock.TextWrappingProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty TextTrimmingProperty = + TextBlock.TextTrimmingProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty LineHeightProperty = + TextBlock.LineHeightProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty MaxLinesProperty = + TextBlock.MaxLinesProperty.AddOwner(); + /// /// Defines the property. /// @@ -107,9 +174,6 @@ namespace Avalonia.Controls.Presenters AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty); AffectsArrange(HorizontalContentAlignmentProperty, VerticalContentAlignmentProperty); AffectsMeasure(BorderThicknessProperty, PaddingProperty); - ContentProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e)); - ContentTemplateProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e)); - TemplatedParentProperty.Changed.AddClassHandler((x, e) => x.TemplatedParentChanged(e)); } public ContentPresenter() @@ -162,6 +226,105 @@ namespace Avalonia.Controls.Presenters set => SetValue(BoxShadowProperty, value); } + /// + /// Gets or sets a brush used to paint the text. + /// + public IBrush? Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + /// + /// Gets or sets the font family. + /// + public FontFamily FontFamily + { + get => GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + /// + /// Gets or sets the font size. + /// + public double FontSize + { + get => GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + /// + /// Gets or sets the font style. + /// + public FontStyle FontStyle + { + get => GetValue(FontStyleProperty); + set => SetValue(FontStyleProperty, value); + } + + /// + /// Gets or sets the font weight. + /// + public FontWeight FontWeight + { + get => GetValue(FontWeightProperty); + set => SetValue(FontWeightProperty, value); + } + + /// + /// Gets or sets the font stretch. + /// + public FontStretch FontStretch + { + get => GetValue(FontStretchProperty); + set => SetValue(FontStretchProperty, value); + } + + /// + /// Gets or sets the text alignment + /// + public TextAlignment TextAlignment + { + get => GetValue(TextAlignmentProperty); + set => SetValue(TextAlignmentProperty, value); + } + + /// + /// Gets or sets the text wrapping + /// + public TextWrapping TextWrapping + { + get => GetValue(TextWrappingProperty); + set => SetValue(TextWrappingProperty, value); + } + + /// + /// Gets or sets the text trimming + /// + public TextTrimming TextTrimming + { + get => GetValue(TextTrimmingProperty); + set => SetValue(TextTrimmingProperty, value); + } + + /// + /// Gets or sets the line height + /// + public double LineHeight + { + get => GetValue(LineHeightProperty); + set => SetValue(LineHeightProperty, value); + } + + /// + /// Gets or sets the max lines + /// + public int MaxLines + { + get => GetValue(MaxLinesProperty); + set => SetValue(MaxLinesProperty, value); + } + /// /// Gets the control displayed by the presenter. /// @@ -240,6 +403,21 @@ namespace Avalonia.Controls.Presenters } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + switch (change.Property.Name) + { + case nameof(Content): + case nameof(ContentTemplate): + ContentChanged(change); + break; + case nameof(TemplatedParent): + TemplatedParentChanged(change); + break; + } + } + /// /// Updates the control based on the control's . /// @@ -254,8 +432,14 @@ namespace Avalonia.Controls.Presenters public void UpdateChild() { var content = Content; + UpdateChild(content); + } + + private void UpdateChild(object? content) + { + var contentTemplate = ContentTemplate; var oldChild = Child; - var newChild = CreateChild(); + var newChild = CreateChild(content, oldChild, contentTemplate); var logicalChildren = Host?.LogicalChildren ?? LogicalChildren; // Remove the old child if we're not recycling it. @@ -271,7 +455,7 @@ namespace Avalonia.Controls.Presenters } // Set the DataContext if the data isn't a control. - if (!(content is IControl)) + if (contentTemplate is { } || !(content is IControl)) { DataContext = content; } @@ -299,6 +483,7 @@ namespace Avalonia.Controls.Presenters } _createdChild = true; + } /// @@ -325,18 +510,23 @@ namespace Avalonia.Controls.Presenters { var content = Content; var oldChild = Child; + return CreateChild(content, oldChild, ContentTemplate); + } + + private IControl? CreateChild(object? content, IControl? oldChild, IDataTemplate? template) + { var newChild = content as IControl; // We want to allow creating Child from the Template, if Content is null. // But it's important to not use DataTemplates, otherwise we will break content presenters in many places, // otherwise it will blow up every ContentPresenter without Content set. - if (newChild == null - && (content != null || ContentTemplate != null)) + if ((newChild == null + && (content != null || template != null)) || (newChild is { } && template is { })) { - var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? + var dataTemplate = this.FindDataTemplate(content, template) ?? ( - RecognizesAccessKey - ? FuncDataTemplate.Access + RecognizesAccessKey + ? FuncDataTemplate.Access : FuncDataTemplate.Default ); @@ -446,7 +636,14 @@ namespace Avalonia.Controls.Presenters if (((ILogical)this).IsAttachedToLogicalTree) { - UpdateChild(); + if (e.Property.Name == nameof(Content)) + { + UpdateChild(e.NewValue); + } + else + { + UpdateChild(); + } } else if (Child != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 343c72f742..4c412e9a41 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -254,7 +254,7 @@ namespace Avalonia.Controls.Presenters /// The movement direction. /// The control from which movement begins. /// The control. - public virtual IControl? GetControlInDirection(NavigationDirection direction, IControl from) + public virtual IControl? GetControlInDirection(NavigationDirection direction, IControl? from) { return null; } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index a34e5d6438..39a512a773 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -226,18 +226,13 @@ namespace Avalonia.Controls.Presenters InvalidateScroll(); } - public override IControl? GetControlInDirection(NavigationDirection direction, IControl from) + public override IControl? GetControlInDirection(NavigationDirection direction, IControl? from) { var generator = Owner.ItemContainerGenerator; var panel = VirtualizingPanel; var itemIndex = generator.IndexFromContainer(from); var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical; - if (itemIndex == -1) - { - return null; - } - var newItemIndex = -1; switch (direction) @@ -250,6 +245,16 @@ namespace Avalonia.Controls.Presenters newItemIndex = ItemCount - 1; break; + default: + if (itemIndex == -1) + { + return null; + } + break; + } + + switch (direction) + { case NavigationDirection.Up: if (vertical) { diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 2c04b03faf..265704ceaa 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -117,7 +117,7 @@ namespace Avalonia.Controls.Presenters } /// - IControl? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from) + IControl? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl? from) { return Virtualizer?.GetControlInDirection(direction, from); } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index f938c8d437..2821fa8cf0 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -158,7 +158,7 @@ namespace Avalonia.Controls.Presenters { ItemsChanged(e); - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); } } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 97fb4c3f43..a8bffcc842 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -389,7 +389,7 @@ namespace Avalonia.Controls.Presenters { var logicalUnits = delta.Y / logicalScrollItemSize.Y; delta = delta.WithY(delta.Y - logicalUnits * logicalScrollItemSize.Y); - dy = logicalUnits * scrollable!.ScrollSize.Height; + dy = logicalUnits; } else dy = delta.Y; @@ -407,7 +407,7 @@ namespace Avalonia.Controls.Presenters { var logicalUnits = delta.X / logicalScrollItemSize.X; delta = delta.WithX(delta.X - logicalUnits * logicalScrollItemSize.X); - dx = logicalUnits * scrollable!.ScrollSize.Width; + dx = logicalUnits; } else dx = delta.X; diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 1c60c0d906..7f2dde7c1e 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Reactive.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; @@ -10,6 +8,7 @@ using Avalonia.Utilities; using Avalonia.VisualTree; using Avalonia.Layout; using Avalonia.Media.Immutable; +using Avalonia.Controls.Documents; namespace Avalonia.Controls.Presenters { @@ -26,14 +25,14 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); - public static readonly StyledProperty SelectionBrushProperty = - AvaloniaProperty.Register(nameof(SelectionBrushProperty)); + public static readonly StyledProperty SelectionBrushProperty = + AvaloniaProperty.Register(nameof(SelectionBrushProperty)); - public static readonly StyledProperty SelectionForegroundBrushProperty = - AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); + public static readonly StyledProperty SelectionForegroundBrushProperty = + AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); - public static readonly StyledProperty CaretBrushProperty = - AvaloniaProperty.Register(nameof(CaretBrushProperty)); + public static readonly StyledProperty CaretBrushProperty = + AvaloniaProperty.Register(nameof(CaretBrushProperty)); public static readonly DirectProperty SelectionStartProperty = TextBox.SelectionStartProperty.AddOwner( @@ -48,8 +47,8 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty TextProperty = + AvaloniaProperty.RegisterDirect( nameof(Text), o => o.Text, (o, v) => o.Text = v); @@ -77,16 +76,14 @@ namespace Avalonia.Controls.Presenters private int _selectionStart; private int _selectionEnd; private bool _caretBlink; - private string _text; + private string? _text; private TextLayout? _textLayout; - private Size _constraint = Size.Infinity; + private Size _constraint; private CharacterHit _lastCharacterHit; private Rect _caretBounds; private Point _navigationPosition; - private ScrollViewer? _scrollViewer; - static TextPresenter() { AffectsRender(CaretBrushProperty, SelectionBrushProperty); @@ -114,7 +111,7 @@ namespace Avalonia.Controls.Presenters /// Gets or sets the text. /// [Content] - public string Text + public string? Text { get => _text; set => SetAndRaise(TextProperty, ref _text, value); @@ -125,8 +122,8 @@ namespace Avalonia.Controls.Presenters /// public FontFamily FontFamily { - get => TextBlock.GetFontFamily(this); - set => TextBlock.SetFontFamily(this, value); + get => TextElement.GetFontFamily(this); + set => TextElement.SetFontFamily(this, value); } /// @@ -134,8 +131,8 @@ namespace Avalonia.Controls.Presenters /// public double FontSize { - get => TextBlock.GetFontSize(this); - set => TextBlock.SetFontSize(this, value); + get => TextElement.GetFontSize(this); + set => TextElement.SetFontSize(this, value); } /// @@ -143,8 +140,8 @@ namespace Avalonia.Controls.Presenters /// public FontStyle FontStyle { - get => TextBlock.GetFontStyle(this); - set => TextBlock.SetFontStyle(this, value); + get => TextElement.GetFontStyle(this); + set => TextElement.SetFontStyle(this, value); } /// @@ -152,8 +149,17 @@ namespace Avalonia.Controls.Presenters /// public FontWeight FontWeight { - get => TextBlock.GetFontWeight(this); - set => TextBlock.SetFontWeight(this, value); + get => TextElement.GetFontWeight(this); + set => TextElement.SetFontWeight(this, value); + } + + /// + /// Gets or sets the font stretch. + /// + public FontStretch FontStretch + { + get => TextElement.GetFontStretch(this); + set => TextElement.SetFontStretch(this, value); } /// @@ -161,8 +167,8 @@ namespace Avalonia.Controls.Presenters /// public IBrush? Foreground { - get => TextBlock.GetForeground(this); - set => TextBlock.SetForeground(this, value); + get => TextElement.GetForeground(this); + set => TextElement.SetForeground(this, value); } /// @@ -186,7 +192,7 @@ namespace Avalonia.Controls.Presenters /// /// Gets the used to render the text. /// - public TextLayout? TextLayout + public TextLayout TextLayout { get { @@ -230,19 +236,19 @@ namespace Avalonia.Controls.Presenters set => SetValue(RevealPasswordProperty, value); } - public IBrush SelectionBrush + public IBrush? SelectionBrush { get => GetValue(SelectionBrushProperty); set => SetValue(SelectionBrushProperty, value); } - public IBrush SelectionForegroundBrush + public IBrush? SelectionForegroundBrush { get => GetValue(SelectionForegroundBrushProperty); set => SetValue(SelectionForegroundBrushProperty, value); } - public IBrush CaretBrush + public IBrush? CaretBrush { get => GetValue(CaretBrushProperty); set => SetValue(CaretBrushProperty, value); @@ -276,6 +282,8 @@ namespace Avalonia.Controls.Presenters } } + protected override bool BypassFlowDirectionPolicies => true; + /// /// Creates the used to render the text. /// @@ -284,20 +292,14 @@ namespace Avalonia.Controls.Presenters /// /// /// A object. - private TextLayout? CreateTextLayoutInternal(Size constraint, string text, Typeface typeface, + private TextLayout CreateTextLayoutInternal(Size constraint, string? text, Typeface typeface, IReadOnlyList>? textStyleOverrides) { var foreground = Foreground; - - if (foreground == null) - { - return null; - } - var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width; var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height; - var textLayout = new TextLayout(text ?? string.Empty, typeface, FontSize, foreground, TextAlignment, + var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment, TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides, flowDirection: FlowDirection); @@ -317,15 +319,10 @@ namespace Avalonia.Controls.Presenters context.FillRectangle(background, new Rect(Bounds.Size)); } - if (TextLayout == null) - { - return; - } - var top = 0d; var left = 0.0; - var (_, textHeight) = TextLayout.Size; + var textHeight = TextLayout.Bounds.Height; if (Bounds.Height < textHeight) { @@ -346,17 +343,11 @@ namespace Avalonia.Controls.Presenters public override void Render(DrawingContext context) { - if (double.IsPositiveInfinity (_constraint.Width)) - { - _constraint = _scrollViewer?.Viewport ?? Size.Infinity; - - InvalidateTextLayout(); - } - var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; + var selectionBrush = SelectionBrush; - if (selectionStart != selectionEnd && TextLayout != null) + if (selectionStart != selectionEnd && selectionBrush != null) { var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; @@ -365,7 +356,7 @@ namespace Avalonia.Controls.Presenters foreach (var rect in rects) { - context.FillRectangle(SelectionBrush, rect); + context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1)); } } @@ -406,10 +397,14 @@ namespace Avalonia.Controls.Presenters var x = Math.Floor(_caretBounds.X) + 0.5; var y = Math.Floor(_caretBounds.Y) + 0.5; var b = Math.Ceiling(_caretBounds.Bottom) - 0.5; + + var caretIndex = _lastCharacterHit.FirstCharacterIndex + _lastCharacterHit.TrailingLength; + var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, _lastCharacterHit.TrailingLength > 0); + var textLine = TextLayout.TextLines[lineIndex]; - if (x >= Bounds.Width) + if (_caretBounds.X > 0 && _caretBounds.X >= textLine.WidthIncludingTrailingWhitespace) { - x = Math.Floor(_caretBounds.X - 1) + 0.5; + x -= 1; } return (new Point(x, y), new Point(x, b)); @@ -431,38 +426,40 @@ namespace Avalonia.Controls.Presenters internal void CaretChanged() { - if (this.GetVisualParent() != null) + if (this.GetVisualParent() == null) { - if (_caretTimer.IsEnabled) - { - _caretBlink = true; - _caretTimer.Stop(); - _caretTimer.Start(); - InvalidateVisual(); - } - else - { - _caretTimer.Start(); - InvalidateVisual(); - _caretTimer.Stop(); - } + return; + } - if (IsMeasureValid) - { - this.BringIntoView(_caretBounds); - } - else - { - // The measure is currently invalid so there's no point trying to bring the - // current char into view until a measure has been carried out as the scroll - // viewer extents may not be up-to-date. - Dispatcher.UIThread.Post( - () => - { - this.BringIntoView(_caretBounds); - }, - DispatcherPriority.Render); - } + if (_caretTimer.IsEnabled) + { + _caretBlink = true; + _caretTimer.Stop(); + _caretTimer.Start(); + InvalidateVisual(); + } + else + { + _caretTimer.Start(); + InvalidateVisual(); + _caretTimer.Stop(); + } + + if (IsMeasureValid) + { + this.BringIntoView(_caretBounds); + } + else + { + // The measure is currently invalid so there's no point trying to bring the + // current char into view until a measure has been carried out as the scroll + // viewer extents may not be up-to-date. + Dispatcher.UIThread.Post( + () => + { + this.BringIntoView(_caretBounds); + }, + DispatcherPriority.Render); } } @@ -470,16 +467,16 @@ namespace Avalonia.Controls.Presenters /// Creates the used to render the text. /// /// A object. - protected virtual TextLayout? CreateTextLayout() + protected virtual TextLayout CreateTextLayout() { - TextLayout? result; + TextLayout result; var text = Text; var typeface = new Typeface(FontFamily, FontStyle, FontWeight); - var selectionStart = SelectionStart; - var selectionEnd = SelectionEnd; + var selectionStart = CoerceCaretIndex(SelectionStart); + var selectionEnd = CoerceCaretIndex(SelectionEnd); var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; @@ -517,14 +514,29 @@ namespace Avalonia.Controls.Presenters protected override Size MeasureOverride(Size availableSize) { - if (!double.IsInfinity(availableSize.Width) && availableSize != _constraint) + _constraint = availableSize; + + _textLayout = null; + + InvalidateArrange(); + + var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); + + return new Size(measuredSize.Width, measuredSize.Height); + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) { - _constraint = availableSize; - - InvalidateTextLayout(); + return finalSize; } - return TextLayout?.Size ?? default; + _constraint = new Size(finalSize.Width, Math.Ceiling(finalSize.Height)); + + _textLayout = null; + + return finalSize; } private int CoerceCaretIndex(int value) @@ -537,16 +549,12 @@ namespace Avalonia.Controls.Presenters private void CaretTimerTick(object? sender, EventArgs e) { _caretBlink = !_caretBlink; + InvalidateVisual(); } public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false) { - if (TextLayout == null) - { - return; - } - var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, trailingEdge); var textLine = TextLayout.TextLines[lineIndex]; @@ -569,29 +577,23 @@ namespace Avalonia.Controls.Presenters } _navigationPosition = _caretBounds.Position; + + CaretChanged(); } public void MoveCaretToPoint(Point point) { - if (TextLayout == null) - { - return; - } - var hit = TextLayout.HitTestPoint(point); UpdateCaret(hit.CharacterHit); _navigationPosition = _caretBounds.Position; + + CaretChanged(); } public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward) { - if (TextLayout == null) - { - return; - } - var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(CaretIndex, _lastCharacterHit.TrailingLength > 0); if (lineIndex < 0) @@ -629,14 +631,16 @@ namespace Avalonia.Controls.Presenters MoveCaretToPoint(new Point(currentX, currentY)); _navigationPosition = navigationPosition.WithY(_caretBounds.Y); + + CaretChanged(); } - public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward) + public CharacterHit GetNextCharacterHit(LogicalDirection direction = LogicalDirection.Forward) { - if (TextLayout == null) + if (Text is null) { - return; - } + return default; + } var characterHit = _lastCharacterHit; var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -645,7 +649,7 @@ namespace Avalonia.Controls.Presenters if (lineIndex < 0) { - return; + return default; } if (direction == LogicalDirection.Forward) @@ -658,7 +662,7 @@ namespace Avalonia.Controls.Presenters caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - if (textLine.NewLineLength > 0 && caretIndex == textLine.TextRange.Start + textLine.TextRange.Length) + if (textLine.NewLineLength > 0 && caretIndex == textLine.FirstTextSourceIndex + textLine.Length) { characterHit = new CharacterHit(caretIndex); } @@ -670,7 +674,7 @@ namespace Avalonia.Controls.Presenters break; } - if (caretIndex - textLine.NewLineLength == textLine.TextRange.Start + textLine.TextRange.Length) + if (caretIndex - textLine.NewLineLength == textLine.FirstTextSourceIndex + textLine.Length) { break; } @@ -706,18 +710,29 @@ namespace Avalonia.Controls.Presenters } } + return characterHit; + } + + public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward) + { + if (FlowDirection == FlowDirection.RightToLeft) + { + direction = direction == LogicalDirection.Forward ? + LogicalDirection.Backward : + LogicalDirection.Forward; + } + + var characterHit = GetNextCharacterHit(direction); + UpdateCaret(characterHit); _navigationPosition = _caretBounds.Position; + + CaretChanged(); } private void UpdateCaret(CharacterHit characterHit) { - if (TextLayout == null) - { - return; - } - _lastCharacterHit = characterHit; var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -743,9 +758,7 @@ namespace Avalonia.Controls.Presenters CaretBoundsChanged?.Invoke(this, EventArgs.Empty); } - - CaretChanged(); - + SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); } @@ -754,19 +767,10 @@ namespace Avalonia.Controls.Presenters return _caretBounds; } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - _scrollViewer = this.FindAncestorOfType(); - } - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - _scrollViewer = null; - _caretTimer.Stop(); _caretTimer.Tick -= CaretTimerTick; @@ -778,19 +782,25 @@ namespace Avalonia.Controls.Presenters switch (change.Property.Name) { - case nameof (TextBlock.Foreground): - case nameof (TextBlock.FontSize): - case nameof (TextBlock.FontStyle): - case nameof (TextBlock.FontWeight): - case nameof (TextBlock.FontFamily): + case nameof (Foreground): + case nameof (FontSize): + case nameof (FontStyle): + case nameof (FontWeight): + case nameof (FontFamily): + case nameof (FontStretch): + case nameof (Text): case nameof (TextAlignment): case nameof (TextWrapping): + case nameof (SelectionStart): case nameof (SelectionEnd): case nameof (SelectionForegroundBrush): + case nameof (PasswordChar): case nameof (RevealPassword): + + case nameof(FlowDirection): { InvalidateTextLayout(); break; diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index a6976721b1..87cf660cad 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using Avalonia.Automation.Peers; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.TextFormatting; @@ -78,9 +79,9 @@ namespace Avalonia.Controls.Primitives } /// - protected override TextLayout? CreateTextLayout(Size constraint, string? text) + protected override TextLayout CreateTextLayout(Size constraint, string? text) { - return base.CreateTextLayout(constraint, StripAccessKey(text)); + return base.CreateTextLayout(constraint, RemoveAccessKeyMarker(text)); } /// @@ -107,29 +108,40 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Returns a string with the first underscore stripped. - /// - /// The text. - /// The text with the first underscore stripped. - [return: NotNullIfNotNull("text")] - private string? StripAccessKey(string? text) + protected override AutomationPeer OnCreateAutomationPeer() { - if (text is null) - { - return null; - } - - var position = text.IndexOf('_'); + return new NoneAutomationPeer(this); + } - if (position == -1) + internal static string? RemoveAccessKeyMarker(string? text) + { + if (!string.IsNullOrEmpty(text)) { - return text; + var accessKeyMarker = "_"; + var doubleAccessKeyMarker = accessKeyMarker + accessKeyMarker; + int index = FindAccessKeyMarker(text); + if (index >= 0 && index < text.Length - 1) + text = text.Remove(index, 1); + text = text.Replace(doubleAccessKeyMarker, accessKeyMarker); } - else + return text; + } + + private static int FindAccessKeyMarker(string text) + { + var length = text.Length; + var startIndex = 0; + while (startIndex < length) { - return text.Substring(0, position) + text.Substring(position + 1); + int index = text.IndexOf('_', startIndex); + if (index == -1) + return -1; + if (index + 1 < length && text[index + 1] != '_') + return index; + startIndex = index + 2; } + + return -1; } /// diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs index 2be91c046e..0b3791be3a 100644 --- a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs +++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives /// The movement direction. /// The control from which movement begins. /// The control. - IControl? GetControlInDirection(NavigationDirection direction, IControl from); + IControl? GetControlInDirection(NavigationDirection direction, IControl? from); /// /// Raises the event. diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs index 6a0408d6d1..468879edd1 100644 --- a/src/Avalonia.Controls/Primitives/OverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs @@ -23,7 +23,14 @@ namespace Avalonia.Controls.Primitives } public bool HitTest(Point point) => Children.HitTestCustom(point); - + + protected override Size MeasureOverride(Size availableSize) + { + foreach (Control child in Children) + child.Measure(availableSize); + return availableSize; + } + protected override Size ArrangeOverride(Size finalSize) { // We are saving it here since child controls might need to know the entire size of the overlay diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 6251d5cda7..6ac544e0fe 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -76,7 +76,7 @@ namespace Avalonia.Controls.Primitives Rect? rect = null) { _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot()!, target, placement, offset, anchor, - gravity, constraintAdjustment, rect); + gravity, constraintAdjustment, rect, FlowDirection); UpdatePosition(); } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index f3fec95bcc..bb546107e0 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; +using Avalonia.Automation.Peers; using Avalonia.Controls.Mixins; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Presenters; @@ -560,6 +561,11 @@ namespace Avalonia.Controls.Primitives } } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new PopupAutomationPeer(this); + } + private static IDisposable SubscribeToEventHandler(T target, TEventHandler handler, Action subscribe, Action unsubscribe) { subscribe(target, handler); @@ -651,7 +657,17 @@ namespace Avalonia.Controls.Primitives { if (PlacementTarget != null) { - FocusManager.Instance?.Focus(PlacementTarget); + var e = (IControl?)PlacementTarget; + + while (e is object && (!e.Focusable || !e.IsEffectivelyEnabled || !e.IsVisible)) + { + e = e.Parent; + } + + if (e is object) + { + FocusManager.Instance?.Focus(e); + } } else { diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 340076a407..8daf1ac68a 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -46,6 +46,7 @@ Copyright © 2019 Nikita Tsukanov using System; using Avalonia.VisualTree; +using Avalonia.Media; namespace Avalonia.Controls.Primitives.PopupPositioning { @@ -444,7 +445,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning TopLevel topLevel, IVisual target, PlacementMode placement, Point offset, PopupAnchor anchor, PopupGravity gravity, - PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect) + PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect, + FlowDirection flowDirection) { // We need a better way for tracking the last pointer position #pragma warning disable CS0618 // Type or member is obsolete @@ -503,6 +505,32 @@ namespace Avalonia.Controls.Primitives.PopupPositioning else throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); } + + // Invert coordinate system if FlowDirection is RTL + if (flowDirection == FlowDirection.RightToLeft) + { + if ((positionerParameters.Anchor & PopupAnchor.Right) == PopupAnchor.Right) + { + positionerParameters.Anchor ^= PopupAnchor.Right; + positionerParameters.Anchor |= PopupAnchor.Left; + } + else if ((positionerParameters.Anchor & PopupAnchor.Left) == PopupAnchor.Left) + { + positionerParameters.Anchor ^= PopupAnchor.Left; + positionerParameters.Anchor |= PopupAnchor.Right; + } + + if ((positionerParameters.Gravity & PopupGravity.Right) == PopupGravity.Right) + { + positionerParameters.Gravity ^= PopupGravity.Right; + positionerParameters.Gravity |= PopupGravity.Left; + } + else if ((positionerParameters.Gravity & PopupGravity.Left) == PopupGravity.Left) + { + positionerParameters.Gravity ^= PopupGravity.Left; + positionerParameters.Gravity |= PopupGravity.Right; + } + } } } diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index 0f0dd7311d..a80a60350e 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -106,9 +106,9 @@ namespace Avalonia.Controls.Primitives.PopupPositioning { var screens = _popup.Screens; - var targetScreen = screens.FirstOrDefault(s => s.Bounds.Contains(anchorRect.TopLeft)) + var targetScreen = screens.FirstOrDefault(s => s.Bounds.ContainsExclusive(anchorRect.TopLeft)) ?? screens.FirstOrDefault(s => s.Bounds.Intersects(anchorRect)) - ?? screens.FirstOrDefault(s => s.Bounds.Contains(parentGeometry.TopLeft)) + ?? screens.FirstOrDefault(s => s.Bounds.ContainsExclusive(parentGeometry.TopLeft)) ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry)) ?? screens.FirstOrDefault(); diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs index 91ed5d975d..51a21323d9 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs @@ -23,8 +23,9 @@ namespace Avalonia.Controls.Primitives.PopupPositioning public IReadOnlyList Screens => - _parent.Screen.AllScreens.Select(s => new ManagedPopupPositionerScreenInfo( - s.Bounds.ToRect(1), s.WorkingArea.ToRect(1))).ToList(); + _parent.Screen.AllScreens + .Select(s => new ManagedPopupPositionerScreenInfo(s.Bounds.ToRect(1), s.WorkingArea.ToRect(1))) + .ToArray(); public Rect ParentClientAreaScreenGeometry { diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index be447ea512..abf56e5420 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; +using Avalonia.Automation.Peers; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; using Avalonia.Media; @@ -16,7 +17,6 @@ namespace Avalonia.Controls.Primitives /// public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { - private readonly TopLevel _parent; private PopupPositionerParameters _positionerParameters; /// @@ -44,9 +44,9 @@ namespace Avalonia.Controls.Primitives /// The dependency resolver to use. If null the default dependency resolver will be used. /// public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver? dependencyResolver) - : base(ValidatingPopupImpl.Wrap(impl), dependencyResolver) + : base(impl, dependencyResolver) { - _parent = parent; + ParentTopLevel = parent; } /// @@ -72,6 +72,8 @@ namespace Avalonia.Controls.Primitives /// IStyleHost? IStyleHost.StylingParent => Parent; + public TopLevel ParentTopLevel { get; } + /// public void Dispose() { @@ -90,8 +92,8 @@ namespace Avalonia.Controls.Primitives PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All, Rect? rect = null) { - _positionerParameters.ConfigurePosition(_parent, target, - placement, offset, anchor, gravity, constraintAdjustment, rect); + _positionerParameters.ConfigurePosition(ParentTopLevel, target, + placement, offset, anchor, gravity, constraintAdjustment, rect, FlowDirection); if (_positionerParameters.Size != default) UpdatePosition(); @@ -168,5 +170,10 @@ namespace Avalonia.Controls.Primitives UpdatePosition(); return ClientSize; } + + protected override AutomationPeer OnCreateAutomationPeer() + { + return new PopupRootAutomationPeer(this); + } } } diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 8460fe3017..6a30097fbb 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -22,6 +22,10 @@ namespace Avalonia.Controls.Primitives /// /// A scrollbar control. /// + [TemplatePart("PART_LineDownButton", typeof(Button))] + [TemplatePart("PART_LineUpButton", typeof(Button))] + [TemplatePart("PART_PageDownButton", typeof(Button))] + [TemplatePart("PART_PageUpButton", typeof(Button))] [PseudoClasses(":vertical", ":horizontal")] public class ScrollBar : RangeBase { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 31de6f6398..cec02c7ae9 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -114,6 +114,12 @@ namespace Avalonia.Controls.Primitives "SelectionChanged", RoutingStrategies.Bubble); + /// + /// Defines the property. + /// + public static readonly StyledProperty WrapSelectionProperty = + AvaloniaProperty.Register(nameof(WrapSelection), defaultValue: false); + private static readonly IList Empty = Array.Empty(); private string _textSearchTerm = string.Empty; private DispatcherTimer? _textSearchTimer; @@ -286,11 +292,11 @@ namespace Avalonia.Controls.Primitives "collection is different to the Items on the control."); } - var oldSelection = _selection?.SelectedItems.ToList(); + var oldSelection = _selection?.SelectedItems.ToArray(); DeinitializeSelectionModel(_selection); _selection = value; - if (oldSelection?.Count > 0) + if (oldSelection?.Length > 0) { RaiseEvent(new SelectionChangedEventArgs( SelectionChangedEvent, @@ -321,6 +327,16 @@ namespace Avalonia.Controls.Primitives set { SetValue(IsTextSearchEnabledProperty, value); } } + /// + /// Gets or sets a value which indicates whether to wrap around when the first + /// or last item is reached. + /// + public bool WrapSelection + { + get { return GetValue(WrapSelectionProperty); } + set { SetValue(WrapSelectionProperty, value); } + } + /// /// Gets or sets the selection mode. /// @@ -515,11 +531,23 @@ namespace Avalonia.Controls.Primitives _textSearchTerm += e.Text; - bool match(ItemContainerInfo info) => - info.ContainerControl is IContentControl control && - control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + bool Match(ItemContainerInfo info) + { + if (info.ContainerControl.IsSet(TextSearch.TextProperty)) + { + var searchText = info.ContainerControl.GetValue(TextSearch.TextProperty); - var info = ItemContainerGenerator?.Containers.FirstOrDefault(match); + if (searchText?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true) + { + return true; + } + } + + return info.ContainerControl is IContentControl control && + control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + } + + var info = ItemContainerGenerator?.Containers.FirstOrDefault(Match); if (info != null) { @@ -580,6 +608,10 @@ namespace Avalonia.Controls.Primitives var newValue = change.NewValue.GetValueOrDefault(); _selection.SingleSelect = !newValue.HasAllFlags(SelectionMode.Multiple); } + else if (change.Property == WrapSelectionProperty) + { + WrapFocus = WrapSelection; + } } /// @@ -813,8 +845,8 @@ namespace Avalonia.Controls.Primitives { var ev = new SelectionChangedEventArgs( SelectionChangedEvent, - e.DeselectedItems.ToList(), - e.SelectedItems.ToList()); + e.DeselectedItems.ToArray(), + e.SelectedItems.ToArray()); RaiseEvent(ev); } } @@ -908,7 +940,7 @@ namespace Avalonia.Controls.Primitives { MarkContainerSelected( container, - Selection.IsSelected(ItemContainerGenerator!.IndexFromContainer(container))); + Selection.IsSelected(ItemContainerGenerator.IndexFromContainer(container))); } } } @@ -956,7 +988,7 @@ namespace Avalonia.Controls.Primitives RaiseEvent(new SelectionChangedEventArgs( SelectionChangedEvent, Array.Empty(), - Selection.SelectedItems.ToList())); + Selection.SelectedItems.ToArray())); } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index af681d6930..6e4ae748d9 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Documents; using Avalonia.Controls.Templates; using Avalonia.Interactivity; using Avalonia.Logging; @@ -41,31 +42,37 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty FontFamilyProperty = - TextBlock.FontFamilyProperty.AddOwner(); + TextElement.FontFamilyProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty FontSizeProperty = - TextBlock.FontSizeProperty.AddOwner(); + TextElement.FontSizeProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty FontStyleProperty = - TextBlock.FontStyleProperty.AddOwner(); + TextElement.FontStyleProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty FontWeightProperty = - TextBlock.FontWeightProperty.AddOwner(); + TextElement.FontWeightProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontStretchProperty = + TextElement.FontStretchProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty ForegroundProperty = - TextBlock.ForegroundProperty.AddOwner(); + TextElement.ForegroundProperty.AddOwner(); /// /// Defines the property. @@ -185,6 +192,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(FontWeightProperty, value); } } + /// + /// Gets or sets the font stretch used to draw the control's text. + /// + public FontStretch FontStretch + { + get { return GetValue(FontStretchProperty); } + set { SetValue(FontStretchProperty, value); } + } + /// /// Gets or sets the brush used to draw the control's text and other foreground elements. /// @@ -340,6 +356,11 @@ namespace Avalonia.Controls.Primitives base.OnDetachedFromLogicalTree(e); } + /// + /// Called when the control's template is applied. + /// In simple terms, this means the method is called just before the control is displayed. + /// + /// The event args. protected virtual void OnApplyTemplate(TemplateAppliedEventArgs e) { } diff --git a/src/Avalonia.Controls/Primitives/TextSearch.cs b/src/Avalonia.Controls/Primitives/TextSearch.cs new file mode 100644 index 0000000000..949532cb16 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/TextSearch.cs @@ -0,0 +1,37 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Allows to customize text searching in . + /// + public static class TextSearch + { + /// + /// Defines the Text attached property. + /// This text will be considered during text search in (such as ) + /// + public static readonly AttachedProperty TextProperty + = AvaloniaProperty.RegisterAttached("Text", typeof(TextSearch)); + + /// + /// Sets the for a control. + /// + /// The control + /// The search text to set + public static void SetText(Control control, string text) + { + control.SetValue(TextProperty, text); + } + + /// + /// Gets the of a control. + /// + /// The control + /// The property value + public static string GetText(Control control) + { + return control.GetValue(TextProperty); + } + } +} diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 4bdf6db2fc..148797c53a 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Interactivity; @@ -169,6 +170,11 @@ namespace Avalonia.Controls.Primitives RaiseEvent(e); } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ToggleButtonAutomationPeer(this); + } + private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e) { var newValue = (bool?)e.NewValue; diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 017a053c48..a4f2cc799a 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.Layout; using Avalonia.Media; @@ -9,6 +10,7 @@ namespace Avalonia.Controls /// /// A control used to indicate the progress of an operation. /// + [TemplatePart("PART_Indicator", typeof(Border))] [PseudoClasses(":vertical", ":horizontal", ":indeterminate")] public class ProgressBar : RangeBase { @@ -137,6 +139,7 @@ namespace Avalonia.Controls static ProgressBar() { + ValueProperty.OverrideMetadata(new DirectPropertyMetadata(defaultBindingMode: BindingMode.OneWay)); ValueProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); MinimumProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); MaximumProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index 05561a38ef..25330614cf 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -6,6 +6,7 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Automation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Presenters")] @@ -14,3 +15,4 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Chrome")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Documents")] diff --git a/src/Avalonia.Controls/Screens.cs b/src/Avalonia.Controls/Screens.cs index ef438576a7..a554f82f61 100644 --- a/src/Avalonia.Controls/Screens.cs +++ b/src/Avalonia.Controls/Screens.cs @@ -2,54 +2,45 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Platform; -using Avalonia.Utilities; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls { public class Screens { - private readonly IScreenImpl? _iScreenImpl; + private readonly IScreenImpl _iScreenImpl; public int ScreenCount => _iScreenImpl?.ScreenCount ?? 0; public IReadOnlyList All => _iScreenImpl?.AllScreens ?? Array.Empty(); public Screen? Primary => All.FirstOrDefault(x => x.Primary); - public Screens(IScreenImpl? iScreenImpl) + public Screens(IScreenImpl iScreenImpl) { _iScreenImpl = iScreenImpl; } - public Screen? ScreenFromBounds(PixelRect bounds){ - - Screen? currMaxScreen = null; - double maxAreaSize = 0; - foreach (Screen screen in All) - { - double left = MathUtilities.Clamp(bounds.X, screen.Bounds.X, screen.Bounds.X + screen.Bounds.Width); - double top = MathUtilities.Clamp(bounds.Y, screen.Bounds.Y, screen.Bounds.Y + screen.Bounds.Height); - double right = MathUtilities.Clamp(bounds.X + bounds.Width, screen.Bounds.X, screen.Bounds.X + screen.Bounds.Width); - double bottom = MathUtilities.Clamp(bounds.Y + bounds.Height, screen.Bounds.Y, screen.Bounds.Y + screen.Bounds.Height); - double area = (right - left) * (bottom - top); - if (area > maxAreaSize) - { - maxAreaSize = area; - currMaxScreen = screen; - } - } - - return currMaxScreen; + public Screen? ScreenFromBounds(PixelRect bounds) + { + return _iScreenImpl.ScreenFromRect(bounds); } - public Screen? ScreenFromPoint(PixelPoint point) + public Screen? ScreenFromWindow(IWindowBaseImpl window) { - return All.FirstOrDefault(x => x.Bounds.Contains(point)); + return _iScreenImpl.ScreenFromWindow(window); + } + + public Screen? ScreenFromPoint(PixelPoint point) + { + return _iScreenImpl.ScreenFromPoint(point); } public Screen? ScreenFromVisual(IVisual visual) { var tl = visual.PointToScreen(visual.Bounds.TopLeft); var br = visual.PointToScreen(visual.Bounds.BottomRight); + return ScreenFromBounds(new PixelRect(tl, br)); } } diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 7c4b87e66a..535f9ae43e 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -1,5 +1,7 @@ using System; using System.Reactive.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -8,8 +10,10 @@ using Avalonia.Interactivity; namespace Avalonia.Controls { /// - /// A control scrolls its content if the content is bigger than the space available. + /// A control which scrolls its content if the content is bigger than the space available. /// + [TemplatePart("PART_HorizontalScrollBar", typeof(ScrollBar))] + [TemplatePart("PART_VerticalScrollBar", typeof(ScrollBar))] public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider { /// @@ -762,6 +766,11 @@ namespace Avalonia.Controls _scrollBarExpandSubscription = SubscribeToScrollBars(e); } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ScrollViewerAutomationPeer(this); + } + private IDisposable? SubscribeToScrollBars(TemplateAppliedEventArgs e) { static IObservable? GetExpandedObservable(ScrollBar? scrollBar) diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index 40c6f63ed8..d92ffb0d1a 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -182,8 +182,8 @@ namespace Avalonia.Controls.Selection try { var items = WritableSelectedItems; - var deselected = e.DeselectedItems.ToList(); - var selected = e.SelectedItems.ToList(); + var deselected = e.DeselectedItems.ToArray(); + var selected = e.SelectedItems.ToArray(); _ignoreSelectedItemsChanges = true; diff --git a/src/Avalonia.Controls/Shapes/Rectangle.cs b/src/Avalonia.Controls/Shapes/Rectangle.cs index f3db2644a9..d60180bab4 100644 --- a/src/Avalonia.Controls/Shapes/Rectangle.cs +++ b/src/Avalonia.Controls/Shapes/Rectangle.cs @@ -2,19 +2,139 @@ using Avalonia.Media; namespace Avalonia.Controls.Shapes { + /// + /// Represents a rectangle with optional rounded corners. + /// public class Rectangle : Shape { + private const double PiOver2 = 1.57079633; // 90 deg to rad + + /// + /// Defines the property. + /// + public static readonly StyledProperty RadiusXProperty = + AvaloniaProperty.Register(nameof(RadiusX)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty RadiusYProperty = + AvaloniaProperty.Register(nameof(RadiusY)); + static Rectangle() { - AffectsGeometry(BoundsProperty, StrokeThicknessProperty); + AffectsGeometry( + BoundsProperty, + RadiusXProperty, + RadiusYProperty, + StrokeThicknessProperty); + } + + /// + /// Gets or sets the radius on the X-axis used to round the corners of the rectangle. + /// Corner radii are represented by an ellipse so this is the X-axis width of the ellipse. + /// + public double RadiusX + { + get => GetValue(RadiusXProperty); + set => SetValue(RadiusXProperty, value); + } + + /// + /// Gets or sets the radius on the Y-axis used to round the corners of the rectangle. + /// Corner radii are represented by an ellipse so this is the Y-axis height of the ellipse. + /// + public double RadiusY + { + get => GetValue(RadiusYProperty); + set => SetValue(RadiusYProperty, value); } + /// protected override Geometry CreateDefiningGeometry() { - var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); - return new RectangleGeometry(rect); + // TODO: If RectangleGeometry ever supports RadiusX/Y like in WPF, + // this code can be removed/combined with that implementation + + double x = RadiusX; + double y = RadiusY; + + if (x == 0 && y == 0) + { + // Optimization when there are no corner radii + var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); + return new RectangleGeometry(rect); + } + else + { + var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); + var geometry = new StreamGeometry(); + var arcSize = new Size(x, y); + + using (StreamGeometryContext context = geometry.Open()) + { + // The rectangle is constructed as follows: + // + // (origin) + // Corner 4 Corner 1 + // Top/Left Line 1 Top/Right + // \_ __________ _/ + // | | + // Line 4 | | Line 2 + // _ |__________| _ + // / Line 3 \ + // Corner 3 Corner 2 + // Bottom/Left Bottom/Right + // + // - Lines 1,3 follow the deflated rectangle bounds minus RadiusX + // - Lines 2,4 follow the deflated rectangle bounds minus RadiusY + // - All corners are constructed using elliptical arcs + + // Line 1 + Corner 1 + context.BeginFigure(new Point(rect.Left + x, rect.Top), true); + context.LineTo(new Point(rect.Right - x, rect.Top)); + context.ArcTo( + new Point(rect.Right, rect.Top + y), + arcSize, + rotationAngle: PiOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + // Line 2 + Corner 2 + context.LineTo(new Point(rect.Right, rect.Bottom - y)); + context.ArcTo( + new Point(rect.Right - x, rect.Bottom), + arcSize, + rotationAngle: PiOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + // Line 3 + Corner 3 + context.LineTo(new Point(rect.Left + x, rect.Bottom)); + context.ArcTo( + new Point(rect.Left, rect.Bottom - y), + arcSize, + rotationAngle: PiOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + // Line 4 + Corner 4 + context.LineTo(new Point(rect.Left, rect.Top + y)); + context.ArcTo( + new Point(rect.Left + x, rect.Top), + arcSize, + rotationAngle: PiOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + context.EndFigure(true); + } + + return geometry; + } } + /// protected override Size MeasureOverride(Size availableSize) { return new Size(StrokeThickness, StrokeThickness); diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 87de04f92f..f2bd1947d6 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections; +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; @@ -8,6 +9,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Utilities; +using Avalonia.Automation; namespace Avalonia.Controls { @@ -40,6 +42,9 @@ namespace Avalonia.Controls /// /// A control that lets the user select from a range of values by moving a Thumb control along a Track. /// + [TemplatePart("PART_DecreaseButton", typeof(Button))] + [TemplatePart("PART_IncreaseButton", typeof(Button))] + [TemplatePart("PART_Track", typeof(Track))] [PseudoClasses(":vertical", ":horizontal", ":pressed")] public class Slider : RangeBase { @@ -105,6 +110,7 @@ namespace Avalonia.Controls RoutingStrategies.Bubble); ValueProperty.OverrideMetadata(new DirectPropertyMetadata(enableDataValidation: true)); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Slider); } /// diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs new file mode 100644 index 0000000000..f2f4e951ae --- /dev/null +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -0,0 +1,499 @@ +using System; +using System.Windows.Input; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; + +namespace Avalonia.Controls +{ + /// + /// A button with primary and secondary parts that can each be pressed separately. + /// The primary part behaves like a and the secondary part opens a flyout. + /// + [TemplatePart("PART_PrimaryButton", typeof(Button))] + [TemplatePart("PART_SecondaryButton", typeof(Button))] + [PseudoClasses(pcFlyoutOpen, pcPressed)] + public class SplitButton : ContentControl, ICommandSource + { + protected const string pcChecked = ":checked"; + protected const string pcPressed = ":pressed"; + protected const string pcFlyoutOpen = ":flyout-open"; + + /// + /// Raised when the user presses the primary part of the . + /// + public event EventHandler Click + { + add => AddHandler(ClickEvent, value); + remove => RemoveHandler(ClickEvent, value); + } + + /// + /// Defines the event. + /// + public static readonly RoutedEvent ClickEvent = + RoutedEvent.Register(nameof(Click), RoutingStrategies.Bubble); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CommandProperty = + Button.CommandProperty.AddOwner( + splitButton => splitButton.Command, + (splitButton, command) => splitButton.Command = command); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandParameterProperty = + Button.CommandParameterProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty FlyoutProperty = + Button.FlyoutProperty.AddOwner(); + + private ICommand? _Command; + + private Button? _primaryButton = null; + private Button? _secondaryButton = null; + + private bool _commandCanExecute = true; + private bool _isAttachedToLogicalTree = false; + private bool _isFlyoutOpen = false; + private bool _isKeyboardPressed = false; + + private IDisposable? _flyoutPropertyChangedDisposable; + + //////////////////////////////////////////////////////////////////////// + // Constructor / Destructors + //////////////////////////////////////////////////////////////////////// + + /// + /// Initializes a new instance of the class. + /// + public SplitButton() + { + } + + //////////////////////////////////////////////////////////////////////// + // Properties + //////////////////////////////////////////////////////////////////////// + + /// + /// Gets or sets the invoked when the primary part is pressed. + /// + public ICommand? Command + { + get => _Command; + set => SetAndRaise(CommandProperty, ref _Command, value); + } + + /// + /// Gets or sets a parameter to be passed to the . + /// + public object? CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + + /// + /// Gets or sets the that is shown when the secondary part is pressed. + /// + public FlyoutBase? Flyout + { + get => GetValue(FlyoutProperty); + set => SetValue(FlyoutProperty, value); + } + + /// + /// Gets a value indicating whether the button is currently checked. + /// + /// + /// This property exists only for the derived and is + /// unused (set to false) within . Doing this allows the + /// two controls to share a default style. + /// + internal virtual bool InternalIsChecked => false; + + /// + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + + //////////////////////////////////////////////////////////////////////// + // Methods + //////////////////////////////////////////////////////////////////////// + + /// + void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + + /// + private void CanExecuteChanged(object? sender, EventArgs e) + { + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } + } + + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// + protected void UpdatePseudoClasses() + { + PseudoClasses.Set(pcFlyoutOpen, _isFlyoutOpen); + PseudoClasses.Set(pcPressed, _isKeyboardPressed); + PseudoClasses.Set(pcChecked, InternalIsChecked); + } + + /// + /// Opens the secondary button's flyout. + /// + protected void OpenFlyout() + { + if (Flyout != null) + { + Flyout.ShowAt(this); + } + } + + /// + /// Closes the secondary button's flyout. + /// + protected void CloseFlyout() + { + if (Flyout != null) + { + Flyout.Hide(); + } + } + + /// + /// Registers all flyout events. + /// + /// The flyout to connect events to. + private void RegisterFlyoutEvents(FlyoutBase? flyout) + { + if (flyout != null) + { + flyout.Opened += Flyout_Opened; + flyout.Closed += Flyout_Closed; + + _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(FlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged); + } + } + + /// + /// Explicitly unregisters all flyout events. + /// + /// The flyout to disconnect events from. + private void UnregisterFlyoutEvents(FlyoutBase? flyout) + { + if (flyout != null) + { + flyout.Opened -= Flyout_Opened; + flyout.Closed -= Flyout_Closed; + + _flyoutPropertyChangedDisposable?.Dispose(); + _flyoutPropertyChangedDisposable = null; + } + } + + /// + /// Explicitly unregisters all events related to the two buttons in OnApplyTemplate(). + /// + private void UnregisterEvents() + { + if (_primaryButton != null) + { + _primaryButton.Click -= PrimaryButton_Click; + } + + if (_secondaryButton != null) + { + _secondaryButton.Click -= SecondaryButton_Click; + } + } + + //////////////////////////////////////////////////////////////////////// + // OnEvent Overridable Methods + //////////////////////////////////////////////////////////////////////// + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + UnregisterEvents(); + UnregisterFlyoutEvents(Flyout); + + _primaryButton = e.NameScope.Find public string? DefaultExtension { get; set; } + /// + /// Gets or sets a value indicating whether to display a warning if the user specifies the name of a file that already exists. + /// + public bool? ShowOverwritePrompt { get; set; } + /// /// Shows the save file dialog. /// diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 019bdfb98a..70fecc7ce1 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Linq; using Avalonia.Collections; +using Avalonia.Automation.Peers; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; @@ -9,12 +10,15 @@ using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.VisualTree; +using Avalonia.Automation; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// A tab control that displays a tab strip along with the content of the selected tab. /// + [TemplatePart("PART_ItemsPresenter", typeof(ItemsPresenter))] public class TabControl : SelectingItemsControl, IContentPresenterHost { /// @@ -68,6 +72,7 @@ namespace Avalonia.Controls ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); SelectedItemProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent()); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Tab); } /// @@ -164,7 +169,7 @@ namespace Avalonia.Controls else { var container = SelectedItem as IContentControl ?? - ItemContainerGenerator!.ContainerFromIndex(SelectedIndex) as IContentControl; + ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as IContentControl; SelectedContentTemplate = container?.ContentTemplate; SelectedContent = container?.Content; } diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 593643a1eb..f68db4743b 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -1,3 +1,5 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; @@ -31,6 +33,7 @@ namespace Avalonia.Controls PressedMixin.Attach(); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); DataContextProperty.Changed.AddClassHandler((x, e) => x.UpdateHeader(e)); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.TabItem); } /// @@ -53,6 +56,8 @@ namespace Avalonia.Controls set { SetValue(IsSelectedProperty, value); } } + protected override AutomationPeer OnCreateAutomationPeer() => new ListItemAutomationPeer(this); + private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) { if (Header == null) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c0f5980321..36e5f7236f 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -1,9 +1,13 @@ -using System.Reactive.Linq; -using Avalonia.LogicalTree; +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Documents; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -using Avalonia.Layout; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -24,69 +28,69 @@ namespace Avalonia.Controls public static readonly StyledProperty PaddingProperty = Decorator.PaddingProperty.AddOwner(); - // TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner - // them into TextBlock. - /// /// Defines the property. /// - public static readonly AttachedProperty FontFamilyProperty = - AvaloniaProperty.RegisterAttached( - nameof(FontFamily), - defaultValue: FontFamily.Default, - inherits: true); + public static readonly StyledProperty FontFamilyProperty = + TextElement.FontFamilyProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty FontSizeProperty = - AvaloniaProperty.RegisterAttached( - nameof(FontSize), - defaultValue: 12, - inherits: true); + public static readonly StyledProperty FontSizeProperty = + TextElement.FontSizeProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty FontStyleProperty = - AvaloniaProperty.RegisterAttached( - nameof(FontStyle), - inherits: true); + public static readonly StyledProperty FontStyleProperty = + TextElement.FontStyleProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontWeightProperty = + TextElement.FontWeightProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty FontWeightProperty = - AvaloniaProperty.RegisterAttached( - nameof(FontWeight), - inherits: true, - defaultValue: FontWeight.Normal); + public static readonly StyledProperty FontStretchProperty = + TextElement.FontStretchProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty ForegroundProperty = - AvaloniaProperty.RegisterAttached( - nameof(Foreground), - Brushes.Black, - inherits: true); + public static readonly StyledProperty ForegroundProperty = + TextElement.ForegroundProperty.AddOwner(); + + /// + /// DependencyProperty for property. + /// + public static readonly AttachedProperty BaselineOffsetProperty = + AvaloniaProperty.RegisterAttached( + nameof(BaselineOffset), + 0, + true); /// /// Defines the property. /// - public static readonly StyledProperty LineHeightProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty LineHeightProperty = + AvaloniaProperty.RegisterAttached( nameof(LineHeight), double.NaN, - validate: IsValidLineHeight); + validate: IsValidLineHeight, + inherits: true); /// /// Defines the property. /// - public static readonly StyledProperty MaxLinesProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty MaxLinesProperty = + AvaloniaProperty.RegisterAttached( nameof(MaxLines), - validate: IsValidMaxLines); + validate: IsValidMaxLines, + inherits: true); /// /// Defines the property. @@ -97,23 +101,35 @@ namespace Avalonia.Controls o => o.Text, (o, v) => o.Text = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty InlinesProperty = + AvaloniaProperty.RegisterDirect( + nameof(Inlines), + o => o.Inlines); + /// /// Defines the property. /// - public static readonly StyledProperty TextAlignmentProperty = - AvaloniaProperty.Register(nameof(TextAlignment)); + public static readonly AttachedProperty TextAlignmentProperty = + AvaloniaProperty.RegisterAttached(nameof(TextAlignment), + inherits: true); /// /// Defines the property. /// - public static readonly StyledProperty TextWrappingProperty = - AvaloniaProperty.Register(nameof(TextWrapping)); + public static readonly AttachedProperty TextWrappingProperty = + AvaloniaProperty.RegisterAttached(nameof(TextWrapping), + inherits: true); /// /// Defines the property. /// - public static readonly StyledProperty TextTrimmingProperty = - AvaloniaProperty.Register(nameof(TextTrimming)); + public static readonly AttachedProperty TextTrimmingProperty = + AvaloniaProperty.RegisterAttached(nameof(TextTrimming), + defaultValue: TextTrimming.None, + inherits: true); /// /// Defines the property. @@ -121,7 +137,6 @@ namespace Avalonia.Controls public static readonly StyledProperty TextDecorationsProperty = AvaloniaProperty.Register(nameof(TextDecorations)); - private string? _text; private TextLayout? _textLayout; private Size _constraint; @@ -140,13 +155,15 @@ namespace Avalonia.Controls /// public TextBlock() { - _text = string.Empty; + Inlines = new InlineCollection(this); + + Inlines.Invalidated += InlinesChanged; } /// /// Gets the used to render the text. /// - public TextLayout? TextLayout + public TextLayout TextLayout { get { @@ -175,15 +192,32 @@ namespace Avalonia.Controls /// /// Gets or sets the text. /// - [Content] public string? Text { - get { return _text; } - set { SetAndRaise(TextProperty, ref _text, value); } + get => Inlines.Text; + set + { + var old = Text; + + if (value == old) + { + return; + } + + Inlines.Text = value; + + RaisePropertyChanged(TextProperty, old, value); + } } /// - /// Gets or sets the font family. + /// Gets or sets the inlines. + /// + [Content] + public InlineCollection Inlines { get; } + + /// + /// Gets or sets the font family used to draw the control's text. /// public FontFamily FontFamily { @@ -192,7 +226,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the font size. + /// Gets or sets the size of the control's text in points. /// public double FontSize { @@ -201,7 +235,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the font style. + /// Gets or sets the font style used to draw the control's text. /// public FontStyle FontStyle { @@ -210,7 +244,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the font weight. + /// Gets or sets the font weight used to draw the control's text. /// public FontWeight FontWeight { @@ -219,7 +253,16 @@ namespace Avalonia.Controls } /// - /// Gets or sets a brush used to paint the text. + /// Gets or sets the font stretch used to draw the control's text. + /// + public FontStretch FontStretch + { + get { return GetValue(FontStretchProperty); } + set { SetValue(FontStretchProperty, value); } + } + + /// + /// Gets or sets the brush used to draw the control's text and other foreground elements. /// public IBrush? Foreground { @@ -280,112 +323,193 @@ namespace Avalonia.Controls get => GetValue(TextDecorationsProperty); set => SetValue(TextDecorationsProperty, value); } + + protected override bool BypassFlowDirectionPolicies => true; /// - /// Gets the value of the attached on a control. + /// The BaselineOffset property provides an adjustment to baseline offset /// - /// The control. - /// The font family. - public static FontFamily GetFontFamily(Control control) + public double BaselineOffset { - return control.GetValue(FontFamilyProperty); + get { return (double)GetValue(BaselineOffsetProperty); } + set { SetValue(BaselineOffsetProperty, value); } } /// - /// Gets the value of the attached on a control. + /// Reads the attached property from the given element /// - /// The control. - /// The font family. - public static double GetFontSize(Control control) + /// The element to which to read the attached property. + public static double GetBaselineOffset(Control control) { - return control.GetValue(FontSizeProperty); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(BaselineOffsetProperty); } /// - /// Gets the value of the attached on a control. + /// Writes the attached property BaselineOffset to the given element. /// - /// The control. - /// The font family. - public static FontStyle GetFontStyle(Control control) + /// The element to which to write the attached property. + /// The property value to set + public static void SetBaselineOffset(Control control, double value) { - return control.GetValue(FontStyleProperty); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(BaselineOffsetProperty, value); } /// - /// Gets the value of the attached on a control. + /// Reads the attached property from the given element /// - /// The control. - /// The font family. - public static FontWeight GetFontWeight(Control control) + /// The element to which to read the attached property. + public static TextAlignment GetTextAlignment(Control control) { - return control.GetValue(FontWeightProperty); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(TextAlignmentProperty); } /// - /// Gets the value of the attached on a control. + /// Writes the attached property BaselineOffset to the given element. /// - /// The control. - /// The foreground. - public static IBrush? GetForeground(Control control) + /// The element to which to write the attached property. + /// The property value to set + public static void SetTextAlignment(Control control, TextAlignment alignment) { - return control.GetValue(ForegroundProperty); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(TextAlignmentProperty, alignment); } /// - /// Sets the value of the attached on a control. + /// Reads the attached property from the given element /// - /// The control. - /// The property value to set. - /// The font family. - public static void SetFontFamily(Control control, FontFamily value) + /// The element to which to read the attached property. + public static TextWrapping GetTextWrapping(Control control) { - control.SetValue(FontFamilyProperty, value); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(TextWrappingProperty); } /// - /// Sets the value of the attached on a control. + /// Writes the attached property BaselineOffset to the given element. /// - /// The control. - /// The property value to set. - /// The font family. - public static void SetFontSize(Control control, double value) + /// The element to which to write the attached property. + /// The property value to set + public static void SetTextWrapping(Control control, TextWrapping wrapping) { - control.SetValue(FontSizeProperty, value); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(TextWrappingProperty, wrapping); } /// - /// Sets the value of the attached on a control. + /// Reads the attached property from the given element /// - /// The control. - /// The property value to set. - /// The font family. - public static void SetFontStyle(Control control, FontStyle value) + /// The element to which to read the attached property. + public static TextTrimming GetTextTrimming(Control control) { - control.SetValue(FontStyleProperty, value); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(TextTrimmingProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetTextTrimming(Control control, TextTrimming trimming) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(TextTrimmingProperty, trimming); } /// - /// Sets the value of the attached on a control. + /// Reads the attached property from the given element /// - /// The control. - /// The property value to set. - /// The font family. - public static void SetFontWeight(Control control, FontWeight value) + /// The element to which to read the attached property. + public static double GetLineHeight(Control control) { - control.SetValue(FontWeightProperty, value); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(LineHeightProperty); } /// - /// Sets the value of the attached on a control. + /// Writes the attached property BaselineOffset to the given element. /// - /// The control. - /// The property value to set. - /// The font family. - public static void SetForeground(Control control, IBrush? value) + /// The element to which to write the attached property. + /// The property value to set + public static void SetLineHeight(Control control, double height) { - control.SetValue(ForegroundProperty, value); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(LineHeightProperty, height); + } + + /// + /// Reads the attached property from the given element + /// + /// The element to which to read the attached property. + public static int GetMaxLines(Control control) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(MaxLinesProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetMaxLines(Control control, int maxLines) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(MaxLinesProperty, maxLines); } + /// /// Renders the to a drawing context. /// @@ -399,25 +523,20 @@ namespace Avalonia.Controls context.FillRectangle(background, new Rect(Bounds.Size)); } - if (TextLayout is null) - { - return; - } - var padding = Padding; var top = padding.Top; - var textSize = TextLayout.Size; + var textHeight = TextLayout.Bounds.Height; - if (Bounds.Height < textSize.Height) + if (Bounds.Height < textHeight) { switch (VerticalAlignment) { case VerticalAlignment.Center: - top += (Bounds.Height - textSize.Height) / 2; + top += (Bounds.Height - textHeight) / 2; break; case VerticalAlignment.Bottom: - top += (Bounds.Height - textSize.Height); + top += (Bounds.Height - textHeight); break; } } @@ -431,16 +550,28 @@ namespace Avalonia.Controls /// The constraint of the text. /// The text to format. /// A object. - protected virtual TextLayout? CreateTextLayout(Size constraint, string? text) + protected virtual TextLayout CreateTextLayout(Size constraint, string? text) { - if (constraint == Size.Empty) + List>? textStyleOverrides = null; + + if (Inlines.HasComplexContent) { - return null; + textStyleOverrides = new List>(Inlines.Count); + + var textPosition = 0; + var stringBuilder = new StringBuilder(); + + foreach (var inline in Inlines) + { + textPosition += inline.BuildRun(stringBuilder, textStyleOverrides, textPosition); + } + + text = stringBuilder.ToString(); } return new TextLayout( text ?? string.Empty, - new Typeface(FontFamily, FontStyle, FontWeight), + new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), FontSize, Foreground ?? Brushes.Transparent, TextAlignment, @@ -451,7 +582,8 @@ namespace Avalonia.Controls constraint.Width, constraint.Height, maxLines: MaxLines, - lineHeight: LineHeight); + lineHeight: LineHeight, + textStyleOverrides: textStyleOverrides); } /// @@ -464,39 +596,43 @@ namespace Avalonia.Controls InvalidateMeasure(); } - /// - /// Measures the control. - /// - /// The available size for the control. - /// The desired size. protected override Size MeasureOverride(Size availableSize) { - if (string.IsNullOrEmpty(Text)) + if (!Inlines.HasComplexContent && string.IsNullOrEmpty(Text)) { return new Size(); } var padding = Padding; + + _constraint = availableSize.Deflate(padding); + + _textLayout = null; - availableSize = availableSize.Deflate(padding); + InvalidateArrange(); - if (_constraint != availableSize) - { - _constraint = availableSize; + var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); - InvalidateTextLayout(); - } + return new Size(measuredSize.Width, measuredSize.Height).Inflate(padding); + } - var measuredSize = TextLayout?.Size ?? Size.Empty; + protected override Size ArrangeOverride(Size finalSize) + { + if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) + { + return finalSize; + } + + _constraint = new Size(finalSize.Width, Math.Ceiling(finalSize.Height)); + + _textLayout = null; - return measuredSize.Inflate(padding); + return finalSize; } - protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + protected override AutomationPeer OnCreateAutomationPeer() { - base.OnAttachedToLogicalTree(e); - - InvalidateTextLayout(); + return new TextBlockAutomationPeer(this); } private static bool IsValidMaxLines(int maxLines) => maxLines >= 0; @@ -513,15 +649,19 @@ namespace Avalonia.Controls case nameof (FontWeight): case nameof (FontStyle): case nameof (FontFamily): + case nameof (FontStretch): case nameof (TextWrapping): case nameof (TextTrimming): case nameof (TextAlignment): + case nameof (FlowDirection): case nameof (Padding): case nameof (LineHeight): case nameof (MaxLines): + + case nameof (InlinesProperty): case nameof (Text): case nameof (TextDecorations): @@ -531,6 +671,11 @@ namespace Avalonia.Controls break; } } + } + + private void InlinesChanged(object? sender, EventArgs e) + { + InvalidateTextLayout(); } } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index dc6b8ffa4c..947830b217 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -16,12 +16,14 @@ using Avalonia.Utilities; using Avalonia.Controls.Metadata; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Automation.Peers; namespace Avalonia.Controls { /// /// Represents a control that can be used to display or edit unformatted text. /// + [TemplatePart("PART_TextPresenter", typeof(TextPresenter))] [PseudoClasses(":empty")] public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost { @@ -76,6 +78,9 @@ namespace Avalonia.Controls public static readonly StyledProperty MaxLengthProperty = AvaloniaProperty.Register(nameof(MaxLength), defaultValue: 0); + public static readonly StyledProperty MaxLinesProperty = + AvaloniaProperty.Register(nameof(MaxLines), defaultValue: 0); + public static readonly DirectProperty TextProperty = TextBlock.TextProperty.AddOwnerWithDataValidation( o => o.Text, @@ -201,7 +206,10 @@ namespace Avalonia.Controls FocusableProperty.OverrideDefaultValue(typeof(TextBox), true); TextInputMethodClientRequestedEvent.AddClassHandler((tb, e) => { - e.Client = tb._imClient; + if (!tb.IsReadOnly) + { + e.Client = tb._imClient; + } }); } @@ -256,6 +264,8 @@ namespace Avalonia.Controls UndoRedoState state; if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) _undoRedoHelper.UpdateLastState(); + + SelectionStart = SelectionEnd = value; } } @@ -300,14 +310,15 @@ namespace Avalonia.Controls { value = CoerceCaretIndex(value); var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value); + if (changed) { UpdateCommandStates(); } - - if (value == SelectionEnd) + + if (SelectionEnd == value && CaretIndex != value) { - CaretIndex = SelectionStart; + CaretIndex = value; } } } @@ -328,8 +339,8 @@ namespace Avalonia.Controls { UpdateCommandStates(); } - - if (value == SelectionStart) + + if (SelectionStart == value && CaretIndex != value) { CaretIndex = value; } @@ -342,6 +353,12 @@ namespace Avalonia.Controls set { SetValue(MaxLengthProperty, value); } } + public int MaxLines + { + get { return GetValue(MaxLinesProperty); } + set { SetValue(MaxLinesProperty, value); } + } + [Content] public string? Text { @@ -351,10 +368,12 @@ namespace Avalonia.Controls if (!_ignoreTextChanges) { var caretIndex = CaretIndex; + var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; - SelectionStart = CoerceCaretIndex(SelectionStart, value); - SelectionEnd = CoerceCaretIndex(SelectionEnd, value); CaretIndex = CoerceCaretIndex(caretIndex, value); + SelectionStart = CoerceCaretIndex(selectionStart, value); + SelectionEnd = CoerceCaretIndex(selectionEnd, value); if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) { @@ -457,7 +476,7 @@ namespace Avalonia.Controls /// public void ClearSelection() { - SelectionStart = SelectionEnd = CaretIndex; + CaretIndex = SelectionStart; } /// @@ -540,14 +559,19 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); + + _imClient.SetPresenter(_presenter, this); + + if (IsFocused) + { + _presenter?.ShowCaret(); + } } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - _imClient.SetPresenter(_presenter, this); - if (IsFocused) { _presenter?.ShowCaret(); @@ -607,6 +631,8 @@ namespace Avalonia.Controls } UpdateCommandStates(); + + _imClient.SetPresenter(_presenter, this); _presenter?.ShowCaret(); } @@ -625,6 +651,8 @@ namespace Avalonia.Controls UpdateCommandStates(); _presenter?.HideCaret(); + + _imClient.SetPresenter(null, null); } protected override void OnTextInput(TextInputEventArgs e) @@ -652,6 +680,39 @@ namespace Avalonia.Controls _selectedTextChangesMadeSinceLastUndoSnapshot++; SnapshotUndoRedo(ignoreChangeCount: false); + if (_presenter != null && MaxLines > 0) + { + var lineCount = _presenter.TextLayout.TextLines.Count; + + var length = 0; + + var graphemeEnumerator = new GraphemeEnumerator(input.AsMemory()); + + while (graphemeEnumerator.MoveNext()) + { + var grapheme = graphemeEnumerator.Current; + + if (grapheme.FirstCodepoint.IsBreakChar) + { + if(lineCount + 1 > MaxLines) + { + break; + } + else + { + lineCount++; + } + } + + length += grapheme.Text.Length; + } + + if (length < input.Length) + { + input = input.Remove(Math.Max(0, length)); + } + } + var text = Text ?? string.Empty; var newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); @@ -846,6 +907,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheEndOfDocument)) { @@ -853,6 +915,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheStartOfLine)) { @@ -860,7 +923,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; - + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheEndOfLine)) { @@ -868,24 +931,31 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheStartOfDocumentWithSelection)) { + SelectionStart = caretIndex; MoveHome(true); + SelectionEnd = _presenter.CaretIndex; movement = true; selection = true; handled = true; } else if (Match(keymap.MoveCursorToTheEndOfDocumentWithSelection)) { + SelectionStart = caretIndex; MoveEnd(true); + SelectionEnd = _presenter.CaretIndex; movement = true; selection = true; handled = true; } else if (Match(keymap.MoveCursorToTheStartOfLineWithSelection)) { + SelectionStart = caretIndex; MoveHome(false); + SelectionEnd = _presenter.CaretIndex; movement = true; selection = true; handled = true; @@ -893,7 +963,9 @@ namespace Avalonia.Controls } else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection)) { + SelectionStart = caretIndex; MoveEnd(false); + SelectionEnd = _presenter.CaretIndex; movement = true; selection = true; handled = true; @@ -930,6 +1002,10 @@ namespace Avalonia.Controls { SelectionEnd = _presenter.CaretIndex; } + else + { + CaretIndex = _presenter.CaretIndex; + } break; } @@ -948,6 +1024,10 @@ namespace Avalonia.Controls { SelectionEnd = _presenter.CaretIndex; } + else + { + CaretIndex = _presenter.CaretIndex; + } break; } @@ -960,34 +1040,28 @@ namespace Avalonia.Controls SetSelectionForControlBackspace(); } - if (!DeleteSelection() && caretIndex > 0) + if (!DeleteSelection()) { - var removedCharacters = 0; - - // \r\n needs special treatment here - if (caretIndex - 1 > 0 && text[caretIndex - 1] == '\n' && text[caretIndex - 2] == '\r') - { - removedCharacters = 2; - } - else - { - Codepoint.ReadAt(text.AsMemory(), caretIndex - 1, out removedCharacters); - } + var characterHit = _presenter.GetNextCharacterHit(LogicalDirection.Backward); - if (removedCharacters == 0) + var backspacePosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + if (caretIndex != backspacePosition) { - return; - } + var start = Math.Min(backspacePosition, caretIndex); + var end = Math.Max(backspacePosition, caretIndex); + + var length = end - start; - var length = Math.Max(0, caretIndex - removedCharacters); + var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); - SetTextInternal(text.Substring(0, length) + - text.Substring(caretIndex)); - - CaretIndex = caretIndex - removedCharacters; - - ClearSelection(); + SetTextInternal(editedText); + + CaretIndex = start; + } } + + SnapshotUndoRedo(); handled = true; break; @@ -1000,16 +1074,21 @@ namespace Avalonia.Controls SetSelectionForControlDelete(); } - if (!DeleteSelection() && caretIndex < text.Length) + if (!DeleteSelection()) { - _presenter.MoveCaretHorizontal(); + var characterHit = _presenter.GetNextCharacterHit(); + + var nextPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - var removedCharacters = Math.Max(0, _presenter.CaretIndex - caretIndex); + if(nextPosition != caretIndex) + { + var start = Math.Min(nextPosition, caretIndex); + var end = Math.Max(nextPosition, caretIndex); - SetTextInternal(text.Substring(0, caretIndex) + - text.Substring(caretIndex + removedCharacters)); + var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); - CaretIndex = caretIndex; + SetTextInternal(editedText); + } } SnapshotUndoRedo(); @@ -1178,6 +1257,11 @@ namespace Avalonia.Controls e.Pointer.Capture(null); } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new TextBoxAutomationPeer(this); + } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { if (property == TextProperty) @@ -1221,40 +1305,69 @@ namespace Avalonia.Controls private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting) { + if (_presenter == null) + { + return; + } + var text = Text ?? string.Empty; var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; if (!wholeWord) { - if (_presenter == null) - { - return; - } - - _presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward); - if (isSelecting) { + _presenter.MoveCaretToTextPosition(selectionEnd); + + _presenter.MoveCaretHorizontal(direction > 0 ? + LogicalDirection.Forward : + LogicalDirection.Backward); + SelectionEnd = _presenter.CaretIndex; } else { - SelectionStart = SelectionEnd = _presenter.CaretIndex; + if (selectionStart != selectionEnd) + { + _presenter.MoveCaretToTextPosition(direction > 0 ? + Math.Max(selectionStart, selectionEnd) : + Math.Min(selectionStart, selectionEnd)); + } + else + { + _presenter.MoveCaretHorizontal(direction > 0 ? + LogicalDirection.Forward : + LogicalDirection.Backward); + } + + CaretIndex = _presenter.CaretIndex; } } else { + int offset; + if (direction > 0) { - var offset = StringUtils.NextWord(text, selectionStart) - selectionStart; - - CaretIndex += offset; + offset = StringUtils.NextWord(text, selectionEnd) - selectionEnd; } else { - var offset = StringUtils.PreviousWord(text, selectionStart) - selectionStart; - - CaretIndex += offset; + offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd; + } + + SelectionEnd += offset; + + _presenter.MoveCaretToTextPosition(SelectionEnd); + + if (!isSelecting) + { + CaretIndex = SelectionEnd; + } + else + { + SelectionStart = selectionStart; } } } @@ -1266,32 +1379,20 @@ namespace Avalonia.Controls return; } - var text = Text ?? string.Empty; var caretIndex = CaretIndex; if (document) { - caretIndex = 0; + _presenter.MoveCaretToTextPosition(0); } - else if (_presenter.TextLayout is not null) + else { - var lines = _presenter.TextLayout.TextLines; - var pos = 0; - - foreach (var line in lines) - { - if (pos + line.TextRange.Length > caretIndex || pos + line.TextRange.Length == text.Length) - { - break; - } + var textLines = _presenter.TextLayout.TextLines; + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false); + var textLine = textLines[lineIndex]; - pos += line.TextRange.Length; - } - - caretIndex = pos; + _presenter.MoveCaretToTextPosition(textLine.FirstTextSourceIndex); } - - CaretIndex = caretIndex; } private void MoveEnd(bool document) @@ -1306,36 +1407,18 @@ namespace Avalonia.Controls if (document) { - caretIndex = text.Length; + _presenter.MoveCaretToTextPosition(text.Length, true); } - else if (_presenter.TextLayout is not null) + else { - var lines = _presenter.TextLayout.TextLines; - var pos = 0; - - foreach (var line in lines) - { - pos += line.TextRange.Length; + var textLines = _presenter.TextLayout.TextLines; + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false); + var textLine = textLines[lineIndex]; - if (pos > caretIndex) - { - if (pos < text.Length) - { - --pos; - if (pos > 0 && text[pos - 1] == '\r' && text[pos] == '\n') - { - --pos; - } - } + var textPosition = textLine.FirstTextSourceIndex + textLine.Length; - break; - } - } - - caretIndex = pos; + _presenter.MoveCaretToTextPosition(textPosition, true); } - - CaretIndex = caretIndex; } /// @@ -1345,50 +1428,56 @@ namespace Avalonia.Controls { SelectionStart = 0; SelectionEnd = Text?.Length ?? 0; - CaretIndex = SelectionEnd; } private bool DeleteSelection(bool raiseTextChanged = true) { - if (!IsReadOnly) - { - var selectionStart = SelectionStart; - var selectionEnd = SelectionEnd; + if (IsReadOnly) return true; + + var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; - if (selectionStart != selectionEnd) - { - var start = Math.Min(selectionStart, selectionEnd); - var end = Math.Max(selectionStart, selectionEnd); - var text = Text!; - SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged); - CaretIndex = start; - ClearSelection(); - return true; - } - else - { - return false; - } - } - else + if (selectionStart != selectionEnd) { + var start = Math.Min(selectionStart, selectionEnd); + var end = Math.Max(selectionStart, selectionEnd); + var text = Text!; + + SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged); + + _presenter?.MoveCaretToTextPosition(start); + + CaretIndex= start; + + ClearSelection(); + return true; } + + CaretIndex = SelectionStart; + + return false; } private string GetSelection() { var text = Text; + if (string.IsNullOrEmpty(text)) + { return ""; + } + var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; var start = Math.Min(selectionStart, selectionEnd); var end = Math.Max(selectionStart, selectionEnd); + if (start == end || (Text?.Length ?? 0) < end) { return ""; } + return text.Substring(start, end - start); } @@ -1414,16 +1503,28 @@ namespace Avalonia.Controls private void SetSelectionForControlBackspace() { - SelectionStart = CaretIndex; + var selectionStart = CaretIndex; + MoveHorizontal(-1, true, false); - SelectionEnd = CaretIndex; + + SelectionStart = selectionStart; } private void SetSelectionForControlDelete() { + if (_text == null || _presenter == null) + { + return; + } + SelectionStart = CaretIndex; - MoveHorizontal(1, true, false); - SelectionEnd = CaretIndex; + + MoveHorizontal(1, true, true); + + if (SelectionEnd < _text.Length && _text[SelectionEnd] == ' ') + { + SelectionEnd++; + } } private void UpdatePseudoclasses() diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index f33f2b9df3..fd6c202c6f 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -9,6 +9,8 @@ namespace Avalonia.Controls /// /// A Toggle Switch control. /// + [TemplatePart("MovingKnobs", typeof(Panel))] + [TemplatePart("SwitchKnob", typeof(Panel))] [PseudoClasses(":dragging")] public class ToggleSwitch : ToggleButton { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 6bba889748..75a34659a2 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -14,7 +15,6 @@ using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; -using JetBrains.Annotations; namespace Avalonia.Controls { @@ -26,6 +26,7 @@ namespace Avalonia.Controls /// It handles scheduling layout, styling and rendering as well as /// tracking the widget's . /// + [TemplatePart("PART_TransparencyFallback", typeof(Border))] public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, @@ -85,6 +86,8 @@ namespace Avalonia.Controls private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; private readonly IPlatformRenderInterface? _renderInterface; private readonly IGlobalStyles? _globalStyles; + private readonly PointerOverPreProcessor? _pointerOverPreProcessor; + private readonly IDisposable? _pointerOverPreProcessorSubscription; private Size _clientSize; private Size? _frameSize; private WindowTransparencyLevel _actualTransparencyLevel; @@ -134,8 +137,6 @@ namespace Avalonia.Controls "Could not create window implementation: maybe no windowing subsystem was initialized?"); } - impl = ValidatingToplevelImpl.Wrap(impl); - PlatformImpl = impl; _actualTransparencyLevel = PlatformImpl.TransparencyLevel; @@ -195,6 +196,9 @@ namespace Avalonia.Controls } impl.LostFocus += PlatformImpl_LostFocus; + + _pointerOverPreProcessor = new PointerOverPreProcessor(this); + _pointerOverPreProcessorSubscription = _inputManager?.PreProcess.Subscribe(_pointerOverPreProcessor); } /// @@ -283,9 +287,7 @@ namespace Avalonia.Controls /// IKeyboardNavigationHandler IInputRoot.KeyboardNavigationHandler => _keyboardNavigationHandler!; - /// - /// Gets or sets the input element that the pointer is currently over. - /// + /// IInputElement? IInputRoot.PointerOverElement { get { return GetValue(PointerOverElementProperty); } @@ -350,6 +352,12 @@ namespace Avalonia.Controls /// protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this); + public override void InvalidateMirrorTransform() + { + } + + protected override bool BypassFlowDirectionPolicies => true; + /// /// Handles a paint notification from . /// @@ -372,10 +380,12 @@ namespace Avalonia.Controls Renderer?.Dispose(); Renderer = null!; - - (this as IInputRoot).MouseDevice?.TopLevelClosed(this); + + _pointerOverPreProcessor?.OnCompleted(); + _pointerOverPreProcessorSubscription?.Dispose(); + PlatformImpl = null; - + var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); @@ -509,12 +519,17 @@ namespace Avalonia.Controls /// The event args. private void HandleInput(RawInputEventArgs e) { + if (e is RawPointerEventArgs pointerArgs) + { + pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position); + } + _inputManager?.ProcessInput(e); } private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) { - (this as IInputRoot).MouseDevice?.SceneInvalidated(this, e.DirtyRect); + _pointerOverPreProcessor?.SceneInvalidated(e.DirtyRect); } void PlatformImpl_LostFocus() diff --git a/src/Avalonia.Controls/TransitioningContentControl.cs b/src/Avalonia.Controls/TransitioningContentControl.cs new file mode 100644 index 0000000000..cb0d229110 --- /dev/null +++ b/src/Avalonia.Controls/TransitioningContentControl.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading; +using Avalonia.Animation; +using Avalonia.Controls.Templates; +using Avalonia.Threading; + +namespace Avalonia.Controls; + +/// +/// Displays according to a . +/// Uses to move between the old and new content values. +/// +public class TransitioningContentControl : ContentControl +{ + private CancellationTokenSource? _lastTransitionCts; + private object? _currentContent; + + /// + /// Defines the property. + /// + public static readonly StyledProperty PageTransitionProperty = + AvaloniaProperty.Register(nameof(PageTransition), + new CrossFade(TimeSpan.FromSeconds(0.125))); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CurrentContentProperty = + AvaloniaProperty.RegisterDirect(nameof(CurrentContent), + o => o.CurrentContent); + + /// + /// Gets or sets the animation played when content appears and disappears. + /// + public IPageTransition? PageTransition + { + get => GetValue(PageTransitionProperty); + set => SetValue(PageTransitionProperty, value); + } + + /// + /// Gets the content currently displayed on the screen. + /// + public object? CurrentContent + { + get => _currentContent; + private set => SetAndRaise(CurrentContentProperty, ref _currentContent, value); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + Dispatcher.UIThread.Post(() => UpdateContentWithTransition(Content)); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _lastTransitionCts?.Cancel(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ContentProperty) + { + Dispatcher.UIThread.Post(() => UpdateContentWithTransition(Content)); + } + } + + /// + /// Updates the content with transitions. + /// + /// New content to set. + private async void UpdateContentWithTransition(object? content) + { + if (VisualRoot is null) + { + return; + } + + _lastTransitionCts?.Cancel(); + _lastTransitionCts = new CancellationTokenSource(); + + if (PageTransition != null) + await PageTransition.Start(this, null, true, _lastTransitionCts.Token); + + CurrentContent = content; + + if (PageTransition != null) + await PageTransition.Start(null, this, true, _lastTransitionCts.Token); + } +} diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 9e0a31ad64..1d806913dd 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -79,7 +79,7 @@ namespace Avalonia.Controls /// Gets the for the tree view. /// public new ITreeItemContainerGenerator ItemContainerGenerator => - (ITreeItemContainerGenerator)base.ItemContainerGenerator!; + (ITreeItemContainerGenerator)base.ItemContainerGenerator; /// /// Gets or sets a value indicating whether to automatically scroll to newly selected items. @@ -849,7 +849,7 @@ namespace Avalonia.Controls /// The desired items. private static void SynchronizeItems(IList items, IEnumerable desired) { - var list = items.Cast().ToList(); + var list = items.Cast(); var toRemove = list.Except(desired).ToList(); var toAdd = desired.Except(list).ToList(); diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 79e8a199f9..a0a3c09942 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -12,6 +12,7 @@ namespace Avalonia.Controls /// /// An item in a . /// + [TemplatePart("PART_Header", typeof(IControl))] [PseudoClasses(":pressed", ":selected")] public class TreeViewItem : HeaderedItemsControl, ISelectable { @@ -89,7 +90,7 @@ namespace Avalonia.Controls /// Gets the for the tree view. /// public new ITreeItemContainerGenerator ItemContainerGenerator => - (ITreeItemContainerGenerator)base.ItemContainerGenerator!; + (ITreeItemContainerGenerator)base.ItemContainerGenerator; /// protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator(); diff --git a/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs b/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs index 0b1c4fc90e..9d13daa453 100644 --- a/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs +++ b/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs @@ -105,7 +105,7 @@ namespace Avalonia.Controls.Utils static void Notify( INotifyCollectionChanged incc, NotifyCollectionChangedEventArgs args, - List> listeners) + WeakReference[] listeners) { foreach (var l in listeners) { @@ -132,7 +132,7 @@ namespace Avalonia.Controls.Utils } } - var l = Listeners.ToList(); + var l = Listeners.ToArray(); if (Dispatcher.UIThread.CheckAccess()) { diff --git a/src/Avalonia.Controls/Utils/StringUtils.cs b/src/Avalonia.Controls/Utils/StringUtils.cs index 53937003c8..b2e56434b2 100644 --- a/src/Avalonia.Controls/Utils/StringUtils.cs +++ b/src/Avalonia.Controls/Utils/StringUtils.cs @@ -150,17 +150,23 @@ namespace Avalonia.Controls.Utils return cursor; } - CharClass cc = GetCharClass(text[cursor]); i = cursor; - // skip over the word, punctuation, or run of whitespace - while (i < cr && GetCharClass(text[i]) == cc) + // skip any whitespace after the word/punct + while (i < cr && char.IsWhiteSpace(text[i])) { i++; } - // skip any whitespace after the word/punct - while (i < cr && char.IsWhiteSpace(text[i])) + if (i >= cr) + { + return i; + } + + var cc = GetCharClass(text[i]); + + // skip over the word, punctuation, or run of whitespace + while (i < cr && GetCharClass(text[i]) == cc) { i++; } diff --git a/src/Avalonia.Controls/ValidatingToplevel.cs b/src/Avalonia.Controls/ValidatingToplevel.cs deleted file mode 100644 index 7e15bf4879..0000000000 --- a/src/Avalonia.Controls/ValidatingToplevel.cs +++ /dev/null @@ -1,344 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Controls.Platform; -using Avalonia.Controls.Primitives.PopupPositioning; -using Avalonia.Input; -using Avalonia.Input.Raw; -using Avalonia.Input.TextInput; -using Avalonia.Platform; -using Avalonia.Rendering; - -namespace Avalonia.Controls; - -internal class ValidatingToplevelImpl : ITopLevelImpl, ITopLevelImplWithNativeControlHost, - ITopLevelImplWithNativeMenuExporter, ITopLevelImplWithTextInputMethod -{ - private readonly ITopLevelImpl _impl; - private bool _disposed; - - public ValidatingToplevelImpl(ITopLevelImpl impl) - { - _impl = impl ?? throw new InvalidOperationException( - "Could not create TopLevel implementation: maybe no windowing subsystem was initialized?"); - } - - public void Dispose() - { - _disposed = true; - _impl.Dispose(); - } - - protected void CheckDisposed() - { - if (_disposed) - throw new ObjectDisposedException(_impl.GetType().FullName); - } - - protected ITopLevelImpl Inner - { - get - { - CheckDisposed(); - return _impl; - } - } - - public static ITopLevelImpl Wrap(ITopLevelImpl impl) - { -#if DEBUG - if (impl is ValidatingToplevelImpl) - return impl; - return new ValidatingToplevelImpl(impl); -#else - return impl; -#endif - } - - public Size ClientSize => Inner.ClientSize; - public Size? FrameSize => Inner.FrameSize; - public double RenderScaling => Inner.RenderScaling; - public IEnumerable Surfaces => Inner.Surfaces; - - public Action? Input - { - get => Inner.Input; - set => Inner.Input = value; - } - - public Action? Paint - { - get => Inner.Paint; - set => Inner.Paint = value; - } - - public Action? Resized - { - get => Inner.Resized; - set => Inner.Resized = value; - } - - public Action? ScalingChanged - { - get => Inner.ScalingChanged; - set => Inner.ScalingChanged = value; - } - - public Action? TransparencyLevelChanged - { - get => Inner.TransparencyLevelChanged; - set => Inner.TransparencyLevelChanged = value; - } - - public IRenderer CreateRenderer(IRenderRoot root) => Inner.CreateRenderer(root); - - public void Invalidate(Rect rect) => Inner.Invalidate(rect); - - public void SetInputRoot(IInputRoot inputRoot) => Inner.SetInputRoot(inputRoot); - - public Point PointToClient(PixelPoint point) => Inner.PointToClient(point); - - public PixelPoint PointToScreen(Point point) => Inner.PointToScreen(point); - - public void SetCursor(ICursorImpl? cursor) => Inner.SetCursor(cursor); - - public Action? Closed - { - get => Inner.Closed; - set => Inner.Closed = value; - } - - public Action? LostFocus - { - get => Inner.LostFocus; - set => Inner.LostFocus = value; - } - - // Exception: for some reason we are notifying platform mouse device from TopLevel.cs - public IMouseDevice MouseDevice => _impl.MouseDevice; - public IPopupImpl? CreatePopup() => Inner.CreatePopup(); - - public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => - Inner.SetTransparencyLevelHint(transparencyLevel); - - - public WindowTransparencyLevel TransparencyLevel => Inner.TransparencyLevel; - public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => Inner.AcrylicCompensationLevels; - public INativeControlHostImpl? NativeControlHost => (Inner as ITopLevelImplWithNativeControlHost)?.NativeControlHost; - - public ITopLevelNativeMenuExporter? NativeMenuExporter => - (Inner as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter; - - public ITextInputMethodImpl? TextInputMethod => (Inner as ITopLevelImplWithTextInputMethod)?.TextInputMethod; -} - -internal class ValidatingWindowBaseImpl : ValidatingToplevelImpl, IWindowBaseImpl -{ - private readonly IWindowBaseImpl _impl; - - public ValidatingWindowBaseImpl(IWindowBaseImpl impl) : base(impl) - { - _impl = impl; - } - - protected new IWindowBaseImpl Inner - { - get - { - CheckDisposed(); - return _impl; - } - } - - public static IWindowBaseImpl Wrap(IWindowBaseImpl impl) - { -#if DEBUG - if (impl is ValidatingToplevelImpl) - return impl; - return new ValidatingWindowBaseImpl(impl); -#else - return impl; -#endif - } - - public void Show(bool activate, bool isDialog) => Inner.Show(activate, isDialog); - - public void Hide() => Inner.Hide(); - - public double DesktopScaling => Inner.DesktopScaling; - public PixelPoint Position => Inner.Position; - - public Action? PositionChanged - { - get => Inner.PositionChanged; - set => Inner.PositionChanged = value; - } - - public void Activate() => Inner.Activate(); - - public Action? Deactivated - { - get => Inner.Deactivated; - set => Inner.Deactivated = value; - } - - public Action? Activated - { - get => Inner.Activated; - set => Inner.Activated = value; - } - - public IPlatformHandle Handle => Inner.Handle; - public Size MaxAutoSizeHint => Inner.MaxAutoSizeHint; - public void SetTopmost(bool value) => Inner.SetTopmost(value); - public IScreenImpl Screen => Inner.Screen; -} - -internal class ValidatingWindowImpl : ValidatingWindowBaseImpl, IWindowImpl -{ - private readonly IWindowImpl _impl; - - public ValidatingWindowImpl(IWindowImpl impl) : base(impl) - { - _impl = impl; - } - - protected new IWindowImpl Inner - { - get - { - CheckDisposed(); - return _impl; - } - } - - public static IWindowImpl Unwrap(IWindowImpl impl) - { - if (impl is ValidatingWindowImpl v) - return v.Inner; - return impl; - } - - public static IWindowImpl Wrap(IWindowImpl impl) - { -#if DEBUG - if (impl is ValidatingToplevelImpl) - return impl; - return new ValidatingWindowImpl(impl); -#else - return impl; -#endif - } - - public WindowState WindowState - { - get => Inner.WindowState; - set => Inner.WindowState = value; - } - - public Action WindowStateChanged - { - get => Inner.WindowStateChanged; - set => Inner.WindowStateChanged = value; - } - - public void SetTitle(string? title) => Inner.SetTitle(title); - - public void SetParent(IWindowImpl parent) - { - //Workaround. SetParent will cast IWindowImpl to WindowImpl but ValidatingWindowImpl isn't actual WindowImpl so it will fail with InvalidCastException. - if (parent is ValidatingWindowImpl validatingToplevelImpl) - { - Inner.SetParent(validatingToplevelImpl.Inner); - } - else - { - Inner.SetParent(parent); - } - } - - public void SetEnabled(bool enable) => Inner.SetEnabled(enable); - - public Action GotInputWhenDisabled - { - get => Inner.GotInputWhenDisabled; - set => Inner.GotInputWhenDisabled = value; - } - - public void SetSystemDecorations(SystemDecorations enabled) => Inner.SetSystemDecorations(enabled); - - public void SetIcon(IWindowIconImpl? icon) => Inner.SetIcon(icon); - - public void ShowTaskbarIcon(bool value) => Inner.ShowTaskbarIcon(value); - - public void CanResize(bool value) => Inner.CanResize(value); - - public Func Closing - { - get => Inner.Closing; - set => Inner.Closing = value; - } - - public bool IsClientAreaExtendedToDecorations => Inner.IsClientAreaExtendedToDecorations; - - public Action ExtendClientAreaToDecorationsChanged - { - get => Inner.ExtendClientAreaToDecorationsChanged; - set => Inner.ExtendClientAreaToDecorationsChanged = value; - } - - public bool NeedsManagedDecorations => Inner.NeedsManagedDecorations; - public Thickness ExtendedMargins => Inner.ExtendedMargins; - public Thickness OffScreenMargin => Inner.OffScreenMargin; - public void BeginMoveDrag(PointerPressedEventArgs e) => Inner.BeginMoveDrag(e); - - public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => Inner.BeginResizeDrag(edge, e); - - public void Resize(Size clientSize, PlatformResizeReason reason) => - Inner.Resize(clientSize, reason); - - public void Move(PixelPoint point) => Inner.Move(point); - - public void SetMinMaxSize(Size minSize, Size maxSize) => Inner.SetMinMaxSize(minSize, maxSize); - - public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) => - Inner.SetExtendClientAreaToDecorationsHint(extendIntoClientAreaHint); - - public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) => - Inner.SetExtendClientAreaChromeHints(hints); - - public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) => - Inner.SetExtendClientAreaTitleBarHeightHint(titleBarHeight); -} - -internal class ValidatingPopupImpl : ValidatingWindowBaseImpl, IPopupImpl -{ - private readonly IPopupImpl _impl; - - public ValidatingPopupImpl(IPopupImpl impl) : base(impl) - { - _impl = impl; - } - - protected new IPopupImpl Inner - { - get - { - CheckDisposed(); - return _impl; - } - } - - public static IPopupImpl Wrap(IPopupImpl impl) - { -#if DEBUG - if (impl is ValidatingToplevelImpl) - return impl; - return new ValidatingPopupImpl(impl); -#else - return impl; -#endif - } - - public IPopupPositioner PopupPositioner => Inner.PopupPositioner; - public void SetWindowManagerAddShadowHint(bool enabled) => Inner.SetWindowManagerAddShadowHint(enabled); -} diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs index 624c61bb82..50b9560cac 100644 --- a/src/Avalonia.Controls/Viewbox.cs +++ b/src/Avalonia.Controls/Viewbox.cs @@ -1,13 +1,15 @@ using Avalonia.Media; +using Avalonia.Metadata; namespace Avalonia.Controls { /// /// Viewbox is used to scale single child to fit in the available space. /// - /// - public class Viewbox : Decorator + public class Viewbox : Control { + private Decorator _containerVisual; + /// /// Defines the property. /// @@ -20,12 +22,27 @@ namespace Avalonia.Controls public static readonly StyledProperty StretchDirectionProperty = AvaloniaProperty.Register(nameof(StretchDirection), StretchDirection.Both); + /// + /// Defines the property + /// + public static readonly StyledProperty ChildProperty = + Decorator.ChildProperty.AddOwner(); + static Viewbox() { ClipToBoundsProperty.OverrideDefaultValue(true); + UseLayoutRoundingProperty.OverrideDefaultValue(true); AffectsMeasure(StretchProperty, StretchDirectionProperty); } + public Viewbox() + { + _containerVisual = new Decorator(); + _containerVisual.RenderTransformOrigin = RelativePoint.TopLeft; + LogicalChildren.Add(_containerVisual); + VisualChildren.Add(_containerVisual); + } + /// /// Gets or sets the stretch mode, /// which determines how child fits into the available space. @@ -45,9 +62,40 @@ namespace Avalonia.Controls set => SetValue(StretchDirectionProperty, value); } + /// + /// Gets or sets the child of the Viewbox + /// + [Content] + public IControl? Child + { + get => GetValue(ChildProperty); + set => SetValue(ChildProperty, value); + } + + /// + /// Gets or sets the transform applied to the container visual that + /// hosts the child of the Viewbox + /// + protected internal ITransform? InternalTransform + { + get => _containerVisual.RenderTransform; + set => _containerVisual.RenderTransform = value; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ChildProperty) + { + _containerVisual.Child = change.NewValue.GetValueOrDefault(); + InvalidateMeasure(); + } + } + protected override Size MeasureOverride(Size availableSize) { - var child = Child; + var child = _containerVisual; if (child != null) { @@ -57,7 +105,7 @@ namespace Avalonia.Controls var size = Stretch.CalculateSize(availableSize, childSize, StretchDirection); - return size.Constrain(availableSize); + return size; } return new Size(); @@ -65,31 +113,21 @@ namespace Avalonia.Controls protected override Size ArrangeOverride(Size finalSize) { - var child = Child; + var child = _containerVisual; if (child != null) { var childSize = child.DesiredSize; var scale = Stretch.CalculateScaling(finalSize, childSize, StretchDirection); - // TODO: Viewbox should have another decorator as a child so we won't affect other render transforms. - var scaleTransform = child.RenderTransform as ScaleTransform; - - if (scaleTransform == null) - { - child.RenderTransform = scaleTransform = new ScaleTransform(scale.X, scale.Y); - child.RenderTransformOrigin = RelativePoint.TopLeft; - } - - scaleTransform.ScaleX = scale.X; - scaleTransform.ScaleY = scale.Y; + InternalTransform = new ScaleTransform(scale.X, scale.Y); child.Arrange(new Rect(childSize)); return childSize * scale; } - return new Size(); + return finalSize; } } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 34b774e23f..93fbf6d17b 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -36,7 +36,7 @@ namespace Avalonia.Controls { get { - var bounds = Orientation == Orientation.Horizontal ? + var bounds = Orientation == Orientation.Horizontal ? _availableSpace.Width : _availableSpace.Height; return Math.Max(0, _takenSpace - bounds); } @@ -129,9 +129,6 @@ namespace Avalonia.Controls protected override IInputElement? GetControlInDirection(NavigationDirection direction, IControl? from) { - if (from == null) - return null; - var logicalScrollable = Parent as ILogicalScrollable; if (logicalScrollable?.IsLogicalScrollEnabled == true) @@ -145,7 +142,7 @@ namespace Avalonia.Controls } internal override void ArrangeChild( - IControl child, + IControl child, Rect rect, Size panelSize, Orientation orientation) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index a5f48bd4a5..993b3aaa1b 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; +using Avalonia.Automation.Peers; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Interactivity; @@ -11,7 +12,6 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; -using JetBrains.Annotations; namespace Avalonia.Controls { @@ -237,14 +237,13 @@ namespace Avalonia.Controls /// /// The window implementation. public Window(IWindowImpl impl) - : base(ValidatingWindowImpl.Wrap(impl)) + : base(impl) { - var wrapped = (IWindowImpl)base.PlatformImpl!; - wrapped.Closing = HandleClosing; - wrapped.GotInputWhenDisabled = OnGotInputWhenDisabled; - wrapped.WindowStateChanged = HandleWindowStateChanged; + impl.Closing = HandleClosing; + impl.GotInputWhenDisabled = OnGotInputWhenDisabled; + impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); - wrapped.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; + impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); @@ -255,6 +254,11 @@ namespace Avalonia.Controls /// public new IWindowImpl? PlatformImpl => (IWindowImpl?)base.PlatformImpl; + /// + /// Gets a collection of child windows owned by this window. + /// + public IReadOnlyList OwnedWindows => _children.Select(x => x.child).ToArray(); + /// /// Gets or sets a value indicating how the window will size itself to fit its content. /// @@ -522,7 +526,7 @@ namespace Avalonia.Controls private void CloseInternal() { - foreach (var (child, _) in _children.ToList()) + foreach (var (child, _) in _children.ToArray()) { child.CloseInternal(); } @@ -546,7 +550,7 @@ namespace Avalonia.Controls bool canClose = true; - foreach (var (child, _) in _children.ToList()) + foreach (var (child, _) in _children.ToArray()) { if (child.ShouldCancelClose(args)) { @@ -854,6 +858,17 @@ namespace Avalonia.Controls private void SetWindowStartupLocation(IWindowBaseImpl? owner = null) { + var startupLocation = WindowStartupLocation; + + if (startupLocation == WindowStartupLocation.CenterOwner && + Owner is Window ownerWindow && + ownerWindow.WindowState == WindowState.Minimized) + { + // If startup location is CenterOwner, but owner is minimized then fall back + // to CenterScreen. This behavior is consistent with WPF. + startupLocation = WindowStartupLocation.CenterScreen; + } + var scaling = owner?.DesktopScaling ?? PlatformImpl?.DesktopScaling ?? 1; // TODO: We really need non-client size here. @@ -861,16 +876,28 @@ namespace Avalonia.Controls PixelPoint.Origin, PixelSize.FromSize(ClientSize, scaling)); - if (WindowStartupLocation == WindowStartupLocation.CenterScreen) + if (startupLocation == WindowStartupLocation.CenterScreen) { - var screen = Screens.ScreenFromPoint(owner?.Position ?? Position); + Screen? screen = null; + + if (owner is not null) + { + screen = Screens.ScreenFromWindow(owner); + + screen ??= Screens.ScreenFromPoint(owner.Position); + } + + if (screen is null) + { + screen = Screens.ScreenFromPoint(Position); + } if (screen != null) { Position = screen.WorkingArea.CenterRect(rect).Position; } } - else if (WindowStartupLocation == WindowStartupLocation.CenterOwner) + else if (startupLocation == WindowStartupLocation.CenterOwner) { if (owner != null) { @@ -1012,5 +1039,10 @@ namespace Avalonia.Controls } } } + + protected override AutomationPeer OnCreateAutomationPeer() + { + return new WindowAutomationPeer(this); + } } } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 4464491020..12ba143c8a 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using Avalonia.Automation.Peers; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; @@ -57,13 +58,12 @@ namespace Avalonia.Controls { } - public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(ValidatingWindowBaseImpl.Wrap(impl), dependencyResolver) + public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(impl, dependencyResolver) { - Screens = new Screens(PlatformImpl?.Screen); - var wrapped = PlatformImpl!; - wrapped.Activated = HandleActivated; - wrapped.Deactivated = HandleDeactivated; - wrapped.PositionChanged = HandlePositionChanged; + Screens = new Screens(impl.Screen); + impl.Activated = HandleActivated; + impl.Deactivated = HandleDeactivated; + impl.PositionChanged = HandlePositionChanged; } /// diff --git a/src/Avalonia.Controls/WindowTransparencyLevel.cs b/src/Avalonia.Controls/WindowTransparencyLevel.cs index f416b5de91..d463f74a0e 100644 --- a/src/Avalonia.Controls/WindowTransparencyLevel.cs +++ b/src/Avalonia.Controls/WindowTransparencyLevel.cs @@ -22,6 +22,11 @@ /// AcrylicBlur, + /// + /// Force acrylic on some incompatible versions of Windows 10. + /// + ForceAcrylicBlur, + /// /// The window background is based on desktop wallpaper tint with a blur. This will only work on Windows 11 /// diff --git a/src/Avalonia.DesignerSupport/ApiCompatBaseline.txt b/src/Avalonia.DesignerSupport/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Avalonia.DesignerSupport/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index f8a7cdc690..0270000d8c 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -16,14 +16,8 @@ - - - - - - diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs index 6b1934ed06..f100be5d5b 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Viewport; +using Avalonia.Utilities; using InputProtocol = Avalonia.Remote.Protocol.Input; namespace Avalonia.DesignerSupport.Remote.HtmlTransport @@ -320,15 +321,13 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport ? null : modifiersText .Split(',') - .Select(x => (InputProtocol.InputModifiers)Enum.Parse( - typeof(InputProtocol.InputModifiers), x, true)) + .Select(x => EnumHelper.Parse(x, true)) .ToArray(); private static InputProtocol.MouseButton ParseMouseButton(string buttonText) => string.IsNullOrWhiteSpace(buttonText) ? InputProtocol.MouseButton.None - : (InputProtocol.MouseButton)Enum.Parse( - typeof(InputProtocol.MouseButton), buttonText, true); + : EnumHelper.Parse(buttonText, true); private static double ParseDouble(string text) => double.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json index 6a5fea5b10..403bb5a59a 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json @@ -377,9 +377,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" @@ -1078,12 +1078,6 @@ "json5": "^2.1.2" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -1636,9 +1630,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "mobx": { @@ -1839,12 +1833,6 @@ "ms": "^2.1.1" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -2592,12 +2580,6 @@ "json5": "^2.1.2" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html index f5847cdd58..cf76de0077 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html @@ -9,6 +9,6 @@
Loading...
- + diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 5cae29cafd..12af602f54 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -1,5 +1,4 @@ using System; -using System.Reactive.Disposables; using Avalonia.Controls; using Avalonia.Controls.Remote.Server; using Avalonia.Input; diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 9dcd4d8e87..a5f478f445 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Reactive.Disposables; using System.Threading.Tasks; +using Avalonia.Automation.Peers; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; @@ -236,5 +237,20 @@ namespace Avalonia.DesignerSupport.Remote public IReadOnlyList AllScreens { get; } = new Screen[] { new Screen(1, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) }; + + public Screen ScreenFromPoint(PixelPoint point) + { + return ScreenHelper.ScreenFromPoint(point, AllScreens); + } + + public Screen ScreenFromRect(PixelRect rect) + { + return ScreenHelper.ScreenFromRect(rect, AllScreens); + } + + public Screen ScreenFromWindow(IWindowBaseImpl window) + { + return ScreenHelper.ScreenFromWindow(window, AllScreens); + } } } diff --git a/src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt b/src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt deleted file mode 100644 index 0493db9ab3..0000000000 --- a/src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt +++ /dev/null @@ -1,3 +0,0 @@ -Compat issues with assembly Avalonia.DesktopRuntime: -TypesMustExist : Type 'Avalonia.Shared.PlatformSupport.AssetLoader' does not exist in the implementation but it does exist in the contract. -Total Issues: 1 diff --git a/src/Avalonia.DesktopRuntime/AppBuilder.cs b/src/Avalonia.DesktopRuntime/AppBuilder.cs deleted file mode 100644 index 2946324c83..0000000000 --- a/src/Avalonia.DesktopRuntime/AppBuilder.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using Avalonia.Controls; -using Avalonia.Platform; -using Avalonia.PlatformSupport; - -namespace Avalonia -{ - /// - /// Initializes platform-specific services for an . - /// - public sealed class AppBuilder : AppBuilderBase - { - /// - /// Initializes a new instance of the class. - /// - public AppBuilder() - : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly)) - { - } - - bool CheckEnvironment(Type checkerType) - { - if (checkerType == null) - return true; - try - { - return ((IModuleEnvironmentChecker) Activator.CreateInstance(checkerType)).IsCompatible; - } - catch - { - return false; - } - } - - /// - /// Instructs the to use the best settings for the platform. - /// - /// An instance. - public AppBuilder UseSubsystemsFromStartupDirectory() - { - var os = RuntimePlatform.GetRuntimeInfo().OperatingSystem; - - LoadAssembliesInDirectory(); - - var windowingSubsystemAttribute = (from assembly in AppDomain.CurrentDomain.GetAssemblies() - from attribute in assembly.GetCustomAttributes() - where attribute.RequiredOS == os && CheckEnvironment(attribute.EnvironmentChecker) - orderby attribute.Priority ascending - select attribute).FirstOrDefault(); - if (windowingSubsystemAttribute == null) - { - throw new InvalidOperationException("No windowing subsystem found. Are you missing assembly references?"); - } - - var renderingSubsystemAttribute = (from assembly in AppDomain.CurrentDomain.GetAssemblies() - from attribute in assembly.GetCustomAttributes() - where attribute.RequiredOS == os && CheckEnvironment(attribute.EnvironmentChecker) - where attribute.RequiresWindowingSubsystem == null - || attribute.RequiresWindowingSubsystem == windowingSubsystemAttribute.Name - orderby attribute.Priority ascending - select attribute).FirstOrDefault(); - - if (renderingSubsystemAttribute == null) - { - throw new InvalidOperationException("No rendering subsystem found. Are you missing assembly references?"); - } - - UseWindowingSubsystem(() => windowingSubsystemAttribute.InitializationType - .GetRuntimeMethod(windowingSubsystemAttribute.InitializationMethod, Type.EmptyTypes).Invoke(null, null), - windowingSubsystemAttribute.Name); - - UseRenderingSubsystem(() => renderingSubsystemAttribute.InitializationType - .GetRuntimeMethod(renderingSubsystemAttribute.InitializationMethod, Type.EmptyTypes).Invoke(null, null), - renderingSubsystemAttribute.Name); - - return this; - } - - private void LoadAssembliesInDirectory() - { - var location = Assembly.GetEntryAssembly().Location; - if (string.IsNullOrWhiteSpace(location)) - return; - var dir = new FileInfo(location).Directory; - if (dir == null) - return; - foreach (var file in dir.EnumerateFiles("*.dll")) - { - try - { - Assembly.LoadFile(file.FullName); - } - catch (Exception) - { - } - } - } - } -} diff --git a/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj b/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj deleted file mode 100644 index 25effae46e..0000000000 --- a/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net6.0;net461;netcoreapp2.0 - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index 1fc3604f70..2fb7c07b6f 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -16,14 +16,8 @@ - - - - - - @@ -34,4 +28,5 @@ + diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 683c2e6549..9029ddf2bd 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -105,13 +105,15 @@ namespace Avalonia.Diagnostics private static IDisposable Open(Application? application, DevToolsOptions options, Window? owner = default) { + var focussedControl = KeyboardDevice.Instance?.FocusedElement as IControl; if (application is null) { throw new ArgumentNullException(nameof(application)); } if (s_open.TryGetValue(application, out var window)) - { + { window.Activate(); + window.SelectedControl(focussedControl); } else { @@ -122,7 +124,7 @@ namespace Avalonia.Diagnostics Height = options.Size.Height, }; window.SetOptions(options); - + window.SelectedControl(focussedControl); window.Closed += DevToolsClosed; s_open.Add(application, window); if (options.ShowAsChildWindow && owner is { }) diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index 46ee8e686c..5672641602 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -41,5 +41,10 @@ namespace Avalonia.Diagnostics /// Default handler is public IScreenshotHandler ScreenshotHandler { get; set; } = Convetions.DefaultScreenshotHandler; + + /// + /// Gets or sets whether DevTools should use the dark mode theme + /// + public bool UseDarkMode { get; set; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs new file mode 100644 index 0000000000..16973a96ef --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs @@ -0,0 +1,56 @@ +using System; +using Avalonia.Data; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Media; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class BindingSetterViewModel : SetterViewModel + { + public BindingSetterViewModel(AvaloniaProperty property, object? value) : base(property, value) + { + switch (value) + { + case Binding binding: + Path = binding.Path; + Tint = Brushes.CornflowerBlue; + ValueTypeTooltip = "Reflection Binding"; + + break; + case CompiledBindingExtension binding: + Path = binding.Path.ToString(); + Tint = Brushes.DarkGreen; + ValueTypeTooltip = "Compiled Binding"; + + break; + case TemplateBinding binding: + if (binding.Property is AvaloniaProperty templateProperty) + { + Path = $"{templateProperty.OwnerType.Name}.{templateProperty.Name}"; + } + else + { + Path = "Unassigned"; + } + + Tint = Brushes.OrangeRed; + ValueTypeTooltip = "Template Binding"; + + break; + default: + throw new ArgumentException("Invalid binding type", nameof(value)); + } + } + + public IBrush Tint { get; } + + public string ValueTypeTooltip { get; } + + public string Path { get; } + + public override void CopyValue() + { + CopyToClipboard(Path); + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 8f0a4d07b0..a1fd425571 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -8,6 +8,7 @@ using System.Reflection; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Metadata; +using Avalonia.Data; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Styling; using Avalonia.VisualTree; @@ -17,7 +18,7 @@ namespace Avalonia.Diagnostics.ViewModels internal class ControlDetailsViewModel : ViewModelBase, IDisposable { private readonly IAvaloniaObject _avaloniaObject; - private IDictionary>? _propertyIndex; + private IDictionary? _propertyIndex; private PropertyViewModel? _selectedProperty; private DataGridCollectionView? _propertiesView; private bool _snapshotStyles; @@ -87,7 +88,16 @@ namespace Avalonia.Diagnostics.ViewModels } else { - setterVm = new SetterViewModel(regularSetter.Property, setterValue); + var isBinding = IsBinding(setterValue); + + if (isBinding) + { + setterVm = new BindingSetterViewModel(regularSetter.Property, setterValue); + } + else + { + setterVm = new SetterViewModel(regularSetter.Property, setterValue); + } } setters.Add(setterVm); @@ -117,6 +127,19 @@ namespace Avalonia.Diagnostics.ViewModels return null; } + private bool IsBinding(object? value) + { + switch (value) + { + case Binding: + case CompiledBindingExtension: + case TemplateBinding: + return true; + } + + return false; + } + public TreePageViewModel TreePage { get; } public DataGridCollectionView? PropertiesView @@ -132,31 +155,19 @@ namespace Avalonia.Diagnostics.ViewModels public object? SelectedEntity { get => _selectedEntity; - set - { - RaiseAndSetIfChanged(ref _selectedEntity, value); - - } + set => RaiseAndSetIfChanged(ref _selectedEntity, value); } public string? SelectedEntityName { get => _selectedEntityName; - set - { - RaiseAndSetIfChanged(ref _selectedEntityName, value); - - } + set => RaiseAndSetIfChanged(ref _selectedEntityName, value); } public string? SelectedEntityType { get => _selectedEntityType; - set - { - RaiseAndSetIfChanged(ref _selectedEntityType, value); - - } + set => RaiseAndSetIfChanged(ref _selectedEntityType, value); } public PropertyViewModel? SelectedProperty @@ -461,9 +472,9 @@ namespace Avalonia.Diagnostics.ViewModels .Concat(GetClrProperties(o, _showImplementedInterfaces)) .OrderBy(x => x, PropertyComparer.Instance) .ThenBy(x => x.Name) - .ToList(); + .ToArray(); - _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList()); + _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToArray()); var view = new DataGridCollectionView(properties); view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); @@ -479,6 +490,31 @@ namespace Avalonia.Diagnostics.ViewModels inpc2.PropertyChanged += ControlPropertyChanged; } } + + internal void SelectProperty(AvaloniaProperty property) + { + SelectedProperty = null; + + if (SelectedEntity != _avaloniaObject) + { + NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString()); + } + + if (PropertiesView is null) + { + return; + } + + foreach (object o in PropertiesView) + { + if (o is AvaloniaPropertyViewModel propertyVm && propertyVm.Property == property) + { + SelectedProperty = propertyVm; + + break; + } + } + } internal void UpdatePropertiesView(bool showImplementedInterfaces) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 140515eb40..9e8a5d8d9b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -7,7 +7,6 @@ using Avalonia.Diagnostics.Models; using Avalonia.Input; using Avalonia.Metadata; using Avalonia.Threading; -using System.Reactive.Linq; using System.Linq; namespace Avalonia.Diagnostics.ViewModels @@ -59,8 +58,8 @@ namespace Avalonia.Diagnostics.ViewModels .Subscribe(e => { PointerOverRoot = e.Root; - PointerOverElement = e.Root.GetInputElementsAt(e.Position).FirstOrDefault(); - }); + PointerOverElement = e.Root.InputHitTest(e.Position); + }); #nullable restore } Console = new ConsoleViewModel(UpdateConsoleContext); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs index e93dc7361b..5202ac963e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs @@ -7,11 +7,14 @@ namespace Avalonia.Diagnostics.ViewModels public object Key { get; } public IBrush Tint { get; } + + public string ValueTypeTooltip { get; } public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object? resourceValue, bool isDynamic) : base(property, resourceValue) { Key = resourceKey; Tint = isDynamic ? Brushes.Orange : Brushes.Brown; + ValueTypeTooltip = isDynamic ? "Dynamic Resource" : "Static Resource"; } public void CopyResourceKey() diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs index 38cbefcb93..559ed49911 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs @@ -34,7 +34,7 @@ namespace Avalonia.Diagnostics.ViewModels IsVisible = true; } - public void CopyValue() + public virtual void CopyValue() { var textToCopy = Value?.ToString(); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 819a203d62..4a5cb0fd3f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -84,7 +84,8 @@ namespace Avalonia.Diagnostics.ViewModels c.GetObservable(Control.ContextMenuProperty), c.GetObservable(FlyoutBase.AttachedFlyoutProperty), c.GetObservable(ToolTipDiagnostics.ToolTipProperty), - (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) => + c.GetObservable(Button.FlyoutProperty), + (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip, ButtonFlyout) => { if (ContextMenu != null) //Note: ContextMenus are special since all the items are added as visual children. @@ -100,6 +101,9 @@ namespace Avalonia.Diagnostics.ViewModels if (ToolTip != null) return GetPopupHostObservable(ToolTip, "ToolTip"); + if (ButtonFlyout != null) + return GetPopupHostObservable(ButtonFlyout, "Flyout"); + return Observable.Return(null); }) .Switch(), diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml index 264a0de359..7dec878e69 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml @@ -11,7 +11,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - > + - > + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index d7acbbd577..cc392853be 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -44,7 +44,9 @@ UseWholeWordFilter="{Binding UseWholeWordFilter}" UseRegexFilter="{Binding UseRegexFilter}"/> - + + + + + + + + + + @@ -148,6 +170,26 @@ + + + + + + + + + + + + + { + + + } + + + + @@ -159,15 +201,15 @@ - + ( - + ) - + @@ -180,11 +222,11 @@ - + - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs index 78919a2105..08ffe2c081 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs @@ -7,9 +7,13 @@ namespace Avalonia.Diagnostics.Views { internal class ControlDetailsView : UserControl { + private DataGrid _dataGrid; + public ControlDetailsView() { InitializeComponent(); + + _dataGrid = this.GetControl("DataGrid"); } private void InitializeComponent() @@ -25,5 +29,25 @@ namespace Avalonia.Diagnostics.Views } } + + private void PropertyNamePressed(object sender, PointerPressedEventArgs e) + { + var mainVm = (ControlDetailsViewModel?) DataContext; + + if (mainVm is null) + { + return; + } + + if (sender is Control control && control.DataContext is SetterViewModel setterVm) + { + mainVm.SelectProperty(setterVm.Property); + + if (mainVm.SelectedProperty is not null) + { + _dataGrid.ScrollIntoView(mainVm.SelectedProperty, null); + } + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml index af6c84a76a..c404a5c382 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml @@ -15,6 +15,7 @@ + @@ -46,6 +47,7 @@ diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 7a894d96fb..3b143d3b58 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -11,6 +11,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Markup.Xaml; using Avalonia.Styling; +using Avalonia.Themes.Default; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.Views @@ -109,7 +110,7 @@ namespace Avalonia.Diagnostics.Views { #pragma warning disable CS0618 // Type or member is obsolete var point = (topLevel as IInputRoot)?.MouseDevice?.GetPosition(topLevel) ?? default; -#pragma warning restore CS0618 // Type or member is obsolete +#pragma warning restore CS0618 // Type or member is obsolete return (IControl?)topLevel.GetVisualsAt(point, x => { @@ -147,6 +148,7 @@ namespace Avalonia.Diagnostics.Views ProcessProperty(control, ContextMenuProperty); ProcessProperty(control, FlyoutBase.AttachedFlyoutProperty); ProcessProperty(control, ToolTipDiagnostics.ToolTipProperty); + ProcessProperty(control, Button.FlyoutProperty); } return popupRoots; @@ -160,13 +162,18 @@ namespace Avalonia.Diagnostics.Views return; } - var root = Root as TopLevel - ?? vm.PointerOverRoot as TopLevel; + var root = vm.PointerOverRoot as TopLevel; + if (root is null) { return; } + if (root is PopupRoot pr && pr.ParentTopLevel != null) + { + root = pr.ParentTopLevel; + } + switch (e.Modifiers) { case RawInputModifiers.Control when (e.Key == Key.LeftShift || e.Key == Key.RightShift): @@ -241,7 +248,25 @@ namespace Avalonia.Diagnostics.Views private void RootClosed(object? sender, EventArgs e) => Close(); - public void SetOptions(DevToolsOptions options) => + public void SetOptions(DevToolsOptions options) + { (DataContext as MainViewModel)?.SetOptions(options); + + if (options.UseDarkMode) + { + if (Styles[0] is SimpleTheme st) + { + st.Mode = SimpleThemeMode.Dark; + } + } + } + + internal void SelectedControl(IControl? control) + { + if (control is { }) + { + (DataContext as MainViewModel)?.SelectControl(control); + } + } } } diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index 770d15ea27..a311efdfb0 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -15,4 +15,5 @@ + diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.cs b/src/Avalonia.Dialogs/ManagedFileChooser.cs index 9058c405a3..199a4d6620 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooser.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; @@ -9,6 +10,8 @@ using Avalonia.LogicalTree; namespace Avalonia.Dialogs { + [TemplatePart("QuickLinks", typeof(Control))] + [TemplatePart("Files", typeof(ListBox))] public class ManagedFileChooser : TemplatedControl { private Control _quickLinksRoot; diff --git a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs index 28d40f13b9..405a248caf 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs @@ -17,6 +17,7 @@ namespace Avalonia.Dialogs private readonly ManagedFileDialogOptions _options; public event Action CancelRequested; public event Action CompleteRequested; + public event Action OverwritePrompt; public AvaloniaList QuickLinks { get; } = new AvaloniaList(); @@ -39,6 +40,7 @@ namespace Avalonia.Dialogs private bool _scheduledSelectionValidation; private bool _alreadyCancelled = false; private string _defaultExtension; + private bool _overwritePrompt; private CompositeDisposable _disposables; public string Location @@ -167,6 +169,7 @@ namespace Avalonia.Dialogs { _savingFile = true; _defaultExtension = sfd.DefaultExtension; + _overwritePrompt = sfd.ShowOverwritePrompt ?? true; FileName = sfd.InitialFileName; } @@ -360,7 +363,16 @@ namespace Avalonia.Dialogs FileName = Path.ChangeExtension(FileName, _defaultExtension); } - CompleteRequested?.Invoke(new[] { Path.Combine(Location, FileName) }); + var fullName = Path.Combine(Location, FileName); + + if (_overwritePrompt && File.Exists(fullName)) + { + OverwritePrompt?.Invoke(fullName); + } + else + { + CompleteRequested?.Invoke(new[] { fullName }); + } } } else diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index f9e62d905b..1970c5557d 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; @@ -31,6 +32,75 @@ namespace Avalonia.Dialogs dialog.Close(); }; + model.OverwritePrompt += async (filename) => + { + Window overwritePromptDialog = new Window() + { + Title = "Confirm Save As", + SizeToContent = SizeToContent.WidthAndHeight, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Padding = new Thickness(10), + MinWidth = 270 + }; + + string name = Path.GetFileName(filename); + + var panel = new DockPanel() + { + HorizontalAlignment = Layout.HorizontalAlignment.Stretch + }; + + var label = new Label() + { + Content = $"{name} already exists.\nDo you want to replace it?" + }; + + panel.Children.Add(label); + DockPanel.SetDock(label, Dock.Top); + + var buttonPanel = new StackPanel() + { + HorizontalAlignment = Layout.HorizontalAlignment.Right, + Orientation = Layout.Orientation.Horizontal, + Spacing = 10 + }; + + var button = new Button() + { + Content = "Yes", + HorizontalAlignment = Layout.HorizontalAlignment.Right + }; + + button.Click += (sender, args) => + { + result = new string[1] { filename }; + overwritePromptDialog.Close(); + dialog.Close(); + }; + + buttonPanel.Children.Add(button); + + button = new Button() + { + Content = "No", + HorizontalAlignment = Layout.HorizontalAlignment.Right + }; + + button.Click += (sender, args) => + { + overwritePromptDialog.Close(); + }; + + buttonPanel.Children.Add(button); + + panel.Children.Add(buttonPanel); + DockPanel.SetDock(buttonPanel, Dock.Bottom); + + overwritePromptDialog.Content = panel; + + await overwritePromptDialog.ShowDialog(dialog); + }; + model.CancelRequested += dialog.Close; await dialog.ShowDialog(parent); diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index a7e83140ae..864c579319 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -198,9 +198,9 @@ namespace Avalonia.FreeDesktop.DBusIme UpdateActive(); } - void ITextInputMethodImpl.SetActive(bool active) + void ITextInputMethodImpl.SetClient(ITextInputMethodClient client) { - _controlActive = active; + _controlActive = client is { }; UpdateActive(); } @@ -272,7 +272,7 @@ namespace Avalonia.FreeDesktop.DBusIme UpdateCursorRect(); } - public abstract void SetOptions(TextInputOptionsQueryEventArgs options); + public abstract void SetOptions(TextInputOptions options); void ITextInputMethodImpl.Reset() { diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 31a061571f..0b85965de7 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -93,7 +93,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx (uint)args.Timestamp).ConfigureAwait(false); } - public override void SetOptions(TextInputOptionsQueryEventArgs options) => + public override void SetOptions(TextInputOptions options) => Enqueue(async () => { if(_context == null) @@ -111,7 +111,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx flags |= FcitxCapabilityFlags.CAPACITY_NUMBER; else if (options.ContentType == TextInputContentType.Password) flags |= FcitxCapabilityFlags.CAPACITY_PASSWORD; - else if (options.ContentType == TextInputContentType.Phone) + else if (options.ContentType == TextInputContentType.Digits) flags |= FcitxCapabilityFlags.CAPACITY_DIALABLE; else if (options.ContentType == TextInputContentType.Url) flags |= FcitxCapabilityFlags.CAPACITY_URL; diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index a73de9dae8..1397eaa57b 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -97,7 +97,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); } - public override void SetOptions(TextInputOptionsQueryEventArgs options) + public override void SetOptions(TextInputOptions options) { // No-op, because ibus } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 90221bb922..addc248d58 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -7,7 +7,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; namespace Avalonia.Headless { diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index b619b9d129..083b16c107 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -133,9 +133,12 @@ namespace Avalonia.Headless class HeadlessTextShaperStub : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, - CultureInfo culture, sbyte bidiLevel) + public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) { + var typeface = options.Typeface; + var fontRenderingEmSize = options.FontRenderingEmSize; + var bidiLevel = options.BidLevel; + return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); } } @@ -157,9 +160,10 @@ namespace Avalonia.Headless return new List { "Arial" }; } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out Typeface typeface) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, + FontFamily fontFamily, CultureInfo culture, out Typeface typeface) { - typeface = new Typeface("Arial", fontStyle, fontWeight); + typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch); return true; } } @@ -199,5 +203,20 @@ namespace Avalonia.Headless new Screen(1, new PixelRect(0, 0, 1920, 1280), new PixelRect(0, 0, 1920, 1280), true), }; + + public Screen ScreenFromPoint(PixelPoint point) + { + return ScreenHelper.ScreenFromPoint(point, AllScreens); + } + + public Screen ScreenFromRect(PixelRect rect) + { + return ScreenHelper.ScreenFromRect(rect, AllScreens); + } + + public Screen ScreenFromWindow(IWindowBaseImpl window) + { + return ScreenHelper.ScreenFromWindow(window, AllScreens); + } } } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 1b582d5775..c976921bc3 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Automation.Peers; using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Controls.Primitives.PopupPositioning; diff --git a/src/Avalonia.Input/ApiCompatBaseline.txt b/src/Avalonia.Input/ApiCompatBaseline.txt deleted file mode 100644 index 270c5305e5..0000000000 --- a/src/Avalonia.Input/ApiCompatBaseline.txt +++ /dev/null @@ -1,14 +0,0 @@ -Compat issues with assembly Avalonia.Input: -MembersMustExist : Member 'public Avalonia.Platform.IPlatformHandle Avalonia.Input.Cursor.PlatformCursor.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.DoubleTappedEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.RightTappedEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.TappedEvent' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.IFocusManager.RemoveFocusScope(Avalonia.Input.IFocusScope)' is present in the implementation but not in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.InputElement.DoubleTappedEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.InputElement.TappedEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_DoubleTapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_Tapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_DoubleTapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_Tapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -TypesMustExist : Type 'Avalonia.Platform.IStandardCursorFactory' does not exist in the implementation but it does exist in the contract. -Total Issues: 12 diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj deleted file mode 100644 index e66d1d7e7c..0000000000 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - net6.0;netstandard2.0 - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs deleted file mode 100644 index 30301788bc..0000000000 --- a/src/Avalonia.Input/FocusManager.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - /// - /// Manages focus for the application. - /// - public class FocusManager : IFocusManager - { - /// - /// The focus scopes in which the focus is currently defined. - /// - private readonly ConditionalWeakTable _focusScopes = - new ConditionalWeakTable(); - - /// - /// Initializes a new instance of the class. - /// - static FocusManager() - { - InputElement.PointerPressedEvent.AddClassHandler( - typeof(IInputElement), - new EventHandler(OnPreviewPointerPressed), - RoutingStrategies.Tunnel); - } - - /// - /// Gets the instance of the . - /// - public static IFocusManager? Instance => AvaloniaLocator.Current.GetService(); - - /// - /// Gets the currently focused . - /// - public IInputElement? Current => KeyboardDevice.Instance?.FocusedElement; - - /// - /// Gets the current focus scope. - /// - public IFocusScope? Scope - { - get; - private set; - } - - /// - /// Focuses a control. - /// - /// The control to focus. - /// The method by which focus was changed. - /// Any key modifiers active at the time of focus. - public void Focus( - IInputElement? control, - NavigationMethod method = NavigationMethod.Unspecified, - KeyModifiers keyModifiers = KeyModifiers.None) - { - if (control != null) - { - var scope = GetFocusScopeAncestors(control) - .FirstOrDefault(); - - if (scope != null) - { - Scope = scope; - SetFocusedElement(scope, control, method, keyModifiers); - } - } - else if (Current != null) - { - // If control is null, set focus to the topmost focus scope. - foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList()) - { - if (scope != Scope && - _focusScopes.TryGetValue(scope, out var element) && - element != null) - { - Focus(element, method); - return; - } - } - - if (Scope is object) - { - // Couldn't find a focus scope, clear focus. - SetFocusedElement(Scope, null); - } - } - } - - public IInputElement? GetFocusedElement(IInputElement e) - { - if (e is IFocusScope scope) - { - _focusScopes.TryGetValue(scope, out var result); - return result; - } - - return null; - } - - /// - /// Sets the currently focused element in the specified scope. - /// - /// The focus scope. - /// The element to focus. May be null. - /// The method by which focus was changed. - /// Any key modifiers active at the time of focus. - /// - /// If the specified scope is the current then the keyboard focus - /// will change. - /// - public void SetFocusedElement( - IFocusScope scope, - IInputElement? element, - NavigationMethod method = NavigationMethod.Unspecified, - KeyModifiers keyModifiers = KeyModifiers.None) - { - scope = scope ?? throw new ArgumentNullException(nameof(scope)); - - if (_focusScopes.TryGetValue(scope, out var existingElement)) - { - if (element != existingElement) - { - _focusScopes.Remove(scope); - _focusScopes.Add(scope, element); - } - } - else - { - _focusScopes.Add(scope, element); - } - - if (Scope == scope) - { - KeyboardDevice.Instance?.SetFocusedElement(element, method, keyModifiers); - } - } - - /// - /// Notifies the focus manager of a change in focus scope. - /// - /// The new focus scope. - public void SetFocusScope(IFocusScope scope) - { - scope = scope ?? throw new ArgumentNullException(nameof(scope)); - - if (!_focusScopes.TryGetValue(scope, out var e)) - { - // TODO: Make this do something useful, i.e. select the first focusable - // control, select a control that the user has specified to have default - // focus etc. - e = scope as IInputElement; - _focusScopes.Add(scope, e); - } - - Scope = scope; - Focus(e); - } - - public void RemoveFocusScope(IFocusScope scope) - { - scope = scope ?? throw new ArgumentNullException(nameof(scope)); - - if (_focusScopes.TryGetValue(scope, out _)) - { - SetFocusedElement(scope, null); - _focusScopes.Remove(scope); - } - - if (Scope == scope) - { - Scope = null; - } - } - - public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope; - - /// - /// Checks if the specified element can be focused. - /// - /// The element. - /// True if the element can be focused. - private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && e.IsVisible; - - /// - /// Gets the focus scope ancestors of the specified control, traversing popups. - /// - /// The control. - /// The focus scopes. - private static IEnumerable GetFocusScopeAncestors(IInputElement control) - { - IInputElement? c = control; - - while (c != null) - { - var scope = c as IFocusScope; - - if (scope != null && c.VisualRoot?.IsVisible == true) - { - yield return scope; - } - - c = c.GetVisualParent() ?? - ((c as IHostedVisualTreeRoot)?.Host as IInputElement); - } - } - - /// - /// Global handler for pointer pressed events. - /// - /// The event sender. - /// The event args. - private static void OnPreviewPointerPressed(object? sender, RoutedEventArgs e) - { - if (sender is null) - return; - - var ev = (PointerPressedEventArgs)e; - var visual = (IVisual)sender; - - if (sender == e.Source && ev.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) - { - IVisual? element = ev.Pointer?.Captured ?? e.Source as IInputElement; - - while (element != null) - { - if (element is IInputElement inputElement && CanFocus(inputElement)) - { - Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.KeyModifiers); - - break; - } - - element = element.VisualParent; - } - } - } - } -} diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs deleted file mode 100644 index 86e6e96a71..0000000000 --- a/src/Avalonia.Input/Gestures.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public static class Gestures - { - private static bool s_isDoubleTapped = false; - public static readonly RoutedEvent TappedEvent = RoutedEvent.Register( - "Tapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent DoubleTappedEvent = RoutedEvent.Register( - "DoubleTapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent RightTappedEvent = RoutedEvent.Register( - "RightTapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent ScrollGestureEvent = - RoutedEvent.Register( - "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent ScrollGestureEndedEvent = - RoutedEvent.Register( - "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = - RoutedEvent.Register( - "PointerMagnifyGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = - RoutedEvent.Register( - "PointerRotateGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = - RoutedEvent.Register( - "PointerSwipeGesture", RoutingStrategies.Bubble, typeof(Gestures)); - -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - private static readonly WeakReference s_lastPress = new WeakReference(null); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - - static Gestures() - { - InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); - InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); - } - - public static void AddTappedHandler(IInteractive element, EventHandler handler) - { - element.AddHandler(TappedEvent, handler); - } - - public static void AddDoubleTappedHandler(IInteractive element, EventHandler handler) - { - element.AddHandler(DoubleTappedEvent, handler); - } - - public static void AddRightTappedHandler(IInteractive element, EventHandler handler) - { - element.AddHandler(RightTappedEvent, handler); - } - - public static void RemoveTappedHandler(IInteractive element, EventHandler handler) - { - element.RemoveHandler(TappedEvent, handler); - } - - public static void RemoveDoubleTappedHandler(IInteractive element, EventHandler handler) - { - element.RemoveHandler(DoubleTappedEvent, handler); - } - - public static void RemoveRightTappedHandler(IInteractive element, EventHandler handler) - { - element.RemoveHandler(RightTappedEvent, handler); - } - - private static void PointerPressed(RoutedEventArgs ev) - { - if (ev.Source is null) - { - return; - } - - if (ev.Route == RoutingStrategies.Bubble) - { - var e = (PointerPressedEventArgs)ev; - var visual = (IVisual)ev.Source; - - if (e.ClickCount <= 1) - { - s_isDoubleTapped = false; - s_lastPress.SetTarget(ev.Source); - } - else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) - { - if (s_lastPress.TryGetTarget(out var target) && target == e.Source) - { - s_isDoubleTapped = true; - e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); - } - } - else - { - s_isDoubleTapped = false; - } - } - } - - private static void PointerReleased(RoutedEventArgs ev) - { - if (ev.Route == RoutingStrategies.Bubble) - { - var e = (PointerReleasedEventArgs)ev; - - if (s_lastPress.TryGetTarget(out var target) && target == e.Source) - { - if (e.InitialPressMouseButton == MouseButton.Left || e.InitialPressMouseButton == MouseButton.Right) - { - if (e.InitialPressMouseButton == MouseButton.Right) - { - e.Source.RaiseEvent(new TappedEventArgs(RightTappedEvent, e)); - } - //s_isDoubleTapped needed here to prevent invoking Tapped event when DoubleTapped is called. - //This behaviour matches UWP behaviour. - else if (s_isDoubleTapped == false) - { - e.Source.RaiseEvent(new TappedEventArgs(TappedEvent, e)); - } - } - } - } - } - } -} diff --git a/src/Avalonia.Input/ICommandSource.cs b/src/Avalonia.Input/ICommandSource.cs deleted file mode 100644 index 410b3a2e47..0000000000 --- a/src/Avalonia.Input/ICommandSource.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Windows.Input; -#nullable enable -namespace Avalonia.Input -{ - /// - /// An interface for classes that know how to invoke a Command. - /// - public interface ICommandSource - { - /// - /// The command that will be executed when the class is "invoked." - /// Classes that implement this interface should enable or disable based on the command's CanExecute return value. - /// The property may be implemented as read-write if desired. - /// - ICommand? Command { get; } - - /// - /// The parameter that will be passed to the command when executing the command. - /// The property may be implemented as read-write if desired. - /// - object? CommandParameter { get; } - - - /// - /// Bor the behavior CanExecuteChanged - /// - /// - /// - void CanExecuteChanged(object sender, System.EventArgs e); - - /// - /// Gets a value indicating whether this control and all its parents are enabled. - /// - bool IsEffectivelyEnabled { get; } - } -} diff --git a/src/Avalonia.Input/IInputRoot.cs b/src/Avalonia.Input/IInputRoot.cs deleted file mode 100644 index 3e2b8cc477..0000000000 --- a/src/Avalonia.Input/IInputRoot.cs +++ /dev/null @@ -1,36 +0,0 @@ -using JetBrains.Annotations; - -namespace Avalonia.Input -{ - /// - /// Defines the interface for top-level input elements. - /// - public interface IInputRoot : IInputElement - { - /// - /// Gets or sets the access key handler. - /// - IAccessKeyHandler AccessKeyHandler { get; } - - /// - /// Gets or sets the keyboard navigation handler. - /// - IKeyboardNavigationHandler KeyboardNavigationHandler { get; } - - /// - /// Gets or sets the input element that the pointer is currently over. - /// - IInputElement? PointerOverElement { get; set; } - - /// - /// Gets or sets a value indicating whether access keys are shown in the window. - /// - bool ShowAccessKeys { get; set; } - - /// - /// Gets associated mouse device - /// - [CanBeNull] - IMouseDevice? MouseDevice { get; } - } -} diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs deleted file mode 100644 index 9506dc36fb..0000000000 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.ComponentModel; - -namespace Avalonia.Input -{ - [Flags, Obsolete("Use KeyModifiers and PointerPointProperties")] - public enum InputModifiers - { - None = 0, - Alt = 1, - Control = 2, - Shift = 4, - Windows = 8, - LeftMouseButton = 16, - RightMouseButton = 32, - MiddleMouseButton = 64 - } - - [Flags] - public enum KeyModifiers - { - None = 0, - Alt = 1, - Control = 2, - Shift = 4, - Meta = 8, - } - - [Flags] - public enum KeyStates - { - None = 0, - Down = 1, - Toggled = 2, - } - - [Flags] - public enum RawInputModifiers - { - None = 0, - Alt = 1, - Control = 2, - Shift = 4, - Meta = 8, - LeftMouseButton = 16, - RightMouseButton = 32, - MiddleMouseButton = 64, - XButton1MouseButton = 128, - XButton2MouseButton = 256, - KeyboardMask = Alt | Control | Shift | Meta - } - - internal static class KeyModifiersUtils - { - public static KeyModifiers ConvertToKey(RawInputModifiers modifiers) => - (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); - } - - public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged - { - IInputElement? FocusedElement { get; } - - void SetFocusedElement( - IInputElement? element, - NavigationMethod method, - KeyModifiers modifiers); - } -} diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs deleted file mode 100644 index 272d1eb8d7..0000000000 --- a/src/Avalonia.Input/IMouseDevice.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Avalonia.Input -{ - /// - /// Represents a mouse device. - /// - public interface IMouseDevice : IPointerDevice - { - /// - /// Gets the mouse position, in screen coordinates. - /// - [Obsolete("Use PointerEventArgs.GetPosition")] - PixelPoint Position { get; } - - void TopLevelClosed(IInputRoot root); - - void SceneInvalidated(IInputRoot root, Rect rect); - } -} diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs deleted file mode 100644 index 1f82cb1ed7..0000000000 --- a/src/Avalonia.Input/IPointerDevice.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public interface IPointerDevice : IInputDevice - { - [Obsolete("Use IPointer")] - IInputElement? Captured { get; } - - [Obsolete("Use IPointer")] - void Capture(IInputElement? control); - - [Obsolete("Use PointerEventArgs.GetPosition")] - Point GetPosition(IVisual relativeTo); - } -} diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs deleted file mode 100644 index 5ec0bd6ee4..0000000000 --- a/src/Avalonia.Input/InputElement.cs +++ /dev/null @@ -1,710 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Metadata; -using Avalonia.Data; -using Avalonia.Input.GestureRecognizers; -using Avalonia.Input.TextInput; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -#nullable enable - -namespace Avalonia.Input -{ - /// - /// Implements input-related functionality for a control. - /// - [PseudoClasses(":disabled", ":focus", ":focus-visible", ":focus-within", ":pointerover")] - public class InputElement : Interactive, IInputElement - { - /// - /// Defines the property. - /// - public static readonly StyledProperty FocusableProperty = - AvaloniaProperty.Register(nameof(Focusable)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty IsEnabledProperty = - AvaloniaProperty.Register(nameof(IsEnabled), true); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsEffectivelyEnabledProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsEffectivelyEnabled), - o => o.IsEffectivelyEnabled); - - /// - /// Gets or sets associated mouse cursor. - /// - public static readonly StyledProperty CursorProperty = - AvaloniaProperty.Register(nameof(Cursor), null, true); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsKeyboardFocusWithinProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsKeyboardFocusWithin), - o => o.IsKeyboardFocusWithin); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsFocusedProperty = - AvaloniaProperty.RegisterDirect(nameof(IsFocused), o => o.IsFocused); - - /// - /// Defines the property. - /// - public static readonly StyledProperty IsHitTestVisibleProperty = - AvaloniaProperty.Register(nameof(IsHitTestVisible), true); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsPointerOverProperty = - AvaloniaProperty.RegisterDirect(nameof(IsPointerOver), o => o.IsPointerOver); - - /// - /// Defines the property. - /// - public static readonly StyledProperty IsTabStopProperty = - KeyboardNavigation.IsTabStopProperty.AddOwner(); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent GotFocusEvent = - RoutedEvent.Register(nameof(GotFocus), RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent LostFocusEvent = - RoutedEvent.Register(nameof(LostFocus), RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent KeyDownEvent = - RoutedEvent.Register( - "KeyDown", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent KeyUpEvent = - RoutedEvent.Register( - "KeyUp", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the property. - /// - public static readonly StyledProperty TabIndexProperty = - KeyboardNavigation.TabIndexProperty.AddOwner(); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent TextInputEvent = - RoutedEvent.Register( - "TextInput", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent TextInputMethodClientRequestedEvent = - RoutedEvent.Register( - "TextInputMethodClientRequested", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent TextInputOptionsQueryEvent = - RoutedEvent.Register( - "TextInputOptionsQuery", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerEnterEvent = - RoutedEvent.Register(nameof(PointerEnter), RoutingStrategies.Direct); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerLeaveEvent = - RoutedEvent.Register(nameof(PointerLeave), RoutingStrategies.Direct); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerMovedEvent = - RoutedEvent.Register( - "PointerMove", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerPressedEvent = - RoutedEvent.Register( - "PointerPressed", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerReleasedEvent = - RoutedEvent.Register( - "PointerReleased", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the routed event. - /// - public static readonly RoutedEvent PointerCaptureLostEvent = - RoutedEvent.Register( - "PointerCaptureLost", - RoutingStrategies.Direct); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerWheelChangedEvent = - RoutedEvent.Register( - "PointerWheelChanged", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; - - /// - /// Defines the event. - /// - public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; - - private bool _isEffectivelyEnabled = true; - private bool _isFocused; - private bool _isKeyboardFocusWithin; - private bool _isFocusVisible; - private bool _isPointerOver; - private GestureRecognizerCollection? _gestureRecognizers; - - /// - /// Initializes static members of the class. - /// - static InputElement() - { - IsEnabledProperty.Changed.Subscribe(IsEnabledChanged); - - GotFocusEvent.AddClassHandler((x, e) => x.OnGotFocus(e)); - LostFocusEvent.AddClassHandler((x, e) => x.OnLostFocus(e)); - KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e)); - KeyUpEvent.AddClassHandler((x, e) => x.OnKeyUp(e)); - TextInputEvent.AddClassHandler((x, e) => x.OnTextInput(e)); - PointerEnterEvent.AddClassHandler((x, e) => x.OnPointerEnterCore(e)); - PointerLeaveEvent.AddClassHandler((x, e) => x.OnPointerLeaveCore(e)); - PointerMovedEvent.AddClassHandler((x, e) => x.OnPointerMoved(e)); - PointerPressedEvent.AddClassHandler((x, e) => x.OnPointerPressed(e)); - PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); - PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); - PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); - } - - public InputElement() - { - UpdatePseudoClasses(IsFocused, IsPointerOver); - } - - /// - /// Occurs when the control receives focus. - /// - public event EventHandler? GotFocus - { - add { AddHandler(GotFocusEvent, value); } - remove { RemoveHandler(GotFocusEvent, value); } - } - - /// - /// Occurs when the control loses focus. - /// - public event EventHandler? LostFocus - { - add { AddHandler(LostFocusEvent, value); } - remove { RemoveHandler(LostFocusEvent, value); } - } - - /// - /// Occurs when a key is pressed while the control has focus. - /// - public event EventHandler? KeyDown - { - add { AddHandler(KeyDownEvent, value); } - remove { RemoveHandler(KeyDownEvent, value); } - } - - /// - /// Occurs when a key is released while the control has focus. - /// - public event EventHandler? KeyUp - { - add { AddHandler(KeyUpEvent, value); } - remove { RemoveHandler(KeyUpEvent, value); } - } - - /// - /// Occurs when a user typed some text while the control has focus. - /// - public event EventHandler? TextInput - { - add { AddHandler(TextInputEvent, value); } - remove { RemoveHandler(TextInputEvent, value); } - } - - /// - /// Occurs when an input element gains input focus and input method is looking for the corresponding client - /// - public event EventHandler? TextInputMethodClientRequested - { - add { AddHandler(TextInputMethodClientRequestedEvent, value); } - remove { RemoveHandler(TextInputMethodClientRequestedEvent, value); } - } - - /// - /// Occurs when an input element gains input focus and input method is asking for required content options - /// - public event EventHandler? TextInputOptionsQuery - { - add { AddHandler(TextInputOptionsQueryEvent, value); } - remove { RemoveHandler(TextInputOptionsQueryEvent, value); } - } - - /// - /// Occurs when the pointer enters the control. - /// - public event EventHandler? PointerEnter - { - add { AddHandler(PointerEnterEvent, value); } - remove { RemoveHandler(PointerEnterEvent, value); } - } - - /// - /// Occurs when the pointer leaves the control. - /// - public event EventHandler? PointerLeave - { - add { AddHandler(PointerLeaveEvent, value); } - remove { RemoveHandler(PointerLeaveEvent, value); } - } - - /// - /// Occurs when the pointer moves over the control. - /// - public event EventHandler? PointerMoved - { - add { AddHandler(PointerMovedEvent, value); } - remove { RemoveHandler(PointerMovedEvent, value); } - } - - /// - /// Occurs when the pointer is pressed over the control. - /// - public event EventHandler? PointerPressed - { - add { AddHandler(PointerPressedEvent, value); } - remove { RemoveHandler(PointerPressedEvent, value); } - } - - /// - /// Occurs when the pointer is released over the control. - /// - public event EventHandler? PointerReleased - { - add { AddHandler(PointerReleasedEvent, value); } - remove { RemoveHandler(PointerReleasedEvent, value); } - } - - /// - /// Occurs when the control or its child control loses the pointer capture for any reason, - /// event will not be triggered for a parent control if capture was transferred to another child of that parent control - /// - public event EventHandler? PointerCaptureLost - { - add => AddHandler(PointerCaptureLostEvent, value); - remove => RemoveHandler(PointerCaptureLostEvent, value); - } - - /// - /// Occurs when the mouse is scrolled over the control. - /// - public event EventHandler? PointerWheelChanged - { - add { AddHandler(PointerWheelChangedEvent, value); } - remove { RemoveHandler(PointerWheelChangedEvent, value); } - } - - /// - /// Occurs when a tap gesture occurs on the control. - /// - public event EventHandler? Tapped - { - add { AddHandler(TappedEvent, value); } - remove { RemoveHandler(TappedEvent, value); } - } - - /// - /// Occurs when a double-tap gesture occurs on the control. - /// - public event EventHandler? DoubleTapped - { - add { AddHandler(DoubleTappedEvent, value); } - remove { RemoveHandler(DoubleTappedEvent, value); } - } - - /// - /// Gets or sets a value indicating whether the control can receive focus. - /// - public bool Focusable - { - get { return GetValue(FocusableProperty); } - set { SetValue(FocusableProperty, value); } - } - - /// - /// Gets or sets a value indicating whether the control is enabled for user interaction. - /// - public bool IsEnabled - { - get { return GetValue(IsEnabledProperty); } - set { SetValue(IsEnabledProperty, value); } - } - - /// - /// Gets or sets associated mouse cursor. - /// - public Cursor? Cursor - { - get { return GetValue(CursorProperty); } - set { SetValue(CursorProperty, value); } - } - - /// - /// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements. - /// - public bool IsKeyboardFocusWithin - { - get => _isKeyboardFocusWithin; - internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value); - } - - /// - /// Gets a value indicating whether the control is focused. - /// - public bool IsFocused - { - get { return _isFocused; } - private set { SetAndRaise(IsFocusedProperty, ref _isFocused, value); } - } - - /// - /// Gets or sets a value indicating whether the control is considered for hit testing. - /// - public bool IsHitTestVisible - { - get { return GetValue(IsHitTestVisibleProperty); } - set { SetValue(IsHitTestVisibleProperty, value); } - } - - /// - /// Gets a value indicating whether the pointer is currently over the control. - /// - public bool IsPointerOver - { - get { return _isPointerOver; } - internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); } - } - - /// - /// Gets or sets a value that indicates whether the control is included in tab navigation. - /// - public bool IsTabStop - { - get => GetValue(IsTabStopProperty); - set => SetValue(IsTabStopProperty, value); - } - - /// - public bool IsEffectivelyEnabled - { - get => _isEffectivelyEnabled; - private set - { - SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); - PseudoClasses.Set(":disabled", !value); - } - } - - /// - /// Gets or sets a value that determines the order in which elements receive focus when the - /// user navigates through controls by pressing the Tab key. - /// - public int TabIndex - { - get => GetValue(TabIndexProperty); - set => SetValue(TabIndexProperty, value); - } - - public List KeyBindings { get; } = new List(); - - /// - /// Allows a derived class to override the enabled state of the control. - /// - /// - /// Derived controls may wish to disable the enabled state of the control without overwriting the - /// user-supplied setting. This can be done by overriding this property - /// to return the overridden enabled state. If the value returned from - /// should change, then the derived control should call . - /// - protected virtual bool IsEnabledCore => IsEnabled; - - public GestureRecognizerCollection GestureRecognizers - => _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this)); - - /// - /// Focuses the control. - /// - public void Focus() - { - FocusManager.Instance?.Focus(this); - } - - /// - protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTreeCore(e); - - if (IsFocused) - { - FocusManager.Instance?.Focus(null); - } - } - - /// - protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTreeCore(e); - UpdateIsEffectivelyEnabled(); - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnGotFocus(GotFocusEventArgs e) - { - var isFocused = e.Source == this; - _isFocusVisible = isFocused && (e.NavigationMethod == NavigationMethod.Directional || e.NavigationMethod == NavigationMethod.Tab); - IsFocused = isFocused; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnLostFocus(RoutedEventArgs e) - { - _isFocusVisible = false; - IsFocused = false; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnKeyDown(KeyEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnKeyUp(KeyEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnTextInput(TextInputEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerEnter(PointerEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerLeave(PointerEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerMoved(PointerEventArgs e) - { - if (_gestureRecognizers?.HandlePointerMoved(e) == true) - e.Handled = true; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerPressed(PointerPressedEventArgs e) - { - if (_gestureRecognizers?.HandlePointerPressed(e) == true) - e.Handled = true; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerReleased(PointerReleasedEventArgs e) - { - if (_gestureRecognizers?.HandlePointerReleased(e) == true) - e.Handled = true; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e) - { - _gestureRecognizers?.HandlePointerCaptureLost(e); - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerWheelChanged(PointerWheelEventArgs e) - { - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == IsFocusedProperty) - { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault(), null); - } - else if (change.Property == IsPointerOverProperty) - { - UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); - } - else if (change.Property == IsKeyboardFocusWithinProperty) - { - PseudoClasses.Set(":focus-within", change.NewValue.GetValueOrDefault()); - } - } - - /// - /// Updates the property value according to the parent - /// control's enabled state and . - /// - protected void UpdateIsEffectivelyEnabled() - { - UpdateIsEffectivelyEnabled(this.GetVisualParent()); - } - - private static void IsEnabledChanged(AvaloniaPropertyChangedEventArgs e) - { - ((InputElement)e.Sender).UpdateIsEffectivelyEnabled(); - } - - /// - /// Called before the event occurs. - /// - /// The event args. - private void OnPointerEnterCore(PointerEventArgs e) - { - IsPointerOver = true; - OnPointerEnter(e); - } - - /// - /// Called before the event occurs. - /// - /// The event args. - private void OnPointerLeaveCore(PointerEventArgs e) - { - IsPointerOver = false; - OnPointerLeave(e); - } - - /// - /// Updates the property based on the parent's - /// . - /// - /// The parent control. - private void UpdateIsEffectivelyEnabled(InputElement? parent) - { - IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true); - - // PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ - // will cause extra allocations and overhead. - - var children = VisualChildren; - - // ReSharper disable once ForCanBeConvertedToForeach - for (int i = 0; i < children.Count; ++i) - { - var child = children[i] as InputElement; - - child?.UpdateIsEffectivelyEnabled(this); - } - } - - private void UpdatePseudoClasses(bool? isFocused, bool? isPointerOver) - { - if (isFocused.HasValue) - { - PseudoClasses.Set(":focus", isFocused.Value); - PseudoClasses.Set(":focus-visible", _isFocusVisible); - } - - if (isPointerOver.HasValue) - { - PseudoClasses.Set(":pointerover", isPointerOver.Value); - } - } - } -} diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs deleted file mode 100644 index 0adbe73263..0000000000 --- a/src/Avalonia.Input/KeyGesture.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Input -{ - /// - /// Defines a keyboard input combination. - /// - public sealed class KeyGesture : IEquatable - { - private static readonly Dictionary s_keySynonyms = new Dictionary - { - { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod }, { ",", Key.OemComma } - }; - - [Obsolete("Use constructor taking KeyModifiers")] - public KeyGesture(Key key, InputModifiers modifiers) - { - Key = key; - KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xf); - } - - public KeyGesture(Key key, KeyModifiers modifiers = KeyModifiers.None) - { - Key = key; - KeyModifiers = modifiers; - } - - public bool Equals(KeyGesture? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - - return Key == other.Key && KeyModifiers == other.KeyModifiers; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - - return obj is KeyGesture gesture && Equals(gesture); - } - - public override int GetHashCode() - { - unchecked - { - return ((int)Key * 397) ^ (int)KeyModifiers; - } - } - - public static bool operator ==(KeyGesture? left, KeyGesture? right) - { - return Equals(left, right); - } - - public static bool operator !=(KeyGesture? left, KeyGesture? right) - { - return !Equals(left, right); - } - - public Key Key { get; } - - [Obsolete("Use KeyModifiers")] - public InputModifiers Modifiers => (InputModifiers)KeyModifiers; - - public KeyModifiers KeyModifiers { get; } - - public static KeyGesture Parse(string gesture) - { - // string.Split can't be used here because "Ctrl++" is a perfectly valid key gesture - - var key = Key.None; - var keyModifiers = KeyModifiers.None; - - var cstart = 0; - - for (var c = 0; c <= gesture.Length; c++) - { - var ch = c == gesture.Length ? '\0' : gesture[c]; - bool isLast = c == gesture.Length; - - if (isLast || (ch == '+' && cstart != c)) - { - var partSpan = gesture.AsSpan(cstart, c - cstart).Trim(); - - if (isLast) - { - key = ParseKey(partSpan.ToString()); - } - else - { - keyModifiers |= ParseModifier(partSpan); - } - - cstart = c + 1; - } - } - - - return new KeyGesture(key, keyModifiers); - } - - public override string ToString() - { - var s = new StringBuilder(); - - static void Plus(StringBuilder s) - { - if (s.Length > 0) - { - s.Append("+"); - } - } - - if (KeyModifiers.HasAllFlags(KeyModifiers.Control)) - { - s.Append("Ctrl"); - } - - if (KeyModifiers.HasAllFlags(KeyModifiers.Shift)) - { - Plus(s); - s.Append("Shift"); - } - - if (KeyModifiers.HasAllFlags(KeyModifiers.Alt)) - { - Plus(s); - s.Append("Alt"); - } - - if (KeyModifiers.HasAllFlags(KeyModifiers.Meta)) - { - Plus(s); - s.Append("Cmd"); - } - - Plus(s); - s.Append(Key); - - return s.ToString(); - } - - public bool Matches(KeyEventArgs keyEvent) => - keyEvent != null && - keyEvent.KeyModifiers == KeyModifiers && - ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(Key); - - // TODO: Move that to external key parser - private static Key ParseKey(string key) - { - if (s_keySynonyms.TryGetValue(key.ToLower(), out Key rv)) - return rv; - - return (Key)Enum.Parse(typeof(Key), key, true); - } - - private static KeyModifiers ParseModifier(ReadOnlySpan modifier) - { - if (modifier.Equals("ctrl".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - return KeyModifiers.Control; - } - - if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase) || - modifier.Equals("win".AsSpan(), StringComparison.OrdinalIgnoreCase) || - modifier.Equals("⌘".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - return KeyModifiers.Meta; - } - - return (KeyModifiers)Enum.Parse(typeof(KeyModifiers), modifier.ToString(), true); - } - - private Key ResolveNumPadOperationKey(Key key) - { - switch (key) - { - case Key.Add: - return Key.OemPlus; - case Key.Subtract: - return Key.OemMinus; - case Key.Decimal: - return Key.OemPeriod; - default: - return key; - } - } - } -} diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs deleted file mode 100644 index 438e3fcadb..0000000000 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ /dev/null @@ -1,269 +0,0 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; -using Avalonia.Input.Raw; -using Avalonia.Input.TextInput; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged - { - private IInputElement? _focusedElement; - private IInputRoot? _focusedRoot; - - public event PropertyChangedEventHandler? PropertyChanged; - - public static IKeyboardDevice? Instance => AvaloniaLocator.Current.GetService(); - - public IInputManager? InputManager => AvaloniaLocator.Current.GetService(); - - public IFocusManager? FocusManager => AvaloniaLocator.Current.GetService(); - - // This should live in the FocusManager, but with the current outdated architecture - // the source of truth about the input focus is in KeyboardDevice - private readonly TextInputMethodManager _textInputManager = new TextInputMethodManager(); - - public IInputElement? FocusedElement - { - get - { - return _focusedElement; - } - - private set - { - _focusedElement = value; - - if (_focusedElement != null && _focusedElement.IsAttachedToVisualTree) - { - _focusedRoot = _focusedElement.VisualRoot as IInputRoot; - } - else - { - _focusedRoot = null; - } - - RaisePropertyChanged(); - _textInputManager.SetFocusedElement(value); - } - } - - private void ClearFocusWithinAncestors(IInputElement? element) - { - var el = element; - - while (el != null) - { - if (el is InputElement ie) - { - ie.IsKeyboardFocusWithin = false; - } - - el = (IInputElement?)el.VisualParent; - } - } - - private void ClearFocusWithin(IInputElement element, bool clearRoot) - { - foreach (var visual in element.VisualChildren) - { - if (visual is IInputElement el && el.IsKeyboardFocusWithin) - { - ClearFocusWithin(el, true); - break; - } - } - - if (clearRoot) - { - if (element is InputElement ie) - { - ie.IsKeyboardFocusWithin = false; - } - } - } - - private void SetIsFocusWithin(IInputElement? oldElement, IInputElement? newElement) - { - if (newElement == null && oldElement != null) - { - ClearFocusWithinAncestors(oldElement); - return; - } - - IInputElement? branch = null; - - var el = newElement; - - while (el != null) - { - if (el.IsKeyboardFocusWithin) - { - branch = el; - break; - } - - el = el.VisualParent as IInputElement; - } - - el = oldElement; - - if (el != null && branch != null) - { - ClearFocusWithin(branch, false); - } - - el = newElement; - - while (el != null && el != branch) - { - if (el is InputElement ie) - { - ie.IsKeyboardFocusWithin = true; - } - - el = el.VisualParent as IInputElement; - } - } - - private void ClearChildrenFocusWithin(IInputElement element, bool clearRoot) - { - foreach (var visual in element.VisualChildren) - { - if (visual is IInputElement el && el.IsKeyboardFocusWithin) - { - ClearChildrenFocusWithin(el, true); - break; - } - } - - if (clearRoot && element is InputElement ie) - { - ie.IsKeyboardFocusWithin = false; - } - } - - public void SetFocusedElement( - IInputElement? element, - NavigationMethod method, - KeyModifiers keyModifiers) - { - if (element != FocusedElement) - { - var interactive = FocusedElement as IInteractive; - - if (FocusedElement != null && - (!FocusedElement.IsAttachedToVisualTree || - _focusedRoot != element?.VisualRoot as IInputRoot) && - _focusedRoot != null) - { - ClearChildrenFocusWithin(_focusedRoot, true); - } - - SetIsFocusWithin(FocusedElement, element); - - FocusedElement = element; - - interactive?.RaiseEvent(new RoutedEventArgs - { - RoutedEvent = InputElement.LostFocusEvent, - }); - - interactive = element as IInteractive; - - interactive?.RaiseEvent(new GotFocusEventArgs - { - RoutedEvent = InputElement.GotFocusEvent, - NavigationMethod = method, - KeyModifiers = keyModifiers, - }); - } - } - - protected void RaisePropertyChanged([CallerMemberName] string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - public void ProcessRawEvent(RawInputEventArgs e) - { - if(e.Handled) - return; - - var element = FocusedElement ?? e.Root; - - if (e is RawKeyEventArgs keyInput) - { - switch (keyInput.Type) - { - case RawKeyEventType.KeyDown: - case RawKeyEventType.KeyUp: - var routedEvent = keyInput.Type == RawKeyEventType.KeyDown - ? InputElement.KeyDownEvent - : InputElement.KeyUpEvent; - - KeyEventArgs ev = new KeyEventArgs - { - RoutedEvent = routedEvent, - Device = this, - Key = keyInput.Key, - KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), - Source = element, - }; - - IVisual? currentHandler = element; - while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown) - { - var bindings = (currentHandler as IInputElement)?.KeyBindings; - if (bindings != null) - { - KeyBinding[]? bindingsCopy = null; - - // Create a copy of the KeyBindings list if there's a binding which matches the event. - // If we don't do this the foreach loop will throw an InvalidOperationException when the KeyBindings list is changed. - // This can happen when a new view is loaded which adds its own KeyBindings to the handler. - foreach (var binding in bindings) - { - if (binding.Gesture?.Matches(ev) == true) - { - bindingsCopy = bindings.ToArray(); - break; - } - } - - if (bindingsCopy is object) - { - foreach (var binding in bindingsCopy) - { - if (ev.Handled) - break; - binding.TryHandle(ev); - } - } - } - currentHandler = currentHandler.VisualParent; - } - - element.RaiseEvent(ev); - e.Handled = ev.Handled; - break; - } - } - - if (e is RawTextInputEventArgs text) - { - var ev = new TextInputEventArgs() - { - Device = this, - Text = text.Text, - Source = element, - RoutedEvent = InputElement.TextInputEvent - }; - - element.RaiseEvent(ev); - e.Handled = ev.Handled; - } - } - } -} diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs deleted file mode 100644 index 34d2038d66..0000000000 --- a/src/Avalonia.Input/MouseDevice.cs +++ /dev/null @@ -1,561 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Input.Raw; -using Avalonia.Interactivity; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - /// - /// Represents a mouse device. - /// - public class MouseDevice : IMouseDevice, IDisposable - { - private int _clickCount; - private Rect _lastClickRect; - private ulong _lastClickTime; - - private readonly Pointer _pointer; - private bool _disposed; - private PixelPoint? _position; - - public MouseDevice(Pointer? pointer = null) - { - _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); - } - - /// - /// Gets the control that is currently capturing by the mouse, if any. - /// - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. To set the mouse capture, call the - /// method. - /// - [Obsolete("Use IPointer instead")] - public IInputElement? Captured => _pointer.Captured; - - /// - /// Gets the mouse position, in screen coordinates. - /// - [Obsolete("Use events instead")] - public PixelPoint Position - { - get => _position ?? new PixelPoint(-1, -1); - protected set => _position = value; - } - - /// - /// Captures mouse input to the specified control. - /// - /// The control. - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. The current mouse capture control is exposed - /// by the property. - /// - public void Capture(IInputElement? control) - { - _pointer.Capture(control); - } - - /// - /// Gets the mouse position relative to a control. - /// - /// The control. - /// The mouse position in the control's coordinates. - public Point GetPosition(IVisual relativeTo) - { - relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); - - if (relativeTo.VisualRoot == null) - { - throw new InvalidOperationException("Control is not attached to visual tree."); - } - -#pragma warning disable CS0618 // Type or member is obsolete - var rootPoint = relativeTo.VisualRoot.PointToClient(Position); -#pragma warning restore CS0618 // Type or member is obsolete - var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); - return rootPoint * transform!.Value; - } - - public void ProcessRawEvent(RawInputEventArgs e) - { - if (!e.Handled && e is RawPointerEventArgs margs) - ProcessRawEvent(margs); - } - - public void TopLevelClosed(IInputRoot root) - { - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - } - - public void SceneInvalidated(IInputRoot root, Rect rect) - { - // Pointer is outside of the target area - if (_position == null ) - { - if (root.PointerOverElement != null) - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - return; - } - - - var clientPoint = root.PointToClient(_position.Value); - - if (rect.Contains(clientPoint)) - { - if (_pointer.Captured == null) - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, - PointerPointProperties.None, KeyModifiers.None); - } - else - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, - PointerPointProperties.None, KeyModifiers.None); - } - } - } - - int ButtonCount(PointerPointProperties props) - { - var rv = 0; - if (props.IsLeftButtonPressed) - rv++; - if (props.IsMiddleButtonPressed) - rv++; - if (props.IsRightButtonPressed) - rv++; - if (props.IsXButton1Pressed) - rv++; - if (props.IsXButton2Pressed) - rv++; - return rv; - } - - private void ProcessRawEvent(RawPointerEventArgs e) - { - e = e ?? throw new ArgumentNullException(nameof(e)); - - var mouse = (MouseDevice)e.Device; - if(mouse._disposed) - return; - - _position = e.Root.PointToScreen(e.Position); - var props = CreateProperties(e); - var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); - switch (e.Type) - { - case RawPointerEventType.LeaveWindow: - LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); - break; - case RawPointerEventType.LeftButtonDown: - case RawPointerEventType.RightButtonDown: - case RawPointerEventType.MiddleButtonDown: - case RawPointerEventType.XButton1Down: - case RawPointerEventType.XButton2Down: - if (ButtonCount(props) > 1) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); - else - e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - props, keyModifiers); - break; - case RawPointerEventType.LeftButtonUp: - case RawPointerEventType.RightButtonUp: - case RawPointerEventType.MiddleButtonUp: - case RawPointerEventType.XButton1Up: - case RawPointerEventType.XButton2Up: - if (ButtonCount(props) != 0) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); - else - e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - break; - case RawPointerEventType.Move: - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); - break; - case RawPointerEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); - break; - case RawPointerEventType.Magnify: - e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); - break; - case RawPointerEventType.Rotate: - e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); - break; - case RawPointerEventType.Swipe: - e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); - break; - } - } - - private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - _position = null; - ClearPointerOver(this, timestamp, root, properties, inputModifiers); - } - - - PointerPointProperties CreateProperties(RawPointerEventArgs args) - { - - var kind = PointerUpdateKind.Other; - - if (args.Type == RawPointerEventType.LeftButtonDown) - kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.MiddleButtonDown) - kind = PointerUpdateKind.MiddleButtonPressed; - if (args.Type == RawPointerEventType.RightButtonDown) - kind = PointerUpdateKind.RightButtonPressed; - if (args.Type == RawPointerEventType.XButton1Down) - kind = PointerUpdateKind.XButton1Pressed; - if (args.Type == RawPointerEventType.XButton2Down) - kind = PointerUpdateKind.XButton2Pressed; - if (args.Type == RawPointerEventType.LeftButtonUp) - kind = PointerUpdateKind.LeftButtonReleased; - if (args.Type == RawPointerEventType.MiddleButtonUp) - kind = PointerUpdateKind.MiddleButtonReleased; - if (args.Type == RawPointerEventType.RightButtonUp) - kind = PointerUpdateKind.RightButtonReleased; - if (args.Type == RawPointerEventType.XButton1Up) - kind = PointerUpdateKind.XButton1Released; - if (args.Type == RawPointerEventType.XButton2Up) - kind = PointerUpdateKind.XButton2Released; - - return new PointerPointProperties(args.InputModifiers, kind); - } - - private MouseButton _lastMouseDownButton; - private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - _pointer.Capture(hit); - var source = GetSource(hit); - if (source != null) - { - var settings = AvaloniaLocator.Current.GetService(); - var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; - var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); - - if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) - { - _clickCount = 0; - } - - ++_clickCount; - _lastClickTime = timestamp; - _lastClickRect = new Rect(p, new Size()) - .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); - _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); - source.RaiseEvent(e); - return e.Handled; - } - } - - return false; - } - - private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers, IReadOnlyList? intermediatePoints) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - IInputElement? source; - - if (_pointer.Captured == null) - { - source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); - } - else - { - SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); - source = _pointer.Captured; - } - - if (source is object) - { - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, - p, timestamp, properties, inputModifiers, intermediatePoints); - - source.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); - - if (source is not null) - { - var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, - _lastMouseDownButton); - - source?.RaiseEvent(e); - _pointer.Capture(null); - return e.Handled; - } - - return false; - } - - private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, - Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); - - if (source is not null) - { - var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - var source = GetSource(hit); - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source, - _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - var source = GetSource(hit); - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source, - _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - var source = GetSource(hit); - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source, - _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private IInteractive? GetSource(IVisual? hit) - { - if (hit is null) - return null; - - return _pointer.Captured ?? - (hit as IInteractive) ?? - hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - } - - private IInputElement? HitTest(IInputElement root, Point p) - { - root = root ?? throw new ArgumentNullException(nameof(root)); - - return _pointer.Captured ?? root.InputHitTest(p); - } - - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - return new PointerEventArgs(ev, source, _pointer, null, default, - timestamp, properties, inputModifiers); - } - - private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); - - if (element!=null && !element.IsAttachedToVisualTree) - { - // element has been removed from visual tree so do top down cleanup - if (root.IsPointerOver) - ClearChildrenPointerOver(e, root,true); - } - while (element != null) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - element = (IInputElement?)element.VisualParent; - } - - root.PointerOverElement = null; - } - - private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) - { - foreach (IInputElement el in element.VisualChildren) - { - if (el.IsPointerOver) - { - ClearChildrenPointerOver(e, el, true); - break; - } - } - if(clearRoot) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - } - } - - private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.InputHitTest(p); - - if (element != root.PointerOverElement) - { - if (element != null) - { - SetPointerOver(device, timestamp, root, element, properties, inputModifiers); - } - else - { - ClearPointerOver(device, timestamp, root, properties, inputModifiers); - } - } - - return element; - } - - private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - element = element ?? throw new ArgumentNullException(nameof(element)); - - IInputElement? branch = null; - - IInputElement? el = element; - - while (el != null) - { - if (el.IsPointerOver) - { - branch = el; - break; - } - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement; - - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); - if (el!=null && branch!=null && !el.IsAttachedToVisualTree) - { - ClearChildrenPointerOver(e,branch,false); - } - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement = element; - e.RoutedEvent = InputElement.PointerEnterEvent; - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - } - - public void Dispose() - { - _disposed = true; - _pointer?.Dispose(); - } - } -} diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs deleted file mode 100644 index 40495a2f0a..0000000000 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Input.Raw; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public class PointerEventArgs : RoutedEventArgs - { - private readonly IVisual? _rootVisual; - private readonly Point _rootVisualPosition; - private readonly PointerPointProperties _properties; - private readonly IReadOnlyList? _previousPoints; - - public PointerEventArgs(RoutedEvent routedEvent, - IInteractive? source, - IPointer pointer, - IVisual? rootVisual, Point rootVisualPosition, - ulong timestamp, - PointerPointProperties properties, - KeyModifiers modifiers) - : base(routedEvent) - { - Source = source; - _rootVisual = rootVisual; - _rootVisualPosition = rootVisualPosition; - _properties = properties; - Pointer = pointer; - Timestamp = timestamp; - KeyModifiers = modifiers; - } - - public PointerEventArgs(RoutedEvent routedEvent, - IInteractive? source, - IPointer pointer, - IVisual? rootVisual, Point rootVisualPosition, - ulong timestamp, - PointerPointProperties properties, - KeyModifiers modifiers, - IReadOnlyList? previousPoints) - : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) - { - _previousPoints = previousPoints; - } - - - class EmulatedDevice : IPointerDevice - { - private readonly PointerEventArgs _ev; - - public EmulatedDevice(PointerEventArgs ev) - { - _ev = ev; - } - - public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException(); - - public IInputElement? Captured => _ev.Pointer.Captured; - public void Capture(IInputElement? control) - { - _ev.Pointer.Capture(control); - } - - public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo); - } - - public IPointer Pointer { get; } - public ulong Timestamp { get; } - - private IPointerDevice? _device; - - [Obsolete("Use Pointer to get pointer-specific information")] - public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this)); - - [Obsolete("Use KeyModifiers and PointerPointProperties")] - public InputModifiers InputModifiers - { - get - { - var mods = (InputModifiers)KeyModifiers; - if (_properties.IsLeftButtonPressed) - mods |= InputModifiers.LeftMouseButton; - if (_properties.IsMiddleButtonPressed) - mods |= InputModifiers.MiddleMouseButton; - if (_properties.IsRightButtonPressed) - mods |= InputModifiers.RightMouseButton; - - return mods; - } - } - - public KeyModifiers KeyModifiers { get; } - - private Point GetPosition(Point pt, IVisual? relativeTo) - { - if (_rootVisual == null) - return default; - if (relativeTo == null) - return pt; - return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; - } - - public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); - - [Obsolete("Use GetCurrentPoint")] - public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo); - - /// - /// Returns the PointerPoint associated with the current event - /// - /// The visual which coordinate system to use. Pass null for toplevel coordinate system - /// - public PointerPoint GetCurrentPoint(IVisual? relativeTo) - => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); - - /// - /// Returns the PointerPoint associated with the current event - /// - /// The visual which coordinate system to use. Pass null for toplevel coordinate system - /// - public IReadOnlyList GetIntermediatePoints(IVisual? relativeTo) - { - if (_previousPoints == null || _previousPoints.Count == 0) - return new[] { GetCurrentPoint(relativeTo) }; - var points = new PointerPoint[_previousPoints.Count + 1]; - for (var c = 0; c < _previousPoints.Count; c++) - { - var pt = _previousPoints[c]; - points[c] = new PointerPoint(Pointer, GetPosition(pt, relativeTo), _properties); - } - - points[points.Length - 1] = GetCurrentPoint(relativeTo); - return points; - } - - /// - /// Returns the current pointer point properties - /// - protected PointerPointProperties Properties => _properties; - } - - public enum MouseButton - { - None, - Left, - Right, - Middle, - XButton1, - XButton2 - } - - public class PointerPressedEventArgs : PointerEventArgs - { - private readonly int _clickCount; - - public PointerPressedEventArgs( - IInteractive source, - IPointer pointer, - IVisual rootVisual, Point rootVisualPosition, - ulong timestamp, - PointerPointProperties properties, - KeyModifiers modifiers, - int clickCount = 1) - : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, - timestamp, properties, modifiers) - { - _clickCount = clickCount; - } - - public int ClickCount => _clickCount; - - [Obsolete("Use PointerPressedEventArgs.GetCurrentPoint(this).Properties")] - public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton(); - } - - public class PointerReleasedEventArgs : PointerEventArgs - { - public PointerReleasedEventArgs( - IInteractive source, IPointer pointer, - IVisual rootVisual, Point rootVisualPosition, ulong timestamp, - PointerPointProperties properties, KeyModifiers modifiers, - MouseButton initialPressMouseButton) - : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition, - timestamp, properties, modifiers) - { - InitialPressMouseButton = initialPressMouseButton; - } - - /// - /// Gets the mouse button that triggered the corresponding PointerPressed event - /// - public MouseButton InitialPressMouseButton { get; } - - [Obsolete("Use InitialPressMouseButton")] - public MouseButton MouseButton => InitialPressMouseButton; - } - - public class PointerCaptureLostEventArgs : RoutedEventArgs - { - public IPointer Pointer { get; } - - public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) - { - Pointer = pointer; - Source = source; - } - } -} diff --git a/src/Avalonia.Input/Properties/AssemblyInfo.cs b/src/Avalonia.Input/Properties/AssemblyInfo.cs deleted file mode 100644 index 433f821ca3..0000000000 --- a/src/Avalonia.Input/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System.Reflection; -using Avalonia.Metadata; - -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")] diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs deleted file mode 100644 index 6e9ce20ff1..0000000000 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace Avalonia.Input.Raw -{ - public class RawDragEvent : RawInputEventArgs - { - public Point Location { get; set; } - public IDataObject Data { get; } - public DragDropEffects Effects { get; set; } - public RawDragEventType Type { get; } - [Obsolete("Use KeyModifiers")] - public InputModifiers Modifiers { get; } - public KeyModifiers KeyModifiers { get; } - - public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, - IInputRoot root, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) - :base(inputDevice, 0, root) - { - Type = type; - Location = location; - Data = data; - Effects = effects; - KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers); -#pragma warning disable CS0618 // Type or member is obsolete - Modifiers = (InputModifiers)modifiers; -#pragma warning restore CS0618 // Type or member is obsolete - } - } -} diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs deleted file mode 100644 index 1fe19d9c55..0000000000 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Avalonia.Input.Raw -{ - public enum RawPointerEventType - { - LeaveWindow, - LeftButtonDown, - LeftButtonUp, - RightButtonDown, - RightButtonUp, - MiddleButtonDown, - MiddleButtonUp, - XButton1Down, - XButton1Up, - XButton2Down, - XButton2Up, - Move, - Wheel, - NonClientLeftButtonDown, - TouchBegin, - TouchUpdate, - TouchEnd, - TouchCancel, - Magnify, - Rotate, - Swipe - } - - /// - /// A raw mouse event. - /// - public class RawPointerEventArgs : RawInputEventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The associated device. - /// The event timestamp. - /// The root from which the event originates. - /// The type of the event. - /// The mouse position, in client DIPs. - /// The input modifiers. - public RawPointerEventArgs( - IInputDevice device, - ulong timestamp, - IInputRoot root, - RawPointerEventType type, - Point position, - RawInputModifiers inputModifiers) - : base(device, timestamp, root) - { - Contract.Requires(device != null); - Contract.Requires(root != null); - - Position = position; - Type = type; - InputModifiers = inputModifiers; - } - - /// - /// Gets the mouse position, in client DIPs. - /// - public Point Position { get; set; } - - /// - /// Gets the type of the event. - /// - public RawPointerEventType Type { get; set; } - - /// - /// Gets the input modifiers. - /// - public RawInputModifiers InputModifiers { get; set; } - - /// - /// Points that were traversed by a pointer since the previous relevant event, - /// only valid for Move and TouchUpdate - /// - public IReadOnlyList? IntermediatePoints { get; set; } - } -} diff --git a/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs b/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs deleted file mode 100644 index 2d24ed30a0..0000000000 --- a/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Avalonia.Input.TextInput -{ - public interface ITextInputMethodImpl - { - void SetActive(bool active); - void SetCursorRect(Rect rect); - void SetOptions(TextInputOptionsQueryEventArgs options); - void Reset(); - } - - public interface ITextInputMethodRoot : IInputRoot - { - ITextInputMethodImpl? InputMethod { get; } - } -} diff --git a/src/Avalonia.Input/TextInput/InputMethodManager.cs b/src/Avalonia.Input/TextInput/InputMethodManager.cs deleted file mode 100644 index 64422a7fdf..0000000000 --- a/src/Avalonia.Input/TextInput/InputMethodManager.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using Avalonia.VisualTree; - -namespace Avalonia.Input.TextInput -{ - internal class TextInputMethodManager - { - private ITextInputMethodImpl? _im; - private IInputElement? _focusedElement; - private ITextInputMethodClient? _client; - private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper(); - - public TextInputMethodManager() - { - _transformTracker.MatrixChanged += UpdateCursorRect; - InputMethod.IsInputMethodEnabledProperty.Changed.Subscribe(OnIsInputMethodEnabledChanged); - } - - private ITextInputMethodClient? Client - { - get => _client; - set - { - if(_client == value) - return; - if (_client != null) - { - _client.CursorRectangleChanged -= OnCursorRectangleChanged; - _client.TextViewVisualChanged -= OnTextViewVisualChanged; - } - - _client = value; - - if (_client != null) - { - _client.CursorRectangleChanged += OnCursorRectangleChanged; - _client.TextViewVisualChanged += OnTextViewVisualChanged; - var optionsQuery = new TextInputOptionsQueryEventArgs - { - RoutedEvent = InputElement.TextInputOptionsQueryEvent - }; - _focusedElement?.RaiseEvent(optionsQuery); - _im?.Reset(); - _im?.SetOptions(optionsQuery); - _transformTracker?.SetVisual(_client?.TextViewVisual); - UpdateCursorRect(); - - _im?.SetActive(true); - } - else - { - _im?.SetActive(false); - _transformTracker.SetVisual(null); - } - } - } - - private void OnIsInputMethodEnabledChanged(AvaloniaPropertyChangedEventArgs obj) - { - if (ReferenceEquals(obj.Sender, _focusedElement)) - { - TryFindAndApplyClient(); - } - } - - private void OnTextViewVisualChanged(object? sender, EventArgs e) - => _transformTracker.SetVisual(_client?.TextViewVisual); - - private void UpdateCursorRect() - { - if (_im == null || _client == null || _focusedElement?.VisualRoot == null) - return; - - var transform = _focusedElement.TransformToVisual(_focusedElement.VisualRoot); - if (transform == null) - _im.SetCursorRect(default); - else - _im.SetCursorRect(_client.CursorRectangle.TransformToAABB(transform.Value)); - } - - private void OnCursorRectangleChanged(object? sender, EventArgs e) - { - if (sender == _client) - UpdateCursorRect(); - } - - public void SetFocusedElement(IInputElement? element) - { - if(_focusedElement == element) - return; - _focusedElement = element; - - var inputMethod = (element?.VisualRoot as ITextInputMethodRoot)?.InputMethod; - if (_im != inputMethod) - _im?.SetActive(false); - - _im = inputMethod; - - TryFindAndApplyClient(); - } - - private void TryFindAndApplyClient() - { - if (_focusedElement is not InputElement focused || - _im == null || - !InputMethod.GetIsInputMethodEnabled(focused)) - { - Client = null; - return; - } - - var clientQuery = new TextInputMethodClientRequestedEventArgs - { - RoutedEvent = InputElement.TextInputMethodClientRequestedEvent - }; - - _focusedElement.RaiseEvent(clientQuery); - Client = clientQuery.Client; - } - } -} diff --git a/src/Avalonia.Input/TextInput/TextInputContentType.cs b/src/Avalonia.Input/TextInput/TextInputContentType.cs deleted file mode 100644 index 5d73fc1552..0000000000 --- a/src/Avalonia.Input/TextInput/TextInputContentType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Avalonia.Input.TextInput -{ - public enum TextInputContentType - { - Normal = 0, - Email = 1, - Phone = 2, - Number = 3, - Url = 4, - Password = 5 - } -} diff --git a/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs b/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs deleted file mode 100644 index 924d0eb166..0000000000 --- a/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Avalonia.Interactivity; - -namespace Avalonia.Input.TextInput -{ - public class TextInputOptionsQueryEventArgs : RoutedEventArgs - { - /// - /// The content type (mostly for determining the shape of the virtual keyboard) - /// - public TextInputContentType ContentType { get; set; } - /// - /// Text is multiline - /// - public bool Multiline { get; set; } - /// - /// Text is in lower case - /// - public bool Lowercase { get; set; } - /// - /// Text is in upper case - /// - public bool Uppercase { get; set; } - /// - /// Automatically capitalize letters at the start of the sentence - /// - public bool AutoCapitalization { get; set; } - /// - /// Text contains sensitive data like card numbers and should not be stored - /// - public bool IsSensitive { get; set; } - } -} diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs deleted file mode 100644 index 12ad182bf8..0000000000 --- a/src/Avalonia.Input/TouchDevice.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Input.Raw; -using Avalonia.Platform; - -namespace Avalonia.Input -{ - /// - /// Handles raw touch events - /// - /// This class is supposed to be used on per-toplevel basis, don't use a shared one - /// - /// - public class TouchDevice : IInputDevice, IDisposable - { - private readonly Dictionary _pointers = new Dictionary(); - private bool _disposed; - private int _clickCount; - private Rect _lastClickRect; - private ulong _lastClickTime; - KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => - (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); - - RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown) - { - var rv = modifiers &= RawInputModifiers.KeyboardMask; - if (isLeftButtonDown) - rv |= RawInputModifiers.LeftMouseButton; - return rv; - } - - public void ProcessRawEvent(RawInputEventArgs ev) - { - if (_disposed) - return; - var args = (RawTouchEventArgs)ev; - if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) - { - if (args.Type == RawPointerEventType.TouchEnd) - return; - var hit = args.Root.InputHitTest(args.Position); - - _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), - PointerType.Touch, _pointers.Count == 0); - pointer.Capture(hit); - } - - - var target = pointer.Captured ?? args.Root; - if (args.Type == RawPointerEventType.TouchBegin) - { - if (_pointers.Count > 1) - { - _clickCount = 1; - _lastClickTime = 0; - _lastClickRect = new Rect(); - } - else - { - var settings = AvaloniaLocator.Current.GetRequiredService(); - - if (!_lastClickRect.Contains(args.Position) - || ev.Timestamp - _lastClickTime > settings.TouchDoubleClickTime.TotalMilliseconds) - { - _clickCount = 0; - } - ++_clickCount; - _lastClickTime = ev.Timestamp; - _lastClickRect = new Rect(args.Position, new Size()) - .Inflate(new Thickness(settings.TouchDoubleClickSize.Width / 2, settings.TouchDoubleClickSize.Height / 2)); - } - - target.RaiseEvent(new PointerPressedEventArgs(target, pointer, - args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, true), - PointerUpdateKind.LeftButtonPressed), - GetKeyModifiers(args.InputModifiers), _clickCount)); - } - - if (args.Type == RawPointerEventType.TouchEnd) - { - _pointers.Remove(args.TouchPointId); - using (pointer) - { - target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, - args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, false), - PointerUpdateKind.LeftButtonReleased), - GetKeyModifiers(args.InputModifiers), MouseButton.Left)); - } - } - - if (args.Type == RawPointerEventType.TouchCancel) - { - _pointers.Remove(args.TouchPointId); - using (pointer) - pointer.Capture(null); - } - - if (args.Type == RawPointerEventType.TouchUpdate) - { - var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); - target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, - args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other), - GetKeyModifiers(args.InputModifiers), args.IntermediatePoints)); - } - - - } - - public void Dispose() - { - if (_disposed) - return; - var values = _pointers.Values.ToList(); - _pointers.Clear(); - _disposed = true; - foreach (var p in values) - p.Dispose(); - } - - } -} diff --git a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj deleted file mode 100644 index 03a53f7d86..0000000000 --- a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - net6.0;netstandard2.0 - - - - - - - - - - - \ No newline at end of file diff --git a/src/Avalonia.Layout/Avalonia.Layout.csproj b/src/Avalonia.Layout/Avalonia.Layout.csproj deleted file mode 100644 index f2c4b0540b..0000000000 --- a/src/Avalonia.Layout/Avalonia.Layout.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net6.0;netstandard2.0 - - - - - - - - - - diff --git a/src/Avalonia.Layout/Properties/AssemblyInfo.cs b/src/Avalonia.Layout/Properties/AssemblyInfo.cs deleted file mode 100644 index efcbf184b5..0000000000 --- a/src/Avalonia.Layout/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Runtime.CompilerServices; -using Avalonia.Metadata; - -[assembly: InternalsVisibleTo("Avalonia.Layout.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")] - diff --git a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj index b796e173c4..d7f39f6642 100644 --- a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj +++ b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj @@ -10,6 +10,8 @@ false all + true + TargetFramework=netstandard2.0 diff --git a/src/Avalonia.MicroCom/CallbackBase.cs b/src/Avalonia.MicroCom/CallbackBase.cs new file mode 100644 index 0000000000..6783ebe3dc --- /dev/null +++ b/src/Avalonia.MicroCom/CallbackBase.cs @@ -0,0 +1,53 @@ +namespace Avalonia.MicroCom +{ + public abstract class CallbackBase : IUnknown, IMicroComShadowContainer + { + private readonly object _lock = new object(); + private bool _referencedFromManaged = true; + private bool _referencedFromNative = false; + private bool _destroyed; + + public bool IsDestroyed => _destroyed; + + protected virtual void Destroyed() + { + + } + + public void Dispose() + { + lock (_lock) + { + _referencedFromManaged = false; + DestroyIfNeeded(); + } + } + + void DestroyIfNeeded() + { + if(_destroyed) + return; + if (_referencedFromManaged == false && _referencedFromNative == false) + { + _destroyed = true; + Destroyed(); + } + } + + public MicroComShadow Shadow { get; set; } + public void OnReferencedFromNative() + { + lock (_lock) + _referencedFromNative = true; + } + + public void OnUnreferencedFromNative() + { + lock (_lock) + { + _referencedFromNative = false; + DestroyIfNeeded(); + } + } + } +} diff --git a/src/Avalonia.MicroCom/MicroComRuntime.cs b/src/Avalonia.MicroCom/MicroComRuntime.cs index e0f524146a..b9a56a69ba 100644 --- a/src/Avalonia.MicroCom/MicroComRuntime.cs +++ b/src/Avalonia.MicroCom/MicroComRuntime.cs @@ -12,6 +12,8 @@ namespace Avalonia.MicroCom new ConcurrentDictionary>(); private static ConcurrentDictionary _guids = new ConcurrentDictionary(); private static ConcurrentDictionary _guidsToTypes = new ConcurrentDictionary(); + + internal static readonly Guid ManagedObjectInterfaceGuid = Guid.Parse("cd7687c0-a9c2-4563-b08e-a399df50c633"); static MicroComRuntime() { @@ -82,6 +84,19 @@ namespace Avalonia.MicroCom return shadow.Target; } + public static bool IsComWrapper(IUnknown obj) => obj is MicroComProxyBase; + + public static object TryUnwrapManagedObject(IUnknown obj) + { + if (obj is not MicroComProxyBase proxy) + return null; + if (proxy.QueryInterface(ManagedObjectInterfaceGuid, out _) != 0) + return null; + // Successful QueryInterface always increments ref counter + proxy.Release(); + return GetObjectFromCcw(proxy.NativePointer); + } + public static bool TryGetTypeForGuid(Guid guid, out Type t) => _guidsToTypes.TryGetValue(guid, out t); public static bool GetVtableFor(Type type, out IntPtr ptr) => _vtables.TryGetValue(type, out ptr); diff --git a/src/Avalonia.MicroCom/MicroComShadow.cs b/src/Avalonia.MicroCom/MicroComShadow.cs index a6a0fd519e..765ff3b9ad 100644 --- a/src/Avalonia.MicroCom/MicroComShadow.cs +++ b/src/Avalonia.MicroCom/MicroComShadow.cs @@ -23,6 +23,12 @@ namespace Avalonia.MicroCom { if (MicroComRuntime.TryGetTypeForGuid(*guid, out var type)) return QueryInterface(type, ppv); + else if (*guid == MicroComRuntime.ManagedObjectInterfaceGuid) + { + ccw->RefCount++; + *ppv = ccw; + return 0; + } else return unchecked((int)0x80004002u); } diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 9301fd4d91..2001a2fcbc 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -17,6 +17,10 @@ PreserveNewest + + + + @@ -24,4 +28,6 @@ + + diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index 8084e06d28..ae24d06bad 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -6,7 +6,7 @@ using Avalonia.Platform; namespace Avalonia.Native { - internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents, IPlatformLifetimeEventsImpl + internal class AvaloniaNativeApplicationPlatform : NativeCallbackBase, IAvnApplicationEvents, IPlatformLifetimeEventsImpl { public event EventHandler ShutdownRequested; diff --git a/src/Avalonia.Native/AvaloniaNativeDragSource.cs b/src/Avalonia.Native/AvaloniaNativeDragSource.cs index 80d54d8a10..f91a299b3b 100644 --- a/src/Avalonia.Native/AvaloniaNativeDragSource.cs +++ b/src/Avalonia.Native/AvaloniaNativeDragSource.cs @@ -30,7 +30,7 @@ namespace Avalonia.Native return visual.VisualRoot as TopLevel; } - class DndCallback : CallbackBase, IAvnDndResultCallback + class DndCallback : NativeCallbackBase, IAvnDndResultCallback { private TaskCompletionSource _tcs; diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 1eadf70b13..4802fed554 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -79,7 +79,7 @@ namespace Avalonia.Native _factory = factory; } - class GCHandleDeallocator : CallbackBase, IAvnGCHandleDeallocatorCallback + class GCHandleDeallocator : NativeCallbackBase, IAvnGCHandleDeallocatorCallback { public void FreeGCHandle(IntPtr handle) { diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs new file mode 100644 index 0000000000..6c4e96b31b --- /dev/null +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Native.Interop; + +#nullable enable + +namespace Avalonia.Native +{ + internal class AvnAutomationPeer : NativeCallbackBase, IAvnAutomationPeer + { + private static readonly ConditionalWeakTable s_wrappers = new(); + private readonly AutomationPeer _inner; + + private AvnAutomationPeer(AutomationPeer inner) + { + _inner = inner; + _inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged(); + if (inner is WindowBaseAutomationPeer window) + window.FocusChanged += (_, _) => Node?.FocusChanged(); + } + + ~AvnAutomationPeer() => Node?.Dispose(); + + public IAvnAutomationNode? Node { get; private set; } + public IAvnString? AcceleratorKey => _inner.GetAcceleratorKey().ToAvnString(); + public IAvnString? AccessKey => _inner.GetAccessKey().ToAvnString(); + public AvnAutomationControlType AutomationControlType => (AvnAutomationControlType)_inner.GetAutomationControlType(); + public IAvnString? AutomationId => _inner.GetAutomationId().ToAvnString(); + public AvnRect BoundingRectangle => _inner.GetBoundingRectangle().ToAvnRect(); + public IAvnAutomationPeerArray Children => new AvnAutomationPeerArray(_inner.GetChildren()); + public IAvnString ClassName => _inner.GetClassName().ToAvnString(); + public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy()); + public IAvnString Name => _inner.GetName().ToAvnString(); + public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent()); + + public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool(); + public int IsContentElement() => _inner.IsContentElement().AsComBool(); + public int IsControlElement() => _inner.IsControlElement().AsComBool(); + public int IsEnabled() => _inner.IsEnabled().AsComBool(); + public int IsKeyboardFocusable() => _inner.IsKeyboardFocusable().AsComBool(); + public void SetFocus() => _inner.SetFocus(); + public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool(); + + public IAvnAutomationPeer? RootPeer + { + get + { + var peer = _inner; + var parent = peer.GetParent(); + + while (peer is not IRootProvider && parent is not null) + { + peer = parent; + parent = peer.GetParent(); + } + + return Wrap(peer); + } + } + + public void SetNode(IAvnAutomationNode node) + { + if (Node is not null) + throw new InvalidOperationException("The AvnAutomationPeer already has a node."); + Node = node; + } + + public int IsRootProvider() => (_inner is IRootProvider).AsComBool(); + + public IAvnWindowBase RootProvider_GetWindow() + { + var window = (WindowBase)((ControlAutomationPeer)_inner).Owner; + return ((WindowBaseImpl)window.PlatformImpl!).Native; + } + + public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus()); + + public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point) + { + var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint()); + + if (result is null) + return null; + + // The OSX accessibility APIs expect non-ignored elements when hit-testing. + while (!result.IsControlElement()) + { + var parent = result.GetParent(); + + if (parent is not null) + result = parent; + else + break; + } + + return Wrap(result); + } + + public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool(); + + public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch + { + ExpandCollapseState.Expanded => 1, + ExpandCollapseState.PartiallyExpanded => 1, + _ => 0, + }; + + public int ExpandCollapseProvider_GetShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool(); + public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand(); + public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse(); + + public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool(); + public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke(); + + public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool(); + public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value; + public double RangeValueProvider_GetMinimum() => ((IRangeValueProvider)_inner).Minimum; + public double RangeValueProvider_GetMaximum() => ((IRangeValueProvider)_inner).Maximum; + public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange; + public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange; + public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value); + + public int IsSelectionItemProvider() => (_inner is ISelectionItemProvider).AsComBool(); + public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool(); + + public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool(); + public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState; + public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle(); + + public int IsValueProvider() => (_inner is IValueProvider).AsComBool(); + public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString(); + public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value); + + [return: NotNullIfNotNull("peer")] + public static AvnAutomationPeer? Wrap(AutomationPeer? peer) + { + return peer is null ? null : s_wrappers.GetValue(peer, x => new(peer)); + } + } + + internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray + { + private readonly AvnAutomationPeer[] _items; + + public AvnAutomationPeerArray(IReadOnlyList items) + { + _items = items.Select(x => AvnAutomationPeer.Wrap(x)).ToArray(); + } + + public uint Count => (uint)_items.Length; + public IAvnAutomationPeer Get(uint index) => _items[index]; + } +} diff --git a/src/Avalonia.Native/AvnString.cs b/src/Avalonia.Native/AvnString.cs index dcd473bae3..1c92bf0eec 100644 --- a/src/Avalonia.Native/AvnString.cs +++ b/src/Avalonia.Native/AvnString.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Text; namespace Avalonia.Native.Interop { @@ -13,6 +14,53 @@ namespace Avalonia.Native.Interop { string[] ToStringArray(); } + + internal class AvnString : NativeCallbackBase, IAvnString + { + private IntPtr _native; + private int _nativeLen; + + public AvnString(string s) => String = s; + + public string String { get; } + public byte[] Bytes => Encoding.UTF8.GetBytes(String); + + public unsafe void* Pointer() + { + EnsureNative(); + return _native.ToPointer(); + } + + public int Length() + { + EnsureNative(); + return _nativeLen; + } + + protected override void Destroyed() + { + if (_native != IntPtr.Zero) + { + Marshal.FreeHGlobal(_native); + _native = IntPtr.Zero; + } + } + + private unsafe void EnsureNative() + { + if (string.IsNullOrEmpty(String)) + return; + if (_native == IntPtr.Zero) + { + _nativeLen = Encoding.UTF8.GetByteCount(String); + _native = Marshal.AllocHGlobal(_nativeLen + 1); + var ptr = (byte*)_native.ToPointer(); + fixed (char* chars = String) + Encoding.UTF8.GetBytes(chars, String.Length, ptr, _nativeLen); + ptr[_nativeLen] = 0; + } + } + } } namespace Avalonia.Native.Interop.Impl { diff --git a/src/Avalonia.Native/CallbackBase.cs b/src/Avalonia.Native/CallbackBase.cs index 455ed4b159..56f9505cb4 100644 --- a/src/Avalonia.Native/CallbackBase.cs +++ b/src/Avalonia.Native/CallbackBase.cs @@ -5,19 +5,8 @@ using Avalonia.Platform; namespace Avalonia.Native { - public class CallbackBase : IUnknown, IMicroComShadowContainer, IMicroComExceptionCallback + public abstract class NativeCallbackBase : CallbackBase, IMicroComExceptionCallback { - private readonly object _lock = new object(); - private bool _referencedFromManaged = true; - private bool _referencedFromNative = false; - private bool _destroyed; - - - protected virtual void Destroyed() - { - - } - public void RaiseException(Exception e) { if (AvaloniaLocator.Current.GetService() is PlatformThreadingInterface threadingInterface) @@ -27,41 +16,5 @@ namespace Avalonia.Native threadingInterface.DispatchException(ExceptionDispatchInfo.Capture(e)); } } - - public void Dispose() - { - lock (_lock) - { - _referencedFromManaged = false; - DestroyIfNeeded(); - } - } - - void DestroyIfNeeded() - { - if(_destroyed) - return; - if (_referencedFromManaged == false && _referencedFromNative == false) - { - _destroyed = true; - Destroyed(); - } - } - - public MicroComShadow Shadow { get; set; } - public void OnReferencedFromNative() - { - lock (_lock) - _referencedFromNative = true; - } - - public void OnUnreferencedFromNative() - { - lock (_lock) - { - _referencedFromNative = false; - DestroyIfNeeded(); - } - } } } diff --git a/src/Avalonia.Native/DeferredFramebuffer.cs b/src/Avalonia.Native/DeferredFramebuffer.cs index 950b6a3197..4e0c037154 100644 --- a/src/Avalonia.Native/DeferredFramebuffer.cs +++ b/src/Avalonia.Native/DeferredFramebuffer.cs @@ -27,7 +27,7 @@ namespace Avalonia.Native public Vector Dpi { get; set; } public PixelFormat Format { get; set; } - class Disposer : CallbackBase + class Disposer : NativeCallbackBase { private IntPtr _ptr; diff --git a/src/Avalonia.Native/Helpers.cs b/src/Avalonia.Native/Helpers.cs index 564434a04c..764ff789dc 100644 --- a/src/Avalonia.Native/Helpers.cs +++ b/src/Avalonia.Native/Helpers.cs @@ -1,4 +1,5 @@ using Avalonia.Native.Interop; +using JetBrains.Annotations; namespace Avalonia.Native { @@ -24,11 +25,21 @@ namespace Avalonia.Native return new AvnPoint { X = pt.X, Y = pt.Y }; } + public static AvnRect ToAvnRect (this Rect rect) + { + return new AvnRect() { X = rect.X, Y= rect.Y, Height = rect.Height, Width = rect.Width }; + } + public static AvnSize ToAvnSize (this Size size) { return new AvnSize { Height = size.Height, Width = size.Width }; } + public static IAvnString ToAvnString(this string s) + { + return s != null ? new AvnString(s) : null; + } + public static Size ToAvaloniaSize (this AvnSize size) { return new Size(size.Width, size.Height); diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index 0e6fdd2df0..e413023f6d 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -7,7 +7,7 @@ using Avalonia.Platform.Interop; namespace Avalonia.Native.Interop { - class MenuEvents : CallbackBase, IAvnMenuEvents + class MenuEvents : NativeCallbackBase, IAvnMenuEvents { private IAvnMenu _parent; diff --git a/src/Avalonia.Native/MenuActionCallback.cs b/src/Avalonia.Native/MenuActionCallback.cs index 5318195f30..3acbb9c19c 100644 --- a/src/Avalonia.Native/MenuActionCallback.cs +++ b/src/Avalonia.Native/MenuActionCallback.cs @@ -3,7 +3,7 @@ using Avalonia.Native.Interop; namespace Avalonia.Native { - public class MenuActionCallback : CallbackBase, IAvnActionCallback + public class MenuActionCallback : NativeCallbackBase, IAvnActionCallback { private Action _action; diff --git a/src/Avalonia.Native/PlatformThreadingInterface.cs b/src/Avalonia.Native/PlatformThreadingInterface.cs index 882d5a2ed3..99aa8a5850 100644 --- a/src/Avalonia.Native/PlatformThreadingInterface.cs +++ b/src/Avalonia.Native/PlatformThreadingInterface.cs @@ -9,7 +9,7 @@ namespace Avalonia.Native { internal class PlatformThreadingInterface : IPlatformThreadingInterface { - class TimerCallback : CallbackBase, IAvnActionCallback + class TimerCallback : NativeCallbackBase, IAvnActionCallback { readonly Action _tick; @@ -24,7 +24,7 @@ namespace Avalonia.Native } } - class SignaledCallback : CallbackBase, IAvnSignaledCallback + class SignaledCallback : NativeCallbackBase, IAvnSignaledCallback { readonly PlatformThreadingInterface _parent; diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 4680a2af17..76d3905b47 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -7,7 +7,6 @@ namespace Avalonia.Native { class PopupImpl : WindowBaseImpl, IPopupImpl { - private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; private readonly IWindowBaseImpl _parent; @@ -15,9 +14,8 @@ namespace Avalonia.Native public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature, - IWindowBaseImpl parent) : base(opts, glFeature) + IWindowBaseImpl parent) : base(factory, opts, glFeature) { - _factory = factory; _opts = opts; _glFeature = glFeature; _parent = parent; diff --git a/src/Avalonia.Native/PredicateCallback.cs b/src/Avalonia.Native/PredicateCallback.cs index 19c470bcb3..dbb65791f0 100644 --- a/src/Avalonia.Native/PredicateCallback.cs +++ b/src/Avalonia.Native/PredicateCallback.cs @@ -3,7 +3,7 @@ using Avalonia.Native.Interop; namespace Avalonia.Native { - public class PredicateCallback : CallbackBase, IAvnPredicateCallback + public class PredicateCallback : NativeCallbackBase, IAvnPredicateCallback { private Func _predicate; diff --git a/src/Avalonia.Native/Properties/AssemblyInfo.cs b/src/Avalonia.Native/Properties/AssemblyInfo.cs deleted file mode 100644 index d4766e45dc..0000000000 --- a/src/Avalonia.Native/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -using Avalonia.Native; -using Avalonia.Platform; - -[assembly: ExportWindowingSubsystem(OperatingSystemType.OSX, 1, "AvaloniaNative", typeof(AvaloniaNativePlatform), nameof(AvaloniaNativePlatform.Initialize))] diff --git a/src/Avalonia.Native/ScreenImpl.cs b/src/Avalonia.Native/ScreenImpl.cs index 7b4a001486..83db2e8a28 100644 --- a/src/Avalonia.Native/ScreenImpl.cs +++ b/src/Avalonia.Native/ScreenImpl.cs @@ -48,5 +48,20 @@ namespace Avalonia.Native _native?.Dispose(); _native = null; } + + public Screen ScreenFromPoint(PixelPoint point) + { + return ScreenHelper.ScreenFromPoint(point, AllScreens); + } + + public Screen ScreenFromRect(PixelRect rect) + { + return ScreenHelper.ScreenFromRect(rect, AllScreens); + } + + public Screen ScreenFromWindow(IWindowBaseImpl window) + { + return ScreenHelper.ScreenFromWindow(window, AllScreens); + } } } diff --git a/src/Avalonia.Native/SystemDialogs.cs b/src/Avalonia.Native/SystemDialogs.cs index 8012813644..4372829df1 100644 --- a/src/Avalonia.Native/SystemDialogs.cs +++ b/src/Avalonia.Native/SystemDialogs.cs @@ -62,7 +62,7 @@ namespace Avalonia.Native } } - internal unsafe class SystemDialogEvents : CallbackBase, IAvnSystemDialogEvents + internal unsafe class SystemDialogEvents : NativeCallbackBase, IAvnSystemDialogEvents { private TaskCompletionSource _tcs; diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index f740be44a2..c082bdb1b8 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -13,7 +13,6 @@ namespace Avalonia.Native { internal class WindowImpl : WindowBaseImpl, IWindowImpl, ITopLevelImplWithNativeMenuExporter { - private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; IAvnWindow _native; @@ -22,9 +21,8 @@ namespace Avalonia.Native internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - AvaloniaNativePlatformOpenGlInterface glFeature) : base(opts, glFeature) + AvaloniaNativePlatformOpenGlInterface glFeature) : base(factory, opts, glFeature) { - _factory = factory; _opts = opts; _glFeature = glFeature; _doubleClickHelper = new DoubleClickHelper(); @@ -87,7 +85,10 @@ namespace Avalonia.Native _native.SetTitleBarColor(new AvnColor { Alpha = color.A, Red = color.R, Green = color.G, Blue = color.B }); } - public void SetTitle(string title) => _native.SetTitle(title); + public void SetTitle(string title) + { + _native.SetTitle(title ?? ""); + } public WindowState WindowState { diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 1917b1575d..735f11bcd5 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using Avalonia.Automation.Peers; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Platform.Surfaces; @@ -46,6 +47,7 @@ namespace Avalonia.Native internal abstract class WindowBaseImpl : IWindowBaseImpl, IFramebufferPlatformSurface, ITopLevelImplWithNativeControlHost { + protected readonly IAvaloniaNativeFactory _factory; protected IInputRoot _inputRoot; IAvnWindowBase _native; private object _syncRoot = new object(); @@ -61,8 +63,10 @@ namespace Avalonia.Native private NativeControlHostImpl _nativeControlHost; private IGlContext _glContext; - internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature) + internal WindowBaseImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, + AvaloniaNativePlatformOpenGlInterface glFeature) { + _factory = factory; _gpu = opts.UseGpu && glFeature != null; _deferredRendering = opts.UseDeferredRendering; @@ -90,6 +94,8 @@ namespace Avalonia.Native Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d), PlatformResizeReason.Layout); } + public IAvnWindowBase Native => _native; + public Size ClientSize { get @@ -151,7 +157,12 @@ namespace Avalonia.Native public IMouseDevice MouseDevice => _mouse; public abstract IPopupImpl CreatePopup(); - protected unsafe class WindowBaseEvents : CallbackBase, IAvnWindowBaseEvents + public AutomationPeer GetAutomationPeer() + { + return _inputRoot is Control c ? ControlAutomationPeer.CreatePeerForElement(c) : null; + } + + protected unsafe class WindowBaseEvents : NativeCallbackBase, IAvnWindowBaseEvents { private readonly WindowBaseImpl _parent; @@ -216,7 +227,6 @@ namespace Avalonia.Native return _parent.RawTextInputEvent(timeStamp, text).AsComBool(); } - void IAvnWindowBaseEvents.ScalingChanged(double scaling) { _parent._savedScaling = scaling; @@ -256,8 +266,13 @@ namespace Avalonia.Native return (AvnDragDropEffects)args.Effects; } } - } + IAvnAutomationPeer IAvnWindowBaseEvents.AutomationPeer + { + get => AvnAutomationPeer.Wrap(_parent.GetAutomationPeer()); + } + } + public void Activate() { _native?.Activate(); diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index f2b9d4997e..8092004989 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -218,6 +218,14 @@ enum SystemDecorations { SystemDecorationsFull = 2, } +enum AvnAutomationProperty +{ + AutomationPeer_BoundingRectangle, + AutomationPeer_ClassName, + AutomationPeer_Name, + RangeValueProvider_Value, +} + struct AvnSize { double Width, Height; @@ -412,6 +420,51 @@ enum AvnPlatformResizeReason ResizeDpiChange, } +enum AvnAutomationControlType +{ + AutomationNone, + AutomationButton, + AutomationCalendar, + AutomationCheckBox, + AutomationComboBox, + AutomationComboBoxItem, + AutomationEdit, + AutomationHyperlink, + AutomationImage, + AutomationListItem, + AutomationList, + AutomationMenu, + AutomationMenuBar, + AutomationMenuItem, + AutomationProgressBar, + AutomationRadioButton, + AutomationScrollBar, + AutomationSlider, + AutomationSpinner, + AutomationStatusBar, + AutomationTab, + AutomationTabItem, + AutomationText, + AutomationToolBar, + AutomationToolTip, + AutomationTree, + AutomationTreeItem, + AutomationCustom, + AutomationGroup, + AutomationThumb, + AutomationDataGrid, + AutomationDataItem, + AutomationDocument, + AutomationSplitButton, + AutomationWindow, + AutomationPane, + AutomationHeader, + AutomationHeaderItem, + AutomationTable, + AutomationTitleBar, + AutomationSeparator, +} + [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] interface IAvaloniaNativeFactory : IUnknown { @@ -522,6 +575,7 @@ interface IAvnWindowBaseEvents : IUnknown AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, AvnInputModifiers modifiers, AvnDragDropEffects effects, IAvnClipboard* clipboard, [intptr]void* dataObjectHandle); + IAvnAutomationPeer* GetAutomationPeer(); } [uuid(1ae178ee-1fcc-447f-b6dd-b7bb727f934c)] @@ -765,3 +819,79 @@ interface IAvnApplicationCommands : IUnknown HRESULT ShowAll(); HRESULT HideOthers(); } + +[uuid(b87016f3-7eec-41de-b385-07844c268dc4)] +interface IAvnAutomationPeer : IUnknown +{ + IAvnAutomationNode* GetNode(); + void SetNode(IAvnAutomationNode* node); + + IAvnString* GetAcceleratorKey(); + IAvnString* GetAccessKey(); + AvnAutomationControlType GetAutomationControlType(); + IAvnString* GetAutomationId(); + AvnRect GetBoundingRectangle(); + IAvnAutomationPeerArray* GetChildren(); + IAvnString* GetClassName(); + IAvnAutomationPeer* GetLabeledBy(); + IAvnString* GetName(); + IAvnAutomationPeer* GetParent(); + bool HasKeyboardFocus(); + bool IsContentElement(); + bool IsControlElement(); + bool IsEnabled(); + bool IsKeyboardFocusable(); + void SetFocus(); + bool ShowContextMenu(); + + IAvnAutomationPeer* GetRootPeer(); + + bool IsRootProvider(); + IAvnWindowBase* RootProvider_GetWindow(); + IAvnAutomationPeer* RootProvider_GetFocus(); + IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point); + + bool IsExpandCollapseProvider(); + bool ExpandCollapseProvider_GetIsExpanded(); + bool ExpandCollapseProvider_GetShowsMenu(); + void ExpandCollapseProvider_Expand(); + void ExpandCollapseProvider_Collapse(); + + bool IsInvokeProvider(); + void InvokeProvider_Invoke(); + + bool IsRangeValueProvider(); + double RangeValueProvider_GetValue(); + double RangeValueProvider_GetMinimum(); + double RangeValueProvider_GetMaximum(); + double RangeValueProvider_GetSmallChange(); + double RangeValueProvider_GetLargeChange(); + void RangeValueProvider_SetValue(double value); + + bool IsSelectionItemProvider(); + bool SelectionItemProvider_IsSelected(); + + bool IsToggleProvider(); + int ToggleProvider_GetToggleState(); + void ToggleProvider_Toggle(); + + bool IsValueProvider(); + IAvnString* ValueProvider_GetValue(); + void ValueProvider_SetValue(char* value); +} + +[uuid(b00af5da-78af-4b33-bfff-4ce13a6239a9)] +interface IAvnAutomationPeerArray : IUnknown +{ + uint GetCount(); + HRESULT Get(uint index, IAvnAutomationPeer**ppv); +} + +[uuid(004dc40b-e435-49dc-bac5-6272ee35382a)] +interface IAvnAutomationNode : IUnknown +{ + void Dispose(); + void ChildrenChanged(); + void PropertyChanged(AvnAutomationProperty property); + void FocusChanged(); +} diff --git a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj index 2b32926008..76924d060f 100644 --- a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj +++ b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj @@ -8,6 +8,7 @@ - - + + + diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index 33773ed8e2..b3469c212b 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -89,7 +89,9 @@ namespace Avalonia.OpenGL.Controls gl.BindTexture(GL_TEXTURE_2D, 0); gl.BindFramebuffer(GL_FRAMEBUFFER, 0); gl.DeleteFramebuffers(1, new[] { _fb }); + _fb = 0; gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); + _depthBuffer = 0; _attachment?.Dispose(); _attachment = null; _bitmap?.Dispose(); diff --git a/src/Avalonia.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs index cadd7cc1f2..6148e58440 100644 --- a/src/Avalonia.OpenGL/Egl/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -46,93 +46,114 @@ namespace Avalonia.OpenGL.Egl } // ReSharper disable UnassignedGetOnlyAutoProperty + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int EglGetError(); [GlEntryPoint("eglGetError")] public EglGetError GetError { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr EglGetDisplay(IntPtr nativeDisplay); [GlEntryPoint("eglGetDisplay")] public EglGetDisplay GetDisplay { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr EglGetPlatformDisplayEXT(int platform, IntPtr nativeDisplay, int[] attrs); [GlEntryPoint("eglGetPlatformDisplayEXT")] [GlOptionalEntryPoint] public EglGetPlatformDisplayEXT GetPlatformDisplayEXT { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool EglInitialize(IntPtr display, out int major, out int minor); [GlEntryPoint("eglInitialize")] - public EglInitialize Initialize { get; } - + public EglInitialize Initialize { get; } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr EglGetProcAddress(Utf8Buffer proc); [GlEntryPoint("eglGetProcAddress")] public EglGetProcAddress GetProcAddress { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool EglBindApi(int api); [GlEntryPoint("eglBindAPI")] public EglBindApi BindApi { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool EglChooseConfig(IntPtr display, int[] attribs, out IntPtr surfaceConfig, int numConfigs, out int choosenConfig); [GlEntryPoint("eglChooseConfig")] public EglChooseConfig ChooseConfig { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr EglCreateContext(IntPtr display, IntPtr config, IntPtr share, int[] attrs); [GlEntryPoint("eglCreateContext")] public EglCreateContext CreateContext { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool EglDestroyContext(IntPtr display, IntPtr context); [GlEntryPoint("eglDestroyContext")] public EglDestroyContext DestroyContext { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr EglCreatePBufferSurface(IntPtr display, IntPtr config, int[] attrs); [GlEntryPoint("eglCreatePbufferSurface")] public EglCreatePBufferSurface CreatePBufferSurface { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool EglMakeCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context); [GlEntryPoint("eglMakeCurrent")] public EglMakeCurrent MakeCurrent { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr EglGetCurrentContext(); [GlEntryPoint("eglGetCurrentContext")] public EglGetCurrentContext GetCurrentContext { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr EglGetCurrentDisplay(); [GlEntryPoint("eglGetCurrentDisplay")] public EglGetCurrentContext GetCurrentDisplay { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr EglGetCurrentSurface(int readDraw); [GlEntryPoint("eglGetCurrentSurface")] public EglGetCurrentSurface GetCurrentSurface { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void EglDisplaySurfaceVoidDelegate(IntPtr display, IntPtr surface); [GlEntryPoint("eglDestroySurface")] public EglDisplaySurfaceVoidDelegate DestroySurface { get; } [GlEntryPoint("eglSwapBuffers")] public EglDisplaySurfaceVoidDelegate SwapBuffers { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr EglCreateWindowSurface(IntPtr display, IntPtr config, IntPtr window, int[] attrs); [GlEntryPoint("eglCreateWindowSurface")] public EglCreateWindowSurface CreateWindowSurface { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool EglGetConfigAttrib(IntPtr display, IntPtr config, int attr, out int rv); [GlEntryPoint("eglGetConfigAttrib")] public EglGetConfigAttrib GetConfigAttrib { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool EglWaitGL(); [GlEntryPoint("eglWaitGL")] public EglWaitGL WaitGL { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool EglWaitClient(); [GlEntryPoint("eglWaitClient")] public EglWaitGL WaitClient { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool EglWaitNative(int engine); [GlEntryPoint("eglWaitNative")] public EglWaitNative WaitNative { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr EglQueryString(IntPtr display, int i); [GlEntryPoint("eglQueryString")] @@ -145,17 +166,20 @@ namespace Avalonia.OpenGL.Egl return null; return Marshal.PtrToStringAnsi(rv); } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr EglCreatePbufferFromClientBuffer(IntPtr display, int buftype, IntPtr buffer, IntPtr config, int[] attrib_list); [GlEntryPoint("eglCreatePbufferFromClientBuffer")] public EglCreatePbufferFromClientBuffer CreatePbufferFromClientBuffer { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool EglQueryDisplayAttribEXT(IntPtr display, int attr, out IntPtr res); [GlEntryPoint("eglQueryDisplayAttribEXT"), GlOptionalEntryPoint] public EglQueryDisplayAttribEXT QueryDisplayAttribExt { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool EglQueryDeviceAttribEXT(IntPtr display, int attr, out IntPtr res); [GlEntryPoint("eglQueryDeviceAttribEXT"), GlOptionalEntryPoint] diff --git a/src/Avalonia.OpenGL/GlBasicInfoInterface.cs b/src/Avalonia.OpenGL/GlBasicInfoInterface.cs index a3383ac5ae..aaba2ec09c 100644 --- a/src/Avalonia.OpenGL/GlBasicInfoInterface.cs +++ b/src/Avalonia.OpenGL/GlBasicInfoInterface.cs @@ -15,9 +15,12 @@ namespace Avalonia.OpenGL public GlBasicInfoInterface(Func nativeGetProcAddress) : base(nativeGetProcAddress, null) { } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlGetIntegerv(int name, out int rv); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr GlGetString(int v); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr GlGetStringi(int v, int v1); } diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index cae245732f..18bebe4cb5 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -60,32 +60,41 @@ namespace Avalonia.OpenGL public T GetProcAddress(string proc) => Marshal.GetDelegateForFunctionPointer(GetProcAddress(proc)); // ReSharper disable UnassignedGetOnlyAutoProperty + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int GlGetError(); [GlEntryPoint("glGetError")] public GlGetError GetError { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlClearStencil(int s); [GlEntryPoint("glClearStencil")] public GlClearStencil ClearStencil { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlClearColor(float r, float g, float b, float a); [GlEntryPoint("glClearColor")] public GlClearColor ClearColor { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlClear(int bits); [GlEntryPoint("glClear")] public GlClear Clear { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlViewport(int x, int y, int width, int height); [GlEntryPoint("glViewport")] public GlViewport Viewport { get; } [GlEntryPoint("glFlush")] - public Action Flush { get; } + public UnmanagedAction Flush { get; } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void UnmanagedAction(); [GlEntryPoint("glFinish")] - public Action Finish { get; } + public UnmanagedAction Finish { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate IntPtr GlGetString(int v); [GlEntryPoint("glGetString")] public GlGetString GetStringNative { get; } @@ -98,26 +107,32 @@ namespace Avalonia.OpenGL return null; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlGetIntegerv(int name, out int rv); [GlEntryPoint("glGetIntegerv")] public GlGetIntegerv GetIntegerv { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlGenFramebuffers(int count, int[] res); [GlEntryPoint("glGenFramebuffers")] public GlGenFramebuffers GenFramebuffers { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlDeleteFramebuffers(int count, int[] framebuffers); [GlEntryPoint("glDeleteFramebuffers")] public GlDeleteFramebuffers DeleteFramebuffers { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlBindFramebuffer(int target, int fb); [GlEntryPoint("glBindFramebuffer")] public GlBindFramebuffer BindFramebuffer { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int GlCheckFramebufferStatus(int target); [GlEntryPoint("glCheckFramebufferStatus")] public GlCheckFramebufferStatus CheckFramebufferStatus { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlBlitFramebuffer(int srcX0, int srcY0, int srcX1, @@ -130,69 +145,84 @@ namespace Avalonia.OpenGL int filter); [GlMinVersionEntryPoint("glBlitFramebuffer", 3, 0), GlOptionalEntryPoint] public GlBlitFramebuffer BlitFramebuffer { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlGenRenderbuffers(int count, int[] res); [GlEntryPoint("glGenRenderbuffers")] public GlGenRenderbuffers GenRenderbuffers { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlDeleteRenderbuffers(int count, int[] renderbuffers); [GlEntryPoint("glDeleteRenderbuffers")] public GlDeleteTextures DeleteRenderbuffers { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlBindRenderbuffer(int target, int fb); [GlEntryPoint("glBindRenderbuffer")] public GlBindRenderbuffer BindRenderbuffer { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlRenderbufferStorage(int target, int internalFormat, int width, int height); [GlEntryPoint("glRenderbufferStorage")] public GlRenderbufferStorage RenderbufferStorage { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlFramebufferRenderbuffer(int target, int attachment, int renderbufferTarget, int renderbuffer); [GlEntryPoint("glFramebufferRenderbuffer")] public GlFramebufferRenderbuffer FramebufferRenderbuffer { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlGenTextures(int count, int[] res); [GlEntryPoint("glGenTextures")] public GlGenTextures GenTextures { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlBindTexture(int target, int fb); [GlEntryPoint("glBindTexture")] public GlBindTexture BindTexture { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlActiveTexture(int texture); [GlEntryPoint("glActiveTexture")] public GlActiveTexture ActiveTexture { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlDeleteTextures(int count, int[] textures); [GlEntryPoint("glDeleteTextures")] public GlDeleteTextures DeleteTextures { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlTexImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, IntPtr data); [GlEntryPoint("glTexImage2D")] public GlTexImage2D TexImage2D { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlCopyTexSubImage2D(int target, int level, int xoffset, int yoffset, int x, int y, int width, int height); [GlEntryPoint("glCopyTexSubImage2D")] public GlCopyTexSubImage2D CopyTexSubImage2D { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlTexParameteri(int target, int name, int value); [GlEntryPoint("glTexParameteri")] public GlTexParameteri TexParameteri { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlFramebufferTexture2D(int target, int attachment, int texTarget, int texture, int level); [GlEntryPoint("glFramebufferTexture2D")] public GlFramebufferTexture2D FramebufferTexture2D { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int GlCreateShader(int shaderType); [GlEntryPoint("glCreateShader")] public GlCreateShader CreateShader { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlShaderSource(int shader, int count, IntPtr strings, IntPtr lengths); [GlEntryPoint("glShaderSource")] public GlShaderSource ShaderSource { get; } @@ -207,14 +237,17 @@ namespace Avalonia.OpenGL } } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlCompileShader(int shader); [GlEntryPoint("glCompileShader")] public GlCompileShader CompileShader { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlGetShaderiv(int shader, int name, int* parameters); [GlEntryPoint("glGetShaderiv")] public GlGetShaderiv GetShaderiv { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlGetShaderInfoLog(int shader, int maxLength, out int length, void*infoLog); [GlEntryPoint("glGetShaderInfoLog")] public GlGetShaderInfoLog GetShaderInfoLog { get; } @@ -237,23 +270,28 @@ namespace Avalonia.OpenGL GetShaderInfoLog(shader, logLength, out len, ptr); return Encoding.UTF8.GetString(logData,0, len); } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int GlCreateProgram(); [GlEntryPoint("glCreateProgram")] public GlCreateProgram CreateProgram { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlAttachShader(int program, int shader); [GlEntryPoint("glAttachShader")] public GlAttachShader AttachShader { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlLinkProgram(int program); [GlEntryPoint("glLinkProgram")] public GlLinkProgram LinkProgram { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlGetProgramiv(int program, int name, int* parameters); [GlEntryPoint("glGetProgramiv")] public GlGetProgramiv GetProgramiv { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlGetProgramInfoLog(int program, int maxLength, out int len, void* infoLog); [GlEntryPoint("glGetProgramInfoLog")] public GlGetProgramInfoLog GetProgramInfoLog { get; } @@ -274,6 +312,7 @@ namespace Avalonia.OpenGL return Encoding.UTF8.GetString(logData,0, len); } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlBindAttribLocation(int program, int index, IntPtr name); [GlEntryPoint("glBindAttribLocation")] public GlBindAttribLocation BindAttribLocation { get; } @@ -283,7 +322,8 @@ namespace Avalonia.OpenGL using (var b = new Utf8Buffer(name)) BindAttribLocation(program, index, b.DangerousGetHandle()); } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlGenBuffers(int len, int[] rv); [GlEntryPoint("glGenBuffers")] public GlGenBuffers GenBuffers { get; } @@ -294,15 +334,18 @@ namespace Avalonia.OpenGL GenBuffers(1, rv); return rv[0]; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlBindBuffer(int target, int buffer); [GlEntryPoint("glBindBuffer")] public GlBindBuffer BindBuffer { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlBufferData(int target, IntPtr size, IntPtr data, int usage); [GlEntryPoint("glBufferData")] public GlBufferData BufferData { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int GlGetAttribLocation(int program, IntPtr name); [GlEntryPoint("glGetAttribLocation")] public GlGetAttribLocation GetAttribLocation { get; } @@ -313,27 +356,33 @@ namespace Avalonia.OpenGL return GetAttribLocation(program, b.DangerousGetHandle()); } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlVertexAttribPointer(int index, int size, int type, int normalized, int stride, IntPtr pointer); [GlEntryPoint("glVertexAttribPointer")] public GlVertexAttribPointer VertexAttribPointer { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlEnableVertexAttribArray(int index); [GlEntryPoint("glEnableVertexAttribArray")] public GlEnableVertexAttribArray EnableVertexAttribArray { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlUseProgram(int program); [GlEntryPoint("glUseProgram")] public GlUseProgram UseProgram { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlDrawArrays(int mode, int first, IntPtr count); [GlEntryPoint("glDrawArrays")] public GlDrawArrays DrawArrays { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlDrawElements(int mode, int count, int type, IntPtr indices); [GlEntryPoint("glDrawElements")] public GlDrawElements DrawElements { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int GlGetUniformLocation(int program, IntPtr name); [GlEntryPoint("glGetUniformLocation")] public GlGetUniformLocation GetUniformLocation { get; } @@ -343,31 +392,42 @@ namespace Avalonia.OpenGL using (var b = new Utf8Buffer(name)) return GetUniformLocation(program, b.DangerousGetHandle()); } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlUniform1f(int location, float falue); [GlEntryPoint("glUniform1f")] public GlUniform1f Uniform1f { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlUniformMatrix4fv(int location, int count, bool transpose, void* value); [GlEntryPoint("glUniformMatrix4fv")] public GlUniformMatrix4fv UniformMatrix4fv { get; } - + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlEnable(int what); [GlEntryPoint("glEnable")] public GlEnable Enable { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlDeleteBuffers(int count, int[] buffers); [GlEntryPoint("glDeleteBuffers")] public GlDeleteBuffers DeleteBuffers { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlDeleteProgram(int program); [GlEntryPoint("glDeleteProgram")] public GlDeleteProgram DeleteProgram { get; } + [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void GlDeleteShader(int shader); [GlEntryPoint("glDeleteShader")] public GlDeleteShader DeleteShader { get; } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void GLGetRenderbufferParameteriv(int target, int name, int[] value); + [GlEntryPoint("glGetRenderbufferParameteriv")] + public GLGetRenderbufferParameteriv GetRenderbufferParameteriv { get; } // ReSharper restore UnassignedGetOnlyAutoProperty } } diff --git a/src/Avalonia.PlatformSupport/AppBuilder.cs b/src/Avalonia.PlatformSupport/AppBuilder.cs new file mode 100644 index 0000000000..136f1f39b3 --- /dev/null +++ b/src/Avalonia.PlatformSupport/AppBuilder.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.PlatformSupport; + +namespace Avalonia +{ + /// + /// Initializes platform-specific services for an . + /// + public sealed class AppBuilder : AppBuilderBase + { + /// + /// Initializes a new instance of the class. + /// + public AppBuilder() + : base(new StandardRuntimePlatform(), + builder => StandardRuntimePlatformServices.Register(builder.ApplicationType?.Assembly)) + { + } + } +} diff --git a/src/Avalonia.PlatformSupport/AssetLoader.cs b/src/Avalonia.PlatformSupport/AssetLoader.cs index 7220694d7b..0e33c3d4c7 100644 --- a/src/Avalonia.PlatformSupport/AssetLoader.cs +++ b/src/Avalonia.PlatformSupport/AssetLoader.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Reflection; using Avalonia.Platform; +using Avalonia.PlatformSupport.Internal; using Avalonia.Utilities; namespace Avalonia.PlatformSupport @@ -13,12 +14,16 @@ namespace Avalonia.PlatformSupport /// public class AssetLoader : IAssetLoader { - private const string AvaloniaResourceName = "!AvaloniaResources"; - private static readonly Dictionary AssemblyNameCache - = new Dictionary(); + private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver(); private AssemblyDescriptor? _defaultResmAssembly; + /// + /// Introduced for tests. + /// + internal static void SetAssemblyDescriptorResolver(IAssemblyDescriptorResolver resolver) => + s_assemblyDescriptorResolver = resolver; + /// /// Initializes a new instance of the class. /// @@ -109,17 +114,18 @@ namespace Avalonia.PlatformSupport /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset public IEnumerable GetAssets(Uri uri, Uri? baseUri) { - if (uri.IsAbsoluteUri && uri.Scheme == "resm") + if (uri.IsAbsoluteResm()) { var assembly = GetAssembly(uri); - return assembly?.Resources?.Where(x => x.Key.Contains(uri.AbsolutePath)) + return assembly?.Resources? + .Where(x => x.Key.IndexOf(uri.GetUnescapeAbsolutePath(), StringComparison.Ordinal) >= 0) .Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? Enumerable.Empty(); } - uri = EnsureAbsolute(uri, baseUri); - if (uri.Scheme == "avares") + uri = uri.EnsureAbsolute(baseUri); + if (uri.IsAvares()) { var (asm, path) = GetResAsmAndPath(uri); if (asm == null) @@ -129,33 +135,23 @@ namespace Avalonia.PlatformSupport "don't know where to look up for the resource, try specifying assembly explicitly."); } - if (asm?.AvaloniaResources == null) + if (asm.AvaloniaResources == null) return Enumerable.Empty(); - path = path.TrimEnd('/') + '/'; - return asm.AvaloniaResources.Where(r => r.Key.StartsWith(path)) + + if (path[path.Length - 1] != '/') + path += '/'; + + return asm.AvaloniaResources + .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) .Select(x => new Uri($"avares://{asm.Name}{x.Key}")); } return Enumerable.Empty(); } - - private Uri EnsureAbsolute(Uri uri, Uri? baseUri) - { - if (uri.IsAbsoluteUri) - return uri; - if(baseUri == null) - throw new ArgumentException($"Relative uri {uri} without base url"); - if (!baseUri.IsAbsoluteUri) - throw new ArgumentException($"Base uri {baseUri} is relative"); - if (baseUri.Scheme == "resm") - throw new ArgumentException( - $"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm"); - return new Uri(baseUri, uri); - } private IAssetDescriptor? GetAsset(Uri uri, Uri? baseUri) { - if (uri.IsAbsoluteUri && uri.Scheme == "resm") + if (uri.IsAbsoluteResm()) { var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly; @@ -172,9 +168,9 @@ namespace Avalonia.PlatformSupport return rv; } - uri = EnsureAbsolute(uri, baseUri); + uri = uri.EnsureAbsolute(baseUri); - if (uri.Scheme == "avares") + if (uri.IsAvares()) { var (asm, path) = GetResAsmAndPath(uri); if (asm.AvaloniaResources == null) @@ -186,209 +182,32 @@ namespace Avalonia.PlatformSupport throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri)); } - private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) + private (IAssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) { - var asm = GetAssembly(uri.Authority); - return (asm, uri.AbsolutePath); + var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority); + return (asm, uri.GetUnescapeAbsolutePath()); } - private AssemblyDescriptor? GetAssembly(Uri? uri) + private IAssemblyDescriptor? GetAssembly(Uri? uri) { if (uri != null) { if (!uri.IsAbsoluteUri) return null; - if (uri.Scheme == "avares") + if (uri.IsAvares()) return GetResAsmAndPath(uri).asm; - if (uri.Scheme == "resm") + if (uri.IsResm()) { - var qs = ParseQueryString(uri); - - if (qs.TryGetValue("assembly", out var assemblyName)) - { - return GetAssembly(assemblyName); - } + var assemblyName = uri.GetAssemblyNameFromQuery(); + if (assemblyName.Length > 0) + return s_assemblyDescriptorResolver.GetAssembly(assemblyName); } } return null; } - private AssemblyDescriptor GetAssembly(string name) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - if (!AssemblyNameCache.TryGetValue(name, out var rv)) - { - var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - var match = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == name); - if (match != null) - { - AssemblyNameCache[name] = rv = new AssemblyDescriptor(match); - } - else - { - // iOS does not support loading assemblies dynamically! -#if NET6_0_OR_GREATER - if (OperatingSystem.IsIOS()) - { - throw new InvalidOperationException( - $"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); - } -#endif - name = Uri.UnescapeDataString(name); - AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name)); - } - } - - return rv; - } - - private Dictionary ParseQueryString(Uri uri) - { - return uri.Query.TrimStart('?') - .Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries) - .Select(p => p.Split('=')) - .ToDictionary(p => p[0], p => p[1]); - } - - private interface IAssetDescriptor - { - Stream GetStream(); - Assembly Assembly { get; } - } - - private class AssemblyResourceDescriptor : IAssetDescriptor - { - private readonly Assembly _asm; - private readonly string _name; - - public AssemblyResourceDescriptor(Assembly asm, string name) - { - _asm = asm; - _name = name; - } - - public Stream GetStream() - { - var s = _asm.GetManifestResourceStream(_name); - return s ?? throw new InvalidOperationException($"Could not find manifest resource stream '{_name}',"); - } - - public Assembly Assembly => _asm; - } - - private class AvaloniaResourceDescriptor : IAssetDescriptor - { - private readonly int _offset; - private readonly int _length; - public Assembly Assembly { get; } - - public AvaloniaResourceDescriptor(Assembly asm, int offset, int length) - { - _offset = offset; - _length = length; - Assembly = asm; - } - - public Stream GetStream() - { - var s = Assembly.GetManifestResourceStream(AvaloniaResourceName) ?? - throw new InvalidOperationException($"Could not find manifest resource stream '{AvaloniaResourceName}',"); - return new SlicedStream(s, _offset, _length); - } - } - - class SlicedStream : Stream - { - private readonly Stream _baseStream; - private readonly int _from; - - public SlicedStream(Stream baseStream, int from, int length) - { - Length = length; - _baseStream = baseStream; - _from = from; - _baseStream.Position = from; - } - public override void Flush() - { - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _baseStream.Read(buffer, offset, (int)Math.Min(count, Length - Position)); - } - - public override long Seek(long offset, SeekOrigin origin) - { - if (origin == SeekOrigin.Begin) - Position = offset; - if (origin == SeekOrigin.End) - Position = _from + Length + offset; - if (origin == SeekOrigin.Current) - Position = Position + offset; - return Position; - } - - public override void SetLength(long value) => throw new NotSupportedException(); - - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - - public override bool CanRead => true; - public override bool CanSeek => _baseStream.CanRead; - public override bool CanWrite => false; - public override long Length { get; } - public override long Position - { - get => _baseStream.Position - _from; - set => _baseStream.Position = value + _from; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - _baseStream.Dispose(); - } - - public override void Close() => _baseStream.Close(); - } - - private class AssemblyDescriptor - { - public AssemblyDescriptor(Assembly assembly) - { - Assembly = assembly; - - if (assembly != null) - { - Resources = assembly.GetManifestResourceNames() - .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); - Name = assembly.GetName().Name; - using (var resources = assembly.GetManifestResourceStream(AvaloniaResourceName)) - { - if (resources != null) - { - Resources.Remove(AvaloniaResourceName); - - var indexLength = new BinaryReader(resources).ReadInt32(); - var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); - var baseOffset = indexLength + 4; - AvaloniaResources = index.ToDictionary(r => "/" + r.Path!.TrimStart('/'), r => (IAssetDescriptor) - new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); - } - } - } - } - - public Assembly Assembly { get; } - public Dictionary? Resources { get; } - public Dictionary? AvaloniaResources { get; } - public string? Name { get; } - } - public static void RegisterResUriParsers() { if (!UriParser.IsKnownScheme("avares")) diff --git a/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj b/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj index be73d87e2c..5336f1e630 100644 --- a/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj +++ b/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj @@ -6,6 +6,7 @@ + @@ -15,4 +16,9 @@ + + + + + diff --git a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs new file mode 100644 index 0000000000..64ffec8482 --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Avalonia.Utilities; + +namespace Avalonia.PlatformSupport.Internal; + +internal interface IAssemblyDescriptor +{ + Assembly Assembly { get; } + Dictionary? Resources { get; } + Dictionary? AvaloniaResources { get; } + string? Name { get; } +} + +internal class AssemblyDescriptor : IAssemblyDescriptor +{ + public AssemblyDescriptor(Assembly assembly) + { + Assembly = assembly; + + if (assembly != null) + { + Resources = assembly.GetManifestResourceNames() + .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); + Name = assembly.GetName().Name; + using (var resources = assembly.GetManifestResourceStream(Constants.AvaloniaResourceName)) + { + if (resources != null) + { + Resources.Remove(Constants.AvaloniaResourceName); + + var indexLength = new BinaryReader(resources).ReadInt32(); + var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); + var baseOffset = indexLength + 4; + AvaloniaResources = index.ToDictionary(r => GetPathRooted(r), r => (IAssetDescriptor) + new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); + } + } + } + } + + public Assembly Assembly { get; } + public Dictionary? Resources { get; } + public Dictionary? AvaloniaResources { get; } + public string? Name { get; } + private static string GetPathRooted(AvaloniaResourcesIndexEntry r) => + r.Path![0] == '/' ? r.Path : '/' + r.Path; +} diff --git a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs new file mode 100644 index 0000000000..28ae35d57d --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Avalonia.PlatformSupport.Internal; + +internal interface IAssemblyDescriptorResolver +{ + IAssemblyDescriptor GetAssembly(string name); +} + +internal class AssemblyDescriptorResolver: IAssemblyDescriptorResolver +{ + private readonly Dictionary _assemblyNameCache = new(); + + public IAssemblyDescriptor GetAssembly(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (!_assemblyNameCache.TryGetValue(name, out var rv)) + { + var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + var match = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == name); + if (match != null) + { + _assemblyNameCache[name] = rv = new AssemblyDescriptor(match); + } + else + { + // iOS does not support loading assemblies dynamically! +#if NET6_0_OR_GREATER + if (OperatingSystem.IsIOS()) + { + throw new InvalidOperationException( + $"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); + } +#endif + name = Uri.UnescapeDataString(name); + _assemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name)); + } + } + + return rv; + } +} diff --git a/src/Avalonia.PlatformSupport/Internal/AssetDescriptor.cs b/src/Avalonia.PlatformSupport/Internal/AssetDescriptor.cs new file mode 100644 index 0000000000..baae1f99e7 --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/AssetDescriptor.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Avalonia.PlatformSupport.Internal; + +internal interface IAssetDescriptor +{ + Stream GetStream(); + Assembly Assembly { get; } +} + +internal class AssemblyResourceDescriptor : IAssetDescriptor +{ + private readonly Assembly _asm; + private readonly string _name; + + public AssemblyResourceDescriptor(Assembly asm, string name) + { + _asm = asm; + _name = name; + } + + public Stream GetStream() + { + var s = _asm.GetManifestResourceStream(_name); + return s ?? throw new InvalidOperationException($"Could not find manifest resource stream '{_name}',"); + } + + public Assembly Assembly => _asm; +} + +internal class AvaloniaResourceDescriptor : IAssetDescriptor +{ + private readonly int _offset; + private readonly int _length; + public Assembly Assembly { get; } + + public AvaloniaResourceDescriptor(Assembly asm, int offset, int length) + { + _offset = offset; + _length = length; + Assembly = asm; + } + + public Stream GetStream() + { + var s = Assembly.GetManifestResourceStream(Constants.AvaloniaResourceName) ?? + throw new InvalidOperationException($"Could not find manifest resource stream '{Constants.AvaloniaResourceName}',"); + return new SlicedStream(s, _offset, _length); + } +} diff --git a/src/Avalonia.PlatformSupport/Internal/Constants.cs b/src/Avalonia.PlatformSupport/Internal/Constants.cs new file mode 100644 index 0000000000..c8a0f7b1ce --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/Constants.cs @@ -0,0 +1,6 @@ +namespace Avalonia.PlatformSupport.Internal; + +internal static class Constants +{ + public static string AvaloniaResourceName => "!AvaloniaResources"; +} diff --git a/src/Avalonia.PlatformSupport/Internal/SlicedStream.cs b/src/Avalonia.PlatformSupport/Internal/SlicedStream.cs new file mode 100644 index 0000000000..e310db964a --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/SlicedStream.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; + +namespace Avalonia.PlatformSupport.Internal; + +internal class SlicedStream : Stream +{ + private readonly Stream _baseStream; + private readonly int _from; + + public SlicedStream(Stream baseStream, int from, int length) + { + Length = length; + _baseStream = baseStream; + _from = from; + _baseStream.Position = from; + } + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _baseStream.Read(buffer, offset, (int)Math.Min(count, Length - Position)); + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + Position = offset; + if (origin == SeekOrigin.End) + Position = _from + Length + offset; + if (origin == SeekOrigin.Current) + Position = Position + offset; + return Position; + } + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override bool CanRead => true; + public override bool CanSeek => _baseStream.CanRead; + public override bool CanWrite => false; + public override long Length { get; } + public override long Position + { + get => _baseStream.Position - _from; + set => _baseStream.Position = value + _from; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + _baseStream.Dispose(); + } + + public override void Close() => _baseStream.Close(); +} diff --git a/src/Avalonia.PlatformSupport/StandardRuntimePlatform.cs b/src/Avalonia.PlatformSupport/StandardRuntimePlatform.cs index 768966ba2d..4eeb9232cf 100644 --- a/src/Avalonia.PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Avalonia.PlatformSupport/StandardRuntimePlatform.cs @@ -201,7 +201,7 @@ namespace Avalonia.PlatformSupport #if NETCOREAPP IsCoreClr = true, #elif NETFRAMEWORK - IsDotNetFramework = false, + IsDotNetFramework = true, #endif IsDesktop = os == OperatingSystemType.Linux || os == OperatingSystemType.OSX || os == OperatingSystemType.WinNT, IsMono = os == OperatingSystemType.Android || os == OperatingSystemType.iOS || os == OperatingSystemType.Browser, diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index 4987349162..21cdef2634 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -48,6 +48,7 @@ namespace Avalonia.ReactiveUI protected override void OnDataContextChanged(EventArgs e) { + base.OnDataContextChanged(e); ViewModel = DataContext as TViewModel; } diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index a475cf5eac..9269dc70f8 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -62,7 +62,7 @@ namespace Avalonia.ReactiveUI /// for the property. /// public static readonly StyledProperty ViewContractProperty = - AvaloniaProperty.Register(nameof(ViewContract)); + AvaloniaProperty.Register(nameof(ViewContract)); /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index c4dd79f468..d26e90b2da 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -10,6 +10,7 @@ namespace Avalonia.ReactiveUI /// /// A ContentControl that animates the transition when its content is changed. /// + [Obsolete("Use TransitioningContentControl in Avalonia.Controls namespace")] public class TransitioningContentControl : ContentControl, IStyleable { /// diff --git a/src/Avalonia.Remote.Protocol/ApiCompatBaseline.txt b/src/Avalonia.Remote.Protocol/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Avalonia.Remote.Protocol/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj index 5a8301a2e9..d746c6db7e 100644 --- a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj +++ b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj @@ -7,7 +7,7 @@ Avalonia.Remote.Protocol - + \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj new file mode 100644 index 0000000000..97e58f8a64 --- /dev/null +++ b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + enable + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/src/Avalonia.SourceGenerator/IsExternalInit.cs b/src/Avalonia.SourceGenerator/IsExternalInit.cs new file mode 100644 index 0000000000..c6ddf762ad --- /dev/null +++ b/src/Avalonia.SourceGenerator/IsExternalInit.cs @@ -0,0 +1,14 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal static class IsExternalInit + { + } +} diff --git a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs new file mode 100644 index 0000000000..4fc9397e7a --- /dev/null +++ b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Avalonia.SourceGenerator +{ + [Generator(LanguageNames.CSharp)] + public class SubtypesFactoryGenerator : IIncrementalGenerator + { + private record struct MethodTarget(IMethodSymbol Method, string MethodDecl, ITypeSymbol BaseType, string Namespace); + private static readonly string s_attributeName = typeof(SubtypesFactoryAttribute).FullName; + + private static bool IsSubtypeOf(ITypeSymbol type, ITypeSymbol baseType) + { + return type.BaseType is not null && (SymbolEqualityComparer.Default.Equals(type.BaseType, baseType) || IsSubtypeOf(type.BaseType, baseType)); + } + + private static void GenerateSubTypes(SourceProductionContext context, MethodTarget methodTarget, ImmutableArray types) + { + var (method, methodDecl, baseType, @namespace) = methodTarget; + var candidateTypes = types.Where(i => IsSubtypeOf(i, baseType)).Where(i => $"{i.ContainingNamespace}.".StartsWith($"{@namespace}.")).ToArray(); + var type = method.ContainingType; + var isGeneric = type.TypeParameters.Length > 0; + var isClass = type.TypeKind == TypeKind.Class; + + var typeDecl = $"partial {(isClass ? "class" : "struct")} {type.Name}{(isGeneric ? $"<{string.Join(", ", type.TypeParameters)}>" : "")}"; + var source = $@"using System; +using System.Collections.Generic; + +namespace {method.ContainingNamespace} +{{ + {typeDecl} + {{ + {methodDecl} + {{ + var hasMatch = false; + (hasMatch, {method.Parameters[1].Name}) = {method.Parameters[0].Name} switch + {{ +{string.Join("\n", candidateTypes.Select(i => $" \"{i.Name}\" => (true, ({method.Parameters[1].Type})new {i}()),"))} + _ => (false, default({method.Parameters[1].Type})) + }}; + + return hasMatch; + }} + }} +}}"; + + context.AddSource($"{type}.{method.MetadataName}.gen.cs", source); + } + + private static MethodTarget? PopulateMethodTargets(GeneratorSyntaxContext context, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + if (context.Node is MethodDeclarationSyntax method) + { + var attributes = method.AttributeLists.SelectMany(i => i.Attributes); + var semanticModel = context.SemanticModel; + foreach (var attribute in attributes) + { + var attributeTypeInfo = semanticModel.GetTypeInfo(attribute); + if (attributeTypeInfo.Type is null || + attributeTypeInfo.Type.ToString() != s_attributeName || + attribute.ArgumentList is null) + { + continue; + } + + var arguments = attribute.ArgumentList.Arguments; + if (arguments.Count != 2) + { + continue; + } + + if (arguments[0].Expression is not TypeOfExpressionSyntax typeOfExpr || + arguments[1].Expression is not LiteralExpressionSyntax and not IdentifierNameSyntax) + { + continue; + } + + var type = semanticModel.GetTypeInfo(typeOfExpr.Type); + var ns = semanticModel.GetConstantValue(arguments[1].Expression); + var methodDeclInfo = semanticModel.GetDeclaredSymbol(method); + + if (type.Type is not ITypeSymbol baseType || + ns.HasValue is false || + ns.Value is not string nsValue || + methodDeclInfo is not IMethodSymbol methodSymbol || + methodSymbol.Parameters.Length != 2 || + methodSymbol.Parameters[1].RefKind != RefKind.Out) + { + continue; + } + + var parameters = new SeparatedSyntaxList().AddRange(method.ParameterList.Parameters.Select(i => i.WithAttributeLists(new SyntaxList()))); + var methodDecl = method + .WithAttributeLists(new SyntaxList()) + .WithParameterList(method.ParameterList.WithParameters(parameters)) + .WithBody(null) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) + .WithoutTrivia().ToString(); + + return new MethodTarget(methodSymbol, methodDecl, baseType, nsValue); + } + } + + return null; + } + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var typesProvider = context.SyntaxProvider.CreateSyntaxProvider( + static (syntaxNode, token) => + { + token.ThrowIfCancellationRequested(); + return syntaxNode is ClassDeclarationSyntax or StructDeclarationSyntax; + }, + static (syntaxContext, token) => + { + token.ThrowIfCancellationRequested(); + return syntaxContext.Node is ClassDeclarationSyntax or StructDeclarationSyntax && + syntaxContext.SemanticModel.GetDeclaredSymbol(syntaxContext.Node) is ITypeSymbol typeSymbol + ? typeSymbol : null; + }) + .SelectMany((type, token) => + { + token.ThrowIfCancellationRequested(); + return type is null ? Array.Empty() : new ITypeSymbol[] { type }; + }); + + var methodsProvider = context.SyntaxProvider.CreateSyntaxProvider( + static (syntaxNode, token) => + { + token.ThrowIfCancellationRequested(); + return syntaxNode is MethodDeclarationSyntax { AttributeLists.Count: > 0 }; + }, PopulateMethodTargets) + .SelectMany((method, token) => + { + token.ThrowIfCancellationRequested(); + return method is null ? Array.Empty() : new MethodTarget[] { method.Value }; + }); + + var generateContext = methodsProvider.Combine(typesProvider.Collect()); + + context.RegisterSourceOutput(generateContext, static (sourceContext, source) => + { + sourceContext.CancellationToken.ThrowIfCancellationRequested(); + GenerateSubTypes(sourceContext, source.Left, source.Right); + }); + } + } +} diff --git a/src/Avalonia.Styling/ApiCompatBaseline.txt b/src/Avalonia.Styling/ApiCompatBaseline.txt deleted file mode 100644 index 0eedc3e360..0000000000 --- a/src/Avalonia.Styling/ApiCompatBaseline.txt +++ /dev/null @@ -1,4 +0,0 @@ -Compat issues with assembly Avalonia.Styling: -InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Styling.IStyleInstance.IsActive' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Styling.IStyleInstance.IsActive.get()' is present in the implementation but not in the contract. -Total Issues: 2 diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj deleted file mode 100644 index 139ba1bd2e..0000000000 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net6.0;netstandard2.0 - Avalonia.Styling - Avalonia - - - - - - - - diff --git a/src/Avalonia.Styling/Controls/ChildNameScope.cs b/src/Avalonia.Styling/Controls/ChildNameScope.cs deleted file mode 100644 index 58114a57fd..0000000000 --- a/src/Avalonia.Styling/Controls/ChildNameScope.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Threading.Tasks; -using Avalonia.Utilities; - -namespace Avalonia.Controls -{ - public class ChildNameScope : INameScope - { - private readonly INameScope _parentScope; - private readonly NameScope _inner = new NameScope(); - - public ChildNameScope(INameScope parentScope) - { - _parentScope = parentScope; - } - - public void Register(string name, object element) => _inner.Register(name, element); - - public SynchronousCompletionAsyncResult FindAsync(string name) - { - var found = Find(name); - if (found != null) - return new SynchronousCompletionAsyncResult(found); - // Not found and both current and parent scope are in completed state - if(IsCompleted) - return new SynchronousCompletionAsyncResult(null); - return DoFindAsync(name); - } - - public SynchronousCompletionAsyncResult DoFindAsync(string name) - { - var src = new SynchronousCompletionAsyncResultSource(); - - void ParentSearch() - { - var parentSearch = _parentScope.FindAsync(name); - if (parentSearch.IsCompleted) - src.SetResult(parentSearch.GetResult()); - else - parentSearch.OnCompleted(() => src.SetResult(parentSearch.GetResult())); - } - if (!_inner.IsCompleted) - { - // Guaranteed to be incomplete at this point - var innerSearch = _inner.FindAsync(name); - innerSearch.OnCompleted(() => - { - var value = innerSearch.GetResult(); - if (value != null) - src.SetResult(value); - else ParentSearch(); - }); - } - else - ParentSearch(); - - return src.AsyncResult; - } - - public object? Find(string name) - { - var found = _inner.Find(name); - if (found != null) - return found; - if (_inner.IsCompleted) - return _parentScope.Find(name); - return null; - } - - public void Complete() => _inner.Complete(); - - public bool IsCompleted => _inner.IsCompleted && _parentScope.IsCompleted; - } -} diff --git a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs deleted file mode 100644 index 0060767565..0000000000 --- a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; - -#nullable enable - -namespace Avalonia.Controls.Metadata -{ - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public sealed class PseudoClassesAttribute : Attribute - { - public PseudoClassesAttribute(params string[] pseudoClasses) - { - PseudoClasses = pseudoClasses; - } - - public IReadOnlyList PseudoClasses { get; } - } -} diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs deleted file mode 100644 index 77f98f85c4..0000000000 --- a/src/Avalonia.Styling/Controls/NameScope.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Avalonia.LogicalTree; -using Avalonia.Utilities; - -namespace Avalonia.Controls -{ - /// - /// Implements a name scope. - /// - public class NameScope : INameScope - { - /// - /// Defines the NameScope attached property. - /// - public static readonly AttachedProperty NameScopeProperty = - AvaloniaProperty.RegisterAttached("NameScope"); - - /// - public bool IsCompleted { get; private set; } - - private readonly Dictionary _inner = new Dictionary(); - - private readonly Dictionary> _pendingSearches = - new Dictionary>(); - - /// - /// Gets the value of the attached on a styled element. - /// - /// The styled element. - /// The value of the NameScope attached property. - public static INameScope GetNameScope(StyledElement styled) - { - _ = styled ?? throw new ArgumentNullException(nameof(styled)); - - return styled.GetValue(NameScopeProperty); - } - - /// - /// Sets the value of the attached on a styled element. - /// - /// The styled element. - /// The value to set. - public static void SetNameScope(StyledElement styled, INameScope value) - { - _ = styled ?? throw new ArgumentNullException(nameof(styled)); - - styled.SetValue(NameScopeProperty, value); - } - - /// - public void Register(string name, object element) - { - if (IsCompleted) - throw new InvalidOperationException("NameScope is completed, no further registrations are allowed"); - - _ = name ?? throw new ArgumentNullException(nameof(name)); - _ = element ?? throw new ArgumentNullException(nameof(element)); - - if (_inner.TryGetValue(name, out var existing)) - { - if (existing != element) - { - throw new ArgumentException($"Control with the name '{name}' already registered."); - } - } - else - { - _inner.Add(name, element); - if (_pendingSearches.TryGetValue(name, out var tcs)) - { - _pendingSearches.Remove(name); - tcs.SetResult(element); - } - } - } - - public SynchronousCompletionAsyncResult FindAsync(string name) - { - var found = Find(name); - if (found != null) - return new SynchronousCompletionAsyncResult(found); - if (IsCompleted) - return new SynchronousCompletionAsyncResult(null); - if (!_pendingSearches.TryGetValue(name, out var tcs)) - // We are intentionally running continuations synchronously here - _pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource(); - - return tcs.AsyncResult; - } - - /// - public object? Find(string name) - { - _ = name ?? throw new ArgumentNullException(nameof(name)); - - _inner.TryGetValue(name, out var result); - return result; - } - - public void Complete() - { - IsCompleted = true; - foreach (var kp in _pendingSearches) - kp.Value.TrySetResult(null); - _pendingSearches.Clear(); - } - - - } -} diff --git a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs deleted file mode 100644 index de41f5292c..0000000000 --- a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -#nullable enable -using System; - -namespace Avalonia.LogicalTree -{ - /// - /// Event args for event. - /// - public class ChildIndexChangedEventArgs : EventArgs - { - public ChildIndexChangedEventArgs() - { - } - - public ChildIndexChangedEventArgs(ILogical child) - { - Child = child; - } - - /// - /// Logical child which index was changed. - /// If null, all children should be reset. - /// - public ILogical? Child { get; } - } -} diff --git a/src/Avalonia.Styling/Properties/AssemblyInfo.cs b/src/Avalonia.Styling/Properties/AssemblyInfo.cs deleted file mode 100644 index ab034740ed..0000000000 --- a/src/Avalonia.Styling/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Runtime.CompilerServices; -using Avalonia.Metadata; - -[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, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - diff --git a/src/Avalonia.Themes.Default/ApiCompatBaseline.txt b/src/Avalonia.Themes.Default/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Avalonia.Themes.Default/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index ef200b5532..40ed4a0f87 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -3,16 +3,10 @@ net6.0;netstandard2.0 - - - - - - - + diff --git a/src/Avalonia.Themes.Default/Controls/Button.xaml b/src/Avalonia.Themes.Default/Controls/Button.xaml index da36abe7ec..a2971c3ff6 100644 --- a/src/Avalonia.Themes.Default/Controls/Button.xaml +++ b/src/Avalonia.Themes.Default/Controls/Button.xaml @@ -18,7 +18,7 @@ Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" RecognizesAccessKey="True" - TextBlock.Foreground="{TemplateBinding Foreground}" + TextElement.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> diff --git a/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml index cf469eeac5..be6642467f 100644 --- a/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml @@ -4,7 +4,7 @@ - + @@ -151,7 +151,7 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" - TextBlock.Foreground="{TemplateBinding Foreground}" + TextElement.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/> @@ -196,7 +196,7 @@ diff --git a/src/Avalonia.Themes.Default/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Default/Controls/DropDownButton.xaml new file mode 100644 index 0000000000..662c6379fb --- /dev/null +++ b/src/Avalonia.Themes.Default/Controls/DropDownButton.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Controls/Expander.xaml b/src/Avalonia.Themes.Default/Controls/Expander.xaml index e72ddea163..2f18faf84a 100644 --- a/src/Avalonia.Themes.Default/Controls/Expander.xaml +++ b/src/Avalonia.Themes.Default/Controls/Expander.xaml @@ -1,5 +1,36 @@ - + + + + + + + + + + + + Expanded content + + + + + Expanded content + + + + + Expanded content + + + + + Expanded content + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Controls/TimePicker.xaml b/src/Avalonia.Themes.Default/Controls/TimePicker.xaml index a58fd62a99..0a5147e335 100644 --- a/src/Avalonia.Themes.Default/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Default/Controls/TimePicker.xaml @@ -48,7 +48,7 @@ ContentTemplate="{TemplateBinding HeaderTemplate}" Margin="{DynamicResource TimePickerTopHeaderMargin}" MaxWidth="{DynamicResource TimePickerThemeMaxWidth}" - TextBlock.Foreground="{DynamicResource ThemeForegroundColor}" + TextElement.Foreground="{DynamicResource ThemeForegroundColor}" HorizontalAlignment="Stretch" VerticalAlignment="Top" /> @@ -74,7 +74,7 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" - TextBlock.Foreground="{TemplateBinding Foreground}" + TextElement.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" /> @@ -135,7 +135,7 @@ diff --git a/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml index b14a239c35..17fb2af16c 100644 --- a/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml @@ -18,7 +18,7 @@ Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" RecognizesAccessKey="True" - TextBlock.Foreground="{TemplateBinding Foreground}" + TextElement.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> diff --git a/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml index 305f1b1814..2c831cf360 100644 --- a/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml @@ -254,7 +254,7 @@ + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 702fcc7c7a..468b723f5b 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -3,8 +3,9 @@ x:Class="Avalonia.Themes.Default.DefaultTheme"> + + - @@ -13,6 +14,7 @@ + @@ -22,7 +24,6 @@ - @@ -38,6 +39,7 @@ + @@ -57,6 +59,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index a1f423c059..5b86de02d5 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -556,6 +556,33 @@ 1 32 + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index f75af76144..eb68270354 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -550,6 +550,33 @@ 1 32 + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + diff --git a/src/Avalonia.Themes.Fluent/ApiCompatBaseline.txt b/src/Avalonia.Themes.Fluent/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 885661536c..35603fe216 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -3,16 +3,10 @@ net6.0;netstandard2.0 - - - - - - - + diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 533fabfb44..f545206a2f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -4,7 +4,7 @@ - /// The bounds of the ellipse. - /// An ellipse geometry.. - IGeometryImpl CreateEllipseGeometry(Rect rect); - - /// - /// Creates a line geometry implementation. - /// - /// The start of the line. - /// The end of the line. - /// A line geometry. - IGeometryImpl CreateLineGeometry(Point p1, Point p2); - - /// - /// Creates a rectangle geometry implementation. - /// - /// The bounds of the rectangle. - /// A rectangle. - IGeometryImpl CreateRectangleGeometry(Rect rect); - - /// - /// Creates a stream geometry implementation. - /// - /// An . - IStreamGeometryImpl CreateStreamGeometry(); - - /// - /// Creates a geometry group implementation. - /// - /// The fill rule. - /// The geometries to group. - /// A combined geometry. - IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); - - /// - /// Creates a geometry group implementation. - /// - /// The combine mode - /// The first geometry. - /// The second geometry. - /// A combined geometry. - IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2); - - /// - /// Creates a renderer. - /// - /// - /// The list of native platform surfaces that can be used for output. - /// - /// An . - IRenderTarget CreateRenderTarget(IEnumerable surfaces); - - /// - /// Creates a render target bitmap implementation. - /// - /// The size of the bitmap in device pixels. - /// The DPI of the bitmap. - /// An . - IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi); - - /// - /// Creates a writeable bitmap implementation. - /// - /// The size of the bitmap in device pixels. - /// The DPI of the bitmap. - /// Pixel format. - /// Alpha format . - /// An . - IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat format, AlphaFormat alphaFormat); - - /// - /// Loads a bitmap implementation from a file.. - /// - /// The filename of the bitmap. - /// An . - IBitmapImpl LoadBitmap(string fileName); - - /// - /// Loads a bitmap implementation from a file.. - /// - /// The stream to read the bitmap from. - /// An . - IBitmapImpl LoadBitmap(Stream stream); - - /// - /// Loads a WriteableBitmap implementation from a stream to a specified width maintaining aspect ratio. - /// - /// The stream to read the bitmap from. - /// The desired width of the resulting bitmap. - /// The to use should resizing be required. - /// An . - IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); - - /// - /// Loads a WriteableBitmap implementation from a stream to a specified height maintaining aspect ratio. - /// - /// The stream to read the bitmap from. - /// The desired height of the resulting bitmap. - /// The to use should resizing be required. - /// An . - IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); - - /// - /// Loads a WriteableBitmap implementation from a file. - /// - /// The filename of the bitmap. - /// An . - IWriteableBitmapImpl LoadWriteableBitmap(string fileName); - - /// - /// Loads a WriteableBitmap implementation from a file. - /// - /// The stream to read the bitmap from. - /// An . - IWriteableBitmapImpl LoadWriteableBitmap(Stream stream); - - /// - /// Loads a bitmap implementation from a stream to a specified width maintaining aspect ratio. - /// - /// The stream to read the bitmap from. - /// The desired width of the resulting bitmap. - /// The to use should resizing be required. - /// An . - IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); - - /// - /// Loads a bitmap implementation from a stream to a specified height maintaining aspect ratio. - /// - /// The stream to read the bitmap from. - /// The desired height of the resulting bitmap. - /// The to use should resizing be required. - /// An . - IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); - - IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); - - /// - /// Loads a bitmap implementation from a pixels in memory. - /// - /// The pixel format. - /// The alpha format. - /// The pointer to source bytes. - /// The size of the bitmap in device pixels. - /// The DPI of the bitmap. - /// The number of bytes per row. - /// An . - IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride); - - /// - /// Creates a platform implementation of a glyph run. - /// - /// The glyph run. - /// - IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun); - - bool SupportsIndividualRoundRects { get; } - - /// - /// Default used on this platform. - /// - public AlphaFormat DefaultAlphaFormat { get; } - - /// - /// Default used on this platform. - /// - public PixelFormat DefaultPixelFormat { get; } - } -} diff --git a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs deleted file mode 100644 index aced05c9d8..0000000000 --- a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Globalization; -using Avalonia.Media; -using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; - -namespace Avalonia.Platform -{ - /// - /// An abstraction that is used produce shaped text. - /// - public interface ITextShaperImpl - { - /// - /// Shapes the specified region within the text and returns a shaped buffer. - /// - /// The text. - /// The typeface. - /// The font rendering em size. - /// The culture. - /// The bidi level. - /// A shaped glyph run. - ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, CultureInfo? culture, sbyte bidiLevel); - } -} diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs deleted file mode 100644 index ebff097199..0000000000 --- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Runtime.CompilerServices; -using Avalonia.Metadata; - -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Transformation")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] - -[assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.Web.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs deleted file mode 100644 index 6d7d6c2e54..0000000000 --- a/src/Avalonia.Visuals/Rect.cs +++ /dev/null @@ -1,576 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Animation.Animators; -using Avalonia.Utilities; - -namespace Avalonia -{ - /// - /// Defines a rectangle. - /// - public readonly struct Rect : IEquatable - { - static Rect() - { - Animation.Animation.RegisterAnimator(prop => typeof(Rect).IsAssignableFrom(prop.PropertyType)); - } - - /// - /// An empty rectangle. - /// - public static readonly Rect Empty = default(Rect); - - /// - /// The X position. - /// - private readonly double _x; - - /// - /// The Y position. - /// - private readonly double _y; - - /// - /// The width. - /// - private readonly double _width; - - /// - /// The height. - /// - private readonly double _height; - - /// - /// Initializes a new instance of the structure. - /// - /// The X position. - /// The Y position. - /// The width. - /// The height. - public Rect(double x, double y, double width, double height) - { - _x = x; - _y = y; - _width = width; - _height = height; - } - - /// - /// Initializes a new instance of the structure. - /// - /// The size of the rectangle. - public Rect(Size size) - { - _x = 0; - _y = 0; - _width = size.Width; - _height = size.Height; - } - - /// - /// Initializes a new instance of the structure. - /// - /// The position of the rectangle. - /// The size of the rectangle. - public Rect(Point position, Size size) - { - _x = position.X; - _y = position.Y; - _width = size.Width; - _height = size.Height; - } - - /// - /// Initializes a new instance of the structure. - /// - /// The top left position of the rectangle. - /// The bottom right position of the rectangle. - public Rect(Point topLeft, Point bottomRight) - { - _x = topLeft.X; - _y = topLeft.Y; - _width = bottomRight.X - topLeft.X; - _height = bottomRight.Y - topLeft.Y; - } - - /// - /// Gets the X position. - /// - public double X => _x; - - /// - /// Gets the Y position. - /// - public double Y => _y; - - /// - /// Gets the width. - /// - public double Width => _width; - - /// - /// Gets the height. - /// - public double Height => _height; - - /// - /// Gets the position of the rectangle. - /// - public Point Position => new Point(_x, _y); - - /// - /// Gets the size of the rectangle. - /// - public Size Size => new Size(_width, _height); - - /// - /// Gets the right position of the rectangle. - /// - public double Right => _x + _width; - - /// - /// Gets the bottom position of the rectangle. - /// - public double Bottom => _y + _height; - - /// - /// Gets the left position. - /// - public double Left => _x; - - /// - /// Gets the top position. - /// - public double Top => _y; - - /// - /// Gets the top left point of the rectangle. - /// - public Point TopLeft => new Point(_x, _y); - - /// - /// Gets the top right point of the rectangle. - /// - public Point TopRight => new Point(Right, _y); - - /// - /// Gets the bottom left point of the rectangle. - /// - public Point BottomLeft => new Point(_x, Bottom); - - /// - /// Gets the bottom right point of the rectangle. - /// - public Point BottomRight => new Point(Right, Bottom); - - /// - /// Gets the center point of the rectangle. - /// - public Point Center => new Point(_x + (_width / 2), _y + (_height / 2)); - - /// - /// Gets a value that indicates whether the rectangle is empty. - /// - // ReSharper disable CompareOfFloatsByEqualityOperator - public bool IsEmpty => _width == 0 && _height == 0; - // ReSharper restore CompareOfFloatsByEqualityOperator - - /// - /// Checks for equality between two s. - /// - /// The first rect. - /// The second rect. - /// True if the rects are equal; otherwise false. - public static bool operator ==(Rect left, Rect right) - { - return left.Equals(right); - } - - /// - /// Checks for inequality between two s. - /// - /// The first rect. - /// The second rect. - /// True if the rects are unequal; otherwise false. - public static bool operator !=(Rect left, Rect right) - { - return !(left == right); - } - - /// - /// Multiplies a rectangle by a scaling vector. - /// - /// The rectangle. - /// The vector scale. - /// The scaled rectangle. - public static Rect operator *(Rect rect, Vector scale) - { - return new Rect( - rect.X * scale.X, - rect.Y * scale.Y, - rect.Width * scale.X, - rect.Height * scale.Y); - } - - /// - /// Multiplies a rectangle by a scale. - /// - /// The rectangle. - /// The scale. - /// The scaled rectangle. - public static Rect operator *(Rect rect, double scale) - { - return new Rect( - rect.X * scale, - rect.Y * scale, - rect.Width * scale, - rect.Height * scale); - } - - /// - /// Divides a rectangle by a vector. - /// - /// The rectangle. - /// The vector scale. - /// The scaled rectangle. - public static Rect operator /(Rect rect, Vector scale) - { - return new Rect( - rect.X / scale.X, - rect.Y / scale.Y, - rect.Width / scale.X, - rect.Height / scale.Y); - } - - /// - /// Determines whether a point in in the bounds of the rectangle. - /// - /// The point. - /// true if the point is in the bounds of the rectangle; otherwise false. - public bool Contains(Point p) - { - return p.X >= _x && p.X <= _x + _width && - p.Y >= _y && p.Y <= _y + _height; - } - - /// - /// Determines whether the rectangle fully contains another rectangle. - /// - /// The rectangle. - /// true if the rectangle is fully contained; otherwise false. - public bool Contains(Rect r) - { - return Contains(r.TopLeft) && Contains(r.BottomRight); - } - - /// - /// Centers another rectangle in this rectangle. - /// - /// The rectangle to center. - /// The centered rectangle. - public Rect CenterRect(Rect rect) - { - return new Rect( - _x + ((_width - rect._width) / 2), - _y + ((_height - rect._height) / 2), - rect._width, - rect._height); - } - - /// - /// Inflates the rectangle. - /// - /// The thickness to be subtracted for each side of the rectangle. - /// The inflated rectangle. - public Rect Inflate(double thickness) - { - return Inflate(new Thickness(thickness)); - } - - /// - /// Inflates the rectangle. - /// - /// The thickness to be subtracted for each side of the rectangle. - /// The inflated rectangle. - public Rect Inflate(Thickness thickness) - { - return new Rect( - new Point(_x - thickness.Left, _y - thickness.Top), - Size.Inflate(thickness)); - } - - /// - /// Deflates the rectangle. - /// - /// The thickness to be subtracted for each side of the rectangle. - /// The deflated rectangle. - public Rect Deflate(double thickness) - { - return Deflate(new Thickness(thickness)); - } - - /// - /// Deflates the rectangle by a . - /// - /// The thickness to be subtracted for each side of the rectangle. - /// The deflated rectangle. - public Rect Deflate(Thickness thickness) - { - return new Rect( - new Point(_x + thickness.Left, _y + thickness.Top), - Size.Deflate(thickness)); - } - - /// - /// Returns a boolean indicating whether the rect is equal to the other given rect. - /// - /// The other rect to test equality against. - /// True if this rect is equal to other; False otherwise. - public bool Equals(Rect other) - { - // ReSharper disable CompareOfFloatsByEqualityOperator - return _x == other._x && - _y == other._y && - _width == other._width && - _height == other._height; - // ReSharper enable CompareOfFloatsByEqualityOperator - } - - /// - /// Returns a boolean indicating whether the given object is equal to this rectangle. - /// - /// The object to compare against. - /// True if the object is equal to this rectangle; false otherwise. - public override bool Equals(object? obj) => obj is Rect other && Equals(other); - - /// - /// Returns the hash code for this instance. - /// - /// The hash code. - public override int GetHashCode() - { - unchecked - { - int hash = 17; - hash = (hash * 23) + X.GetHashCode(); - hash = (hash * 23) + Y.GetHashCode(); - hash = (hash * 23) + Width.GetHashCode(); - hash = (hash * 23) + Height.GetHashCode(); - return hash; - } - } - - /// - /// Gets the intersection of two rectangles. - /// - /// The other rectangle. - /// The intersection. - public Rect Intersect(Rect rect) - { - var newLeft = (rect.X > X) ? rect.X : X; - var newTop = (rect.Y > Y) ? rect.Y : Y; - var newRight = (rect.Right < Right) ? rect.Right : Right; - var newBottom = (rect.Bottom < Bottom) ? rect.Bottom : Bottom; - - if ((newRight > newLeft) && (newBottom > newTop)) - { - return new Rect(newLeft, newTop, newRight - newLeft, newBottom - newTop); - } - else - { - return Empty; - } - } - - /// - /// Determines whether a rectangle intersects with this rectangle. - /// - /// The other rectangle. - /// - /// True if the specified rectangle intersects with this one; otherwise false. - /// - public bool Intersects(Rect rect) - { - return (rect.X < Right) && (X < rect.Right) && (rect.Y < Bottom) && (Y < rect.Bottom); - } - - /// - /// Returns the axis-aligned bounding box of a transformed rectangle. - /// - /// The transform. - /// The bounding box - public Rect TransformToAABB(Matrix matrix) - { - ReadOnlySpan points = stackalloc Point[4] - { - TopLeft.Transform(matrix), - TopRight.Transform(matrix), - BottomRight.Transform(matrix), - BottomLeft.Transform(matrix) - }; - - var left = double.MaxValue; - var right = double.MinValue; - var top = double.MaxValue; - var bottom = double.MinValue; - - foreach (var p in points) - { - if (p.X < left) left = p.X; - if (p.X > right) right = p.X; - if (p.Y < top) top = p.Y; - if (p.Y > bottom) bottom = p.Y; - } - - return new Rect(new Point(left, top), new Point(right, bottom)); - } - - /// - /// Translates the rectangle by an offset. - /// - /// The offset. - /// The translated rectangle. - public Rect Translate(Vector offset) - { - return new Rect(Position + offset, Size); - } - - /// - /// Normalizes the rectangle so both the and are positive, without changing the location of the rectangle - /// - /// Normalized Rect - /// - /// Empty rect will be return when Rect contains invalid values. Like NaN. - /// - public Rect Normalize() - { - Rect rect = this; - - if(double.IsNaN(rect.Right) || double.IsNaN(rect.Bottom) || - double.IsNaN(rect.X) || double.IsNaN(rect.Y) || - double.IsNaN(Height) || double.IsNaN(Width)) - { - return Rect.Empty; - } - - if (rect.Width < 0) - { - var x = X + Width; - var width = X - x; - - rect = rect.WithX(x).WithWidth(width); - } - - if (rect.Height < 0) - { - var y = Y + Height; - var height = Y - y; - - rect = rect.WithY(y).WithHeight(height); - } - - return rect; - } - - - /// - /// Gets the union of two rectangles. - /// - /// The other rectangle. - /// The union. - public Rect Union(Rect rect) - { - if (IsEmpty) - { - return rect; - } - else if (rect.IsEmpty) - { - return this; - } - else - { - var x1 = Math.Min(X, rect.X); - var x2 = Math.Max(Right, rect.Right); - var y1 = Math.Min(Y, rect.Y); - var y2 = Math.Max(Bottom, rect.Bottom); - - return new Rect(new Point(x1, y1), new Point(x2, y2)); - } - } - - /// - /// Returns a new with the specified X position. - /// - /// The x position. - /// The new . - public Rect WithX(double x) - { - return new Rect(x, _y, _width, _height); - } - - /// - /// Returns a new with the specified Y position. - /// - /// The y position. - /// The new . - public Rect WithY(double y) - { - return new Rect(_x, y, _width, _height); - } - - /// - /// Returns a new with the specified width. - /// - /// The width. - /// The new . - public Rect WithWidth(double width) - { - return new Rect(_x, _y, width, _height); - } - - /// - /// Returns a new with the specified height. - /// - /// The height. - /// The new . - public Rect WithHeight(double height) - { - return new Rect(_x, _y, _width, height); - } - - /// - /// Returns the string representation of the rectangle. - /// - /// The string representation of the rectangle. - public override string ToString() - { - return string.Format( - CultureInfo.InvariantCulture, - "{0}, {1}, {2}, {3}", - _x, - _y, - _width, - _height); - } - - /// - /// Parses a string. - /// - /// The string. - /// The parsed . - public static Rect Parse(string s) - { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect.")) - { - return new Rect( - tokenizer.ReadDouble(), - tokenizer.ReadDouble(), - tokenizer.ReadDouble(), - tokenizer.ReadDouble() - ); - } - } - } -} diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs deleted file mode 100644 index 4550dbd54b..0000000000 --- a/src/Avalonia.Visuals/RelativePoint.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; -using System.Globalization; - -using Avalonia.Animation.Animators; -using Avalonia.Utilities; - -namespace Avalonia -{ - /// - /// Defines the reference point units of an or - /// . - /// - public enum RelativeUnit - { - /// - /// The point is expressed as a fraction of the containing element's size. - /// - Relative, - - /// - /// The point is absolute (i.e. in pixels). - /// - Absolute, - } - - /// - /// Defines a point that may be defined relative to a containing element. - /// - public readonly struct RelativePoint : IEquatable - { - /// - /// A point at the top left of the containing element. - /// - public static readonly RelativePoint TopLeft = new RelativePoint(0, 0, RelativeUnit.Relative); - - /// - /// A point at the center of the containing element. - /// - public static readonly RelativePoint Center = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); - - /// - /// A point at the bottom right of the containing element. - /// - public static readonly RelativePoint BottomRight = new RelativePoint(1, 1, RelativeUnit.Relative); - - private readonly Point _point; - - private readonly RelativeUnit _unit; - - static RelativePoint() - { - Animation.Animation.RegisterAnimator(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType)); - } - - /// - /// Initializes a new instance of the struct. - /// - /// The X point. - /// The Y point - /// The unit. - public RelativePoint(double x, double y, RelativeUnit unit) - : this(new Point(x, y), unit) - { - } - - /// - /// Initializes a new instance of the struct. - /// - /// The point. - /// The unit. - public RelativePoint(Point point, RelativeUnit unit) - { - _point = point; - _unit = unit; - } - - /// - /// Gets the point. - /// - public Point Point => _point; - - /// - /// Gets the unit. - /// - public RelativeUnit Unit => _unit; - - /// - /// Checks for equality between two s. - /// - /// The first point. - /// The second point. - /// True if the points are equal; otherwise false. - public static bool operator ==(RelativePoint left, RelativePoint right) - { - return left.Equals(right); - } - - /// - /// Checks for inequality between two s. - /// - /// The first point. - /// The second point. - /// True if the points are unequal; otherwise false. - public static bool operator !=(RelativePoint left, RelativePoint right) - { - return !left.Equals(right); - } - - /// - /// Checks if the equals another object. - /// - /// The other object. - /// True if the objects are equal, otherwise false. - public override bool Equals(object? obj) => obj is RelativePoint other && Equals(other); - - /// - /// Checks if the equals another point. - /// - /// The other point. - /// True if the objects are equal, otherwise false. - public bool Equals(RelativePoint p) - { - return Unit == p.Unit && Point == p.Point; - } - - /// - /// Gets a hashcode for a . - /// - /// A hash code. - public override int GetHashCode() - { - unchecked - { - return (_point.GetHashCode() * 397) ^ (int)_unit; - } - } - - /// - /// Converts a into pixels. - /// - /// The size of the visual. - /// The origin point in pixels. - public Point ToPixels(Size size) - { - return _unit == RelativeUnit.Absolute ? - _point : - new Point(_point.X * size.Width, _point.Y * size.Height); - } - - /// - /// Parses a string. - /// - /// The string. - /// The parsed . - public static RelativePoint Parse(string s) - { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint.")) - { - var x = tokenizer.ReadString(); - var y = tokenizer.ReadString(); - - var unit = RelativeUnit.Absolute; - var scale = 1.0; - - if (x.EndsWith("%")) - { - if (!y.EndsWith("%")) - { - throw new FormatException("If one coordinate is relative, both must be."); - } - - x = x.TrimEnd('%'); - y = y.TrimEnd('%'); - unit = RelativeUnit.Relative; - scale = 0.01; - } - - return new RelativePoint( - double.Parse(x, CultureInfo.InvariantCulture) * scale, - double.Parse(y, CultureInfo.InvariantCulture) * scale, - unit); - } - } - - /// - /// Returns a String representing this RelativePoint instance. - /// - /// The string representation. - public override string ToString() - { - return _unit == RelativeUnit.Absolute ? - _point.ToString() : - string.Format(CultureInfo.InvariantCulture, "{0}%, {1}%", _point.X * 100, _point.Y * 100); - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs deleted file mode 100644 index 23016de148..0000000000 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ /dev/null @@ -1,351 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Logging; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering -{ - /// - /// A renderer which renders the state of the visual tree without an intermediate scene graph - /// representation. - /// - /// - /// The immediate renderer supports only clip-bound-based hit testing; a control's geometry is - /// not taken into account. - /// - public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer - { - private readonly IVisual _root; - private readonly IRenderRoot? _renderRoot; - private bool _updateTransformedBounds = true; - private IRenderTarget? _renderTarget; - - /// - /// Initializes a new instance of the class. - /// - /// The control to render. - public ImmediateRenderer(IVisual root) - { - _root = root ?? throw new ArgumentNullException(nameof(root)); - _renderRoot = root as IRenderRoot; - } - - private ImmediateRenderer(IVisual root, bool updateTransformedBounds) - { - _root = root ?? throw new ArgumentNullException(nameof(root)); - _renderRoot = root as IRenderRoot; - _updateTransformedBounds = updateTransformedBounds; - } - - /// - public bool DrawFps { get; set; } - - /// - public bool DrawDirtyRects { get; set; } - - /// - public event EventHandler? SceneInvalidated; - - /// - public void Paint(Rect rect) - { - if (_renderTarget == null) - { - _renderTarget = ((IRenderRoot)_root).CreateRenderTarget(); - } - - try - { - using (var context = new DrawingContext(_renderTarget.CreateDrawingContext(this))) - { - context.PlatformImpl.Clear(Colors.Transparent); - - using (context.PushTransformContainer()) - { - Render(context, _root, _root.Bounds); - } - - if (DrawDirtyRects) - { - var color = (uint)new Random().Next(0xffffff) | 0x44000000; - context.FillRectangle( - new SolidColorBrush(color), - rect); - } - - if (DrawFps) - { - RenderFps(context, _root.Bounds, null); - } - } - } - catch (RenderTargetCorruptedException ex) - { - Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex); - _renderTarget.Dispose(); - _renderTarget = null; - } - - SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect)); - } - - /// - public void Resized(Size size) - { - } - - /// - /// Renders a visual to a render target. - /// - /// The visual. - /// The render target. - public static void Render(IVisual visual, IRenderTarget target) - { - using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) - using (var context = new DrawingContext(target.CreateDrawingContext(renderer))) - { - renderer.Render(context, visual, visual.Bounds); - } - } - - /// - /// Renders a visual to a drawing context. - /// - /// The visual. - /// The drawing context. - public static void Render(IVisual visual, DrawingContext context) - { - using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) - { - renderer.Render(context, visual, visual.Bounds); - } - } - - /// - public void AddDirty(IVisual visual) - { - if (visual.Bounds != Rect.Empty) - { - var m = visual.TransformToVisual(_root); - - if (m.HasValue) - { - var bounds = new Rect(visual.Bounds.Size).TransformToAABB(m.Value); - - //use transformedbounds as previous render state of the visual bounds - //so we can invalidate old and new bounds of a control in case it moved/shrinked - if (visual.TransformedBounds.HasValue) - { - var trb = visual.TransformedBounds.Value; - var trBounds = trb.Bounds.TransformToAABB(trb.Transform); - - if (trBounds != bounds) - { - _renderRoot?.Invalidate(trBounds); - } - } - - _renderRoot?.Invalidate(bounds); - } - } - } - - /// - /// Ends the operation of the renderer. - /// - public void Dispose() - { - _renderTarget?.Dispose(); - } - - /// - public IEnumerable HitTest(Point p, IVisual root, Func filter) - { - return HitTest(root, p, filter); - } - - public IVisual? HitTestFirst(Point p, IVisual root, Func filter) - { - return HitTest(root, p, filter).FirstOrDefault(); - } - - /// - public void RecalculateChildren(IVisual visual) => AddDirty(visual); - - /// - public void Start() - { - } - - /// - public void Stop() - { - } - - /// - Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) - { - (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized(); - return brush.Visual?.Bounds.Size ?? Size.Empty; - } - - /// - void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) - { - var visual = brush.Visual; - Render(new DrawingContext(context), visual, visual.Bounds); - } - - internal static void Render(IVisual visual, DrawingContext context, bool updateTransformedBounds) - { - using var renderer = new ImmediateRenderer(visual, updateTransformedBounds); - renderer.Render(context, visual, visual.Bounds); - } - - private static void ClearTransformedBounds(IVisual visual) - { - foreach (var e in visual.GetSelfAndVisualDescendants()) - { - visual.TransformedBounds = null; - } - } - - private static Rect GetTransformedBounds(IVisual visual) - { - if (visual.RenderTransform == null) - { - return visual.Bounds; - } - else - { - var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); - var offset = Matrix.CreateTranslation(visual.Bounds.Position + origin); - var m = (-offset) * visual.RenderTransform.Value * (offset); - return visual.Bounds.TransformToAABB(m); - } - } - - private static IEnumerable HitTest( - IVisual visual, - Point p, - Func? filter) - { - _ = visual ?? throw new ArgumentNullException(nameof(visual)); - - if (filter?.Invoke(visual) != false) - { - bool containsPoint; - - if (visual is ICustomSimpleHitTest custom) - { - containsPoint = custom.HitTest(p); - } - else - { - containsPoint = visual.TransformedBounds?.Contains(p) == true; - } - - if ((containsPoint || !visual.ClipToBounds) && visual.VisualChildren.Count > 0) - { - foreach (var child in visual.VisualChildren.SortByZIndex()) - { - foreach (var result in HitTest(child, p, filter)) - { - yield return result; - } - } - } - - if (containsPoint) - { - yield return visual; - } - } - } - - private void Render(DrawingContext context, IVisual visual, Rect clipRect) - { - var opacity = visual.Opacity; - var clipToBounds = visual.ClipToBounds; - var bounds = new Rect(visual.Bounds.Size); - - if (visual.IsVisible && opacity > 0) - { - var m = Matrix.CreateTranslation(visual.Bounds.Position); - - var renderTransform = Matrix.Identity; - - if (visual.RenderTransform != null) - { - var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); - var offset = Matrix.CreateTranslation(origin); - renderTransform = (-offset) * visual.RenderTransform.Value * (offset); - } - - m = renderTransform * m; - - if (clipToBounds) - { - if (visual.RenderTransform != null) - { - clipRect = new Rect(visual.Bounds.Size); - } - else - { - clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size)); - } - } - - using (context.PushPostTransform(m)) - using (context.PushOpacity(opacity)) - using (clipToBounds -#pragma warning disable CS0618 // Type or member is obsolete - ? visual is IVisualWithRoundRectClip roundClipVisual - ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius)) - : context.PushClip(bounds) - : default(DrawingContext.PushedState)) -#pragma warning restore CS0618 // Type or member is obsolete - - using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState)) - using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState)) - using (context.PushTransformContainer()) - { - visual.Render(context); - -#pragma warning disable 0618 - var transformed = - new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform); -#pragma warning restore 0618 - - if (_updateTransformedBounds) - visual.TransformedBounds = transformed; - - foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)) - { - var childBounds = GetTransformedBounds(child); - - if (!child.ClipToBounds || clipRect.Intersects(childBounds)) - { - var childClipRect = child.RenderTransform == null - ? clipRect.Translate(-childBounds.Position) - : clipRect; - Render(context, child, childClipRect); - } - else if (_updateTransformedBounds) - { - ClearTransformedBounds(child); - } - } - } - } - - if (!visual.IsVisible && _updateTransformedBounds) - { - ClearTransformedBounds(visual); - } - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs deleted file mode 100644 index 45b62b843b..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Avalonia.Platform; -using Avalonia.Visuals.Media.Imaging; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents an bitmap blending mode push or pop. - /// - internal class BitmapBlendModeNode : IDrawOperation - { - /// - /// Initializes a new instance of the class that represents an - /// push. - /// - /// The to push. - public BitmapBlendModeNode(BitmapBlendingMode bitmapBlend) - { - BlendingMode = bitmapBlend; - } - - /// - /// Initializes a new instance of the class that represents an - /// pop. - /// - public BitmapBlendModeNode() - { - } - - /// - public Rect Bounds => Rect.Empty; - - /// - /// Gets the BitmapBlend to be pushed or null if the operation represents a pop. - /// - public BitmapBlendingMode? BlendingMode { get; } - - /// - public bool HitTest(Point p) => false; - - /// - /// Determines if this draw operation equals another. - /// - /// the how to compare - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(BitmapBlendingMode? blendingMode) => BlendingMode == blendingMode; - - /// - public void Render(IDrawingContextImpl context) - { - if (BlendingMode.HasValue) - { - context.PushBitmapBlendMode(BlendingMode.Value); - } - else - { - context.PopBitmapBlendMode(); - } - } - - public void Dispose() - { - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs deleted file mode 100644 index 9710ca6c3c..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ /dev/null @@ -1,479 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A drawing context which builds a scene graph. - /// - internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport - { - private readonly ISceneBuilder _sceneBuilder; - private VisualNode? _node; - private int _childIndex; - private int _drawOperationindex; - - /// - /// Initializes a new instance of the class. - /// - /// - /// A scene builder used for constructing child scenes for visual brushes. - /// - /// The scene layers. - public DeferredDrawingContextImpl(ISceneBuilder sceneBuilder, SceneLayers layers) - { - _sceneBuilder = sceneBuilder; - Layers = layers; - } - - /// - public Matrix Transform { get; set; } = Matrix.Identity; - - /// - /// Gets the layers in the scene being built. - /// - public SceneLayers Layers { get; } - - /// - /// Informs the drawing context of the visual node that is about to be rendered. - /// - /// The visual node. - /// - /// An object which when disposed will commit the changes to visual node. - /// - public UpdateState BeginUpdate(VisualNode node) - { - _ = node ?? throw new ArgumentNullException(nameof(node)); - - if (_node != null) - { - if (_childIndex < _node.Children.Count) - { - _node.ReplaceChild(_childIndex, node); - } - else - { - _node.AddChild(node); - } - - ++_childIndex; - } - - var state = new UpdateState(this, _node, _childIndex, _drawOperationindex); - _node = node; - _childIndex = _drawOperationindex = 0; - return state; - } - - /// - public void Clear(Color color) - { - // Cannot clear a deferred scene. - } - - /// - public void Dispose() - { - // Nothing to do here since we allocate no unmanaged resources. - } - - /// - /// Removes any remaining drawing operations from the visual node. - /// - /// - /// Drawing operations are updated in place, overwriting existing drawing operations if - /// they are different. Once drawing has completed for the current visual node, it is - /// possible that there are stale drawing operations at the end of the list. This method - /// trims these stale drawing operations. - /// - public void TrimChildren() - { - _node!.TrimChildren(_childIndex); - } - - /// - public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) - { - Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) - { - Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) - { - // This method is currently only used to composite layers so shouldn't be called here. - throw new NotSupportedException(); - } - - /// - public void DrawLine(IPen pen, Point p1, Point p2) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) - { - Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, - BoxShadows boxShadows = default) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) - { - Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, material, rect)) - { - Add(new ExperimentalAcrylicNode(Transform, material, rect)); - } - else - { - ++_drawOperationindex; - } - } - - public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) - { - Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); - } - else - { - ++_drawOperationindex; - } - } - - public void Custom(ICustomDrawOperation custom) - { - var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, custom)) - Add(new CustomDrawOperation(custom, Transform)); - else - ++_drawOperationindex; - } - - /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) - { - Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); - } - - else - { - ++_drawOperationindex; - } - } - public IDrawingContextLayerImpl CreateLayer(Size size) - { - throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); - } - - /// - public void PopClip() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new ClipNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PopGeometryClip() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new GeometryClipNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PopBitmapBlendMode() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new BitmapBlendModeNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PopOpacity() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new OpacityNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PopOpacityMask() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null, null)) - { - Add(new OpacityMaskNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushClip(Rect clip) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, clip)) - { - Add(new ClipNode(Transform, clip)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushClip(RoundedRect clip) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, clip)) - { - Add(new ClipNode(Transform, clip)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushGeometryClip(IGeometryImpl? clip) - { - if (clip is null) - return; - - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, clip)) - { - Add(new GeometryClipNode(Transform, clip)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushOpacity(double opacity) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(opacity)) - { - Add(new OpacityNode(opacity)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushOpacityMask(IBrush mask, Rect bounds) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(mask, bounds)) - { - Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(blendingMode)) - { - Add(new BitmapBlendModeNode(blendingMode)); - } - else - { - ++_drawOperationindex; - } - } - - public readonly struct UpdateState : IDisposable - { - public UpdateState( - DeferredDrawingContextImpl owner, - VisualNode? node, - int childIndex, - int drawOperationIndex) - { - Owner = owner; - Node = node; - ChildIndex = childIndex; - DrawOperationIndex = drawOperationIndex; - } - - public void Dispose() - { - Owner._node!.TrimDrawOperations(Owner._drawOperationindex); - - var dirty = Owner.Layers.GetOrAdd(Owner._node.LayerRoot!).Dirty; - - var drawOperations = Owner._node.DrawOperations; - var drawOperationsCount = drawOperations.Count; - - for (var i = 0; i < drawOperationsCount; i++) - { - dirty.Add(drawOperations[i].Item.Bounds); - } - - Owner._node = Node; - Owner._childIndex = ChildIndex; - Owner._drawOperationindex = DrawOperationIndex; - } - - public DeferredDrawingContextImpl Owner { get; } - public VisualNode? Node { get; } - public int ChildIndex { get; } - public int DrawOperationIndex { get; } - } - - private void Add(T node) where T : class, IDrawOperation - { - using (var refCounted = RefCountable.Create(node)) - { - Add(refCounted); - } - } - - private void Add(IRef node) - { - if (_drawOperationindex < _node!.DrawOperations.Count) - { - _node.ReplaceDrawOperation(_drawOperationindex, node); - } - else - { - _node.AddDrawOperation(node); - } - - ++_drawOperationindex; - } - - private IRef? NextDrawAs() where T : class, IDrawOperation - { - return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef : null; - } - - private IDictionary? CreateChildScene(IBrush? brush) - { - var visualBrush = brush as VisualBrush; - - if (visualBrush != null) - { - var visual = visualBrush.Visual; - - if (visual != null) - { - (visual as IVisualBrushInitialize)?.EnsureInitialized(); - var scene = new Scene(visual); - _sceneBuilder.UpdateAll(scene); - return new Dictionary { { visualBrush.Visual, scene } }; - } - } - - return null; - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs deleted file mode 100644 index 504256b932..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents an ellipse draw. - /// - internal class EllipseNode : BrushDrawOperation - { - public EllipseNode( - Matrix transform, - IBrush? brush, - IPen? pen, - Rect rect, - IDictionary? childScenes = null) - : base(rect.Inflate(pen?.Thickness ?? 0), transform) - { - Transform = transform; - Brush = brush?.ToImmutable(); - Pen = pen?.ToImmutable(); - Rect = rect; - ChildScenes = childScenes; - } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - - /// - /// Gets the stroke pen. - /// - public ImmutablePen? Pen { get; } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the rect of the ellipse to draw. - /// - public Rect Rect { get; } - - public override IDictionary? ChildScenes { get; } - - public bool Equals(Matrix transform, IBrush? brush, IPen? pen, Rect rect) - { - return transform == Transform && - Equals(brush, Brush) && - Equals(Pen, pen) && - rect.Equals(Rect); - } - - public override void Render(IDrawingContextImpl context) - { - context.DrawEllipse(Brush, Pen, Rect); - } - - public override bool HitTest(Point p) - { - if (!Transform.TryInvert(out Matrix inverted)) - { - return false; - } - - p *= inverted; - - var center = Rect.Center; - - var strokeThickness = Pen?.Thickness ?? 0; - - var rx = Rect.Width / 2 + strokeThickness / 2; - var ry = Rect.Height / 2 + strokeThickness / 2; - - var dx = p.X - center.X; - var dy = p.Y - center.Y; - - if (Math.Abs(dx) > rx || Math.Abs(dy) > ry) - { - return false; - } - - if (Brush != null) - { - return Contains(rx, ry); - } - else if (strokeThickness > 0) - { - bool inStroke = Contains(rx, ry); - - rx = Rect.Width / 2 - strokeThickness / 2; - ry = Rect.Height / 2 - strokeThickness / 2; - - bool inInner = Contains(rx, ry); - - return inStroke && !inInner; - } - - bool Contains(double radiusX, double radiusY) - { - var rx2 = radiusX * radiusX; - var ry2 = radiusY * radiusY; - - var distance = ry2 * dx * dx + rx2 * dy * dy; - - return distance <= rx2 * ry2; - } - - return false; - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs deleted file mode 100644 index 7de1035441..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents a geometry draw. - /// - internal class GeometryNode : BrushDrawOperation - { - /// - /// Initializes a new instance of the class. - /// - /// The transform. - /// The fill brush. - /// The stroke pen. - /// The geometry. - /// Child scenes for drawing visual brushes. - public GeometryNode(Matrix transform, - IBrush? brush, - IPen? pen, - IGeometryImpl geometry, - IDictionary? childScenes = null) - : base(geometry.GetRenderBounds(pen), transform) - { - Transform = transform; - Brush = brush?.ToImmutable(); - Pen = pen?.ToImmutable(); - Geometry = geometry; - ChildScenes = childScenes; - } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - - /// - /// Gets the stroke pen. - /// - public ImmutablePen? Pen { get; } - - /// - /// Gets the geometry to draw. - /// - public IGeometryImpl Geometry { get; } - - /// - public override IDictionary? ChildScenes { get; } - - /// - /// Determines if this draw operation equals another. - /// - /// The transform of the other draw operation. - /// The fill of the other draw operation. - /// The stroke of the other draw operation. - /// The geometry of the other draw operation. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(Matrix transform, IBrush? brush, IPen? pen, IGeometryImpl geometry) - { - return transform == Transform && - Equals(brush, Brush) && - Equals(Pen, pen) && - Equals(geometry, Geometry); - } - - /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawGeometry(Brush, Pen, Geometry); - } - - /// - public override bool HitTest(Point p) - { - if (Transform.HasInverse) - { - p *= Transform.Invert(); - return (Brush != null && Geometry.FillContains(p)) || - (Pen != null && Geometry.StrokeContains(Pen, p)); - } - - return false; - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs deleted file mode 100644 index d3da19d8c9..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Avalonia.Platform; -using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents an image draw. - /// - internal class ImageNode : DrawOperation - { - /// - /// Initializes a new instance of the class. - /// - /// The transform. - /// The image to draw. - /// The draw opacity. - /// The source rect. - /// The destination rect. - /// The bitmap interpolation mode. - public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) - : base(destRect, transform) - { - Transform = transform; - Source = source.Clone(); - Opacity = opacity; - SourceRect = sourceRect; - DestRect = destRect; - BitmapInterpolationMode = bitmapInterpolationMode; - SourceVersion = Source.Item.Version; - } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the image to draw. - /// - public IRef Source { get; } - - /// - /// Source bitmap Version - /// - public int SourceVersion { get; } - - /// - /// Gets the draw opacity. - /// - public double Opacity { get; } - - /// - /// Gets the source rect. - /// - public Rect SourceRect { get; } - - /// - /// Gets the destination rect. - /// - public Rect DestRect { get; } - - /// - /// Gets the bitmap interpolation mode. - /// - /// - /// The scaling mode. - /// - public BitmapInterpolationMode BitmapInterpolationMode { get; } - - /// - /// The bitmap blending mode. - /// - /// - /// The blending mode. - /// - public BitmapBlendingMode BitmapBlendingMode { get; } - - /// - /// Determines if this draw operation equals another. - /// - /// The transform of the other draw operation. - /// The image of the other draw operation. - /// The opacity of the other draw operation. - /// The source rect of the other draw operation. - /// The dest rect of the other draw operation. - /// The bitmap interpolation mode. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) - { - return transform == Transform && - Equals(source.Item, Source.Item) && - source.Item.Version == SourceVersion && - opacity == Opacity && - sourceRect == SourceRect && - destRect == DestRect && - bitmapInterpolationMode == BitmapInterpolationMode; - } - - /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); - } - - /// - public override bool HitTest(Point p) => Bounds.Contains(p); - - public override void Dispose() - { - Source?.Dispose(); - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs deleted file mode 100644 index 63c22efc3f..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ /dev/null @@ -1,466 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Threading; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// Builds a scene graph from a visual tree. - /// - public class SceneBuilder : ISceneBuilder - { - /// - public void UpdateAll(Scene scene) - { - _ = scene ?? throw new ArgumentNullException(nameof(scene)); - Dispatcher.UIThread.VerifyAccess(); - - UpdateSize(scene); - scene.Layers.GetOrAdd(scene.Root.Visual); - - using (var impl = new DeferredDrawingContextImpl(this, scene.Layers)) - using (var context = new DrawingContext(impl)) - { - var clip = new Rect(scene.Root.Visual.Bounds.Size); - Update(context, scene, (VisualNode)scene.Root, clip, true); - } - } - - /// - public bool Update(Scene scene, IVisual visual) - { - _ = scene ?? throw new ArgumentNullException(nameof(scene)); - _ = visual ?? throw new ArgumentNullException(nameof(visual)); - - Dispatcher.UIThread.VerifyAccess(); - - if (!scene.Root.Visual.IsVisible) - { - throw new AvaloniaInternalException("Cannot update the scene for an invisible root visual."); - } - - var node = (VisualNode?)scene.FindNode(visual); - - if (visual == scene.Root.Visual) - { - UpdateSize(scene); - } - - if (visual.VisualRoot == scene.Root.Visual) - { - if (node?.Parent != null && - visual.VisualParent != null && - node.Parent.Visual != visual.VisualParent) - { - // The control has changed parents. Remove the node and recurse into the new parent node. - ((VisualNode)node.Parent).RemoveChild(node); - Deindex(scene, node); - node = (VisualNode?)scene.FindNode(visual.VisualParent); - } - - if (visual.IsVisible) - { - // If the node isn't yet part of the scene, find the nearest ancestor that is. - node = node ?? FindExistingAncestor(scene, visual); - - // We don't need to do anything if this part of the tree has already been fully - // updated. - if (node != null && !node.SubTreeUpdated) - { - // If the control we've been asked to update isn't part of the scene then - // we're carrying out an add operation, so recurse and add all the - // descendents too. - var recurse = node.Visual != visual; - - using (var impl = new DeferredDrawingContextImpl(this, scene.Layers)) - using (var context = new DrawingContext(impl)) - { - var clip = new Rect(scene.Root.Visual.Bounds.Size); - - if (node.Parent != null) - { - context.PushPostTransform(node.Parent.Transform); - clip = node.Parent.ClipBounds; - } - - using (context.PushTransformContainer()) - { - Update(context, scene, node, clip, recurse); - } - } - - return true; - } - } - else - { - if (node != null) - { - // The control has been hidden so remove it from its parent and deindex the - // node and its descendents. - ((VisualNode?)node.Parent)?.RemoveChild(node); - Deindex(scene, node); - return true; - } - } - } - else if (node != null) - { - // The control has been removed so remove it from its parent and deindex the - // node and its descendents. - var trim = FindFirstDeadAncestor(scene, node); - ((VisualNode)trim.Parent!).RemoveChild(trim); - Deindex(scene, trim); - return true; - } - - return false; - } - - private static VisualNode? FindExistingAncestor(Scene scene, IVisual visual) - { - var node = scene.FindNode(visual); - - while (node == null && visual.IsVisible) - { - var parent = visual.VisualParent; - - if (parent is null) - return null; - - visual = parent; - node = scene.FindNode(visual); - } - - return visual.IsVisible ? (VisualNode?)node : null; - } - - private static VisualNode FindFirstDeadAncestor(Scene scene, IVisualNode node) - { - var parent = node.Parent; - - while (parent!.Visual.VisualRoot == null) - { - node = parent; - parent = node.Parent; - } - - return (VisualNode)node; - } - - private static object GetOrCreateChildNode(Scene scene, IVisual child, VisualNode parent) - { - var result = (VisualNode?)scene.FindNode(child); - - if (result != null && result.Parent != parent) - { - Deindex(scene, result); - result = null; - } - - return result ?? CreateNode(scene, child, parent); - } - - private static void Update(DrawingContext context, Scene scene, VisualNode node, Rect clip, bool forceRecurse) - { - var visual = node.Visual; - var opacity = visual.Opacity; - var clipToBounds = visual.ClipToBounds; -#pragma warning disable CS0618 // Type or member is obsolete - var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ? - roundRectClip.ClipToBoundsRadius : - default; -#pragma warning restore CS0618 // Type or member is obsolete - - var bounds = new Rect(visual.Bounds.Size); - var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; - - contextImpl.Layers.Find(node.LayerRoot!)?.Dirty.Add(node.Bounds); - - if (visual.IsVisible) - { - var m = node != scene.Root ? - Matrix.CreateTranslation(visual.Bounds.Position) : - Matrix.Identity; - - var renderTransform = Matrix.Identity; - - if (visual.RenderTransform != null) - { - var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); - var offset = Matrix.CreateTranslation(origin); - renderTransform = (-offset) * visual.RenderTransform.Value * (offset); - } - - m = renderTransform * m; - - using (contextImpl.BeginUpdate(node)) - using (context.PushPostTransform(m)) - using (context.PushTransformContainer()) - { - var globalBounds = bounds.TransformToAABB(contextImpl.Transform); - var clipBounds = clipToBounds ? - globalBounds.Intersect(clip) : - clip; - - forceRecurse = forceRecurse || - node.ClipBounds != clipBounds || - node.Opacity != opacity || - node.Transform != contextImpl.Transform; - - node.Transform = contextImpl.Transform; - node.ClipBounds = clipBounds; - node.ClipToBounds = clipToBounds; - node.LayoutBounds = globalBounds; - node.ClipToBoundsRadius = clipToBoundsRadius; - node.GeometryClip = visual.Clip?.PlatformImpl; - node.Opacity = opacity; - - // TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning. - node.OpacityMask = visual.OpacityMask?.ToImmutable(); - - if (ShouldStartLayer(visual)) - { - if (node.LayerRoot != visual) - { - MakeLayer(scene, node); - } - else - { - UpdateLayer(node, scene.Layers[node.LayerRoot]); - } - } - else if (node.LayerRoot == node.Visual && node.Parent != null) - { - ClearLayer(scene, node); - } - - if (node.ClipToBounds) - { - clip = clip.Intersect(node.ClipBounds); - } - - try - { - visual.Render(context); - } - catch { } - - var transformed = new TransformedBounds(new Rect(visual.Bounds.Size), clip, node.Transform); - visual.TransformedBounds = transformed; - - if (forceRecurse) - { - var visualChildren = (IList) visual.VisualChildren; - - node.TryPreallocateChildren(visualChildren.Count); - - if (visualChildren.Count == 1) - { - var childNode = GetOrCreateChildNode(scene, visualChildren[0], node); - Update(context, scene, (VisualNode)childNode, clip, forceRecurse); - } - else if (visualChildren.Count > 1) - { - var count = visualChildren.Count; - var sortedChildren = new (IVisual visual, int index)[count]; - - for (var i = 0; i < count; i++) - { - sortedChildren[i] = (visualChildren[i], i); - } - - // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. - Array.Sort(sortedChildren, (lhs, rhs) => - { - var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); - - return result == 0 ? lhs.index.CompareTo(rhs.index) : result; - }); - - foreach (var child in sortedChildren) - { - var childNode = GetOrCreateChildNode(scene, child.Item1, node); - Update(context, scene, (VisualNode)childNode, clip, forceRecurse); - } - } - - node.SubTreeUpdated = true; - contextImpl.TrimChildren(); - } - } - } - else - { - contextImpl.BeginUpdate(node).Dispose(); - } - } - - private void UpdateSize(Scene scene) - { - var renderRoot = scene.Root.Visual as IRenderRoot; - var newSize = renderRoot?.ClientSize ?? scene.Root.Visual.Bounds.Size; - - scene.Scaling = renderRoot?.RenderScaling ?? 1; - - if (scene.Size != newSize) - { - var oldSize = scene.Size; - - scene.Size = newSize; - - Rect horizontalDirtyRect = Rect.Empty; - Rect verticalDirtyRect = Rect.Empty; - - if (newSize.Width > oldSize.Width) - { - horizontalDirtyRect = new Rect(oldSize.Width, 0, newSize.Width - oldSize.Width, oldSize.Height); - } - - if (newSize.Height > oldSize.Height) - { - verticalDirtyRect = new Rect(0, oldSize.Height, newSize.Width, newSize.Height - oldSize.Height); - } - - foreach (var layer in scene.Layers) - { - layer.Dirty.Add(horizontalDirtyRect); - layer.Dirty.Add(verticalDirtyRect); - } - } - } - - private static VisualNode CreateNode(Scene scene, IVisual visual, VisualNode parent) - { - var node = new VisualNode(visual, parent); - node.LayerRoot = parent.LayerRoot; - scene.Add(node); - return node; - } - - private static void Deindex(Scene scene, VisualNode node) - { - var nodeChildren = node.Children; - var nodeChildrenCount = nodeChildren.Count; - - for (var i = 0; i < nodeChildrenCount; i++) - { - if (nodeChildren[i] is VisualNode visual) - { - Deindex(scene, visual); - } - } - - scene.Remove(node); - - node.SubTreeUpdated = true; - - scene.Layers[node.LayerRoot!].Dirty.Add(node.Bounds); - - node.Visual.TransformedBounds = null; - - if (node.LayerRoot == node.Visual && node.Visual != scene.Root.Visual) - { - scene.Layers.Remove(node.LayerRoot); - } - } - - private static void ClearLayer(Scene scene, VisualNode node) - { - var parent = (VisualNode)node.Parent!; - var oldLayerRoot = node.LayerRoot; - var newLayerRoot = parent.LayerRoot!; - var existingDirtyRects = scene.Layers[node.LayerRoot!].Dirty; - var newDirtyRects = scene.Layers[newLayerRoot].Dirty; - - existingDirtyRects.Coalesce(); - - foreach (var r in existingDirtyRects) - { - newDirtyRects.Add(r); - } - - var oldLayer = scene.Layers[oldLayerRoot!]; - PropagateLayer(node, scene.Layers[newLayerRoot], oldLayer); - scene.Layers.Remove(oldLayer); - } - - private static void MakeLayer(Scene scene, VisualNode node) - { - var oldLayerRoot = node.LayerRoot!; - var layer = scene.Layers.Add(node.Visual); - var oldLayer = scene.Layers[oldLayerRoot!]; - - UpdateLayer(node, layer); - PropagateLayer(node, layer, scene.Layers[oldLayerRoot]); - } - - private static void UpdateLayer(VisualNode node, SceneLayer layer) - { - layer.Opacity = node.Visual.Opacity; - - if (node.Visual.OpacityMask != null) - { - layer.OpacityMask = node.Visual.OpacityMask?.ToImmutable(); - layer.OpacityMaskRect = node.ClipBounds; - } - else - { - layer.OpacityMask = null; - layer.OpacityMaskRect = Rect.Empty; - } - - layer.GeometryClip = node.HasAncestorGeometryClip ? - CreateLayerGeometryClip(node) : - null; - } - - private static void PropagateLayer(VisualNode node, SceneLayer layer, SceneLayer oldLayer) - { - node.LayerRoot = layer.LayerRoot; - - layer.Dirty.Add(node.Bounds); - oldLayer.Dirty.Add(node.Bounds); - - foreach (VisualNode child in node.Children) - { - // If the child is not the start of a new layer, recurse. - if (child.LayerRoot != child.Visual) - { - PropagateLayer(child, layer, oldLayer); - } - } - } - - // HACK: Disabled layers because they're broken in current renderer. See #2244. - private static bool ShouldStartLayer(IVisual visual) => false; - - private static IGeometryImpl? CreateLayerGeometryClip(VisualNode node) - { - IGeometryImpl? result = null; - VisualNode? n = node; - - for (;;) - { - n = (VisualNode?)n!.Parent; - - if (n == null || (n.GeometryClip == null && !n.HasAncestorGeometryClip)) - { - break; - } - - if (n?.GeometryClip != null) - { - var transformed = n.GeometryClip.WithTransform(n.Transform); - - result = result == null ? transformed : result.Intersect(transformed); - } - } - - return result; - } - } -} diff --git a/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs deleted file mode 100644 index ee85d1e876..0000000000 --- a/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Avalonia.Utilities -{ - /// - /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region. - /// - /// The type of elements in the slice. - [DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))] - public readonly struct ReadOnlySlice : IReadOnlyList where T : struct - { - private readonly int _offset; - - /// - /// Gets an empty - /// - public static ReadOnlySlice Empty => new ReadOnlySlice(Array.Empty()); - - private readonly ReadOnlyMemory _buffer; - - public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { } - - public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length, int offset = 0) - { -#if DEBUG - if (start.CompareTo(0) < 0) - { - throw new ArgumentOutOfRangeException(nameof (start)); - } - - if (length.CompareTo(buffer.Length) > 0) - { - throw new ArgumentOutOfRangeException(nameof (length)); - } -#endif - - _buffer = buffer; - Start = start; - Length = length; - _offset = offset; - } - - /// - /// Gets the start. - /// - /// - /// The start. - /// - public int Start { get; } - - /// - /// Gets the end. - /// - /// - /// The end. - /// - public int End => Start + Length - 1; - - /// - /// Gets the length. - /// - /// - /// The length. - /// - public int Length { get; } - - /// - /// Gets a value that indicates whether this instance of is Empty. - /// - public bool IsEmpty => Length == 0; - - /// - /// The underlying span. - /// - public ReadOnlySpan Span => _buffer.Span.Slice(_offset, Length); - - /// - /// The underlying buffer. - /// - public ReadOnlyMemory Buffer => _buffer; - - /// - /// Returns a value to specified element of the slice. - /// - /// The index of the element to return. - /// The . - /// - /// Thrown when index less than 0 or index greater than or equal to . - /// - public T this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { -#if DEBUG - if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) - { - throw new ArgumentOutOfRangeException(nameof (index)); - } -#endif - return Span[index]; - } - } - - /// - /// Returns a sub slice of elements that start at the specified index and has the specified number of elements. - /// - /// The start of the sub slice. - /// The length of the sub slice. - /// A that contains the specified number of elements from the specified start. - public ReadOnlySlice AsSlice(int start, int length) - { - if (IsEmpty) - { - return this; - } - - if (length == 0) - { - return Empty; - } - - if (start < 0 || _offset + start > _buffer.Length - 1) - { - throw new ArgumentOutOfRangeException(nameof(start)); - } - - if (_offset + start + length > _buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, start, length, _offset); - } - - /// - /// Returns a specified number of contiguous elements from the start of the slice. - /// - /// The number of elements to return. - /// A that contains the specified number of elements from the start of this slice. - public ReadOnlySlice Take(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, Start, length, _offset); - } - - /// - /// Bypasses a specified number of elements in the slice and then returns the remaining elements. - /// - /// The number of elements to skip before returning the remaining elements. - /// A that contains the elements that occur after the specified index in this slice. - public ReadOnlySlice Skip(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, Start + length, Length - length, _offset + length); - } - - /// - /// Returns an enumerator for the slice. - /// - public ImmutableReadOnlyListStructEnumerator GetEnumerator() - { - return new ImmutableReadOnlyListStructEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - int IReadOnlyCollection.Count => Length; - - T IReadOnlyList.this[int index] => this[index]; - - public static implicit operator ReadOnlySlice(T[] array) - { - return new ReadOnlySlice(array); - } - - public static implicit operator ReadOnlySlice(ReadOnlyMemory memory) - { - return new ReadOnlySlice(memory); - } - - internal class ReadOnlySliceDebugView - { - private readonly ReadOnlySlice _readOnlySlice; - - public ReadOnlySliceDebugView(ReadOnlySlice readOnlySlice) - { - _readOnlySlice = readOnlySlice; - } - - public int Start => _readOnlySlice.Start; - - public int End => _readOnlySlice.End; - - public int Length => _readOnlySlice.Length; - - public bool IsEmpty => _readOnlySlice.IsEmpty; - - public ReadOnlySpan Items => _readOnlySlice.Span; - } - } -} diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs deleted file mode 100644 index 324b253a0f..0000000000 --- a/src/Avalonia.Visuals/Visual.cs +++ /dev/null @@ -1,649 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Specialized; -using Avalonia.Collections; -using Avalonia.Data; -using Avalonia.Logging; -using Avalonia.LogicalTree; -using Avalonia.Media; -using Avalonia.Metadata; -using Avalonia.Rendering; -using Avalonia.Utilities; -using Avalonia.VisualTree; - -#nullable enable - -namespace Avalonia -{ - /// - /// Base class for controls that provides rendering and related visual properties. - /// - /// - /// The class represents elements that have a visual on-screen - /// representation and stores all the information needed for an - /// to render the control. To traverse the visual tree, use the - /// extension methods defined in . - /// - [UsableDuringInitialization] - public class Visual : StyledElement, IVisual - { - /// - /// Defines the property. - /// - public static readonly DirectProperty BoundsProperty = - AvaloniaProperty.RegisterDirect(nameof(Bounds), o => o.Bounds); - - public static readonly DirectProperty TransformedBoundsProperty = - AvaloniaProperty.RegisterDirect( - nameof(TransformedBounds), - o => o.TransformedBounds); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ClipToBoundsProperty = - AvaloniaProperty.Register(nameof(ClipToBounds)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ClipProperty = - AvaloniaProperty.Register(nameof(Clip)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty IsVisibleProperty = - AvaloniaProperty.Register(nameof(IsVisible), true); - - /// - /// Defines the property. - /// - public static readonly StyledProperty OpacityProperty = - AvaloniaProperty.Register(nameof(Opacity), 1); - - /// - /// Defines the property. - /// - public static readonly StyledProperty OpacityMaskProperty = - AvaloniaProperty.Register(nameof(OpacityMask)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty RenderTransformProperty = - AvaloniaProperty.Register(nameof(RenderTransform)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty RenderTransformOriginProperty = - AvaloniaProperty.Register(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center); - - /// - /// Defines the property. - /// - public static readonly DirectProperty VisualParentProperty = - AvaloniaProperty.RegisterDirect(nameof(IVisual.VisualParent), o => o._visualParent); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ZIndexProperty = - AvaloniaProperty.Register(nameof(ZIndex)); - - private Rect _bounds; - private TransformedBounds? _transformedBounds; - private IRenderRoot? _visualRoot; - private IVisual? _visualParent; - - /// - /// Initializes static members of the class. - /// - static Visual() - { - AffectsRender( - BoundsProperty, - ClipProperty, - ClipToBoundsProperty, - IsVisibleProperty, - OpacityProperty); - RenderTransformProperty.Changed.Subscribe(RenderTransformChanged); - ZIndexProperty.Changed.Subscribe(ZIndexChanged); - } - - /// - /// Initializes a new instance of the class. - /// - public Visual() - { - // Disable transitions until we're added to the visual tree. - DisableTransitions(); - - var visualChildren = new AvaloniaList(); - visualChildren.ResetBehavior = ResetBehavior.Remove; - visualChildren.Validate = visual => ValidateVisualChild(visual); - visualChildren.CollectionChanged += VisualChildrenChanged; - VisualChildren = visualChildren; - } - - /// - /// Raised when the control is attached to a rooted visual tree. - /// - public event EventHandler? AttachedToVisualTree; - - /// - /// Raised when the control is detached from a rooted visual tree. - /// - public event EventHandler? DetachedFromVisualTree; - - /// - /// Gets the bounds of the control relative to its parent. - /// - public Rect Bounds - { - get { return _bounds; } - protected set { SetAndRaise(BoundsProperty, ref _bounds, value); } - } - - /// - /// Gets the bounds of the control relative to the window, accounting for rendering transforms. - /// - public TransformedBounds? TransformedBounds => _transformedBounds; - - /// - /// Gets or sets a value indicating whether the control should be clipped to its bounds. - /// - public bool ClipToBounds - { - get { return GetValue(ClipToBoundsProperty); } - set { SetValue(ClipToBoundsProperty, value); } - } - - /// - /// Gets or sets the geometry clip for this visual. - /// - public Geometry? Clip - { - get { return GetValue(ClipProperty); } - set { SetValue(ClipProperty, value); } - } - - /// - /// Gets a value indicating whether this control and all its parents are visible. - /// - public bool IsEffectivelyVisible - { - get - { - IVisual? node = this; - - while (node != null) - { - if (!node.IsVisible) - { - return false; - } - - node = node.VisualParent; - } - - return true; - } - } - - /// - /// Gets or sets a value indicating whether this control is visible. - /// - public bool IsVisible - { - get { return GetValue(IsVisibleProperty); } - set { SetValue(IsVisibleProperty, value); } - } - - /// - /// Gets or sets the opacity of the control. - /// - public double Opacity - { - get { return GetValue(OpacityProperty); } - set { SetValue(OpacityProperty, value); } - } - - /// - /// Gets or sets the opacity mask of the control. - /// - public IBrush? OpacityMask - { - get { return GetValue(OpacityMaskProperty); } - set { SetValue(OpacityMaskProperty, value); } - } - - /// - /// Gets or sets the render transform of the control. - /// - public ITransform? RenderTransform - { - get { return GetValue(RenderTransformProperty); } - set { SetValue(RenderTransformProperty, value); } - } - - /// - /// Gets or sets the transform origin of the control. - /// - public RelativePoint RenderTransformOrigin - { - get { return GetValue(RenderTransformOriginProperty); } - set { SetValue(RenderTransformOriginProperty, value); } - } - - /// - /// Gets or sets the Z index of the control. - /// - /// - /// Controls with a higher will appear in front of controls with - /// a lower ZIndex. If two controls have the same ZIndex then the control that appears - /// later in the containing element's children collection will appear on top. - /// - public int ZIndex - { - get { return GetValue(ZIndexProperty); } - set { SetValue(ZIndexProperty, value); } - } - - /// - /// Gets the control's child visuals. - /// - protected IAvaloniaList VisualChildren - { - get; - private set; - } - - /// - /// Gets the root of the visual tree, if the control is attached to a visual tree. - /// - protected IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot); - - /// - /// Gets a value indicating whether this control is attached to a visual root. - /// - bool IVisual.IsAttachedToVisualTree => VisualRoot != null; - - /// - /// Gets the control's child controls. - /// - IAvaloniaReadOnlyList IVisual.VisualChildren => VisualChildren; - - /// - /// Gets the control's parent visual. - /// - IVisual? IVisual.VisualParent => _visualParent; - - /// - /// Gets the root of the visual tree, if the control is attached to a visual tree. - /// - IRenderRoot? IVisual.VisualRoot => VisualRoot; - - TransformedBounds? IVisual.TransformedBounds - { - get { return _transformedBounds; } - set { SetAndRaise(TransformedBoundsProperty, ref _transformedBounds, value); } - } - - /// - /// Invalidates the visual and queues a repaint. - /// - public void InvalidateVisual() - { - VisualRoot?.Renderer?.AddDirty(this); - } - - /// - /// Renders the visual to a . - /// - /// The drawing context. - public virtual void Render(DrawingContext context) - { - Contract.Requires(context != null); - } - - /// - /// Indicates that a property change should cause to be - /// called. - /// - /// The properties. - /// - /// This method should be called in a control's static constructor with each property - /// on the control which when changed should cause a redraw. This is similar to WPF's - /// FrameworkPropertyMetadata.AffectsRender flag. - /// - [Obsolete("Use AffectsRender and specify the control type.")] - protected static void AffectsRender(params AvaloniaProperty[] properties) - { - AffectsRender(properties); - } - - /// - /// Indicates that a property change should cause to be - /// called. - /// - /// The control which the property affects. - /// The properties. - /// - /// This method should be called in a control's static constructor with each property - /// on the control which when changed should cause a redraw. This is similar to WPF's - /// FrameworkPropertyMetadata.AffectsRender flag. - /// - protected static void AffectsRender(params AvaloniaProperty[] properties) - where T : Visual - { - static void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is T sender) - { - sender.InvalidateVisual(); - } - } - - static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is T sender) - { - if (e.OldValue is IAffectsRender oldValue) - { - WeakEventHandlerManager.Unsubscribe(oldValue, nameof(oldValue.Invalidated), sender.AffectsRenderInvalidated); - } - - if (e.NewValue is IAffectsRender newValue) - { - WeakEventHandlerManager.Subscribe(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated); - } - - sender.InvalidateVisual(); - } - } - - foreach (var property in properties) - { - if (property.CanValueAffectRender()) - { - property.Changed.Subscribe(e => InvalidateAndSubscribe(e)); - } - else - { - property.Changed.Subscribe(e => Invalidate(e)); - } - } - } - - protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - base.LogicalChildrenCollectionChanged(sender, e); - VisualRoot?.Renderer?.RecalculateChildren(this); - } - - /// - /// Calls the method - /// for this control and all of its visual descendants. - /// - /// The event args. - protected virtual void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Attached to visual tree"); - - _visualRoot = e.Root; - - if (RenderTransform is IMutableTransform mutableTransform) - { - mutableTransform.Changed += RenderTransformChanged; - } - - EnableTransitions(); - OnAttachedToVisualTree(e); - AttachedToVisualTree?.Invoke(this, e); - InvalidateVisual(); - - var visualChildren = VisualChildren; - - if (visualChildren != null) - { - var visualChildrenCount = visualChildren.Count; - - for (var i = 0; i < visualChildrenCount; i++) - { - if (visualChildren[i] is Visual child) - { - child.OnAttachedToVisualTreeCore(e); - } - } - } - } - - /// - /// Calls the method - /// for this control and all of its visual descendants. - /// - /// The event args. - protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Detached from visual tree"); - - _visualRoot = null; - - if (RenderTransform is IMutableTransform mutableTransform) - { - mutableTransform.Changed -= RenderTransformChanged; - } - - DisableTransitions(); - OnDetachedFromVisualTree(e); - DetachedFromVisualTree?.Invoke(this, e); - e.Root?.Renderer?.AddDirty(this); - - var visualChildren = VisualChildren; - - if (visualChildren != null) - { - var visualChildrenCount = visualChildren.Count; - - for (var i = 0; i < visualChildrenCount; i++) - { - if (visualChildren[i] is Visual child) - { - child.OnDetachedFromVisualTreeCore(e); - } - } - } - } - - /// - /// Called when the control is added to a rooted visual tree. - /// - /// The event args. - protected virtual void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - } - - /// - /// Called when the control is removed from a rooted visual tree. - /// - /// The event args. - protected virtual void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - } - - /// - /// Called when the control's visual parent changes. - /// - /// The old visual parent. - /// The new visual parent. - protected virtual void OnVisualParentChanged(IVisual? oldParent, IVisual? newParent) - { - RaisePropertyChanged( - VisualParentProperty, - new Optional(oldParent), - new BindingValue(newParent), - BindingPriority.LocalValue); - } - - protected internal sealed override void LogBindingError(AvaloniaProperty property, Exception e) - { - // Don't log a binding error unless the control is attached to a logical tree. - if (((ILogical)this).IsAttachedToLogicalTree) - { - if (e is BindingChainException b && - string.IsNullOrEmpty(b.ExpressionErrorPoint) && - DataContext == null) - { - // The error occurred at the root of the binding chain and DataContext is null; - // don't log this - the DataContext probably hasn't been set up yet. - return; - } - - Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log( - this, - "Error in binding to {Target}.{Property}: {Message}", - this, - property, - e.Message); - } - } - - /// - /// Called when a visual's changes. - /// - /// The event args. - private static void RenderTransformChanged(AvaloniaPropertyChangedEventArgs e) - { - var sender = e.Sender as Visual; - - if (sender?.VisualRoot != null) - { - var oldValue = e.OldValue as Transform; - var newValue = e.NewValue as Transform; - - if (oldValue != null) - { - oldValue.Changed -= sender.RenderTransformChanged; - } - - if (newValue != null) - { - newValue.Changed += sender.RenderTransformChanged; - } - - sender.InvalidateVisual(); - } - } - - /// - /// Ensures a visual child is not null and not already parented. - /// - /// The visual child. - private static void ValidateVisualChild(IVisual c) - { - if (c == null) - { - throw new ArgumentNullException(nameof(c), "Cannot add null to VisualChildren."); - } - - if (c.VisualParent != null) - { - throw new InvalidOperationException("The control already has a visual parent."); - } - } - - /// - /// Called when the property changes on any control. - /// - /// The event args. - private static void ZIndexChanged(AvaloniaPropertyChangedEventArgs e) - { - var sender = e.Sender as IVisual; - var parent = sender?.VisualParent; - sender?.InvalidateVisual(); - parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); - } - - /// - /// Called when the 's event - /// is fired. - /// - /// The sender. - /// The event args. - private void RenderTransformChanged(object? sender, EventArgs e) - { - InvalidateVisual(); - } - - /// - /// Sets the visual parent of the Visual. - /// - /// The visual parent. - private void SetVisualParent(Visual? value) - { - if (_visualParent == value) - { - return; - } - - var old = _visualParent; - _visualParent = value; - - if (_visualRoot != null) - { - var e = new VisualTreeAttachmentEventArgs(old!, _visualRoot); - OnDetachedFromVisualTreeCore(e); - } - - if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) - { - var root = this.FindAncestorOfType() ?? - throw new AvaloniaInternalException("Visual is atached to visual tree but root could not be found."); - var e = new VisualTreeAttachmentEventArgs(_visualParent, root); - OnAttachedToVisualTreeCore(e); - } - - OnVisualParentChanged(old, value); - } - - private void AffectsRenderInvalidated(object? sender, EventArgs e) => InvalidateVisual(); - - /// - /// Called when the collection changes. - /// - /// The sender. - /// The event args. - private void VisualChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - SetVisualParent(e.NewItems!, this); - break; - - case NotifyCollectionChangedAction.Remove: - SetVisualParent(e.OldItems!, null); - break; - - case NotifyCollectionChangedAction.Replace: - SetVisualParent(e.OldItems!, null); - SetVisualParent(e.NewItems!, this); - break; - } - } - - private static void SetVisualParent(IList children, Visual? parent) - { - var count = children.Count; - - for (var i = 0; i < count; i++) - { - var visual = (Visual) children[i]!; - - visual.SetVisualParent(parent); - } - } - } -} diff --git a/src/Avalonia.Visuals/VisualExtensions.cs b/src/Avalonia.Visuals/VisualExtensions.cs deleted file mode 100644 index ff8a515db3..0000000000 --- a/src/Avalonia.Visuals/VisualExtensions.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using Avalonia.VisualTree; - -namespace Avalonia -{ - /// - /// Extension methods for . - /// - public static class VisualExtensions - { - /// - /// Converts a point from screen to client coordinates. - /// - /// The visual. - /// The point in screen coordinates. - /// The point in client coordinates. - public static Point PointToClient(this IVisual visual, PixelPoint point) - { - var root = visual.VisualRoot ?? - throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); - var rootPoint = root.PointToClient(point); - return root.TranslatePoint(rootPoint, visual)!.Value; - } - - /// - /// Converts a point from client to screen coordinates. - /// - /// The visual. - /// The point in client coordinates. - /// The point in screen coordinates. - public static PixelPoint PointToScreen(this IVisual visual, Point point) - { - var root = visual.VisualRoot ?? - throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); - var p = visual.TranslatePoint(point, root); - return visual.VisualRoot.PointToScreen(p!.Value); - } - - /// - /// Returns a transform that transforms the visual's coordinates into the coordinates - /// of the specified . - /// - /// The visual whose coordinates are to be transformed. - /// The visual to translate the coordinates to. - /// - /// A containing the transform or null if the visuals don't share a - /// common ancestor. - /// - public static Matrix? TransformToVisual(this IVisual from, IVisual to) - { - var common = from.FindCommonVisualAncestor(to); - - if (common != null) - { - var thisOffset = GetOffsetFrom(common, from); - var thatOffset = GetOffsetFrom(common, to); - - if (!thatOffset.TryInvert(out var thatOffsetInverted)) - { - return null; - } - - return thatOffsetInverted * thisOffset; - } - - return null; - } - - /// - /// Translates a point relative to this visual to coordinates that are relative to the specified visual. - /// - /// The visual. - /// The point value, as relative to this visual. - /// The visual to translate the given point into. - /// - /// A point value, now relative to the target visual rather than this source element, or null if the - /// two elements have no common ancestor. - /// - public static Point? TranslatePoint(this IVisual visual, Point point, IVisual relativeTo) - { - var transform = visual.TransformToVisual(relativeTo); - - if (transform.HasValue) - { - return point.Transform(transform.Value); - } - - return null; - } - - /// - /// Gets a transform from an ancestor to a descendent. - /// - /// The ancestor visual. - /// The visual. - /// The transform. - private static Matrix GetOffsetFrom(IVisual ancestor, IVisual visual) - { - var result = Matrix.Identity; - IVisual? v = visual; - - while (v != ancestor) - { - if (v.RenderTransform?.Value != null) - { - var origin = v.RenderTransformOrigin.ToPixels(v.Bounds.Size); - var offset = Matrix.CreateTranslation(origin); - var renderTransform = (-offset) * v.RenderTransform.Value * (offset); - - result *= renderTransform; - } - - var topLeft = v.Bounds.TopLeft; - - if (topLeft != default) - { - result *= Matrix.CreateTranslation(topLeft); - } - - v = v.VisualParent; - - if (v == null) - { - throw new ArgumentException("'visual' is not a descendant of 'ancestor'."); - } - } - - return result; - } - } -} diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs deleted file mode 100644 index 97c4554de6..0000000000 --- a/src/Avalonia.Visuals/VisualTree/IVisual.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using Avalonia.Collections; -using Avalonia.Media; -using Avalonia.Rendering; - -#nullable enable - -namespace Avalonia.VisualTree -{ - /// - /// Represents control that has a visual on-screen representation. - /// - /// - /// The interface defines the interface required for a renderer to - /// render a control. You should not usually need to reference this interface unless - /// you are writing a renderer; instead use the extension methods defined in - /// to traverse the visual tree. This interface is - /// implemented by . It should not be necessary to implement it - /// anywhere else. - /// - public interface IVisual - { - /// - /// Raised when the control is attached to a rooted visual tree. - /// - event EventHandler? AttachedToVisualTree; - - /// - /// Raised when the control is detached from a rooted visual tree. - /// - event EventHandler? DetachedFromVisualTree; - - /// - /// Gets the bounds of the control relative to its parent. - /// - Rect Bounds { get; } - - /// - /// Gets the bounds of the control relative to the window, accounting for rendering transforms. - /// - TransformedBounds? TransformedBounds { get; set; } - - /// - /// Gets a value indicating whether the control should be clipped to its bounds. - /// - bool ClipToBounds { get; set; } - - /// - /// Gets or sets the geometry clip for this visual. - /// - Geometry? Clip { get; set; } - - /// - /// Gets a value indicating whether this control is attached to a visual root. - /// - bool IsAttachedToVisualTree { get; } - - /// - /// Gets a value indicating whether this control and all its parents are visible. - /// - bool IsEffectivelyVisible { get; } - - /// - /// Gets or sets a value indicating whether this control is visible. - /// - bool IsVisible { get; set; } - - /// - /// Gets or sets the opacity of the control. - /// - double Opacity { get; set; } - - /// - /// Gets or sets the opacity mask for the control. - /// - IBrush? OpacityMask { get; set; } - - /// - /// Gets or sets the render transform of the control. - /// - ITransform? RenderTransform { get; set; } - - /// - /// Gets or sets the render transform origin of the control. - /// - RelativePoint RenderTransformOrigin { get; set; } - - /// - /// Gets the control's child visuals. - /// - IAvaloniaReadOnlyList VisualChildren { get; } - - /// - /// Gets the control's parent visual. - /// - IVisual? VisualParent { get; } - - /// - /// Gets the root of the visual tree, if the control is attached to a visual tree. - /// - IRenderRoot? VisualRoot { get; } - - /// - /// Gets or sets the Z index of the node. - /// - int ZIndex { get; set; } - - /// - /// Invalidates the visual and queues a repaint. - /// - void InvalidateVisual(); - - /// - /// Renders the control to a . - /// - /// The context. - void Render(DrawingContext context); - } -} diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index fcdc10e999..1e70608168 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -9,7 +9,7 @@ namespace Avalonia.X11.Glx unsafe class GlxDisplay { private readonly X11Info _x11; - private readonly List _probeProfiles; + private readonly GlVersion[] _probeProfiles; private readonly IntPtr _fbconfig; private readonly XVisualInfo* _visual; private string[] _displayExtensions; @@ -21,7 +21,7 @@ namespace Avalonia.X11.Glx public GlxDisplay(X11Info x11, IList probeProfiles) { _x11 = x11; - _probeProfiles = probeProfiles.ToList(); + _probeProfiles = probeProfiles.ToArray(); _displayExtensions = Glx.GetExtensions(_x11.Display); var baseAttribs = new[] @@ -76,10 +76,10 @@ namespace Avalonia.X11.Glx if (Glx.GetFBConfigAttrib(_x11.Display, _fbconfig, GLX_STENCIL_SIZE, out var stencil) == 0) stencilSize = stencil; - var pbuffers = Enumerable.Range(0, 2).Select(_ => Glx.CreatePbuffer(_x11.Display, _fbconfig, new[] - { - GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 - })).ToList(); + var attributes = new[] { GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 }; + + Glx.CreatePbuffer(_x11.Display, _fbconfig, attributes); + Glx.CreatePbuffer(_x11.Display, _fbconfig, attributes); XLib.XFlush(_x11.Display); @@ -104,7 +104,6 @@ namespace Avalonia.X11.Glx $"Renderer '{glInterface.Renderer}' is blacklisted by '{item}'"); } } - } IntPtr CreatePBuffer() diff --git a/src/Avalonia.X11/ICELib.cs b/src/Avalonia.X11/ICELib.cs index 8ef21dd000..19c973a5e0 100644 --- a/src/Avalonia.X11/ICELib.cs +++ b/src/Avalonia.X11/ICELib.cs @@ -46,7 +46,7 @@ namespace Avalonia.X11 IntPtr iceConn, bool swap, int offendingMinorOpcode, - ulong offendingSequence, + nuint offendingSequence, int errorClass, int severity, IntPtr values diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index 77e410162f..a0f146afa8 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -63,7 +63,7 @@ namespace Avalonia.X11.NativeDialogs public static IDisposable ConnectSignal(IntPtr obj, string name, T handler) { var handle = GCHandle.Alloc(handler); - var ptr = Marshal.GetFunctionPointerForDelegate((Delegate)(object)handler); + var ptr = Marshal.GetFunctionPointerForDelegate(handler); using (var utf = new Utf8Buffer(name)) { var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0); @@ -179,6 +179,9 @@ namespace Avalonia.X11.NativeDialogs [DllImport(GtkName)] public static extern void gtk_file_chooser_set_select_multiple(IntPtr chooser, bool allow); + + [DllImport(GtkName)] + public static extern void gtk_file_chooser_set_do_overwrite_confirmation(IntPtr chooser, bool do_overwrite_confirmation); [DllImport(GtkName)] public static extern void diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index 6cf6c6f35f..1a6514eb03 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -16,7 +16,7 @@ namespace Avalonia.X11.NativeDialogs { private Task _initialized; private unsafe Task ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action, - bool multiSelect, string initialFileName, IEnumerable filters, string defaultExtension) + bool multiSelect, string initialFileName, IEnumerable filters, string defaultExtension, bool overwritePrompt) { IntPtr dlg; using (var name = new Utf8Buffer(title)) @@ -109,6 +109,8 @@ namespace Avalonia.X11.NativeDialogs gtk_file_chooser_set_filename(dlg, fn); } + gtk_file_chooser_set_do_overwrite_confirmation(dlg, overwritePrompt); + gtk_window_present(dlg); return tcs.Task; } @@ -148,7 +150,7 @@ namespace Avalonia.X11.NativeDialogs (dialog as OpenFileDialog)?.AllowMultiple ?? false, Path.Combine(string.IsNullOrEmpty(dialog.Directory) ? "" : dialog.Directory, string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters, - (dialog as SaveFileDialog)?.DefaultExtension)); + (dialog as SaveFileDialog)?.DefaultExtension, (dialog as SaveFileDialog)?.ShowOverwritePrompt ?? false)); } public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) @@ -160,7 +162,7 @@ namespace Avalonia.X11.NativeDialogs return await await RunOnGlibThread(async () => { var res = await ShowDialog(dialog.Title, platformImpl, - GtkFileChooserAction.SelectFolder, false, dialog.Directory, null, null); + GtkFileChooserAction.SelectFolder, false, dialog.Directory, null, null, false); return res?.FirstOrDefault(); }); } diff --git a/src/Avalonia.X11/SMLib.cs b/src/Avalonia.X11/SMLib.cs index 6ffcad60bf..f8f13e32f8 100644 --- a/src/Avalonia.X11/SMLib.cs +++ b/src/Avalonia.X11/SMLib.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Runtime.InteropServices; @@ -7,20 +8,19 @@ namespace Avalonia.X11 { private const string LibSm = "libSM.so.6"; - [DllImport(LibSm, CharSet = CharSet.Ansi)] + [DllImport(LibSm, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] public static extern IntPtr SmcOpenConnection( - [MarshalAs(UnmanagedType.LPWStr)] string networkId, + [MarshalAs(UnmanagedType.LPStr)] string? networkId, IntPtr content, int xsmpMajorRev, int xsmpMinorRev, - ulong mask, + nuint mask, ref SmcCallbacks callbacks, - [MarshalAs(UnmanagedType.LPWStr)] [Out] - out string previousId, - [MarshalAs(UnmanagedType.LPWStr)] [Out] - out string clientIdRet, + [MarshalAs(UnmanagedType.LPStr)] string? previousId, + ref IntPtr clientIdRet, int errorLength, - [Out] char[] errorStringRet); + [Out] byte[] errorStringRet + ); [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] public static extern int SmcCloseConnection( @@ -124,7 +124,7 @@ namespace Avalonia.X11 IntPtr smcConn, bool swap, int offendingMinorOpcode, - ulong offendingSequence, + nuint offendingSequence, int errorClass, int severity, IntPtr values diff --git a/src/Avalonia.X11/X11Globals.cs b/src/Avalonia.X11/X11Globals.cs index 057693f810..39834a44b3 100644 --- a/src/Avalonia.X11/X11Globals.cs +++ b/src/Avalonia.X11/X11Globals.cs @@ -41,7 +41,7 @@ namespace Avalonia.X11 { _wmName = value; // The collection might change during enumeration - foreach (var s in _subscribers.ToList()) + foreach (var s in _subscribers.ToArray()) s.WmChanged(value); } } @@ -69,7 +69,7 @@ namespace Avalonia.X11 { _isCompositionEnabled = value; // The collection might change during enumeration - foreach (var s in _subscribers.ToList()) + foreach (var s in _subscribers.ToArray()) s.CompositionChanged(value); } } diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index 6632573ba2..36939f4103 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -2,12 +2,9 @@ using System; using System.IO; using System.Runtime.InteropServices; using Avalonia.Controls.Platform.Surfaces; -using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; -using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; -using static Avalonia.X11.XLib; + namespace Avalonia.X11 { class X11IconLoader : IPlatformIconLoader diff --git a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs index 06e1d8879f..8412bd0730 100644 --- a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs +++ b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Text; using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Threading; @@ -14,10 +15,10 @@ namespace Avalonia.X11 internal unsafe class X11PlatformLifetimeEvents : IDisposable, IPlatformLifetimeEventsImpl { private readonly AvaloniaX11Platform _platform; - private const ulong SmcSaveYourselfProcMask = 1L; - private const ulong SmcDieProcMask = 2L; - private const ulong SmcSaveCompleteProcMask = 4L; - private const ulong SmcShutdownCancelledProcMask = 8L; + private const nuint SmcSaveYourselfProcMask = 1; + private const nuint SmcDieProcMask = 2; + private const nuint SmcSaveCompleteProcMask = 4; + private const nuint SmcShutdownCancelledProcMask = 8; private static readonly ConcurrentDictionary s_nativeToManagedMapper = new ConcurrentDictionary(); @@ -62,24 +63,24 @@ namespace Avalonia.X11 return; } - var errorBuf = new char[255]; - - var smcConn = SMLib.SmcOpenConnection(null!, + byte[] errorBuf = new byte[255]; + IntPtr clientIdRet = IntPtr.Zero; + var smcConn = SMLib.SmcOpenConnection(null, IntPtr.Zero, 1, 0, SmcSaveYourselfProcMask | SmcSaveCompleteProcMask | SmcShutdownCancelledProcMask | SmcDieProcMask, ref s_callbacks, - out _, - out _, + null, + ref clientIdRet, errorBuf.Length, errorBuf); if (smcConn == IntPtr.Zero) { Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, - $"SMLib/ICELib reported a new error: {new string(errorBuf)}"); + $"SMLib/ICELib reported a new error: {Encoding.ASCII.GetString(errorBuf)}"); return; } @@ -152,14 +153,14 @@ namespace Avalonia.X11 } private static void StaticErrorHandler(IntPtr smcConn, bool swap, int offendingMinorOpcode, - ulong offendingSequence, int errorClass, int severity, IntPtr values) + nuint offendingSequence, int errorClass, int severity, IntPtr values) { GetInstance(smcConn) ?.ErrorHandler(swap, offendingMinorOpcode, offendingSequence, errorClass, severity, values); } // ReSharper disable UnusedParameter.Local - private void ErrorHandler(bool swap, int offendingMinorOpcode, ulong offendingSequence, int errorClass, + private void ErrorHandler(bool swap, int offendingMinorOpcode, nuint offendingSequence, int errorClass, int severity, IntPtr values) { Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs index bf5c74e0e5..bcaafb6a53 100644 --- a/src/Avalonia.X11/X11Screens.cs +++ b/src/Avalonia.X11/X11Screens.cs @@ -200,6 +200,21 @@ namespace Avalonia.X11 } + public Screen ScreenFromPoint(PixelPoint point) + { + return ScreenHelper.ScreenFromPoint(point, AllScreens); + } + + public Screen ScreenFromRect(PixelRect rect) + { + return ScreenHelper.ScreenFromRect(rect, AllScreens); + } + + public Screen ScreenFromWindow(IWindowBaseImpl window) + { + return ScreenHelper.ScreenFromWindow(window, AllScreens); + } + public int ScreenCount => _impl.Screens.Length; public IReadOnlyList AllScreens => diff --git a/src/Avalonia.X11/X11Window.Xim.cs b/src/Avalonia.X11/X11Window.Xim.cs index ecb23ff097..bedd1d22fc 100644 --- a/src/Avalonia.X11/X11Window.Xim.cs +++ b/src/Avalonia.X11/X11Window.Xim.cs @@ -10,7 +10,6 @@ namespace Avalonia.X11 { partial class X11Window { - class XimInputMethod : ITextInputMethodImpl, IX11InputMethodControl { private readonly X11Window _parent; @@ -58,9 +57,9 @@ namespace Avalonia.X11 UpdateActive(); } - public void SetActive(bool active) + public void SetClient(ITextInputMethodClient client) { - _controlActive = active; + _controlActive = client is { }; UpdateActive(); } @@ -87,7 +86,7 @@ namespace Avalonia.X11 // No-op } - public void SetOptions(TextInputOptionsQueryEventArgs options) + public void SetOptions(TextInputOptions options) { // No-op } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 520b35dd03..066156a652 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -134,8 +134,8 @@ namespace Avalonia.X11 SetWindowValuemask.WinGravity | SetWindowValuemask.BackingStore)), ref attr); else _renderHandle = _handle; - - Handle = new PlatformHandle(_handle, "XID"); + + Handle = new SurfacePlatformHandle(this); _realSize = new PixelSize(defaultWidth, defaultHeight); platform.Windows[_handle] = OnEvent; XEventMask ignoredMask = XEventMask.SubstructureRedirectMask @@ -169,7 +169,9 @@ namespace Avalonia.X11 if (glx != null) surfaces.Insert(0, new GlxGlPlatformSurface(glx.Display, glx.DeferredContext, new SurfaceInfo(this, _x11.Display, _handle, _renderHandle))); - + + surfaces.Add(Handle); + Surfaces = surfaces.ToArray(); UpdateMotifHints(); UpdateSizeHints(null); @@ -1142,5 +1144,23 @@ namespace Avalonia.X11 public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8); public bool NeedsManagedDecorations => false; + + + public class SurfacePlatformHandle : IPlatformNativeSurfaceHandle + { + private readonly X11Window _owner; + + public PixelSize Size => _owner.ToPixelSize(_owner.ClientSize); + + public double Scaling => _owner.RenderScaling; + + public SurfacePlatformHandle(X11Window owner) + { + _owner = owner; + } + + public IntPtr Handle => _owner._renderHandle; + public string? HandleDescriptor => "XID"; + } } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs index e218da5f41..64fafc65f3 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs @@ -1,7 +1,9 @@ using System; +using System.IO; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; using static Avalonia.LinuxFramebuffer.Output.LibDrm; @@ -142,10 +144,28 @@ namespace Avalonia.LinuxFramebuffer.Output public int Fd { get; private set; } public DrmCard(string path = null) { - path = path ?? "/dev/dri/card0"; - Fd = open(path, 2, 0); - if (Fd == -1) - throw new Win32Exception("Couldn't open " + path); + if(path == null) + { + var files = Directory.GetFiles("/dev/dri/"); + + foreach(var file in files) + { + var match = Regex.Match(file, "card[0-9]+"); + + if(match.Success) + { + Fd = open(file, 2, 0); + if(Fd != -1) break; + } + } + + if(Fd == -1) throw new Win32Exception("Couldn't open /dev/dri/card[0-9]+"); + } + else + { + Fd = open(path, 2, 0); + if(Fd != -1) throw new Win32Exception($"Couldn't open {path}"); + } } public DrmResources GetResources() => new DrmResources(Fd); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj index f75c96c759..b89ea8399a 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj @@ -12,5 +12,6 @@ - + + diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs index ece90762cb..1e2a77c34d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs @@ -177,11 +177,13 @@ namespace Avalonia.Markup.Xaml.XamlIl var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); var clrPropertyBuilder = tb.DefineNestedType("ClrProperties_" + Guid.NewGuid().ToString("N")); var indexerClosureType = _sreBuilder.DefineType("IndexerClosure_" + Guid.NewGuid().ToString("N")); + var trampolineBuilder = _sreBuilder.DefineType("Trampolines_" + Guid.NewGuid().ToString("N")); var compiler = new AvaloniaXamlIlCompiler(new AvaloniaXamlIlCompilerConfiguration(_sreTypeSystem, asm, _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter, new XamlIlClrPropertyInfoEmitter(_sreTypeSystem.CreateTypeBuilder(clrPropertyBuilder)), - new XamlIlPropertyInfoAccessorFactoryEmitter(_sreTypeSystem.CreateTypeBuilder(indexerClosureType))), + new XamlIlPropertyInfoAccessorFactoryEmitter(_sreTypeSystem.CreateTypeBuilder(indexerClosureType)), + new XamlIlTrampolineBuilder(_sreTypeSystem.CreateTypeBuilder(trampolineBuilder))), _sreEmitMappings, _sreContextType) { EnableIlVerification = true }; @@ -196,6 +198,7 @@ namespace Avalonia.Markup.Xaml.XamlIl compiler.ParseAndCompile(xaml, uri?.ToString(), null, _sreTypeSystem.CreateTypeBuilder(tb), overrideType); var created = tb.CreateTypeInfo(); clrPropertyBuilder.CreateTypeInfo(); + trampolineBuilder.CreateTypeInfo(); return LoadOrPopulate(created, rootInstance); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs index f6f47dce0d..9fc6b5d517 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs @@ -7,6 +7,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { public XamlIlClrPropertyInfoEmitter ClrPropertyEmitter { get; } public XamlIlPropertyInfoAccessorFactoryEmitter AccessorFactoryEmitter { get; } + public XamlIlTrampolineBuilder TrampolineBuilder { get; } public AvaloniaXamlIlCompilerConfiguration(IXamlTypeSystem typeSystem, IXamlAssembly defaultAssembly, @@ -15,13 +16,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XamlValueConverter customValueConverter, XamlIlClrPropertyInfoEmitter clrPropertyEmitter, XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter, + XamlIlTrampolineBuilder trampolineBuilder, IXamlIdentifierGenerator identifierGenerator = null) : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter, identifierGenerator) { ClrPropertyEmitter = clrPropertyEmitter; AccessorFactoryEmitter = accessorFactoryEmitter; + TrampolineBuilder = trampolineBuilder; AddExtra(ClrPropertyEmitter); AddExtra(AccessorFactoryEmitter); + AddExtra(TrampolineBuilder); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index 1db0208310..8ed94f6b20 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -39,6 +39,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { typeSystem.GetType("Avalonia.Metadata.ContentAttribute") }, + WhitespaceSignificantCollectionAttributes = + { + typeSystem.GetType("Avalonia.Metadata.WhitespaceSignificantCollectionAttribute") + }, + TrimSurroundingWhitespaceAttributes = + { + typeSystem.GetType("Avalonia.Metadata.TrimSurroundingWhitespaceAttribute") + }, ProvideValueTarget = typeSystem.GetType("Avalonia.Markup.Xaml.IProvideValueTarget"), RootObjectProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IRootObjectProvider"), RootObjectProviderIntermediateRootPropertyName = "IntermediateRootObject", diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 4592b9c8b4..d907bcbef9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -160,6 +160,29 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return true; } + if (type.Equals(types.RelativePoint)) + { + try + { + var relativePoint = RelativePoint.Parse(text); + + var relativePointTypeRef = new XamlAstClrTypeReference(node, types.RelativePoint, false); + + result = new XamlAstNewClrObjectNode(node, relativePointTypeRef, types.RelativePointFullConstructor, new List + { + new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.X), + new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.Y), + new XamlConstantNode(node, types.RelativeUnit, (int) relativePoint.Unit), + }); + + return true; + } + catch + { + throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a relative point", node); + } + } + if (type.Equals(types.GridLength)) { try @@ -201,7 +224,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions if (type.Equals(types.Classes)) { var classes = text.Split(' '); - var classNodes = classes.Select(c => new XamlAstTextNode(node, c, types.XamlIlTypes.String)).ToArray(); + var classNodes = classes.Select(c => new XamlAstTextNode(node, c, type: types.XamlIlTypes.String)).ToArray(); result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, types.Classes, types.XamlIlTypes.String, classNodes); return true; @@ -221,6 +244,32 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } + if (type.Equals(types.TextTrimming)) + { + foreach (var property in types.TextTrimming.Properties) + { + if (property.PropertyType == types.TextTrimming && property.Name.Equals(text, StringComparison.OrdinalIgnoreCase)) + { + result = new XamlStaticOrTargetedReturnMethodCallNode(node, property.Getter, Enumerable.Empty()); + + return true; + } + } + } + + if (type.Equals(types.TextDecorationCollection)) + { + foreach (var property in types.TextDecorations.Properties) + { + if (property.PropertyType == types.TextDecorationCollection && property.Name.Equals(text, StringComparison.OrdinalIgnoreCase)) + { + result = new XamlStaticOrTargetedReturnMethodCallNode(node, property.Getter, Enumerable.Empty()); + + return true; + } + } + } + result = null; return false; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs index 048a6220c5..3cc3504e16 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -117,7 +117,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return parentDataContextNode.DataContextType; }; - XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startTypeResolver, context.ParentNodes().OfType().First().Type.GetClrType()); + var selfType = context.ParentNodes().OfType().First().Type.GetClrType(); + + // When using self bindings with setters we need to change target type to resolved selector type. + if (context.GetAvaloniaTypes().ISetter.IsAssignableFrom(selfType)) + { + selfType = context.ParentNodes().OfType().First().TargetType.GetClrType(); + } + + XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startTypeResolver, selfType); } return node; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index 79589a5a4f..4c4df1f53a 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -77,7 +77,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers throw new XamlParseException($"Cannot find '{property.Property}' on '{type}", node); if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, - new XamlAstTextNode(node, property.Value, context.Configuration.WellKnownTypes.String), + new XamlAstTextNode(node, property.Value, type: context.Configuration.WellKnownTypes.String), targetProperty.PropertyType, out var typedValue)) throw new XamlParseException( $"Cannot convert '{property.Value}' to '{targetProperty.PropertyType.GetFqn()}", @@ -118,7 +118,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers .GetAvaloniaPropertyType(targetPropertyField, context.GetAvaloniaTypes(), node); if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, - new XamlAstTextNode(node, attachedProperty.Value, context.Configuration.WellKnownTypes.String), + new XamlAstTextNode(node, attachedProperty.Value, type: context.Configuration.WellKnownTypes.String), targetPropertyType, out var typedValue)) throw new XamlParseException( $"Cannot convert '{attachedProperty.Value}' to '{targetPropertyType.GetFqn()}", diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 89b88790dc..76f3cc071f 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -20,10 +20,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlMethod AvaloniaObjectBindMethod { get; } public IXamlMethod AvaloniaObjectSetValueMethod { get; } public IXamlType IDisposable { get; } + public IXamlType ICommand { get; } public XamlTypeWellKnownTypes XamlIlTypes { get; } public XamlLanguageTypeMappings XamlIlMappings { get; } public IXamlType Transitions { get; } public IXamlType AssignBindingAttribute { get; } + public IXamlType DependsOnAttribute { get; } public IXamlType UnsetValueType { get; } public IXamlType StyledElement { get; } public IXamlType IStyledElement { get; } @@ -69,6 +71,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlConstructor MatrixFullConstructor { get; } public IXamlType CornerRadius { get; } public IXamlConstructor CornerRadiusFullConstructor { get; } + public IXamlType RelativeUnit { get; } + public IXamlType RelativePoint { get; } + public IXamlConstructor RelativePointFullConstructor { get; } public IXamlType GridLength { get; } public IXamlConstructor GridLengthConstructorValueType { get; } public IXamlType Color { get; } @@ -85,6 +90,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType IBrush { get; } public IXamlType ImmutableSolidColorBrush { get; } public IXamlConstructor ImmutableSolidColorBrushConstructorColor { get; } + public IXamlType TypeUtilities { get; } + public IXamlType TextDecorationCollection { get; } + public IXamlType TextDecorations { get; } + public IXamlType TextTrimming { get; } + public IXamlType ISetter { get; } public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { @@ -98,8 +108,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers BindingPriority = cfg.TypeSystem.GetType("Avalonia.Data.BindingPriority"); IBinding = cfg.TypeSystem.GetType("Avalonia.Data.IBinding"); IDisposable = cfg.TypeSystem.GetType("System.IDisposable"); + ICommand = cfg.TypeSystem.GetType("System.Windows.Input.ICommand"); Transitions = cfg.TypeSystem.GetType("Avalonia.Animation.Transitions"); AssignBindingAttribute = cfg.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute"); + DependsOnAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DependsOnAttribute"); AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, IAvaloniaObject, AvaloniaProperty, IBinding, cfg.WellKnownTypes.Object); @@ -166,6 +178,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers (Matrix, MatrixFullConstructor) = GetNumericTypeInfo("Avalonia.Matrix", XamlIlTypes.Double, 6); (CornerRadius, CornerRadiusFullConstructor) = GetNumericTypeInfo("Avalonia.CornerRadius", XamlIlTypes.Double, 4); + RelativeUnit = cfg.TypeSystem.GetType("Avalonia.RelativeUnit"); + RelativePoint = cfg.TypeSystem.GetType("Avalonia.RelativePoint"); + RelativePointFullConstructor = RelativePoint.GetConstructor(new List { XamlIlTypes.Double, XamlIlTypes.Double, RelativeUnit }); + GridLength = cfg.TypeSystem.GetType("Avalonia.Controls.GridLength"); GridLengthConstructorValueType = GridLength.GetConstructor(new List { XamlIlTypes.Double, cfg.TypeSystem.GetType("Avalonia.Controls.GridUnitType") }); Color = cfg.TypeSystem.GetType("Avalonia.Media.Color"); @@ -187,6 +203,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers IBrush = cfg.TypeSystem.GetType("Avalonia.Media.IBrush"); ImmutableSolidColorBrush = cfg.TypeSystem.GetType("Avalonia.Media.Immutable.ImmutableSolidColorBrush"); ImmutableSolidColorBrushConstructorColor = ImmutableSolidColorBrush.GetConstructor(new List { UInt }); + TypeUtilities = cfg.TypeSystem.GetType("Avalonia.Utilities.TypeUtilities"); + TextDecorationCollection = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorationCollection"); + TextDecorations = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorations"); + TextTrimming = cfg.TypeSystem.GetType("Avalonia.Media.TextTrimming"); + ISetter = cfg.TypeSystem.GetType("Avalonia.Styling.ISetter"); } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 7f7f60ed94..c8de8f00f6 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -14,7 +14,7 @@ using XamlX.Emit; using XamlX.IL; using Avalonia.Utilities; -using XamlIlEmitContext = XamlX.Emit.XamlEmitContext; +using XamlIlEmitContext = XamlX.Emit.XamlEmitContextWithLocals; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { @@ -32,6 +32,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions selfType, bindingPath.Path); + transformed = TransformForTargetTyping(transformed, context); + bindingResultType = transformed.BindingResultType; binding.Arguments[0] = transformed; } @@ -54,6 +56,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions selfType, bindingPathNode.Path); + transformed = TransformForTargetTyping(transformed, context); + bindingResultType = transformed.BindingResultType; bindingPathAssignment.Values[0] = transformed; } @@ -66,7 +70,41 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return bindingResultType; } - private static IXamlIlBindingPathNode TransformBindingPath(AstTransformationContext context, IXamlLineInfo lineInfo, Func startTypeResolver, IXamlType selfType, IEnumerable bindingExpression) + private static XamlIlBindingPathNode TransformForTargetTyping(XamlIlBindingPathNode transformed, AstTransformationContext context) + { + var parentNode = context.ParentNodes().OfType().FirstOrDefault(); + + if (parentNode == null) + { + return transformed; + } + + var lastElement = + transformed.Elements[transformed.Elements.Count - 1]; + + if (parentNode.Property?.Getter?.ReturnType == context.GetAvaloniaTypes().ICommand && lastElement is XamlIlClrMethodPathElementNode methodPathElement) + { + IXamlMethod executeMethod = methodPathElement.Method; + IXamlMethod canExecuteMethod = executeMethod.DeclaringType.FindMethod(new FindMethodMethodSignature($"Can{executeMethod.Name}", context.Configuration.WellKnownTypes.Boolean, context.Configuration.WellKnownTypes.Object)); + List dependsOnProperties = new(); + if (canExecuteMethod is not null) + { + foreach (var attr in canExecuteMethod.CustomAttributes) + { + if (attr.Type == context.GetAvaloniaTypes().DependsOnAttribute) + { + dependsOnProperties.Add((string)attr.Parameters[0]); + } + } + } + transformed.Elements.RemoveAt(transformed.Elements.Count - 1); + transformed.Elements.Add(new XamlIlClrMethodAsCommandPathElementNode(context.GetAvaloniaTypes().ICommand, executeMethod, canExecuteMethod, dependsOnProperties)); + } + + return transformed; + } + + private static XamlIlBindingPathNode TransformBindingPath(AstTransformationContext context, IXamlLineInfo lineInfo, Func startTypeResolver, IXamlType selfType, IEnumerable bindingExpression) { List transformNodes = new List(); List nodes = new List(); @@ -126,16 +164,18 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe, XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo))); } - else + else if (GetAllDefinedProperties(targetType).FirstOrDefault(p => p.Name == propName.PropertyName) is IXamlProperty clrProperty) { - var clrProperty = GetAllDefinedProperties(targetType).FirstOrDefault(p => p.Name == propName.PropertyName); - - if (clrProperty is null) - { - throw new XamlX.XamlParseException($"Unable to resolve property of name '{propName.PropertyName}' on type '{targetType}'.", lineInfo); - } nodes.Add(new XamlIlClrPropertyPathElementNode(clrProperty)); } + else if (GetAllDefinedMethods(targetType).FirstOrDefault(m => m.Name == propName.PropertyName) is IXamlMethod method) + { + nodes.Add(new XamlIlClrMethodPathElementNode(method, context.Configuration.WellKnownTypes.Delegate)); + } + else + { + throw new XamlX.XamlParseException($"Unable to resolve property or method of name '{propName.PropertyName}' on type '{targetType}'.", lineInfo); + } break; } case BindingExpressionGrammar.IndexerNode indexer: @@ -284,6 +324,17 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } + static IEnumerable GetAllDefinedMethods(IXamlType type) + { + foreach (var t in TraverseTypeHierarchy(type)) + { + foreach (var m in t.Methods) + { + yield return m; + } + } + } + static IEnumerable TraverseTypeHierarchy(IXamlType type) { if (type.IsInterface) @@ -538,6 +589,131 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions public IXamlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; } + class XamlIlClrMethodPathElementNode : IXamlIlBindingPathElementNode + { + + public XamlIlClrMethodPathElementNode(IXamlMethod method, IXamlType systemDelegateType) + { + Method = method; + Type = systemDelegateType; + } + public IXamlMethod Method { get; } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + IXamlTypeBuilder newDelegateTypeBuilder = null; + IXamlType specificDelegateType; + if (Method.ReturnType == context.Configuration.WellKnownTypes.Void && Method.Parameters.Count == 0) + { + specificDelegateType = context.Configuration.TypeSystem + .GetType("System.Action"); + } + else if (Method.ReturnType == context.Configuration.WellKnownTypes.Void && Method.Parameters.Count <= 16) + { + specificDelegateType = context.Configuration.TypeSystem + .GetType($"System.Action`{Method.Parameters.Count}") + .MakeGenericType(Method.Parameters); + } + else if (Method.Parameters.Count <= 16) + { + List genericParameters = new(); + genericParameters.AddRange(Method.Parameters); + genericParameters.Add(Method.ReturnType); + specificDelegateType = context.Configuration.TypeSystem + .GetType($"System.Func`{Method.Parameters.Count + 1}") + .MakeGenericType(genericParameters); + } + else + { + // In this case, we need to emit our own delegate type. + string delegateTypeName = context.Configuration.IdentifierGenerator.GenerateIdentifierPart(); + specificDelegateType = newDelegateTypeBuilder = context.DefineDelegateSubType(delegateTypeName, Method.ReturnType, Method.Parameters); + } + + codeGen + .Ldtoken(Method) + .Ldtoken(specificDelegateType) + .EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Method")); + + newDelegateTypeBuilder?.CreateType(); + } + } + + class XamlIlClrMethodAsCommandPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlMethod _executeMethod; + private readonly IXamlMethod _canExecuteMethod; + private readonly IReadOnlyList _dependsOnProperties; + + public XamlIlClrMethodAsCommandPathElementNode(IXamlType iCommandType, IXamlMethod executeMethod, IXamlMethod canExecuteMethod, IReadOnlyList dependsOnProperties) + { + Type = iCommandType; + _executeMethod = executeMethod; + _canExecuteMethod = canExecuteMethod; + _dependsOnProperties = dependsOnProperties; + } + + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + var trampolineBuilder = context.Configuration.GetExtra(); + var objectType = context.Configuration.WellKnownTypes.Object; + codeGen + .Ldstr(_executeMethod.Name) + .Ldnull() + .Ldftn(trampolineBuilder.EmitCommandExecuteTrampoline(context, _executeMethod)) + .Newobj(context.Configuration.TypeSystem.GetType("System.Action`2") + .MakeGenericType(objectType, objectType) + .GetConstructor(new() { objectType, context.Configuration.TypeSystem.GetType("System.IntPtr") })); + + if (_canExecuteMethod is null) + { + codeGen.Ldnull(); + } + else + { + codeGen + .Ldnull() + .Ldftn(trampolineBuilder.EmitCommandCanExecuteTrampoline(context, _canExecuteMethod)) + .Newobj(context.Configuration.TypeSystem.GetType("System.Func`3") + .MakeGenericType(objectType, objectType, context.Configuration.WellKnownTypes.Boolean) + .GetConstructor(new() { objectType, context.Configuration.TypeSystem.GetType("System.IntPtr") })); + } + + if (_dependsOnProperties is { Count:> 1 }) + { + using var dependsOnPropertiesArray = context.GetLocalOfType(context.Configuration.WellKnownTypes.String.MakeArrayType(1)); + codeGen + .Ldc_I4(_dependsOnProperties.Count) + .Newarr(context.Configuration.WellKnownTypes.String) + .Stloc(dependsOnPropertiesArray.Local); + + for (int i = 0; i < _dependsOnProperties.Count; i++) + { + codeGen + .Ldloc(dependsOnPropertiesArray.Local) + .Ldc_I4(i) + .Ldstr(_dependsOnProperties[i]) + .Stelem_ref(); + } + codeGen.Ldloc(dependsOnPropertiesArray.Local); + } + else + { + codeGen.Ldnull(); + } + + codeGen + .EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Command")); + } + } + class XamlIlClrIndexerPathElementNode : IXamlIlBindingPathElementNode { private readonly IXamlProperty _property; @@ -660,10 +836,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } - class XamlIlBindingPathNode : XamlAstNode, IXamlIlBindingPathNode, IXamlAstEmitableNode + class XamlIlBindingPathNode : XamlAstNode, IXamlIlBindingPathNode, IXamlAstLocalsEmitableNode { private readonly List _transformElements; - private readonly List _elements; public XamlIlBindingPathNode(IXamlLineInfo lineInfo, IXamlType bindingPathType, @@ -672,16 +847,18 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { Type = new XamlAstClrTypeReference(lineInfo, bindingPathType, false); _transformElements = transformElements; - _elements = elements; + Elements = elements; } public IXamlType BindingResultType => _transformElements.Count > 0 ? _transformElements[0].Type - : _elements[_elements.Count - 1].Type; + : Elements[Elements.Count - 1].Type; public IXamlAstTypeReference Type { get; } + public List Elements { get; } + public XamlILNodeEmitResult Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) { var types = context.GetAvaloniaTypes(); @@ -692,7 +869,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions transform.Emit(context, codeGen); } - foreach (var element in _elements) + foreach (var element in Elements) { element.Emit(context, codeGen); } @@ -710,11 +887,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions _transformElements[i] = (IXamlIlBindingPathElementNode)ast.Visit(visitor); } } - for (int i = 0; i < _elements.Count; i++) + for (int i = 0; i < Elements.Count; i++) { - if (_elements[i] is IXamlAstNode ast) + if (Elements[i] is IXamlAstNode ast) { - _elements[i] = (IXamlIlBindingPathElementNode)ast.Visit(visitor); + Elements[i] = (IXamlIlBindingPathElementNode)ast.Visit(visitor); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlTrampolineBuilder.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlTrampolineBuilder.cs new file mode 100644 index 0000000000..a28607f0f4 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlTrampolineBuilder.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using XamlX.Emit; +using XamlX.IL; +using XamlX.TypeSystem; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using System.Reflection.Emit; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + internal class XamlIlTrampolineBuilder + { + private IXamlTypeBuilder _builder; + private Dictionary _trampolines = new(); + + public XamlIlTrampolineBuilder(IXamlTypeBuilder builder) + { + _builder = builder; + } + + public IXamlMethod EmitCommandExecuteTrampoline(XamlEmitContext context, IXamlMethod executeMethod) + { + Debug.Assert(!executeMethod.IsStatic); + string methodName = $"{executeMethod.DeclaringType.GetFqn()}+{executeMethod.Name}_{executeMethod.Parameters.Count}!CommandExecuteTrampoline"; + if (_trampolines.TryGetValue(methodName, out var method)) + { + return method; + } + var trampoline = _builder.DefineMethod( + context.Configuration.WellKnownTypes.Void, + new[] { context.Configuration.WellKnownTypes.Object, context.Configuration.WellKnownTypes.Object }, + methodName, + true, + true, + false); + var gen = trampoline.Generator; + if (executeMethod.DeclaringType.IsValueType) + { + gen.Ldarg_0() + .Unbox(executeMethod.DeclaringType); + } + else + { + gen.Ldarg_0() + .Castclass(executeMethod.DeclaringType); + } + if (executeMethod.Parameters.Count != 0) + { + Debug.Assert(executeMethod.Parameters.Count == 1); + if (executeMethod.Parameters[0] != context.Configuration.WellKnownTypes.Object) + { + var convertedValue = gen.DefineLocal(context.Configuration.WellKnownTypes.Object); + gen.Ldtype(executeMethod.Parameters[0]) + .Ldarg(1) + .EmitCall(context.Configuration.WellKnownTypes.CultureInfo.FindMethod(m => m.Name == "get_CurrentCulture")) + .Ldloca(convertedValue) + .EmitCall( + context.GetAvaloniaTypes().TypeUtilities.FindMethod(m => m.Name == "TryConvert"), + swallowResult: true) + .Ldloc(convertedValue) + .Unbox_Any(executeMethod.Parameters[0]); + } + else + { + gen.Ldarg(1); + } + } + gen.EmitCall(executeMethod, swallowResult: true); + gen.Ret(); + + _trampolines.Add(methodName, trampoline); + return trampoline; + } + + public IXamlMethod EmitCommandCanExecuteTrampoline(XamlEmitContext context, IXamlMethod canExecuteMethod) + { + Debug.Assert(!canExecuteMethod.IsStatic); + Debug.Assert(canExecuteMethod.Parameters.Count == 1); + Debug.Assert(canExecuteMethod.ReturnType == context.Configuration.WellKnownTypes.Boolean); + string methodName = $"{canExecuteMethod.DeclaringType.GetFqn()}+{canExecuteMethod.Name}!CommandCanExecuteTrampoline"; + if (_trampolines.TryGetValue(methodName, out var method)) + { + return method; + } + var trampoline = _builder.DefineMethod( + context.Configuration.WellKnownTypes.Boolean, + new[] { context.Configuration.WellKnownTypes.Object, context.Configuration.WellKnownTypes.Object }, + methodName, + true, + true, + false); + if (canExecuteMethod.DeclaringType.IsValueType) + { + trampoline.Generator + .Ldarg_0() + .Unbox(canExecuteMethod.DeclaringType); + } + else + { + trampoline.Generator + .Ldarg_0() + .Castclass(canExecuteMethod.DeclaringType); + } + trampoline.Generator + .Ldarg(1) + .Emit(OpCodes.Tailcall) + .EmitCall(canExecuteMethod) + .Ret(); + + _trampolines.Add(methodName, trampoline); + return trampoline; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index 8e20d65eb5..daaac590e0 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit 8e20d65eb5f1efbae08e49b18f39bfdce32df7b3 +Subproject commit daaac590e078967b78045f74c38ef046d00d8582 diff --git a/src/Markup/Avalonia.Markup.Xaml/ApiCompatBaseline.txt b/src/Markup/Avalonia.Markup.Xaml/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 86132c5d27..622ec416e8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -18,8 +18,10 @@ + + @@ -54,16 +56,11 @@ - - - - - - + diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CommandAccessorPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CommandAccessorPlugin.cs new file mode 100644 index 0000000000..970cc767f7 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CommandAccessorPlugin.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Text; +using System.Windows.Input; +using Avalonia.Data; +using Avalonia.Data.Core.Plugins; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + internal class CommandAccessorPlugin : IPropertyAccessorPlugin + { + private readonly Action _execute; + private readonly Func _canExecute; + private readonly ISet _dependsOnProperties; + + public CommandAccessorPlugin(Action execute, Func canExecute, ISet dependsOnProperties) + { + _execute = execute; + _canExecute = canExecute; + _dependsOnProperties = dependsOnProperties; + } + + public bool Match(object obj, string propertyName) + { + throw new InvalidOperationException("The CommandAccessorPlugin does not support dynamic matching"); + } + + public IPropertyAccessor Start(WeakReference reference, string propertyName) + { + return new CommandAccessor(reference, _execute, _canExecute, _dependsOnProperties); + } + + private sealed class CommandAccessor : PropertyAccessorBase + { + private readonly WeakReference _reference; + private Command _command; + private readonly ISet _dependsOnProperties; + + public CommandAccessor(WeakReference reference, Action execute, Func canExecute, ISet dependsOnProperties) + { + Contract.Requires(reference != null); + + _reference = reference; + _dependsOnProperties = dependsOnProperties; + _command = new Command(reference, execute, canExecute); + + } + + public override object Value => _reference.TryGetTarget(out var _) ? _command : null; + + private void RaiseCanExecuteChanged() + { + _command.RaiseCanExecuteChanged(); + } + + private sealed class Command : ICommand + { + private readonly WeakReference _target; + private readonly Action _execute; + private readonly Func _canExecute; + + public event EventHandler CanExecuteChanged; + + public Command(WeakReference target, Action execute, Func canExecute) + { + _target = target; + _execute = execute; + _canExecute = canExecute; + } + + public void RaiseCanExecuteChanged() + { + Threading.Dispatcher.UIThread.Post(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty) + , Threading.DispatcherPriority.Input); + } + + public bool CanExecute(object parameter) + { + if (_target.TryGetTarget(out var target)) + { + if (_canExecute == null) + { + return true; + } + return _canExecute(target, parameter); + } + return false; + } + + public void Execute(object parameter) + { + if (_target.TryGetTarget(out var target)) + { + _execute(target, parameter); + } + } + } + + public override Type PropertyType => typeof(ICommand); + + public override bool SetValue(object value, BindingPriority priority) + { + return false; + } + + void OnNotifyPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (string.IsNullOrEmpty(e.PropertyName) || _dependsOnProperties.Contains(e.PropertyName)) + { + RaiseCanExecuteChanged(); + } + } + + protected override void SubscribeCore() + { + SendCurrentValue(); + SubscribeToChanges(); + } + + protected override void UnsubscribeCore() + { + if (_dependsOnProperties is { Count: > 0 } && _reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) + { + WeakEventHandlerManager.Unsubscribe( + inpc, + nameof(INotifyPropertyChanged.PropertyChanged), + OnNotifyPropertyChanged); + } + } + + private void SendCurrentValue() + { + try + { + var value = Value; + PublishValue(value); + } + catch { } + } + + private void SubscribeToChanges() + { + if (_dependsOnProperties is { Count:>0 } && _reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) + { + WeakEventHandlerManager.Subscribe( + inpc, + nameof(INotifyPropertyChanged.PropertyChanged), + OnNotifyPropertyChanged); + } + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index 11489c39aa..73a14fd437 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Avalonia.Controls; using Avalonia.Data.Core; using Avalonia.Data.Core.Plugins; @@ -36,6 +37,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings case PropertyElement prop: node = new PropertyAccessorNode(prop.Property.Name, enableValidation, new PropertyInfoAccessorPlugin(prop.Property, prop.AccessorFactory)); break; + case MethodAsCommandElement methodAsCommand: + node = new PropertyAccessorNode(methodAsCommand.MethodName, enableValidation, new CommandAccessorPlugin(methodAsCommand.ExecuteMethod, methodAsCommand.CanExecuteMethod, methodAsCommand.DependsOnProperties)); + break; + case MethodAsDelegateElement methodAsDelegate: + node = new PropertyAccessorNode(methodAsDelegate.Method.Name, enableValidation, new MethodAccessorPlugin(methodAsDelegate.Method, methodAsDelegate.DelegateType)); + break; case ArrayElementPathElement arr: node = new PropertyAccessorNode(CommonPropertyNames.IndexerName, enableValidation, new ArrayElementPlugin(arr.Indices, arr.ElementType)); break; @@ -92,6 +99,18 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return this; } + public CompiledBindingPathBuilder Method(RuntimeMethodHandle handle, RuntimeTypeHandle delegateType) + { + _elements.Add(new MethodAsDelegateElement(handle, delegateType)); + return this; + } + + public CompiledBindingPathBuilder Command(string methodName, Action executeHelper, Func canExecuteHelper, string[] dependsOnProperties) + { + _elements.Add(new MethodAsCommandElement(methodName, executeHelper, canExecuteHelper, dependsOnProperties ?? Array.Empty())); + return this; + } + public CompiledBindingPathBuilder StreamTask() { _elements.Add(new TaskStreamPathElement()); @@ -178,6 +197,35 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings => _isFirstElement ? Property.Name : $".{Property.Name}"; } + internal class MethodAsDelegateElement : ICompiledBindingPathElement + { + public MethodAsDelegateElement(RuntimeMethodHandle method, RuntimeTypeHandle delegateType) + { + Method = (MethodInfo)MethodBase.GetMethodFromHandle(method); + DelegateType = Type.GetTypeFromHandle(delegateType); + } + + public MethodInfo Method { get; } + + public Type DelegateType { get; } + } + + internal class MethodAsCommandElement : ICompiledBindingPathElement + { + public MethodAsCommandElement(string methodName, Action executeHelper, Func canExecuteHelper, string[] dependsOnElements) + { + MethodName = methodName; + ExecuteMethod = executeHelper; + CanExecuteMethod = canExecuteHelper; + DependsOnProperties = new HashSet(dependsOnElements); + } + + public string MethodName { get; } + public Action ExecuteMethod { get; } + public Func CanExecuteMethod { get; } + public HashSet DependsOnProperties { get; } + } + internal interface IStronglyTypedStreamElement : ICompiledBindingPathElement { IStreamPlugin CreatePlugin(); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/MethodAccessorPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/MethodAccessorPlugin.cs new file mode 100644 index 0000000000..45ad45e658 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/MethodAccessorPlugin.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Text; +using Avalonia.Data; +using Avalonia.Data.Core.Plugins; + +#nullable enable + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + internal class MethodAccessorPlugin : IPropertyAccessorPlugin + { + private MethodInfo _method; + private readonly Type _delegateType; + + public MethodAccessorPlugin(MethodInfo method, Type delegateType) + { + _method = method; + _delegateType = delegateType; + } + + public bool Match(object obj, string propertyName) + { + throw new InvalidOperationException("The MethodAccessorPlugin does not support dynamic matching"); + } + + public IPropertyAccessor Start(WeakReference reference, string propertyName) + { + Debug.Assert(_method.Name == propertyName); + return new Accessor(reference, _method, _delegateType); + } + + private sealed class Accessor : PropertyAccessorBase + { + public Accessor(WeakReference reference, MethodInfo method, Type delegateType) + { + _ = reference ?? throw new ArgumentNullException(nameof(reference)); + _ = method ?? throw new ArgumentNullException(nameof(method)); + + PropertyType = delegateType; + + if (method.IsStatic) + { + Value = method.CreateDelegate(PropertyType); + } + else if (reference.TryGetTarget(out var target)) + { + Value = method.CreateDelegate(PropertyType, target); + } + } + + public override Type? PropertyType { get; } + + public override object? Value { get; } + + public override bool SetValue(object? value, BindingPriority priority) => false; + + protected override void SubscribeCore() + { + try + { + PublishValue(Value); + } + catch { } + } + + protected override void UnsubscribeCore() + { + } + } + } +} diff --git a/src/Markup/Avalonia.Markup/ApiCompatBaseline.txt b/src/Markup/Avalonia.Markup/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Markup/Avalonia.Markup/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 21370b3bf5..f7f1a111ba 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -11,11 +11,10 @@ - - + diff --git a/src/Shared/ModuleInitializer.cs b/src/Shared/ModuleInitializer.cs index c14b150c1a..b5bb31ed81 100644 --- a/src/Shared/ModuleInitializer.cs +++ b/src/Shared/ModuleInitializer.cs @@ -3,7 +3,7 @@ namespace System.Runtime.CompilerServices #if !NET5_0_OR_GREATER internal class ModuleInitializerAttribute : Attribute { - + } #endif } diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs index 25b4b41e56..084593ffc6 100644 --- a/src/Shared/RawEventGrouping.cs +++ b/src/Shared/RawEventGrouping.cs @@ -53,7 +53,7 @@ internal class RawEventGrouper : IDisposable _eventCallback?.Invoke(ev); - if (ev is RawPointerEventArgs { IntermediatePoints: PooledList list }) + if (ev is RawPointerEventArgs { IntermediatePoints.Value: PooledList list }) list.Dispose(); if (Dispatcher.UIThread.HasJobsWithPriority(DispatcherPriority.Input + 1)) @@ -110,10 +110,14 @@ internal class RawEventGrouper : IDisposable AddToQueue(args); } + private static IReadOnlyList GetPooledList() => new PooledList(); + private static readonly Func> s_getPooledListDelegate = GetPooledList; + private static void MergeEvents(RawPointerEventArgs last, RawPointerEventArgs current) { - last.IntermediatePoints ??= new PooledList(); - ((PooledList)last.IntermediatePoints).Add(last.Position); + + last.IntermediatePoints ??= new Lazy?>(s_getPooledListDelegate); + ((PooledList)last.IntermediatePoints.Value!).Add(new RawPointerPoint { Position = last.Position }); last.Position = current.Position; last.Timestamp = current.Timestamp; last.InputModifiers = current.InputModifiers; diff --git a/src/Shared/SourceGeneratorAttributes.cs b/src/Shared/SourceGeneratorAttributes.cs new file mode 100644 index 0000000000..fdb5977d23 --- /dev/null +++ b/src/Shared/SourceGeneratorAttributes.cs @@ -0,0 +1,17 @@ +using System; + +namespace Avalonia.SourceGenerator +{ + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class SubtypesFactoryAttribute : Attribute + { + public SubtypesFactoryAttribute(Type baseType, string @namespace) + { + BaseType = baseType; + Namespace = @namespace; + } + + public string Namespace { get; } + public Type BaseType { get; } + } +} diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 50ace8209c..26c0fb756e 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -16,5 +16,6 @@ - + + diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index e695a9cb41..2548b9f5aa 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -9,7 +9,7 @@ using Avalonia.Rendering; using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia @@ -614,10 +614,21 @@ namespace Avalonia.Skia var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint(); // would be nice to cache these shaders possibly? - using (var shader = - SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) + if (linearGradient.Transform is null) { - paintWrapper.Paint.Shader = shader; + using (var shader = + SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; + } + } + else + { + using (var shader = + SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode, linearGradient.Transform.Value.ToSKMatrix())) + { + paintWrapper.Paint.Shader = shader; + } } break; @@ -632,10 +643,21 @@ namespace Avalonia.Skia if (origin.Equals(center)) { // when the origin is the same as the center the Skia RadialGradient acts the same as D2D - using (var shader = - SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) + if (radialGradient.Transform is null) { - paintWrapper.Paint.Shader = shader; + using (var shader = + SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; + } + } + else + { + using (var shader = + SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode, radialGradient.Transform.Value.ToSKMatrix())) + { + paintWrapper.Paint.Shader = shader; + } } } else @@ -659,12 +681,25 @@ namespace Avalonia.Skia } // compose with a background colour of the final stop to match D2D's behaviour of filling with the final color - using (var shader = SKShader.CreateCompose( - SKShader.CreateColor(reversedColors[0]), - SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode) - )) + if (radialGradient.Transform is null) { - paintWrapper.Paint.Shader = shader; + using (var shader = SKShader.CreateCompose( + SKShader.CreateColor(reversedColors[0]), + SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode) + )) + { + paintWrapper.Paint.Shader = shader; + } + } + else + { + using (var shader = SKShader.CreateCompose( + SKShader.CreateColor(reversedColors[0]), + SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode, radialGradient.Transform.Value.ToSKMatrix()) + )) + { + paintWrapper.Paint.Shader = shader; + } } } @@ -679,6 +714,11 @@ namespace Avalonia.Skia var angle = (float)(conicGradient.Angle - 90); var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y); + if (conicGradient.Transform is { }) + { + rotation = rotation.PreConcat(conicGradient.Transform.Value.ToSKMatrix()); + } + using (var shader = SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation)) { @@ -751,6 +791,11 @@ namespace Avalonia.Skia tileTransform, SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); + if (tileBrush.Transform is { }) + { + paintTransform = paintTransform.PreConcat(tileBrush.Transform.Value.ToSKMatrix()); + } + using (var shader = image.ToShader(tileX, tileY, paintTransform)) { paintWrapper.Paint.Shader = shader; diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 6b560ac739..125dd0e455 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -29,27 +29,27 @@ namespace Avalonia.Skia [ThreadStatic] private static string[] t_languageTagBuffer; public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, + FontWeight fontWeight, FontStretch fontStretch, FontFamily fontFamily, CultureInfo culture, out Typeface fontKey) { SKFontStyle skFontStyle; switch (fontWeight) { - case FontWeight.Normal when fontStyle == FontStyle.Normal: + case FontWeight.Normal when fontStyle == FontStyle.Normal && fontStretch == FontStretch.Normal: skFontStyle = SKFontStyle.Normal; break; - case FontWeight.Normal when fontStyle == FontStyle.Italic: + case FontWeight.Normal when fontStyle == FontStyle.Italic && fontStretch == FontStretch.Normal: skFontStyle = SKFontStyle.Italic; break; - case FontWeight.Bold when fontStyle == FontStyle.Normal: + case FontWeight.Bold when fontStyle == FontStyle.Normal && fontStretch == FontStretch.Normal: skFontStyle = SKFontStyle.Bold; break; - case FontWeight.Bold when fontStyle == FontStyle.Italic: + case FontWeight.Bold when fontStyle == FontStyle.Italic && fontStretch == FontStretch.Normal: skFontStyle = SKFontStyle.BoldItalic; break; default: - skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle); + skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, (SKFontStyleWidth)fontStretch, (SKFontStyleSlant)fontStyle); break; } @@ -80,7 +80,7 @@ namespace Avalonia.Skia continue; } - fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight); + fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight, fontStretch); return true; } @@ -91,7 +91,7 @@ namespace Avalonia.Skia if (skTypeface != null) { - fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight); + fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight, fontStretch); return true; } @@ -109,15 +109,20 @@ namespace Avalonia.Skia if (typeface.FontFamily.Key == null) { var defaultName = SKTypeface.Default.FamilyName; - var fontStyle = new SKFontStyle((SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); + + var fontStyle = new SKFontStyle((SKFontStyleWeight)typeface.Weight, (SKFontStyleWidth)typeface.Stretch, + (SKFontStyleSlant)typeface.Style); foreach (var familyName in typeface.FontFamily.FamilyNames) { + if(familyName == FontFamily.DefaultFontFamilyName) + { + continue; + } + skTypeface = _skFontManager.MatchFamily(familyName, fontStyle); - if (skTypeface is null - || (!skTypeface.FamilyName.Equals(familyName, StringComparison.Ordinal) - && defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal))) + if (skTypeface is null || defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal)) { continue; } @@ -125,7 +130,10 @@ namespace Avalonia.Skia break; } - skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle); + // MatchTypeface can return "null" if matched typeface wasn't found for the style + // Fallback to the default typeface and styles instead. + skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle) + ?? SKTypeface.Default; } else { diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index b4c5619c85..809f50ab8b 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -69,6 +69,7 @@ namespace Avalonia.Skia gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb); var size = glSession.Size; + var colorType = SKColorType.Rgba8888; var scaling = glSession.Scaling; if (size.Width <= 0 || size.Height <= 0 || scaling < 0) { @@ -81,12 +82,16 @@ namespace Avalonia.Skia { _grContext.ResetContext(); - var renderTarget = - new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize, - new GRGlFramebufferInfo((uint)fb, SKColorType.Rgba8888.ToGlSizedFormat())); + var samples = disp.SampleCount; + var maxSamples = _grContext.GetMaxSurfaceSampleCount(colorType); + if (samples > maxSamples) + samples = maxSamples; + + var glInfo = new GRGlFramebufferInfo((uint)fb, colorType.ToGlSizedFormat()); + var renderTarget = new GRBackendRenderTarget(size.Width, size.Height, samples, disp.StencilSize, glInfo); var surface = SKSurface.Create(_grContext, renderTarget, glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, - SKColorType.Rgba8888); + colorType); success = true; diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs index 72438609d5..d0b45b7c5d 100644 --- a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs @@ -1,10 +1,11 @@ -using Avalonia.Platform; +using System; +using Avalonia.Platform; using Avalonia.Rendering; using SkiaSharp; namespace Avalonia.Skia.Helpers { - public class DrawingContextHelper + public static class DrawingContextHelper { /// /// Wrap Skia canvas in drawing context so we can use Avalonia api to render to external skia canvas @@ -27,5 +28,71 @@ namespace Avalonia.Skia.Helpers return new DrawingContextImpl(createInfo); } + + /// + /// Unsupported - Wraps a GPU Backed SkiaSurface in an Avalonia DrawingContext. + /// + [Obsolete] + public static ISkiaDrawingContextImpl WrapSkiaSurface(this SKSurface surface, GRContext grContext, Vector dpi, params IDisposable[] disposables) + { + var createInfo = new DrawingContextImpl.CreateInfo + { + GrContext = grContext, + Surface = surface, + Dpi = dpi, + DisableTextLcdRendering = false, + }; + + return new DrawingContextImpl(createInfo, disposables); + } + + /// + /// Unsupported - Wraps a non-GPU Backed SkiaSurface in an Avalonia DrawingContext. + /// + [Obsolete] + public static ISkiaDrawingContextImpl WrapSkiaSurface(this SKSurface surface, Vector dpi, params IDisposable[] disposables) + { + var createInfo = new DrawingContextImpl.CreateInfo + { + Surface = surface, + Dpi = dpi, + DisableTextLcdRendering = false, + }; + + return new DrawingContextImpl(createInfo, disposables); + } + + [Obsolete] + public static ISkiaDrawingContextImpl CreateDrawingContext(Size size, Vector dpi, GRContext grContext = null) + { + if (grContext is null) + { + var surface = SKSurface.Create( + new SKImageInfo( + (int)Math.Ceiling(size.Width), + (int)Math.Ceiling(size.Height), + SKImageInfo.PlatformColorType, + SKAlphaType.Premul)); + + return WrapSkiaSurface(surface, dpi, surface); + } + else + { + var surface = SKSurface.Create(grContext, false, + new SKImageInfo( + (int)Math.Ceiling(size.Width), + (int)Math.Ceiling(size.Height), + SKImageInfo.PlatformColorType, + SKAlphaType.Premul)); + + return WrapSkiaSurface(surface, grContext, dpi, surface); + } + } + + [Obsolete] + public static void DrawTo(this ISkiaDrawingContextImpl source, ISkiaDrawingContextImpl destination, SKPaint paint = null) + { + destination.SkCanvas.DrawSurface(source.SkSurface, new SKPoint(0, 0), paint); + } } } diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index 3010f49420..5628ae177c 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -5,7 +5,7 @@ using System.Security.Cryptography; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Skia.Helpers; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index af3b570fd7..d3c3585cd0 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -10,7 +10,7 @@ using Avalonia.Media; using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; using Avalonia.Platform; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia @@ -35,6 +35,9 @@ namespace Avalonia.Skia var gl = AvaloniaLocator.Current.GetService(); if (gl != null) _skiaGpu = new GlSkiaGpu(gl, maxResourceBytes); + + //TODO: SKFont crashes when disposed in finalizer so we keep it alive + GC.SuppressFinalize(s_font); } public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 2b9d0b103e..1f3f20730f 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -1,6 +1,4 @@ using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using Avalonia.Media; using SkiaSharp; @@ -18,48 +16,183 @@ namespace Avalonia.Skia public SKTypeface Get(Typeface typeface) { - return GetNearestMatch(_typefaces, typeface); + return GetNearestMatch(typeface); } - private static SKTypeface GetNearestMatch(IDictionary typefaces, Typeface key) + private SKTypeface GetNearestMatch(Typeface key) { - if (typefaces.TryGetValue(key, out var typeface)) + if (_typefaces.Count == 0) { - return typeface; + return null; } - var initialWeight = (int)key.Weight; + if (_typefaces.TryGetValue(key, out var typeface)) + { + return typeface; + } + + if(key.Style != FontStyle.Normal) + { + key = new Typeface(key.FontFamily, FontStyle.Normal, key.Weight, key.Stretch); + } + + if(key.Stretch != FontStretch.Normal) + { + if(TryFindStretchFallback(key, out typeface)) + { + return typeface; + } + + if(key.Weight != FontWeight.Normal) + { + if (TryFindStretchFallback(new Typeface(key.FontFamily, key.Style, FontWeight.Normal, key.Stretch), out typeface)) + { + return typeface; + } + } + + key = new Typeface(key.FontFamily, key.Style, key.Weight, FontStretch.Normal); + } + + if(TryFindWeightFallback(key, out typeface)) + { + return typeface; + } + if (TryFindStretchFallback(key, out typeface)) + { + return typeface; + } + + //Nothing was found so we try some regular typeface. + if (_typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface)) + { + return typeface; + } + + SKTypeface skTypeface = null; + + foreach(var pair in _typefaces) + { + skTypeface = pair.Value; + + if (skTypeface.FamilyName.Contains(key.FontFamily.Name)) + { + return skTypeface; + } + } + + return skTypeface; + } + + private bool TryFindStretchFallback(Typeface key, out SKTypeface typeface) + { + typeface = null; + var stretch = (int)key.Stretch; + + if (stretch < 5) + { + for (var i = 0; stretch + i < 9; i++) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch + i)), out typeface)) + { + return true; + } + } + } + else + { + for (var i = 0; stretch - i > 1; i++) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch - i)), out typeface)) + { + return true; + } + } + } + + return false; + } + + private bool TryFindWeightFallback(Typeface key, out SKTypeface typeface) + { + typeface = null; var weight = (int)key.Weight; - weight -= weight % 50; // make sure we start at a full weight + //If the target weight given is between 400 and 500 inclusive + if (weight >= 400 && weight <= 500) + { + //Look for available weights between the target and 500, in ascending order. + for (var i = 0; weight + i <= 500; i += 50) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights greater than 500, in ascending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) + { + return true; + } + } + } - for (var i = 0; i < 2; i++) + //If a weight less than 400 is given, look for available weights less than the target, in descending order. + if (weight < 400) { - for (var j = 0; j < initialWeight; j += 50) + for (var i = 0; weight - i >= 100; i += 50) { - if (weight - j >= 100) + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) { - if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) - { - return typeface; - } + return true; } + } - if (weight + j > 900) + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) { - continue; + return true; } + } + } - if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) + //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order. + if (weight > 500) + { + for (var i = 0; weight + i <= 900; i += 50) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) { - return typeface; + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) + { + return true; } } } - //Nothing was found so we try to get a regular typeface. - return typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface) ? typeface : null; + return false; } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 2816146806..5ca7f40d17 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -51,13 +51,13 @@ namespace Avalonia.Skia if (typeface == null) throw new InvalidOperationException("Typeface could not be loaded."); - if (typeface.FamilyName != fontFamily.Name) + if (!typeface.FamilyName.Contains(fontFamily.Name)) { continue; } var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(), - (FontWeight)typeface.FontWeight); + (FontWeight)typeface.FontWeight, (FontStretch)typeface.FontWidth); typeFaceCollection.AddTypeface(key, typeface); } diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 75b4231640..166f2e8f98 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -1,7 +1,7 @@ using System; using Avalonia.Media; using Avalonia.Platform; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index c4d11f4613..a0890262e7 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -13,12 +13,16 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, - CultureInfo culture, sbyte bidiLevel) + public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) { + var typeface = options.Typeface; + var fontRenderingEmSize = options.FontRenderingEmSize; + var bidiLevel = options.BidLevel; + var culture = options.Culture; + using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); + buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length); MergeBreakPair(buffer); @@ -61,6 +65,15 @@ namespace Avalonia.Skia var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); + if(glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t') + { + glyphIndex = typeface.GetGlyph(' '); + + glyphAdvance = options.IncrementalTabWidth > 0 ? + options.IncrementalTabWidth : + 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; + } + var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); shapedBuffer[i] = targetInfo; diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index 63a1e8f869..506edf0627 100644 --- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -4,7 +4,7 @@ using System.Threading; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Skia.Helpers; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj index cc604a9753..98fdccfe83 100644 --- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj +++ b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj @@ -5,6 +5,7 @@ enable Avalonia.Web.Blazor preview + false true diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 07c776efb4..1ccf53943a 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -224,7 +224,7 @@ namespace Avalonia.Web.Blazor private void OnKeyDown(KeyboardEventArgs e) { - _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Key, GetModifiers(e)); + _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Code, GetModifiers(e)); } private void OnKeyUp(KeyboardEventArgs e) @@ -367,10 +367,12 @@ namespace Avalonia.Web.Blazor } } - public void SetActive(bool active) + public void SetClient(ITextInputMethodClient? client) { _inputHelper.Clear(); + var active = client is { }; + if (active) { _inputHelper.Show(); @@ -386,7 +388,7 @@ namespace Avalonia.Web.Blazor { } - public void SetOptions(TextInputOptionsQueryEventArgs options) + public void SetOptions(TextInputOptions options) { } diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts index ff6dc4a8f8..932dfa1e1f 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts @@ -6,7 +6,7 @@ // Here be dragons! // This is community-maintained definition file intended to ease the process of developing -// high quality JavaScript interop code to be used in Blazor application from your C# .Net code. +// high quality JavaScript interop code to be used in Blazor application from your C# .NET code. // Could be removed without a notice in case official definition types ships with Blazor itself. // tslint:disable:no-unnecessary-generics diff --git a/src/Web/Avalonia.Web.Blazor/WinStubs.cs b/src/Web/Avalonia.Web.Blazor/WinStubs.cs index 7b2bff6bfd..7c30a96d35 100644 --- a/src/Web/Avalonia.Web.Blazor/WinStubs.cs +++ b/src/Web/Avalonia.Web.Blazor/WinStubs.cs @@ -55,5 +55,20 @@ namespace Avalonia.Web.Blazor public IReadOnlyList AllScreens { get; } = new[] { new Screen(96, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) }; + + public Screen? ScreenFromPoint(PixelPoint point) + { + return ScreenHelper.ScreenFromPoint(point, AllScreens); + } + + public Screen? ScreenFromRect(PixelRect rect) + { + return ScreenHelper.ScreenFromRect(rect, AllScreens); + } + + public Screen? ScreenFromWindow(IWindowBaseImpl window) + { + return ScreenHelper.ScreenFromWindow(window, AllScreens); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index a1d8a60cb9..bd1f05b30f 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -18,4 +18,5 @@ + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index c32c58605f..d9e992bb80 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -9,7 +9,7 @@ using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; using TextAlignment = Avalonia.Media.TextAlignment; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index 78bf25d607..792bf2d0be 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -5,6 +5,7 @@ using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; using FontStyle = SharpDX.DirectWrite.FontStyle; using FontWeight = SharpDX.DirectWrite.FontWeight; +using FontStretch = SharpDX.DirectWrite.FontStretch; namespace Avalonia.Direct2D1.Media { @@ -32,7 +33,7 @@ namespace Avalonia.Direct2D1.Media { return fontCollection.GetFontFamily(index).GetFirstMatchingFont( (FontWeight)typeface.Weight, - FontStretch.Normal, + (FontStretch)typeface.Stretch, (FontStyle)typeface.Style); } } @@ -41,7 +42,7 @@ namespace Avalonia.Direct2D1.Media return InstalledFontCollection.GetFontFamily(index).GetFirstMatchingFont( (FontWeight)typeface.Weight, - FontStretch.Normal, + (FontStretch)typeface.Stretch, (FontStyle)typeface.Style); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index b62a6fa5a6..a259d8fab9 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -5,11 +5,11 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SharpDX; using SharpDX.Direct2D1; using SharpDX.Mathematics.Interop; -using BitmapInterpolationMode = Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode; +using BitmapInterpolationMode = Avalonia.Media.Imaging.BitmapInterpolationMode; namespace Avalonia.Direct2D1.Media { diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 6d95d759ec..c996337520 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -4,6 +4,7 @@ using Avalonia.Media; using Avalonia.Platform; using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; +using FontStretch = Avalonia.Media.FontStretch; using FontStyle = Avalonia.Media.FontStyle; using FontWeight = Avalonia.Media.FontWeight; @@ -32,7 +33,7 @@ namespace Avalonia.Direct2D1.Media } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, + FontWeight fontWeight, FontStretch fontStretch, FontFamily fontFamily, CultureInfo culture, out Typeface typeface) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -40,7 +41,8 @@ namespace Avalonia.Direct2D1.Media for (var i = 0; i < familyCount; i++) { var font = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i) - .GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, FontStretch.Normal, + .GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, + (SharpDX.DirectWrite.FontStretch)fontStretch, (SharpDX.DirectWrite.FontStyle)fontStyle).GetFont(0); if (!font.HasCharacter(codepoint)) @@ -50,7 +52,7 @@ namespace Avalonia.Direct2D1.Media var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - typeface = new Typeface(fontFamilyName, fontStyle, fontWeight); + typeface = new Typeface(fontFamilyName, fontStyle, fontWeight, fontStretch); return true; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index 09b900b0c2..296cefad4e 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Direct2D1.Media { private readonly OptionalDispose _bitmap; - private readonly Visuals.Media.Imaging.BitmapInterpolationMode _bitmapInterpolationMode; + private readonly Avalonia.Media.Imaging.BitmapInterpolationMode _bitmapInterpolationMode; public ImageBrushImpl( ITileBrush brush, diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index cac35aa4ae..df07f7f39c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -15,21 +15,21 @@ namespace Avalonia.Direct2D1.Media { private readonly BitmapDecoder _decoder; - private static BitmapInterpolationMode ConvertInterpolationMode(Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) + private static BitmapInterpolationMode ConvertInterpolationMode(Avalonia.Media.Imaging.BitmapInterpolationMode interpolationMode) { switch (interpolationMode) { - case Visuals.Media.Imaging.BitmapInterpolationMode.Default: + case Avalonia.Media.Imaging.BitmapInterpolationMode.Default: return BitmapInterpolationMode.Fant; - case Visuals.Media.Imaging.BitmapInterpolationMode.LowQuality: + case Avalonia.Media.Imaging.BitmapInterpolationMode.LowQuality: return BitmapInterpolationMode.NearestNeighbor; - case Visuals.Media.Imaging.BitmapInterpolationMode.MediumQuality: + case Avalonia.Media.Imaging.BitmapInterpolationMode.MediumQuality: return BitmapInterpolationMode.Fant; default: - case Visuals.Media.Imaging.BitmapInterpolationMode.HighQuality: + case Avalonia.Media.Imaging.BitmapInterpolationMode.HighQuality: return BitmapInterpolationMode.HighQualityCubic; } @@ -118,7 +118,7 @@ namespace Avalonia.Direct2D1.Media } } - public WicBitmapImpl(Stream stream, int decodeSize, bool horizontal, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) + public WicBitmapImpl(Stream stream, int decodeSize, bool horizontal, Avalonia.Media.Imaging.BitmapInterpolationMode interpolationMode) { _decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, stream, DecodeOptions.CacheOnLoad); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs index fef6a9aa82..885be132a4 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Direct2D1.Media.Imaging class WriteableWicBitmapImpl : WicBitmapImpl, IWriteableBitmapImpl { public WriteableWicBitmapImpl(Stream stream, int decodeSize, bool horizontal, - Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) + Avalonia.Media.Imaging.BitmapInterpolationMode interpolationMode) : base(stream, decodeSize, horizontal, interpolationMode) { } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 62cf031f86..59027a663f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -14,9 +14,13 @@ namespace Avalonia.Direct2D1.Media internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, - CultureInfo culture, sbyte bidiLevel) + public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) { + var typeface = options.Typeface; + var fontRenderingEmSize = options.FontRenderingEmSize; + var bidiLevel = options.BidLevel; + var culture = options.Culture; + using (var buffer = new Buffer()) { buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); @@ -62,6 +66,15 @@ internal class TextShaperImpl : ITextShaperImpl var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); + if (glyphIndex == 0 && text[glyphCluster] == '\t') + { + glyphIndex = typeface.GetGlyph(' '); + + glyphAdvance = options.IncrementalTabWidth > 0 ? + options.IncrementalTabWidth : + 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; + } + var targetInfo = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); diff --git a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs index cdeb675bcd..89fd6c2dd8 100644 --- a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs +++ b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs @@ -1,9 +1,4 @@ using System.Runtime.CompilerServices; -using Avalonia.Platform; -using Avalonia.Direct2D1; - -[assembly: ExportRenderingSubsystem(OperatingSystemType.WinNT, 1, "Direct2D1", typeof(Direct2D1Platform), nameof(Direct2D1Platform.Initialize), - typeof(Direct2DChecker))] [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] diff --git a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj index 9f49808422..cc8a40e2d4 100644 --- a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj +++ b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj @@ -18,4 +18,5 @@ + diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs new file mode 100644 index 0000000000..5f3f863493 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs @@ -0,0 +1,19 @@ +using Avalonia.Automation; +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IExpandCollapseProvider + { + ExpandCollapseState UIA.IExpandCollapseProvider.ExpandCollapseState + { + get => InvokeSync(x => x.ExpandCollapseState); + } + + void UIA.IExpandCollapseProvider.Expand() => InvokeSync(x => x.Expand()); + void UIA.IExpandCollapseProvider.Collapse() => InvokeSync(x => x.Collapse()); + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs new file mode 100644 index 0000000000..b91cb76888 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs @@ -0,0 +1,19 @@ +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IRangeValueProvider + { + double UIA.IRangeValueProvider.Value => InvokeSync(x => x.Value); + bool UIA.IRangeValueProvider.IsReadOnly => InvokeSync(x => x.IsReadOnly); + double UIA.IRangeValueProvider.Maximum => InvokeSync(x => x.Maximum); + double UIA.IRangeValueProvider.Minimum => InvokeSync(x => x.Minimum); + double UIA.IRangeValueProvider.LargeChange => 1; + double UIA.IRangeValueProvider.SmallChange => 1; + + public void SetValue(double value) => InvokeSync(x => x.SetValue(value)); + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs new file mode 100644 index 0000000000..4f2d4ae269 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs @@ -0,0 +1,32 @@ +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IScrollProvider, UIA.IScrollItemProvider + { + bool UIA.IScrollProvider.HorizontallyScrollable => InvokeSync(x => x.HorizontallyScrollable); + double UIA.IScrollProvider.HorizontalScrollPercent => InvokeSync(x => x.HorizontalScrollPercent); + double UIA.IScrollProvider.HorizontalViewSize => InvokeSync(x => x.HorizontalViewSize); + bool UIA.IScrollProvider.VerticallyScrollable => InvokeSync(x => x.VerticallyScrollable); + double UIA.IScrollProvider.VerticalScrollPercent => InvokeSync(x => x.VerticalScrollPercent); + double UIA.IScrollProvider.VerticalViewSize => InvokeSync(x => x.VerticalViewSize); + + void UIA.IScrollProvider.Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount) + { + InvokeSync(x => x.Scroll(horizontalAmount, verticalAmount)); + } + + void UIA.IScrollProvider.SetScrollPercent(double horizontalPercent, double verticalPercent) + { + InvokeSync(x => x.SetScrollPercent(horizontalPercent, verticalPercent)); + } + + void UIA.IScrollItemProvider.ScrollIntoView() + { + InvokeSync(() => Peer.BringIntoView()); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs new file mode 100644 index 0000000000..61903ab5b0 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.ISelectionProvider, UIA.ISelectionItemProvider + { + bool UIA.ISelectionProvider.CanSelectMultiple => InvokeSync(x => x.CanSelectMultiple); + bool UIA.ISelectionProvider.IsSelectionRequired => InvokeSync(x => x.IsSelectionRequired); + bool UIA.ISelectionItemProvider.IsSelected => InvokeSync(x => x.IsSelected); + + UIA.IRawElementProviderSimple? UIA.ISelectionItemProvider.SelectionContainer + { + get + { + var peer = InvokeSync(x => x.SelectionContainer); + return GetOrCreate(peer as AutomationPeer); + } + } + + UIA.IRawElementProviderSimple[] UIA.ISelectionProvider.GetSelection() + { + var peers = InvokeSync>(x => x.GetSelection()); + return peers?.Select(x => (UIA.IRawElementProviderSimple)GetOrCreate(x)!).ToArray() ?? + Array.Empty(); + } + + void UIA.ISelectionItemProvider.AddToSelection() => InvokeSync(x => x.AddToSelection()); + void UIA.ISelectionItemProvider.RemoveFromSelection() => InvokeSync(x => x.RemoveFromSelection()); + void UIA.ISelectionItemProvider.Select() => InvokeSync(x => x.Select()); + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs new file mode 100644 index 0000000000..38f4d80946 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs @@ -0,0 +1,13 @@ +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IToggleProvider + { + ToggleState UIA.IToggleProvider.ToggleState => InvokeSync(x => x.ToggleState); + void UIA.IToggleProvider.Toggle() => InvokeSync(x => x.Toggle()); + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs new file mode 100644 index 0000000000..34f5dfe0b9 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IValueProvider + { + bool UIA.IValueProvider.IsReadOnly => InvokeSync(x => x.IsReadOnly); + string? UIA.IValueProvider.Value => InvokeSync(x => x.Value); + + void UIA.IValueProvider.SetValue([MarshalAs(UnmanagedType.LPWStr)] string? value) + { + InvokeSync(x => x.SetValue(value)); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs new file mode 100644 index 0000000000..70e415aff1 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Threading; +using Avalonia.Win32.Interop.Automation; +using AAP = Avalonia.Automation.Provider; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + [ComVisible(true)] + internal partial class AutomationNode : MarshalByRefObject, + IRawElementProviderSimple, + IRawElementProviderSimple2, + IRawElementProviderFragment, + IRawElementProviderAdviseEvents, + IInvokeProvider + { + private static Dictionary s_propertyMap = new Dictionary() + { + { AutomationElementIdentifiers.BoundingRectangleProperty, UiaPropertyId.BoundingRectangle }, + { AutomationElementIdentifiers.ClassNameProperty, UiaPropertyId.ClassName }, + { AutomationElementIdentifiers.NameProperty, UiaPropertyId.Name }, + { ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, UiaPropertyId.ExpandCollapseExpandCollapseState }, + { RangeValuePatternIdentifiers.IsReadOnlyProperty, UiaPropertyId.RangeValueIsReadOnly}, + { RangeValuePatternIdentifiers.MaximumProperty, UiaPropertyId.RangeValueMaximum }, + { RangeValuePatternIdentifiers.MinimumProperty, UiaPropertyId.RangeValueMinimum }, + { RangeValuePatternIdentifiers.ValueProperty, UiaPropertyId.RangeValueValue }, + { ScrollPatternIdentifiers.HorizontallyScrollableProperty, UiaPropertyId.ScrollHorizontallyScrollable }, + { ScrollPatternIdentifiers.HorizontalScrollPercentProperty, UiaPropertyId.ScrollHorizontalScrollPercent }, + { ScrollPatternIdentifiers.HorizontalViewSizeProperty, UiaPropertyId.ScrollHorizontalViewSize }, + { ScrollPatternIdentifiers.VerticallyScrollableProperty, UiaPropertyId.ScrollVerticallyScrollable }, + { ScrollPatternIdentifiers.VerticalScrollPercentProperty, UiaPropertyId.ScrollVerticalScrollPercent }, + { ScrollPatternIdentifiers.VerticalViewSizeProperty, UiaPropertyId.ScrollVerticalViewSize }, + { SelectionPatternIdentifiers.CanSelectMultipleProperty, UiaPropertyId.SelectionCanSelectMultiple }, + { SelectionPatternIdentifiers.IsSelectionRequiredProperty, UiaPropertyId.SelectionIsSelectionRequired }, + { SelectionPatternIdentifiers.SelectionProperty, UiaPropertyId.SelectionSelection }, + }; + + private static ConditionalWeakTable s_nodes = + new ConditionalWeakTable(); + + private readonly int[] _runtimeId; + private int _raiseFocusChanged; + private int _raisePropertyChanged; + + public AutomationNode(AutomationPeer peer) + { + _runtimeId = new int[] { 3, GetHashCode() }; + Peer = peer; + s_nodes.Add(peer, this); + } + + public AutomationPeer Peer { get; protected set; } + + public Rect BoundingRectangle + { + get => InvokeSync(() => + { + if (GetRoot() is RootAutomationNode root) + return root.ToScreen(Peer.GetBoundingRectangle()); + return default; + }); + } + + public virtual IRawElementProviderFragmentRoot? FragmentRoot + { + get => InvokeSync(() => GetRoot()) as IRawElementProviderFragmentRoot; + } + + public virtual IRawElementProviderSimple? HostRawElementProvider => null; + public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider; + + public void ChildrenChanged() + { + UiaCoreProviderApi.UiaRaiseStructureChangedEvent( + this, + StructureChangeType.ChildrenInvalidated, + null, + 0); + } + + public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) + { + if (_raisePropertyChanged > 0 && s_propertyMap.TryGetValue(property, out var id)) + { + UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(this, (int)id, oldValue, newValue); + } + } + + [return: MarshalAs(UnmanagedType.IUnknown)] + public virtual object? GetPatternProvider(int patternId) + { + AutomationNode? ThisIfPeerImplementsProvider() => Peer.GetProvider() is object ? this : null; + + return (UiaPatternId)patternId switch + { + UiaPatternId.ExpandCollapse => ThisIfPeerImplementsProvider(), + UiaPatternId.Invoke => ThisIfPeerImplementsProvider(), + UiaPatternId.RangeValue => ThisIfPeerImplementsProvider(), + UiaPatternId.Scroll => ThisIfPeerImplementsProvider(), + UiaPatternId.ScrollItem => this, + UiaPatternId.Selection => ThisIfPeerImplementsProvider(), + UiaPatternId.SelectionItem => ThisIfPeerImplementsProvider(), + UiaPatternId.Toggle => ThisIfPeerImplementsProvider(), + UiaPatternId.Value => ThisIfPeerImplementsProvider(), + _ => null, + }; + } + + public virtual object? GetPropertyValue(int propertyId) + { + return (UiaPropertyId)propertyId switch + { + UiaPropertyId.AcceleratorKey => InvokeSync(() => Peer.GetAcceleratorKey()), + UiaPropertyId.AccessKey => InvokeSync(() => Peer.GetAccessKey()), + UiaPropertyId.AutomationId => InvokeSync(() => Peer.GetAutomationId()), + UiaPropertyId.ClassName => InvokeSync(() => Peer.GetClassName()), + UiaPropertyId.ClickablePoint => new[] { BoundingRectangle.Center.X, BoundingRectangle.Center.Y }, + UiaPropertyId.ControlType => InvokeSync(() => ToUiaControlType(Peer.GetAutomationControlType())), + UiaPropertyId.Culture => CultureInfo.CurrentCulture.LCID, + UiaPropertyId.FrameworkId => "Avalonia", + UiaPropertyId.HasKeyboardFocus => InvokeSync(() => Peer.HasKeyboardFocus()), + UiaPropertyId.IsContentElement => InvokeSync(() => Peer.IsContentElement()), + UiaPropertyId.IsControlElement => InvokeSync(() => Peer.IsControlElement()), + UiaPropertyId.IsEnabled => InvokeSync(() => Peer.IsEnabled()), + UiaPropertyId.IsKeyboardFocusable => InvokeSync(() => Peer.IsKeyboardFocusable()), + UiaPropertyId.LocalizedControlType => InvokeSync(() => Peer.GetLocalizedControlType()), + UiaPropertyId.Name => InvokeSync(() => Peer.GetName()), + UiaPropertyId.ProcessId => Process.GetCurrentProcess().Id, + UiaPropertyId.RuntimeId => _runtimeId, + _ => null, + }; + } + + public int[]? GetRuntimeId() => _runtimeId; + + public virtual IRawElementProviderFragment? Navigate(NavigateDirection direction) + { + AutomationNode? GetSibling(int direction) + { + var children = Peer.GetParent()?.GetChildren(); + + for (var i = 0; i < (children?.Count ?? 0); ++i) + { + if (ReferenceEquals(children![i], Peer)) + { + var j = i + direction; + if (j >= 0 && j < children.Count) + return GetOrCreate(children[j]); + } + } + + return null; + } + + return InvokeSync(() => + { + return direction switch + { + NavigateDirection.Parent => GetOrCreate(Peer.GetParent()), + NavigateDirection.NextSibling => GetSibling(1), + NavigateDirection.PreviousSibling => GetSibling(-1), + NavigateDirection.FirstChild => GetOrCreate(Peer.GetChildren().FirstOrDefault()), + NavigateDirection.LastChild => GetOrCreate(Peer.GetChildren().LastOrDefault()), + _ => null, + }; + }) as IRawElementProviderFragment; + } + + public void SetFocus() => InvokeSync(() => Peer.SetFocus()); + + public static AutomationNode? GetOrCreate(AutomationPeer? peer) + { + return peer is null ? null : s_nodes.GetValue(peer, Create); + } + + public static void Release(AutomationPeer peer) => s_nodes.Remove(peer); + + IRawElementProviderSimple[]? IRawElementProviderFragment.GetEmbeddedFragmentRoots() => null; + void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu()); + void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke()); + + void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties) + { + switch ((UiaEventId)eventId) + { + case UiaEventId.AutomationPropertyChanged: + ++_raisePropertyChanged; + break; + case UiaEventId.AutomationFocusChanged: + ++_raiseFocusChanged; + break; + } + } + + void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties) + { + switch ((UiaEventId)eventId) + { + case UiaEventId.AutomationPropertyChanged: + --_raisePropertyChanged; + break; + case UiaEventId.AutomationFocusChanged: + --_raiseFocusChanged; + break; + } + } + + protected void InvokeSync(Action action) + { + if (Dispatcher.UIThread.CheckAccess()) + action(); + else + Dispatcher.UIThread.InvokeAsync(action).Wait(); + } + + protected T InvokeSync(Func func) + { + if (Dispatcher.UIThread.CheckAccess()) + return func(); + else + return Dispatcher.UIThread.InvokeAsync(func).Result; + } + + protected void InvokeSync(Action action) + { + if (Peer.GetProvider() is TInterface i) + { + try + { + InvokeSync(() => action(i)); + } + catch (AggregateException e) when (e.InnerException is ElementNotEnabledException) + { + throw new COMException(e.Message, UiaCoreProviderApi.UIA_E_ELEMENTNOTENABLED); + } + } + else + { + throw new NotSupportedException(); + } + } + + protected TResult InvokeSync(Func func) + { + if (Peer.GetProvider() is TInterface i) + { + try + { + return InvokeSync(() => func(i)); + } + catch (AggregateException e) when (e.InnerException is ElementNotEnabledException) + { + throw new COMException(e.Message, UiaCoreProviderApi.UIA_E_ELEMENTNOTENABLED); + } + } + + throw new NotSupportedException(); + } + + protected void RaiseFocusChanged(AutomationNode? focused) + { + if (_raiseFocusChanged > 0) + { + UiaCoreProviderApi.UiaRaiseAutomationEvent( + focused, + (int)UiaEventId.AutomationFocusChanged); + } + } + + private AutomationNode? GetRoot() + { + Dispatcher.UIThread.VerifyAccess(); + + var peer = Peer; + var parent = peer.GetParent(); + + while (peer.GetProvider() is null && parent is object) + { + peer = parent; + parent = peer.GetParent(); + } + + return peer is object ? GetOrCreate(peer) : null; + } + + private static AutomationNode Create(AutomationPeer peer) + { + return peer.GetProvider() is object ? + new RootAutomationNode(peer) : + new AutomationNode(peer); + } + + private static UiaControlTypeId ToUiaControlType(AutomationControlType role) + { + return role switch + { + AutomationControlType.None => UiaControlTypeId.Group, + AutomationControlType.Button => UiaControlTypeId.Button, + AutomationControlType.Calendar => UiaControlTypeId.Calendar, + AutomationControlType.CheckBox => UiaControlTypeId.CheckBox, + AutomationControlType.ComboBox => UiaControlTypeId.ComboBox, + AutomationControlType.ComboBoxItem => UiaControlTypeId.ListItem, + AutomationControlType.Edit => UiaControlTypeId.Edit, + AutomationControlType.Hyperlink => UiaControlTypeId.Hyperlink, + AutomationControlType.Image => UiaControlTypeId.Image, + AutomationControlType.ListItem => UiaControlTypeId.ListItem, + AutomationControlType.List => UiaControlTypeId.List, + AutomationControlType.Menu => UiaControlTypeId.Menu, + AutomationControlType.MenuBar => UiaControlTypeId.MenuBar, + AutomationControlType.MenuItem => UiaControlTypeId.MenuItem, + AutomationControlType.ProgressBar => UiaControlTypeId.ProgressBar, + AutomationControlType.RadioButton => UiaControlTypeId.RadioButton, + AutomationControlType.ScrollBar => UiaControlTypeId.ScrollBar, + AutomationControlType.Slider => UiaControlTypeId.Slider, + AutomationControlType.Spinner => UiaControlTypeId.Spinner, + AutomationControlType.StatusBar => UiaControlTypeId.StatusBar, + AutomationControlType.Tab => UiaControlTypeId.Tab, + AutomationControlType.TabItem => UiaControlTypeId.TabItem, + AutomationControlType.Text => UiaControlTypeId.Text, + AutomationControlType.ToolBar => UiaControlTypeId.ToolBar, + AutomationControlType.ToolTip => UiaControlTypeId.ToolTip, + AutomationControlType.Tree => UiaControlTypeId.Tree, + AutomationControlType.TreeItem => UiaControlTypeId.TreeItem, + AutomationControlType.Custom => UiaControlTypeId.Custom, + AutomationControlType.Group => UiaControlTypeId.Group, + AutomationControlType.Thumb => UiaControlTypeId.Thumb, + AutomationControlType.DataGrid => UiaControlTypeId.DataGrid, + AutomationControlType.DataItem => UiaControlTypeId.DataItem, + AutomationControlType.Document => UiaControlTypeId.Document, + AutomationControlType.SplitButton => UiaControlTypeId.SplitButton, + AutomationControlType.Window => UiaControlTypeId.Window, + AutomationControlType.Pane => UiaControlTypeId.Pane, + AutomationControlType.Header => UiaControlTypeId.Header, + AutomationControlType.HeaderItem => UiaControlTypeId.HeaderItem, + AutomationControlType.Table => UiaControlTypeId.Table, + AutomationControlType.TitleBar => UiaControlTypeId.TitleBar, + AutomationControlType.Separator => UiaControlTypeId.Separator, + _ => UiaControlTypeId.Custom, + }; + } + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs new file mode 100644 index 0000000000..1085aa1b42 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -0,0 +1,73 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Platform; +using Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal class RootAutomationNode : AutomationNode, + IRawElementProviderFragmentRoot + { + public RootAutomationNode(AutomationPeer peer) + : base(peer) + { + Peer = base.Peer.GetProvider() ?? throw new AvaloniaInternalException( + "Attempt to create RootAutomationNode from peer which does not implement IRootProvider."); + Peer.FocusChanged += FocusChanged; + } + + public override IRawElementProviderFragmentRoot? FragmentRoot => this; + public new IRootProvider Peer { get; } + public IWindowBaseImpl? WindowImpl => Peer.PlatformImpl as IWindowBaseImpl; + + public IRawElementProviderFragment? ElementProviderFromPoint(double x, double y) + { + if (WindowImpl is null) + return null; + + var p = WindowImpl.PointToClient(new PixelPoint((int)x, (int)y)); + var peer = (WindowBaseAutomationPeer)Peer; + var found = InvokeSync(() => peer.GetPeerFromPoint(p)); + var result = GetOrCreate(found) as IRawElementProviderFragment; + return result; + } + + public IRawElementProviderFragment? GetFocus() + { + var focus = InvokeSync(() => Peer.GetFocus()); + return GetOrCreate(focus); + } + + public void FocusChanged(object sender, EventArgs e) + { + RaiseFocusChanged(GetOrCreate(Peer.GetFocus())); + } + + public Rect ToScreen(Rect rect) + { + if (WindowImpl is null) + return default; + return new PixelRect( + WindowImpl.PointToScreen(rect.TopLeft), + WindowImpl.PointToScreen(rect.BottomRight)) + .ToRect(1); + } + + public override IRawElementProviderSimple? HostRawElementProvider + { + get + { + var handle = WindowImpl?.Handle.Handle ?? IntPtr.Zero; + if (handle == IntPtr.Zero) + return null; + var hr = UiaCoreProviderApi.UiaHostProviderFromHwnd(handle, out var result); + Marshal.ThrowExceptionForHR(hr); + return result; + } + } + } +} diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index d727acdc22..b9a1880435 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -7,11 +7,15 @@ + + + - + + diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs index a18116aade..7538dedfca 100644 --- a/src/Windows/Avalonia.Win32/ClipboardFormats.cs +++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs @@ -14,11 +14,11 @@ namespace Avalonia.Win32 class ClipboardFormat { - public short Format { get; private set; } + public ushort Format { get; private set; } public string Name { get; private set; } - public short[] Synthesized { get; private set; } + public ushort[] Synthesized { get; private set; } - public ClipboardFormat(string name, short format, params short[] synthesized) + public ClipboardFormat(string name, ushort format, params ushort[] synthesized) { Format = format; Name = name; @@ -28,12 +28,12 @@ namespace Avalonia.Win32 private static readonly List FormatList = new List() { - new ClipboardFormat(DataFormats.Text, (short)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (short)UnmanagedMethods.ClipboardFormat.CF_TEXT), - new ClipboardFormat(DataFormats.FileNames, (short)UnmanagedMethods.ClipboardFormat.CF_HDROP), + new ClipboardFormat(DataFormats.Text, (ushort)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (ushort)UnmanagedMethods.ClipboardFormat.CF_TEXT), + new ClipboardFormat(DataFormats.FileNames, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP), }; - private static string QueryFormatName(short format) + private static string QueryFormatName(ushort format) { StringBuilder sb = new StringBuilder(MAX_FORMAT_NAME_LENGTH); if (UnmanagedMethods.GetClipboardFormatName(format, sb, sb.Capacity) > 0) @@ -41,7 +41,7 @@ namespace Avalonia.Win32 return null; } - public static string GetFormat(short format) + public static string GetFormat(ushort format) { lock (FormatList) { @@ -58,7 +58,7 @@ namespace Avalonia.Win32 } } - public static short GetFormat(string format) + public static ushort GetFormat(string format) { lock (FormatList) { @@ -68,7 +68,7 @@ namespace Avalonia.Win32 int id = UnmanagedMethods.RegisterClipboardFormat(format); if (id == 0) throw new Win32Exception(); - pd = new ClipboardFormat(format, (short)id); + pd = new ClipboardFormat(format, (ushort)id); FormatList.Add(pd); } return pd.Format; diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 047b7c361f..7cf8b14bed 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -78,12 +78,13 @@ namespace Avalonia.Win32 public async Task SetDataObjectAsync(IDataObject data) { Dispatcher.UIThread.VerifyAccess(); - var wrapper = new DataObject(data); + using var wrapper = new DataObject(data); var i = OleRetryCount; while (true) { - var hr = UnmanagedMethods.OleSetClipboard(wrapper); + var ptr = MicroCom.MicroComRuntime.GetNativeIntPtr(wrapper); + var hr = UnmanagedMethods.OleSetClipboard(ptr); if (hr == 0) break; @@ -106,9 +107,9 @@ namespace Avalonia.Win32 if (hr == 0) { - var wrapper = new OleDataObject(dataObject); + using var proxy = MicroCom.MicroComRuntime.CreateProxyFor(dataObject, true); + using var wrapper = new OleDataObject(proxy); var formats = wrapper.GetDataFormats().ToArray(); - Marshal.ReleaseComObject(dataObject); return formats; } @@ -130,9 +131,9 @@ namespace Avalonia.Win32 if (hr == 0) { - var wrapper = new OleDataObject(dataObject); + using var proxy = MicroCom.MicroComRuntime.CreateProxyFor(dataObject, true); + using var wrapper = new OleDataObject(proxy); var rv = wrapper.Get(format); - Marshal.ReleaseComObject(dataObject); return rv; } diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index 3066403186..7a96ca9ee0 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -8,22 +8,25 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization.Formatters.Binary; using Avalonia.Input; +using Avalonia.MicroCom; using Avalonia.Win32.Interop; + +using FORMATETC = Avalonia.Win32.Interop.FORMATETC; using IDataObject = Avalonia.Input.IDataObject; namespace Avalonia.Win32 { - class DataObject : IDataObject, IOleDataObject + internal sealed class DataObject : CallbackBase, IDataObject, Win32Com.IDataObject { // Compatibility with WinForms + WPF... internal static readonly byte[] SerializedObjectGUID = new Guid("FD9EA796-3B13-4370-A679-56106BB288FB").ToByteArray(); - class FormatEnumerator : IEnumFORMATETC + class FormatEnumerator : CallbackBase, Win32Com.IEnumFORMATETC { private FORMATETC[] _formats; - private int _current; + private uint _current; - private FormatEnumerator(FORMATETC[] formats, int current) + private FormatEnumerator(FORMATETC[] formats, uint current) { _formats = formats; _current = current; @@ -46,59 +49,70 @@ namespace Avalonia.Win32 return result; } - public void Clone(out IEnumFORMATETC newEnum) - { - newEnum = new FormatEnumerator(_formats, _current); - } - - public int Next(int celt, FORMATETC[] rgelt, int[] pceltFetched) + public unsafe uint Next(uint celt, FORMATETC* rgelt, uint* results) { if (rgelt == null) - return unchecked((int)UnmanagedMethods.HRESULT.E_INVALIDARG); + return (uint)UnmanagedMethods.HRESULT.E_INVALIDARG; - int i = 0; + uint i = 0; while (i < celt && _current < _formats.Length) { rgelt[i] = _formats[_current]; _current++; i++; } - if (pceltFetched != null) - pceltFetched[0] = i; if (i != celt) - return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE); - return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + return (uint)UnmanagedMethods.HRESULT.S_FALSE; + + // "results" parameter can be NULL if celt is 1. + if (celt != 1 || results != default) + *results = i; + return 0; + } + + public uint Skip(uint celt) + { + _current += Math.Min(celt, int.MaxValue - _current); + if (_current >= _formats.Length) + return (uint)UnmanagedMethods.HRESULT.S_FALSE; + return 0; } - public int Reset() + public void Reset() { _current = 0; - return unchecked((int)UnmanagedMethods.HRESULT.S_OK); } - public int Skip(int celt) + public Win32Com.IEnumFORMATETC Clone() { - _current += Math.Min(celt, int.MaxValue - _current); - if (_current >= _formats.Length) - return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE); - return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + return new FormatEnumerator(_formats, _current); } } - private const int DV_E_TYMED = unchecked((int)0x80040069); - private const int DV_E_DVASPECT = unchecked((int)0x8004006B); - private const int DV_E_FORMATETC = unchecked((int)0x80040064); - private const int OLE_E_ADVISENOTSUPPORTED = unchecked((int)0x80040003); - private const int STG_E_MEDIUMFULL = unchecked((int)0x80030070); + private const uint DV_E_TYMED = 0x80040069; + private const uint DV_E_DVASPECT = 0x8004006B; + private const uint DV_E_FORMATETC = 0x80040064; + private const uint OLE_E_ADVISENOTSUPPORTED = 0x80040003; + private const uint STG_E_MEDIUMFULL = 0x80030070; + private const int GMEM_ZEROINIT = 0x0040; private const int GMEM_MOVEABLE = 0x0002; - IDataObject _wrapped; - + private IDataObject _wrapped; + public DataObject(IDataObject wrapped) { + if (wrapped == null) + { + throw new ArgumentNullException(nameof(wrapped)); + } + if (_wrapped is DataObject || _wrapped is OleDataObject) + { + throw new InvalidOperationException(); + } + _wrapped = wrapped; } @@ -131,123 +145,123 @@ namespace Avalonia.Win32 #region IOleDataObject - int IOleDataObject.DAdvise(ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection) + unsafe int Win32Com.IDataObject.DAdvise(FORMATETC* pFormatetc, int advf, void* adviseSink) { - if (_wrapped is IOleDataObject ole) - return ole.DAdvise(ref pFormatetc, advf, adviseSink, out connection); - connection = 0; - return OLE_E_ADVISENOTSUPPORTED; + if (_wrapped is Win32Com.IDataObject ole) + return ole.DAdvise(pFormatetc, advf, adviseSink); + return 0; } - void IOleDataObject.DUnadvise(int connection) + void Win32Com.IDataObject.DUnadvise(int connection) { - if (_wrapped is IOleDataObject ole) + if (_wrapped is Win32Com.IDataObject ole) ole.DUnadvise(connection); - Marshal.ThrowExceptionForHR(OLE_E_ADVISENOTSUPPORTED); + throw new COMException(nameof(OLE_E_ADVISENOTSUPPORTED), unchecked((int)OLE_E_ADVISENOTSUPPORTED)); } - int IOleDataObject.EnumDAdvise(out IEnumSTATDATA enumAdvise) + unsafe void* Win32Com.IDataObject.EnumDAdvise() { - if (_wrapped is IOleDataObject ole) - return ole.EnumDAdvise(out enumAdvise); + if (_wrapped is Win32Com.IDataObject ole) + return ole.EnumDAdvise(); - enumAdvise = null; - return OLE_E_ADVISENOTSUPPORTED; + return null; } - IEnumFORMATETC IOleDataObject.EnumFormatEtc(DATADIR direction) + Win32Com.IEnumFORMATETC Win32Com.IDataObject.EnumFormatEtc(int direction) { - if (_wrapped is IOleDataObject ole) + if (_wrapped is Win32Com.IDataObject ole) return ole.EnumFormatEtc(direction); - if (direction == DATADIR.DATADIR_GET) + if ((DATADIR)direction == DATADIR.DATADIR_GET) return new FormatEnumerator(_wrapped); - throw new NotSupportedException(); + throw new COMException(nameof(UnmanagedMethods.HRESULT.E_NOTIMPL), unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL)); } - int IOleDataObject.GetCanonicalFormatEtc(ref FORMATETC formatIn, out FORMATETC formatOut) + unsafe FORMATETC Win32Com.IDataObject.GetCanonicalFormatEtc(FORMATETC* formatIn) { - if (_wrapped is IOleDataObject ole) - return ole.GetCanonicalFormatEtc(ref formatIn, out formatOut); + if (_wrapped is Win32Com.IDataObject ole) + return ole.GetCanonicalFormatEtc(formatIn); - formatOut = new FORMATETC(); + var formatOut = new FORMATETC(); formatOut.ptd = IntPtr.Zero; - return unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL); + + throw new COMException(nameof(UnmanagedMethods.HRESULT.E_NOTIMPL), unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL)); } - void IOleDataObject.GetData(ref FORMATETC format, out STGMEDIUM medium) + unsafe uint Win32Com.IDataObject.GetData(FORMATETC* format, Interop.STGMEDIUM* medium) { - if (_wrapped is IOleDataObject ole) + if (_wrapped is Win32Com.IDataObject ole) { - ole.GetData(ref format, out medium); - return; + return ole.GetData(format, medium); } - if(!format.tymed.HasAllFlags(TYMED.TYMED_HGLOBAL)) - Marshal.ThrowExceptionForHR(DV_E_TYMED); - if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) - Marshal.ThrowExceptionForHR(DV_E_DVASPECT); + if (!format->tymed.HasAllFlags(TYMED.TYMED_HGLOBAL)) + return DV_E_TYMED; + + if (format->dwAspect != DVASPECT.DVASPECT_CONTENT) + return DV_E_DVASPECT; - string fmt = ClipboardFormats.GetFormat(format.cfFormat); + string fmt = ClipboardFormats.GetFormat(format->cfFormat); if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt)) - Marshal.ThrowExceptionForHR(DV_E_FORMATETC); + return DV_E_FORMATETC; - medium = default(STGMEDIUM); - medium.tymed = TYMED.TYMED_HGLOBAL; - int result = WriteDataToHGlobal(fmt, ref medium.unionmember); - Marshal.ThrowExceptionForHR(result); + * medium = default(Interop.STGMEDIUM); + medium->tymed = TYMED.TYMED_HGLOBAL; + return WriteDataToHGlobal(fmt, ref medium->unionmember); } - void IOleDataObject.GetDataHere(ref FORMATETC format, ref STGMEDIUM medium) + unsafe uint Win32Com.IDataObject.GetDataHere(FORMATETC* format, Interop.STGMEDIUM* medium) { - if (_wrapped is IOleDataObject ole) + if (_wrapped is Win32Com.IDataObject ole) { - ole.GetDataHere(ref format, ref medium); - return; + return ole.GetDataHere(format, medium); } - if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasAllFlags(TYMED.TYMED_HGLOBAL)) - Marshal.ThrowExceptionForHR(DV_E_TYMED); + if (medium->tymed != TYMED.TYMED_HGLOBAL || !format->tymed.HasAllFlags(TYMED.TYMED_HGLOBAL)) + return DV_E_TYMED; - if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) - Marshal.ThrowExceptionForHR(DV_E_DVASPECT); + if (format->dwAspect != DVASPECT.DVASPECT_CONTENT) + return DV_E_DVASPECT; - string fmt = ClipboardFormats.GetFormat(format.cfFormat); + string fmt = ClipboardFormats.GetFormat(format->cfFormat); if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt)) - Marshal.ThrowExceptionForHR(DV_E_FORMATETC); + return DV_E_FORMATETC; - if (medium.unionmember == IntPtr.Zero) - Marshal.ThrowExceptionForHR(STG_E_MEDIUMFULL); + if (medium->unionmember == IntPtr.Zero) + return STG_E_MEDIUMFULL; - int result = WriteDataToHGlobal(fmt, ref medium.unionmember); - Marshal.ThrowExceptionForHR(result); + return WriteDataToHGlobal(fmt, ref medium->unionmember); } - int IOleDataObject.QueryGetData(ref FORMATETC format) + unsafe uint Win32Com.IDataObject.QueryGetData(FORMATETC* format) { - if (_wrapped is IOleDataObject ole) - return ole.QueryGetData(ref format); - if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) + if (_wrapped is Win32Com.IDataObject ole) + { + return ole.QueryGetData(format); + } + + if (format->dwAspect != DVASPECT.DVASPECT_CONTENT) return DV_E_DVASPECT; - if (!format.tymed.HasAllFlags(TYMED.TYMED_HGLOBAL)) + if (!format->tymed.HasAllFlags(TYMED.TYMED_HGLOBAL)) return DV_E_TYMED; - string dataFormat = ClipboardFormats.GetFormat(format.cfFormat); - if (!string.IsNullOrEmpty(dataFormat) && _wrapped.Contains(dataFormat)) - return unchecked((int)UnmanagedMethods.HRESULT.S_OK); - return DV_E_FORMATETC; + var dataFormat = ClipboardFormats.GetFormat(format->cfFormat); + + if (string.IsNullOrEmpty(dataFormat) || !_wrapped.Contains(dataFormat)) + return DV_E_FORMATETC; + + return 0; } - void IOleDataObject.SetData(ref FORMATETC formatIn, ref STGMEDIUM medium, bool release) + unsafe uint Win32Com.IDataObject.SetData(FORMATETC* pformatetc, Interop.STGMEDIUM* pmedium, int fRelease) { - if (_wrapped is IOleDataObject ole) + if (_wrapped is Win32Com.IDataObject ole) { - ole.SetData(ref formatIn, ref medium, release); - return; + return ole.SetData(pformatetc, pmedium, fRelease); } - Marshal.ThrowExceptionForHR(unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL)); + return (uint)UnmanagedMethods.HRESULT.E_NOTIMPL; } - private int WriteDataToHGlobal(string dataFormat, ref IntPtr hGlobal) + private uint WriteDataToHGlobal(string dataFormat, ref IntPtr hGlobal) { object data = _wrapped.Get(dataFormat); if (dataFormat == DataFormats.Text || data is string) @@ -288,7 +302,7 @@ namespace Avalonia.Win32 } } - private unsafe int WriteBytesToHGlobal(ref IntPtr hGlobal, ReadOnlySpan data) + private unsafe uint WriteBytesToHGlobal(ref IntPtr hGlobal, ReadOnlySpan data) { int required = data.Length; if (hGlobal == IntPtr.Zero) @@ -312,7 +326,7 @@ namespace Avalonia.Win32 } } - private int WriteFileListToHGlobal(ref IntPtr hGlobal, IEnumerable files) + private uint WriteFileListToHGlobal(ref IntPtr hGlobal, IEnumerable files) { if (!files?.Any() ?? false) return unchecked((int)UnmanagedMethods.HRESULT.S_OK); @@ -344,7 +358,7 @@ namespace Avalonia.Win32 } } - private int WriteStringToHGlobal(ref IntPtr hGlobal, string data) + private uint WriteStringToHGlobal(ref IntPtr hGlobal, string data) { int required = (data.Length + 1) * sizeof(char); if (hGlobal == IntPtr.Zero) @@ -367,6 +381,15 @@ namespace Avalonia.Win32 } } + protected override void Destroyed() + { + ReleaseWrapped(); + } + + public void ReleaseWrapped() + { + _wrapped = null; + } #endregion } } diff --git a/src/Windows/Avalonia.Win32/DragSource.cs b/src/Windows/Avalonia.Win32/DragSource.cs index f74e59a8f8..1159c5bfc9 100644 --- a/src/Windows/Avalonia.Win32/DragSource.cs +++ b/src/Windows/Avalonia.Win32/DragSource.cs @@ -8,17 +8,26 @@ namespace Avalonia.Win32 { class DragSource : IPlatformDragSource { - public Task DoDragDrop(PointerEventArgs triggerEvent, + public unsafe Task DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects) { Dispatcher.UIThread.VerifyAccess(); + triggerEvent.Pointer.Capture(null); - OleDragSource src = new OleDragSource(); - DataObject dataObject = new DataObject(data); - int allowed = (int)OleDropTarget.ConvertDropEffect(allowedEffects); + + using var dataObject = new DataObject(data); + using var src = new OleDragSource(); + var allowed = OleDropTarget.ConvertDropEffect(allowedEffects); + + var objPtr = MicroCom.MicroComRuntime.GetNativeIntPtr(dataObject); + var srcPtr = MicroCom.MicroComRuntime.GetNativeIntPtr(src); + + UnmanagedMethods.DoDragDrop(objPtr, srcPtr, (int)allowed, out var finalEffect); + + // Force releasing of internal wrapper to avoid memory leak, if drop target keeps com reference. + dataObject.ReleaseWrapped(); - UnmanagedMethods.DoDragDrop(dataObject, src, allowed, out var finalEffect); - return Task.FromResult(OleDropTarget.ConvertDropEffect((DropEffect)finalEffect)); + return Task.FromResult(OleDropTarget.ConvertDropEffect((Win32Com.DropEffect)finalEffect)); } } } diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index 71e33554f1..9ff6f76ac4 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -74,12 +74,12 @@ namespace Avalonia.Win32.Input } } - public void SetActive(bool active) + public void SetClient(ITextInputMethodClient client) { - _active = active; + _active = client is { }; Dispatcher.UIThread.Post(() => { - if (active) + if (_active) { if (DefaultImc != IntPtr.Zero) { @@ -216,7 +216,7 @@ namespace Avalonia.Win32.Input ImmSetCompositionFont(himc, ref logFont); } - public void SetOptions(TextInputOptionsQueryEventArgs options) + public void SetOptions(TextInputOptions options) { // we're skipping this. not usable on windows } diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IDockProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IDockProvider.cs new file mode 100644 index 0000000000..2787434d26 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IDockProvider.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("70d46e77-e3a8-449d-913c-e30eb2afecdb")] + public enum DockPosition + { + Top, + Left, + Bottom, + Right, + Fill, + None + } + + [ComVisible(true)] + [Guid("159bc72c-4ad3-485e-9637-d7052edf0146")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IDockProvider + { + void SetDockPosition(DockPosition dockPosition); + DockPosition DockPosition { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IExpandCollapseProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IExpandCollapseProvider.cs new file mode 100644 index 0000000000..67be1e6c71 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IExpandCollapseProvider.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Automation; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("d847d3a5-cab0-4a98-8c32-ecb45c59ad24")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IExpandCollapseProvider + { + void Expand(); + void Collapse(); + ExpandCollapseState ExpandCollapseState { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IGridItemProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IGridItemProvider.cs new file mode 100644 index 0000000000..f911c38472 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IGridItemProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("d02541f1-fb81-4d64-ae32-f520f8a6dbd1")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IGridItemProvider + { + int Row { get; } + int Column { get; } + int RowSpan { get; } + int ColumnSpan { get; } + IRawElementProviderSimple ContainingGrid { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs new file mode 100644 index 0000000000..a8caf26524 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("b17d6187-0907-464b-a168-0ef17a1572b1")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IGridProvider + { + IRawElementProviderSimple GetItem(int row, int column); + int RowCount { get; } + int ColumnCount { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IInvokeProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IInvokeProvider.cs new file mode 100644 index 0000000000..f35646d456 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IInvokeProvider.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Description: Invoke pattern provider interface + +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("54fcb24b-e18e-47a2-b4d3-eccbe77599a2")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IInvokeProvider + { + void Invoke(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IMultipleViewProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IMultipleViewProvider.cs new file mode 100644 index 0000000000..c487a0f5df --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IMultipleViewProvider.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("6278cab1-b556-4a1a-b4e0-418acc523201")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IMultipleViewProvider + { + string GetViewName(int viewId); + void SetCurrentView(int viewId); + int CurrentView { get; } + int[] GetSupportedViews(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRangeValueProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRangeValueProvider.cs new file mode 100644 index 0000000000..558f38a2cc --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRangeValueProvider.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("36dc7aef-33e6-4691-afe1-2be7274b3d33")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IRangeValueProvider + { + void SetValue(double value); + double Value { get; } + bool IsReadOnly { [return: MarshalAs(UnmanagedType.Bool)] get; } + double Maximum { get; } + double Minimum { get; } + double LargeChange { get; } + double SmallChange { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderAdviseEvents.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderAdviseEvents.cs new file mode 100644 index 0000000000..1e799e05a2 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderAdviseEvents.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.InteropServices; + + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("a407b27b-0f6d-4427-9292-473c7bf93258")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IRawElementProviderAdviseEvents : IRawElementProviderSimple + { + void AdviseEventAdded(int eventId, int [] properties); + void AdviseEventRemoved(int eventId, int [] properties); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs new file mode 100644 index 0000000000..a62aa842cb --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.InteropServices; + +#nullable enable + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("670c3006-bf4c-428b-8534-e1848f645122")] + public enum NavigateDirection + { + Parent, + NextSibling, + PreviousSibling, + FirstChild, + LastChild, + } + + // NOTE: This interface needs to be public otherwise Navigate is never called. I have no idea + // why given that IRawElementProviderSimple and IRawElementProviderFragmentRoot seem to get + // called fine when they're internal, but I lost a couple of days to this. + [ComVisible(true)] + [Guid("f7063da8-8359-439c-9297-bbc5299a7d87")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IRawElementProviderFragment : IRawElementProviderSimple + { + IRawElementProviderFragment? Navigate(NavigateDirection direction); + int[]? GetRuntimeId(); + Rect BoundingRectangle { get; } + IRawElementProviderSimple[]? GetEmbeddedFragmentRoots(); + void SetFocus(); + IRawElementProviderFragmentRoot? FragmentRoot { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs new file mode 100644 index 0000000000..71d1bdce60 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("620ce2a5-ab8f-40a9-86cb-de3c75599b58")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IRawElementProviderFragmentRoot : IRawElementProviderFragment + { + IRawElementProviderFragment ElementProviderFromPoint(double x, double y); + IRawElementProviderFragment GetFocus(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs new file mode 100644 index 0000000000..439036290e --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs @@ -0,0 +1,285 @@ +using System; +using System.Runtime.InteropServices; + +#nullable enable + +namespace Avalonia.Win32.Interop.Automation +{ + [Flags] + public enum ProviderOptions + { + ClientSideProvider = 0x0001, + ServerSideProvider = 0x0002, + NonClientAreaProvider = 0x0004, + OverrideProvider = 0x0008, + ProviderOwnsSetFocus = 0x0010, + UseComThreading = 0x0020 + } + + internal enum UiaPropertyId + { + RuntimeId = 30000, + BoundingRectangle, + ProcessId, + ControlType, + LocalizedControlType, + Name, + AcceleratorKey, + AccessKey, + HasKeyboardFocus, + IsKeyboardFocusable, + IsEnabled, + AutomationId, + ClassName, + HelpText, + ClickablePoint, + Culture, + IsControlElement, + IsContentElement, + LabeledBy, + IsPassword, + NativeWindowHandle, + ItemType, + IsOffscreen, + Orientation, + FrameworkId, + IsRequiredForForm, + ItemStatus, + IsDockPatternAvailable, + IsExpandCollapsePatternAvailable, + IsGridItemPatternAvailable, + IsGridPatternAvailable, + IsInvokePatternAvailable, + IsMultipleViewPatternAvailable, + IsRangeValuePatternAvailable, + IsScrollPatternAvailable, + IsScrollItemPatternAvailable, + IsSelectionItemPatternAvailable, + IsSelectionPatternAvailable, + IsTablePatternAvailable, + IsTableItemPatternAvailable, + IsTextPatternAvailable, + IsTogglePatternAvailable, + IsTransformPatternAvailable, + IsValuePatternAvailable, + IsWindowPatternAvailable, + ValueValue, + ValueIsReadOnly, + RangeValueValue, + RangeValueIsReadOnly, + RangeValueMinimum, + RangeValueMaximum, + RangeValueLargeChange, + RangeValueSmallChange, + ScrollHorizontalScrollPercent, + ScrollHorizontalViewSize, + ScrollVerticalScrollPercent, + ScrollVerticalViewSize, + ScrollHorizontallyScrollable, + ScrollVerticallyScrollable, + SelectionSelection, + SelectionCanSelectMultiple, + SelectionIsSelectionRequired, + GridRowCount, + GridColumnCount, + GridItemRow, + GridItemColumn, + GridItemRowSpan, + GridItemColumnSpan, + GridItemContainingGrid, + DockDockPosition, + ExpandCollapseExpandCollapseState, + MultipleViewCurrentView, + MultipleViewSupportedViews, + WindowCanMaximize, + WindowCanMinimize, + WindowWindowVisualState, + WindowWindowInteractionState, + WindowIsModal, + WindowIsTopmost, + SelectionItemIsSelected, + SelectionItemSelectionContainer, + TableRowHeaders, + TableColumnHeaders, + TableRowOrColumnMajor, + TableItemRowHeaderItems, + TableItemColumnHeaderItems, + ToggleToggleState, + TransformCanMove, + TransformCanResize, + TransformCanRotate, + IsLegacyIAccessiblePatternAvailable, + LegacyIAccessibleChildId, + LegacyIAccessibleName, + LegacyIAccessibleValue, + LegacyIAccessibleDescription, + LegacyIAccessibleRole, + LegacyIAccessibleState, + LegacyIAccessibleHelp, + LegacyIAccessibleKeyboardShortcut, + LegacyIAccessibleSelection, + LegacyIAccessibleDefaultAction, + AriaRole, + AriaProperties, + IsDataValidForForm, + ControllerFor, + DescribedBy, + FlowsTo, + ProviderDescription, + IsItemContainerPatternAvailable, + IsVirtualizedItemPatternAvailable, + IsSynchronizedInputPatternAvailable, + OptimizeForVisualContent, + IsObjectModelPatternAvailable, + AnnotationAnnotationTypeId, + AnnotationAnnotationTypeName, + AnnotationAuthor, + AnnotationDateTime, + AnnotationTarget, + IsAnnotationPatternAvailable, + IsTextPattern2Available, + StylesStyleId, + StylesStyleName, + StylesFillColor, + StylesFillPatternStyle, + StylesShape, + StylesFillPatternColor, + StylesExtendedProperties, + IsStylesPatternAvailable, + IsSpreadsheetPatternAvailable, + SpreadsheetItemFormula, + SpreadsheetItemAnnotationObjects, + SpreadsheetItemAnnotationTypes, + IsSpreadsheetItemPatternAvailable, + Transform2CanZoom, + IsTransformPattern2Available, + LiveSetting, + IsTextChildPatternAvailable, + IsDragPatternAvailable, + DragIsGrabbed, + DragDropEffect, + DragDropEffects, + IsDropTargetPatternAvailable, + DropTargetDropTargetEffect, + DropTargetDropTargetEffects, + DragGrabbedItems, + Transform2ZoomLevel, + Transform2ZoomMinimum, + Transform2ZoomMaximum, + FlowsFrom, + IsTextEditPatternAvailable, + IsPeripheral, + IsCustomNavigationPatternAvailable, + PositionInSet, + SizeOfSet, + Level, + AnnotationTypes, + AnnotationObjects, + LandmarkType, + LocalizedLandmarkType, + FullDescription, + FillColor, + OutlineColor, + FillType, + VisualEffects, + OutlineThickness, + CenterPoint, + Rotatation, + Size + } + + internal enum UiaPatternId + { + Invoke = 10000, + Selection, + Value, + RangeValue, + Scroll, + ExpandCollapse, + Grid, + GridItem, + MultipleView, + Window, + SelectionItem, + Dock, + Table, + TableItem, + Text, + Toggle, + Transform, + ScrollItem, + LegacyIAccessible, + ItemContainer, + VirtualizedItem, + SynchronizedInput, + ObjectModel, + Annotation, + Text2, + Styles, + Spreadsheet, + SpreadsheetItem, + Transform2, + TextChild, + Drag, + DropTarget, + TextEdit, + CustomNavigation + }; + + internal enum UiaControlTypeId + { + Button = 50000, + Calendar, + CheckBox, + ComboBox, + Edit, + Hyperlink, + Image, + ListItem, + List, + Menu, + MenuBar, + MenuItem, + ProgressBar, + RadioButton, + ScrollBar, + Slider, + Spinner, + StatusBar, + Tab, + TabItem, + Text, + ToolBar, + ToolTip, + Tree, + TreeItem, + Custom, + Group, + Thumb, + DataGrid, + DataItem, + Document, + SplitButton, + Window, + Pane, + Header, + HeaderItem, + Table, + TitleBar, + Separator, + SemanticZoom, + AppBar + }; + + [ComVisible(true)] + [Guid("d6dd68d1-86fd-4332-8666-9abedea2d24c")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IRawElementProviderSimple + { + ProviderOptions ProviderOptions { get; } + [return: MarshalAs(UnmanagedType.IUnknown)] + object? GetPatternProvider(int patternId); + object? GetPropertyValue(int propertyId); + IRawElementProviderSimple? HostRawElementProvider { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs new file mode 100644 index 0000000000..f3504b8d77 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.InteropServices; + +#nullable enable + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("a0a839a9-8da1-4a82-806a-8e0d44e79f56")] + public interface IRawElementProviderSimple2 + { + void ShowContextMenu(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IScrollItemProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IScrollItemProvider.cs new file mode 100644 index 0000000000..c34c8667ef --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IScrollItemProvider.cs @@ -0,0 +1,13 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("2360c714-4bf1-4b26-ba65-9b21316127eb")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IScrollItemProvider + { + void ScrollIntoView(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IScrollProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IScrollProvider.cs new file mode 100644 index 0000000000..154d52c6af --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IScrollProvider.cs @@ -0,0 +1,21 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Automation.Provider; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("b38b8077-1fc3-42a5-8cae-d40c2215055a")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IScrollProvider + { + void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount); + void SetScrollPercent(double horizontalPercent, double verticalPercent); + double HorizontalScrollPercent { get; } + double VerticalScrollPercent { get; } + double HorizontalViewSize { get; } + double VerticalViewSize { get; } + bool HorizontallyScrollable { [return: MarshalAs(UnmanagedType.Bool)] get; } + bool VerticallyScrollable { [return: MarshalAs(UnmanagedType.Bool)] get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs new file mode 100644 index 0000000000..1de0cf0f9b --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("2acad808-b2d4-452d-a407-91ff1ad167b2")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ISelectionItemProvider + { + void Select(); + void AddToSelection(); + void RemoveFromSelection(); + bool IsSelected { [return: MarshalAs(UnmanagedType.Bool)] get; } + IRawElementProviderSimple? SelectionContainer { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionProvider.cs new file mode 100644 index 0000000000..8a5924126d --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("fb8b03af-3bdf-48d4-bd36-1a65793be168")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ISelectionProvider + { + IRawElementProviderSimple [] GetSelection(); + bool CanSelectMultiple { [return: MarshalAs(UnmanagedType.Bool)] get; } + bool IsSelectionRequired { [return: MarshalAs(UnmanagedType.Bool)] get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ISynchronizedInputProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ISynchronizedInputProvider.cs new file mode 100644 index 0000000000..def1bbd4b9 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ISynchronizedInputProvider.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("fdc8f176-aed2-477a-8c89-5604c66f278d")] + public enum SynchronizedInputType + { + KeyUp = 0x01, + KeyDown = 0x02, + MouseLeftButtonUp = 0x04, + MouseLeftButtonDown = 0x08, + MouseRightButtonUp = 0x10, + MouseRightButtonDown = 0x20 + } + + [ComVisible(true)] + [Guid("29db1a06-02ce-4cf7-9b42-565d4fab20ee")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ISynchronizedInputProvider + { + void StartListening(SynchronizedInputType inputType); + void Cancel(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ITableItemProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ITableItemProvider.cs new file mode 100644 index 0000000000..36751122d1 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ITableItemProvider.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("b9734fa6-771f-4d78-9c90-2517999349cd")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITableItemProvider : IGridItemProvider + { + IRawElementProviderSimple [] GetRowHeaderItems(); + IRawElementProviderSimple [] GetColumnHeaderItems(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ITableProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ITableProvider.cs new file mode 100644 index 0000000000..e82bda3272 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ITableProvider.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("15fdf2e2-9847-41cd-95dd-510612a025ea")] + public enum RowOrColumnMajor + { + RowMajor, + ColumnMajor, + Indeterminate, + } + + [ComVisible(true)] + [Guid("9c860395-97b3-490a-b52a-858cc22af166")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITableProvider : IGridProvider + { + IRawElementProviderSimple [] GetRowHeaders(); + IRawElementProviderSimple [] GetColumnHeaders(); + RowOrColumnMajor RowOrColumnMajor { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs new file mode 100644 index 0000000000..3f8fbc80c7 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [Flags] + [ComVisible(true)] + [Guid("3d9e3d8f-bfb0-484f-84ab-93ff4280cbc4")] + public enum SupportedTextSelection + { + None, + Single, + Multiple, + } + + [ComVisible(true)] + [Guid("3589c92c-63f3-4367-99bb-ada653b77cf2")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITextProvider + { + ITextRangeProvider [] GetSelection(); + ITextRangeProvider [] GetVisibleRanges(); + ITextRangeProvider RangeFromChild(IRawElementProviderSimple childElement); + ITextRangeProvider RangeFromPoint(Point screenLocation); + ITextRangeProvider DocumentRange { get; } + SupportedTextSelection SupportedTextSelection { get; } + } +} + + diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ITextRangeProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ITextRangeProvider.cs new file mode 100644 index 0000000000..9ebb4c9f49 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ITextRangeProvider.cs @@ -0,0 +1,48 @@ +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + public enum TextPatternRangeEndpoint + { + Start = 0, + End = 1, + } + + public enum TextUnit + { + Character = 0, + Format = 1, + Word = 2, + Line = 3, + Paragraph = 4, + Page = 5, + Document = 6, + } + + [ComVisible(true)] + [Guid("5347ad7b-c355-46f8-aff5-909033582f63")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITextRangeProvider + + { + ITextRangeProvider Clone(); + [return: MarshalAs(UnmanagedType.Bool)] + bool Compare(ITextRangeProvider range); + int CompareEndpoints(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint); + void ExpandToEnclosingUnit(TextUnit unit); + ITextRangeProvider FindAttribute(int attribute, object value, [MarshalAs(UnmanagedType.Bool)] bool backward); + ITextRangeProvider FindText(string text, [MarshalAs(UnmanagedType.Bool)] bool backward, [MarshalAs(UnmanagedType.Bool)] bool ignoreCase); + object GetAttributeValue(int attribute); + double [] GetBoundingRectangles(); + IRawElementProviderSimple GetEnclosingElement(); + string GetText(int maxLength); + int Move(TextUnit unit, int count); + int MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, TextUnit unit, int count); + void MoveEndpointByRange(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint); + void Select(); + void AddToSelection(); + void RemoveFromSelection(); + void ScrollIntoView([MarshalAs(UnmanagedType.Bool)] bool alignToTop); + IRawElementProviderSimple[] GetChildren(); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IToggleProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IToggleProvider.cs new file mode 100644 index 0000000000..e4072a1250 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IToggleProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Automation.Provider; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("56d00bd0-c4f4-433c-a836-1a52a57e0892")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IToggleProvider + { + void Toggle( ); + ToggleState ToggleState { get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ITransformProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ITransformProvider.cs new file mode 100644 index 0000000000..4859f2d078 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ITransformProvider.cs @@ -0,0 +1,18 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("6829ddc4-4f91-4ffa-b86f-bd3e2987cb4c")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITransformProvider + { + void Move( double x, double y ); + void Resize( double width, double height ); + void Rotate( double degrees ); + bool CanMove { [return: MarshalAs(UnmanagedType.Bool)] get; } + bool CanResize { [return: MarshalAs(UnmanagedType.Bool)] get; } + bool CanRotate { [return: MarshalAs(UnmanagedType.Bool)] get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IValueProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IValueProvider.cs new file mode 100644 index 0000000000..919be647f8 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IValueProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.InteropServices; + +#nullable enable + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("c7935180-6fb3-4201-b174-7df73adbf64a")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IValueProvider + { + void SetValue([MarshalAs(UnmanagedType.LPWStr)] string? value); + string? Value { get; } + bool IsReadOnly { [return: MarshalAs(UnmanagedType.Bool)] get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IWindowProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IWindowProvider.cs new file mode 100644 index 0000000000..d915beb601 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IWindowProvider.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("fdc8f176-aed2-477a-8c89-ea04cc5f278d")] + public enum WindowVisualState + { + Normal, + Maximized, + Minimized + } + + [ComVisible(true)] + [Guid("65101cc7-7904-408e-87a7-8c6dbd83a18b")] + public enum WindowInteractionState + { + Running, + Closing, + ReadyForUserInteraction, + BlockedByModalWindow, + NotResponding + } + + [ComVisible(true)] + [Guid("987df77b-db06-4d77-8f8a-86a9c3bb90b9")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IWindowProvider + { + void SetVisualState(WindowVisualState state); + void Close(); + [return: MarshalAs(UnmanagedType.Bool)] + bool WaitForInputIdle(int milliseconds); + bool Maximizable { [return: MarshalAs(UnmanagedType.Bool)] get; } + bool Minimizable { [return: MarshalAs(UnmanagedType.Bool)] get; } + bool IsModal { [return: MarshalAs(UnmanagedType.Bool)] get; } + WindowVisualState VisualState { get; } + WindowInteractionState InteractionState { get; } + bool IsTopmost { [return: MarshalAs(UnmanagedType.Bool)] get; } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreProviderApi.cs b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreProviderApi.cs new file mode 100644 index 0000000000..4ba7a710d4 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreProviderApi.cs @@ -0,0 +1,91 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + [ComVisible(true)] + [Guid("d8e55844-7043-4edc-979d-593cc6b4775e")] + internal enum AsyncContentLoadedState + { + Beginning, + Progress, + Completed, + } + + [ComVisible(true)] + [Guid("e4cfef41-071d-472c-a65c-c14f59ea81eb")] + internal enum StructureChangeType + { + ChildAdded, + ChildRemoved, + ChildrenInvalidated, + ChildrenBulkAdded, + ChildrenBulkRemoved, + ChildrenReordered, + } + + internal enum UiaEventId + { + ToolTipOpened = 20000, + ToolTipClosed, + StructureChanged, + MenuOpened, + AutomationPropertyChanged, + AutomationFocusChanged, + AsyncContentLoaded, + MenuClosed, + LayoutInvalidated, + Invoke_Invoked, + SelectionItem_ElementAddedToSelection, + SelectionItem_ElementRemovedFromSelection, + SelectionItem_ElementSelected, + Selection_Invalidated, + Text_TextSelectionChanged, + Text_TextChanged, + Window_WindowOpened, + Window_WindowClosed, + MenuModeStart, + MenuModeEnd, + InputReachedTarget, + InputReachedOtherElement, + InputDiscarded, + SystemAlert, + LiveRegionChanged, + HostedFragmentRootsInvalidated, + Drag_DragStart, + Drag_DragCancel, + Drag_DragComplete, + DropTarget_DragEnter, + DropTarget_DragLeave, + DropTarget_Dropped, + TextEdit_TextChanged, + TextEdit_ConversionTargetChanged, + Changes + }; + + internal static class UiaCoreProviderApi + { + public const int UIA_E_ELEMENTNOTENABLED = unchecked((int)0x80040200); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern bool UiaClientsAreListening(); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr UiaReturnRawElementProvider(IntPtr hwnd, IntPtr wParam, IntPtr lParam, IRawElementProviderSimple el); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern int UiaHostProviderFromHwnd(IntPtr hwnd, [MarshalAs(UnmanagedType.Interface)] out IRawElementProviderSimple provider); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern int UiaRaiseAutomationEvent(IRawElementProviderSimple provider, int id); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern int UiaRaiseAutomationPropertyChangedEvent(IRawElementProviderSimple provider, int id, object oldValue, object newValue); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern int UiaRaiseStructureChangedEvent(IRawElementProviderSimple provider, StructureChangeType structureChangeType, int[] runtimeId, int runtimeIdLen); + + [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] + public static extern int UiaDisconnectProvider(IRawElementProviderSimple provider); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs new file mode 100644 index 0000000000..4375b2fde1 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs @@ -0,0 +1,62 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Win32.Interop.Automation +{ + internal static class UiaCoreTypesApi + { + private const string StartListeningExportName = "SynchronizedInputPattern_StartListening"; + + internal enum AutomationIdType + { + Property, + Pattern, + Event, + ControlType, + TextAttribute + } + + internal const int UIA_E_ELEMENTNOTENABLED = unchecked((int)0x80040200); + internal const int UIA_E_ELEMENTNOTAVAILABLE = unchecked((int)0x80040201); + internal const int UIA_E_NOCLICKABLEPOINT = unchecked((int)0x80040202); + internal const int UIA_E_PROXYASSEMBLYNOTLOADED = unchecked((int)0x80040203); + + internal static int UiaLookupId(AutomationIdType type, ref Guid guid) + { + return RawUiaLookupId( type, ref guid ); + } + + internal static object UiaGetReservedNotSupportedValue() + { + object notSupportedValue; + CheckError(RawUiaGetReservedNotSupportedValue(out notSupportedValue)); + return notSupportedValue; + } + + internal static object UiaGetReservedMixedAttributeValue() + { + object mixedAttributeValue; + CheckError(RawUiaGetReservedMixedAttributeValue(out mixedAttributeValue)); + return mixedAttributeValue; + } + + private static void CheckError(int hr) + { + if (hr >= 0) + { + return; + } + + Marshal.ThrowExceptionForHR(hr, (IntPtr)(-1)); + } + + [DllImport("UIAutomationCore.dll", EntryPoint = "UiaLookupId", CharSet = CharSet.Unicode)] + private static extern int RawUiaLookupId(AutomationIdType type, ref Guid guid); + + [DllImport("UIAutomationCore.dll", EntryPoint = "UiaGetReservedNotSupportedValue", CharSet = CharSet.Unicode)] + private static extern int RawUiaGetReservedNotSupportedValue([MarshalAs(UnmanagedType.IUnknown)] out object notSupportedValue); + + [DllImport("UIAutomationCore.dll", EntryPoint = "UiaGetReservedMixedAttributeValue", CharSet = CharSet.Unicode)] + private static extern int RawUiaGetReservedMixedAttributeValue([MarshalAs(UnmanagedType.IUnknown)] out object mixedAttributeValue); + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index c74c5fbc01..9d53691597 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -765,6 +765,14 @@ namespace Avalonia.Win32.Interop DWMWA_CLOAK, DWMWA_CLOAKED, DWMWA_FREEZE_REPRESENTATION, + DWMWA_PASSIVE_UPDATE_MODE, + DWMWA_USE_HOSTBACKDROPBRUSH, + DWMWA_USE_IMMERSIVE_DARK_MODE = 20, + DWMWA_WINDOW_CORNER_PREFERENCE = 33, + DWMWA_BORDER_COLOR, + DWMWA_CAPTION_COLOR, + DWMWA_TEXT_COLOR, + DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, DWMWA_LAST }; @@ -862,7 +870,7 @@ namespace Avalonia.Win32.Interop public void Init() { - biSize = (uint)Marshal.SizeOf(this); + biSize = (uint)sizeof(BITMAPINFOHEADER); } } @@ -1277,10 +1285,10 @@ namespace Avalonia.Win32.Interop public static extern IntPtr SetClipboardData(ClipboardFormat uFormat, IntPtr hMem); [DllImport("ole32.dll", PreserveSig = false)] - public static extern int OleGetClipboard(out IOleDataObject dataObject); + public static extern int OleGetClipboard(out IntPtr dataObject); [DllImport("ole32.dll", PreserveSig = true)] - public static extern int OleSetClipboard(IOleDataObject dataObject); + public static extern int OleSetClipboard(IntPtr dataObject); [DllImport("kernel32.dll", ExactSpelling = true)] public static extern IntPtr GlobalLock(IntPtr handle); @@ -1424,7 +1432,7 @@ namespace Avalonia.Win32.Interop public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count); [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] - public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target); + public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IntPtr target); [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] public static extern HRESULT RevokeDragDrop(IntPtr hwnd); @@ -1448,7 +1456,7 @@ namespace Avalonia.Win32.Interop public static extern int DragQueryFile(IntPtr hDrop, int iFile, StringBuilder lpszFile, int cch); [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)] - internal static extern void DoDragDrop(IOleDataObject dataObject, IDropSource dropSource, int allowedEffects, out int finalEffect); + internal static extern void DoDragDrop(IntPtr dataObject, IntPtr dropSource, int allowedEffects, [Out] out int finalEffect); [DllImport("dwmapi.dll")] public static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); @@ -1456,6 +1464,9 @@ namespace Avalonia.Win32.Interop [DllImport("dwmapi.dll")] public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out RECT pvAttribute, int cbAttribute); + [DllImport("dwmapi.dll")] + public static extern int DwmSetWindowAttribute(IntPtr hwnd, int dwAttribute, void* pvAttribute, int cbAttribute); + [DllImport("dwmapi.dll")] public static extern int DwmIsCompositionEnabled(out bool enabled); @@ -1521,7 +1532,7 @@ namespace Avalonia.Win32.Interop internal static Version RtlGetVersion() { RTL_OSVERSIONINFOEX v = new RTL_OSVERSIONINFOEX(); - v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(v); + v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(); if (RtlGetVersion(ref v) == 0) { return new Version((int)v.dwMajorVersion, (int)v.dwMinorVersion, (int)v.dwBuildNumber); @@ -1914,7 +1925,7 @@ namespace Avalonia.Win32.Interop get { WINDOWPLACEMENT result = new WINDOWPLACEMENT(); - result.Length = Marshal.SizeOf(result); + result.Length = Marshal.SizeOf(); return result; } } @@ -2064,64 +2075,6 @@ namespace Avalonia.Win32.Interop } } - [Flags] - internal enum DropEffect : int - { - None = 0, - Copy = 1, - Move = 2, - Link = 4, - Scroll = -2147483648, - } - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("00000122-0000-0000-C000-000000000046")] - internal interface IDropTarget - { - [PreserveSig] - UnmanagedMethods.HRESULT DragEnter([MarshalAs(UnmanagedType.Interface)] [In] IOleDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); - [PreserveSig] - UnmanagedMethods.HRESULT DragOver([MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); - [PreserveSig] - UnmanagedMethods.HRESULT DragLeave(); - [PreserveSig] - UnmanagedMethods.HRESULT Drop([MarshalAs(UnmanagedType.Interface)] [In] IOleDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); - } - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("00000121-0000-0000-C000-000000000046")] - internal interface IDropSource - { - [PreserveSig] - int QueryContinueDrag(int fEscapePressed, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState); - [PreserveSig] - int GiveFeedback([MarshalAs(UnmanagedType.U4)] [In] int dwEffect); - } - - - [ComImport] - [Guid("0000010E-0000-0000-C000-000000000046")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IOleDataObject - { - void GetData([In] ref FORMATETC format, out STGMEDIUM medium); - void GetDataHere([In] ref FORMATETC format, ref STGMEDIUM medium); - [PreserveSig] - int QueryGetData([In] ref FORMATETC format); - [PreserveSig] - int GetCanonicalFormatEtc([In] ref FORMATETC formatIn, out FORMATETC formatOut); - void SetData([In] ref FORMATETC formatIn, [In] ref STGMEDIUM medium, [MarshalAs(UnmanagedType.Bool)] bool release); - IEnumFORMATETC EnumFormatEtc(DATADIR direction); - [PreserveSig] - int DAdvise([In] ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection); - void DUnadvise(int connection); - [PreserveSig] - int EnumDAdvise(out IEnumSTATDATA enumAdvise); - } - - [StructLayoutAttribute(LayoutKind.Sequential)] internal struct _DROPFILES { @@ -2132,6 +2085,24 @@ namespace Avalonia.Win32.Interop public bool fWide; } + [StructLayoutAttribute(LayoutKind.Sequential)] + internal struct STGMEDIUM + { + public TYMED tymed; + public IntPtr unionmember; + public IntPtr pUnkForRelease; + } + + [StructLayoutAttribute(LayoutKind.Sequential)] + internal struct FORMATETC + { + public ushort cfFormat; + public IntPtr ptd; + public DVASPECT dwAspect; + public int lindex; + public TYMED tymed; + } + [Flags] internal enum PixelFormatDescriptorFlags : uint { diff --git a/src/Windows/Avalonia.Win32/OleContext.cs b/src/Windows/Avalonia.Win32/OleContext.cs index c6e04a29b4..c025d06fe7 100644 --- a/src/Windows/Avalonia.Win32/OleContext.cs +++ b/src/Windows/Avalonia.Win32/OleContext.cs @@ -4,6 +4,7 @@ using System.Threading; using Avalonia.Platform; using Avalonia.Threading; using Avalonia.Win32.Interop; +using Avalonia.Win32.Win32Com; namespace Avalonia.Win32 { @@ -46,7 +47,8 @@ namespace Avalonia.Win32 return false; } - return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, target) == UnmanagedMethods.HRESULT.S_OK; + var trgPtr = MicroCom.MicroComRuntime.GetNativeIntPtr(target); + return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, trgPtr) == UnmanagedMethods.HRESULT.S_OK; } internal bool UnregisterDragDrop(IPlatformHandle hwnd) diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs index f111fe0a6b..ba17177473 100644 --- a/src/Windows/Avalonia.Win32/OleDataObject.cs +++ b/src/Windows/Avalonia.Win32/OleDataObject.cs @@ -1,7 +1,6 @@ using System; using System.Buffers; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -9,17 +8,20 @@ using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using Avalonia.Input; +using Avalonia.MicroCom; using Avalonia.Win32.Interop; +using IDataObject = Avalonia.Input.IDataObject; + namespace Avalonia.Win32 { - class OleDataObject : Avalonia.Input.IDataObject + internal class OleDataObject : IDataObject, IDisposable { - private IOleDataObject _wrapped; + private readonly Win32Com.IDataObject _wrapped; - public OleDataObject(IOleDataObject wrapped) + public OleDataObject(Win32Com.IDataObject wrapped) { - _wrapped = wrapped; + _wrapped = wrapped.CloneReference(); } public bool Contains(string dataFormat) @@ -34,12 +36,12 @@ namespace Avalonia.Win32 public string GetText() { - return GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT) as string; + return (string)GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT); } public IEnumerable GetFileNames() { - return GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT) as IEnumerable; + return (IEnumerable)GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT); } public object Get(string dataFormat) @@ -47,16 +49,17 @@ namespace Avalonia.Win32 return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT); } - private object GetDataFromOleHGLOBAL(string format, DVASPECT aspect) + private unsafe object GetDataFromOleHGLOBAL(string format, DVASPECT aspect) { - FORMATETC formatEtc = new FORMATETC(); + var formatEtc = new Interop.FORMATETC(); formatEtc.cfFormat = ClipboardFormats.GetFormat(format); formatEtc.dwAspect = aspect; formatEtc.lindex = -1; formatEtc.tymed = TYMED.TYMED_HGLOBAL; - if (_wrapped.QueryGetData(ref formatEtc) == 0) + if (_wrapped.QueryGetData(&formatEtc) == 0) { - _wrapped.GetData(ref formatEtc, out STGMEDIUM medium); + Interop.STGMEDIUM medium = default; + _ = _wrapped.GetData(&formatEtc, &medium); try { if (medium.unionmember != IntPtr.Zero && medium.tymed == TYMED.TYMED_HGLOBAL) @@ -140,38 +143,53 @@ namespace Avalonia.Win32 } } - private IEnumerable GetDataFormatsCore() + private unsafe IEnumerable GetDataFormatsCore() { - var enumFormat = _wrapped.EnumFormatEtc(DATADIR.DATADIR_GET); + var formatsList = new List(); + var enumFormat = _wrapped.EnumFormatEtc((int)DATADIR.DATADIR_GET); if (enumFormat != null) { enumFormat.Reset(); - var formats = ArrayPool.Shared.Rent(1); - var fetched = ArrayPool.Shared.Rent(1); + var formats = ArrayPool.Shared.Rent(1); try { + uint fetched = 0; do { - fetched[0] = 0; - if (enumFormat.Next(1, formats, fetched) == 0 && fetched[0] > 0) + fixed (Interop.FORMATETC* formatsPtr = formats) + { + // Read one item at a time. + // When "celt" parameter is 1, "pceltFetched" is ignored. + var res = enumFormat.Next(1, formatsPtr, &fetched); + if (res != 0) + { + break; + } + } + if (fetched > 0) { if (formats[0].ptd != IntPtr.Zero) Marshal.FreeCoTaskMem(formats[0].ptd); - yield return ClipboardFormats.GetFormat(formats[0].cfFormat); + formatsList.Add(ClipboardFormats.GetFormat(formats[0].cfFormat)); } } - while (fetched[0] > 0); + while (fetched > 0); } finally { - ArrayPool.Shared.Return(formats); - ArrayPool.Shared.Return(fetched); + ArrayPool.Shared.Return(formats); } } + return formatsList; + } + + public void Dispose() + { + _wrapped.Dispose(); } } } diff --git a/src/Windows/Avalonia.Win32/OleDragSource.cs b/src/Windows/Avalonia.Win32/OleDragSource.cs index c903a9ca57..30503339da 100644 --- a/src/Windows/Avalonia.Win32/OleDragSource.cs +++ b/src/Windows/Avalonia.Win32/OleDragSource.cs @@ -1,9 +1,12 @@ using System.Linq; + +using Avalonia.MicroCom; using Avalonia.Win32.Interop; +using Avalonia.Win32.Win32Com; namespace Avalonia.Win32 { - class OleDragSource : IDropSource + internal class OleDragSource : CallbackBase, IDropSource { private const int DRAGDROP_S_USEDEFAULTCURSORS = 0x00040102; private const int DRAGDROP_S_DROP = 0x00040100; @@ -30,7 +33,7 @@ namespace Avalonia.Win32 return unchecked((int)UnmanagedMethods.HRESULT.S_OK); } - public int GiveFeedback(int dwEffect) + public int GiveFeedback(DropEffect dwEffect) { return DRAGDROP_S_USEDEFAULTCURSORS; } diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index d8cb52a914..3d0d35228c 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -1,12 +1,15 @@ -using Avalonia.Input; +using System; + +using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.MicroCom; using Avalonia.Platform; using Avalonia.Win32.Interop; -using IDataObject = Avalonia.Input.IDataObject; +using DropEffect = Avalonia.Win32.Win32Com.DropEffect; namespace Avalonia.Win32 { - internal class OleDropTarget : IDropTarget + internal class OleDropTarget : CallbackBase, Win32Com.IDropTarget { private readonly IInputRoot _target; private readonly ITopLevelImpl _tl; @@ -65,58 +68,51 @@ namespace Avalonia.Win32 return modifiers; } - UnmanagedMethods.HRESULT IDropTarget.DragEnter(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect) + unsafe void Win32Com.IDropTarget.DragEnter(Win32Com.IDataObject pDataObj, int grfKeyState, UnmanagedMethods.POINT pt, DropEffect* pdwEffect) { var dispatch = _tl?.Input; if (dispatch == null) { - pdwEffect = DropEffect.None; - return UnmanagedMethods.HRESULT.S_OK; + *pdwEffect= (int)DropEffect.None; } - _currentDrag = pDataObj as IDataObject; - if (_currentDrag == null) - _currentDrag = new OleDataObject(pDataObj); + + SetDataObject(pDataObj); var args = new RawDragEvent( _dragDevice, RawDragEventType.DragEnter, - _target, - GetDragLocation(pt), + _target, + GetDragLocation(pt), _currentDrag, - ConvertDropEffect(pdwEffect), + ConvertDropEffect(*pdwEffect), ConvertKeyState(grfKeyState) ); dispatch(args); - pdwEffect = ConvertDropEffect(args.Effects); - - return UnmanagedMethods.HRESULT.S_OK; + *pdwEffect = ConvertDropEffect(args.Effects); } - UnmanagedMethods.HRESULT IDropTarget.DragOver(int grfKeyState, long pt, ref DropEffect pdwEffect) + unsafe void Win32Com.IDropTarget.DragOver(int grfKeyState, UnmanagedMethods.POINT pt, DropEffect* pdwEffect) { var dispatch = _tl?.Input; if (dispatch == null) { - pdwEffect = DropEffect.None; - return UnmanagedMethods.HRESULT.S_OK; + *pdwEffect = (int)DropEffect.None; } var args = new RawDragEvent( _dragDevice, RawDragEventType.DragOver, - _target, - GetDragLocation(pt), - _currentDrag, - ConvertDropEffect(pdwEffect), + _target, + GetDragLocation(pt), + _currentDrag, + ConvertDropEffect(*pdwEffect), ConvertKeyState(grfKeyState) ); dispatch(args); - pdwEffect = ConvertDropEffect(args.Effects); - - return UnmanagedMethods.HRESULT.S_OK; + *pdwEffect = ConvertDropEffect(args.Effects); } - UnmanagedMethods.HRESULT IDropTarget.DragLeave() + void Win32Com.IDropTarget.DragLeave() { try { @@ -129,56 +125,91 @@ namespace Avalonia.Win32 DragDropEffects.None, RawInputModifiers.None )); - return UnmanagedMethods.HRESULT.S_OK; } finally { - _currentDrag = null; + ReleaseDataObject(); } } - UnmanagedMethods.HRESULT IDropTarget.Drop(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect) + unsafe void Win32Com.IDropTarget.Drop(Win32Com.IDataObject pDataObj, int grfKeyState, UnmanagedMethods.POINT pt, DropEffect* pdwEffect) { try { var dispatch = _tl?.Input; if (dispatch == null) { - pdwEffect = DropEffect.None; - return UnmanagedMethods.HRESULT.S_OK; + *pdwEffect = (int)DropEffect.None; } - _currentDrag = pDataObj as IDataObject; - if (_currentDrag == null) - _currentDrag= new OleDataObject(pDataObj); - + SetDataObject(pDataObj); + var args = new RawDragEvent( _dragDevice, RawDragEventType.Drop, - _target, - GetDragLocation(pt), - _currentDrag, - ConvertDropEffect(pdwEffect), + _target, + GetDragLocation(pt), + _currentDrag, + ConvertDropEffect(*pdwEffect), ConvertKeyState(grfKeyState) ); dispatch(args); - pdwEffect = ConvertDropEffect(args.Effects); - - return UnmanagedMethods.HRESULT.S_OK; + *pdwEffect = ConvertDropEffect(args.Effects); } finally { - _currentDrag = null; + ReleaseDataObject(); } } - private Point GetDragLocation(long dragPoint) + private void SetDataObject(Win32Com.IDataObject pDataObj) { - int x = (int)dragPoint; - int y = (int)(dragPoint >> 32); + var newDrag = GetAvaloniaObjectFromCOM(pDataObj); + if (_currentDrag != newDrag) + { + ReleaseDataObject(); + _currentDrag = newDrag; + } + } - var screenPt = new PixelPoint(x, y); + private void ReleaseDataObject() + { + // OleDataObject keeps COM reference, so it should be disposed. + if (_currentDrag is OleDataObject oleDragSource) + { + oleDragSource?.Dispose(); + _currentDrag = null; + } + } + + private Point GetDragLocation(UnmanagedMethods.POINT dragPoint) + { + var screenPt = new PixelPoint(dragPoint.X, dragPoint.Y); return _target.PointToClient(screenPt); } + + protected override void Destroyed() + { + ReleaseDataObject(); + } + + public static unsafe IDataObject GetAvaloniaObjectFromCOM(Win32Com.IDataObject pDataObj) + { + if (pDataObj is null) + { + throw new ArgumentNullException(nameof(pDataObj)); + } + if (pDataObj is IDataObject disposableDataObject) + { + return disposableDataObject; + } + + var dataObject = MicroComRuntime.TryUnwrapManagedObject(pDataObj) as DataObject; + if (dataObject is not null) + { + return dataObject; + } + return new OleDataObject(pDataObj); + } } } diff --git a/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs deleted file mode 100644 index 30a3f71cad..0000000000 --- a/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -using Avalonia.Platform; -using Avalonia.Win32; - -[assembly: ExportWindowingSubsystem(OperatingSystemType.WinNT, 1, "Win32", typeof(Win32Platform), nameof(Win32Platform.Initialize))] diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs index 442794f0f0..96e45927da 100644 --- a/src/Windows/Avalonia.Win32/ScreenImpl.cs +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Platform; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -70,5 +71,43 @@ namespace Avalonia.Win32 { _allScreens = null; } + + public Screen ScreenFromWindow(IWindowBaseImpl window) + { + var handle = window.Handle.Handle; + + var monitor = MonitorFromWindow(handle, MONITOR.MONITOR_DEFAULTTONULL); + + return FindScreenByHandle(monitor); + } + + public Screen ScreenFromPoint(PixelPoint point) + { + var monitor = MonitorFromPoint(new POINT + { + X = point.X, + Y = point.Y + }, MONITOR.MONITOR_DEFAULTTONULL); + + return FindScreenByHandle(monitor); + } + + public Screen ScreenFromRect(PixelRect rect) + { + var monitor = MonitorFromRect(new RECT + { + left = rect.TopLeft.X, + top = rect.TopLeft.Y, + right = rect.TopRight.X, + bottom = rect.BottomRight.Y + }, MONITOR.MONITOR_DEFAULTTONULL); + + return FindScreenByHandle(monitor); + } + + private Screen FindScreenByHandle(IntPtr handle) + { + return AllScreens.Cast().FirstOrDefault(m => m.Handle == handle); + } } } diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index 29844368db..410ba55627 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -40,6 +40,16 @@ namespace Avalonia.Win32 { options |= FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT; } + + if (dialog is SaveFileDialog saveFileDialog) + { + var overwritePrompt = saveFileDialog.ShowOverwritePrompt ?? true; + + if (!overwritePrompt) + { + options &= ~FILEOPENDIALOGOPTIONS.FOS_OVERWRITEPROMPT; + } + } frm.SetOptions(options); var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? ""; @@ -79,7 +89,16 @@ namespace Avalonia.Win32 } } - frm.Show(hWnd); + var showResult = frm.Show(hWnd); + + if ((uint)showResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) + { + return result; + } + else if ((uint)showResult != (uint)UnmanagedMethods.HRESULT.S_OK) + { + throw new Win32Exception(showResult); + } if (openDialog?.AllowMultiple == true) { @@ -108,10 +127,6 @@ namespace Avalonia.Win32 } catch (COMException ex) { - if ((uint)ex.HResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) - { - return result; - } throw new Win32Exception(ex.HResult); } })!; @@ -151,7 +166,17 @@ namespace Avalonia.Win32 } } - frm.Show(hWnd); + var showResult = frm.Show(hWnd); + + if ((uint)showResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) + { + return result; + } + else if ((uint)showResult != (uint)UnmanagedMethods.HRESULT.S_OK) + { + throw new Win32Exception(showResult); + } + if (frm.Result is not null) { result = GetAbsoluteFilePath(frm.Result); @@ -161,10 +186,6 @@ namespace Avalonia.Win32 } catch (COMException ex) { - if ((uint)ex.HResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) - { - return result; - } throw new Win32Exception(ex.HResult); } }); diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 23395dd9b5..6484ae6c54 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -217,8 +217,9 @@ namespace Avalonia.Win32 } public IReadOnlyList Screens => - _hiddenWindow.Screens.All.Select(s => new ManagedPopupPositionerScreenInfo( - s.Bounds.ToRect(1), s.Bounds.ToRect(1))).ToList(); + _hiddenWindow.Screens.All + .Select(s => new ManagedPopupPositionerScreenInfo(s.Bounds.ToRect(1), s.Bounds.ToRect(1))) + .ToArray(); public Rect ParentClientAreaScreenGeometry { diff --git a/src/Windows/Avalonia.Win32/Win32Com/win32.idl b/src/Windows/Avalonia.Win32/Win32Com/win32.idl index 904ac41ff2..689f5b7a2e 100644 --- a/src/Windows/Avalonia.Win32/Win32Com/win32.idl +++ b/src/Windows/Avalonia.Win32/Win32Com/win32.idl @@ -27,6 +27,8 @@ @clr-map REFGUID System.Guid* @clr-map REFIID System.Guid* @clr-map WCHAR System.Char +@clr-map STGMEDIUM Avalonia.Win32.Interop.STGMEDIUM +@clr-map FORMATETC Avalonia.Win32.Interop.FORMATETC [flags] enum FILEOPENDIALOGOPTIONS @@ -53,6 +55,16 @@ enum FILEOPENDIALOGOPTIONS FOS_DEFAULTNOMINIMODE = 0x20000000 } +[flags] +enum DropEffect +{ + None = 0, + Copy = 1, + Move = 2, + Link = 4, + Scroll = -2147483648, +} + [ object, uuid(43826d1e-e718-42ee-bc55-a1e261c37bfe), @@ -112,7 +124,7 @@ interface IShellItemArray : IUnknown interface IModalWindow : IUnknown { [local] - HRESULT Show( + int Show( [in, unique] HWND hwndOwner); } @@ -189,3 +201,114 @@ interface IFileOpenDialog : IFileDialog HRESULT GetSelectedItems( [out] IShellItemArray** ppsai); } + +[ + object, + uuid(00000103-0000-0000-C000-000000000046), + pointer_default(unique) +] +interface IEnumFORMATETC : IUnknown +{ + UINT32 Next( + [in] ULONG celt, + [out] FORMATETC* rgelt, + ULONG* pceltFetched); + + UINT32 Skip( + [in] ULONG celt); + + HRESULT Reset(); + + HRESULT Clone( + [out] IEnumFORMATETC** ppenum); +} + +[ + object, + uuid(0000010e-0000-0000-C000-000000000046), + pointer_default(unique) +] +interface IDataObject : IUnknown +{ + UINT32 GetData( + [in, unique] FORMATETC* pformatetcIn, + [out] STGMEDIUM* pmedium); + + UINT32 GetDataHere( + [in, unique] FORMATETC* pformatetc, + [in] STGMEDIUM* pmedium); + + UINT32 QueryGetData( + [in, unique] FORMATETC* pformatetc); + + HRESULT GetCanonicalFormatEtc( + [in, unique] FORMATETC* pformatectIn, + [out] FORMATETC* pformatetcOut); + + UINT32 SetData( + [in, unique] FORMATETC* pformatetc, + [in, unique] STGMEDIUM* pmedium, + [in] BOOL fRelease); + + HRESULT EnumFormatEtc( + [in] DWORD dwDirection, + [out] IEnumFORMATETC** ppenumFormatEtc); + + HRESULT DAdvise( + [in] FORMATETC* pformatetc, + [in] DWORD advf, + [in, unique] void* pAdvSink, + [out] DWORD* pdwConnection); + + HRESULT DUnadvise( + [in] DWORD dwConnection); + + HRESULT EnumDAdvise( + [out] void** ppenumAdvise); +} + +[ + local, + object, + uuid(00000121-0000-0000-C000-000000000046) +] + +interface IDropSource : IUnknown +{ + INT32 QueryContinueDrag([in] BOOL fEscapePressed, [in] DWORD grfKeyState); + + INT32 GiveFeedback([in] DropEffect dwEffect); +} + +[ + object, + uuid(00000122-0000-0000-C000-000000000046), + pointer_default(unique) +] +interface IDropTarget : IUnknown +{ + HRESULT DragEnter + ( + [in, unique] IDataObject* pDataObj, + [in] DWORD grfKeyState, + [in] POINT pt, + [in] DropEffect* pdwEffect + ); + + HRESULT DragOver + ( + [in] DWORD grfKeyState, + [in] POINT pt, + [in] DropEffect* pdwEffect + ); + + HRESULT DragLeave(); + + HRESULT Drop + ( + [in, unique] IDataObject* pDataObj, + [in] DWORD grfKeyState, + [in] POINT pt, + [in] DropEffect* pdwEffect + ); +} diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 0376a41f8c..2a3f4a3384 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -24,9 +24,23 @@ namespace Avalonia.Win32 { var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); - if (egl != null && opts.UseWindowsUIComposition) + if (egl != null) { - WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); + if (opts.EglRendererBlacklist != null) + { + foreach (var item in opts.EglRendererBlacklist) + { + if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) + { + return null; + } + } + } + + if (opts.UseWindowsUIComposition) + { + WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); + } } return egl; diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 5cfbab40e4..dc5e5324c4 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -56,6 +56,11 @@ namespace Avalonia /// GPU rendering will not be enabled if this is set to false. /// public bool? AllowEglInitialization { get; set; } + + public IList EglRendererBlacklist { get; set; } = new List + { + "Microsoft Basic Render" + }; /// /// Enables multitouch support. The default value is true. diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 57b0f71306..76af12e8ca 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -17,11 +17,11 @@ namespace Avalonia.Win32.WinRT.Composition { class WinUICompositorConnection : IRenderTimer { + public static readonly Version MinHostBackdropVersion = new Version(10, 0, 22000); private readonly float? _backdropCornerRadius; private readonly EglContext _syncContext; private readonly ICompositionBrush _micaBrush; private ICompositor _compositor; - private ICompositor2 _compositor2; private ICompositor5 _compositor5; private ICompositorInterop _compositorInterop; private AngleWin32EglDisplay _angle; @@ -39,12 +39,11 @@ namespace Avalonia.Win32.WinRT.Composition _syncContext = _gl.PrimaryEglContext; _angle = (AngleWin32EglDisplay)_gl.Display; _compositor = NativeWinRTMethods.CreateInstance("Windows.UI.Composition.Compositor"); - _compositor2 = _compositor.QueryInterface(); _compositor5 = _compositor.QueryInterface(); _compositorInterop = _compositor.QueryInterface(); _compositorDesktopInterop = _compositor.QueryInterface(); using var device = MicroComRuntime.CreateProxyFor(_angle.GetDirect3DDevice(), true); - + _device = _compositorInterop.CreateGraphicsDevice(device); _blurBrush = CreateAcrylicBlurBackdropBrush(); _micaBrush = CreateMicaBackdropBrush(); @@ -71,7 +70,7 @@ namespace Avalonia.Win32.WinRT.Composition AvaloniaLocator.CurrentMutable.BindToSelf(connect); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); tcs.SetResult(true); - + } catch (Exception e) { @@ -99,7 +98,7 @@ namespace Avalonia.Win32.WinRT.Composition } public void Dispose() { - + } public void Invoke(IAsyncAction asyncInfo, AsyncStatus asyncStatus) @@ -118,12 +117,12 @@ namespace Avalonia.Win32.WinRT.Composition { } } - + private void RunLoop() { { var st = Stopwatch.StartNew(); - using (var act = _compositor5.RequestCommitAsync()) + using (var act = _compositor5.RequestCommitAsync()) act.SetCompleted(new RunLoopHandler(this)); while (true) { @@ -172,12 +171,12 @@ namespace Avalonia.Win32.WinRT.Composition using var sc = _syncContext.EnsureLocked(); using var desktopTarget = _compositorDesktopInterop.CreateDesktopWindowTarget(hWnd, 0); using var target = desktopTarget.QueryInterface(); - + using var drawingSurface = _device.CreateDrawingSurface(new UnmanagedMethods.SIZE(), DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); using var surface = drawingSurface.QueryInterface(); using var surfaceInterop = drawingSurface.QueryInterface(); - + using var surfaceBrush = _compositor.CreateSurfaceBrushWithSurface(surface); using var brush = surfaceBrush.QueryInterface(); @@ -190,7 +189,7 @@ namespace Avalonia.Win32.WinRT.Composition using var containerVisual2 = container.QueryInterface(); containerVisual2.SetRelativeSizeAdjustment(new Vector2(1, 1)); using var containerChildren = container.Children; - + target.SetRoot(containerVisual); using var blur = CreateBlurVisual(_blurBrush); @@ -202,10 +201,10 @@ namespace Avalonia.Win32.WinRT.Composition } var compositionRoundedRectangleGeometry = ClipVisual(blur, mica); - + containerChildren.InsertAtTop(blur); containerChildren.InsertAtTop(visual); - + return new WinUICompositedWindow(_syncContext, _compositor, _pumpLock, target, surfaceInterop, visual, blur, mica, compositionRoundedRectangleGeometry); } @@ -234,10 +233,8 @@ namespace Avalonia.Win32.WinRT.Composition var blurEffect = new WinUIGaussianBlurEffect(backDropParameterAsSource); using var blurEffectFactory = _compositor.CreateEffectFactory(blurEffect); using var compositionEffectBrush = blurEffectFactory.CreateBrush(); - using var backdrop = _compositor2.CreateBackdropBrush(); - using var backdropBrush = backdrop.QueryInterface(); - - + using var backdropBrush = CreateBackdropBrush(); + var saturateEffect = new SaturationEffect(blurEffect); using var satEffectFactory = _compositor.CreateEffectFactory(saturateEffect); using var sat = satEffectFactory.CreateBrush(); @@ -261,8 +258,8 @@ namespace Avalonia.Win32.WinRT.Composition foreach (var visual in containerVisuals) { visual?.SetClip(geometricClipWithGeometry.QueryInterface()); - } - + } + return roundedRectangleGeometry.CloneReference(); } @@ -271,8 +268,8 @@ namespace Avalonia.Win32.WinRT.Composition using var spriteVisual = _compositor.CreateSpriteVisual(); using var visual = spriteVisual.QueryInterface(); using var visual2 = spriteVisual.QueryInterface(); - - + + spriteVisual.SetBrush(compositionBrush); visual.SetIsVisible(0); visual2.SetRelativeSizeAdjustment(new Vector2(1.0f, 1.0f)); @@ -280,6 +277,29 @@ namespace Avalonia.Win32.WinRT.Composition return visual.CloneReference(); } + private ICompositionBrush CreateBackdropBrush() + { + ICompositionBackdropBrush brush = null; + try + { + if (Win32Platform.WindowsVersion >= MinHostBackdropVersion) + { + using var compositor3 = _compositor.QueryInterface(); + brush = compositor3.CreateHostBackdropBrush(); + } + else + { + using var compositor2 = _compositor.QueryInterface(); + brush = compositor2.CreateBackdropBrush(); + } + + return brush.QueryInterface(); + } + finally + { + brush?.Dispose(); + } + } public event Action Tick; } diff --git a/src/Windows/Avalonia.Win32/WinRT/winrt.idl b/src/Windows/Avalonia.Win32/WinRT/winrt.idl index 18a9a26fca..851df9dae6 100644 --- a/src/Windows/Avalonia.Win32/WinRT/winrt.idl +++ b/src/Windows/Avalonia.Win32/WinRT/winrt.idl @@ -358,6 +358,12 @@ interface ICompositor2 : IInspectable [overload("CreateStepEasingFunction")] HRESULT CreateStepEasingFunctionWithStepCount([in] INT32 stepCount, [out] [retval] void** result); } +[uuid(C9DD8EF0-6EB1-4E3C-A658-675D9C64D4AB)] +interface ICompositor3 : IInspectable +{ + HRESULT CreateHostBackdropBrush([out][retval] ICompositionBackdropBrush** result); +} + [uuid(0D8FB190-F122-5B8D-9FDD-543B0D8EB7F3)] interface ICompositorWithBlurredWallpaperBackdropBrush : IInspectable { diff --git a/src/Windows/Avalonia.Win32/WinScreen.cs b/src/Windows/Avalonia.Win32/WinScreen.cs index 0cf9fe31db..f103cc3b66 100644 --- a/src/Windows/Avalonia.Win32/WinScreen.cs +++ b/src/Windows/Avalonia.Win32/WinScreen.cs @@ -9,9 +9,11 @@ namespace Avalonia.Win32 public WinScreen(double pixelDensity, PixelRect bounds, PixelRect workingArea, bool primary, IntPtr hMonitor) : base(pixelDensity, bounds, workingArea, primary) { - this._hMonitor = hMonitor; + _hMonitor = hMonitor; } + public IntPtr Handle => _hMonitor; + public override int GetHashCode() { return (int)_hMonitor; @@ -19,7 +21,7 @@ namespace Avalonia.Win32 public override bool Equals(object obj) { - return (obj is WinScreen screen) ? this._hMonitor == screen._hMonitor : base.Equals(obj); + return (obj is WinScreen screen) ? _hMonitor == screen._hMonitor : base.Equals(obj); } } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 88a0744e3e..64ab15bc30 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -2,12 +2,15 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Text; +using Avalonia.Automation.Peers; using Avalonia.Controls; using Avalonia.Controls.Remote; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; +using Avalonia.Win32.Automation; using Avalonia.Win32.Input; +using Avalonia.Win32.Interop.Automation; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 @@ -19,6 +22,7 @@ namespace Avalonia.Win32 protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { const double wheelDelta = 120.0; + const long UiaRootObjectId = -25; uint timestamp = unchecked((uint)GetMessageTime()); RawInputEventArgs e = null; var shouldTakeFocus = false; @@ -77,6 +81,8 @@ namespace Avalonia.Win32 case WindowsMessage.WM_DESTROY: { + UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null); + //Window doesn't exist anymore _hwnd = IntPtr.Zero; //Remove root reference to this class, so unmanaged delegate can be collected @@ -503,6 +509,15 @@ namespace Avalonia.Win32 case WindowsMessage.WM_IME_ENDCOMPOSITION: Imm32InputMethod.Current.IsComposing = false; break; + + case WindowsMessage.WM_GETOBJECT: + if ((long)lParam == UiaRootObjectId) + { + var peer = ControlAutomationPeer.CreatePeerForElement((Control)_owner); + var node = AutomationNode.GetOrCreate(peer); + return UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, wParam, lParam, node); + } + break; } #if USE_MANAGED_DRAG diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index 8a340aac5e..48f5f8f871 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -23,16 +23,10 @@ namespace Avalonia.Win32 AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0); var borderThickness = new RECT(); - if (GetStyle().HasAllFlags(WindowStyles.WS_THICKFRAME)) - { - AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle()), false, 0); - borderThickness.left *= -1; - borderThickness.top *= -1; - } - else if (GetStyle().HasAllFlags(WindowStyles.WS_BORDER)) - { - borderThickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; - } + + AdjustWindowRectEx(ref borderThickness, (uint)GetStyle(), false, 0); + borderThickness.left *= -1; + borderThickness.top *= -1; if (_extendTitleBarHint >= 0) { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e4f5268285..f0036236ec 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Automation.Peers; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; @@ -13,6 +14,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Win32.Automation; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; using Avalonia.Win32.OpenGl; @@ -84,7 +86,7 @@ namespace Avalonia.Win32 private Size _minSize; private Size _maxSize; private POINT _maxTrackSize; - private WindowImpl _parent; + private WindowImpl _parent; private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; private bool _isCloseRequested; private bool _shown; @@ -172,7 +174,7 @@ namespace Avalonia.Win32 public Action PositionChanged { get; set; } public Action WindowStateChanged { get; set; } - + public Action LostFocus { get; set; } public Action TransparencyLevelChanged { get; set; } @@ -228,7 +230,7 @@ namespace Avalonia.Win32 return new Size(rcWindow.Width, rcWindow.Height) / RenderScaling; } - DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, Marshal.SizeOf(typeof(RECT))); + DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, Marshal.SizeOf()); return new Size(rect.Width, rect.Height) / RenderScaling; } } @@ -245,7 +247,7 @@ namespace Avalonia.Win32 { get { - if(_isFullScreenActive) + if (_isFullScreenActive) { return WindowState.FullScreen; } @@ -265,10 +267,10 @@ namespace Avalonia.Win32 { if (IsWindowVisible(_hwnd)) { - ShowWindow(value, true); + ShowWindow(value, value != WindowState.Minimized); // If the window is minimized, it shouldn't be activated } - _showWindowState = value; + _showWindowState = value; } } @@ -276,7 +278,7 @@ namespace Avalonia.Win32 protected IntPtr Hwnd => _hwnd; - public void SetTransparencyLevelHint (WindowTransparencyLevel transparencyLevel) + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { TransparencyLevel = EnableBlur(transparencyLevel); } @@ -316,12 +318,12 @@ namespace Avalonia.Win32 } var blurInfo = new DWM_BLURBEHIND(false); - + if (transparencyLevel == WindowTransparencyLevel.Blur) { blurInfo = new DWM_BLURBEHIND(true); } - + DwmEnableBlurBehindWindow(_hwnd, ref blurInfo); if (transparencyLevel == WindowTransparencyLevel.Transparent) @@ -337,7 +339,7 @@ namespace Avalonia.Win32 private WindowTransparencyLevel Win8xEnableBlur(WindowTransparencyLevel transparencyLevel) { var accent = new AccentPolicy(); - var accentStructSize = Marshal.SizeOf(accent); + var accentStructSize = Marshal.SizeOf(); if (transparencyLevel == WindowTransparencyLevel.AcrylicBlur) { @@ -377,13 +379,29 @@ namespace Avalonia.Win32 { if (_isUsingComposition) { - _blurHost?.SetBlur(transparencyLevel switch + var effect = transparencyLevel switch { WindowTransparencyLevel.Mica => BlurEffect.Mica, WindowTransparencyLevel.AcrylicBlur => BlurEffect.Acrylic, WindowTransparencyLevel.Blur => BlurEffect.Acrylic, _ => BlurEffect.None - }); + }; + + if (Win32Platform.WindowsVersion >= WinUICompositorConnection.MinHostBackdropVersion) + { + unsafe + { + int pvUseBackdropBrush = effect == BlurEffect.Acrylic ? 1 : 0; + DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_USE_HOSTBACKDROPBRUSH, &pvUseBackdropBrush, sizeof(int)); + } + } + + if (Win32Platform.WindowsVersion < WinUICompositorConnection.MinHostBackdropVersion && effect == BlurEffect.Mica) + { + effect = BlurEffect.Acrylic; + } + + _blurHost?.SetBlur(effect); return transparencyLevel; } @@ -392,7 +410,7 @@ namespace Avalonia.Win32 bool canUseAcrylic = Win32Platform.WindowsVersion.Major > 10 || Win32Platform.WindowsVersion.Build >= 19628; var accent = new AccentPolicy(); - var accentStructSize = Marshal.SizeOf(accent); + var accentStructSize = Marshal.SizeOf(); if (transparencyLevel == WindowTransparencyLevel.AcrylicBlur && !canUseAcrylic) { @@ -415,7 +433,8 @@ namespace Avalonia.Win32 break; case WindowTransparencyLevel.AcrylicBlur: - case (WindowTransparencyLevel.AcrylicBlur + 1): // hack-force acrylic. + case WindowTransparencyLevel.ForceAcrylicBlur: // hack-force acrylic. + case WindowTransparencyLevel.Mica: accent.AccentState = AccentState.ACCENT_ENABLE_ACRYLIC; transparencyLevel = WindowTransparencyLevel.AcrylicBlur; break; @@ -440,7 +459,7 @@ namespace Avalonia.Win32 } } - public IEnumerable Surfaces => new object[] { Handle, _gl, _framebuffer }; + public IEnumerable Surfaces => new object[] { (IPlatformNativeSurfaceHandle)Handle, _gl, _framebuffer }; public PixelPoint Position { @@ -481,12 +500,12 @@ namespace Avalonia.Win32 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); - return Win32Platform.UseDeferredRendering - ? _isUsingComposition + return Win32Platform.UseDeferredRendering + ? _isUsingComposition ? new DeferredRenderer(root, loop) { RenderOnlyOnRenderThread = true - } + } : (IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock) : new ImmediateRenderer(root); } @@ -529,6 +548,7 @@ namespace Avalonia.Win32 if (_dropTarget != null) { OleContext.Current?.UnregisterDragDrop(Handle); + _dropTarget.Dispose(); _dropTarget = null; } @@ -540,7 +560,7 @@ namespace Avalonia.Win32 { BeforeCloseCleanup(true); } - + DestroyWindow(_hwnd); _hwnd = IntPtr.Zero; } @@ -606,7 +626,7 @@ namespace Avalonia.Win32 public void SetParent(IWindowImpl parent) { _parent = (WindowImpl)parent; - + var parentHwnd = _parent?._hwnd ?? IntPtr.Zero; if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar) @@ -630,13 +650,16 @@ namespace Avalonia.Win32 public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { + if (_windowProperties.IsResizable) + { #if USE_MANAGED_DRAG - _managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position.ToPoint(_scaling))); + _managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position.ToPoint(_scaling))); #else - _mouseDevice.Capture(null); - DefWindowProc(_hwnd, (int)WindowsMessage.WM_NCLBUTTONDOWN, - new IntPtr((int)s_edgeLookup[edge]), IntPtr.Zero); + _mouseDevice.Capture(null); + DefWindowProc(_hwnd, (int)WindowsMessage.WM_NCLBUTTONDOWN, + new IntPtr((int)s_edgeLookup[edge]), IntPtr.Zero); #endif + } } public void SetTitle(string title) @@ -717,7 +740,7 @@ namespace Avalonia.Win32 _isUsingComposition ? (int)WindowStyles.WS_EX_NOREDIRECTIONBITMAP : 0, atom, null, - (int)WindowStyles.WS_OVERLAPPEDWINDOW | (int) WindowStyles.WS_CLIPCHILDREN, + (int)WindowStyles.WS_OVERLAPPEDWINDOW | (int)WindowStyles.WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, @@ -763,7 +786,7 @@ namespace Avalonia.Win32 throw new Win32Exception(); } - Handle = new PlatformHandle(_hwnd, PlatformConstants.WindowHandleType); + Handle = new WindowImplPlatformHandle(this); _multitouch = Win32Platform.Options.EnableMultitouch ?? true; @@ -773,7 +796,7 @@ namespace Avalonia.Win32 } if (ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8) - { + { var monitor = MonitorFromWindow( _hwnd, MONITOR.MONITOR_DEFAULTTONEAREST); @@ -856,14 +879,14 @@ namespace Avalonia.Win32 } TaskBarList.MarkFullscreen(_hwnd, fullscreen); - + ExtendClientArea(); } private MARGINS UpdateExtendMargins() { RECT borderThickness = new RECT(); - RECT borderCaptionThickness = new RECT(); + RECT borderCaptionThickness = new RECT(); AdjustWindowRectEx(ref borderCaptionThickness, (uint)(GetStyle()), false, 0); AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle() & ~WindowStyles.WS_CAPTION), false, 0); @@ -886,7 +909,7 @@ namespace Avalonia.Win32 if (_extendTitleBarHint != -1) { - borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling); + borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling); } margins.cyTopHeight = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : 1; @@ -911,7 +934,7 @@ namespace Avalonia.Win32 { return; } - + if (DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled) { _isClientAreaExtended = false; @@ -939,11 +962,11 @@ namespace Avalonia.Win32 _offScreenMargin = new Thickness(); _extendedMargins = new Thickness(); - - Resize(new Size(rcWindow.Width/ RenderScaling, rcWindow.Height / RenderScaling), PlatformResizeReason.Layout); + + Resize(new Size(rcWindow.Width / RenderScaling, rcWindow.Height / RenderScaling), PlatformResizeReason.Layout); } - if(!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && + if (!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome))) { EnableCloseButton(_hwnd); @@ -959,12 +982,12 @@ namespace Avalonia.Win32 private void ShowWindow(WindowState state, bool activate) { _shown = true; - + if (_isClientAreaExtended) { ExtendClientArea(); } - + ShowWindowCommand? command; var newWindowProperties = _windowProperties; @@ -973,7 +996,7 @@ namespace Avalonia.Win32 { case WindowState.Minimized: newWindowProperties.IsFullScreen = false; - command = activate ? ShowWindowCommand.Minimize : ShowWindowCommand.ShowMinNoActive; + command = ShowWindowCommand.Minimize; break; case WindowState.Maximized: newWindowProperties.IsFullScreen = false; @@ -982,7 +1005,7 @@ namespace Avalonia.Win32 case WindowState.Normal: newWindowProperties.IsFullScreen = false; - command = IsWindowVisible(_hwnd) ? ShowWindowCommand.Restore : + command = IsWindowVisible(_hwnd) ? ShowWindowCommand.Restore : activate ? ShowWindowCommand.Normal : ShowWindowCommand.ShowNoActivate; break; @@ -1013,7 +1036,7 @@ namespace Avalonia.Win32 SetForegroundWindow(_hwnd); } } - + private void BeforeCloseCleanup(bool isDisposing) { // Based on https://github.com/dotnet/wpf/blob/master/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Window.cs#L4270-L4337 @@ -1031,7 +1054,7 @@ namespace Avalonia.Win32 // Our window closed callback will set enabled state to a correct value after child window gets destroyed. _parent.SetEnabled(true); } - + // We also need to activate our parent window since again OS might try to activate a window behind if it is not set. if (wasActive) { @@ -1058,7 +1081,7 @@ namespace Avalonia.Win32 SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); } } - } + } private WindowStyles GetWindowStateStyles() { @@ -1235,7 +1258,7 @@ namespace Avalonia.Win32 SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); } - } + } } private const int MF_BYCOMMAND = 0x0; @@ -1285,9 +1308,9 @@ namespace Avalonia.Win32 public void SetExtendClientAreaToDecorationsHint(bool hint) { _isClientAreaExtended = hint; - - ExtendClientArea(); - } + + ExtendClientArea(); + } public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) { @@ -1295,7 +1318,7 @@ namespace Avalonia.Win32 ExtendClientArea(); } - + /// public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) { @@ -1309,7 +1332,7 @@ namespace Avalonia.Win32 /// public Action ExtendClientAreaToDecorationsChanged { get; set; } - + /// public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome); @@ -1348,7 +1371,7 @@ namespace Avalonia.Win32 { private readonly WindowImpl _owner; private readonly PlatformResizeReason _restore; - + public ResizeReasonScope(WindowImpl owner, PlatformResizeReason restore) { _owner = owner; @@ -1359,5 +1382,17 @@ namespace Avalonia.Win32 } public ITextInputMethodImpl TextInputMethod => Imm32InputMethod.Current; + + private class WindowImplPlatformHandle : IPlatformNativeSurfaceHandle + { + private readonly WindowImpl _owner; + public WindowImplPlatformHandle(WindowImpl owner) => _owner = owner; + public IntPtr Handle => _owner.Hwnd; + public string HandleDescriptor => PlatformConstants.WindowHandleType; + + public PixelSize Size => PixelSize.FromSize(_owner.ClientSize, Scaling); + + public double Scaling => _owner.RenderScaling; + } } } diff --git a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj index e9015d857c..ea3a79941d 100644 --- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj +++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj @@ -1,17 +1,13 @@ - + - xamarin.ios10 - true - latest + net6.0-ios + 10.0 + true - - - - - - TargetFramework=netstandard2.0 - + + + diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs index b75aad17cf..f976b2feb4 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs @@ -1,6 +1,7 @@ +using Foundation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -using Foundation; + using UIKit; namespace Avalonia.iOS @@ -8,7 +9,18 @@ namespace Avalonia.iOS public class AvaloniaAppDelegate : UIResponder, IUIApplicationDelegate where TApp : Application, new() { - protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder; + class SingleViewLifetime : ISingleViewApplicationLifetime + { + public AvaloniaView View; + + public Control MainView + { + get => View.Content; + set => View.Content = value; + } + } + + protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseiOS(); [Export("window")] public UIWindow Window { get; set; } @@ -18,7 +30,9 @@ namespace Avalonia.iOS { var builder = AppBuilder.Configure(); CustomizeAppBuilder(builder); - var lifetime = new Lifetime(); + + var lifetime = new SingleViewLifetime(); + builder.AfterSetup(_ => { Window = new UIWindow(); @@ -35,15 +49,5 @@ namespace Avalonia.iOS Window.Hidden = false; return true; } - - class Lifetime : ISingleViewApplicationLifetime - { - public AvaloniaView View; - public Control MainView - { - get => View.Content; - set => View.Content = value; - } - } } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs index dc963726b0..d49ce5310c 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs @@ -1,32 +1,146 @@ -using Avalonia.Input; -using Avalonia.Input.Raw; using Foundation; using ObjCRuntime; +using Avalonia.Input.TextInput; +using Avalonia.Input; +using Avalonia.Input.Raw; using UIKit; -namespace Avalonia.iOS +namespace Avalonia.iOS; + +#nullable enable + +[Adopts("UITextInputTraits")] +[Adopts("UIKeyInput")] +public partial class AvaloniaView : ITextInputMethodImpl { - [Adopts("UIKeyInput")] - public partial class AvaloniaView + private ITextInputMethodClient? _currentClient; + + public override bool CanResignFirstResponder => true; + public override bool CanBecomeFirstResponder => true; + + [Export("hasText")] + public bool HasText { - public override bool CanBecomeFirstResponder => true; + get + { + if (_currentClient is { } && _currentClient.SupportsSurroundingText && + _currentClient.SurroundingText.Text.Length > 0) + { + return true; + } + + return false; + } + } + + [Export("keyboardType")] public UIKeyboardType KeyboardType { get; private set; } = UIKeyboardType.Default; - [Export("hasText")] public bool HasText => false; + [Export("isSecureTextEntry")] public bool IsSecureEntry { get; private set; } - [Export("insertText:")] - public void InsertText(string text) => + [Export("insertText:")] + public void InsertText(string text) + { + if (KeyboardDevice.Instance is { }) + { _topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance, 0, InputRoot, text)); + } + } - [Export("deleteBackward")] - public void DeleteBackward() + [Export("deleteBackward")] + public void DeleteBackward() + { + if (KeyboardDevice.Instance is { }) { // TODO: pass this through IME infrastructure instead of emulating a backspace press _topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance, 0, InputRoot, RawKeyEventType.KeyDown, Key.Back, RawInputModifiers.None)); - + _topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance, 0, InputRoot, RawKeyEventType.KeyUp, Key.Back, RawInputModifiers.None)); } } -} \ No newline at end of file + + void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) + { + _currentClient = client; + + if (client is { }) + { + BecomeFirstResponder(); + } + else + { + ResignFirstResponder(); + } + } + + void ITextInputMethodImpl.SetCursorRect(Rect rect) + { + + } + + void ITextInputMethodImpl.SetOptions(TextInputOptions options) + { + IsSecureEntry = false; + + switch (options.ContentType) + { + case TextInputContentType.Normal: + KeyboardType = UIKeyboardType.Default; + break; + + case TextInputContentType.Alpha: + KeyboardType = UIKeyboardType.AsciiCapable; + break; + + case TextInputContentType.Digits: + KeyboardType = UIKeyboardType.PhonePad; + break; + + case TextInputContentType.Pin: + KeyboardType = UIKeyboardType.NumberPad; + IsSecureEntry = true; + break; + + case TextInputContentType.Number: + KeyboardType = UIKeyboardType.PhonePad; + break; + + case TextInputContentType.Email: + KeyboardType = UIKeyboardType.EmailAddress; + break; + + case TextInputContentType.Url: + KeyboardType = UIKeyboardType.Url; + break; + + case TextInputContentType.Name: + KeyboardType = UIKeyboardType.NamePhonePad; + break; + + case TextInputContentType.Password: + KeyboardType = UIKeyboardType.Default; + IsSecureEntry = true; + break; + + case TextInputContentType.Social: + KeyboardType = UIKeyboardType.Twitter; + break; + + case TextInputContentType.Search: + KeyboardType = UIKeyboardType.WebSearch; + break; + } + + if (options.IsSensitive) + { + IsSecureEntry = true; + } + } + + void ITextInputMethodImpl.Reset() + { + ResignFirstResponder(); + } +} diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 5bb2f64879..e8108dd3de 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -2,12 +2,13 @@ using System; using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Controls.Embedding; +using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.Platform; using Avalonia.Rendering; using CoreAnimation; -using CoreGraphics; using Foundation; using ObjCRuntime; using OpenGLES; @@ -42,7 +43,7 @@ namespace Avalonia.iOS MultipleTouchEnabled = true; } - internal class TopLevelImpl : ITopLevelImpl + internal class TopLevelImpl : ITopLevelImplWithTextInputMethod { private readonly AvaloniaView _view; public AvaloniaView View => _view; @@ -109,6 +110,8 @@ namespace Avalonia.iOS public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(); + + public ITextInputMethodImpl? TextInputMethod => _view; } [Export("layerClass")] diff --git a/src/iOS/Avalonia.iOS/Boilerplate/AppBuilder.cs b/src/iOS/Avalonia.iOS/Boilerplate/AppBuilder.cs deleted file mode 100644 index d5830510f6..0000000000 --- a/src/iOS/Avalonia.iOS/Boilerplate/AppBuilder.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Avalonia.Controls; -using Avalonia.PlatformSupport; - -namespace Avalonia -{ - public class AppBuilder : AppBuilderBase - { - public AppBuilder() : base(new StandardRuntimePlatform(), - b => StandardRuntimePlatformServices.Register(b.ApplicationType.Assembly)) - { - this.UseSkia().UseWindowingSubsystem(iOS.Platform.Register); - } - } -} diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index f9c787b6a8..bd1969081d 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -2,7 +2,6 @@ using System; using System.Reactive.Disposables; using Avalonia.OpenGL; using OpenGLES; -using OpenTK.Graphics.ES30; namespace Avalonia.iOS { @@ -75,7 +74,21 @@ namespace Avalonia.iOS public GlVersion Version { get; } = new GlVersion(GlProfileType.OpenGLES, 3, 0); public GlInterface GlInterface { get; } - public int SampleCount { get; } = 0; - public int StencilSize { get; } = 9; + public int SampleCount + { + get + { + GlInterface.GetIntegerv(GlConsts.GL_SAMPLES, out var samples); + return samples; + } + } + public int StencilSize + { + get + { + GlInterface.GetIntegerv(GlConsts.GL_STENCIL_BITS, out var stencil); + return stencil; + } + } } } diff --git a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs index 5e5e1da949..0e8945d921 100644 --- a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs +++ b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs @@ -4,7 +4,6 @@ using System.Threading; using Avalonia.OpenGL; using Avalonia.OpenGL.Surfaces; using CoreAnimation; -using OpenTK.Graphics.ES30; namespace Avalonia.iOS { @@ -35,7 +34,7 @@ namespace Avalonia.iOS public void Dispose() { - GL.Finish(); + _ctx.GlInterface.Finish(); _fbo.Present(); _restoreContext.Dispose(); } @@ -85,7 +84,7 @@ namespace Avalonia.iOS var ctx = Platform.GlFeature.Context; using (ctx.MakeCurrent()) { - var fbo = new SizeSynchronizedLayerFbo(ctx.Context, _layer); + var fbo = new SizeSynchronizedLayerFbo(ctx.Context, ctx.GlInterface, _layer); if (!fbo.Sync()) throw new InvalidOperationException("Unable to create render target"); return new RenderTarget(ctx, fbo); diff --git a/src/iOS/Avalonia.iOS/Extensions.cs b/src/iOS/Avalonia.iOS/Extensions.cs index bf6262e5c5..80f6b419c9 100644 --- a/src/iOS/Avalonia.iOS/Extensions.cs +++ b/src/iOS/Avalonia.iOS/Extensions.cs @@ -12,7 +12,7 @@ namespace Avalonia.iOS public static Point ToAvalonia(this CGPoint point) => new Point(point.X, point.Y); - static nfloat ColorComponent(byte c) => ((float) c) / 255; + static float ColorComponent(byte c) => (float) c / 255; public static UIColor ToUiColor(this Color color) => new UIColor( ColorComponent(color.R), @@ -20,4 +20,4 @@ namespace Avalonia.iOS ColorComponent(color.B), ColorComponent(color.A)); } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/LayerFbo.cs b/src/iOS/Avalonia.iOS/LayerFbo.cs index 907af58c7e..955aaef59f 100644 --- a/src/iOS/Avalonia.iOS/LayerFbo.cs +++ b/src/iOS/Avalonia.iOS/LayerFbo.cs @@ -1,64 +1,71 @@ using System; +using Avalonia.OpenGL; using CoreAnimation; using OpenGLES; -using OpenTK.Graphics.ES20; namespace Avalonia.iOS { public class LayerFbo { private readonly EAGLContext _context; + private readonly GlInterface _gl; private readonly CAEAGLLayer _layer; - private int _framebuffer; - private int _renderbuffer; - private int _depthBuffer; + private int[] _framebuffer; + private int[] _renderbuffer; + private int[] _depthBuffer; private bool _disposed; - private LayerFbo(EAGLContext context, CAEAGLLayer layer, in int framebuffer, in int renderbuffer, in int depthBuffer) + private LayerFbo(EAGLContext context, GlInterface gl, CAEAGLLayer layer, int[] framebuffer, int[] renderbuffer, int[] depthBuffer) { _context = context; + _gl = gl; _layer = layer; _framebuffer = framebuffer; _renderbuffer = renderbuffer; _depthBuffer = depthBuffer; } - public static LayerFbo TryCreate(EAGLContext context, CAEAGLLayer layer) + public static LayerFbo TryCreate(EAGLContext context, GlInterface gl, CAEAGLLayer layer) { if (context != EAGLContext.CurrentContext) return null; - GL.GenFramebuffers(1, out int fb); - GL.GenRenderbuffers(1, out int rb); - GL.BindFramebuffer(FramebufferTarget.Framebuffer, fb); - GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, rb); - context.RenderBufferStorage((uint) All.Renderbuffer, layer); + + var fb = new int[2]; + var rb = new int[2]; + var db = new int[2]; - GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferSlot.ColorAttachment0, RenderbufferTarget.Renderbuffer, rb); + gl.GenRenderbuffers(1, rb); + gl.BindRenderbuffer(GlConsts.GL_RENDERBUFFER, rb[0]); + context.RenderBufferStorage(GlConsts.GL_RENDERBUFFER, layer); + + gl.GenFramebuffers(1, fb); + gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, fb[0]); + gl.FramebufferRenderbuffer(GlConsts.GL_FRAMEBUFFER, GlConsts.GL_COLOR_ATTACHMENT0, GlConsts.GL_RENDERBUFFER, rb[0]); - int w; - int h; - GL.GetRenderbufferParameter(RenderbufferTarget.Renderbuffer, RenderbufferParameterName.RenderbufferWidth, out w); - GL.GetRenderbufferParameter(RenderbufferTarget.Renderbuffer, RenderbufferParameterName.RenderbufferHeight, out h); + int[] w = new int[1]; + int[] h = new int[1]; + gl.GetRenderbufferParameteriv(GlConsts.GL_RENDERBUFFER, GlConsts.GL_RENDERBUFFER_WIDTH, w); + gl.GetRenderbufferParameteriv(GlConsts.GL_RENDERBUFFER, GlConsts.GL_RENDERBUFFER_HEIGHT, h); - GL.GenRenderbuffers(1, out int depthBuffer); + gl.GenRenderbuffers(1, db); //GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, depthBuffer); //GL.RenderbufferStorage(RenderbufferTarget.Renderbuffer, RenderbufferInternalFormat.DepthComponent16, w, h); - GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferSlot.DepthAttachment, RenderbufferTarget.Renderbuffer, depthBuffer); + gl.FramebufferRenderbuffer(GlConsts.GL_FRAMEBUFFER, GlConsts.GL_DEPTH_ATTACHMENT, GlConsts.GL_RENDERBUFFER, db[0]); - var frameBufferError = GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer); - if(frameBufferError != FramebufferErrorCode.FramebufferComplete) + var frameBufferError = gl.CheckFramebufferStatus(GlConsts.GL_FRAMEBUFFER); + if(frameBufferError != GlConsts.GL_FRAMEBUFFER_COMPLETE) { - GL.DeleteFramebuffers(1, ref fb); - GL.DeleteRenderbuffers(1, ref depthBuffer); - GL.DeleteRenderbuffers(1, ref rb); + gl.DeleteFramebuffers(1, fb); + gl.DeleteRenderbuffers(1, db); + gl.DeleteRenderbuffers(1, rb); return null; } - return new LayerFbo(context, layer, fb, rb, depthBuffer) + return new LayerFbo(context, gl, layer, fb, rb, db) { - Width = w, - Height = h + Width = w[0], + Height = h[0] }; } @@ -67,13 +74,13 @@ namespace Avalonia.iOS public void Bind() { - GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer); + _gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, _framebuffer[0]); } public void Present() { Bind(); - var success = _context.PresentRenderBuffer((uint) All.Renderbuffer); + var success = _context.PresentRenderBuffer(GlConsts.GL_RENDERBUFFER); } public void Dispose() @@ -81,9 +88,9 @@ namespace Avalonia.iOS if(_disposed) return; _disposed = true; - GL.DeleteFramebuffers(1, ref _framebuffer); - GL.DeleteRenderbuffers(1, ref _depthBuffer); - GL.DeleteRenderbuffers(1, ref _renderbuffer); + _gl.DeleteFramebuffers(1, _framebuffer); + _gl.DeleteRenderbuffers(1, _depthBuffer); + _gl.DeleteRenderbuffers(1, _renderbuffer); if (_context != EAGLContext.CurrentContext) throw new InvalidOperationException("Associated EAGLContext is not current"); } @@ -92,15 +99,16 @@ namespace Avalonia.iOS class SizeSynchronizedLayerFbo : IDisposable { private readonly EAGLContext _context; + private readonly GlInterface _gl; private readonly CAEAGLLayer _layer; private LayerFbo _fbo; - private nfloat _oldLayerWidth, _oldLayerHeight, _oldLayerScale; + private double _oldLayerWidth, _oldLayerHeight, _oldLayerScale; - public SizeSynchronizedLayerFbo(EAGLContext context, CAEAGLLayer layer) + public SizeSynchronizedLayerFbo(EAGLContext context, GlInterface gl, CAEAGLLayer layer) { _context = context; + _gl = gl; _layer = layer; - } public bool Sync() @@ -112,7 +120,7 @@ namespace Avalonia.iOS return true; _fbo?.Dispose(); _fbo = null; - _fbo = LayerFbo.TryCreate(_context, _layer); + _fbo = LayerFbo.TryCreate(_context, _gl, _layer); _oldLayerWidth = _layer.Bounds.Width; _oldLayerHeight = _layer.Bounds.Height; _oldLayerScale = _layer.ContentsScale; @@ -140,4 +148,4 @@ namespace Avalonia.iOS public int Height => _fbo?.Height ?? 0; public double Scaling => _oldLayerScale; } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 88f60ace1f..2738e502de 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -1,10 +1,25 @@ using System; + +using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +namespace Avalonia +{ + public static class IOSApplicationExtensions + { + public static T UseiOS(this T builder) where T : AppBuilderBase, new() + { + return builder + .UseWindowingSubsystem(iOS.Platform.Register, "iOS") + .UseSkia(); + } + } +} + namespace Avalonia.iOS { static class Platform @@ -29,7 +44,7 @@ namespace Avalonia.iOS GlFeature ??= new EaglFeature(); Timer ??= new DisplayLinkTimer(); var keyboard = new KeyboardDevice(); - var softKeyboard = new SoftKeyboardHelper(); + AvaloniaLocator.CurrentMutable .Bind().ToConstant(GlFeature) .Bind().ToConstant(new CursorFactoryStub()) @@ -42,11 +57,6 @@ namespace Avalonia.iOS .Bind().ToConstant(Timer) .Bind().ToConstant(new PlatformThreadingInterface()) .Bind().ToConstant(keyboard); - keyboard.PropertyChanged += (_, changed) => - { - if (changed.PropertyName == nameof(KeyboardDevice.FocusedElement)) - softKeyboard.UpdateKeyboard(keyboard.FocusedElement); - }; } diff --git a/src/iOS/Avalonia.iOS/SingleViewLifetime.cs b/src/iOS/Avalonia.iOS/SingleViewLifetime.cs deleted file mode 100644 index 914f0ba548..0000000000 --- a/src/iOS/Avalonia.iOS/SingleViewLifetime.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Avalonia.iOS -{ - public class SingleViewLifetime - { - - } -} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/SoftKeyboardHelper.cs b/src/iOS/Avalonia.iOS/SoftKeyboardHelper.cs deleted file mode 100644 index b05ab280d2..0000000000 --- a/src/iOS/Avalonia.iOS/SoftKeyboardHelper.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Input; - -namespace Avalonia.iOS -{ - public class SoftKeyboardHelper - { - private AvaloniaView _oldView; - - public void UpdateKeyboard(IInputElement focusedElement) - { - if (_oldView?.IsFirstResponder == true) - _oldView?.ResignFirstResponder(); - _oldView = null; - - //TODO: Raise a routed event to determine if any control wants to become the text input handler - if (focusedElement is TextBox) - { - var view = ((focusedElement.VisualRoot as TopLevel)?.PlatformImpl as AvaloniaView.TopLevelImpl)?.View; - view?.BecomeFirstResponder(); - } - } - } -} \ No newline at end of file diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index a2d27fd579..1cf68c1605 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -7,15 +7,9 @@ - - - - - - diff --git a/src/tools/DevAnalyzers/DevAnalyzers.csproj b/src/tools/DevAnalyzers/DevAnalyzers.csproj new file mode 100644 index 0000000000..53e3e74e76 --- /dev/null +++ b/src/tools/DevAnalyzers/DevAnalyzers.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + 10 + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs b/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs new file mode 100644 index 0000000000..8ecd9119f6 --- /dev/null +++ b/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs @@ -0,0 +1,40 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace DevAnalyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class GenericVirtualAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "AVADEV1001"; + + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId, + "Do not use generic virtual methods", + "Method '{0}' is a generic virtual method", + "Performance", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Generic virtual methods affect JIT startup time adversly and should be avoided."); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + } + + private static void AnalyzeMethod(SymbolAnalysisContext context) + { + var symbol = (IMethodSymbol)context.Symbol; + + if (symbol.IsGenericMethod && + (symbol.IsVirtual || symbol.ContainingType.TypeKind == TypeKind.Interface)) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name)); + } + } +} diff --git a/src/tools/DevAnalyzers/GlobalSuppressions.cs b/src/tools/DevAnalyzers/GlobalSuppressions.cs new file mode 100644 index 0000000000..9428b904b8 --- /dev/null +++ b/src/tools/DevAnalyzers/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")] diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs deleted file mode 100644 index 1d5296bebd..0000000000 --- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs +++ /dev/null @@ -1,515 +0,0 @@ -using System; -using Avalonia.Animation.Animators; -using Avalonia.Controls; -using Avalonia.Controls.Shapes; -using Avalonia.Data; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Styling; -using Avalonia.UnitTests; -using Moq; -using Xunit; - -namespace Avalonia.Animation.UnitTests -{ - public class AnimatableTests - { - [Fact] - public void Transition_Is_Not_Applied_When_Not_Attached_To_Visual_Tree() - { - var target = CreateTarget(); - var control = new Control - { - Transitions = new Transitions { target.Object }, - }; - - control.Opacity = 0.5; - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5), - Times.Never); - } - - [Fact] - public void Transition_Is_Not_Applied_To_Initial_Style() - { - using (Start()) - { - var target = CreateTarget(); - var control = new Control - { - Transitions = new Transitions { target.Object }, - }; - - var root = new TestRoot - { - Styles = - { - new Style(x => x.OfType()) - { - Setters = - { - new Setter(Visual.OpacityProperty, 0.8), - } - } - } - }; - - root.Child = control; - - Assert.Equal(0.8, control.Opacity); - - target.Verify(x => x.Apply( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Never); - } - } - - [Fact] - public void Transition_Is_Applied_When_Local_Value_Changes() - { - using var app = Start(); - var target = CreateTarget(); - var control = CreateControl(target.Object); - - control.Opacity = 0.5; - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5)); - } - - [Fact] - public void Transition_Is_Not_Applied_When_Animated_Value_Changes() - { - var target = CreateTarget(); - var control = CreateControl(target.Object); - - control.SetValue(Visual.OpacityProperty, 0.5, BindingPriority.Animation); - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5), - Times.Never); - } - - - [Theory] - [InlineData(null)] //null value - [InlineData("stringValue")] //string value - public void Invalid_Values_In_Animation_Should_Not_Crash_Animations(object invalidValue) - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Layoutable.WidthProperty, 1d), - }, - KeyTime = TimeSpan.FromSeconds(0) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Layoutable.WidthProperty, 2d), - }, - KeyTime = TimeSpan.FromSeconds(2), - }; - - var keyframe3 = new KeyFrame() - { - Setters = - { - new Setter(Layoutable.WidthProperty, invalidValue), - }, - KeyTime = TimeSpan.FromSeconds(3), - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(3), - Children = - { - keyframe1, - keyframe2, - keyframe3 - }, - IterationCount = new IterationCount(5), - PlaybackDirection = PlaybackDirection.Alternate, - }; - - var rect = new Rectangle() - { - Width = 11, - }; - - var originalValue = rect.Width; - - var clock = new TestClock(); - var animationRun = animation.RunAsync(rect, clock); - - clock.Step(TimeSpan.Zero); - Assert.Equal(rect.Width, 1); - clock.Step(TimeSpan.FromSeconds(2)); - Assert.Equal(rect.Width, 2); - clock.Step(TimeSpan.FromSeconds(3)); - //here we have invalid value so value should be expected and set to initial original value - Assert.Equal(rect.Width, originalValue); - } - - [Fact] - public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present() - { - using var app = Start(); - var target = CreateTarget(); - var control = CreateControl(target.Object); - - control.SetValue(Visual.OpacityProperty, 0.5); - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5)); - target.Invocations.Clear(); - - control.SetValue(Visual.OpacityProperty, 0.8, BindingPriority.StyleTrigger); - - target.Verify(x => x.Apply( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Never); - } - - [Fact] - public void Transition_Is_Disposed_When_Local_Value_Changes() - { - using var app = Start(); - var target = CreateTarget(); - var control = CreateControl(target.Object); - var sub = new Mock(); - - target.Setup(x => x.Apply(control, It.IsAny(), 1.0, 0.5)).Returns(sub.Object); - - control.Opacity = 0.5; - sub.Invocations.Clear(); - control.Opacity = 0.4; - - sub.Verify(x => x.Dispose()); - } - - [Fact] - public void New_Transition_Is_Applied_When_Local_Value_Changes() - { - using var app = Start(); - var target = CreateTarget(); - var control = CreateControl(target.Object); - - target.Setup(x => x.Property).Returns(Visual.OpacityProperty); - target.Setup(x => x.Apply(control, It.IsAny(), 1.0, 0.5)) - .Callback(() => - { - control.SetValue(Visual.OpacityProperty, 0.9, BindingPriority.Animation); - }) - .Returns(Mock.Of()); - - control.Opacity = 0.5; - - Assert.Equal(0.9, control.Opacity); - target.Invocations.Clear(); - - control.Opacity = 0.4; - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 0.9, - 0.4)); - } - - [Fact] - public void Transition_Is_Not_Applied_When_Removed_From_Visual_Tree() - { - using var app = Start(); - var target = CreateTarget(); - var control = CreateControl(target.Object); - - control.Opacity = 0.5; - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5)); - target.Invocations.Clear(); - - var root = (TestRoot)control.Parent; - root.Child = null; - control.Opacity = 0.8; - - target.Verify(x => x.Apply( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Never); - } - - [Fact] - public void Animation_Is_Cancelled_When_Transition_Removed() - { - using var app = Start(); - var target = CreateTarget(); - var control = CreateControl(target.Object); - var sub = new Mock(); - - target.Setup(x => x.Apply( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())).Returns(sub.Object); - - control.Opacity = 0.5; - control.Transitions.RemoveAt(0); - - sub.Verify(x => x.Dispose()); - } - - [Fact] - public void Animation_Is_Cancelled_When_New_Style_Activates() - { - using (Start()) - { - var target = CreateTarget(); - var control = CreateStyledControl(target.Object); - var sub = new Mock(); - - target.Setup(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5)).Returns(sub.Object); - - control.Opacity = 0.5; - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5), - Times.Once); - - control.Classes.Add("foo"); - - sub.Verify(x => x.Dispose()); - } - } - - [Fact] - public void Transition_From_Style_Trigger_Is_Applied() - { - using (Start()) - { - var target = CreateTransition(Control.WidthProperty); - var control = CreateStyledControl(transition2: target.Object); - var sub = new Mock(); - - control.Classes.Add("foo"); - control.Width = 100; - - target.Verify(x => x.Apply( - control, - It.IsAny(), - double.NaN, - 100.0), - Times.Once); - } - } - - [Fact] - public void Replacing_Transitions_During_Animation_Does_Not_Throw_KeyNotFound() - { - // Issue #4059 - using (Start()) - { - Border target; - var clock = new TestClock(); - var root = new TestRoot - { - Clock = clock, - Styles = - { - new Style(x => x.OfType()) - { - Setters = - { - new Setter(Border.TransitionsProperty, - new Transitions - { - new DoubleTransition - { - Property = Border.OpacityProperty, - Duration = TimeSpan.FromSeconds(1), - }, - }), - }, - }, - new Style(x => x.OfType().Class("foo")) - { - Setters = - { - new Setter(Border.TransitionsProperty, - new Transitions - { - new DoubleTransition - { - Property = Border.OpacityProperty, - Duration = TimeSpan.FromSeconds(1), - }, - }), - new Setter(Border.OpacityProperty, 0.0), - }, - }, - }, - Child = target = new Border - { - Background = Brushes.Red, - } - }; - - root.Measure(Size.Infinity); - root.Arrange(new Rect(root.DesiredSize)); - - target.Classes.Add("foo"); - clock.Step(TimeSpan.FromSeconds(0)); - clock.Step(TimeSpan.FromSeconds(0.5)); - - Assert.Equal(0.5, target.Opacity); - - target.Classes.Remove("foo"); - } - } - - [Fact] - public void Transitions_Can_Be_Changed_To_Collection_That_Contains_The_Same_Transitions() - { - var target = CreateTarget(); - var control = CreateControl(target.Object); - - control.Transitions = new Transitions { target.Object }; - } - - [Fact] - public void Transitions_Can_Re_Set_During_Batch_Update() - { - var target = CreateTarget(); - var control = CreateControl(target.Object); - - // Assigning and then clearing Transitions ensures we have a transition state - // collection created. - control.Transitions = null; - - control.BeginBatchUpdate(); - - // Setting opacity then Transitions means that we receive the Transitions change - // after the Opacity change when EndBatchUpdate is called. - control.Opacity = 0.5; - control.Transitions = new Transitions { target.Object }; - - // Which means that the transition state hasn't been initialized with the new - // Transitions when the Opacity change notification gets raised here. - control.EndBatchUpdate(); - } - - private static IDisposable Start() - { - var clock = new MockGlobalClock(); - var services = TestServices.RealStyler.With(globalClock: clock); - return UnitTestApplication.Start(services); - } - - private static Mock CreateTarget() - { - return CreateTransition(Visual.OpacityProperty); - } - - private static Control CreateControl(ITransition transition) - { - var control = new Control - { - Transitions = new Transitions { transition }, - }; - - var root = new TestRoot(control); - return control; - } - - private static Control CreateStyledControl( - ITransition transition1 = null, - ITransition transition2 = null) - { - transition1 = transition1 ?? CreateTarget().Object; - transition2 = transition2 ?? CreateTransition(Control.WidthProperty).Object; - - var control = new Control - { - Styles = - { - new Style(x => x.OfType()) - { - Setters = - { - new Setter - { - Property = Control.TransitionsProperty, - Value = new Transitions { transition1 }, - } - } - }, - new Style(x => x.OfType().Class("foo")) - { - Setters = - { - new Setter - { - Property = Control.TransitionsProperty, - Value = new Transitions { transition2 }, - } - } - } - } - }; - - var root = new TestRoot(control); - return control; - } - - private static Mock CreateTransition(AvaloniaProperty property) - { - var target = new Mock(); - var sub = new Mock(); - - target.Setup(x => x.Property).Returns(property); - target.Setup(x => x.Apply( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())).Returns(sub.Object); - - return target; - } - } -} diff --git a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs deleted file mode 100644 index 58bd7a42c3..0000000000 --- a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs +++ /dev/null @@ -1,448 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Animation; -using Avalonia.Controls; -using Avalonia.Styling; -using Avalonia.UnitTests; -using Avalonia.Data; -using Xunit; -using Avalonia.Animation.Easings; -using System.Threading; -using System.Reactive.Linq; - -namespace Avalonia.Animation.UnitTests -{ - public class AnimationIterationTests - { - [Fact] - public void Check_KeyTime_Correctly_Converted_To_Cue() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 100d), - }, - KeyTime = TimeSpan.FromSeconds(0.5) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 0d), - }, - KeyTime = TimeSpan.FromSeconds(0) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(1), - Children = - { - keyframe2, - keyframe1 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 100d - }; - - var clock = new TestClock(); - var animationRun = animation.RunAsync(border, clock); - - clock.Step(TimeSpan.Zero); - Assert.Equal(border.Width, 0d); - - clock.Step(TimeSpan.FromSeconds(1)); - Assert.Equal(border.Width, 100d); - - } - - - [Fact] - public void Check_Initial_Inter_and_Trailing_Delay_Values() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 200d), - }, - Cue = new Cue(1d) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 100d), - }, - Cue = new Cue(0d) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(3), - Delay = TimeSpan.FromSeconds(3), - DelayBetweenIterations = TimeSpan.FromSeconds(3), - IterationCount = new IterationCount(2), - Children = - { - keyframe2, - keyframe1 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 100d - }; - - var clock = new TestClock(); - var animationRun = animation.RunAsync(border, clock); - - clock.Step(TimeSpan.Zero); - - // Initial Delay. - clock.Step(TimeSpan.FromSeconds(1)); - Assert.Equal(border.Width, 0d); - - clock.Step(TimeSpan.FromSeconds(6)); - - // First Inter-Iteration delay. - clock.Step(TimeSpan.FromSeconds(8)); - Assert.Equal(border.Width, 200d); - - // Trailing Delay should be non-existent. - clock.Step(TimeSpan.FromSeconds(14)); - Assert.True(animationRun.Status == TaskStatus.RanToCompletion); - Assert.Equal(border.Width, 100d); - } - - [Fact] - public void Check_FillModes_Start_and_End_Values_if_Retained() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 0d), - }, - Cue = new Cue(0.0d) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 300d), - }, - Cue = new Cue(1.0d) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(0.05d), - Delay = TimeSpan.FromSeconds(0.05d), - Easing = new SineEaseInOut(), - FillMode = FillMode.Both, - Children = - { - keyframe1, - keyframe2 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 100d, - }; - - var clock = new TestClock(); - var animationRun = animation.RunAsync(border, clock); - - clock.Step(TimeSpan.FromSeconds(0d)); - Assert.Equal(border.Width, 0d); - - clock.Step(TimeSpan.FromSeconds(0.050d)); - Assert.Equal(border.Width, 0d); - - clock.Step(TimeSpan.FromSeconds(0.100d)); - Assert.Equal(border.Width, 300d); - } - - [Fact(Skip = "See #6111")] - public void Dispose_Subscription_Should_Stop_Animation() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 200d), - }, - Cue = new Cue(1d) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 100d), - }, - Cue = new Cue(0d) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(10), - Delay = TimeSpan.FromSeconds(0), - DelayBetweenIterations = TimeSpan.FromSeconds(0), - IterationCount = new IterationCount(1), - Children = - { - keyframe2, - keyframe1 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 50d - }; - var propertyChangedCount = 0; - var animationCompletedCount = 0; - border.PropertyChanged += (sender, e) => - { - if (e.Property == Control.WidthProperty) - { - propertyChangedCount++; - } - }; - - var clock = new TestClock(); - var disposable = animation.Apply(border, clock, Observable.Return(true), () => animationCompletedCount++); - - Assert.Equal(0, propertyChangedCount); - - clock.Step(TimeSpan.FromSeconds(0)); - Assert.Equal(0, animationCompletedCount); - Assert.Equal(1, propertyChangedCount); - - disposable.Dispose(); - - // Clock ticks should be ignored after Dispose - clock.Step(TimeSpan.FromSeconds(5)); - clock.Step(TimeSpan.FromSeconds(6)); - clock.Step(TimeSpan.FromSeconds(7)); - - // On animation disposing (cancellation) on completed is not invoked (is it expected?) - Assert.Equal(0, animationCompletedCount); - // Initial property changed before cancellation + animation value removal. - Assert.Equal(2, propertyChangedCount); - } - - [Fact] - public void Do_Not_Run_Cancelled_Animation() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 200d), - }, - Cue = new Cue(1d) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 100d), - }, - Cue = new Cue(0d) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(10), - Delay = TimeSpan.FromSeconds(0), - DelayBetweenIterations = TimeSpan.FromSeconds(0), - IterationCount = new IterationCount(1), - Children = - { - keyframe2, - keyframe1 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 100d - }; - var propertyChangedCount = 0; - border.PropertyChanged += (sender, e) => - { - if (e.Property == Control.WidthProperty) - { - propertyChangedCount++; - } - }; - - var clock = new TestClock(); - var cancellationTokenSource = new CancellationTokenSource(); - cancellationTokenSource.Cancel(); - var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token); - - clock.Step(TimeSpan.FromSeconds(10)); - Assert.Equal(0, propertyChangedCount); - Assert.True(animationRun.IsCompleted); - } - - [Fact(Skip = "See #6111")] - public void Cancellation_Should_Stop_Animation() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 200d), - }, - Cue = new Cue(1d) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 100d), - }, - Cue = new Cue(0d) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(10), - Delay = TimeSpan.FromSeconds(0), - DelayBetweenIterations = TimeSpan.FromSeconds(0), - IterationCount = new IterationCount(1), - Children = - { - keyframe2, - keyframe1 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 50d - }; - var propertyChangedCount = 0; - border.PropertyChanged += (sender, e) => - { - if (e.Property == Control.WidthProperty) - { - propertyChangedCount++; - } - }; - - var clock = new TestClock(); - var cancellationTokenSource = new CancellationTokenSource(); - var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token); - - Assert.Equal(0, propertyChangedCount); - - clock.Step(TimeSpan.FromSeconds(0)); - Assert.False(animationRun.IsCompleted); - Assert.Equal(1, propertyChangedCount); - - cancellationTokenSource.Cancel(); - clock.Step(TimeSpan.FromSeconds(1)); - clock.Step(TimeSpan.FromSeconds(2)); - clock.Step(TimeSpan.FromSeconds(3)); - //Assert.Equal(2, propertyChangedCount); - - animationRun.Wait(); - - clock.Step(TimeSpan.FromSeconds(6)); - Assert.True(animationRun.IsCompleted); - Assert.Equal(2, propertyChangedCount); - } - - [Fact] - public void Cancellation_Of_Completed_Animation_Does_Not_Fail() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 200d), - }, - Cue = new Cue(1d) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 100d), - }, - Cue = new Cue(0d) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(10), - Delay = TimeSpan.FromSeconds(0), - DelayBetweenIterations = TimeSpan.FromSeconds(0), - IterationCount = new IterationCount(1), - Children = - { - keyframe2, - keyframe1 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 50d - }; - var propertyChangedCount = 0; - border.PropertyChanged += (sender, e) => - { - if (e.Property == Control.WidthProperty) - { - propertyChangedCount++; - } - }; - - var clock = new TestClock(); - var cancellationTokenSource = new CancellationTokenSource(); - var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token); - - Assert.Equal(0, propertyChangedCount); - - clock.Step(TimeSpan.FromSeconds(0)); - Assert.False(animationRun.IsCompleted); - Assert.Equal(1, propertyChangedCount); - - clock.Step(TimeSpan.FromSeconds(10)); - Assert.True(animationRun.IsCompleted); - Assert.Equal(2, propertyChangedCount); - - cancellationTokenSource.Cancel(); - animationRun.Wait(); - } - } -} diff --git a/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj b/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj deleted file mode 100644 index e07ece5460..0000000000 --- a/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - net6.0 - Library - true - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs deleted file mode 100644 index fa2ed61e65..0000000000 --- a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System; -using Avalonia.Animation.Easings; -using Avalonia.Controls.Shapes; -using Avalonia.Media; -using Avalonia.Styling; -using Xunit; - -namespace Avalonia.Animation.UnitTests -{ - public class KeySplineTests - { - [Theory] - [InlineData("1,2 3,4")] - [InlineData("1 2 3 4")] - [InlineData("1 2,3 4")] - [InlineData("1,2,3,4")] - public void Can_Parse_KeySpline_Via_TypeConverter(string input) - { - var conv = new KeySplineTypeConverter(); - - var keySpline = (KeySpline)conv.ConvertFrom(input); - - Assert.Equal(1, keySpline.ControlPointX1); - Assert.Equal(2, keySpline.ControlPointY1); - Assert.Equal(3, keySpline.ControlPointX2); - Assert.Equal(4, keySpline.ControlPointY2); - } - - [Theory] - [InlineData("1,2F,3,4")] - [InlineData("Foo,Bar,Fee,Buzz")] - public void Can_Handle_Invalid_String_KeySpline_Via_TypeConverter(string input) - { - var conv = new KeySplineTypeConverter(); - - Assert.ThrowsAny(() => (KeySpline)conv.ConvertFrom(input)); - } - - [Theory] - [InlineData(0.00)] - [InlineData(0.50)] - [InlineData(1.00)] - public void KeySpline_X_Values_In_Range_Do_Not_Throw(double input) - { - var keySpline = new KeySpline(); - keySpline.ControlPointX1 = input; // no exception will be thrown -- test will fail if exception thrown - keySpline.ControlPointX2 = input; // no exception will be thrown -- test will fail if exception thrown - } - - [Theory] - [InlineData(-0.01)] - [InlineData(1.01)] - public void KeySpline_X_Values_Cannot_Be_Out_Of_Range(double input) - { - var keySpline = new KeySpline(); - Assert.Throws(() => keySpline.ControlPointX1 = input); - Assert.Throws(() => keySpline.ControlPointX2 = input); - } - - [Fact] - public void SplineEasing_Can_Be_Mutated() - { - var easing = new SplineEasing(); - - Assert.Equal(0, easing.Ease(0)); - Assert.Equal(1, easing.Ease(1)); - - easing.X1 = 0.25; - easing.Y1 = 0.5; - easing.X2 = 0.75; - easing.Y2 = 1.0; - - Assert.NotEqual(0.5, easing.Ease(0.5)); - } - - /* - To get the test values for the KeySpline test, you can: - 1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations - 2) Add the following xaml somewhere: -