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..a792774d94 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -77,7 +77,7 @@ 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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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 @@ -95,8 +95,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 +105,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 +113,12 @@ 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\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 @@ -178,8 +174,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}" @@ -222,6 +216,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 +232,10 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -1096,26 +1098,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 +1542,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 @@ -2064,6 +2022,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 +2190,54 @@ 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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2213,7 +2267,6 @@ Global {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 +2275,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} @@ -2241,10 +2295,14 @@ 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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/Directory.Build.props b/Directory.Build.props index c6610695c4..835decc672 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,5 +4,6 @@ $(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netcoreapp2.0\Avalonia.Designer.HostApp.dll false + false diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 40669f4f53..79456b117b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,6 +1,3 @@ -variables: - MSBuildEnableWorkloadResolver: 'false' - jobs: - job: GetPRNumber @@ -41,7 +38,7 @@ jobs: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 6.0.100' inputs: - version: 6.0.100 + version: 6.0.200 - task: CmdLine@2 displayName: 'Run Build' @@ -72,7 +69,7 @@ jobs: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 6.0.100' inputs: - version: 6.0.100 + version: 6.0.200 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -144,7 +141,13 @@ jobs: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 6.0.100' inputs: - version: 6.0.100 + version: 6.0.200 + + - task: CmdLine@2 + displayName: 'Install Workloads' + inputs: + script: | + dotnet workload install --no-cache --disable-parallel android ios --skip-manifest-update --source "https://api.nuget.org/v3/index.json" - 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..6bf69603c0 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -16,7 +16,6 @@ - diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 1d84d5289a..6dd6cccb53 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..60bebaad40 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + 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..30265268dc 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,11 @@ { "sdk": { - "version": "6.0.100" + "version": "6.0.200", + "rollForward": "latestFeature" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", + "Xamarin.Legacy.Sdk": "0.1.2-alpha6", "MSBuild.Sdk.Extras": "3.0.22", "AggregatePackage.NuGet.Sdk" : "0.1.12" } 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/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..620b750a40 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; @@ -2465,6 +2515,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..72d90abbf3 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) 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 96c7937559..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, Android 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) 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..9777bb46c3 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 - + + - - - - 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..520bbdf32b 100644 --- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj +++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj @@ -1,6 +1,7 @@  net6.0 + false enable True diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index db1e16166a..12d1d5645e 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 - - + + - - - - 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 @@ - + {d0a739b9-3c68-4ba6-a328-41606954b6bd} diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index 7e49cb5dfa..880b210a6c 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -13,7 +13,6 @@ namespace Avalonia.Android { private readonly TView _host; private readonly InputMethodManager _imm; - private IInputElement _inputElement; public AndroidInputMethod(TView host) { @@ -25,7 +24,7 @@ namespace Avalonia.Android _host.Focusable = true; _host.FocusableInTouchMode = true; - _host.ViewTreeObserver.AddOnGlobalLayoutListener(new SoftKeyboardListner(_host)); + _host.ViewTreeObserver.AddOnGlobalLayoutListener(new SoftKeyboardListener(_host)); } public void Reset() @@ -33,8 +32,10 @@ namespace Avalonia.Android _imm.RestartInput(_host); } - public void SetActive(bool active) + public void SetClient(ITextInputMethodClient client) { + var active = client is { }; + if (active) { _host.RequestFocus(); @@ -49,20 +50,8 @@ namespace Avalonia.Android { } - public void SetOptions(TextInputOptionsQueryEventArgs options) + public void SetOptions(TextInputOptions options) { - if (_inputElement != null) - { - _inputElement.PointerReleased -= RestoreSoftKeyboard; - } - - _inputElement = options.Source as InputElement; - - if (_inputElement == null) - { - _imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None); - } - _host.InitEditorInfo((outAttrs) => { outAttrs.InputType = options.ContentType switch @@ -70,7 +59,7 @@ namespace Avalonia.Android TextInputContentType.Email => global::Android.Text.InputTypes.TextVariationEmailAddress, TextInputContentType.Number => global::Android.Text.InputTypes.ClassNumber, TextInputContentType.Password => global::Android.Text.InputTypes.TextVariationPassword, - TextInputContentType.Phone => global::Android.Text.InputTypes.ClassPhone, + TextInputContentType.Digits => global::Android.Text.InputTypes.ClassPhone, TextInputContentType.Url => global::Android.Text.InputTypes.TextVariationUri, _ => global::Android.Text.InputTypes.ClassText }; @@ -83,9 +72,9 @@ namespace Avalonia.Android if (options.Multiline) outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagMultiLine; - }); - //_inputElement.PointerReleased += RestoreSoftKeyboard; + outAttrs.ImeOptions |= ImeFlags.NoFullscreen | ImeFlags.NoExtractUi; + }); } private void RestoreSoftKeyboard(object sender, PointerReleasedEventArgs e) diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 2d4f6a305f..61aa6ce946 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -1,16 +1,15 @@ using System; - +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Android; using Avalonia.Android.Platform; using Avalonia.Android.Platform.Input; -using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; -using Avalonia.PlatformSupport; using Avalonia.Skia; namespace Avalonia @@ -20,9 +19,10 @@ namespace Avalonia public static T UseAndroid(this T builder) where T : AppBuilderBase, new() { var options = AvaloniaLocator.Current.GetService() ?? new AndroidPlatformOptions(); - builder.UseWindowingSubsystem(() => AndroidPlatform.Initialize(builder.ApplicationType, options), "Android"); - builder.UseSkia(); - return builder; + + return builder + .UseWindowingSubsystem(() => AndroidPlatform.Initialize(options), "Android") + .UseSkia(); } } } @@ -44,7 +44,7 @@ namespace Avalonia.Android public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); - public static void Initialize(Type appType, AndroidPlatformOptions options) + public static void Initialize(AndroidPlatformOptions options) { Options = options; diff --git a/src/Android/Avalonia.Android/AppBuilder.cs b/src/Android/Avalonia.Android/AppBuilder.cs deleted file mode 100644 index 04f1ff00d0..0000000000 --- a/src/Android/Avalonia.Android/AppBuilder.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Avalonia.Controls; -using Avalonia.PlatformSupport; - -namespace Avalonia -{ - public sealed class AppBuilder : AppBuilderBase - { - public AppBuilder() : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType()?.Assembly)) - { - - } - } -} diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 5c33dbcea6..203c3accd6 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,15 +1,19 @@ - + - monoandroid11.0 + net6.0-android + $(TargetFrameworks);monoandroid11.0 + 21 true + true + portable - - TargetFramework=netstandard2.0 - + + + + + - - diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs index 3c9f373a66..f5d620a97a 100644 --- a/src/Android/Avalonia.Android/AvaloniaActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs @@ -1,35 +1,82 @@ -using Android.App; using Android.OS; -using Android.Views; +using AndroidX.AppCompat.App; +using Android.Content.Res; +using AndroidX.Lifecycle; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls; namespace Avalonia.Android { - public abstract class AvaloniaActivity : Activity + public abstract class AvaloniaActivity : AppCompatActivity where TApp : Application, new() { + internal class SingleViewLifetime : ISingleViewApplicationLifetime + { + public AvaloniaView View { get; internal set; } + + public Control MainView + { + get => (Control)View.Content; + set => View.Content = value; + } + } + internal AvaloniaView View; - object _content; + internal AvaloniaViewModel _viewModel; + + protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid(); protected override void OnCreate(Bundle savedInstanceState) { + var builder = AppBuilder.Configure(); + + CustomizeAppBuilder(builder); + View = new AvaloniaView(this); - if (_content != null) - View.Content = _content; SetContentView(View); + + var lifetime = new SingleViewLifetime(); + lifetime.View = View; + + builder.AfterSetup(x => + { + _viewModel = new ViewModelProvider(this).Get(Java.Lang.Class.FromType(typeof(AvaloniaViewModel))) as AvaloniaViewModel; + + if (_viewModel.Content != null) + { + View.Content = _viewModel.Content; + } + + View.Prepare(); + }); + + builder.SetupWithLifetime(lifetime); + base.OnCreate(savedInstanceState); } - public object Content { get { - return _content; + return _viewModel.Content; } set { - _content = value; + _viewModel.Content = value; if (View != null) View.Content = value; } } + + public override void OnConfigurationChanged(Configuration newConfig) + { + base.OnConfigurationChanged(newConfig); + } + + protected override void OnDestroy() + { + View.Content = null; + + base.OnDestroy(); + } } } diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 8de3657283..8177cf1f69 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -12,7 +12,7 @@ namespace Avalonia.Android { public class AvaloniaView : FrameLayout { - private readonly EmbeddableControlRoot _root; + private EmbeddableControlRoot _root; private readonly ViewImpl _view; private IDisposable? _timerSubscription; @@ -21,6 +21,11 @@ namespace Avalonia.Android { _view = new ViewImpl(context); AddView(_view.View); + + } + + internal void Prepare () + { _root = new EmbeddableControlRoot(_view); _root.Prepare(); } diff --git a/src/Android/Avalonia.Android/AvaloniaViewModel.cs b/src/Android/Avalonia.Android/AvaloniaViewModel.cs new file mode 100644 index 0000000000..1b2c00987a --- /dev/null +++ b/src/Android/Avalonia.Android/AvaloniaViewModel.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Android +{ + internal class AvaloniaViewModel : AndroidX.Lifecycle.ViewModel + { + public object Content { get; set; } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 34784612f1..5343b57251 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -2,18 +2,22 @@ using System; using Android.Content; using Android.Graphics; using Android.OS; +using Android.Runtime; using Android.Util; using Android.Views; +using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Platform; namespace Avalonia.Android { - public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformHandle + public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformNativeSurfaceHandle { bool _invalidateQueued; readonly object _lock = new object(); private readonly Handler _handler; - + + IntPtr IPlatformHandle.Handle => + AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, Holder.Surface.Handle); public InvalidationAwareSurfaceView(Context context) : base(context) { @@ -25,7 +29,7 @@ namespace Avalonia.Android { lock (_lock) { - if(_invalidateQueued) + if (_invalidateQueued) return; _handler.Post(() => { @@ -70,7 +74,7 @@ namespace Avalonia.Android public void SurfaceDestroyed(ISurfaceHolder holder) { Log.Info("AVALONIA", "Surface Destroyed"); - + } protected void DoDraw() @@ -83,5 +87,9 @@ namespace Avalonia.Android } protected abstract void Draw(); public string HandleDescriptor => "SurfaceView"; + + public PixelSize Size => new PixelSize(Holder.SurfaceFrame.Width(), Holder.SurfaceFrame.Height()); + + public double Scaling => Resources.DisplayMetrics.Density; } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 0afb1db141..8a475676a5 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using Android.Content; using Android.Graphics; -using Android.Runtime; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.OpenGL; @@ -38,11 +37,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform _keyboardHelper = new AndroidKeyboardEventsHelper(this); _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, GetAvaloniaPointFromEvent); - _gl = GlPlatformSurface.TryCreate(this); _framebuffer = new FramebufferManager(this); - RenderScaling = (int)_view.Resources.DisplayMetrics.Density; + RenderScaling = (int)_view.Scaling; MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); @@ -77,7 +75,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IPlatformHandle Handle => _view; - public IEnumerable Surfaces => new object[] { _gl, _framebuffer }; + public IEnumerable Surfaces => new object[] { _gl, _framebuffer, Handle }; public IRenderer CreateRenderer(IRenderRoot root) => AndroidPlatform.Options.UseDeferredRendering @@ -216,10 +214,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); - IntPtr EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo.Handle => - AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, _view.Holder.Surface.Handle); + IntPtr EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle; - public PixelSize Size => new PixelSize(_view.Holder.SurfaceFrame.Width(), _view.Holder.SurfaceFrame.Height()); + public PixelSize Size => _view.Size; public double Scaling => RenderScaling; diff --git a/src/Android/Avalonia.Android/Resources/AboutResources.txt b/src/Android/Avalonia.Android/Resources/AboutResources.txt deleted file mode 100644 index 194ae28a59..0000000000 --- a/src/Android/Avalonia.Android/Resources/AboutResources.txt +++ /dev/null @@ -1,50 +0,0 @@ -Images, layout descriptions, binary blobs and string dictionaries can be included -in your application as resource files. Various Android APIs are designed to -operate on the resource IDs instead of dealing with images, strings or binary blobs -directly. - -For example, a sample Android app that contains a user interface layout (main.xml), -an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) -would keep its resources in the "Resources" directory of the application: - -Resources/ - drawable-hdpi/ - icon.png - - drawable-ldpi/ - icon.png - - drawable-mdpi/ - icon.png - - layout/ - main.xml - - values/ - strings.xml - -In order to get the build system to recognize Android resources, set the build action to -"AndroidResource". The native Android APIs do not operate directly with filenames, but -instead operate on resource IDs. When you compile an Android application that uses resources, -the build system will package the resources for distribution and generate a class called -"Resource" that contains the tokens for each one of the resources included. For example, -for the above Resources layout, this is what the Resource class would expose: - -public class Resource { - public class drawable { - public const int icon = 0x123; - } - - public class layout { - public const int main = 0x456; - } - - public class strings { - public const int first_string = 0xabc; - public const int second_string = 0xbcd; - } -} - -You would then use R.drawable.icon to reference the drawable/icon.png file, or Resource.layout.main -to reference the layout/main.xml file, or Resource.strings.first_string to reference the first -string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/src/Android/Avalonia.Android/Resources/Values/Strings.xml b/src/Android/Avalonia.Android/Resources/Values/Strings.xml deleted file mode 100644 index 3823c6f4c6..0000000000 --- a/src/Android/Avalonia.Android/Resources/Values/Strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - Hello World, Click Me! - $projectname$ - \ No newline at end of file diff --git a/src/Android/Avalonia.Android/SoftKeyboardListner.cs b/src/Android/Avalonia.Android/SoftKeyboardListener.cs similarity index 89% rename from src/Android/Avalonia.Android/SoftKeyboardListner.cs rename to src/Android/Avalonia.Android/SoftKeyboardListener.cs index df658f6314..5e996639ed 100644 --- a/src/Android/Avalonia.Android/SoftKeyboardListner.cs +++ b/src/Android/Avalonia.Android/SoftKeyboardListener.cs @@ -9,7 +9,7 @@ using Avalonia.Input; namespace Avalonia.Android { - class SoftKeyboardListner : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener + class SoftKeyboardListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener { private const int DefaultKeyboardHeightDP = 100; private static readonly int EstimatedKeyboardDP = DefaultKeyboardHeightDP + (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop ? 48 : 0); @@ -17,7 +17,7 @@ namespace Avalonia.Android private readonly View _host; private bool _wasKeyboard; - public SoftKeyboardListner(View view) + public SoftKeyboardListener(View view) { _host = view; } diff --git a/src/Android/Avalonia.AndroidTestApplication/Assets/AboutAssets.txt b/src/Android/Avalonia.AndroidTestApplication/Assets/AboutAssets.txt deleted file mode 100644 index ee39886295..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/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 you 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"); \ No newline at end of file diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index 9104f1618c..db0bb01410 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -1,153 +1,43 @@ - - + - Debug - AnyCPU - 8.0.30703 - 2.0 - {FF69B927-C545-49AE-8E16-3D14D621AA12} - {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - Properties - Avalonia.AndroidTestApplication - Avalonia.AndroidTestApplication - 512 - true - Resources\Resource.Designer.cs - Off - False - v11.0 - Properties\AndroidManifest.xml + net6.0-android + 21 + Exe + enable + com.Avalonia.AndroidTestApplication + 1 + 1.0 + apk + true + portable - - True - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - True - None - True - False - False - armeabi-v7a;x86 - Xamarin - False - True - False - False - False - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - False - Full - True - False - False - armeabi-v7a,x86 - Xamarin - False - False - False - False - False - False - - - - - - - - - - - - - - + - - - - - - - Designer + + Resources\drawable\Icon.png + + + True + True + True + True + + - - - - + + + True + + + + True + + - - {7b92af71-6287-4693-9dcb-bd5b6e927e23} - Avalonia.Android - - - {3e53a01a-b331-47f3-b828-4a5717e77a24} - Avalonia.Markup.Xaml - - - {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 - + + - - - - - - diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs index 5f33cadf2e..8f4beb2737 100644 --- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs +++ b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs @@ -1,9 +1,10 @@ using System; using Android.App; using Android.Content.PM; -using Android.OS; using Avalonia.Android; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Input.TextInput; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Styling; @@ -14,20 +15,15 @@ namespace Avalonia.AndroidTestApplication [Activity(Label = "Main", MainLauncher = true, Icon = "@drawable/icon", + Theme = "@style/Theme.AppCompat.NoActionBar", + ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, LaunchMode = LaunchMode.SingleInstance/*, ScreenOrientation = ScreenOrientation.Landscape*/)] - public class MainBaseActivity : AvaloniaActivity + public class MainActivity : AvaloniaActivity { - protected override void OnCreate(Bundle savedInstanceState) + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) { - if (Avalonia.Application.Current == null) - { - AppBuilder.Configure() - .UseAndroid() - .SetupWithoutStarting(); - } - base.OnCreate(savedInstanceState); - Content = App.CreateSimpleWindow(); + return base.CustomizeAppBuilder(builder); } } @@ -35,13 +31,17 @@ namespace Avalonia.AndroidTestApplication { public override void Initialize() { - Styles.Add(new DefaultTheme()); - - var baseLight = (IStyle)AvaloniaXamlLoader.Load( - new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default")); - Styles.Add(baseLight); + Styles.Add(new SimpleTheme(new Uri("avares://Avalonia.AndroidTestApplication"))); + } + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) + { + singleViewLifetime.MainView = CreateSimpleWindow(); + } + base.OnFrameworkInitializationCompleted(); } // This provides a simple UI tree for testing input handling, drawing, etc @@ -74,12 +74,12 @@ namespace Avalonia.AndroidTestApplication Foreground = Brushes.Black }, - CreateTextBox(Input.TextInput.TextInputContentType.Normal), - CreateTextBox(Input.TextInput.TextInputContentType.Password), - CreateTextBox(Input.TextInput.TextInputContentType.Email), - CreateTextBox(Input.TextInput.TextInputContentType.Url), - CreateTextBox(Input.TextInput.TextInputContentType.Phone), - CreateTextBox(Input.TextInput.TextInputContentType.Number), + CreateTextBox(TextInputContentType.Normal), + CreateTextBox(TextInputContentType.Password), + CreateTextBox(TextInputContentType.Email), + CreateTextBox(TextInputContentType.Url), + CreateTextBox(TextInputContentType.Digits), + CreateTextBox(TextInputContentType.Number), } } }; @@ -87,16 +87,16 @@ namespace Avalonia.AndroidTestApplication return window; } - private static TextBox CreateTextBox(Input.TextInput.TextInputContentType contentType) + private static TextBox CreateTextBox(TextInputContentType contentType) { var textBox = new TextBox() { Margin = new Thickness(20, 10), Watermark = contentType.ToString(), BorderThickness = new Thickness(3), - FontSize = 20 + FontSize = 20, + [TextInputOptions.ContentTypeProperty] = contentType }; - textBox.TextInputOptionsQuery += (s, e) => { e.ContentType = contentType; }; return textBox; } diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml index 57ee503005..ad8134f628 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml +++ b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml @@ -1,6 +1,4 @@  - - + - - \ No newline at end of file + diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AssemblyInfo.cs b/src/Android/Avalonia.AndroidTestApplication/Properties/AssemblyInfo.cs deleted file mode 100644 index 2528202974..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("Avalonia.AndroidTestApplication")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Avalonia.AndroidTestApplication")] -[assembly: AssemblyCopyright("Copyright © 2015")] -[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")] \ No newline at end of file diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs deleted file mode 100644 index 87fd47df25..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs +++ /dev/null @@ -1,79 +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("Avalonia.AndroidTestApplication.Resource", IsApplication=true)] - -namespace Avalonia.AndroidTestApplication -{ - - - [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 Drawable - { - - // aapt resource value: 0x7F010000 - public const int Icon = 2130771968; - - static Drawable() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Drawable() - { - } - } - - public partial class String - { - - // aapt resource value: 0x7F020000 - public const int ApplicationName = 2130837504; - - // aapt resource value: 0x7F020001 - public const int Hello = 2130837505; - - static String() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private String() - { - } - } - } -} -#pragma warning restore 1591 diff --git a/src/Android/Avalonia.AndroidTestApplication/app.config b/src/Android/Avalonia.AndroidTestApplication/app.config deleted file mode 100644 index fc064cedfb..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/Avalonia.Animation/Easing/Easing.cs b/src/Avalonia.Animation/Easing/Easing.cs index 2f4b93dab1..c721772f3e 100644 --- a/src/Avalonia.Animation/Easing/Easing.cs +++ b/src/Avalonia.Animation/Easing/Easing.cs @@ -39,7 +39,7 @@ namespace Avalonia.Animation.Easings var derivedTypes = typeof(Easing).Assembly.GetTypes() .Where(p => p.Namespace == s_thisType.Namespace) .Where(p => p.IsSubclassOf(s_thisType)) - .Select(p => p).ToList(); + .Select(p => p); foreach (var easingType in derivedTypes) _easingTypes.Add(easingType.Name, easingType); diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs index 2fe68e824d..750fb263f5 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs @@ -123,7 +123,7 @@ namespace Avalonia.Collections { var e = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Remove, - old.ToList(), + old.ToArray(), -1); CollectionChanged(this, e); } diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 6414328e90..9972e72dd4 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -222,7 +222,7 @@ namespace Avalonia.Collections { var e = ResetBehavior == ResetBehavior.Reset ? EventArgsCache.ResetCollectionChanged : - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, _inner.ToList(), 0); + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, _inner.ToArray(), 0); _inner.Clear(); diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 5353e8175d..4e47a720f0 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -99,14 +99,15 @@ namespace Avalonia.Data private sealed class TwoWayBindingDisposable : IDisposable { - private readonly IDisposable _first; - private readonly IDisposable _second; + private readonly IDisposable _toTargetSubscription; + private readonly IDisposable _fromTargetSubsription; + private bool _isDisposed; - public TwoWayBindingDisposable(IDisposable first, IDisposable second) + public TwoWayBindingDisposable(IDisposable toTargetSubscription, IDisposable fromTargetSubsription) { - _first = first; - _second = second; + _toTargetSubscription = toTargetSubscription; + _fromTargetSubsription = fromTargetSubsription; } public void Dispose() @@ -116,8 +117,8 @@ namespace Avalonia.Data return; } - _first.Dispose(); - _second.Dispose(); + _fromTargetSubsription.Dispose(); + _toTargetSubscription.Dispose(); _isDisposed = true; } diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index 160c7301f5..1ca70140ec 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -22,19 +22,9 @@ namespace Avalonia.Data.Core.Plugins var method = GetFirstMethodWithName(instance.GetType(), methodName); - if (method != null) + if (method is not null) { - var parameters = method.GetParameters(); - - if (parameters.Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8) - { - var exception = new ArgumentException( - "Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", - nameof(methodName)); - return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); - } - - return new Accessor(reference, method, parameters); + return new Accessor(reference, method); } else { @@ -82,18 +72,20 @@ namespace Avalonia.Data.Core.Plugins private sealed class Accessor : PropertyAccessorBase { - public Accessor(WeakReference reference, MethodInfo method, ParameterInfo[] parameters) + public Accessor(WeakReference reference, MethodInfo method) { _ = reference ?? throw new ArgumentNullException(nameof(reference)); _ = method ?? throw new ArgumentNullException(nameof(method)); var returnType = method.ReturnType; - bool hasReturn = returnType != typeof(void); - var signatureTypeCount = (hasReturn ? 1 : 0) + parameters.Length; + var parameters = method.GetParameters(); + + var signatureTypeCount = parameters.Length + 1; var paramTypes = new Type[signatureTypeCount]; + for (var i = 0; i < parameters.Length; i++) { ParameterInfo parameter = parameters[i]; @@ -101,16 +93,9 @@ namespace Avalonia.Data.Core.Plugins paramTypes[i] = parameter.ParameterType; } - if (hasReturn) - { - paramTypes[paramTypes.Length - 1] = returnType; + paramTypes[paramTypes.Length - 1] = returnType; - PropertyType = Expression.GetFuncType(paramTypes); - } - else - { - PropertyType = Expression.GetActionType(paramTypes); - } + PropertyType = Expression.GetDelegateType(paramTypes); if (method.IsStatic) { diff --git a/src/Avalonia.Base/Data/Core/PropertyPath.cs b/src/Avalonia.Base/Data/Core/PropertyPath.cs index 665953c4a1..f5b3f92353 100644 --- a/src/Avalonia.Base/Data/Core/PropertyPath.cs +++ b/src/Avalonia.Base/Data/Core/PropertyPath.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core public PropertyPath(IEnumerable elements) { - Elements = elements.ToList(); + Elements = elements.ToArray(); } } diff --git a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs new file mode 100644 index 0000000000..c46891b3ad --- /dev/null +++ b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace Avalonia.Metadata +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class TrimSurroundingWhitespaceAttribute : Attribute + { + + } +} diff --git a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs new file mode 100644 index 0000000000..aeaa38dad9 --- /dev/null +++ b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Avalonia.Metadata +{ + /// + /// Indicates that a collection type should be processed as being whitespace significant by a XAML processor. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class WhitespaceSignificantCollectionAttribute : Attribute + { + } +} diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index 053c7a7547..9ffb5872f0 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -11,4 +11,5 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Visuals, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.PlatformSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 16ba571a5a..535a826c1e 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -159,9 +159,9 @@ namespace Avalonia } /// - public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + public override void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) { - vistor.Visit(this, ref data); + visitor.Visit(this, ref data); } /// @@ -242,11 +242,5 @@ namespace Avalonia _ = type ?? throw new ArgumentNullException(nameof(type)); return GetMetadata(type).DefaultValue; } - - [DebuggerHidden] - private Func Cast(Func validate) - { - return (o, v) => validate((THost)o, v); - } } } diff --git a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs index 09e408cb42..bd3b86f06d 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs @@ -23,7 +23,7 @@ namespace Avalonia.Utilities var assetDoc = XDocument.Load(stream); XNamespace assetNs = assetDoc.Root!.Attribute("xmlns")!.Value; - List entries= + List entries = (from entry in assetDoc.Root.Element(assetNs + "Entries")!.Elements(assetNs + "AvaloniaResourcesIndexEntry") select new AvaloniaResourcesIndexEntry { diff --git a/src/Avalonia.Base/Utilities/UriExtensions.cs b/src/Avalonia.Base/Utilities/UriExtensions.cs new file mode 100644 index 0000000000..c706f72a63 --- /dev/null +++ b/src/Avalonia.Base/Utilities/UriExtensions.cs @@ -0,0 +1,70 @@ +using System; + +namespace Avalonia.Utilities; + +internal static class UriExtensions +{ + public static bool IsAbsoluteResm(this Uri uri) => + uri.IsAbsoluteUri && uri.IsResm(); + + public static bool IsResm(this Uri uri) => uri.Scheme == "resm"; + + public static bool IsAvares(this Uri uri) => uri.Scheme == "avares"; + + public static Uri EnsureAbsolute(this 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.IsResm()) + throw new ArgumentException( + $"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm"); + return new Uri(baseUri, uri); + } + + public static string GetUnescapeAbsolutePath(this Uri uri) => + Uri.UnescapeDataString(uri.AbsolutePath); + + public static string GetUnescapeAbsoluteUri(this Uri uri) => + Uri.UnescapeDataString(uri.AbsoluteUri); + + public static string GetAssemblyNameFromQuery(this Uri uri) + { + const string assembly = "assembly"; + + var query = Uri.UnescapeDataString(uri.Query); + + // Skip the '?' + var currentIndex = 1; + while (currentIndex < query.Length) + { + var isFind = false; + for (var i = 0; i < assembly.Length; ++currentIndex, ++i) + if (query[currentIndex] == assembly[i]) + { + isFind = i == assembly.Length - 1; + } + else + { + break; + } + + // Skip the '=' + ++currentIndex; + + var beginIndex = currentIndex; + while (currentIndex < query.Length && query[currentIndex] != '&') + ++currentIndex; + + if (isFind) + return query.Substring(beginIndex, currentIndex - beginIndex); + + ++currentIndex; + } + + return ""; + } +} diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index e864ea2007..5a7daa6d12 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -83,6 +83,9 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index ae2bf99d1e..c20b2f656e 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -105,7 +105,7 @@ namespace Avalonia.Build.Tasks { var typeToXamlIndex = new Dictionary(); - foreach (var s in sources.ToList()) + foreach (var s in sources.ToArray()) { if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml") || s.Path.ToLowerInvariant().EndsWith(".axaml")) { diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 593d79471e..fa437de186 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -124,6 +124,9 @@ namespace Avalonia.Build.Tasks var indexerAccessorClosure = new TypeDefinition("CompiledAvaloniaXaml", "!IndexerAccessorFactoryClosure", TypeAttributes.Class, asm.MainModule.TypeSystem.Object); asm.MainModule.Types.Add(indexerAccessorClosure); + var trampolineBuilder = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlTrampolines", + TypeAttributes.Class, asm.MainModule.TypeSystem.Object); + asm.MainModule.Types.Add(trampolineBuilder); var (xamlLanguage , emitConfig) = AvaloniaXamlIlLanguage.Configure(typeSystem); var compilerConfig = new AvaloniaXamlIlCompilerConfiguration(typeSystem, @@ -133,6 +136,7 @@ namespace Avalonia.Build.Tasks AvaloniaXamlIlLanguage.CustomValueConverter, new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)), new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)), + new XamlIlTrampolineBuilder(typeSystem.CreateTypeBuilder(trampolineBuilder)), new DeterministicIdGenerator()); @@ -255,6 +259,8 @@ namespace Avalonia.Build.Tasks true), (closureName, closureBaseType) => populateBuilder.DefineSubType(closureBaseType, closureName, false), + (closureName, returnType, parameterTypes) => + populateBuilder.DefineDelegateSubType(closureName, false, returnType, parameterTypes), res.Uri, res ); diff --git a/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt b/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs index fe6acdc532..906ec661ae 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs @@ -3946,7 +3946,7 @@ namespace Avalonia.Collections { sort.Initialize(itemType); - if(seq is IOrderedEnumerable orderedEnum) + if (seq is IOrderedEnumerable orderedEnum) { seq = sort.ThenBy(orderedEnum); } diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 2c206b53f6..ce84a7fe84 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -30,6 +30,7 @@ MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownV MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.TopLevel' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. +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 +49,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 +72,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: 73 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/Button.cs b/src/Avalonia.Controls/Button.cs index a7a4759182..899521536f 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; @@ -237,7 +238,7 @@ namespace Avalonia.Controls { HotKey = _hotkey; } - + base.OnAttachedToLogicalTree(e); if (Command != null) @@ -455,6 +456,8 @@ namespace Avalonia.Controls } } + protected override AutomationPeer OnCreateAutomationPeer() => new ButtonAutomationPeer(this); + /// protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { @@ -472,6 +475,8 @@ namespace Avalonia.Controls } } + internal void PerformClick() => OnClick(); + /// /// Called when the event fires. /// @@ -534,6 +539,7 @@ namespace Avalonia.Controls if (e.Key == Key.Enter && IsVisible && IsEnabled) { OnClick(); + e.Handled = true; } } @@ -547,6 +553,7 @@ namespace Avalonia.Controls if (e.Key == Key.Escape && IsVisible && IsEnabled) { OnClick(); + e.Handled = true; } } 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/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 72b09b7a3c..d4e7fc0e47 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; @@ -12,12 +13,14 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.VisualTree; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// A drop-down list control. /// + [TemplatePart("PART_Popup", typeof(Popup))] public class ComboBox : SelectingItemsControl { /// @@ -295,6 +298,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) 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..cec662aad8 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.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; @@ -69,6 +70,7 @@ namespace Avalonia.Controls private DataTemplates? _dataTemplates; private IControl? _focusAdorner; + private AutomationPeer? _automationPeer; /// /// Gets or sets the control's focus adorner. @@ -242,6 +244,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); 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..a923c44d05 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs @@ -2,6 +2,8 @@ using System.Globalization; using System.Linq; using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -58,18 +60,17 @@ namespace Avalonia.Controls.Primitives private Vector _offset; private bool _hasInit; private bool _suppressUpdateOffset; - private ListBoxItem? _pressedItem; 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); } @@ -523,26 +524,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,7 +548,7 @@ 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) { 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..4083524881 --- /dev/null +++ b/src/Avalonia.Controls/Documents/TextElement.cs @@ -0,0 +1,129 @@ +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 = + AvaloniaProperty.Register(nameof(Background), inherits: true); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontFamilyProperty = + TextBlock.FontFamilyProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontSizeProperty = + TextBlock.FontSizeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontStyleProperty = + TextBlock.FontStyleProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontWeightProperty = + TextBlock.FontWeightProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty ForegroundProperty = + TextBlock.ForegroundProperty.AddOwner(); + + /// + /// 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 a brush used to paint the text. + /// + public IBrush? Foreground + { + get { return GetValue(ForegroundProperty); } + set { 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(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/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/Image.cs b/src/Avalonia.Controls/Image.cs index c448729643..3d67880638 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); } /// diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index ed8f9efb2e..0cd72dc91c 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; @@ -335,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); 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/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 185b834052..cddd621b8e 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,6 +20,7 @@ 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 { @@ -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); 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/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/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 9886dd913a..93acd88fb1 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -107,9 +107,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() @@ -240,6 +237,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 +266,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 +289,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 +317,7 @@ namespace Avalonia.Controls.Presenters } _createdChild = true; + } /// @@ -325,18 +344,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 +470,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 361febf305..39a512a773 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -226,7 +226,7 @@ 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; 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/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 8629af5243..10ce31088a 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -152,6 +152,15 @@ namespace Avalonia.Controls.Presenters set => TextBlock.SetFontWeight(this, value); } + /// + /// Gets or sets the font stretch. + /// + public FontStretch FontStretch + { + get => TextBlock.GetFontStretch(this); + set => TextBlock.SetFontStretch(this, value); + } + /// /// Gets or sets a brush used to paint the text. /// @@ -310,7 +319,7 @@ namespace Avalonia.Controls.Presenters var top = 0d; var left = 0.0; - var (_, textHeight) = TextLayout.Size; + var textHeight = TextLayout.Bounds.Height; if (Bounds.Height < textHeight) { @@ -385,10 +394,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)); @@ -459,8 +472,8 @@ namespace Avalonia.Controls.Presenters 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; @@ -498,25 +511,29 @@ namespace Avalonia.Controls.Presenters protected override Size MeasureOverride(Size availableSize) { - _textLayout = null; - _constraint = availableSize; + + _textLayout = null; + + InvalidateArrange(); - return TextLayout.Size; + var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); + + return new Size(measuredSize.Width, measuredSize.Height); } protected override Size ArrangeOverride(Size finalSize) { - if (!double.IsInfinity(_constraint.Width)) + if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) { - return base.ArrangeOverride(finalSize); + return finalSize; } _constraint = finalSize; - + _textLayout = null; - return base.ArrangeOverride(finalSize); + return finalSize; } private int CoerceCaretIndex(int value) @@ -615,11 +632,11 @@ namespace Avalonia.Controls.Presenters CaretChanged(); } - public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward) + public CharacterHit GetNextCharacterHit(LogicalDirection direction = LogicalDirection.Forward) { if (Text is null) { - return; + return default; } if (FlowDirection == FlowDirection.RightToLeft) @@ -636,7 +653,7 @@ namespace Avalonia.Controls.Presenters if (lineIndex < 0) { - return; + return default; } if (direction == LogicalDirection.Forward) @@ -697,6 +714,13 @@ namespace Avalonia.Controls.Presenters } } + return characterHit; + } + + public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward) + { + var characterHit = GetNextCharacterHit(direction); + UpdateCaret(characterHit); _navigationPosition = _caretBounds.Position; 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/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/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 9dd68bfe68..99ef93d8e8 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; @@ -43,7 +44,7 @@ namespace Avalonia.Controls.Primitives /// The dependency resolver to use. If null the default dependency resolver will be used. /// public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver? dependencyResolver) - : base(ValidatingPopupImpl.Wrap(impl), dependencyResolver) + : base(impl, dependencyResolver) { ParentTopLevel = parent; } @@ -169,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 b4cfd9404c..cec02c7ae9 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -292,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, @@ -531,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) { @@ -833,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); } } @@ -976,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/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/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 index f1d07b2679..69922f279c 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -13,6 +13,8 @@ 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 { diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index d2161deb6e..ae1605a985 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -77,6 +77,7 @@ namespace Avalonia.Controls /// /// A control with two views: A collapsible pane and an area for content /// + [TemplatePart("PART_PaneRoot", typeof(Panel))] [PseudoClasses(":open", ":closed")] [PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline")] [PseudoClasses(":left", ":right")] diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index e0ec23b31f..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); } /// 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 ed012bd8b1..69b55b7222 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 { @@ -62,6 +66,15 @@ namespace Avalonia.Controls 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. /// @@ -97,6 +110,14 @@ 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. /// @@ -113,7 +134,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty TextTrimmingProperty = - AvaloniaProperty.Register(nameof(TextTrimming)); + AvaloniaProperty.Register(nameof(TextTrimming), defaultValue: TextTrimming.None); /// /// Defines the property. @@ -121,7 +142,6 @@ namespace Avalonia.Controls public static readonly StyledProperty TextDecorationsProperty = AvaloniaProperty.Register(nameof(TextDecorations)); - private string? _text; private TextLayout? _textLayout; private Size _constraint; @@ -140,13 +160,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,13 +197,30 @@ 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 inlines. + /// + [Content] + public InlineCollection Inlines { get; } + /// /// Gets or sets the font family. /// @@ -217,6 +256,15 @@ namespace Avalonia.Controls 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. @@ -295,7 +343,7 @@ namespace Avalonia.Controls /// Gets the value of the attached on a control. /// /// The control. - /// The font family. + /// The font size. public static double GetFontSize(Control control) { return control.GetValue(FontSizeProperty); @@ -305,7 +353,7 @@ namespace Avalonia.Controls /// Gets the value of the attached on a control. /// /// The control. - /// The font family. + /// The font style. public static FontStyle GetFontStyle(Control control) { return control.GetValue(FontStyleProperty); @@ -315,11 +363,21 @@ namespace Avalonia.Controls /// Gets the value of the attached on a control. /// /// The control. - /// The font family. + /// The font weight. public static FontWeight GetFontWeight(Control control) { return control.GetValue(FontWeightProperty); } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font stretch. + public static FontStretch GetFontStretch(Control control) + { + return control.GetValue(FontStretchProperty); + } /// /// Gets the value of the attached on a control. @@ -336,7 +394,6 @@ namespace Avalonia.Controls /// /// The control. /// The property value to set. - /// The font family. public static void SetFontFamily(Control control, FontFamily value) { control.SetValue(FontFamilyProperty, value); @@ -347,7 +404,6 @@ namespace Avalonia.Controls /// /// The control. /// The property value to set. - /// The font family. public static void SetFontSize(Control control, double value) { control.SetValue(FontSizeProperty, value); @@ -358,7 +414,6 @@ namespace Avalonia.Controls /// /// The control. /// The property value to set. - /// The font family. public static void SetFontStyle(Control control, FontStyle value) { control.SetValue(FontStyleProperty, value); @@ -369,18 +424,26 @@ namespace Avalonia.Controls /// /// The control. /// The property value to set. - /// The font family. public static void SetFontWeight(Control control, FontWeight value) { control.SetValue(FontWeightProperty, value); } + + /// + /// 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); + } /// /// Sets the value of the attached on a control. /// /// The control. /// The property value to set. - /// The font family. public static void SetForeground(Control control, IBrush? value) { control.SetValue(ForegroundProperty, value); @@ -399,25 +462,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 +489,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 +521,8 @@ namespace Avalonia.Controls constraint.Width, constraint.Height, maxLines: MaxLines, - lineHeight: LineHeight); + lineHeight: LineHeight, + textStyleOverrides: textStyleOverrides); } /// @@ -464,32 +535,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); + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) + { + return finalSize; } + + _constraint = finalSize; + + _textLayout = null; - var measuredSize = TextLayout?.Size ?? Size.Empty; + return finalSize; + } - return measuredSize.Inflate(padding); + protected override AutomationPeer OnCreateAutomationPeer() + { + return new TextBlockAutomationPeer(this); } private static bool IsValidMaxLines(int maxLines) => maxLines >= 0; @@ -524,6 +606,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 4ec0c4c5e1..d9bd2ddef9 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 { @@ -201,7 +203,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 +261,8 @@ namespace Avalonia.Controls UndoRedoState state; if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) _undoRedoHelper.UpdateLastState(); + + SelectionStart = SelectionEnd = value; } } @@ -300,14 +307,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 +336,8 @@ namespace Avalonia.Controls { UpdateCommandStates(); } - - if (value == SelectionStart) + + if (SelectionStart == value && CaretIndex != value) { CaretIndex = value; } @@ -351,10 +359,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 +467,7 @@ namespace Avalonia.Controls /// public void ClearSelection() { - SelectionStart = SelectionEnd = CaretIndex; + CaretIndex = SelectionStart; } /// @@ -855,6 +865,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheEndOfDocument)) { @@ -862,6 +873,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheStartOfLine)) { @@ -869,7 +881,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; - + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheEndOfLine)) { @@ -877,24 +889,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; @@ -902,7 +921,9 @@ namespace Avalonia.Controls } else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection)) { + SelectionStart = caretIndex; MoveEnd(false); + SelectionEnd = _presenter.CaretIndex; movement = true; selection = true; handled = true; @@ -941,7 +962,7 @@ namespace Avalonia.Controls } else { - SelectionStart = SelectionEnd = _presenter.CaretIndex; + CaretIndex = _presenter.CaretIndex; } break; @@ -963,7 +984,7 @@ namespace Avalonia.Controls } else { - SelectionStart = SelectionEnd = _presenter.CaretIndex; + CaretIndex = _presenter.CaretIndex; } break; @@ -979,32 +1000,19 @@ namespace Avalonia.Controls if (!DeleteSelection() && caretIndex > 0) { - var removedCharacters = 0; - - // \r\n needs special treatment here - if (caretIndex - 1 > 0 && text[caretIndex - 1] == '\n' && text[caretIndex - 2] == '\r') - { - removedCharacters = 2; - } - else - { - Codepoint.ReadAt(text.AsMemory(), caretIndex - 1, out removedCharacters); - } - - if (removedCharacters == 0) - { - return; - } + _presenter.MoveCaretHorizontal(LogicalDirection.Backward); + var removedCharacters = Math.Max(0, caretIndex - _presenter.CaretIndex); + var length = Math.Max(0, caretIndex - removedCharacters); SetTextInternal(text.Substring(0, length) + text.Substring(caretIndex)); - - CaretIndex = caretIndex - removedCharacters; - - ClearSelection(); + + CaretIndex = _presenter.CaretIndex; } + + SnapshotUndoRedo(); handled = true; break; @@ -1019,14 +1027,13 @@ namespace Avalonia.Controls if (!DeleteSelection() && caretIndex < text.Length) { - _presenter.MoveCaretHorizontal(); + var characterHit = _presenter.GetNextCharacterHit(); - var removedCharacters = Math.Max(0, _presenter.CaretIndex - caretIndex); + var removedCharacters = Math.Max(0, + characterHit.FirstCharacterIndex + characterHit.TrailingLength - caretIndex); SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters)); - - CaretIndex = caretIndex; } SnapshotUndoRedo(); @@ -1195,6 +1202,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) @@ -1240,6 +1252,7 @@ namespace Avalonia.Controls { var text = Text ?? string.Empty; var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; if (!wholeWord) { @@ -1248,30 +1261,52 @@ namespace Avalonia.Controls 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; + + if (!isSelecting) + { + CaretIndex = SelectionEnd; } } } @@ -1283,32 +1318,20 @@ namespace Avalonia.Controls return; } - var text = Text ?? string.Empty; var caretIndex = CaretIndex; if (document) { - caretIndex = 0; + _presenter.MoveCaretToTextPosition(0); } 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; - } - - pos += line.TextRange.Length; - } + var textLines = _presenter.TextLayout.TextLines; + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, true); + var textLine = textLines[lineIndex]; - caretIndex = pos; + _presenter.MoveCaretToTextPosition(textLine.TextRange.Start); } - - CaretIndex = caretIndex; } private void MoveEnd(bool document) @@ -1323,36 +1346,24 @@ namespace Avalonia.Controls if (document) { - caretIndex = text.Length; + _presenter.MoveCaretToTextPosition(text.Length, true); } else { - var lines = _presenter.TextLayout.TextLines; - var pos = 0; - - foreach (var line in lines) + var textLines = _presenter.TextLayout.TextLines; + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, true); + var textLine = textLines[lineIndex]; + + if (caretIndex == textLine.TextRange.Start + textLine.TextRange.Length - textLine.NewLineLength && + lineIndex + 1 < textLines.Count) { - pos += line.TextRange.Length; - - if (pos > caretIndex) - { - if (pos < text.Length) - { - --pos; - if (pos > 0 && text[pos - 1] == '\r' && text[pos] == '\n') - { - --pos; - } - } - - break; - } + textLine = textLines[++lineIndex]; } - caretIndex = pos; - } + var textPosition = textLine.TextRange.Start + textLine.TextRange.Length - textLine.NewLineLength; - CaretIndex = caretIndex; + _presenter.MoveCaretToTextPosition(textPosition, true); + } } /// @@ -1362,50 +1373,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); } @@ -1431,16 +1448,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..95a597e831 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; @@ -26,6 +27,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, @@ -134,8 +136,6 @@ namespace Avalonia.Controls "Could not create window implementation: maybe no windowing subsystem was initialized?"); } - impl = ValidatingToplevelImpl.Wrap(impl); - PlatformImpl = impl; _actualTransparencyLevel = PlatformImpl.TransparencyLevel; 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 9a276e74d2..1d806913dd 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -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 20c0ed386d..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 { 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 f27568694d..93fbf6d17b 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -133,7 +133,7 @@ namespace Avalonia.Controls if (logicalScrollable?.IsLogicalScrollEnabled == true) { - return logicalScrollable.GetControlInDirection(direction, from!); + return logicalScrollable.GetControlInDirection(direction, from); } else { diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index a5f48bd4a5..700dfa6dd1 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; @@ -237,14 +238,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 +255,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 +527,7 @@ namespace Avalonia.Controls private void CloseInternal() { - foreach (var (child, _) in _children.ToList()) + foreach (var (child, _) in _children.ToArray()) { child.CloseInternal(); } @@ -546,7 +551,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 +859,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 +877,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 +1040,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/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/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/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/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..db48aaaa8e 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 a5a933571d..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 => { @@ -247,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/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.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/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index b619b9d129..f22fb4f317 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -157,9 +157,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 +200,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 index 270c5305e5..68b4d4754f 100644 --- a/src/Avalonia.Input/ApiCompatBaseline.txt +++ b/src/Avalonia.Input/ApiCompatBaseline.txt @@ -4,11 +4,26 @@ 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.TextInputOptionsQueryEvent' 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.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.add_TextInputOptionsQuery(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. +MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_TextInputOptionsQuery(System.EventHandler)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetActive(System.Boolean)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetActive(System.Boolean)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetClient(Avalonia.Input.TextInput.ITextInputMethodClient)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetOptions(Avalonia.Input.TextInput.TextInputOptions)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetOptions(Avalonia.Input.TextInput.TextInputOptionsQueryEventArgs)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetOptions(Avalonia.Input.TextInput.TextInputOptionsQueryEventArgs)' does not exist in the implementation but it does exist in the contract. +EnumValuesMustMatch : Enum value 'Avalonia.Input.TextInput.TextInputContentType Avalonia.Input.TextInput.TextInputContentType.Email' is (System.Int32)5 in the implementation but (System.Int32)1 in the contract. +EnumValuesMustMatch : Enum value 'Avalonia.Input.TextInput.TextInputContentType Avalonia.Input.TextInput.TextInputContentType.Number' is (System.Int32)4 in the implementation but (System.Int32)3 in the contract. +EnumValuesMustMatch : Enum value 'Avalonia.Input.TextInput.TextInputContentType Avalonia.Input.TextInput.TextInputContentType.Password' is (System.Int32)8 in the implementation but (System.Int32)5 in the contract. +MembersMustExist : Member 'public Avalonia.Input.TextInput.TextInputContentType Avalonia.Input.TextInput.TextInputContentType Avalonia.Input.TextInput.TextInputContentType.Phone' does not exist in the implementation but it does exist in the contract. +EnumValuesMustMatch : Enum value 'Avalonia.Input.TextInput.TextInputContentType Avalonia.Input.TextInput.TextInputContentType.Url' is (System.Int32)6 in the implementation but (System.Int32)4 in the contract. +TypesMustExist : Type 'Avalonia.Input.TextInput.TextInputOptionsQueryEventArgs' 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 +Total Issues: 27 diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 30301788bc..c7b5b8e27f 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -73,7 +73,7 @@ namespace Avalonia.Input else if (Current != null) { // If control is null, set focus to the topmost focus scope. - foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList()) + foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToArray()) { if (scope != Scope && _focusScopes.TryGetValue(scope, out var element) && diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 5ec0bd6ee4..6bc9294ddd 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -126,14 +126,6 @@ namespace Avalonia.Input 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. @@ -283,15 +275,6 @@ namespace Avalonia.Input 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. diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 438e3fcadb..3df717b8c4 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -24,30 +24,7 @@ namespace Avalonia.Input // 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); - } - } + public IInputElement? FocusedElement => _focusedElement; private void ClearFocusWithinAncestors(IInputElement? element) { @@ -162,8 +139,8 @@ namespace Avalonia.Input } SetIsFocusWithin(FocusedElement, element); - - FocusedElement = element; + _focusedElement = element; + _focusedRoot = _focusedElement?.VisualRoot as IInputRoot; interactive?.RaiseEvent(new RoutedEventArgs { @@ -178,6 +155,9 @@ namespace Avalonia.Input NavigationMethod = method, KeyModifiers = keyModifiers, }); + + _textInputManager.SetFocusedElement(element); + RaisePropertyChanged(nameof(FocusedElement)); } } diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 34d2038d66..a5d54bb047 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -5,6 +5,7 @@ using System.Reactive.Linq; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.Platform; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Input @@ -146,6 +147,8 @@ namespace Avalonia.Input if(mouse._disposed) return; + if (e.Type == RawPointerEventType.NonClientLeftButtonDown) return; + _position = e.Root.PointToScreen(e.Position); var props = CreateProperties(e); var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); @@ -273,7 +276,7 @@ namespace Avalonia.Input } private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers, IReadOnlyList? intermediatePoints) + KeyModifiers inputModifiers, Lazy?>? intermediatePoints) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); @@ -334,6 +337,13 @@ namespace Avalonia.Input var hit = HitTest(root, p); var source = GetSource(hit); + // KeyModifiers.Shift should scroll in horizontal direction. This does not work on every platform. + // If Shift-Key is pressed and X is close to 0 we swap the Vector. + if (inputModifiers == KeyModifiers.Shift && MathUtilities.IsZero(delta.X)) + { + delta = new Vector(delta.Y, delta.X); + } + if (source is not null) { var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 40495a2f0a..0604d09dc4 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -11,7 +11,7 @@ namespace Avalonia.Input private readonly IVisual? _rootVisual; private readonly Point _rootVisualPosition; private readonly PointerPointProperties _properties; - private readonly IReadOnlyList? _previousPoints; + private Lazy?>? _previousPoints; public PointerEventArgs(RoutedEvent routedEvent, IInteractive? source, @@ -38,7 +38,7 @@ namespace Avalonia.Input ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, - IReadOnlyList? previousPoints) + Lazy?>? previousPoints) : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { _previousPoints = previousPoints; @@ -121,13 +121,14 @@ namespace Avalonia.Input /// public IReadOnlyList GetIntermediatePoints(IVisual? relativeTo) { - if (_previousPoints == null || _previousPoints.Count == 0) + var previousPoints = _previousPoints?.Value; + 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 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); + var pt = previousPoints[c]; + points[c] = new PointerPoint(Pointer, GetPosition(pt.Position, relativeTo), _properties); } points[points.Length - 1] = GetCurrentPoint(relativeTo); diff --git a/src/Avalonia.Input/Properties/AssemblyInfo.cs b/src/Avalonia.Input/Properties/AssemblyInfo.cs index 433f821ca3..6a68bf60d1 100644 --- a/src/Avalonia.Input/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Input/Properties/AssemblyInfo.cs @@ -2,4 +2,5 @@ using System.Reflection; using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.TextInput")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")] diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 1fe19d9c55..c157fa059c 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -33,6 +33,8 @@ namespace Avalonia.Input.Raw /// public class RawPointerEventArgs : RawInputEventArgs { + private RawPointerPoint _point; + /// /// Initializes a new instance of the class. /// @@ -58,11 +60,50 @@ namespace Avalonia.Input.Raw Type = type; InputModifiers = inputModifiers; } + + /// + /// 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 point properties and position, in client DIPs. + /// The input modifiers. + public RawPointerEventArgs( + IInputDevice device, + ulong timestamp, + IInputRoot root, + RawPointerEventType type, + RawPointerPoint point, + RawInputModifiers inputModifiers) + : base(device, timestamp, root) + { + Contract.Requires(device != null); + Contract.Requires(root != null); + + Point = point; + Type = type; + InputModifiers = inputModifiers; + } + + /// + /// Gets the pointer properties and position, in client DIPs. + /// + public RawPointerPoint Point + { + get => _point; + set => _point = value; + } /// /// Gets the mouse position, in client DIPs. /// - public Point Position { get; set; } + public Point Position + { + get => _point.Position; + set => _point.Position = value; + } /// /// Gets the type of the event. @@ -78,6 +119,19 @@ namespace Avalonia.Input.Raw /// Points that were traversed by a pointer since the previous relevant event, /// only valid for Move and TouchUpdate /// - public IReadOnlyList? IntermediatePoints { get; set; } + public Lazy?>? IntermediatePoints { get; set; } + } + + public struct RawPointerPoint + { + /// + /// Pointer position, in client DIPs. + /// + public Point Position { get; set; } + + public RawPointerPoint() + { + Position = default; + } } } diff --git a/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs b/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs index 2d24ed30a0..4404c903b7 100644 --- a/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs +++ b/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs @@ -2,9 +2,9 @@ namespace Avalonia.Input.TextInput { public interface ITextInputMethodImpl { - void SetActive(bool active); + void SetClient(ITextInputMethodClient? client); void SetCursorRect(Rect rect); - void SetOptions(TextInputOptionsQueryEventArgs options); + void SetOptions(TextInputOptions options); void Reset(); } diff --git a/src/Avalonia.Input/TextInput/InputMethodManager.cs b/src/Avalonia.Input/TextInput/InputMethodManager.cs index 64422a7fdf..4734224da4 100644 --- a/src/Avalonia.Input/TextInput/InputMethodManager.cs +++ b/src/Avalonia.Input/TextInput/InputMethodManager.cs @@ -35,21 +35,26 @@ namespace Avalonia.Input.TextInput { _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); + + if (_focusedElement is StyledElement target) + { + _im?.SetOptions(TextInputOptions.FromStyledElement(target)); + } + else + { + _im?.SetOptions(TextInputOptions.Default); + } + + _transformTracker.SetVisual(_client?.TextViewVisual); UpdateCursorRect(); - _im?.SetActive(true); + _im?.SetClient(_client); } else { - _im?.SetActive(false); + _im?.SetClient(null); _transformTracker.SetVisual(null); } } @@ -91,9 +96,12 @@ namespace Avalonia.Input.TextInput _focusedElement = element; var inputMethod = (element?.VisualRoot as ITextInputMethodRoot)?.InputMethod; - if (_im != inputMethod) - _im?.SetActive(false); + if (_im != inputMethod) + { + _im?.SetClient(null); + } + _im = inputMethod; TryFindAndApplyClient(); diff --git a/src/Avalonia.Input/TextInput/TextInputContentType.cs b/src/Avalonia.Input/TextInput/TextInputContentType.cs index 5d73fc1552..02a9385354 100644 --- a/src/Avalonia.Input/TextInput/TextInputContentType.cs +++ b/src/Avalonia.Input/TextInput/TextInputContentType.cs @@ -1,12 +1,62 @@ namespace Avalonia.Input.TextInput -{ +{ public enum TextInputContentType { + /// + /// Default keyboard for the users configured input method. + /// Normal = 0, - Email = 1, - Phone = 2, - Number = 3, - Url = 4, - Password = 5 + + /// + /// Display a keyboard that only has alphabetic characters. + /// + Alpha, + + /// + /// Display a numeric keypad only capable of numbers. i.e. Phone number + /// + Digits, + + /// + /// Display a numeric keypad for inputting a PIN. + /// + Pin, + + /// + /// Display a numeric keypad capable of inputting numbers including decimal seperator and sign. + /// + Number, + + /// + /// Display a keyboard for entering an email address. + /// + Email, + + /// + /// Display a keyboard for entering a URL. + /// + Url, + + /// + /// Display a keyboard for entering a persons name. + /// + Name, + + /// + /// Display a keyboard for entering sensitive data. + /// + Password, + + /// + /// Display a keyboard suitable for #tag and @mentions. + /// Not available on all platforms, will fallback to a suitable keyboard + /// when not available. + /// + Social, + + /// + /// Display a keyboard for entering a search keyword. + /// + Search } } diff --git a/src/Avalonia.Input/TextInput/TextInputOptions.cs b/src/Avalonia.Input/TextInput/TextInputOptions.cs new file mode 100644 index 0000000000..af5f142a25 --- /dev/null +++ b/src/Avalonia.Input/TextInput/TextInputOptions.cs @@ -0,0 +1,220 @@ +namespace Avalonia.Input.TextInput; + +public class TextInputOptions +{ + public static TextInputOptions FromStyledElement(StyledElement avaloniaObject) + { + var result = new TextInputOptions + { + ContentType = GetContentType(avaloniaObject), + Multiline = GetMultiline(avaloniaObject), + AutoCapitalization = GetAutoCapitalization(avaloniaObject), + IsSensitive = GetIsSensitive(avaloniaObject), + Lowercase = GetLowercase(avaloniaObject), + Uppercase = GetUppercase(avaloniaObject) + }; + + return result; + } + + public static readonly TextInputOptions Default = new(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty ContentTypeProperty = + AvaloniaProperty.RegisterAttached( + "ContentType", + defaultValue: TextInputContentType.Normal, + inherits: true); + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetContentType(StyledElement avaloniaObject, TextInputContentType value) + { + avaloniaObject.SetValue(ContentTypeProperty, value); + } + + /// + /// Gets the value of the attached . + /// + /// The target. + /// TextInputContentType + public static TextInputContentType GetContentType(StyledElement avaloniaObject) + { + return avaloniaObject.GetValue(ContentTypeProperty); + } + + /// + /// The content type (mostly for determining the shape of the virtual keyboard) + /// + public TextInputContentType ContentType { get; set; } + + /// + /// Defines the property. + /// + public static readonly AttachedProperty MultilineProperty = + AvaloniaProperty.RegisterAttached( + "Multiline", + inherits: true); + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetMultiline(StyledElement avaloniaObject, bool value) + { + avaloniaObject.SetValue(MultilineProperty, value); + } + + /// + /// Gets the value of the attached . + /// + /// The target. + /// true if multiline + public static bool GetMultiline(StyledElement avaloniaObject) + { + return avaloniaObject.GetValue(MultilineProperty); + } + + /// + /// Text is multiline + /// + public bool Multiline { get; set; } + + /// + /// Defines the property. + /// + public static readonly AttachedProperty LowercaseProperty = + AvaloniaProperty.RegisterAttached( + "Lowercase", + inherits: true); + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetLowercase(StyledElement avaloniaObject, bool value) + { + avaloniaObject.SetValue(LowercaseProperty, value); + } + + /// + /// Gets the value of the attached . + /// + /// The target. + /// true if Lowercase + public static bool GetLowercase(StyledElement avaloniaObject) + { + return avaloniaObject.GetValue(LowercaseProperty); + } + + /// + /// Text is in lower case + /// + public bool Lowercase { get; set; } + + /// + /// Defines the property. + /// + public static readonly AttachedProperty UppercaseProperty = + AvaloniaProperty.RegisterAttached( + "Uppercase", + inherits: true); + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetUppercase(StyledElement avaloniaObject, bool value) + { + avaloniaObject.SetValue(UppercaseProperty, value); + } + + /// + /// Gets the value of the attached . + /// + /// The target. + /// true if Uppercase + public static bool GetUppercase(StyledElement avaloniaObject) + { + return avaloniaObject.GetValue(UppercaseProperty); + } + + /// + /// Text is in upper case + /// + public bool Uppercase { get; set; } + + /// + /// Defines the property. + /// + public static readonly AttachedProperty AutoCapitalizationProperty = + AvaloniaProperty.RegisterAttached( + "AutoCapitalization", + inherits: true); + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetAutoCapitalization(StyledElement avaloniaObject, bool value) + { + avaloniaObject.SetValue(AutoCapitalizationProperty, value); + } + + /// + /// Gets the value of the attached . + /// + /// The target. + /// true if AutoCapitalization + public static bool GetAutoCapitalization(StyledElement avaloniaObject) + { + return avaloniaObject.GetValue(AutoCapitalizationProperty); + } + + /// + /// Automatically capitalize letters at the start of the sentence + /// + public bool AutoCapitalization { get; set; } + + /// + /// Defines the property. + /// + public static readonly AttachedProperty IsSensitiveProperty = + AvaloniaProperty.RegisterAttached( + "IsSensitive", + inherits: true); + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetIsSensitive(StyledElement avaloniaObject, bool value) + { + avaloniaObject.SetValue(IsSensitiveProperty, value); + } + + /// + /// Gets the value of the attached . + /// + /// The target. + /// true if IsSensitive + public static bool GetIsSensitive(StyledElement avaloniaObject) + { + return avaloniaObject.GetValue(IsSensitiveProperty); + } + + /// + /// Text contains sensitive data like card numbers and should not be stored + /// + public bool IsSensitive { get; set; } +} 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 index 12ad182bf8..ed7ec5465c 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -114,7 +114,7 @@ namespace Avalonia.Input { if (_disposed) return; - var values = _pointers.Values.ToList(); + var values = _pointers.Values.ToArray(); _pointers.Clear(); _disposed = true; foreach (var p in values) diff --git a/src/Avalonia.Interactivity/ApiCompatBaseline.txt b/src/Avalonia.Interactivity/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Avalonia.Interactivity/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Avalonia.Layout/ApiCompatBaseline.txt b/src/Avalonia.Layout/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Avalonia.Layout/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 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..70ab38e786 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -17,6 +17,10 @@ PreserveNewest + + + + 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/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..fb03ec2f6e 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 AssemblyDescriptorResolver s_assemblyDescriptorResolver = new(); private AssemblyDescriptor? _defaultResmAssembly; + /// + /// Introduced for tests. + /// + internal static void SetAssemblyDescriptorResolver(AssemblyDescriptorResolver 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) @@ -188,8 +184,8 @@ namespace Avalonia.PlatformSupport private (AssemblyDescriptor 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) @@ -198,197 +194,20 @@ namespace Avalonia.PlatformSupport { 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..420ac0796c 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..a3de7f2b8a --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Avalonia.Utilities; + +namespace Avalonia.PlatformSupport.Internal; + +internal 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(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..a78051a9c4 --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Avalonia.PlatformSupport.Internal; + +internal class AssemblyDescriptorResolver +{ + private readonly Dictionary _assemblyNameCache = new(); + + public 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; + } +} 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.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs index 0060767565..30e8661e89 100644 --- a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs +++ b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs @@ -5,14 +5,27 @@ using System.Collections.Generic; namespace Avalonia.Controls.Metadata { + /// + /// Defines all pseudoclasses by name referenced and implemented by a control. + /// + /// + /// This is currently used for code-completion in certain IDEs. + /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class PseudoClassesAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// + /// The list of pseudoclass names. public PseudoClassesAttribute(params string[] pseudoClasses) { PseudoClasses = pseudoClasses; } + /// + /// Gets the list of pseudoclass names. + /// public IReadOnlyList PseudoClasses { get; } } } diff --git a/src/Avalonia.Styling/Controls/Metadata/TemplatePartAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/TemplatePartAttribute.cs new file mode 100644 index 0000000000..3b8f971713 --- /dev/null +++ b/src/Avalonia.Styling/Controls/Metadata/TemplatePartAttribute.cs @@ -0,0 +1,55 @@ +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; + +#nullable enable + +namespace Avalonia.Controls.Metadata +{ + /// + /// Defines a control template part referenced by name in code. + /// Template part names should begin with the "PART_" prefix. + /// + /// + /// Style authors should be able to identify the part type used for styling the specific control. + /// The part is usually required in the style and should have a specific predefined name. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class TemplatePartAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public TemplatePartAttribute() + { + Name = string.Empty; + Type = typeof(object); + } + + /// + /// Initializes a new instance of the class. + /// + /// The part name used by the class to identify a required element in the style. + /// The type of the element that should be used as a part with name. + public TemplatePartAttribute(string name, Type type) + { + Name = name; + Type = type; + } + + /// + /// Gets or sets the part name used by the class to identify a required element in the style. + /// Template part names should begin with the "PART_" prefix. + /// + public string Name { get; set; } + + /// + /// Gets or sets the type of the element that should be used as a part with name specified + /// in . + /// + public Type Type { get; set; } + } +} 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/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/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 009b56852a..846d45b839 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"> + + - @@ -22,7 +23,6 @@ - @@ -38,6 +38,7 @@ + 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/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index 857d70084c..7e9f5d9429 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -101,23 +101,17 @@ IsVisible="False" HorizontalAlignment="Right" /> - - - - - - + - @@ -195,8 +188,8 @@ - @@ -213,8 +206,8 @@ - @@ -226,8 +219,8 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml index a32e0b580c..c5bc7b6a38 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml @@ -5,6 +5,12 @@ + + + + + + Expanded content @@ -90,7 +96,7 @@ - @@ -168,7 +170,7 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml index a9e6569158..a2b50a859d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml @@ -220,7 +220,7 @@ + diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs index 0979de16d0..5e97635c9a 100644 --- a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs @@ -30,6 +30,7 @@ namespace Avalonia.Animation.Animators return new ImmutableRadialGradientBrush( InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), + oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, oldValue.SpreadMethod, s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center), s_relativePointAnimator.Interpolate(progress, oldRadial.GradientOrigin, newRadial.GradientOrigin), @@ -39,6 +40,7 @@ namespace Avalonia.Animation.Animators return new ImmutableConicGradientBrush( InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), + oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, oldValue.SpreadMethod, s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center), s_doubleAnimator.Interpolate(progress, oldConic.Angle, newConic.Angle)); @@ -47,6 +49,7 @@ namespace Avalonia.Animation.Animators return new ImmutableLinearGradientBrush( InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), + oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, oldValue.SpreadMethod, s_relativePointAnimator.Interpolate(progress, oldLinear.StartPoint, newLinear.StartPoint), s_relativePointAnimator.Interpolate(progress, oldLinear.EndPoint, newLinear.EndPoint)); @@ -98,16 +101,19 @@ namespace Avalonia.Animation.Animators case IRadialGradientBrush oldRadial: return new ImmutableRadialGradientBrush( CreateStopsFromSolidColorBrush(solidColorBrush, oldRadial.GradientStops), solidColorBrush.Opacity, + oldRadial.Transform is { } ? new ImmutableTransform(oldRadial.Transform.Value) : null, oldRadial.SpreadMethod, oldRadial.Center, oldRadial.GradientOrigin, oldRadial.Radius); case IConicGradientBrush oldConic: return new ImmutableConicGradientBrush( CreateStopsFromSolidColorBrush(solidColorBrush, oldConic.GradientStops), solidColorBrush.Opacity, + oldConic.Transform is { } ? new ImmutableTransform(oldConic.Transform.Value) : null, oldConic.SpreadMethod, oldConic.Center, oldConic.Angle); case ILinearGradientBrush oldLinear: return new ImmutableLinearGradientBrush( CreateStopsFromSolidColorBrush(solidColorBrush, oldLinear.GradientStops), solidColorBrush.Opacity, + oldLinear.Transform is { } ? new ImmutableTransform(oldLinear.Transform.Value) : null, oldLinear.SpreadMethod, oldLinear.StartPoint, oldLinear.EndPoint); default: diff --git a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs index 57f9f3c1a5..b87b2681d6 100644 --- a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs @@ -12,6 +12,8 @@ namespace Avalonia.Animation.Animators /// public class ISolidColorBrushAnimator : Animator { + private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator(); + public override ISolidColorBrush? Interpolate(double progress, ISolidColorBrush? oldValue, ISolidColorBrush? newValue) { if (oldValue is null || newValue is null) @@ -19,7 +21,9 @@ namespace Avalonia.Animation.Animators return progress >= 0.5 ? newValue : oldValue; } - return new ImmutableSolidColorBrush(ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color)); + return new ImmutableSolidColorBrush( + ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color), + s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity)); } public override IDisposable BindAnimation(Animatable control, IObservable instance) diff --git a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs index 2a6eae3e38..62119a0051 100644 --- a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs +++ b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs @@ -41,7 +41,7 @@ namespace Avalonia.Animation { var transitionTasks = PageTransitions .Select(transition => transition.Start(from, to, forward, cancellationToken)) - .ToList(); + .ToArray(); return Task.WhenAll(transitionTasks); } } diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs index b5a6062593..6f5d12d824 100644 --- a/src/Avalonia.Visuals/Animation/PageSlide.cs +++ b/src/Avalonia.Visuals/Animation/PageSlide.cs @@ -79,6 +79,7 @@ namespace Avalonia.Animation var animation = new Animation { Easing = SlideOutEasing, + FillMode = FillMode.Forward, Children = { new KeyFrame @@ -109,6 +110,7 @@ namespace Avalonia.Animation to.IsVisible = true; var animation = new Animation { + FillMode = FillMode.Forward, Easing = SlideInEasing, Children = { diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index 828ea1f184..b725993b44 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -6,6 +6,7 @@ MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract. MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.DrawingContext.DrawText(Avalonia.Media.IBrush, Avalonia.Point, Avalonia.Media.FormattedText)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Boolean Avalonia.Media.FontManager.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.FormattedText..ctor()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.FormattedText..ctor(Avalonia.Platform.IPlatformRenderInterface)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.FormattedText..ctor(System.String, Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextAlignment, Avalonia.Media.TextWrapping, Avalonia.Size)' does not exist in the implementation but it does exist in the contract. @@ -40,6 +41,8 @@ MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphIndices.set( MembersMustExist : Member 'public Avalonia.Utilities.ReadOnlySlice Avalonia.Media.GlyphRun.GlyphOffsets.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphOffsets.set(Avalonia.Utilities.ReadOnlySlice)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphTypeface.set(Avalonia.Media.GlyphTypeface)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.ITransform Avalonia.Media.IBrush.Transform' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.ITransform Avalonia.Media.IBrush.Transform.get()' is present in the implementation but not in the contract. CannotSealType : Type 'Avalonia.Media.Pen' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. MembersMustExist : Member 'protected void Avalonia.Media.Pen.AffectsRender(Avalonia.AvaloniaProperty[])' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Media.Pen.RaiseInvalidated(System.EventArgs)' does not exist in the implementation but it does exist in the contract. @@ -49,7 +52,21 @@ MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult..ctor()' MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.IsInside.set(System.Boolean)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.IsTrailing.set(System.Boolean)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.TextPosition.set(System.Int32)' does not exist in the implementation but it does exist in the contract. +CannotMakeTypeAbstract : Type 'Avalonia.Media.TextTrimming' is abstract in the implementation but is not abstract in the contract. +TypeCannotChangeClassification : Type 'Avalonia.Media.TextTrimming' is a 'class' in the implementation but is a 'struct' in the contract. +MembersMustExist : Member 'public Avalonia.Media.TextTrimming Avalonia.Media.TextTrimming Avalonia.Media.TextTrimming.CharacterEllipsis' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.TextTrimming Avalonia.Media.TextTrimming Avalonia.Media.TextTrimming.None' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.TextTrimming Avalonia.Media.TextTrimming Avalonia.Media.TextTrimming.WordEllipsis' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Int32 System.Int32 Avalonia.Media.TextTrimming.value__' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.Typeface..ctor(Avalonia.Media.FontFamily, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.Typeface..ctor(System.String, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableConicGradientBrush..ctor(System.Collections.Generic.IReadOnlyList, System.Double, Avalonia.Media.GradientSpreadMethod, System.Nullable, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Media.Immutable.ImmutableGradientBrush..ctor(System.Collections.Generic.IReadOnlyList, System.Double, Avalonia.Media.GradientSpreadMethod)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableLinearGradientBrush..ctor(System.Collections.Generic.IReadOnlyList, System.Double, Avalonia.Media.GradientSpreadMethod, System.Nullable, System.Nullable)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableRadialGradientBrush..ctor(System.Collections.Generic.IReadOnlyList, System.Double, Avalonia.Media.GradientSpreadMethod, System.Nullable, System.Nullable, System.Double)' does not exist in the implementation but it does exist in the contract. TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract. +MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableSolidColorBrush..ctor(Avalonia.Media.Color, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Media.Immutable.ImmutableTileBrush..ctor(Avalonia.Media.AlignmentX, Avalonia.Media.AlignmentY, Avalonia.RelativeRect, System.Double, Avalonia.RelativeRect, Avalonia.Media.Stretch, Avalonia.Media.TileMode, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. @@ -68,9 +85,13 @@ MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextC MembersMustExist : Member 'public Avalonia.Media.TextFormatting.ShapedTextCharacters.SplitTextCharactersResult Avalonia.Media.TextFormatting.ShapedTextCharacters.Split(System.Int32)' does not exist in the implementation but it does exist in the contract. TypesMustExist : Type 'Avalonia.Media.TextFormatting.ShapedTextCharacters.SplitTextCharactersResult' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected System.Boolean Avalonia.Media.TextFormatting.TextCharacters.TryGetRunProperties(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, Avalonia.Media.Typeface, System.Int32)' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public System.Collections.Generic.IReadOnlyList Avalonia.Media.TextFormatting.TextCollapsingProperties.Collapse(Avalonia.Media.TextFormatting.TextLine)' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextCollapsingStyle Avalonia.Media.TextFormatting.TextCollapsingProperties.Style.get()' does not exist in the implementation but it does exist in the contract. +TypesMustExist : Type 'Avalonia.Media.TextFormatting.TextCollapsingStyle' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextEndOfLine..ctor()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout..ctor(System.String, Avalonia.Media.Typeface, System.Double, Avalonia.Media.IBrush, Avalonia.Media.TextAlignment, Avalonia.Media.TextWrapping, Avalonia.Media.TextTrimming, Avalonia.Media.TextDecorationCollection, System.Double, System.Double, System.Double, System.Int32, System.Collections.Generic.IReadOnlyList>)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLayout.Size.get()' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed' is abstract in the implementation but is missing in the contract. @@ -112,13 +133,23 @@ CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextForma CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment.get()' is abstract in the implementation but is missing in the contract. MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Media.TextFormatting.TextShaper.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract. +CannotSealType : Type 'Avalonia.Media.TextFormatting.TextTrailingCharacterEllipsis' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextTrailingCharacterEllipsis..ctor(System.Double, Avalonia.Media.TextFormatting.TextRunProperties)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextCollapsingStyle Avalonia.Media.TextFormatting.TextTrailingCharacterEllipsis.Style.get()' does not exist in the implementation but it does exist in the contract. +CannotSealType : Type 'Avalonia.Media.TextFormatting.TextTrailingWordEllipsis' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextTrailingWordEllipsis..ctor(System.Double, Avalonia.Media.TextFormatting.TextRunProperties)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextCollapsingStyle Avalonia.Media.TextFormatting.TextTrailingWordEllipsis.Style.get()' does not exist in the implementation but it does exist in the contract. TypesMustExist : Type 'Avalonia.Media.TextFormatting.Unicode.BiDiClass' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.TextFormatting.Unicode.BiDiClass Avalonia.Media.TextFormatting.Unicode.Codepoint.BiDiClass.get()' does not exist in the implementation but it does exist in the contract. +TypesMustExist : Type 'Avalonia.Platform.ExportRenderingSubsystemAttribute' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawEllipse(Avalonia.Media.IBrush, Avalonia.Media.IPen, Avalonia.Rect)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawText(Avalonia.Media.IBrush, Avalonia.Point, Avalonia.Platform.IFormattedTextImpl)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.IDrawingContextImpl.DrawText(Avalonia.Media.IBrush, Avalonia.Point, Avalonia.Platform.IFormattedTextImpl)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PopBitmapBlendMode()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PushBitmapBlendMode(Avalonia.Visuals.Media.Imaging.BitmapBlendingMode)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontStretch, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' is present in the implementation but not in the contract. TypesMustExist : Type 'Avalonia.Platform.IFormattedTextImpl' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength.get()' is present in the implementation but not in the contract. @@ -145,4 +176,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphR MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice..ctor(System.ReadOnlyMemory, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract. -Total Issues: 146 +Total Issues: 177 diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index 6c978e970e..a0b1f99fa1 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -12,6 +12,9 @@ + + + diff --git a/src/Avalonia.Visuals/Media/Brush.cs b/src/Avalonia.Visuals/Media/Brush.cs index 11fbe9393a..9d989979a7 100644 --- a/src/Avalonia.Visuals/Media/Brush.cs +++ b/src/Avalonia.Visuals/Media/Brush.cs @@ -18,13 +18,19 @@ namespace Avalonia.Media public static readonly StyledProperty OpacityProperty = AvaloniaProperty.Register(nameof(Opacity), 1.0); + /// + /// Defines the property. + /// + public static readonly StyledProperty TransformProperty = + AvaloniaProperty.Register(nameof(Transform)); + /// public event EventHandler? Invalidated; static Brush() { Animation.Animation.RegisterAnimator(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType)); - AffectsRender(OpacityProperty); + AffectsRender(OpacityProperty, TransformProperty); } /// @@ -36,6 +42,15 @@ namespace Avalonia.Media set { SetValue(OpacityProperty, value); } } + /// + /// Gets or sets the transform of the brush. + /// + public ITransform? Transform + { + get { return GetValue(TransformProperty); } + set { SetValue(TransformProperty, value); } + } + /// /// Parses a brush string. /// diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 083c15cd13..aa730b3219 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -258,7 +258,7 @@ namespace Avalonia.Media public override string ToString() { uint rgb = ToUint32(); - return KnownColors.GetKnownColorName(rgb) ?? $"#{rgb:x8}"; + return KnownColors.GetKnownColorName(rgb) ?? $"#{rgb.ToString("x8", CultureInfo.InvariantCulture)}"; } /// @@ -273,8 +273,17 @@ namespace Avalonia.Media } /// - /// Check if two colors are equal. + /// Returns the HSV color model equivalent of this RGB color. /// + /// The HSV equivalent color. + public HsvColor ToHsv() + { + // Use the by-channel conversion method directly for performance + // Don't use the HsvColor(Color) constructor to avoid an extra HsvColor + return HsvColor.FromRgb(R, G, B, A); + } + + /// public bool Equals(Color other) { return A == other.A && R == other.R && G == other.G && B == other.B; diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index cdea5f5fa3..0e485d0db8 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -142,52 +142,6 @@ namespace Avalonia.Media Color IExperimentalAcrylicMaterial.TintColor => _effectiveTintColor; - private struct HsvColor - { - public float Hue { get; set; } - public float Saturation { get; set; } - public float Value { get; set; } - } - - private static HsvColor RgbToHsv(Color color) - { - var r = color.R / 255.0f; - var g = color.G / 255.0f; - var b = color.B / 255.0f; - var max = Math.Max(r, Math.Max(g, b)); - var min = Math.Min(r, Math.Min(g, b)); - - float h, s, v; - h = v = max; - - var d = max - min; - s = max == 0 ? 0 : d / max; - - if (max == min) - { - h = 0; // achromatic - } - else - { - if (max == r) - { - h = (g - b) / d + (g < b ? 6 : 0); - } - else if (max == g) - { - h = (b - r) / d + 2; - } - else if (max == b) - { - h = (r - g) / d + 4; - } - - h /= 6; - } - - return new HsvColor { Hue = h, Saturation = s, Value = v }; - } - private static Color GetEffectiveTintColor(Color tintColor, double tintOpacity) { // Update tintColor's alpha with the combined opacity value @@ -198,7 +152,7 @@ namespace Avalonia.Media private static double GetTintOpacityModifier(Color tintColor) { - // This method supresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by + // This method suppresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by // compressing the range of allowable values - for example, a user-defined value of 100% will be mapped to 45% for pure // white (100% luminosity), 85% for pure black (0% luminosity), and 90% for pure gray (50% luminosity). The intensity of // the effect increases linearly as luminosity deviates from 50%. After this effect is calculated, we cancel it out @@ -210,22 +164,22 @@ namespace Avalonia.Media const double midPointMaxOpacity = 0.45; // 50% luminosity const double blackMaxOpacity = 0.45; // 0% luminosity - var hsv = RgbToHsv(tintColor); + var hsv = tintColor.ToHsv(); double opacityModifier = midPointMaxOpacity; - if (hsv.Value != midPoint) + if (hsv.V != midPoint) { // Determine maximum suppression amount double lowestMaxOpacity = midPointMaxOpacity; double maxDeviation = midPoint; - if (hsv.Value > midPoint) + if (hsv.V > midPoint) { lowestMaxOpacity = whiteMaxOpacity; // At white (100% hsvV) maxDeviation = 1 - maxDeviation; } - else if (hsv.Value < midPoint) + else if (hsv.V < midPoint) { lowestMaxOpacity = blackMaxOpacity; // At black (0% hsvV) } @@ -233,14 +187,14 @@ namespace Avalonia.Media double maxOpacitySuppression = midPointMaxOpacity - lowestMaxOpacity; // Determine normalized deviation from the midpoint - double deviation = Math.Abs(hsv.Value - midPoint); + double deviation = Math.Abs(hsv.V - midPoint); double normalizedDeviation = deviation / maxDeviation; // If we have saturation, reduce opacity suppression to allow that color to come through more - if (hsv.Saturation > 0) + if (hsv.S > 0) { // Dampen opacity suppression based on how much saturation there is - maxOpacitySuppression *= Math.Max(1 - (hsv.Saturation * 2), 0.0); + maxOpacitySuppression *= Math.Max(1 - (hsv.S * 2), 0.0); } double opacitySuppression = maxOpacitySuppression * normalizedDeviation; diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index 72c1c8dcac..37091b82e3 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -118,26 +118,22 @@ namespace Avalonia.Media /// The codepoint to match against. /// The font style. /// The font weight. + /// The font stretch. /// The font family. This is optional and used for fallback lookup. /// The culture. /// The matching . /// /// True, if the could match the character to specified parameters, False otherwise. /// - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, + FontStretch fontStretch, FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface) { if(_fontFallbacks != null) { foreach (var fallback in _fontFallbacks) { - if(fallback is null) - { - continue; - } - - typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight); + typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); var glyphTypeface = typeface.GlyphTypeface; @@ -147,7 +143,7 @@ namespace Avalonia.Media } } - return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out typeface); + return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, fontFamily, culture, out typeface); } } } diff --git a/src/Avalonia.Visuals/Media/FontStretch.cs b/src/Avalonia.Visuals/Media/FontStretch.cs new file mode 100644 index 0000000000..b110475215 --- /dev/null +++ b/src/Avalonia.Visuals/Media/FontStretch.cs @@ -0,0 +1,19 @@ +namespace Avalonia.Media +{ + /// + /// FontStretch describes relative change from the normal aspect ratio + /// as specified by a font designer for the glyphs in a font. + /// + public enum FontStretch + { + Normal = 5, + UltraCondensed = 1, + ExtraCondensed = 2, + Condensed = 3, + SemiCondensed = 4, + SemiExpanded = 6, + Expanded = 7, + ExtraExpanded = 8, + UltraExpanded = 9 + } +} diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs index a54dad7623..365fb6e412 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media.Fonts { @@ -12,18 +13,10 @@ namespace Avalonia.Media.Fonts /// /// /// - public static IEnumerable LoadFontAssets(FontFamilyKey fontFamilyKey) - { - var sourceWithoutArguments = fontFamilyKey.Source.OriginalString.Split('?').First(); - - if (sourceWithoutArguments.EndsWith(".ttf") - || sourceWithoutArguments.EndsWith(".otf")) - { - return GetFontAssetsByExpression(fontFamilyKey); - } - - return GetFontAssetsBySource(fontFamilyKey); - } + public static IEnumerable LoadFontAssets(FontFamilyKey fontFamilyKey) => + IsFontTtfOrOtf(fontFamilyKey.Source) ? + GetFontAssetsByExpression(fontFamilyKey) : + GetFontAssetsBySource(fontFamilyKey); /// /// Searches for font assets at a given location and returns a quantity of found assets @@ -34,11 +27,7 @@ namespace Avalonia.Media.Fonts { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); - - var matchingAssets = - availableAssets.Where(x => x.AbsolutePath.EndsWith(".ttf") || x.AbsolutePath.EndsWith(".otf")); - - return matchingAssets; + return availableAssets.Where(x => IsFontTtfOrOtf(x)); } /// @@ -49,71 +38,124 @@ namespace Avalonia.Media.Fonts /// private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey) { + var (fileNameWithoutExtension, extension) = GetFileName(fontFamilyKey, out var location); + var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension); + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri); - var fileName = GetFileName(fontFamilyKey, out var fileExtension, out var location); + return availableResources.Where(x => IsContainsFile(x, filePattern, extension)); + } - var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri); + private static (string fileNameWithoutExtension, string extension) GetFileName( + FontFamilyKey fontFamilyKey, out Uri location) + { + if (fontFamilyKey.Source.IsAbsoluteResm()) + { + var fileName = GetFileNameAndExtension(fontFamilyKey.Source.GetUnescapeAbsolutePath(), '.'); + + var uriLocation = fontFamilyKey.Source.GetUnescapeAbsoluteUri() + .Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty); + location = new Uri(uriLocation, UriKind.RelativeOrAbsolute); + + return fileName; + } - string compareTo; + var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString); + var fullFilename = filename.fileNameWithoutExtension + filename.extension; - if (fontFamilyKey.Source.IsAbsoluteUri) + if (fontFamilyKey.BaseUri != null) { - if (fontFamilyKey.Source.Scheme == "resm") - { - compareTo = location.AbsolutePath + "." + fileName.Split('*').First(); - } - else - { - compareTo = location.AbsolutePath + fileName.Split('*').First(); - } + var relativePath = fontFamilyKey.Source.OriginalString + .Replace(fullFilename, string.Empty); + + location = new Uri(fontFamilyKey.BaseUri, relativePath); } else { - compareTo = location.AbsolutePath + fileName.Split('*').First(); + var uriString = fontFamilyKey.Source + .GetUnescapeAbsoluteUri() + .Replace(fullFilename, string.Empty); + location = new Uri(uriString); } - var matchingResources = availableResources.Where( - x => x.AbsolutePath.Contains(compareTo) - && x.AbsolutePath.EndsWith(fileExtension)); + return filename; + } - return matchingResources; + private static string CreateFilePattern( + FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension) + { + var path = location.GetUnescapeAbsolutePath(); + var file = GetSubString(fileNameWithoutExtension, '*'); + return fontFamilyKey.Source.IsAbsoluteResm() + ? path + "." + file + : path + file; } - private static string GetFileName(FontFamilyKey fontFamilyKey, out string fileExtension, out Uri location) + private static bool IsContainsFile(Uri x, string filePattern, string fileExtension) { - if (fontFamilyKey.Source.IsAbsoluteUri && fontFamilyKey.Source.Scheme == "resm") - { - fileExtension = "." + fontFamilyKey.Source.AbsolutePath.Split('.').LastOrDefault(); + var path = x.GetUnescapeAbsolutePath(); + return path.IndexOf(filePattern, StringComparison.Ordinal) >= 0 + && path.EndsWith(fileExtension, StringComparison.Ordinal); + } - var fileName = fontFamilyKey.Source.LocalPath.Replace(fileExtension, string.Empty).Split('.').Last(); + private static bool IsFontTtfOrOtf(Uri uri) + { + var sourceWithoutArguments = GetSubString(uri.OriginalString, '?'); + return sourceWithoutArguments.EndsWith(".ttf", StringComparison.Ordinal) + || sourceWithoutArguments.EndsWith(".otf", StringComparison.Ordinal); + } - location = new Uri(fontFamilyKey.Source.AbsoluteUri.Replace("." + fileName + fileExtension, string.Empty), UriKind.RelativeOrAbsolute); + private static (string fileNameWithoutExtension, string extension) GetFileNameAndExtension( + string path, char directorySeparator = '/') + { + var pathAsSpan = path.AsSpan(); + pathAsSpan = IsPathRooted(pathAsSpan, directorySeparator) ? + pathAsSpan.Slice(1, path.Length - 1) : + pathAsSpan; - return fileName; - } + var extension = GetFileExtension(pathAsSpan); + if (extension.Length == pathAsSpan.Length) + return (extension.ToString(), string.Empty); - var pathSegments = fontFamilyKey.Source.OriginalString.Split('/'); + var fileName = GetFileName(pathAsSpan, directorySeparator, extension.Length); + return (fileName.ToString(), extension.ToString()); + } - var fileNameWithExtension = pathSegments.Last(); + private static bool IsPathRooted(ReadOnlySpan path, char directorySeparator) => + path.Length > 0 && path[0] == directorySeparator; - var fileNameSegments = fileNameWithExtension.Split('.'); + private static ReadOnlySpan GetFileExtension(ReadOnlySpan path) + { + for (var i = path.Length - 1; i > 0; --i) + { + if (path[i] == '.') + return path.Slice(i, path.Length - i); + } - fileExtension = "." + fileNameSegments.Last(); + return path; + } - if (fontFamilyKey.BaseUri != null) + private static ReadOnlySpan GetFileName(ReadOnlySpan path, char directorySeparator, int extensionLength) + { + for (var i = path.Length - extensionLength - 1; i >= 0; --i) { - var relativePath = fontFamilyKey.Source.OriginalString - .Replace(fileNameWithExtension, string.Empty); - - location = new Uri(fontFamilyKey.BaseUri, relativePath); + if (path[i] == directorySeparator) + return path.Slice(i + 1, path.Length - i - extensionLength - 1); } - else + + return path.Slice(0, path.Length - extensionLength); + } + + private static string GetSubString(string path, char separator) + { + for (var i = 0; i < path.Length; i++) { - location = new Uri(fontFamilyKey.Source.AbsolutePath.Replace(fileNameWithExtension, string.Empty)); + if (path[i] == separator) + return path.Substring(0, i); } - return fileNameSegments.First(); + return path; } } } diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index 12c40e4d59..1cac3243e3 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/src/Avalonia.Visuals/Media/FormattedText.cs @@ -854,19 +854,9 @@ namespace Avalonia.Media var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!; - TextCollapsingProperties trailingEllipsis; + TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps)); - if (_that._trimming == TextTrimming.CharacterEllipsis) - { - trailingEllipsis = new TextTrailingCharacterEllipsis(maxLineLength, lastRunProps); - } - else - { - Debug.Assert(_that._trimming == TextTrimming.WordEllipsis); - trailingEllipsis = new TextTrailingWordEllipsis(maxLineLength, lastRunProps); - } - - var collapsedLine = line.Collapse(trailingEllipsis); + var collapsedLine = line.Collapse(collapsingProperties); line = collapsedLine; } @@ -1121,11 +1111,6 @@ namespace Avalonia.Media { set { - if ((int)value < 0 || (int)value > (int)TextTrimming.WordEllipsis) - { - throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(TextTrimming)); - } - _trimming = value; _defaultParaProps.SetTextWrapping(_trimming == TextTrimming.None ? diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index ef5ffb8d78..ec270d796a 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -391,8 +391,13 @@ namespace Avalonia.Media var nextCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); - return nextCharacterHit == characterHit ? - characterHit : + if (characterHit == nextCharacterHit) + { + return characterHit; + } + + return characterHit.TrailingLength > 0 ? + nextCharacterHit : new CharacterHit(nextCharacterHit.FirstCharacterIndex); } diff --git a/src/Avalonia.Visuals/Media/GradientStops.cs b/src/Avalonia.Visuals/Media/GradientStops.cs index efc11bacd6..c3d0bb5ceb 100644 --- a/src/Avalonia.Visuals/Media/GradientStops.cs +++ b/src/Avalonia.Visuals/Media/GradientStops.cs @@ -17,7 +17,7 @@ namespace Avalonia.Media public IReadOnlyList ToImmutable() { - return this.Select(x => new ImmutableGradientStop(x.Offset, x.Color)).ToList(); + return this.Select(x => new ImmutableGradientStop(x.Offset, x.Color)).ToArray(); } } } diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs new file mode 100644 index 0000000000..4a0277b4d4 --- /dev/null +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -0,0 +1,615 @@ +// Color conversion portions of this source file are adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Globalization; +using System.Text; +using Avalonia.Utilities; + +namespace Avalonia.Media +{ + /// + /// Defines a color using the hue/saturation/value (HSV) model. + /// +#if !BUILDTASK + public +#endif + readonly struct HsvColor : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The Alpha (transparency) channel value in the range from 0..1. + /// The Hue channel value in the range from 0..360. + /// Note that 360 is equivalent to 0 and will be adjusted automatically. + /// The Saturation channel value in the range from 0..1. + /// The Value channel value in the range from 0..1. + public HsvColor( + double alpha, + double hue, + double saturation, + double value) + { + A = MathUtilities.Clamp(alpha, 0.0, 1.0); + H = MathUtilities.Clamp(hue, 0.0, 360.0); + S = MathUtilities.Clamp(saturation, 0.0, 1.0); + V = MathUtilities.Clamp(value, 0.0, 1.0); + + // The maximum value of Hue is technically 360 minus epsilon (just below 360). + // This is because, in a color circle, 360 degrees is equivalent to 0 degrees. + // However, that is too tricky to work with in code and isn't as intuitive. + // Therefore, since 360 == 0, just wrap 360 if needed back to 0. + H = (H == 360.0 ? 0 : H); + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// This constructor exists only for internal use where performance is critical. + /// Whether or not the channel values are in the correct ranges must be known. + /// + /// The Alpha (transparency) channel value in the range from 0..1. + /// The Hue channel value in the range from 0..360. + /// Note that 360 is equivalent to 0 and will be adjusted automatically. + /// The Saturation channel value in the range from 0..1. + /// The Value channel value in the range from 0..1. + /// Whether to clamp channel values to their required ranges. + internal HsvColor( + double alpha, + double hue, + double saturation, + double value, + bool clampValues) + { + if (clampValues) + { + A = MathUtilities.Clamp(alpha, 0.0, 1.0); + H = MathUtilities.Clamp(hue, 0.0, 360.0); + S = MathUtilities.Clamp(saturation, 0.0, 1.0); + V = MathUtilities.Clamp(value, 0.0, 1.0); + + // See comments in constructor above + H = (H == 360.0 ? 0 : H); + } + else + { + A = alpha; + H = hue; + S = saturation; + V = value; + } + } + + /// + /// Initializes a new instance of the struct. + /// + /// The RGB color to convert to HSV. + public HsvColor(Color color) + { + var hsv = HsvColor.FromRgb(color); + + A = hsv.A; + H = hsv.H; + S = hsv.S; + V = hsv.V; + } + + /// + /// Gets the Alpha (transparency) channel value in the range from 0..1. + /// + public double A { get; } + + /// + /// Gets the Hue channel value in the range from 0..360. + /// Note that 360 is equivalent to 0 and will be adjusted automatically. + /// + public double H { get; } + + /// + /// Gets the Saturation channel value in the range from 0..1. + /// + public double S { get; } + + /// + /// Gets the Value channel value in the range from 0..1. + /// + public double V { get; } + + /// + public bool Equals(HsvColor other) + { + return other.A == A && + other.H == H && + other.S == S && + other.V == V; + } + + /// + public override bool Equals(object? obj) + { + if (obj is HsvColor hsvColor) + { + return Equals(hsvColor); + } + else + { + return false; + } + } + + /// + /// Gets a hashcode for this object. + /// Hashcode is not guaranteed to be unique. + /// + /// The hashcode for this object. + public override int GetHashCode() + { + // Same algorithm as Color + // This is used instead of HashCode.Combine() due to .NET Standard 2.0 requirements + unchecked + { + int hashCode = A.GetHashCode(); + hashCode = (hashCode * 397) ^ H.GetHashCode(); + hashCode = (hashCode * 397) ^ S.GetHashCode(); + hashCode = (hashCode * 397) ^ V.GetHashCode(); + return hashCode; + } + } + + /// + /// Returns the RGB color model equivalent of this HSV color. + /// + /// The RGB equivalent color. + public Color ToRgb() + { + // Use the by-channel conversion method directly for performance + return HsvColor.ToRgb(H, S, V, A); + } + + /// + public override string ToString() + { + var sb = new StringBuilder(); + + // Use a format similar to HSL in HTML/CSS "hsla(0, 100%, 50%, 0.5)" + // + // However: + // - To ensure precision is never lost, allow decimal places + // - To maintain numerical consistency do not use percent + // + // Example: + // + // hsva(hue, saturation, value, alpha) + // hsva(230, 1.0, 0.5, 1.0) + // + // Where: + // + // hue : double from 0 to 360 + // saturation : double from 0 to 1 + // (HTML uses a percentage) + // value : double from 0 to 1 + // (HTML uses a percentage) + // alpha : double from 0 to 1 + // (HTML does not use a percentage for alpha) + + sb.Append("hsva("); + sb.Append(H.ToString(CultureInfo.InvariantCulture)); + sb.Append(", "); + sb.Append(S.ToString(CultureInfo.InvariantCulture)); + sb.Append(", "); + sb.Append(V.ToString(CultureInfo.InvariantCulture)); + sb.Append(", "); + sb.Append(A.ToString(CultureInfo.InvariantCulture)); + sb.Append(')'); + + return sb.ToString(); + } + + /// + /// Parses an HSV color string. + /// + /// The HSV color string to parse. + /// The parsed . + public static HsvColor Parse(string s) + { + if (s is null) + { + throw new ArgumentNullException(nameof(s)); + } + + if (TryParse(s, out HsvColor hsvColor)) + { + return hsvColor; + } + + throw new FormatException($"Invalid HSV color string: '{s}'."); + } + + /// + /// Parses an HSV color string. + /// + /// The HSV color string to parse. + /// The parsed . + /// True if parsing was successful; otherwise, false. + public static bool TryParse(string s, out HsvColor hsvColor) + { + hsvColor = default; + + if (s is null) + { + return false; + } + + string workingString = s.Trim(); + + if (workingString.Length == 0 || + workingString.IndexOf(",", StringComparison.Ordinal) < 0) + { + return false; + } + + if (workingString.Length > 6 && + workingString.StartsWith("hsva(", StringComparison.OrdinalIgnoreCase) && + workingString.EndsWith(")", StringComparison.Ordinal)) + { + workingString = workingString.Substring(5, workingString.Length - 6); + } + + if (workingString.Length > 5 && + workingString.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) && + workingString.EndsWith(")", StringComparison.Ordinal)) + { + workingString = workingString.Substring(4, workingString.Length - 5); + } + + string[] components = workingString.Split(','); + + if (components.Length == 3) // HSV + { + if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && + double.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double saturation) && + double.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out double value)) + { + hsvColor = new HsvColor(1.0, hue, saturation, value); + return true; + } + } + else if (components.Length == 4) // HSVA + { + if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && + double.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double saturation) && + double.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out double value) && + double.TryParse(components[3], NumberStyles.Number, CultureInfo.InvariantCulture, out double alpha)) + { + hsvColor = new HsvColor(alpha, hue, saturation, value); + return true; + } + } + + return false; + } + + /// + /// Creates a new from individual color channel values. + /// + /// + /// This exists for symmetry with the struct; however, the + /// appropriate constructor should commonly be used instead. + /// + /// The Alpha (transparency) channel value in the range from 0..1. + /// The Hue channel value in the range from 0..360. + /// The Saturation channel value in the range from 0..1. + /// The Value channel value in the range from 0..1. + /// A new built from the individual color channel values. + public static HsvColor FromAhsv(double a, double h, double s, double v) + { + return new HsvColor(a, h, s, v); + } + + /// + /// Converts the given HSV color to it's RGB color equivalent. + /// + /// The color in the HSV color model. + /// A new RGB equivalent to the given HSVA values. + public static Color ToRgb(HsvColor hsvColor) + { + return HsvColor.ToRgb(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A); + } + + /// + /// Converts the given HSVA color channel values to it's RGB color equivalent. + /// + /// The hue channel value in the HSV color model in the range from 0..360. + /// The saturation channel value in the HSV color model in the range from 0..1. + /// The value channel value in the HSV color model in the range from 0..1. + /// The alpha channel value in the range from 0..1. + /// A new RGB equivalent to the given HSVA values. + public static Color ToRgb( + double hue, + double saturation, + double value, + double alpha = 1.0) + { + // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT) + // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp + // This was used because it is the best documented and likely most optimized for performance + // Alpha channel support was added + + // We want the hue to be between 0 and 359, + // so we first ensure that that's the case. + while (hue >= 360.0) + { + hue -= 360.0; + } + + while (hue < 0.0) + { + hue += 360.0; + } + + // We similarly clamp saturation, value and alpha between 0 and 1. + saturation = saturation < 0.0 ? 0.0 : saturation; + saturation = saturation > 1.0 ? 1.0 : saturation; + + value = value < 0.0 ? 0.0 : value; + value = value > 1.0 ? 1.0 : value; + + alpha = alpha < 0.0 ? 0.0 : alpha; + alpha = alpha > 1.0 ? 1.0 : alpha; + + // The first thing that we need to do is to determine the chroma (see above for its definition). + // Remember from above that: + // + // 1. The chroma is the difference between the maximum and the minimum of the RGB channels, + // 2. The value is the maximum of the RGB channels, and + // 3. The saturation comes from dividing the chroma by the maximum of the RGB channels (i.e., the value). + // + // From these facts, you can see that we can retrieve the chroma by simply multiplying the saturation and the value, + // and we can retrieve the minimum of the RGB channels by subtracting the chroma from the value. + var chroma = saturation * value; + var min = value - chroma; + + // If the chroma is zero, then we have a greyscale color. In that case, the maximum and the minimum RGB channels + // have the same value (and, indeed, all of the RGB channels are the same), so we can just immediately return + // the minimum value as the value of all the channels. + if (chroma == 0) + { + return Color.FromArgb( + (byte)Math.Round(alpha * 255), + (byte)Math.Round(min * 255), + (byte)Math.Round(min * 255), + (byte)Math.Round(min * 255)); + } + + // If the chroma is not zero, then we need to continue. The first step is to figure out + // what section of the color wheel we're located in. In order to do that, we'll divide the hue by 60. + // The resulting value means we're in one of the following locations: + // + // 0 - Between red and yellow. + // 1 - Between yellow and green. + // 2 - Between green and cyan. + // 3 - Between cyan and blue. + // 4 - Between blue and purple. + // 5 - Between purple and red. + // + // In each of these sextants, one of the RGB channels is completely present, one is partially present, and one is not present. + // For example, as we transition between red and yellow, red is completely present, green is becoming increasingly present, and blue is not present. + // Then, as we transition from yellow and green, green is now completely present, red is becoming decreasingly present, and blue is still not present. + // As we transition from green to cyan, green is still completely present, blue is becoming increasingly present, and red is no longer present. And so on. + // + // To convert from hue to RGB value, we first need to figure out which of the three channels is in which configuration + // in the sextant that we're located in. Next, we figure out what value the completely-present color should have. + // We know that chroma = (max - min), and we know that this color is the max color, so to find its value we simply add + // min to chroma to retrieve max. Finally, we consider how far we've transitioned from the pure form of that color + // to the next color (e.g., how far we are from pure red towards yellow), and give a value to the partially present channel + // equal to the minimum plus the chroma (i.e., the max minus the min), multiplied by the percentage towards the new color. + // This gets us a value between the maximum and the minimum representing the partially present channel. + // Finally, the not-present color must be equal to the minimum value, since it is the one least participating in the overall color. + int sextant = (int)(hue / 60); + double intermediateColorPercentage = (hue / 60) - sextant; + double max = chroma + min; + + double r = 0; + double g = 0; + double b = 0; + + switch (sextant) + { + case 0: + r = max; + g = min + (chroma * intermediateColorPercentage); + b = min; + break; + case 1: + r = min + (chroma * (1 - intermediateColorPercentage)); + g = max; + b = min; + break; + case 2: + r = min; + g = max; + b = min + (chroma * intermediateColorPercentage); + break; + case 3: + r = min; + g = min + (chroma * (1 - intermediateColorPercentage)); + b = max; + break; + case 4: + r = min + (chroma * intermediateColorPercentage); + g = min; + b = max; + break; + case 5: + r = max; + g = min; + b = min + (chroma * (1 - intermediateColorPercentage)); + break; + } + + return Color.FromArgb( + (byte)Math.Round(alpha * 255), + (byte)Math.Round(r * 255), + (byte)Math.Round(g * 255), + (byte)Math.Round(b * 255)); + } + + /// + /// Converts the given RGB color to it's HSV color equivalent. + /// + /// The color in the RGB color model. + /// A new equivalent to the given RGBA values. + public static HsvColor FromRgb(Color color) + { + return HsvColor.FromRgb(color.R, color.G, color.B, color.A); + } + + /// + /// Converts the given RGBA color channel values to it's HSV color equivalent. + /// + /// The red channel value in the RGB color model. + /// The green channel value in the RGB color model. + /// The blue channel value in the RGB color model. + /// The alpha channel value. + /// A new equivalent to the given RGBA values. + public static HsvColor FromRgb( + byte red, + byte green, + byte blue, + byte alpha = 0xFF) + { + // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT) + // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp + // This was used because it is the best documented and likely most optimized for performance + // Alpha channel support was added + + // Normalize RGBA channel values into the 0..1 range used by this algorithm + double r = red / 255.0; + double g = green / 255.0; + double b = blue / 255.0; + double a = alpha / 255.0; + + double hue; + double saturation; + double value; + + double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b); + double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b); + + // The value, a number between 0 and 1, is the largest of R, G, and B (divided by 255). + // Conceptually speaking, it represents how much color is present. + // If at least one of R, G, B is 255, then there exists as much color as there can be. + // If RGB = (0, 0, 0), then there exists no color at all - a value of zero corresponds + // to black (i.e., the absence of any color). + value = max; + + // The "chroma" of the color is a value directly proportional to the extent to which + // the color diverges from greyscale. If, for example, we have RGB = (255, 255, 0), + // then the chroma is maximized - this is a pure yellow, no gray of any kind. + // On the other hand, if we have RGB = (128, 128, 128), then the chroma being zero + // implies that this color is pure greyscale, with no actual hue to be found. + var chroma = max - min; + + // If the chrome is zero, then hue is technically undefined - a greyscale color + // has no hue. For the sake of convenience, we'll just set hue to zero, since + // it will be unused in this circumstance. Since the color is purely gray, + // saturation is also equal to zero - you can think of saturation as basically + // a measure of hue intensity, such that no hue at all corresponds to a + // nonexistent intensity. + if (chroma == 0) + { + hue = 0.0; + saturation = 0.0; + } + else + { + // In this block, hue is properly defined, so we'll extract both hue + // and saturation information from the RGB color. + + // Hue can be thought of as a cyclical thing, between 0 degrees and 360 degrees. + // A hue of 0 degrees is red; 120 degrees is green; 240 degrees is blue; and 360 is back to red. + // Every other hue is somewhere between either red and green, green and blue, and blue and red, + // so every other hue can be thought of as an angle on this color wheel. + // These if/else statements determines where on this color wheel our color lies. + if (r == max) + { + // If the red channel is the most pronounced channel, then we exist + // somewhere between (-60, 60) on the color wheel - i.e., the section around 0 degrees + // where red dominates. We figure out where in that section we are exactly + // by considering whether the green or the blue channel is greater - by subtracting green from blue, + // then if green is greater, we'll nudge ourselves closer to 60, whereas if blue is greater, then + // we'll nudge ourselves closer to -60. We then divide by chroma (which will actually make the result larger, + // since chroma is a value between 0 and 1) to normalize the value to ensure that we get the right hue + // even if we're very close to greyscale. + hue = 60 * (g - b) / chroma; + } + else if (g == max) + { + // We do the exact same for the case where the green channel is the most pronounced channel, + // only this time we want to see if we should tilt towards the blue direction or the red direction. + // We add 120 to center our value in the green third of the color wheel. + hue = 120 + (60 * (b - r) / chroma); + } + else // blue == max + { + // And we also do the exact same for the case where the blue channel is the most pronounced channel, + // only this time we want to see if we should tilt towards the red direction or the green direction. + // We add 240 to center our value in the blue third of the color wheel. + hue = 240 + (60 * (r - g) / chroma); + } + + // Since we want to work within the range [0, 360), we'll add 360 to any value less than zero - + // this will bump red values from within -60 to -1 to 300 to 359. The hue is the same at both values. + if (hue < 0.0) + { + hue += 360.0; + } + + // The saturation, our final HSV axis, can be thought of as a value between 0 and 1 indicating how intense our color is. + // To find it, we divide the chroma - the distance between the minimum and the maximum RGB channels - by the maximum channel (i.e., the value). + // This effectively normalizes the chroma - if the maximum is 0.5 and the minimum is 0, the saturation will be (0.5 - 0) / 0.5 = 1, + // meaning that although this color is not as bright as it can be, the dark color is as intense as it possibly could be. + // If, on the other hand, the maximum is 0.5 and the minimum is 0.25, then the saturation will be (0.5 - 0.25) / 0.5 = 0.5, + // meaning that this color is partially washed out. + // A saturation value of 0 corresponds to a greyscale color, one in which the color is *completely* washed out and there is no actual hue. + saturation = chroma / value; + } + + return new HsvColor(a, hue, saturation, value, false); + } + + /// + /// Indicates whether the values of two specified objects are equal. + /// + /// The first object to compare. + /// The second object to compare. + /// True if left and right are equal; otherwise, false. + public static bool operator ==(HsvColor left, HsvColor right) + { + return left.Equals(right); + } + + /// + /// Indicates whether the values of two specified objects are not equal. + /// + /// The first object to compare. + /// The second object to compare. + /// True if left and right are not equal; otherwise, false. + public static bool operator !=(HsvColor left, HsvColor right) + { + return !(left == right); + } + + /// + /// Explicit conversion from an to a . + /// + /// The to convert. + public static explicit operator Color(HsvColor hsvColor) + { + return hsvColor.ToRgb(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/IBrush.cs b/src/Avalonia.Visuals/Media/IBrush.cs index 15b7681be4..830c066182 100644 --- a/src/Avalonia.Visuals/Media/IBrush.cs +++ b/src/Avalonia.Visuals/Media/IBrush.cs @@ -12,5 +12,10 @@ namespace Avalonia.Media /// Gets the opacity of the brush. /// double Opacity { get; } + + /// + /// Gets the transform of the brush. + /// + ITransform? Transform { get; } } } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs index d3c80dfcad..4b97615c4c 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs @@ -12,16 +12,18 @@ namespace Avalonia.Media.Immutable /// /// The gradient stops. /// The opacity of the brush. + /// The transform of the brush. /// The spread method. /// The center point for the gradient. /// The starting angle for the gradient. public ImmutableConicGradientBrush( IReadOnlyList gradientStops, double opacity = 1, + ImmutableTransform? transform = null, GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad, RelativePoint? center = null, double angle = 0) - : base(gradientStops, opacity, spreadMethod) + : base(gradientStops, opacity, transform, spreadMethod) { Center = center ?? RelativePoint.Center; Angle = angle; diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs index 1f6e3bbcfd..f1e51687d0 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs @@ -12,14 +12,17 @@ namespace Avalonia.Media.Immutable /// /// The gradient stops. /// The opacity of the brush. + /// The transform of the brush. /// The spread method. protected ImmutableGradientBrush( IReadOnlyList gradientStops, double opacity, + ImmutableTransform? transform, GradientSpreadMethod spreadMethod) { GradientStops = gradientStops; Opacity = opacity; + Transform = transform; SpreadMethod = spreadMethod; } @@ -28,7 +31,7 @@ namespace Avalonia.Media.Immutable /// /// The brush from which this brush's properties should be copied. protected ImmutableGradientBrush(GradientBrush source) - : this(source.GradientStops.ToImmutable(), source.Opacity, source.SpreadMethod) + : this(source.GradientStops.ToImmutable(), source.Opacity, source.Transform?.ToImmutable(), source.SpreadMethod) { } @@ -39,6 +42,11 @@ namespace Avalonia.Media.Immutable /// public double Opacity { get; } + /// + /// Gets the transform of the brush. + /// + public ITransform? Transform { get; } + /// public GradientSpreadMethod SpreadMethod { get; } } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs index 15da8f8b43..2d4fe45c8e 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs @@ -16,6 +16,7 @@ namespace Avalonia.Media.Immutable /// The vertical alignment of a tile in the destination. /// The rectangle on the destination in which to paint a tile. /// The opacity of the brush. + /// The transform of the brush. /// The rectangle of the source image that will be displayed. /// /// How the source rectangle will be stretched to fill the destination rect. @@ -28,6 +29,7 @@ namespace Avalonia.Media.Immutable AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, double opacity = 1, + ImmutableTransform? transform = null, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, TileMode tileMode = TileMode.None, @@ -37,6 +39,7 @@ namespace Avalonia.Media.Immutable alignmentY, destinationRect ?? RelativeRect.Fill, opacity, + transform, sourceRect ?? RelativeRect.Fill, stretch, tileMode, diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs index 912d77d763..64c0f9b44e 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs @@ -12,16 +12,18 @@ namespace Avalonia.Media.Immutable /// /// The gradient stops. /// The opacity of the brush. + /// The transform of the brush. /// The spread method. /// The start point for the gradient. /// The end point for the gradient. public ImmutableLinearGradientBrush( IReadOnlyList gradientStops, double opacity = 1, + ImmutableTransform? transform = null, GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad, RelativePoint? startPoint = null, RelativePoint? endPoint = null) - : base(gradientStops, opacity, spreadMethod) + : base(gradientStops, opacity, transform, spreadMethod) { StartPoint = startPoint ?? RelativePoint.TopLeft; EndPoint = endPoint ?? RelativePoint.BottomRight; diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs index e26fbab5f5..3da4bdd8e9 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs @@ -12,6 +12,7 @@ namespace Avalonia.Media.Immutable /// /// The gradient stops. /// The opacity of the brush. + /// The transform of the brush. /// The spread method. /// The start point for the gradient. /// @@ -23,11 +24,12 @@ namespace Avalonia.Media.Immutable public ImmutableRadialGradientBrush( IReadOnlyList gradientStops, double opacity = 1, + ImmutableTransform? transform = null, GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad, RelativePoint? center = null, RelativePoint? gradientOrigin = null, double radius = 0.5) - : base(gradientStops, opacity, spreadMethod) + : base(gradientStops, opacity, transform, spreadMethod) { Center = center ?? RelativePoint.Center; GradientOrigin = gradientOrigin ?? RelativePoint.Center; diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs index 46e1df4054..9b1b2500ef 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs @@ -12,10 +12,12 @@ namespace Avalonia.Media.Immutable /// /// The color to use. /// The opacity of the brush. - public ImmutableSolidColorBrush(Color color, double opacity = 1) + /// The transform of the brush. + public ImmutableSolidColorBrush(Color color, double opacity = 1, ImmutableTransform? transform = null) { Color = color; Opacity = opacity; + Transform = null; } /// @@ -32,7 +34,7 @@ namespace Avalonia.Media.Immutable /// /// The brush from which this brush's properties should be copied. public ImmutableSolidColorBrush(ISolidColorBrush source) - : this(source.Color, source.Opacity) + : this(source.Color, source.Opacity, source.Transform?.ToImmutable()) { } @@ -46,11 +48,16 @@ namespace Avalonia.Media.Immutable /// public double Opacity { get; } + /// + /// Gets the transform of the brush. + /// + public ITransform? Transform { get; } + public bool Equals(ImmutableSolidColorBrush? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Color.Equals(other.Color) && Opacity.Equals(other.Opacity); + return Color.Equals(other.Color) && Opacity.Equals(other.Opacity) && (Transform == null && other.Transform == null ? true : (Transform != null && Transform.Equals(other.Transform))); } public override bool Equals(object? obj) @@ -62,7 +69,7 @@ namespace Avalonia.Media.Immutable { unchecked { - return (Color.GetHashCode() * 397) ^ Opacity.GetHashCode(); + return (Color.GetHashCode() * 397) ^ Opacity.GetHashCode() ^ (Transform is null ? 0 : Transform.GetHashCode()); } } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs index fd4d921516..1019751733 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs @@ -14,6 +14,7 @@ namespace Avalonia.Media.Immutable /// The vertical alignment of a tile in the destination. /// The rectangle on the destination in which to paint a tile. /// The opacity of the brush. + /// The transform of the brush. /// The rectangle of the source image that will be displayed. /// /// How the source rectangle will be stretched to fill the destination rect. @@ -25,6 +26,7 @@ namespace Avalonia.Media.Immutable AlignmentY alignmentY, RelativeRect destinationRect, double opacity, + ImmutableTransform? transform, RelativeRect sourceRect, Stretch stretch, TileMode tileMode, @@ -34,6 +36,7 @@ namespace Avalonia.Media.Immutable AlignmentY = alignmentY; DestinationRect = destinationRect; Opacity = opacity; + Transform = transform; SourceRect = sourceRect; Stretch = stretch; TileMode = tileMode; @@ -50,6 +53,7 @@ namespace Avalonia.Media.Immutable source.AlignmentY, source.DestinationRect, source.Opacity, + source.Transform?.ToImmutable(), source.SourceRect, source.Stretch, source.TileMode, @@ -69,6 +73,11 @@ namespace Avalonia.Media.Immutable /// public double Opacity { get; } + /// + /// Gets the transform of the brush. + /// + public ITransform? Transform { get; } + /// public RelativeRect SourceRect { get; } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableTransform.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableTransform.cs new file mode 100644 index 0000000000..d5ff2b8317 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableTransform.cs @@ -0,0 +1,21 @@ +using Avalonia.VisualTree; + +namespace Avalonia.Media.Immutable +{ + /// + /// Represents a transform on an . + /// + public class ImmutableTransform : ITransform + { + public Matrix Value { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The transform matrix. + public ImmutableTransform(Matrix matrix) + { + Value = matrix; + } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs index 4c6aae08ab..0fd2905660 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs @@ -16,6 +16,7 @@ namespace Avalonia.Media.Immutable /// The vertical alignment of a tile in the destination. /// The rectangle on the destination in which to paint a tile. /// The opacity of the brush. + /// The transform of the brush. /// The rectangle of the source image that will be displayed. /// /// How the source rectangle will be stretched to fill the destination rect. @@ -28,6 +29,7 @@ namespace Avalonia.Media.Immutable AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, double opacity = 1, + ImmutableTransform? transform = null, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, TileMode tileMode = TileMode.None, @@ -37,6 +39,7 @@ namespace Avalonia.Media.Immutable alignmentY, destinationRect ?? RelativeRect.Fill, opacity, + transform, sourceRect ?? RelativeRect.Fill, stretch, tileMode, diff --git a/src/Avalonia.Visuals/Media/PixelRect.cs b/src/Avalonia.Visuals/Media/PixelRect.cs index 0059a4b483..855ba104ad 100644 --- a/src/Avalonia.Visuals/Media/PixelRect.cs +++ b/src/Avalonia.Visuals/Media/PixelRect.cs @@ -168,6 +168,18 @@ namespace Avalonia { return p.X >= X && p.X <= Right && p.Y >= Y && p.Y <= Bottom; } + + /// + /// Determines whether a point is in the bounds of the rectangle, exclusive of the + /// rectangle's bottom/right edge. + /// + /// The point. + /// true if the point is in the bounds of the rectangle; otherwise false. + public bool ContainsExclusive(PixelPoint p) + { + return p.X >= X && p.X < X + Width && + p.Y >= Y && p.Y < Y + Height; + } /// /// Determines whether the rectangle fully contains another rectangle. diff --git a/src/Avalonia.Visuals/Media/TextCollapsingCreateInfo.cs b/src/Avalonia.Visuals/Media/TextCollapsingCreateInfo.cs new file mode 100644 index 0000000000..78f15b724a --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextCollapsingCreateInfo.cs @@ -0,0 +1,16 @@ +using Avalonia.Media.TextFormatting; + +namespace Avalonia.Media +{ + public readonly struct TextCollapsingCreateInfo + { + public readonly double Width; + public readonly TextRunProperties TextRunProperties; + + public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties) + { + Width = width; + TextRunProperties = textRunProperties; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs index 98344141f1..1b0feaa718 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs @@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length); - if (textRange.Start + textRange.Length < text.Start) + if (textRange.Start + textRange.Length <= text.Start) { continue; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs index 9d648af8fb..b31a6f4d13 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs @@ -22,5 +22,41 @@ namespace Avalonia.Media.TextFormatting public override TextRunProperties Properties { get; } public sbyte BidiLevel { get; } + + public bool CanShapeTogether(ShapeableTextCharacters shapeableTextCharacters) + { + if (!Text.Buffer.Equals(shapeableTextCharacters.Text.Buffer)) + { + return false; + } + + if (Text.Start + Text.Length != shapeableTextCharacters.Text.Start) + { + return false; + } + + if (BidiLevel != shapeableTextCharacters.BidiLevel) + { + return false; + } + + if (!MathUtilities.AreClose(Properties.FontRenderingEmSize, + shapeableTextCharacters.Properties.FontRenderingEmSize)) + { + return false; + } + + if (Properties.Typeface != shapeableTextCharacters.Properties.Typeface) + { + return false; + } + + if (Properties.BaselineAlignment != shapeableTextCharacters.Properties.BaselineAlignment) + { + return false; + } + + return true; + } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs index ee38cf39e0..47a6334e39 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs @@ -105,17 +105,12 @@ namespace Avalonia.Media.TextFormatting /// The split result. internal SplitResult Split(int length) { - var glyphCount = FindGlyphIndex(Text.Start + length); - if (Text.Length == length) { return new SplitResult(this, null); } - if (Text.Length == glyphCount) - { - return new SplitResult(this, null); - } + var glyphCount = FindGlyphIndex(Text.Start + length); var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index 96b3857098..fb85766003 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -131,6 +132,29 @@ namespace Avalonia.Media.TextFormatting return length > 0; } + internal bool TryMeasureCharactersBackwards(double availableWidth, out int length, out double width) + { + length = 0; + width = 0; + + for (var i = ShapedBuffer.Length - 1; i >= 0; i--) + { + var advance = ShapedBuffer.GlyphAdvances[i]; + + if (width + advance > availableWidth) + { + break; + } + + Codepoint.ReadAt(GlyphRun.Characters, length, out var count); + + length += count; + width += advance; + } + + return length > 0; + } + internal SplitResult Split(int length) { if (IsReversed) @@ -138,16 +162,13 @@ namespace Avalonia.Media.TextFormatting Reverse(); } +#if DEBUG if(length == 0) { throw new ArgumentOutOfRangeException(nameof(length), "length must be greater than zero."); } - - if(length == ShapedBuffer.Length) - { - return new SplitResult(this, null); - } - +#endif + var splitBuffer = ShapedBuffer.Split(length); var first = new ShapedTextCharacters(splitBuffer.First, Properties); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index faa73719c8..c4b2dfb3a5 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -79,14 +79,12 @@ namespace Avalonia.Media.TextFormatting if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _)) { return new ShapeableTextCharacters(text.Take(fallbackCount), - new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize, - defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel); + defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); } } - return new ShapeableTextCharacters(text.Take(count), - new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, - defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel); + return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface), + biDiLevel); } if (previousTypeface is not null) @@ -94,8 +92,7 @@ namespace Avalonia.Media.TextFormatting if(TryGetShapeableLength(text, previousTypeface.Value, out count, out _)) { return new ShapeableTextCharacters(text.Take(count), - new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize, - defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel); + defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); } } @@ -118,14 +115,14 @@ namespace Avalonia.Media.TextFormatting //ToDo: Fix FontFamily fallback var matchFound = FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, - defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); + defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, + out currentTypeface); if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _)) { //Fallback found - return new ShapeableTextCharacters(text.Take(count), - new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, - defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel); + return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface), + biDiLevel); } // no fallback found @@ -147,9 +144,7 @@ namespace Avalonia.Media.TextFormatting count += grapheme.Text.Length; } - return new ShapeableTextCharacters(text.Take(count), - new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, - defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel); + return new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel); } /// @@ -181,6 +176,12 @@ namespace Avalonia.Media.TextFormatting var currentScript = currentGrapheme.FirstCodepoint.Script; + //Stop at the first missing glyph + if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + { + break; + } + if (currentScript != script) { if (script is Script.Unknown || currentScript != Script.Common && @@ -197,12 +198,6 @@ namespace Avalonia.Media.TextFormatting } } - //Stop at the first missing glyph - if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) - { - break; - } - length += currentGrapheme.Text.Length; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs index ffd65423a3..a46f9537d0 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs @@ -1,23 +1,26 @@ -namespace Avalonia.Media.TextFormatting +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting { /// - /// Properties of text collapsing + /// Properties of text collapsing. /// public abstract class TextCollapsingProperties { /// - /// Gets the width in which the collapsible range is constrained to + /// Gets the width in which the collapsible range is constrained to. /// public abstract double Width { get; } /// - /// Gets the text run that is used as collapsing symbol + /// Gets the text run that is used as collapsing symbol. /// public abstract TextRun Symbol { get; } /// - /// Gets the style of collapsing + /// Collapses given text line. /// - public abstract TextCollapsingStyle Style { get; } + /// Text line to collapse. + public abstract IReadOnlyList? Collapse(TextLine textLine); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs deleted file mode 100644 index 1523cc4d9a..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Avalonia.Media.TextFormatting -{ - /// - /// Text collapsing style - /// - public enum TextCollapsingStyle - { - /// - /// Collapse trailing characters - /// - TrailingCharacter, - - /// - /// Collapse trailing words - /// - TrailingWord, - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs new file mode 100644 index 0000000000..2031c2ec99 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using Avalonia.Media.TextFormatting.Unicode; + +namespace Avalonia.Media.TextFormatting +{ + internal class TextEllipsisHelper + { + public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) + { + var shapedTextRuns = textLine.TextRuns as List; + + if (shapedTextRuns is null) + { + return null; + } + + var runIndex = 0; + var currentWidth = 0.0; + var collapsedLength = 0; + var textRange = textLine.TextRange; + var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight); + + if (properties.Width < shapedSymbol.GlyphRun.Size.Width) + { + return new List(0); + } + + var availableWidth = properties.Width - shapedSymbol.Size.Width; + + while (runIndex < shapedTextRuns.Count) + { + var currentRun = shapedTextRuns[runIndex]; + + currentWidth += currentRun.Size.Width; + + if (currentWidth > availableWidth) + { + if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength)) + { + if (isWordEllipsis && measuredLength < textRange.End) + { + var currentBreakPosition = 0; + + var lineBreaker = new LineBreakEnumerator(currentRun.Text); + + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + { + var nextBreakPosition = lineBreaker.Current.PositionMeasure; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition >= measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; + } + + measuredLength = currentBreakPosition; + } + } + + collapsedLength += measuredLength; + + var shapedTextCharacters = new List(shapedTextRuns.Count); + + if (collapsedLength > 0) + { + var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, collapsedLength); + + shapedTextCharacters.AddRange(splitResult.First); + + TextLineImpl.SortRuns(shapedTextCharacters); + } + + shapedTextCharacters.Add(shapedSymbol); + + return shapedTextCharacters; + } + + availableWidth -= currentRun.Size.Width; + + collapsedLength += currentRun.GlyphRun.Characters.Length; + + runIndex++; + } + + return null; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 101f273798..13ed850715 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -171,25 +173,83 @@ namespace Avalonia.Media.TextFormatting resolvedFlowDirection = (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; - foreach (var shapeableRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels)) + var shapeableRuns = new List(textRuns.Count); + + foreach (var coalescedRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels)) { - for (var index = 0; index < shapeableRuns.Count; index++) + shapeableRuns.AddRange(coalescedRuns); + } + + for (var index = 0; index < shapeableRuns.Count; index++) + { + var currentRun = shapeableRuns[index]; + var groupedRuns = new List(2) { currentRun }; + var text = currentRun.Text; + var start = currentRun.Text.Start; + var length = currentRun.Text.Length; + var bufferOffset = currentRun.Text.BufferOffset; + + while (index + 1 < shapeableRuns.Count) { - var currentRun = shapeableRuns[index]; + var nextRun = shapeableRuns[index + 1]; - var shapedBuffer = TextShaper.Current.ShapeText(currentRun.Text, currentRun.Properties.Typeface.GlyphTypeface, - currentRun.Properties.FontRenderingEmSize, currentRun.Properties.CultureInfo, currentRun.BidiLevel); + if (currentRun.CanShapeTogether(nextRun)) + { + groupedRuns.Add(nextRun); - var shapedCharacters = new ShapedTextCharacters(shapedBuffer, currentRun.Properties); + length += nextRun.Text.Length; + + if (start > nextRun.Text.Start) + { + start = nextRun.Text.Start; + } + if (bufferOffset > nextRun.Text.BufferOffset) + { + bufferOffset = nextRun.Text.BufferOffset; + } - shapedTextCharacters.Add(shapedCharacters); + text = new ReadOnlySlice(text.Buffer, start, length, bufferOffset); + + index++; + + currentRun = nextRun; + + continue; + } + + break; } + + shapedTextCharacters.AddRange(ShapeTogether(groupedRuns, text)); } return shapedTextCharacters; } + private static IReadOnlyList ShapeTogether( + IReadOnlyList textRuns, ReadOnlySlice text) + { + var shapedRuns = new List(textRuns.Count); + var firstRun = textRuns[0]; + + var shapedBuffer = TextShaper.Current.ShapeText(text, firstRun.Properties.Typeface.GlyphTypeface, + firstRun.Properties.FontRenderingEmSize, firstRun.Properties.CultureInfo, firstRun.BidiLevel); + + for (var i = 0; i < textRuns.Count; i++) + { + var currentRun = textRuns[i]; + + var splitResult = shapedBuffer.Split(currentRun.Text.Length); + + shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties)); + + shapedBuffer = splitResult.Second!; + } + + return shapedRuns; + } + /// /// Coalesces ranges of the same bidi level to form /// @@ -369,7 +429,9 @@ namespace Avalonia.Media.TextFormatting if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) { - return lastCluster - textRange.Start; + var measuredLength = lastCluster - textRange.Start; + + return measuredLength == 0 ? 1 : measuredLength; } lastCluster = glyphInfo.GlyphCluster; @@ -394,7 +456,7 @@ namespace Avalonia.Media.TextFormatting double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection, TextLineBreak? currentLineBreak) { - var measuredLength = MeasureLength(textRuns, textRange, paragraphWidth); + var measuredLength = MeasureLength(textRuns, textRange, paragraphWidth); var currentLength = 0; @@ -506,11 +568,6 @@ namespace Avalonia.Media.TextFormatting break; } - if (measuredLength == 0) - { - measuredLength = 1; - } - var splitResult = SplitShapedRuns(textRuns, measuredLength); textRange = new TextRange(textRange.Start, measuredLength); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 64f0eaab53..0ff127694b 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -41,7 +41,7 @@ namespace Avalonia.Media.TextFormatting IBrush? foreground, TextAlignment textAlignment = TextAlignment.Left, TextWrapping textWrapping = TextWrapping.NoWrap, - TextTrimming textTrimming = TextTrimming.None, + TextTrimming? textTrimming = null, TextDecorationCollection? textDecorations = null, FlowDirection flowDirection = FlowDirection.LeftToRight, double maxWidth = double.PositiveInfinity, @@ -58,7 +58,7 @@ namespace Avalonia.Media.TextFormatting CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textDecorations, flowDirection, lineHeight); - _textTrimming = textTrimming; + _textTrimming = textTrimming ?? TextTrimming.None; _textStyleOverrides = textStyleOverrides; @@ -106,12 +106,12 @@ namespace Avalonia.Media.TextFormatting public IReadOnlyList TextLines { get; private set; } /// - /// Gets the size of the layout. + /// Gets the bounds of the layout. /// /// /// The bounds. /// - public Size Size { get; private set; } + public Rect Bounds { get; private set; } /// /// Draws the text layout. @@ -153,7 +153,7 @@ namespace Avalonia.Media.TextFormatting var lineX = lastLine.Width; - var lineY = Size.Height - lastLine.Height; + var lineY = Bounds.Bottom - lastLine.Height; return new Rect(lineX, lineY, 0, lastLine.Height); } @@ -463,7 +463,7 @@ namespace Avalonia.Media.TextFormatting var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 || - y > Size.Height; + y > Bounds.Bottom; if (textPosition == textLine.TextRange.Start + textLine.TextRange.Length) { @@ -505,17 +505,23 @@ namespace Avalonia.Media.TextFormatting /// Updates the current bounds. /// /// The text line. + /// The current left. /// The current width. /// The current height. - private static void UpdateBounds(TextLine textLine, ref double width, ref double height) + private static void UpdateBounds(TextLine textLine,ref double left, ref double width, ref double height) { - var lineWidth = textLine.WidthIncludingTrailingWhitespace + textLine.Start * 2; + var lineWidth = textLine.WidthIncludingTrailingWhitespace; if (width < lineWidth) { width = lineWidth; } + if (left > textLine.Start) + { + left = textLine.Start; + } + height += textLine.Height; } @@ -548,14 +554,14 @@ namespace Avalonia.Media.TextFormatting { var textLine = CreateEmptyTextLine(0); - Size = new Size(0, textLine.Height); + Bounds = new Rect(0,0,0, textLine.Height); return new List { textLine }; } var textLines = new List(); - double width = 0.0, height = 0.0; + double left = double.PositiveInfinity, width = 0.0, height = 0.0; var currentPosition = 0; @@ -569,23 +575,27 @@ namespace Avalonia.Media.TextFormatting var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth, _paragraphProperties, previousLine?.TextLineBreak); - currentPosition += textLine.TextRange.Length; +#if DEBUG + if (textLine.TextRange.Length == 0) + { + throw new InvalidOperationException($"{nameof(textLine)} should not be empty."); + } +#endif - if (textLines.Count > 0) + currentPosition += textLine.TextRange.Length; + + //Fulfill max height constraint + if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight) { - if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) && - height + textLine.Height > MaxHeight) + if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None) { - if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None) - { - var collapsedLine = - previousLine.Collapse(GetCollapsingProperties(MaxWidth)); + var collapsedLine = + previousLine.Collapse(GetCollapsingProperties(MaxWidth)); - textLines[textLines.Count - 1] = collapsedLine; - } - - break; + textLines[textLines.Count - 1] = collapsedLine; } + + break; } var hasOverflowed = textLine.HasOverflowed; @@ -597,10 +607,16 @@ namespace Avalonia.Media.TextFormatting textLines.Add(textLine); - UpdateBounds(textLine, ref width, ref height); + UpdateBounds(textLine, ref left, ref width, ref height); previousLine = textLine; + //Fulfill max lines constraint + if (MaxLines > 0 && textLines.Count >= MaxLines) + { + break; + } + if (currentPosition != _text.Length || textLine.NewLineLength <= 0) { continue; @@ -610,10 +626,10 @@ namespace Avalonia.Media.TextFormatting textLines.Add(emptyTextLine); - UpdateBounds(emptyTextLine, ref width, ref height); + UpdateBounds(emptyTextLine,ref left, ref width, ref height); } - Size = new Size(width, height); + Bounds = new Rect(left, 0, width, height); return textLines; } @@ -625,14 +641,7 @@ namespace Avalonia.Media.TextFormatting /// The . private TextCollapsingProperties GetCollapsingProperties(double width) { - return _textTrimming switch - { - TextTrimming.CharacterEllipsis => new TextTrailingCharacterEllipsis(width, - _paragraphProperties.DefaultTextRunProperties), - TextTrimming.WordEllipsis => new TextTrailingWordEllipsis(width, - _paragraphProperties.DefaultTextRunProperties), - _ => throw new ArgumentOutOfRangeException(), - }; + return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties)); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs new file mode 100644 index 0000000000..74c4573630 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Ellipsis based on a fixed length leading prefix and suffix growing from the end at character granularity. + /// + public sealed class TextLeadingPrefixCharacterEllipsis : TextCollapsingProperties + { + private readonly int _prefixLength; + + /// + /// Construct a text trailing word ellipsis collapsing properties. + /// + /// Text used as collapsing symbol. + /// Length of leading prefix. + /// width in which collapsing is constrained to + /// text run properties of ellispis symbol + public TextLeadingPrefixCharacterEllipsis( + ReadOnlySlice ellipsis, + int prefixLength, + double width, + TextRunProperties textRunProperties) + { + if (_prefixLength < 0) + { + throw new ArgumentOutOfRangeException(nameof(prefixLength)); + } + + _prefixLength = prefixLength; + Width = width; + Symbol = new TextCharacters(ellipsis, textRunProperties); + } + + /// + public sealed override double Width { get; } + + /// + public sealed override TextRun Symbol { get; } + + public override IReadOnlyList? Collapse(TextLine textLine) + { + var shapedTextRuns = textLine.TextRuns as List; + + if (shapedTextRuns is null) + { + return null; + } + + var runIndex = 0; + var currentWidth = 0.0; + var shapedSymbol = TextFormatterImpl.CreateSymbol(Symbol, FlowDirection.LeftToRight); + + if (Width < shapedSymbol.GlyphRun.Size.Width) + { + return new List(0); + } + + // Overview of ellipsis structure + // Prefix length run | Ellipsis symbol | Post split run growing from the end | + var availableWidth = Width - shapedSymbol.Size.Width; + + while (runIndex < shapedTextRuns.Count) + { + var currentRun = shapedTextRuns[runIndex]; + + currentWidth += currentRun.Size.Width; + + if (currentWidth > availableWidth) + { + currentRun.TryMeasureCharacters(availableWidth, out var measuredLength); + + var shapedTextCharacters = new List(shapedTextRuns.Count); + + if (measuredLength > 0) + { + List? preSplitRuns = null; + List? postSplitRuns = null; + + if (_prefixLength > 0) + { + var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, Math.Min(_prefixLength, measuredLength)); + + shapedTextCharacters.AddRange(splitResult.First); + + TextLineImpl.SortRuns(shapedTextCharacters); + + preSplitRuns = splitResult.First; + postSplitRuns = splitResult.Second; + } + else + { + postSplitRuns = shapedTextRuns; + } + + shapedTextCharacters.Add(shapedSymbol); + + if (measuredLength > _prefixLength && postSplitRuns is not null) + { + var availableSuffixWidth = availableWidth; + + if (preSplitRuns is not null) + { + foreach (var run in preSplitRuns) + { + availableSuffixWidth -= run.Size.Width; + } + } + + for (int i = postSplitRuns.Count - 1; i >= 0; i--) + { + var run = postSplitRuns[i]; + + if (run.TryMeasureCharactersBackwards(availableSuffixWidth, out int suffixCount, out double suffixWidth)) + { + availableSuffixWidth -= suffixWidth; + + if (suffixCount > 0) + { + var splitSuffix = run.Split(run.TextSourceLength - suffixCount); + + shapedTextCharacters.Add(splitSuffix.Second!); + } + } + } + } + } + else + { + shapedTextCharacters.Add(shapedSymbol); + } + + return shapedTextCharacters; + } + + availableWidth -= currentRun.Size.Width; + + runIndex++; + } + + return null; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index 53e44de779..49bee6e776 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -106,78 +106,18 @@ namespace Avalonia.Media.TextFormatting var collapsingProperties = collapsingPropertiesList[0]; - var runIndex = 0; - var currentWidth = 0.0; - var textRange = TextRange; - var collapsedLength = 0; - - var shapedSymbol = TextFormatterImpl.CreateSymbol(collapsingProperties.Symbol, _paragraphProperties.FlowDirection); + var collapsedRuns = collapsingProperties.Collapse(this); - var availableWidth = collapsingProperties.Width - shapedSymbol.GlyphRun.Size.Width; - - while (runIndex < _textRuns.Count) + if (collapsedRuns is List shapedRuns) { - var currentRun = _textRuns[runIndex]; - - currentWidth += currentRun.Size.Width; + var collapsedLine = new TextLineImpl(shapedRuns, TextRange, _paragraphWidth, _paragraphProperties, _flowDirection, TextLineBreak, true); - if (currentWidth > availableWidth) + if (shapedRuns.Count > 0) { - if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength)) - { - if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord && - measuredLength < textRange.End) - { - var currentBreakPosition = 0; - - var lineBreaker = new LineBreakEnumerator(currentRun.Text); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) - { - var nextBreakPosition = lineBreaker.Current.PositionMeasure; - - if (nextBreakPosition == 0) - { - break; - } - - if (nextBreakPosition >= measuredLength) - { - break; - } - - currentBreakPosition = nextBreakPosition; - } - - measuredLength = currentBreakPosition; - } - } - - collapsedLength += measuredLength; - - var splitResult = TextFormatterImpl.SplitShapedRuns(_textRuns, collapsedLength); - - var shapedTextCharacters = new List(splitResult.First.Count + 1); - - shapedTextCharacters.AddRange(splitResult.First); - - SortRuns(shapedTextCharacters); - - shapedTextCharacters.Add(shapedSymbol); - - textRange = new TextRange(textRange.Start, collapsedLength); - - var textLine = new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties, - _flowDirection, TextLineBreak, true); - - return textLine.FinalizeLine(); + collapsedLine.FinalizeLine(); } - availableWidth -= currentRun.Size.Width; - - collapsedLength += currentRun.GlyphRun.Characters.Length; - - runIndex++; + return collapsedLine; } return this; @@ -546,7 +486,7 @@ namespace Avalonia.Media.TextFormatting out _); var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == - TextRange.Length; + TextRange.Start + TextRange.Length; if (isAtEnd && !run.GlyphRun.IsLeftToRight) { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs index 513778b596..99fcbd805f 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs @@ -90,5 +90,11 @@ namespace Avalonia.Media.TextFormatting { return !Equals(left, right); } + + internal TextRunProperties WithTypeface(Typeface typeface) + { + return new GenericTextRunProperties(typeface, FontRenderingEmSize, + TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment); + } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index 4bd46e8c75..83acaa021e 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -1,24 +1,24 @@ -using Avalonia.Utilities; +using System.Collections.Generic; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { /// - /// a collapsing properties to collapse whole line toward the end - /// at character granularity and with ellipsis being the collapsing symbol + /// A collapsing properties to collapse whole line toward the end + /// at character granularity. /// - public class TextTrailingCharacterEllipsis : TextCollapsingProperties + public sealed class TextTrailingCharacterEllipsis : TextCollapsingProperties { - private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); - /// /// Construct a text trailing character ellipsis collapsing properties /// - /// width in which collapsing is constrained to - /// text run properties of ellispis symbol - public TextTrailingCharacterEllipsis(double width, TextRunProperties textRunProperties) + /// Text used as collapsing symbol. + /// Width in which collapsing is constrained to. + /// Text run properties of ellispis symbol. + public TextTrailingCharacterEllipsis(ReadOnlySlice ellipsis, double width, TextRunProperties textRunProperties) { Width = width; - Symbol = new TextCharacters(s_ellipsis, textRunProperties); + Symbol = new TextCharacters(ellipsis, textRunProperties); } /// @@ -27,7 +27,9 @@ namespace Avalonia.Media.TextFormatting /// public sealed override TextRun Symbol { get; } - /// - public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingCharacter; + public override IReadOnlyList? Collapse(TextLine textLine) + { + return TextEllipsisHelper.Collapse(textLine, this, false); + } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs index 9dffddd207..ff2e4cf325 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -1,37 +1,39 @@ -using Avalonia.Utilities; +using System.Collections.Generic; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { /// /// a collapsing properties to collapse whole line toward the end - /// at word granularity and with ellipsis being the collapsing symbol + /// at word granularity. /// - public class TextTrailingWordEllipsis : TextCollapsingProperties + public sealed class TextTrailingWordEllipsis : TextCollapsingProperties { - private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); - /// - /// Construct a text trailing word ellipsis collapsing properties + /// Construct a text trailing word ellipsis collapsing properties. /// - /// width in which collapsing is constrained to - /// text run properties of ellispis symbol + /// Text used as collapsing symbol. + /// width in which collapsing is constrained to. + /// text run properties of ellispis symbol. public TextTrailingWordEllipsis( + ReadOnlySlice ellipsis, double width, TextRunProperties textRunProperties ) { Width = width; - Symbol = new TextCharacters(s_ellipsis, textRunProperties); + Symbol = new TextCharacters(ellipsis, textRunProperties); } - /// public sealed override double Width { get; } /// public sealed override TextRun Symbol { get; } - /// - public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingWord; + public override IReadOnlyList? Collapse(TextLine textLine) + { + return TextEllipsisHelper.Collapse(textLine, this, true); + } } } diff --git a/src/Avalonia.Visuals/Media/TextLeadingPrefixTrimming.cs b/src/Avalonia.Visuals/Media/TextLeadingPrefixTrimming.cs new file mode 100644 index 0000000000..19ca1a0198 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextLeadingPrefixTrimming.cs @@ -0,0 +1,31 @@ +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; + +namespace Avalonia.Media +{ + public sealed class TextLeadingPrefixTrimming : TextTrimming + { + private readonly ReadOnlySlice _ellipsis; + private readonly int _prefixLength; + + public TextLeadingPrefixTrimming(char ellipsis, int prefixLength) : this(new[] { ellipsis }, prefixLength) + { + } + + public TextLeadingPrefixTrimming(char[] ellipsis, int prefixLength) + { + _prefixLength = prefixLength; + _ellipsis = new ReadOnlySlice(ellipsis); + } + + public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) + { + return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties); + } + + public override string ToString() + { + return nameof(PrefixCharacterEllipsis); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextNoneTrimming.cs b/src/Avalonia.Visuals/Media/TextNoneTrimming.cs new file mode 100644 index 0000000000..ec238cf17e --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextNoneTrimming.cs @@ -0,0 +1,18 @@ +using System; +using Avalonia.Media.TextFormatting; + +namespace Avalonia.Media +{ + internal sealed class TextNoneTrimming : TextTrimming + { + public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) + { + throw new NotSupportedException(); + } + + public override string ToString() + { + return nameof(None); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextTrailingTrimming.cs b/src/Avalonia.Visuals/Media/TextTrailingTrimming.cs new file mode 100644 index 0000000000..5bb35f0ba7 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextTrailingTrimming.cs @@ -0,0 +1,36 @@ +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; + +namespace Avalonia.Media +{ + public sealed class TextTrailingTrimming : TextTrimming + { + private readonly ReadOnlySlice _ellipsis; + private readonly bool _isWordBased; + + public TextTrailingTrimming(char ellipsis, bool isWordBased) : this(new[] {ellipsis}, isWordBased) + { + } + + public TextTrailingTrimming(char[] ellipsis, bool isWordBased) + { + _isWordBased = isWordBased; + _ellipsis = new ReadOnlySlice(ellipsis); + } + + public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) + { + if (_isWordBased) + { + return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties); + } + + return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties); + } + + public override string ToString() + { + return _isWordBased ? nameof(WordEllipsis) : nameof(CharacterEllipsis); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextTrimming.cs b/src/Avalonia.Visuals/Media/TextTrimming.cs index f8a63dfede..b6f5be496f 100644 --- a/src/Avalonia.Visuals/Media/TextTrimming.cs +++ b/src/Avalonia.Visuals/Media/TextTrimming.cs @@ -1,23 +1,76 @@ -namespace Avalonia.Media +using System; +using Avalonia.Media.TextFormatting; + +namespace Avalonia.Media { /// /// Describes how text is trimmed when it overflows. /// - public enum TextTrimming + public abstract class TextTrimming { + public static char s_defaultEllipsisChar = '\u2026'; + /// /// Text is not trimmed. /// - None, + public static TextTrimming None { get; } = new TextNoneTrimming(); /// /// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text. /// - CharacterEllipsis, + public static TextTrimming CharacterEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, false); /// /// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text. /// - WordEllipsis + public static TextTrimming WordEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, true); + + /// + /// Text is trimmed after a given prefix length. An ellipsis (...) is drawn in between prefix and suffix and represents remaining text. + /// + public static TextTrimming PrefixCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 8); + + /// + /// Text is trimmed at a character boundary starting from the beginning. An ellipsis (...) is drawn in place of remaining text. + /// + public static TextTrimming LeadingCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 0); + + /// + /// Creates properties that will be used for collapsing lines of text. + /// + /// Contextual info about text that will be collapsed. + public abstract TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo); + + /// + /// Parses a text trimming string. Names must match static properties defined in this class. + /// + /// The text trimming string. + /// The . + public static TextTrimming Parse(string s) + { + bool Matches(string name) + { + return name.Equals(s, StringComparison.OrdinalIgnoreCase); + } + + if (Matches(nameof(None))) + { + return None; + } + if (Matches(nameof(CharacterEllipsis))) + { + return CharacterEllipsis; + } + else if (Matches(nameof(WordEllipsis))) + { + return WordEllipsis; + } + else if (Matches(nameof(PrefixCharacterEllipsis))) + { + return PrefixCharacterEllipsis; + } + + throw new FormatException($"Invalid text trimming string: '{s}'."); + } } } diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs index 60701ecfaf..023a8b9cdd 100644 --- a/src/Avalonia.Visuals/Media/Transform.cs +++ b/src/Avalonia.Visuals/Media/Transform.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Animation; using Avalonia.Animation.Animators; +using Avalonia.Media.Immutable; using Avalonia.VisualTree; namespace Avalonia.Media @@ -44,6 +45,15 @@ namespace Avalonia.Media Changed?.Invoke(this, EventArgs.Empty); } + /// + /// Converts a transform to an immutable transform. + /// + /// The immutable transform + public ImmutableTransform ToImmutable() + { + return new ImmutableTransform(this.Value); + } + /// /// Returns a String representing this transform matrix instance. /// diff --git a/src/Avalonia.Visuals/Media/TransformExtensions.cs b/src/Avalonia.Visuals/Media/TransformExtensions.cs new file mode 100644 index 0000000000..ccf2231ce2 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TransformExtensions.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Media.Immutable; + +namespace Avalonia.Media +{ + /// + /// Extension methods for transform classes. + /// + public static class TransformExtensions + { + /// + /// Converts a transform to an immutable transform. + /// + /// The transform. + /// + /// The result of calling if the transform is mutable, + /// otherwise . + /// + public static ImmutableTransform ToImmutable(this ITransform transform) + { + _ = transform ?? throw new ArgumentNullException(nameof(transform)); + + return (transform as Transform)?.ToImmutable() ?? new ImmutableTransform(transform.Value); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 45540a5812..f0daa841d9 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -16,18 +16,26 @@ namespace Avalonia.Media /// The font family. /// The font style. /// The font weight. + /// The font stretch. public Typeface([NotNull] FontFamily fontFamily, FontStyle style = FontStyle.Normal, - FontWeight weight = FontWeight.Normal) + FontWeight weight = FontWeight.Normal, + FontStretch stretch = FontStretch.Normal) { if (weight <= 0) { throw new ArgumentException("Font weight must be > 0."); } + + if ((int)stretch < 1) + { + throw new ArgumentException("Font stretch must be > 1."); + } FontFamily = fontFamily; Style = style; Weight = weight; + Stretch = stretch; } /// @@ -36,10 +44,12 @@ namespace Avalonia.Media /// The name of the font family. /// The font style. /// The font weight. + /// The font stretch. public Typeface(string fontFamilyName, FontStyle style = FontStyle.Normal, - FontWeight weight = FontWeight.Normal) - : this(new FontFamily(fontFamilyName), style, weight) + FontWeight weight = FontWeight.Normal, + FontStretch stretch = FontStretch.Normal) + : this(new FontFamily(fontFamilyName), style, weight, stretch) { } @@ -59,6 +69,11 @@ namespace Avalonia.Media /// Gets the font weight. /// public FontWeight Weight { get; } + + /// + /// Gets the font stretch. + /// + public FontStretch Stretch { get; } /// /// Gets the glyph typeface. @@ -85,7 +100,8 @@ namespace Avalonia.Media public bool Equals(Typeface other) { - return FontFamily == other.FontFamily && Style == other.Style && Weight == other.Weight; + return FontFamily == other.FontFamily && Style == other.Style && + Weight == other.Weight && Stretch == other.Stretch; } public override int GetHashCode() @@ -95,6 +111,7 @@ namespace Avalonia.Media var hashCode = (FontFamily != null ? FontFamily.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (int)Style; hashCode = (hashCode * 397) ^ (int)Weight; + hashCode = (hashCode * 397) ^ (int)Stretch; return hashCode; } } diff --git a/src/Avalonia.Visuals/Platform/ExportRenderingSubsystemAttribute.cs b/src/Avalonia.Visuals/Platform/ExportRenderingSubsystemAttribute.cs deleted file mode 100644 index db157304f4..0000000000 --- a/src/Avalonia.Visuals/Platform/ExportRenderingSubsystemAttribute.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace Avalonia.Platform -{ - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public class ExportRenderingSubsystemAttribute : Attribute - { - public ExportRenderingSubsystemAttribute(OperatingSystemType requiredOS, int priority, string name, Type initializationType, string initializationMethod, - Type? environmentChecker = null) - { - Name = name; - InitializationType = initializationType; - InitializationMethod = initializationMethod; - EnvironmentChecker = environmentChecker; - RequiredOS = requiredOS; - 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; } - public string? RequiresWindowingSubsystem { get; set; } - } -} diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 113c72d373..0110287afd 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -23,6 +23,7 @@ namespace Avalonia.Platform /// The codepoint to match against. /// The font style. /// The font weight. + /// The font stretch. /// The font family. This is optional and used for fallback lookup. /// The culture. /// The matching typeface. @@ -30,7 +31,7 @@ namespace Avalonia.Platform /// True, if the could match the character to specified parameters, False otherwise. /// bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, + FontWeight fontWeight, FontStretch fontStretch, FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface); /// diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index a295a8cdc9..e4a280938d 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -11,6 +11,7 @@ namespace Avalonia.Platform /// public interface IPlatformRenderInterface { + /// /// Creates an ellipse geometry implementation. /// /// The bounds of the ellipse. diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 6d7d6c2e54..7930228d99 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -252,6 +252,18 @@ namespace Avalonia return p.X >= _x && p.X <= _x + _width && p.Y >= _y && p.Y <= _y + _height; } + + /// + /// Determines whether a point is in the bounds of the rectangle, exclusive of the + /// rectangle's bottom/right edge. + /// + /// The point. + /// true if the point is in the bounds of the rectangle; otherwise false. + public bool ContainsExclusive(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. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryBoundsHelper.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryBoundsHelper.cs new file mode 100644 index 0000000000..b1129e81c4 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryBoundsHelper.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.SceneGraph; + +internal static class GeometryBoundsHelper +{ + /// + /// Calculates the bounds of a given geometry with respect to the pens + /// + /// The calculated bounds without s + /// The pen with information about the s + /// + public static Rect CalculateBoundsWithLineCaps(this Rect originalBounds, IPen? pen) + { + if (pen is null || MathUtilities.IsZero(pen.Thickness)) return originalBounds; + + switch (pen.LineCap) + { + case PenLineCap.Flat: + return originalBounds; + case PenLineCap.Round: + return originalBounds.Inflate(pen.Thickness / 2); + case PenLineCap.Square: + return originalBounds.Inflate(pen.Thickness); + default: + throw new ArgumentOutOfRangeException(); + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index 7de1035441..70748989d6 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -24,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph IPen? pen, IGeometryImpl geometry, IDictionary? childScenes = null) - : base(geometry.GetRenderBounds(pen), transform) + : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform) { Transform = transform; Brush = brush?.ToImmutable(); diff --git a/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs index ee85d1e876..ad545b2923 100644 --- a/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs +++ b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs @@ -13,7 +13,7 @@ namespace Avalonia.Utilities [DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))] public readonly struct ReadOnlySlice : IReadOnlyList where T : struct { - private readonly int _offset; + private readonly int _bufferOffset; /// /// Gets an empty @@ -24,7 +24,7 @@ namespace Avalonia.Utilities public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { } - public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length, int offset = 0) + public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length, int bufferOffset = 0) { #if DEBUG if (start.CompareTo(0) < 0) @@ -41,7 +41,7 @@ namespace Avalonia.Utilities _buffer = buffer; Start = start; Length = length; - _offset = offset; + _bufferOffset = bufferOffset; } /// @@ -74,12 +74,17 @@ namespace Avalonia.Utilities public bool IsEmpty => Length == 0; /// - /// The underlying span. + /// Get the underlying span. /// - public ReadOnlySpan Span => _buffer.Span.Slice(_offset, Length); + public ReadOnlySpan Span => _buffer.Span.Slice(_bufferOffset, Length); /// - /// The underlying buffer. + /// Get the buffer offset. + /// + public int BufferOffset => _bufferOffset; + + /// + /// Get the underlying buffer. /// public ReadOnlyMemory Buffer => _buffer; @@ -124,17 +129,17 @@ namespace Avalonia.Utilities return Empty; } - if (start < 0 || _offset + start > _buffer.Length - 1) + if (start < 0 || _bufferOffset + start > _buffer.Length - 1) { throw new ArgumentOutOfRangeException(nameof(start)); } - if (_offset + start + length > _buffer.Length) + if (_bufferOffset + start + length > _buffer.Length) { throw new ArgumentOutOfRangeException(nameof(length)); } - return new ReadOnlySlice(_buffer, start, length, _offset); + return new ReadOnlySlice(_buffer, start, length, _bufferOffset); } /// @@ -154,7 +159,7 @@ namespace Avalonia.Utilities throw new ArgumentOutOfRangeException(nameof(length)); } - return new ReadOnlySlice(_buffer, Start, length, _offset); + return new ReadOnlySlice(_buffer, Start, length, _bufferOffset); } /// @@ -174,7 +179,7 @@ namespace Avalonia.Utilities throw new ArgumentOutOfRangeException(nameof(length)); } - return new ReadOnlySlice(_buffer, Start + length, Length - length, _offset + length); + return new ReadOnlySlice(_buffer, Start + length, Length - length, _bufferOffset + length); } /// 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/SMLib.cs b/src/Avalonia.X11/SMLib.cs index e2b39cfcff..f8f13e32f8 100644 --- a/src/Avalonia.X11/SMLib.cs +++ b/src/Avalonia.X11/SMLib.cs @@ -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/X11PlatformLifetimeEvents.cs b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs index 1a17a018e8..8412bd0730 100644 --- a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs +++ b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs @@ -153,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/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..88529ae3a0 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -201,7 +201,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 +221,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..99072ace02 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; } @@ -85,6 +87,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 +105,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); @@ -187,6 +196,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..548aae31a8 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 @@ + + 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/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/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index e695a9cb41..642808eacb 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -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..075a2cc746 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,10 +109,17 @@ 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 @@ -125,7 +132,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/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index af3b570fd7..1c92c7d193 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -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..f66df9e6e9 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,168 @@ 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; + } + + //Nothing was found so we try some regular cases. + if (_typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface)) + { + return typeface; + } + + return _typefaces.TryGetValue(new Typeface(key.FontFamily, FontStyle.Italic), out typeface) ? + typeface : + null; + } + + 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; + } + } - for (var i = 0; i < 2; i++) + //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; + } + } + } + + //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 (_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 <= 900; 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 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)) { - continue; + return true; } + } - if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) + //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 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/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/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/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/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/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..ba842757d7 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -7,6 +7,9 @@ + + + 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 1809fcf98b..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 }; @@ -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); @@ -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/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 54e9aa583f..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), @@ -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/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 d1945e6c85..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; } @@ -245,7 +247,7 @@ namespace Avalonia.Win32 { get { - if(_isFullScreenActive) + if (_isFullScreenActive) { return WindowState.FullScreen; } @@ -268,7 +270,7 @@ namespace Avalonia.Win32 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) @@ -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; } @@ -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; @@ -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..39a0305b1e 100644 --- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj +++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj @@ -1,17 +1,22 @@ - + + - xamarin.ios10 - true - latest + net6.0-ios + $(TargetFrameworks);xamarin.ios10 + 10.0 + true - - - - - - TargetFramework=netstandard2.0 - + + + + + + + <_BuiltProjectOutputGroupOutputIntermediate Remove="$(OutDir)$(_DeploymentTargetApplicationManifestFileName)" /> + + + 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/tests/Avalonia.Animation.UnitTests/BrushTransitionTests.cs b/tests/Avalonia.Animation.UnitTests/BrushTransitionTests.cs new file mode 100644 index 0000000000..1986bd2ee3 --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/BrushTransitionTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Animation.UnitTests +{ + public class BrushTransitionTests + { + [Fact] + public void SolidColorBrush_Opacity_IsInteroplated() + { + Test(0, new SolidColorBrush { Opacity = 0 }, new SolidColorBrush { Opacity = 0 }); + Test(0, new SolidColorBrush { Opacity = 0 }, new SolidColorBrush { Opacity = 1 }); + Test(0.5, new SolidColorBrush { Opacity = 0 }, new SolidColorBrush { Opacity = 1 }); + Test(0.5, new SolidColorBrush { Opacity = 0.5 }, new SolidColorBrush { Opacity = 0.5 }); + Test(1, new SolidColorBrush { Opacity = 1 }, new SolidColorBrush { Opacity = 1 }); + // TODO: investigate why this case fails. + //Test2(1, new SolidColorBrush { Opacity = 0 }, new SolidColorBrush { Opacity = 1 }); + } + + [Fact] + public void LinearGradientBrush_Opacity_IsInteroplated() + { + Test(0, new LinearGradientBrush { Opacity = 0 }, new LinearGradientBrush { Opacity = 0 }); + Test(0, new LinearGradientBrush { Opacity = 0 }, new LinearGradientBrush { Opacity = 1 }); + Test(0.5, new LinearGradientBrush { Opacity = 0 }, new LinearGradientBrush { Opacity = 1 }); + Test(0.5, new LinearGradientBrush { Opacity = 0.5 }, new LinearGradientBrush { Opacity = 0.5 }); + Test(1, new LinearGradientBrush { Opacity = 1 }, new LinearGradientBrush { Opacity = 1 }); + } + + private static void Test(double progress, IBrush oldBrush, IBrush newBrush) + { + var clock = new TestClock(); + var border = new Border() { Background = oldBrush }; + BrushTransition sut = new BrushTransition + { + Duration = TimeSpan.FromSeconds(1), + Property = Border.BackgroundProperty + }; + + sut.Apply(border, clock, oldBrush, newBrush); + clock.Pulse(TimeSpan.Zero); + clock.Pulse(sut.Duration * progress); + + Assert.NotNull(border.Background); + Assert.Equal(oldBrush.Opacity + (newBrush.Opacity - oldBrush.Opacity) * progress, border.Background.Opacity); + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 7c4c5c96a5..6b4a6f89df 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -11,6 +11,7 @@ using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; +using Avalonia.Utilities; using Microsoft.Reactive.Testing; using Moq; using Xunit; @@ -785,7 +786,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }); - Assert.False(source.SetterCalled); + Assert.False(source.ValueSetterCalled); } [Fact] @@ -796,7 +797,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source }); - Assert.False(source.SetterCalled); + Assert.False(source.ValueSetterCalled); } [Fact] @@ -829,6 +830,59 @@ namespace Avalonia.Base.UnitTests subscription.Dispose(); } + [Fact] + public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_With_Value() + { + var target = new Class1(); + var source = new TestTwoWayBindingViewModel() { Value = 1 }; + source.ResetSetterCalled(); + + target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }); + + Assert.False(source.ValueSetterCalled); + } + + [Fact] + public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_Indexer_With_Value() + { + var target = new Class1(); + var source = new TestTwoWayBindingViewModel() { [0] = 1 }; + source.ResetSetterCalled(); + + target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source }); + + Assert.False(source.ValueSetterCalled); + } + + + [Fact] + public void Disposing_a_TwoWay_Binding_Should_Set_Default_Value_On_Binding_Target_But_Not_On_Source() + { + var target = new Class3(); + + // Create a source class which has a Value set to -1 and a Minimum set to -2 + var source = new TestTwoWayBindingViewModel() { Value = -1, Minimum = -2 }; + + // Reset the setter counter + source.ResetSetterCalled(); + + // 1. bind the minimum + var disposable_1 = target.Bind(Class3.MinimumProperty, new Binding("Minimum", BindingMode.TwoWay) { Source = source }); + // 2. Bind the value + var disposable_2 = target.Bind(Class3.ValueProperty, new Binding("Value", BindingMode.TwoWay) { Source = source }); + + // Dispose the minimum binding + disposable_1.Dispose(); + // Dispose the value binding + disposable_2.Dispose(); + + + // The value setter should be called here as we have disposed minimum fist and the default value of minimum is 0, so this should be changed. + Assert.True(source.ValueSetterCalled); + // The minimum value should not be changed in the source. + Assert.False(source.MinimumSetterCalled); + } + /// /// Returns an observable that returns a single value but does not complete. /// @@ -864,6 +918,56 @@ namespace Avalonia.Base.UnitTests AvaloniaProperty.Register("Bar", "bardefault"); } + private class Class3 : AvaloniaObject + { + static Class3() + { + MinimumProperty.Changed.Subscribe(x => OnMinimumChanged(x)); + } + + private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is Class3 s) + { + s.SetValue(ValueProperty, MathUtilities.Clamp(s.Value, e.NewValue.Value, double.PositiveInfinity)); + } + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register(nameof(Value), 0); + + /// + /// Gets or sets the Value property + /// + public double Value + { + get { return GetValue(ValueProperty); } + set { SetValue(ValueProperty, value); } + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinimumProperty = + AvaloniaProperty.Register(nameof(Minimum), 0); + + /// + /// Gets or sets the minimum property + /// + public double Minimum + { + get { return GetValue(MinimumProperty); } + set { SetValue(MinimumProperty, value); } + } + + + } + + private class TestOneTimeBinding : IBinding { private IObservable _source; @@ -928,7 +1032,18 @@ namespace Avalonia.Base.UnitTests set { _value = value; - SetterCalled = true; + ValueSetterCalled = true; + } + } + + private double _minimum; + public double Minimum + { + get => _minimum; + set + { + _minimum = value; + MinimumSetterCalled = true; } } @@ -938,11 +1053,18 @@ namespace Avalonia.Base.UnitTests set { _value = value; - SetterCalled = true; + ValueSetterCalled = true; } } - public bool SetterCalled { get; private set; } + public bool ValueSetterCalled { get; private set; } + public bool MinimumSetterCalled { get; private set; } + + public void ResetSetterCalled() + { + ValueSetterCalled = false; + MinimumSetterCalled = false; + } } } } diff --git a/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs new file mode 100644 index 0000000000..5c3ac6adeb --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs @@ -0,0 +1,29 @@ +using System; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests.Utilities; + +public class UriExtensionsTests +{ + [Fact] + public void Assembly_Name_From_Query_Parsed() + { + const string key = "assembly"; + const string value = "Avalonia.Themes.Default"; + + var uri = new Uri($"resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?{key}={value}"); + var name = uri.GetAssemblyNameFromQuery(); + + Assert.Equal(value, name); + } + + [Fact] + public void Assembly_Name_From_Empty_Query_Not_Parsed() + { + var uri = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml"); + var name = uri.GetAssemblyNameFromQuery(); + + Assert.Equal(string.Empty, name); + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs new file mode 100644 index 0000000000..b128e6d83a --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Automation/ControlAutomationPeerTests.cs @@ -0,0 +1,229 @@ +using System; +using System.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +#nullable enable + +namespace Avalonia.Controls.UnitTests.Automation +{ + public class ControlAutomationPeerTests + { + public class Children + { + [Fact] + public void Creates_Children_For_Controls_In_Visual_Tree() + { + var panel = new Panel + { + Children = + { + new Border(), + new Border(), + }, + }; + + var target = CreatePeer(panel); + + Assert.Equal( + panel.GetVisualChildren(), + target.GetChildren().Cast().Select(x => x.Owner)); + } + + [Fact] + public void Creates_Children_when_Controls_Attached_To_Visual_Tree() + { + var contentControl = new ContentControl + { + Template = new FuncControlTemplate((o, ns) => + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = o[!ContentControl.ContentProperty], + }), + Content = new Border(), + }; + + var target = CreatePeer(contentControl); + + Assert.Empty(target.GetChildren()); + + contentControl.Measure(Size.Infinity); + + Assert.Equal(1, target.GetChildren().Count); + } + + [Fact] + public void Updates_Children_When_VisualChildren_Added() + { + var panel = new Panel + { + Children = + { + new Border(), + new Border(), + }, + }; + + var target = CreatePeer(panel); + var children = target.GetChildren(); + + Assert.Equal(2, children.Count); + + panel.Children.Add(new Decorator()); + + children = target.GetChildren(); + Assert.Equal(3, children.Count); + } + + [Fact] + public void Updates_Children_When_VisualChildren_Removed() + { + var panel = new Panel + { + Children = + { + new Border(), + new Border(), + }, + }; + + var target = CreatePeer(panel); + var children = target.GetChildren(); + + Assert.Equal(2, children.Count); + + panel.Children.RemoveAt(1); + + children = target.GetChildren(); + Assert.Equal(1, children.Count); + } + + [Fact] + public void Updates_Children_When_Visibility_Changes() + { + var panel = new Panel + { + Children = + { + new Border(), + new Border(), + }, + }; + + var target = CreatePeer(panel); + var children = target.GetChildren(); + + Assert.Equal(2, children.Count); + + panel.Children[1].IsVisible = false; + children = target.GetChildren(); + Assert.Equal(1, children.Count); + + panel.Children[1].IsVisible = true; + children = target.GetChildren(); + Assert.Equal(2, children.Count); + } + } + + public class Parent + { + [Fact] + public void Connects_Peer_To_Tree_When_GetParent_Called() + { + var border = new Border(); + var tree = new Decorator + { + Child = new Decorator + { + Child = border, + } + }; + + // We're accessing Border directly without going via its ancestors. Because the tree + // is built lazily, ensure that calling GetParent causes the ancestor tree to be built. + var target = CreatePeer(border); + + var parentPeer = Assert.IsAssignableFrom(target.GetParent()); + Assert.Same(border.GetVisualParent(), parentPeer.Owner); + } + + [Fact] + public void Parent_Updated_When_Moved_To_Separate_Visual_Tree() + { + var border = new Border(); + var root1 = new Decorator { Child = border }; + var root2 = new Decorator(); + var target = CreatePeer(border); + + var parentPeer = Assert.IsAssignableFrom(target.GetParent()); + Assert.Same(root1, parentPeer.Owner); + + root1.Child = null; + + Assert.Null(target.GetParent()); + + root2.Child = border; + + parentPeer = Assert.IsAssignableFrom(target.GetParent()); + Assert.Same(root2, parentPeer.Owner); + } + } + + private static AutomationPeer CreatePeer(Control control) + { + return ControlAutomationPeer.CreatePeerForElement(control); + } + + private class TestControl : Control + { + protected override AutomationPeer OnCreateAutomationPeer() + { + return new TestAutomationPeer(this); + } + } + + private class AutomationTestRoot : TestRoot + { + protected override AutomationPeer OnCreateAutomationPeer() + { + return new TestRootAutomationPeer(this); + } + } + + private class TestAutomationPeer : ControlAutomationPeer + { + public TestAutomationPeer( Control owner) + : base(owner) + { + } + } + + private class TestRootAutomationPeer : ControlAutomationPeer, IRootProvider + { + public TestRootAutomationPeer(Control owner) + : base(owner) + { + } + + public ITopLevelImpl PlatformImpl => throw new System.NotImplementedException(); + public event EventHandler? FocusChanged; + + public AutomationPeer GetFocus() + { + throw new System.NotImplementedException(); + } + + public AutomationPeer GetPeerFromPoint(Point p) + { + throw new System.NotImplementedException(); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index aa63e18691..e87990ebb1 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -156,112 +156,127 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Selection_Should_Be_Cleared_On_Recycled_Items() { - var target = new ListBox + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Template = ListBoxTemplate(), - Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), - SelectedIndex = 0, - }; + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), + SelectedIndex = 0, + }; - Prepare(target); + Prepare(target); - // Make sure we're virtualized and first item is selected. - Assert.Equal(10, target.Presenter.Panel.Children.Count); - Assert.True(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected); + // Make sure we're virtualized and first item is selected. + Assert.Equal(10, target.Presenter.Panel.Children.Count); + Assert.True(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected); - // Scroll down a page. - target.Scroll.Offset = new Vector(0, 10); + // Scroll down a page. + target.Scroll.Offset = new Vector(0, 10); - // Make sure recycled item isn't now selected. - Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected); + // Make sure recycled item isn't now selected. + Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected); + } } [Fact] public void ScrollViewer_Should_Have_Correct_Extent_And_Viewport() { - var target = new ListBox + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Template = ListBoxTemplate(), - Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectedIndex = 0, - }; + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectedIndex = 0, + }; - Prepare(target); + Prepare(target); - Assert.Equal(new Size(20, 20), target.Scroll.Extent); - Assert.Equal(new Size(100, 10), target.Scroll.Viewport); + Assert.Equal(new Size(20, 20), target.Scroll.Extent); + Assert.Equal(new Size(100, 10), target.Scroll.Viewport); + } } [Fact] public void Containers_Correct_After_Clear_Add_Remove() { - // Issue #1936 - var items = new AvaloniaList(Enumerable.Range(0, 11).Select(x => $"Item {x}")); - var target = new ListBox + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Template = ListBoxTemplate(), - Items = items, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectedIndex = 0, - }; + // Issue #1936 + var items = new AvaloniaList(Enumerable.Range(0, 11).Select(x => $"Item {x}")); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectedIndex = 0, + }; - Prepare(target); + Prepare(target); - items.Clear(); - items.AddRange(Enumerable.Range(0, 11).Select(x => $"Item {x}")); - items.Remove("Item 2"); + items.Clear(); + items.AddRange(Enumerable.Range(0, 11).Select(x => $"Item {x}")); + items.Remove("Item 2"); - Assert.Equal( - items, - target.Presenter.Panel.Children.Cast().Select(x => (string)x.Content)); + Assert.Equal( + items, + target.Presenter.Panel.Children.Cast().Select(x => (string)x.Content)); + } } [Fact] public void Toggle_Selection_Should_Update_Containers() { - var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray(); - var target = new ListBox + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Template = ListBoxTemplate(), - Items = items, - SelectionMode = SelectionMode.Toggle, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }) - }; + var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray(); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + SelectionMode = SelectionMode.Toggle, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }) + }; - Prepare(target); + Prepare(target); - var lbItems = target.GetLogicalChildren().OfType().ToArray(); + var lbItems = target.GetLogicalChildren().OfType().ToArray(); - var item = lbItems[0]; + var item = lbItems[0]; - Assert.Equal(false, item.IsSelected); + Assert.Equal(false, item.IsSelected); - RaisePressedEvent(target, item, MouseButton.Left); + RaisePressedEvent(target, item, MouseButton.Left); - Assert.Equal(true, item.IsSelected); + Assert.Equal(true, item.IsSelected); - RaisePressedEvent(target, item, MouseButton.Left); + RaisePressedEvent(target, item, MouseButton.Left); - Assert.Equal(false, item.IsSelected); + Assert.Equal(false, item.IsSelected); + } } [Fact] public void Can_Decrease_Number_Of_Materialized_Items_By_Removing_From_Source_Collection() { - var items = new AvaloniaList(Enumerable.Range(0, 20).Select(x => $"Item {x}")); - var target = new ListBox + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Template = ListBoxTemplate(), - Items = items, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }) - }; + var items = new AvaloniaList(Enumerable.Range(0, 20).Select(x => $"Item {x}")); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }) + }; - Prepare(target); - target.Scroll.Offset = new Vector(0, 1); + Prepare(target); + target.Scroll.Offset = new Vector(0, 1); - items.RemoveRange(0, 11); + items.RemoveRange(0, 11); + } } private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton) @@ -272,35 +287,38 @@ namespace Avalonia.Controls.UnitTests [Fact] public void ListBox_After_Scroll_IndexOutOfRangeException_Shouldnt_Be_Thrown() { - var items = Enumerable.Range(0, 11).Select(x => $"{x}").ToArray(); - - var target = new ListBox + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Template = ListBoxTemplate(), - Items = items, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 11 }) - }; + var items = Enumerable.Range(0, 11).Select(x => $"{x}").ToArray(); - Prepare(target); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 11 }) + }; + + Prepare(target); - var panel = target.Presenter.Panel as IVirtualizingPanel; + var panel = target.Presenter.Panel as IVirtualizingPanel; - var listBoxItems = panel.Children.OfType(); + var listBoxItems = panel.Children.OfType(); - //virtualization should have created exactly 10 items - Assert.Equal(10, listBoxItems.Count()); - Assert.Equal("0", listBoxItems.First().DataContext); - Assert.Equal("9", listBoxItems.Last().DataContext); + //virtualization should have created exactly 10 items + Assert.Equal(10, listBoxItems.Count()); + Assert.Equal("0", listBoxItems.First().DataContext); + Assert.Equal("9", listBoxItems.Last().DataContext); - //instead pixeloffset > 0 there could be pretty complex sequence for repro - //it involves add/remove/scroll to end multiple actions - //which i can't find so far :(, but this is the simplest way to add it to unit test - panel.PixelOffset = 1; + //instead pixeloffset > 0 there could be pretty complex sequence for repro + //it involves add/remove/scroll to end multiple actions + //which i can't find so far :(, but this is the simplest way to add it to unit test + panel.PixelOffset = 1; - //here scroll to end -> IndexOutOfRangeException is thrown - target.Scroll.Offset = new Vector(0, 2); + //here scroll to end -> IndexOutOfRangeException is thrown + target.Scroll.Offset = new Vector(0, 2); - Assert.True(true); + Assert.True(true); + } } [Fact] @@ -374,41 +392,44 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Clicking_Item_Should_Raise_BringIntoView_For_Correct_Control() { - // Issue #3934 - var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray(); - var target = new ListBox + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Template = ListBoxTemplate(), - Items = items, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), - SelectionMode = SelectionMode.AlwaysSelected, - VirtualizationMode = ItemVirtualizationMode.None, - }; + // Issue #3934 + var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray(); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), + SelectionMode = SelectionMode.AlwaysSelected, + VirtualizationMode = ItemVirtualizationMode.None, + }; - Prepare(target); + Prepare(target); - // First an item that is not index 0 must be selected. - _mouse.Click(target.Presenter.Panel.Children[1]); - Assert.Equal(1, target.Selection.AnchorIndex); + // First an item that is not index 0 must be selected. + _mouse.Click(target.Presenter.Panel.Children[1]); + Assert.Equal(1, target.Selection.AnchorIndex); - // We're going to be clicking on item 9. - var item = (ListBoxItem)target.Presenter.Panel.Children[9]; - var raised = 0; + // We're going to be clicking on item 9. + var item = (ListBoxItem)target.Presenter.Panel.Children[9]; + var raised = 0; - // Make sure a RequestBringIntoView event is raised for item 9. It won't be handled - // by the ScrollContentPresenter as the item is already visible, so we don't need - // handledEventsToo: true. Issue #3934 failed here because item 0 was being scrolled - // into view due to SelectionMode.AlwaysSelected. - target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => - { - Assert.Same(item, e.TargetObject); - ++raised; - }); + // Make sure a RequestBringIntoView event is raised for item 9. It won't be handled + // by the ScrollContentPresenter as the item is already visible, so we don't need + // handledEventsToo: true. Issue #3934 failed here because item 0 was being scrolled + // into view due to SelectionMode.AlwaysSelected. + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => + { + Assert.Same(item, e.TargetObject); + ++raised; + }); - // Click item 9. - _mouse.Click(item); + // Click item 9. + _mouse.Click(item); - Assert.Equal(1, raised); + Assert.Equal(1, raised); + } } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index a579e869b0..6d20c72674 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -353,6 +353,29 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Null(target.Host); } + [Fact] + public void Content_Should_Become_DataContext_When_ControlTemplate_Is_Not_Null() + { + var (target, _) = CreateTarget(); + + var textBlock = new TextBlock + { + [!TextBlock.TextProperty] = new Binding("Name"), + }; + + var canvas = new Canvas() + { + Name = "Canvas" + }; + + target.ContentTemplate = new FuncDataTemplate((_, __) => textBlock); + target.Content = canvas; + + Assert.NotNull(target.DataContext); + Assert.Equal(canvas, target.DataContext); + Assert.Equal("Canvas", textBlock.Text); + } + (ContentPresenter presenter, ContentControl templatedParent) CreateTarget() { var templatedParent = new ContentControl diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 9c1822ff5c..cac8ca885d 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -525,6 +525,7 @@ namespace Avalonia.Controls.UnitTests.Primitives using (CreateServicesWithFocus()) { var window = PreparedWindow(); + window.Focusable = true; var tb = new TextBox(); var p = new Popup diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 514d3b5475..0e0ca7cd25 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1,9 +1,11 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using System.Reactive.Disposables; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; @@ -13,7 +15,9 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Data; +using Avalonia.Platform; using Avalonia.Styling; +using Avalonia.Threading; using Avalonia.UnitTests; using Moq; using Xunit; @@ -1895,6 +1899,54 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, carouselRaised); } + [Fact] + public void Setting_IsTextSearchEnabled_Enables_Or_Disables_Text_Search() + { + var pti = Mock.Of(x => x.CurrentThreadIsLoopThread == true); + + Mock.Get(pti) + .Setup(v => v.StartTimer(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Disposable.Empty); + + using (UnitTestApplication.Start(TestServices.StyledWindow.With(threadingInterface: pti))) + { + var items = new[] + { + new Item { [TextSearch.TextProperty] = "Foo" }, + new Item { [TextSearch.TextProperty] = "Bar" } + }; + + var target = new SelectingItemsControl + { + Items = items, + Template = Template(), + IsTextSearchEnabled = false + }; + + Prepare(target); + + target.RaiseEvent(new TextInputEventArgs + { + RoutedEvent = InputElement.TextInputEvent, + Device = KeyboardDevice.Instance, + Text = "Foo" + }); + + Assert.Null(target.SelectedItem); + + target.IsTextSearchEnabled = true; + + target.RaiseEvent(new TextInputEventArgs + { + RoutedEvent = InputElement.TextInputEvent, + Device = KeyboardDevice.Instance, + Text = "Foo" + }); + + Assert.Equal(items[0], target.SelectedItem); + } + } + private static void Prepare(SelectingItemsControl target) { var root = new TestRoot diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index b180a536a5..0ed1f8d2d0 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -1,3 +1,5 @@ +using System; +using Avalonia.Controls.Documents; using Avalonia.Data; using Avalonia.Media; using Avalonia.Rendering; @@ -60,5 +62,47 @@ namespace Avalonia.Controls.UnitTests renderer.Verify(x => x.AddDirty(target), Times.Once); } + + [Fact] + public void Changing_InlinesCollection_Should_Invalidate_Measure() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new TextBlock(); + + target.Measure(Size.Infinity); + + Assert.True(target.IsMeasureValid); + + target.Inlines.Add(new Run("Hello")); + + Assert.False(target.IsMeasureValid); + + target.Measure(Size.Infinity); + + Assert.True(target.IsMeasureValid); + } + } + + [Fact] + public void Changing_Inlines_Properties_Should_Invalidate_Measure() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new TextBlock(); + + var inline = new Run("Hello"); + + target.Inlines.Add(inline); + + target.Measure(Size.Infinity); + + Assert.True(target.IsMeasureValid); + + inline.Text = "1337"; + + Assert.False(target.IsMeasureValid); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs index 7eaec35506..d33e55341b 100644 --- a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); Assert.Equal(new Size(200, 100), target.DesiredSize); - var scaleTransform = target.Child.RenderTransform as ScaleTransform; + var scaleTransform = target.InternalTransform as ScaleTransform; Assert.NotNull(scaleTransform); Assert.Equal(2.0, scaleTransform.ScaleX); @@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); Assert.Equal(new Size(100, 50), target.DesiredSize); - var scaleTransform = target.Child.RenderTransform as ScaleTransform; + var scaleTransform = target.InternalTransform as ScaleTransform; Assert.NotNull(scaleTransform); Assert.Equal(1.0, scaleTransform.ScaleX); @@ -54,7 +54,7 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); Assert.Equal(new Size(200, 200), target.DesiredSize); - var scaleTransform = target.Child.RenderTransform as ScaleTransform; + var scaleTransform = target.InternalTransform as ScaleTransform; Assert.NotNull(scaleTransform); Assert.Equal(2.0, scaleTransform.ScaleX); @@ -72,7 +72,7 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); Assert.Equal(new Size(200, 200), target.DesiredSize); - var scaleTransform = target.Child.RenderTransform as ScaleTransform; + var scaleTransform = target.InternalTransform as ScaleTransform; Assert.NotNull(scaleTransform); Assert.Equal(4.0, scaleTransform.ScaleX); @@ -90,7 +90,7 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); Assert.Equal(new Size(400, 200), target.DesiredSize); - var scaleTransform = target.Child.RenderTransform as ScaleTransform; + var scaleTransform = target.InternalTransform as ScaleTransform; Assert.NotNull(scaleTransform); Assert.Equal(4.0, scaleTransform.ScaleX); @@ -108,7 +108,7 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); Assert.Equal(new Size(200, 100), target.DesiredSize); - var scaleTransform = target.Child.RenderTransform as ScaleTransform; + var scaleTransform = target.InternalTransform as ScaleTransform; Assert.NotNull(scaleTransform); Assert.Equal(2.0, scaleTransform.ScaleX); @@ -136,7 +136,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(expectedWidth, expectedHeight), target.DesiredSize); - var scaleTransform = target.Child.RenderTransform as ScaleTransform; + var scaleTransform = target.InternalTransform as ScaleTransform; Assert.NotNull(scaleTransform); Assert.Equal(expectedScale, scaleTransform.ScaleX); @@ -164,7 +164,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(expectedWidth, expectedHeight), target.DesiredSize); - var scaleTransform = target.Child.RenderTransform as ScaleTransform; + var scaleTransform = target.InternalTransform as ScaleTransform; Assert.NotNull(scaleTransform); Assert.Equal(expectedScale, scaleTransform.ScaleX); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 4166242455..f643a84e37 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -516,6 +516,8 @@ namespace Avalonia.Controls.UnitTests var screens = new Mock(); screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object, screen2.Object }); + screens.Setup(x => x.ScreenFromPoint(It.IsAny())).Returns(screen1.Object); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); @@ -821,7 +823,7 @@ namespace Avalonia.Controls.UnitTests target.Width = 410; target.LayoutManager.ExecuteLayoutPass(); - var windowImpl = Mock.Get(ValidatingWindowImpl.Unwrap(target.PlatformImpl)); + var windowImpl = Mock.Get(target.PlatformImpl); windowImpl.Verify(x => x.Resize(new Size(410, 800), PlatformResizeReason.Application)); Assert.Equal(410, target.Width); } diff --git a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj index 5358b71571..f4c6434a68 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj +++ b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj @@ -15,7 +15,6 @@ - diff --git a/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json b/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json index eb707cb73f..5477bcfd29 100644 --- a/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json +++ b/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json @@ -1673,9 +1673,9 @@ "dev": true }, "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, "picomatch": { diff --git a/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj b/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj index 4353b7b09c..cdadc3dcba 100644 --- a/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj +++ b/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj @@ -9,7 +9,6 @@ - diff --git a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs index c354dbe72e..32ba14d337 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs @@ -129,5 +129,30 @@ namespace Avalonia.Input.UnitTests public bool CanExecute(object parameter) => true; public void Execute(object parameter) => _action(); } + + [Fact] + public void Control_Focus_Should_Be_Set_Before_FocusedElement_Raises_PropertyChanged() + { + var target = new KeyboardDevice(); + var focused = new Mock(); + var root = Mock.Of(); + var raised = 0; + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.FocusedElement)) + { + focused.Verify(x => x.RaiseEvent(It.IsAny())); + ++raised; + } + }; + + target.SetFocusedElement( + focused.Object, + NavigationMethod.Unspecified, + KeyModifiers.None); + + Assert.Equal(1, raised); + } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs b/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs new file mode 100644 index 0000000000..bad015506f --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs @@ -0,0 +1,39 @@ +using OpenQA.Selenium.Appium; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class AutomationTests + { + private readonly AppiumDriver _session; + + public AutomationTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Automation"); + tab.Click(); + } + + [Fact] + public void AutomationId() + { + // AutomationID can be specified by the Name or AutomationProperties.AutomationId + // properties, with the latter taking precedence. + var byName = _session.FindElementByAccessibilityId("TextBlockWithName"); + var byAutomationId = _session.FindElementByAccessibilityId("TextBlockWithNameAndAutomationId"); + } + + [Fact] + public void LabeledBy() + { + var label = _session.FindElementByAccessibilityId("TextBlockAsLabel"); + var labeledTextBox = _session.FindElementByAccessibilityId("LabeledByTextBox"); + + Assert.Equal("Label for TextBox", label.Text); + Assert.Equal("Label for TextBox", labeledTextBox.GetName()); + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj new file mode 100644 index 0000000000..095f0e63e0 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + + + + + + + + + diff --git a/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs new file mode 100644 index 0000000000..2ac859e091 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices; +using OpenQA.Selenium.Appium; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class ButtonTests + { + private readonly AppiumDriver _session; + + public ButtonTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Button"); + tab.Click(); + } + + [Fact] + public void DisabledButton() + { + var button = _session.FindElementByAccessibilityId("DisabledButton"); + + Assert.Equal("Disabled Button", button.Text); + Assert.False(button.Enabled); + } + + [Fact] + public void BasicButton() + { + var button = _session.FindElementByAccessibilityId("BasicButton"); + + Assert.Equal("Basic Button", button.Text); + Assert.True(button.Enabled); + } + + [Fact] + public void ButtonWithTextBlock() + { + var button = _session.FindElementByAccessibilityId("ButtonWithTextBlock"); + + Assert.Equal("Button with TextBlock", button.Text); + } + + [PlatformFact(SkipOnOSX = true)] + public void ButtonWithAcceleratorKey() + { + var button = _session.FindElementByAccessibilityId("ButtonWithAcceleratorKey"); + + Assert.Equal("Ctrl+B", button.GetAttribute("AcceleratorKey")); + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs new file mode 100644 index 0000000000..02e7ac60c4 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs @@ -0,0 +1,62 @@ +using OpenQA.Selenium.Appium; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class CheckBoxTests + { + private readonly AppiumDriver _session; + + public CheckBoxTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("CheckBox"); + tab.Click(); + } + + [Fact] + public void UncheckedCheckBox() + { + var checkBox = _session.FindElementByAccessibilityId("UncheckedCheckBox"); + + Assert.Equal("Unchecked", checkBox.GetName()); + Assert.Equal(false, checkBox.GetIsChecked()); + + checkBox.Click(); + Assert.Equal(true, checkBox.GetIsChecked()); + } + + [Fact] + public void CheckedCheckBox() + { + var checkBox = _session.FindElementByAccessibilityId("CheckedCheckBox"); + + Assert.Equal("Checked", checkBox.GetName()); + Assert.Equal(true, checkBox.GetIsChecked()); + + checkBox.Click(); + Assert.Equal(false, checkBox.GetIsChecked()); + } + + [Fact] + public void ThreeStateCheckBox() + { + var checkBox = _session.FindElementByAccessibilityId("ThreeStateCheckBox"); + + Assert.Equal("ThreeState", checkBox.GetName()); + Assert.Null(checkBox.GetIsChecked()); + + checkBox.Click(); + Assert.Equal(false, checkBox.GetIsChecked()); + + checkBox.Click(); + Assert.Equal(true, checkBox.GetIsChecked()); + + checkBox.Click(); + Assert.Null(checkBox.GetIsChecked()); + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs new file mode 100644 index 0000000000..fad3e1eb9f --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -0,0 +1,100 @@ +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class ComboBoxTests + { + private readonly AppiumDriver _session; + + public ComboBoxTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("ComboBox"); + tab.Click(); + } + + [Fact] + public void Can_Change_Selection_Using_Mouse() + { + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); + + _session.FindElementByAccessibilityId("ComboBoxSelectFirst").Click(); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + + comboBox.Click(); + _session.FindElementByName("Item 1").SendClick(); + + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); + } + + [Fact] + public void Can_Change_Selection_From_Unselected_Using_Mouse() + { + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); + + _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); + Assert.Equal(string.Empty, comboBox.GetComboBoxValue()); + + comboBox.Click(); + _session.FindElementByName("Item 0").SendClick(); + + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + } + + [PlatformFact(SkipOnOSX = true)] + public void Can_Change_Selection_With_Keyboard() + { + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); + + _session.FindElementByAccessibilityId("ComboBoxSelectFirst").Click(); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.LeftAlt + Keys.ArrowDown); + comboBox.SendKeys(Keys.ArrowDown); + + var item = _session.FindElementByName("Item 1"); + item.SendKeys(Keys.Enter); + + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); + } + + [PlatformFact(SkipOnOSX = true)] + public void Can_Change_Selection_With_Keyboard_From_Unselected() + { + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); + + _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); + Assert.Equal(string.Empty, comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.LeftAlt + Keys.ArrowDown); + comboBox.SendKeys(Keys.ArrowDown); + + var item = _session.FindElementByName("Item 0"); + item.SendKeys(Keys.Enter); + + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + } + + [PlatformFact(SkipOnOSX = true)] + public void Can_Cancel_Keyboard_Selection_With_Escape() + { + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); + + _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); + Assert.Equal(string.Empty, comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.LeftAlt + Keys.ArrowDown); + comboBox.SendKeys(Keys.ArrowDown); + + var item = _session.FindElementByName("Item 0"); + item.SendKeys(Keys.Escape); + + Assert.Equal(string.Empty, comboBox.GetComboBoxValue()); + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs b/tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs new file mode 100644 index 0000000000..bb2dd1fbec --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [CollectionDefinition("Default")] + public class DefaultCollection : ICollectionFixture + { + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs new file mode 100644 index 0000000000..15e22f4424 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Interactions; + +namespace Avalonia.IntegrationTests.Appium +{ + internal static class ElementExtensions + { + public static IReadOnlyList GetChildren(this AppiumWebElement element) => + element.FindElementsByXPath("*/*"); + + public static string GetComboBoxValue(this AppiumWebElement element) + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + element.Text : + element.GetAttribute("value"); + } + + public static string GetName(this AppiumWebElement element) => GetAttribute(element, "Name", "title"); + + public static bool? GetIsChecked(this AppiumWebElement element) => + GetAttribute(element, "Toggle.ToggleState", "value") switch + { + "0" => false, + "1" => true, + "2" => null, + _ => throw new ArgumentOutOfRangeException($"Unexpected IsChecked value.") + }; + + public static void SendClick(this AppiumWebElement element) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + element.Click(); + } + else + { + // The Click() method seems to correspond to accessibilityPerformPress on macOS but certain controls + // such as list items don't support this action, so instead simulate a physical click as VoiceOver + // does. + new Actions(element.WrappedDriver).MoveToElement(element).Click().Perform(); + } + } + + public static string GetAttribute(AppiumWebElement element, string windows, string macOS) + { + return element.GetAttribute(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windows : macOS); + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs new file mode 100644 index 0000000000..625742ac20 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs @@ -0,0 +1,103 @@ +using System.Threading; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Interactions; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class ListBoxTests + { + private readonly AppiumDriver _session; + + public ListBoxTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("ListBox"); + tab.Click(); + } + + [Fact] + public void Can_Select_Item_By_Clicking() + { + var listBox = GetTarget(); + var item2 = listBox.FindElementByName("Item 2"); + var item4 = listBox.FindElementByName("Item 4"); + + Assert.False(item2.Selected); + Assert.False(item4.Selected); + + item2.SendClick(); + Assert.True(item2.Selected); + Assert.False(item4.Selected); + + item4.SendClick(); + Assert.False(item2.Selected); + Assert.True(item4.Selected); + } + + [Fact(Skip = "WinAppDriver seems unable to consistently send a Ctrl key and appium-mac2-driver just hangs")] + public void Can_Select_Items_By_Ctrl_Clicking() + { + var listBox = GetTarget(); + var item2 = listBox.FindElementByName("Item 2"); + var item4 = listBox.FindElementByName("Item 4"); + + Assert.False(item2.Selected); + Assert.False(item4.Selected); + + new Actions(_session) + .Click(item2) + .KeyDown(Keys.Control) + .Click(item4) + .KeyUp(Keys.Control) + .Perform(); + + Assert.True(item2.Selected); + Assert.True(item4.Selected); + } + + // appium-mac2-driver just hangs + [PlatformFact(SkipOnOSX = true)] + public void Can_Select_Range_By_Shift_Clicking() + { + var listBox = GetTarget(); + var item2 = listBox.FindElementByName("Item 2"); + var item3 = listBox.FindElementByName("Item 3"); + var item4 = listBox.FindElementByName("Item 4"); + + Assert.False(item2.Selected); + Assert.False(item3.Selected); + Assert.False(item4.Selected); + + new Actions(_session) + .Click(item2) + .KeyDown(Keys.Shift) + .Click(item4) + .KeyUp(Keys.Shift) + .Perform(); + + Assert.True(item2.Selected); + Assert.True(item3.Selected); + Assert.True(item4.Selected); + } + + [Fact] + public void Is_Virtualized() + { + var listBox = GetTarget(); + var children = listBox.GetChildren(); + + Assert.True(children.Count < 100); + } + + private AppiumWebElement GetTarget() + { + _session.FindElementByAccessibilityId("ListBoxSelectionClear").Click(); + return _session.FindElementByAccessibilityId("BasicListBox"); + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs new file mode 100644 index 0000000000..e9a433b975 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs @@ -0,0 +1,63 @@ +using OpenQA.Selenium.Appium; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class MenuTests + { + private readonly AppiumDriver _session; + + public MenuTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Menu"); + tab.Click(); + } + + [Fact] + public void Click_Child() + { + var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); + + rootMenuItem.SendClick(); + + var childMenuItem = _session.FindElementByAccessibilityId("Child1MenuItem"); + childMenuItem.SendClick(); + + var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); + Assert.Equal("_Child 1", clickedMenuItem.Text); + } + + [Fact] + public void Click_Grandchild() + { + var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); + + rootMenuItem.SendClick(); + + var childMenuItem = _session.FindElementByAccessibilityId("Child2MenuItem"); + childMenuItem.SendClick(); + + var grandchildMenuItem = _session.FindElementByAccessibilityId("GrandchildMenuItem"); + grandchildMenuItem.SendClick(); + + var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); + Assert.Equal("_Grandchild", clickedMenuItem.Text); + } + + [PlatformFact(SkipOnOSX = true)] + public void Child_AcceleratorKey() + { + var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); + + rootMenuItem.SendClick(); + + var childMenuItem = _session.FindElementByAccessibilityId("Child1MenuItem"); + + Assert.Equal("Ctrl+O", childMenuItem.GetAttribute("AcceleratorKey")); + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs new file mode 100644 index 0000000000..fde01f0e41 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs @@ -0,0 +1,37 @@ +using OpenQA.Selenium.Appium; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class NativeMenuTests + { + private readonly AppiumDriver _session; + + public NativeMenuTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Automation"); + tab.Click(); + } + + [PlatformFact(SkipOnWindows = true)] + public void View_Menu_Select_Button_Tab() + { + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var buttonTab = tabs.FindElementByName("Button"); + var menuBar = _session.FindElementByXPath("/XCUIElementTypeApplication/XCUIElementTypeMenuBar"); + var viewMenu = menuBar.FindElementByName("View"); + + Assert.False(buttonTab.Selected); + + viewMenu.Click(); + var buttonMenu = viewMenu.FindElementByName("Button"); + buttonMenu.Click(); + + Assert.True(buttonTab.Selected); + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs b/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs new file mode 100644 index 0000000000..60338b92c2 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + internal class PlatformFactAttribute : FactAttribute + { + public override string? Skip + { + get + { + if (SkipOnWindows && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return "Ignored on Windows"; + if (SkipOnOSX && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return "Ignored on MacOS"; + return null; + } + set => throw new NotSupportedException(); + } + public bool SkipOnOSX { get; set; } + public bool SkipOnWindows { get; set; } + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/Properties/AssemblyInfo.cs b/tests/Avalonia.IntegrationTests.Appium/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f9248a3152 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using Xunit; + +// Don't run tests in parallel. +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs new file mode 100644 index 0000000000..b3385d8ee7 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs @@ -0,0 +1,72 @@ +using System; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Enums; +using OpenQA.Selenium.Appium.Mac; +using OpenQA.Selenium.Appium.Windows; + +namespace Avalonia.IntegrationTests.Appium +{ + public class TestAppFixture : IDisposable + { + private const string TestAppPath = @"..\..\..\..\..\samples\IntegrationTestApp\bin\Debug\net6.0\IntegrationTestApp.exe"; + private const string TestAppBundleId = "net.avaloniaui.avalonia.integrationtestapp"; + + public TestAppFixture() + { + var opts = new AppiumOptions(); + var path = Path.GetFullPath(TestAppPath); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + opts.AddAdditionalCapability(MobileCapabilityType.App, path); + opts.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.Windows); + opts.AddAdditionalCapability(MobileCapabilityType.DeviceName, "WindowsPC"); + + Session = new WindowsDriver( + new Uri("http://127.0.0.1:4723"), + opts); + + // https://github.com/microsoft/WinAppDriver/issues/1025 + SetForegroundWindow(new IntPtr(int.Parse( + Session.WindowHandles[0].Substring(2), + NumberStyles.AllowHexSpecifier))); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + opts.AddAdditionalCapability("appium:bundleId", TestAppBundleId); + opts.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.MacOS); + opts.AddAdditionalCapability(MobileCapabilityType.AutomationName, "mac2"); + opts.AddAdditionalCapability("appium:showServerLogs", true); + + Session = new MacDriver( + new Uri("http://127.0.0.1:4723/wd/hub"), + opts); + } + else + { + throw new NotSupportedException("Unsupported platform."); + } + } + + public AppiumDriver Session { get; } + + public void Dispose() + { + try + { + Session.Close(); + } + catch + { + // Closing the session currently seems to crash the mac2 driver. + } + } + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetForegroundWindow(IntPtr hWnd); + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/readme.md b/tests/Avalonia.IntegrationTests.Appium/readme.md new file mode 100644 index 0000000000..ee630a31fd --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/readme.md @@ -0,0 +1,30 @@ +# Running Integration Tests + +## Windows + +### Prerequisites + +- Install WinAppDriver: https://github.com/microsoft/WinAppDriver + +### Running + +- Run WinAppDriver (it gets installed to the start menu) +- Run the tests in this project + +## macOS + +### Prerequisites + +- Install Appium: https://appium.io/ +- Give [Xcode helper the required permissions](https://apple.stackexchange.com/questions/334008) +- `cd samples/IntegrationTestApp` then `./bundle.sh` to create an app bundle for `IntegrationTestApp` +- Register the app bundle by running `open -n ./bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app` + +### Running + +- Run `appium` +- Run the tests in this project + +Each time you make a change to Avalonia or `IntegrationTestApp`, re-run the `bundle.sh` script (registration only needs to be done once). + + diff --git a/tests/Avalonia.IntegrationTests.Appium/xunit.runner.json b/tests/Avalonia.IntegrationTests.Appium/xunit.runner.json new file mode 100644 index 0000000000..f78bc2f0c6 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +} \ No newline at end of file diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index eed767e771..087d42370e 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -496,7 +496,7 @@ namespace Avalonia.LeakTests AttachShowAndDetachContextMenu(window); - Mock.Get(ValidatingWindowImpl.Unwrap(window.PlatformImpl)).Invocations.Clear(); + Mock.Get(window.PlatformImpl).Invocations.Clear(); dotMemory.Check(memory => Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); dotMemory.Check(memory => @@ -541,7 +541,7 @@ namespace Avalonia.LeakTests BuildAndShowContextMenu(window); BuildAndShowContextMenu(window); - Mock.Get(ValidatingWindowImpl.Unwrap(window.PlatformImpl)).Invocations.Clear(); + Mock.Get(window.PlatformImpl).Invocations.Clear(); dotMemory.Check(memory => Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); dotMemory.Check(memory => diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs index b0623aa456..72e0ac5e57 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs @@ -23,8 +23,8 @@ namespace Avalonia.Markup.UnitTests.Parsers public static void StaticMethod() { } - public static void TooManyParameters(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9) { } - public static int TooManyParametersWithReturnType(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) => 1; + public static void ManyParameters(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9) { } + public static int ManyParametersWithReturnType(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) => 1; } [Fact] @@ -44,6 +44,8 @@ namespace Avalonia.Markup.UnitTests.Parsers [InlineData(nameof(TestObject.MethodWithReturn), typeof(Func))] [InlineData(nameof(TestObject.MethodWithReturnAndParameters), typeof(Func))] [InlineData(nameof(TestObject.StaticMethod), typeof(Action))] + [InlineData(nameof(TestObject.ManyParameters), typeof(Action))] + [InlineData(nameof(TestObject.ManyParametersWithReturnType), typeof(Func))] public async Task Should_Get_Method_WithCorrectDelegateType(string methodName, Type expectedType) { var data = new TestObject(); @@ -68,21 +70,5 @@ namespace Avalonia.Markup.UnitTests.Parsers GC.KeepAlive(data); } - - [Theory] - [InlineData(nameof(TestObject.TooManyParameters))] - [InlineData(nameof(TestObject.TooManyParametersWithReturnType))] - public async Task Should_Return_Error_Notification_If_Too_Many_Parameters(string methodName) - { - var data = new TestObject(); - var observer = ExpressionObserverBuilder.Build(data, methodName); - var result = await observer.Take(1); - - Assert.IsType(result); - - Assert.Equal(BindingErrorType.Error, ((BindingNotification)result).ErrorType); - - GC.KeepAlive(data); - } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 15c6f5877f..c1c2284372 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Globalization; using System.Reactive.Subjects; using System.Text; @@ -9,6 +10,7 @@ using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Data.Converters; using Avalonia.Data.Core; +using Avalonia.Input; using Avalonia.Markup.Data; using Avalonia.Media; using Avalonia.UnitTests; @@ -1062,6 +1064,205 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void Binds_To_Self_In_Style() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + +