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 f1bdfdbbc3..c8e513f94c 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -5,48 +5,26 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Visuals", "src\Avalonia.Visuals\Avalonia.Visuals.csproj", "{EB582467-6ABB-43A1-B052-E981BA910E3A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Layout", "src\Avalonia.Layout\Avalonia.Layout.csproj", "{42472427-4774-4C81-8AFF-9F27B8E31721}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Windows", "Windows", "{B39A8919-9F95-48FE-AD7B-76E08B509888}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32", "src\Windows\Avalonia.Win32\Avalonia.Win32.csproj", "{811A76CF-1CF6-440F-963B-BBE31BD72A82}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1", "src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj", "{3E908F67-5543-4879-A1DC-08EACE79B3CD}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Input", "src\Avalonia.Input\Avalonia.Input.csproj", "{62024B2D-53EB-4638-B26B-85EEAA54866E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Interactivity", "src\Avalonia.Interactivity\Avalonia.Interactivity.csproj", "{6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls", "src\Avalonia.Controls\Avalonia.Controls.csproj", "{D2221C82-4A25-4583-9B43-D791E3F6820C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Styling", "src\Avalonia.Styling\Avalonia.Styling.csproj", "{F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Default", "src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Diagnostics", "src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj", "{7062AE20-5DCC-4442-9645-8195BDECE63E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Animation", "src\Avalonia.Animation\Avalonia.Animation.csproj", "{D211E587-D8BC-45B9-95A4-F297C8FA5200}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Styling.UnitTests", "tests\Avalonia.Styling.UnitTests\Avalonia.Styling.UnitTests.csproj", "{47ECDF59-DEF8-4C53-87B1-2098A3429059}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.UnitTests", "tests\Avalonia.Controls.UnitTests\Avalonia.Controls.UnitTests.csproj", "{5CCB5571-7C30-4E7D-967D-0E2158EBD91F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Visuals.UnitTests", "tests\Avalonia.Visuals.UnitTests\Avalonia.Visuals.UnitTests.csproj", "{76716382-3159-460E-BDA6-C5715CF606D7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base.UnitTests", "tests\Avalonia.Base.UnitTests\Avalonia.Base.UnitTests.csproj", "{2905FF23-53FB-45E6-AA49-6AF47A172056}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Layout.UnitTests", "tests\Avalonia.Layout.UnitTests\Avalonia.Layout.UnitTests.csproj", "{DB070A10-BF39-4752-8456-86E9D5928478}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Interactivity.UnitTests", "tests\Avalonia.Interactivity.UnitTests\Avalonia.Interactivity.UnitTests.csproj", "{08478EF5-44E8-42E9-92D6-15E00EC038D8}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1.RenderTests", "tests\Avalonia.Direct2D1.RenderTests\Avalonia.Direct2D1.RenderTests.csproj", "{DABFD304-D6A4-4752-8123-C2CCF7AC7831}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Input.UnitTests", "tests\Avalonia.Input.UnitTests\Avalonia.Input.UnitTests.csproj", "{AC18926A-E784-40FE-B09D-BB0FE2B599F0}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1.UnitTests", "tests\Avalonia.Direct2D1.UnitTests\Avalonia.Direct2D1.UnitTests.csproj", "{EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.UnitTests", "tests\Avalonia.Markup.Xaml.UnitTests\Avalonia.Markup.Xaml.UnitTests.csproj", "{99135EAB-653D-47E4-A378-C96E1278CA44}" @@ -61,6 +39,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs + src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}" @@ -77,8 +56,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Android", "src\Android\Avalonia.Android\Avalonia.Android.csproj", "{7B92AF71-6287-4693-9DCB-BD5B6E927E23}" EndProject -Project("{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 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.iOS", "src\iOS\Avalonia.iOS\Avalonia.iOS.csproj", "{4488AD85-1495-4809-9AA4-DDFE0A48527E}" @@ -95,8 +72,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog", "samples\C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Desktop", "samples\ControlCatalog.Desktop\ControlCatalog.Desktop.csproj", "{2B888490-D14A-4BCA-AB4B-48676FA93C9B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{57E0455D-D565-44BB-B069-EE1AA20F8337}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{52F55355-D120-42AC-8116-8410A7D602FA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" @@ -119,12 +94,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Base.props = build\Base.props build\Binding.props = build\Binding.props build\CoreLibraries.props = build\CoreLibraries.props + build\DevAnalyzers.props = build\DevAnalyzers.props build\EmbedXaml.props = build\EmbedXaml.props build\HarfBuzzSharp.props = build\HarfBuzzSharp.props - build\iOSWorkarounds.props = build\iOSWorkarounds.props build\JetBrains.Annotations.props = build\JetBrains.Annotations.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props - build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props build\Microsoft.CSharp.props = build\Microsoft.CSharp.props build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props build\Moq.props = build\Moq.props @@ -137,11 +111,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\SharedVersion.props = build\SharedVersion.props build\SharpDX.props = build\SharpDX.props build\SkiaSharp.props = build\SkiaSharp.props + build\SourceGenerators.props = build\SourceGenerators.props build\SourceLink.props = build\SourceLink.props build\System.Drawing.Common.props = build\System.Drawing.Common.props build\System.Memory.props = build\System.Memory.props build\UnitTests.NetFX.props = build\UnitTests.NetFX.props build\XUnit.props = build\XUnit.props + build\ImageSharp.props = build\ImageSharp.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}" @@ -177,8 +153,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.OpenGL", "src\Aval EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Native", "src\Avalonia.Native\Avalonia.Native.csproj", "{12A91A62-C064-42CA-9A8C-A1272F354388}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesktopRuntime", "src\Avalonia.DesktopRuntime\Avalonia.DesktopRuntime.csproj", "{878FEFE0-CD14-41CB-90B0-DBCB163E8F15}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Packages", "Packages", "{E870DCD7-F46A-498D-83FC-D0FD13E0A11C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia", "packages\Avalonia\Avalonia.csproj", "{D49233F8-F29C-47DD-9975-C4C9E4502720}" @@ -189,14 +163,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Build.Tasks", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "nukebuild\_build.csproj", "{3F00BC43-5095-477F-93D8-E65B08179A00}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Animation.UnitTests", "tests\Avalonia.Animation.UnitTests\Avalonia.Animation.UnitTests.csproj", "{AF227847-E65C-4BE9-BCE9-B551357788E0}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{1ECC012A-8837-4AE2-9BDA-3E2857898727}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" @@ -221,6 +195,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}" @@ -233,6 +211,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -273,54 +259,6 @@ Global {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|iPhone.Build.0 = Release|Any CPU {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|Any CPU.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|iPhone.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|iPhone.Build.0 = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|Any CPU.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|iPhone.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|iPhone.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|Any CPU.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|iPhone.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|Any CPU.Build.0 = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|iPhone.Build.0 = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|Any CPU.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|Any CPU.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|iPhone.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|iPhone.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -369,54 +307,6 @@ Global {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhone.Build.0 = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|Any CPU.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|iPhone.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|iPhone.Build.0 = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|Any CPU.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|iPhone.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|iPhone.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|Any CPU.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|iPhone.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|iPhone.Build.0 = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|Any CPU.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|iPhone.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|iPhone.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -441,30 +331,6 @@ Global {D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|iPhone.Build.0 = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|Any CPU.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|iPhone.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|iPhone.Build.0 = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|Any CPU.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|iPhone.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|iPhone.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -513,54 +379,6 @@ Global {7062AE20-5DCC-4442-9645-8195BDECE63E}.Release|iPhone.Build.0 = Release|Any CPU {7062AE20-5DCC-4442-9645-8195BDECE63E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {7062AE20-5DCC-4442-9645-8195BDECE63E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|Any CPU.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|iPhone.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|iPhone.Build.0 = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|Any CPU.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|iPhone.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|iPhone.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|Any CPU.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|iPhone.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|Any CPU.Build.0 = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|iPhone.Build.0 = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|Any CPU.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|Any CPU.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|iPhone.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|iPhone.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -585,30 +403,6 @@ Global {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|iPhone.Build.0 = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|Any CPU.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|iPhone.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|iPhone.Build.0 = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|Any CPU.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|iPhone.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|iPhone.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -633,54 +427,6 @@ Global {2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|iPhone.Build.0 = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|Any CPU.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|iPhone.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|iPhone.Build.0 = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|Any CPU.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|iPhone.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|iPhone.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|Any CPU.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|iPhone.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|iPhone.Build.0 = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|Any CPU.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|iPhone.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|iPhone.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -705,30 +451,6 @@ Global {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|iPhone.Build.0 = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|Any CPU.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|iPhone.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|iPhone.Build.0 = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|Any CPU.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|iPhone.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|iPhone.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -911,22 +633,6 @@ Global {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|Any CPU.Build.0 = Release|Any CPU {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|iPhone.ActiveCfg = Release|Any CPU {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.Build.0 = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.Deploy.0 = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|iPhone.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -1095,26 +801,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 @@ -1559,30 +1245,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 @@ -1679,30 +1341,6 @@ Global {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|iPhone.Build.0 = Release|Any CPU {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhone.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhone.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|Any CPU.Build.0 = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhone.ActiveCfg = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhone.Build.0 = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU @@ -2063,6 +1701,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 @@ -2183,6 +1869,126 @@ Global {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhone.Build.0 = Release|Any CPU {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.AppStore|iPhone.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|iPhone.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.Build.0 = Release|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhone.ActiveCfg = Release|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhone.Build.0 = Release|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhone.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhone.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|Any CPU.Build.0 = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.ActiveCfg = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.Build.0 = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhone.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhone.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.Build.0 = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhone.ActiveCfg = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhone.Build.0 = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhone.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhone.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.Build.0 = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.ActiveCfg = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.Build.0 = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhone.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhone.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.Build.0 = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.ActiveCfg = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.Build.0 = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2190,14 +1996,9 @@ Global GlobalSection(NestedProjects) = preSolution {811A76CF-1CF6-440F-963B-BBE31BD72A82} = {B39A8919-9F95-48FE-AD7B-76E08B509888} {3E908F67-5543-4879-A1DC-08EACE79B3CD} = {B39A8919-9F95-48FE-AD7B-76E08B509888} - {47ECDF59-DEF8-4C53-87B1-2098A3429059} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {5CCB5571-7C30-4E7D-967D-0E2158EBD91F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {76716382-3159-460E-BDA6-C5715CF606D7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {2905FF23-53FB-45E6-AA49-6AF47A172056} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {DB070A10-BF39-4752-8456-86E9D5928478} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {08478EF5-44E8-42E9-92D6-15E00EC038D8} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {DABFD304-D6A4-4752-8123-C2CCF7AC7831} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {AC18926A-E784-40FE-B09D-BB0FE2B599F0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {99135EAB-653D-47E4-A378-C96E1278CA44} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {3E53A01A-B331-47F3-B828-4A5717E77A24} = {8B6A8209-894F-4BA1-B880-965FD453982C} @@ -2205,14 +2006,12 @@ Global {8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098} {7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} - {FF69B927-C545-49AE-8E16-3D14D621AA12} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} {4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1} {E1AA3DBF-9056-4530-9376-18119A7A3FFE} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {88060192-33D5-4932-B0F9-8BD2763E857D} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {D0A739B9-3C68-4BA6-A328-41606954B6BD} = {9B9E3891-2366-4253-A952-D08BCEB71098} {2B888490-D14A-4BCA-AB4B-48676FA93C9B} = {9B9E3891-2366-4253-A952-D08BCEB71098} - {57E0455D-D565-44BB-B069-EE1AA20F8337} = {9B9E3891-2366-4253-A952-D08BCEB71098} {52F55355-D120-42AC-8116-8410A7D602FA} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {F1381F98-4D24-409A-A6C5-1C5B1E08BB08} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {FBCAF3D0-2808-4934-8E96-3F607594517B} = {9B9E3891-2366-4253-A952-D08BCEB71098} @@ -2221,6 +2020,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} @@ -2231,7 +2031,6 @@ Global {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {D49233F8-F29C-47DD-9975-C4C9E4502720} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C} {3C471044-3640-45E3-B1B2-16D2FF8399EE} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C} - {AF227847-E65C-4BE9-BCE9-B551357788E0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} @@ -2240,10 +2039,15 @@ Global {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} {BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {676D6BFD-029D-4E43-BFC7-3892265CE251} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {25831348-EB2A-483E-9576-E8F6528674A5} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} {C08E9894-AA92-426E-BF56-033E262CAD3E} = {9B9E3891-2366-4253-A952-D08BCEB71098} {26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {CE910927-CE5A-456F-BC92-E4C757354A5C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/Directory.Build.props b/Directory.Build.props index 835decc672..42daa2df7f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,9 +1,11 @@ + $(MSBuildThisFileDirectory)build-intermediate/nuget $(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netcoreapp2.0\Avalonia.Designer.HostApp.dll false false + False diff --git a/NuGet.Config b/NuGet.Config index 7a1f28bea7..7d2bd8abd2 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -5,5 +5,6 @@ + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ede5aec0e8..edf3c3d819 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -31,14 +31,14 @@ jobs: vmImage: 'ubuntu-20.04' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.414' + displayName: 'Use .NET Core SDK 3.1.418' inputs: - version: 3.1.414 + version: 3.1.418 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.100' + displayName: 'Use .NET Core SDK 6.0.202' inputs: - version: 6.0.100 + version: 6.0.202 - task: CmdLine@2 displayName: 'Run Build' @@ -62,14 +62,14 @@ jobs: vmImage: 'macOS-10.15' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.414' + displayName: 'Use .NET Core SDK 3.1.418' inputs: - version: 3.1.414 + version: 3.1.418 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.100' + displayName: 'Use .NET Core SDK 6.0.202' inputs: - version: 6.0.100 + version: 6.0.202 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -134,20 +134,20 @@ jobs: SolutionDir: '$(Build.SourcesDirectory)' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.414' + displayName: 'Use .NET Core SDK 3.1.418' inputs: - version: 3.1.414 + version: 3.1.418 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.100' + displayName: 'Use .NET Core SDK 6.0.202' inputs: - version: 6.0.100 + version: 6.0.202 - task: CmdLine@2 displayName: 'Install Workloads' inputs: script: | - dotnet workload install android + dotnet workload install android ios - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/build/ApiCompatAttributeExcludeList.txt b/build/ApiCompatAttributeExcludeList.txt new file mode 100644 index 0000000000..1df5a30ec3 --- /dev/null +++ b/build/ApiCompatAttributeExcludeList.txt @@ -0,0 +1,2 @@ +T:Avalonia.Metadata.NotClientImplementableAttribute +T:Avalonia.Metadata.UnstableAttribute diff --git a/build/AvaloniaPublicKey.props b/build/AvaloniaPublicKey.props new file mode 100644 index 0000000000..89215635c0 --- /dev/null +++ b/build/AvaloniaPublicKey.props @@ -0,0 +1,5 @@ + + + 0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87 + + diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index 3fccad2641..9448a31d73 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -1,22 +1,13 @@ - - - - - - - - - diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props new file mode 100644 index 0000000000..14e4f6a563 --- /dev/null +++ b/build/DevAnalyzers.props @@ -0,0 +1,9 @@ + + + + + diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 1d84d5289a..85e7a1f34d 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/ImageSharp.props b/build/ImageSharp.props new file mode 100644 index 0000000000..178c274ac9 --- /dev/null +++ b/build/ImageSharp.props @@ -0,0 +1,5 @@ + + + + + diff --git a/build/JetBrains.dotMemoryUnit.props b/build/JetBrains.dotMemoryUnit.props index eb4e2b6f15..5d74d474cf 100644 --- a/build/JetBrains.dotMemoryUnit.props +++ b/build/JetBrains.dotMemoryUnit.props @@ -1,5 +1,5 @@ - + diff --git a/build/Magick.NET-Q16-AnyCPU.props b/build/Magick.NET-Q16-AnyCPU.props deleted file mode 100644 index 21d9cdcb1f..0000000000 --- a/build/Magick.NET-Q16-AnyCPU.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/build/NetFX.props b/build/NetFX.props index 8ffc9ec561..14adb54035 100644 --- a/build/NetFX.props +++ b/build/NetFX.props @@ -1,7 +1,6 @@  - 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..1ee4aa56a2 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props new file mode 100644 index 0000000000..d000af1bf6 --- /dev/null +++ b/build/SourceGenerators.props @@ -0,0 +1,10 @@ + + + + + + diff --git a/build/SourceLink.props b/build/SourceLink.props index 9f05848881..dd7ecc8d2a 100644 --- a/build/SourceLink.props +++ b/build/SourceLink.props @@ -19,7 +19,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 74d7b482cc..396e0c915c 100644 --- a/dirs.proj +++ b/dirs.proj @@ -10,21 +10,20 @@ + + - - - - - + + diff --git a/global.json b/global.json index 1f93ed27c3..a6792b05c7 100644 --- a/global.json +++ b/global.json @@ -1,10 +1,10 @@ { "sdk": { - "version": "6.0.100" + "version": "6.0.202", + "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/inc/rendertarget.h b/native/Avalonia.Native/inc/rendertarget.h index 2b0338d099..a59915777f 100644 --- a/native/Avalonia.Native/inc/rendertarget.h +++ b/native/Avalonia.Native/inc/rendertarget.h @@ -1,3 +1,8 @@ +#pragma once + +#include "com.h" +#include "comimpl.h" +#include "avalonia-native.h" @protocol IRenderTarget -(void) setNewLayer: (CALayer*) layer; diff --git a/native/Avalonia.Native/src/OSX/AutoFitContentView.h b/native/Avalonia.Native/src/OSX/AutoFitContentView.h new file mode 100644 index 0000000000..7f1f764141 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AutoFitContentView.h @@ -0,0 +1,17 @@ +// +// Created by Dan Walmsley on 05/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#pragma once + +#import +#include "avalonia-native.h" + +@interface AutoFitContentView : NSView +-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; +-(void) ShowTitleBar: (bool) show; +-(void) SetTitleBarHeightHint: (double) height; + +-(void) ShowBlur: (bool) show; +@end \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/AutoFitContentView.mm b/native/Avalonia.Native/src/OSX/AutoFitContentView.mm new file mode 100644 index 0000000000..0fa4540726 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AutoFitContentView.mm @@ -0,0 +1,106 @@ +// +// Created by Dan Walmsley on 05/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#include "AvnView.h" +#include "AutoFitContentView.h" +#include "WindowInterfaces.h" +#include "WindowProtocol.h" + +@implementation AutoFitContentView +{ + NSVisualEffectView* _titleBarMaterial; + NSBox* _titleBarUnderline; + NSView* _content; + NSVisualEffectView* _blurBehind; + double _titleBarHeightHint; + bool _settingSize; +} + +-(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content +{ + _titleBarHeightHint = -1; + _content = content; + _settingSize = false; + + [self setAutoresizesSubviews:true]; + [self setWantsLayer:true]; + + _titleBarMaterial = [NSVisualEffectView new]; + [_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow]; + [_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar]; + [_titleBarMaterial setWantsLayer:true]; + _titleBarMaterial.hidden = true; + + _titleBarUnderline = [NSBox new]; + _titleBarUnderline.boxType = NSBoxSeparator; + _titleBarUnderline.fillColor = [NSColor underPageBackgroundColor]; + _titleBarUnderline.hidden = true; + + [self addSubview:_titleBarMaterial]; + [self addSubview:_titleBarUnderline]; + + _blurBehind = [NSVisualEffectView new]; + [_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [_blurBehind setMaterial:NSVisualEffectMaterialLight]; + [_blurBehind setWantsLayer:true]; + _blurBehind.hidden = true; + + [_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + + [self addSubview:_blurBehind]; + [self addSubview:_content]; + + [self setWantsLayer:true]; + return self; +} + +-(void) ShowBlur:(bool)show +{ + _blurBehind.hidden = !show; +} + +-(void) ShowTitleBar: (bool) show +{ + _titleBarMaterial.hidden = !show; + _titleBarUnderline.hidden = !show; +} + +-(void) SetTitleBarHeightHint: (double) height +{ + _titleBarHeightHint = height; + + [self setFrameSize:self.frame.size]; +} + +-(void)setFrameSize:(NSSize)newSize +{ + if(_settingSize) + { + return; + } + + _settingSize = true; + [super setFrameSize:newSize]; + + auto window = (id ) [self window]; + + // TODO get actual titlebar size + + double height = _titleBarHeightHint == -1 ? [window getExtendedTitleBarHeight] : _titleBarHeightHint; + + NSRect tbar; + tbar.origin.x = 0; + tbar.origin.y = newSize.height - height; + tbar.size.width = newSize.width; + tbar.size.height = height; + + [_titleBarMaterial setFrame:tbar]; + tbar.size.height = height < 1 ? 0 : 1; + [_titleBarUnderline setFrame:tbar]; + + _settingSize = false; +} +@end \ No newline at end of file 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..6fc3977d4e 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 @@ -7,6 +7,24 @@ objects = { /* Begin PBXBuildFile section */ + 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391E45702740FE9DD69695 /* ResizeScope.mm */; }; + 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 183919BF108EB72A029F7671 /* WindowImpl.mm */; }; + 183914E50CF6D2EFC1667F7C /* WindowInterfaces.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391DB45C7D892E61BF388C /* WindowInterfaces.h */; }; + 1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391884C7476DA4E53A492D /* AvnPanelWindow.mm */; }; + 183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */; }; + 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391BBB7782C296D424071F /* INSWindowHolder.h */; }; + 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */; }; + 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391CD090AA776E7E841AC9 /* WindowImpl.h */; }; + 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839166350F32661F3ABD70F /* AutoFitContentView.mm */; }; + 18391AC16726CBC45856233B /* AvnWindow.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839155B28B20FFB672D29C6 /* AvnWindow.mm */; }; + 18391AC65ADD7DDD33FBE737 /* PopupImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 183910513F396141938832B5 /* PopupImpl.h */; }; + 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839171D898F9BFC1373631A /* ResizeScope.h */; }; + 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */ = {isa = PBXBuildFile; fileRef = 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */; }; + 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839132D0E2454D911F1D1F9 /* AvnView.mm */; }; + 18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391BB698579F40F1783F31 /* PopupImpl.mm */; }; + 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */; }; + 18391ED5F611FF62C45F196D /* AvnView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391D1669284AD2EC9E866A /* AvnView.h */; }; + 18391F1E2411C79405A9943A /* WindowProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839122E037567BDD1D09DEB /* WindowProtocol.h */; }; 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; }; 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; }; @@ -28,11 +46,30 @@ AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB1E522B217613570091CD71 /* OpenGL.framework */; }; 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 */ + 183910513F396141938832B5 /* PopupImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PopupImpl.h; sourceTree = ""; }; + 1839122E037567BDD1D09DEB /* WindowProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowProtocol.h; sourceTree = ""; }; + 1839132D0E2454D911F1D1F9 /* AvnView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnView.mm; sourceTree = ""; }; + 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IWindowStateChanged.h; sourceTree = ""; }; + 1839155B28B20FFB672D29C6 /* AvnWindow.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnWindow.mm; sourceTree = ""; }; + 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowBaseImpl.h; sourceTree = ""; }; + 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoFitContentView.h; sourceTree = ""; }; + 1839166350F32661F3ABD70F /* AutoFitContentView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AutoFitContentView.mm; sourceTree = ""; }; + 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowBaseImpl.mm; sourceTree = ""; }; + 1839171D898F9BFC1373631A /* ResizeScope.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResizeScope.h; sourceTree = ""; }; + 18391884C7476DA4E53A492D /* AvnPanelWindow.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnPanelWindow.mm; sourceTree = ""; }; + 183919BF108EB72A029F7671 /* WindowImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowImpl.mm; sourceTree = ""; }; + 18391BB698579F40F1783F31 /* PopupImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PopupImpl.mm; sourceTree = ""; }; + 18391BBB7782C296D424071F /* INSWindowHolder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = INSWindowHolder.h; sourceTree = ""; }; + 18391CD090AA776E7E841AC9 /* WindowImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowImpl.h; sourceTree = ""; }; + 18391D1669284AD2EC9E866A /* AvnView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvnView.h; sourceTree = ""; }; + 18391DB45C7D892E61BF388C /* WindowInterfaces.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowInterfaces.h; sourceTree = ""; }; + 18391E45702740FE9DD69695 /* ResizeScope.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ResizeScope.mm; sourceTree = ""; }; 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; }; 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = rendertarget.mm; sourceTree = ""; }; @@ -46,7 +83,6 @@ 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; 37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = ""; }; 37C09D8721580FE4006A6758 /* SystemDialogs.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SystemDialogs.mm; sourceTree = ""; }; - 37C09D8A21581EF2006A6758 /* window.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = window.h; sourceTree = ""; }; 37DDA9AF219330F8002E132B /* AvnString.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnString.mm; sourceTree = ""; }; 37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = ""; }; 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; }; @@ -60,10 +96,11 @@ AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; AB1E522B217613570091CD71 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; AB661C1D2148230F00291242 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; - AB661C1F2148286E00291242 /* window.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = window.mm; sourceTree = ""; }; 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 +134,8 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + BC11A5BC2608D58F0017BAD0 /* automation.h */, + BC11A5BD2608D58F0017BAD0 /* automation.mm */, 1A1852DB23E05814008F0DED /* deadlock.mm */, 1A002B9D232135EE00021753 /* app.mm */, 37DDA9B121933371002E132B /* AvnString.h */, @@ -112,8 +151,6 @@ AB661C212148288600291242 /* common.h */, 379860FE214DA0C000CD0246 /* KeyTransform.h */, 37E2330E21583241000CB7E2 /* KeyTransform.mm */, - AB661C1F2148286E00291242 /* window.mm */, - 37C09D8A21581EF2006A6758 /* window.h */, AB00E4F62147CA920032A60A /* main.mm */, 37155CE3233C00EB0034DCE9 /* menu.h */, 520624B222973F4100C4DCEF /* menu.mm */, @@ -124,6 +161,24 @@ 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, AB7A61F02147C815003C5833 /* Products */, AB661C1C2148230E00291242 /* Frameworks */, + 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */, + 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */, + 18391BBB7782C296D424071F /* INSWindowHolder.h */, + 183919BF108EB72A029F7671 /* WindowImpl.mm */, + 18391CD090AA776E7E841AC9 /* WindowImpl.h */, + 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */, + 18391E45702740FE9DD69695 /* ResizeScope.mm */, + 1839171D898F9BFC1373631A /* ResizeScope.h */, + 1839132D0E2454D911F1D1F9 /* AvnView.mm */, + 18391D1669284AD2EC9E866A /* AvnView.h */, + 1839166350F32661F3ABD70F /* AutoFitContentView.mm */, + 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */, + 18391884C7476DA4E53A492D /* AvnPanelWindow.mm */, + 1839122E037567BDD1D09DEB /* WindowProtocol.h */, + 1839155B28B20FFB672D29C6 /* AvnWindow.mm */, + 18391DB45C7D892E61BF388C /* WindowInterfaces.h */, + 18391BB698579F40F1783F31 /* PopupImpl.mm */, + 183910513F396141938832B5 /* PopupImpl.h */, ); sourceTree = ""; }; @@ -143,6 +198,17 @@ buildActionMask = 2147483647; files = ( 37155CE4233C00EB0034DCE9 /* menu.h in Headers */, + BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */, + 183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */, + 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */, + 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */, + 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */, + 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */, + 18391ED5F611FF62C45F196D /* AvnView.h in Headers */, + 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */, + 18391F1E2411C79405A9943A /* WindowProtocol.h in Headers */, + 183914E50CF6D2EFC1667F7C /* WindowInterfaces.h in Headers */, + 18391AC65ADD7DDD33FBE737 /* PopupImpl.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -213,6 +279,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 */, @@ -220,7 +287,14 @@ 1A465D10246AB61600C5858B /* dnd.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, - AB661C202148286E00291242 /* window.mm in Sources */, + 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */, + 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */, + 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */, + 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */, + 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */, + 1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */, + 18391AC16726CBC45856233B /* AvnWindow.mm in Sources */, + 18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme index 5d20a135b9..87a8312c38 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme @@ -56,10 +56,14 @@ + + * 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/AvnView.h b/native/Avalonia.Native/src/OSX/AvnView.h new file mode 100644 index 0000000000..86a68d34c5 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnView.h @@ -0,0 +1,27 @@ +// +// Created by Dan Walmsley on 05/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// +#pragma once +#import + + +#import +#import +#include "common.h" +#include "WindowImpl.h" +#include "KeyTransform.h" + +@class AvnAccessibilityElement; + +@interface AvnView : NSView +-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; +-(NSEvent* _Nonnull) lastMouseDownEvent; +-(AvnPoint) translateLocalPoint:(AvnPoint)pt; +-(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; +-(void) onClosed; + +-(AvnPlatformResizeReason) getResizeReason; +-(void) setResizeReason:(AvnPlatformResizeReason)reason; ++ (AvnPoint)toAvnPoint:(CGPoint)p; +@end \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm new file mode 100644 index 0000000000..02526afbcb --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -0,0 +1,712 @@ +// +// Created by Dan Walmsley on 05/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#include "AvnView.h" +#include "automation.h" +#import "WindowInterfaces.h" + +@implementation AvnView +{ + ComPtr _parent; + NSTrackingArea* _area; + bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; + AvnInputModifiers _modifierState; + NSEvent* _lastMouseDownEvent; + bool _lastKeyHandled; + AvnPixelSize _lastPixelSize; + NSObject* _renderTarget; + AvnPlatformResizeReason _resizeReason; + AvnAccessibilityElement* _accessibilityChild; +} + +- (void)onClosed +{ + @synchronized (self) + { + _parent = nullptr; + } +} + +- (NSEvent*) lastMouseDownEvent +{ + return _lastMouseDownEvent; +} + +- (void) updateRenderTarget +{ + [_renderTarget resize:_lastPixelSize withScale:static_cast([[self window] backingScaleFactor])]; + [self setNeedsDisplayInRect:[self frame]]; +} + +-(AvnView*) initWithParent: (WindowBaseImpl*) parent +{ + self = [super init]; + _renderTarget = parent->renderTarget; + [self setWantsLayer:YES]; + [self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize]; + + _parent = parent; + _area = nullptr; + _lastPixelSize.Height = 100; + _lastPixelSize.Width = 100; + [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; + + _modifierState = AvnInputModifiersNone; + return self; +} + +- (BOOL)isFlipped +{ + return YES; +} + +- (BOOL)wantsUpdateLayer +{ + return YES; +} + +- (void)setLayer:(CALayer *)layer +{ + [_renderTarget setNewLayer: layer]; + [super setLayer: layer]; +} + +- (BOOL)isOpaque +{ + return YES; +} + +- (BOOL)acceptsFirstResponder +{ + return true; +} + +- (BOOL)acceptsFirstMouse:(NSEvent *)event +{ + return true; +} + +- (BOOL)canBecomeKeyView +{ + return true; +} + +-(void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + + if(_area != nullptr) + { + [self removeTrackingArea:_area]; + _area = nullptr; + } + + if (_parent == nullptr) + { + return; + } + + NSRect rect = NSZeroRect; + rect.size = newSize; + + NSTrackingAreaOptions options = NSTrackingActiveAlways | NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingEnabledDuringMouseDrag; + _area = [[NSTrackingArea alloc] initWithRect:rect options:options owner:self userInfo:nullptr]; + [self addTrackingArea:_area]; + + _parent->UpdateCursor(); + + auto fsize = [self convertSizeToBacking: [self frame].size]; + + if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height) + { + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + [self updateRenderTarget]; + + auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; + _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); + } +} + +- (void)updateLayer +{ + AvnInsidePotentialDeadlock deadlock; + if (_parent == nullptr) + { + return; + } + + _parent->BaseEvents->RunRenderPriorityJobs(); + + if (_parent == nullptr) + { + return; + } + + _parent->BaseEvents->Paint(); +} + +- (void)drawRect:(NSRect)dirtyRect +{ + return; +} + +-(void) setSwRenderedFrame: (AvnFramebuffer*) fb dispose: (IUnknown*) dispose +{ + @autoreleasepool { + [_renderTarget setSwFrame:fb]; + dispose->Release(); + } +} + +- (AvnPoint) translateLocalPoint:(AvnPoint)pt +{ + pt.Y = [self bounds].size.height - pt.Y; + return pt; +} + ++ (AvnPoint)toAvnPoint:(CGPoint)p +{ + AvnPoint result; + + result.X = p.x; + result.Y = p.y; + + return result; +} + +- (void) viewDidChangeBackingProperties +{ + auto fsize = [self convertSizeToBacking: [self frame].size]; + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + [self updateRenderTarget]; + + if(_parent != nullptr) + { + _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + } + + [super viewDidChangeBackingProperties]; +} + +- (bool) ignoreUserInput:(bool)trigerInputWhenDisabled +{ + if(_parent == nullptr) + { + return TRUE; + } + + auto parentWindow = _parent->GetWindowProtocol(); + + if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) + { + if(trigerInputWhenDisabled) + { + auto window = dynamic_cast(_parent.getRaw()); + + if(window != nullptr) + { + window->WindowEvents->GotInputWhenDisabled(); + } + } + + return TRUE; + } + + return FALSE; +} + +- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type +{ + bool triggerInputWhenDisabled = type != Move; + + if([self ignoreUserInput: triggerInputWhenDisabled]) + { + return; + } + + auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; + auto avnPoint = [AvnView toAvnPoint:localPoint]; + auto point = [self translateLocalPoint:avnPoint]; + AvnVector delta = { 0, 0}; + + if(type == Wheel) + { + auto speed = 5; + + if([event hasPreciseScrollingDeltas]) + { + speed = 50; + } + + delta.X = [event scrollingDeltaX] / speed; + delta.Y = [event scrollingDeltaY] / speed; + + if(delta.X == 0 && delta.Y == 0) + { + return; + } + } + else if (type == Magnify) + { + delta.X = delta.Y = [event magnification]; + } + else if (type == Rotate) + { + delta.X = delta.Y = [event rotation]; + } + else if (type == Swipe) + { + delta.X = [event deltaX]; + delta.Y = [event deltaY]; + } + + uint32 timestamp = static_cast([event timestamp] * 1000); + auto modifiers = [self getModifiers:[event modifierFlags]]; + + if(type != Move || + ( + [self window] != nil && + ( + [[self window] firstResponder] == nil + || ![[[self window] firstResponder] isKindOfClass: [NSView class]] + ) + ) + ) + [self becomeFirstResponder]; + + if(_parent != nullptr) + { + _parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta); + } + + [super mouseMoved:event]; +} + +- (BOOL) resignFirstResponder +{ + _parent->BaseEvents->LostFocus(); + return YES; +} + +- (void)mouseMoved:(NSEvent *)event +{ + [self mouseEvent:event withType:Move]; +} + +- (void)mouseDown:(NSEvent *)event +{ + _isLeftPressed = true; + _lastMouseDownEvent = event; + [self mouseEvent:event withType:LeftButtonDown]; +} + +- (void)otherMouseDown:(NSEvent *)event +{ + _lastMouseDownEvent = event; + + switch(event.buttonNumber) + { + case 2: + case 3: + _isMiddlePressed = true; + [self mouseEvent:event withType:MiddleButtonDown]; + break; + case 4: + _isXButton1Pressed = true; + [self mouseEvent:event withType:XButton1Down]; + break; + case 5: + _isXButton2Pressed = true; + [self mouseEvent:event withType:XButton2Down]; + break; + + default: + break; + } +} + +- (void)rightMouseDown:(NSEvent *)event +{ + _isRightPressed = true; + _lastMouseDownEvent = event; + [self mouseEvent:event withType:RightButtonDown]; +} + +- (void)mouseUp:(NSEvent *)event +{ + _isLeftPressed = false; + [self mouseEvent:event withType:LeftButtonUp]; +} + +- (void)otherMouseUp:(NSEvent *)event +{ + switch(event.buttonNumber) + { + case 2: + case 3: + _isMiddlePressed = false; + [self mouseEvent:event withType:MiddleButtonUp]; + break; + case 4: + _isXButton1Pressed = false; + [self mouseEvent:event withType:XButton1Up]; + break; + case 5: + _isXButton2Pressed = false; + [self mouseEvent:event withType:XButton2Up]; + break; + + default: + break; + } +} + +- (void)rightMouseUp:(NSEvent *)event +{ + _isRightPressed = false; + [self mouseEvent:event withType:RightButtonUp]; +} + +- (void)mouseDragged:(NSEvent *)event +{ + [self mouseEvent:event withType:Move]; + [super mouseDragged:event]; +} + +- (void)otherMouseDragged:(NSEvent *)event +{ + [self mouseEvent:event withType:Move]; + [super otherMouseDragged:event]; +} + +- (void)rightMouseDragged:(NSEvent *)event +{ + [self mouseEvent:event withType:Move]; + [super rightMouseDragged:event]; +} + +- (void)scrollWheel:(NSEvent *)event +{ + [self mouseEvent:event withType:Wheel]; + [super scrollWheel:event]; +} + +- (void)magnifyWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Magnify]; + [super magnifyWithEvent:event]; +} + +- (void)rotateWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Rotate]; + [super rotateWithEvent:event]; +} + +- (void)swipeWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Swipe]; + [super swipeWithEvent:event]; +} + +- (void)mouseEntered:(NSEvent *)event +{ + [super mouseEntered:event]; +} + +- (void)mouseExited:(NSEvent *)event +{ + [self mouseEvent:event withType:LeaveWindow]; + [super mouseExited:event]; +} + +- (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type +{ + if([self ignoreUserInput: false]) + { + return; + } + + auto key = s_KeyMap[[event keyCode]]; + + uint32_t timestamp = static_cast([event timestamp] * 1000); + auto modifiers = [self getModifiers:[event modifierFlags]]; + + if(_parent != nullptr) + { + _lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key); + } +} + +- (BOOL)performKeyEquivalent:(NSEvent *)event +{ + bool result = _lastKeyHandled; + + _lastKeyHandled = false; + + return result; +} + +- (void)flagsChanged:(NSEvent *)event +{ + auto newModifierState = [self getModifiers:[event modifierFlags]]; + + bool isAltCurrentlyPressed = (_modifierState & Alt) == Alt; + bool isControlCurrentlyPressed = (_modifierState & Control) == Control; + bool isShiftCurrentlyPressed = (_modifierState & Shift) == Shift; + bool isCommandCurrentlyPressed = (_modifierState & Windows) == Windows; + + bool isAltPressed = (newModifierState & Alt) == Alt; + bool isControlPressed = (newModifierState & Control) == Control; + bool isShiftPressed = (newModifierState & Shift) == Shift; + bool isCommandPressed = (newModifierState & Windows) == Windows; + + + if (isAltPressed && !isAltCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if (isAltCurrentlyPressed && !isAltPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + if (isControlPressed && !isControlCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if (isControlCurrentlyPressed && !isControlPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + if (isShiftPressed && !isShiftCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if(isShiftCurrentlyPressed && !isShiftPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + if(isCommandPressed && !isCommandCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if(isCommandCurrentlyPressed && ! isCommandPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + _modifierState = newModifierState; + + [[self inputContext] handleEvent:event]; + [super flagsChanged:event]; +} + +- (void)keyDown:(NSEvent *)event +{ + [self keyboardEvent:event withType:KeyDown]; + [[self inputContext] handleEvent:event]; + [super keyDown:event]; +} + +- (void)keyUp:(NSEvent *)event +{ + [self keyboardEvent:event withType:KeyUp]; + [super keyUp:event]; +} + +- (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod +{ + unsigned int rv = 0; + + if (mod & NSEventModifierFlagControl) + rv |= Control; + if (mod & NSEventModifierFlagShift) + rv |= Shift; + if (mod & NSEventModifierFlagOption) + rv |= Alt; + if (mod & NSEventModifierFlagCommand) + rv |= Windows; + + if (_isLeftPressed) + rv |= LeftMouseButton; + if (_isMiddlePressed) + rv |= MiddleMouseButton; + if (_isRightPressed) + rv |= RightMouseButton; + if (_isXButton1Pressed) + rv |= XButton1MouseButton; + if (_isXButton2Pressed) + rv |= XButton2MouseButton; + + return (AvnInputModifiers)rv; +} + +- (BOOL)hasMarkedText +{ + return _lastKeyHandled; +} + +- (NSRange)markedRange +{ + return NSMakeRange(NSNotFound, 0); +} + +- (NSRange)selectedRange +{ + return NSMakeRange(NSNotFound, 0); +} + +- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange +{ + +} + +- (void)unmarkText +{ + +} + +- (NSArray *)validAttributesForMarkedText +{ + return [NSArray new]; +} + +- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange +{ + return [NSAttributedString new]; +} + +- (void)insertText:(id)string replacementRange:(NSRange)replacementRange +{ + if(!_lastKeyHandled) + { + if(_parent != nullptr) + { + _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); + } + } +} + +- (NSUInteger)characterIndexForPoint:(NSPoint)point +{ + return 0; +} + +- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange +{ + CGRect result = { 0 }; + + return result; +} + +- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info +{ + auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; + auto avnPoint = [AvnView toAvnPoint:localPoint]; + auto point = [self translateLocalPoint:avnPoint]; + auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; + NSDragOperation nsop = [info draggingSourceOperationMask]; + + auto effects = ConvertDragDropEffects(nsop); + int reffects = (int)_parent->BaseEvents + ->DragEvent(type, point, modifiers, effects, + CreateClipboard([info draggingPasteboard], nil), + GetAvnDataObjectHandleFromDraggingInfo(info)); + + NSDragOperation ret = static_cast(0); + + // Ensure that the managed part didn't add any new effects + reffects = (int)effects & reffects; + + // OSX requires exactly one operation + if((reffects & (int)AvnDragDropEffects::Copy) != 0) + ret = NSDragOperationCopy; + else if((reffects & (int)AvnDragDropEffects::Move) != 0) + ret = NSDragOperationMove; + else if((reffects & (int)AvnDragDropEffects::Link) != 0) + ret = NSDragOperationLink; + if(ret == 0) + ret = NSDragOperationNone; + return ret; +} + +- (NSDragOperation)draggingEntered:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender]; +} + +- (NSDragOperation)draggingUpdated:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender]; +} + +- (void)draggingExited:(id )sender +{ + [self triggerAvnDragEvent: AvnDragEventType::Leave info:sender]; +} + +- (BOOL)prepareForDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone; +} + +- (BOOL)performDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone; +} + +- (void)concludeDragOperation:(nullable id )sender +{ + +} + +- (AvnPlatformResizeReason)getResizeReason +{ + return _resizeReason; +} + +- (void)setResizeReason:(AvnPlatformResizeReason)reason +{ + _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 \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm new file mode 100644 index 0000000000..f51c693777 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -0,0 +1,466 @@ +// +// Created by Dan Walmsley on 06/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + + +#import +#import "WindowProtocol.h" +#import "WindowBaseImpl.h" + +#ifdef IS_NSPANEL +#define BASE_CLASS NSPanel +#define CLASS_NAME AvnPanel +#else +#define BASE_CLASS NSWindow +#define CLASS_NAME AvnWindow +#endif + +#import +#include "common.h" +#include "menu.h" +#include "automation.h" +#include "WindowBaseImpl.h" +#include "WindowImpl.h" +#include "AvnView.h" +#include "WindowInterfaces.h" +#include "PopupImpl.h" + +@implementation CLASS_NAME +{ + ComPtr _parent; + bool _closed; + bool _isEnabled; + bool _canBecomeKeyWindow; + bool _isExtended; + AvnMenu* _menu; +} + +-(void) setIsExtended:(bool)value; +{ + _isExtended = value; +} + +-(bool) isDialog +{ + return _parent->IsDialog(); +} + +-(double) getExtendedTitleBarHeight +{ + if(_isExtended) + { + for (id subview in self.contentView.superview.subviews) + { + if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) + { + NSView *titlebarView = [subview subviews][0]; + + return (double)titlebarView.frame.size.height; + } + } + + return -1; + } + else + { + return 0; + } +} + +- (void)performClose:(id)sender +{ + if([[self delegate] respondsToSelector:@selector(windowShouldClose:)]) + { + if(![[self delegate] windowShouldClose:self]) return; + } + else if([self respondsToSelector:@selector(windowShouldClose:)]) + { + if(![self windowShouldClose:self]) return; + } + + [self close]; +} + +- (void)pollModalSession:(nonnull NSModalSession)session +{ + auto response = [NSApp runModalSession:session]; + + if(response == NSModalResponseContinue) + { + dispatch_async(dispatch_get_main_queue(), ^{ + [self pollModalSession:session]; + }); + } + else if (!_closed) + { + [self orderOut:self]; + [NSApp endModalSession:session]; + } +} + +-(void) showWindowMenuWithAppMenu +{ + if(_menu != nullptr) + { + auto appMenuItem = ::GetAppMenuItem(); + + if(appMenuItem != nullptr) + { + auto appMenu = [appMenuItem menu]; + + [appMenu removeItem:appMenuItem]; + + [_menu insertItem:appMenuItem atIndex:0]; + + [_menu setHasGlobalMenuItem:true]; + } + + [NSApp setMenu:_menu]; + } + else + { + [self showAppMenuOnly]; + } +} + +-(void) showAppMenuOnly +{ + auto appMenuItem = ::GetAppMenuItem(); + + if(appMenuItem != nullptr) + { + auto appMenu = ::GetAppMenu(); + + auto nativeAppMenu = dynamic_cast(appMenu); + + [[appMenuItem menu] removeItem:appMenuItem]; + + if(_menu != nullptr) + { + [_menu setHasGlobalMenuItem:false]; + } + + [nativeAppMenu->GetNative() addItem:appMenuItem]; + + [NSApp setMenu:nativeAppMenu->GetNative()]; + } +} + +-(void) applyMenu:(AvnMenu *)menu +{ + if(menu == nullptr) + { + menu = [AvnMenu new]; + } + + _menu = menu; +} + +-(CLASS_NAME*) initWithParent: (WindowBaseImpl*) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; +{ + // https://jameshfisher.com/2020/07/10/why-is-the-contentrect-of-my-nswindow-ignored/ + // create nswindow with specific contentRect, otherwise we wont be able to resize the window + // until several ms after the window is physically on the screen. + self = [super initWithContentRect:contentRect styleMask: styleMask backing:NSBackingStoreBuffered defer:false]; + + [self setReleasedWhenClosed:false]; + _parent = parent; + [self setDelegate:self]; + _closed = false; + _isEnabled = true; + + [self backingScaleFactor]; + [self setOpaque:NO]; + [self setBackgroundColor: [NSColor clearColor]]; + + _isExtended = false; + +#ifdef IS_NSPANEL + [self setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces|NSWindowCollectionBehaviorFullScreenAuxiliary]; +#endif + + return self; +} + +- (BOOL)windowShouldClose:(NSWindow *)sender +{ + auto window = dynamic_cast(_parent.getRaw()); + + if(window != nullptr) + { + return !window->WindowEvents->Closing(); + } + + return true; +} + +- (void)windowDidChangeBackingProperties:(NSNotification *)notification +{ + [self backingScaleFactor]; +} + + + +- (void)windowWillClose:(NSNotification *)notification +{ + _closed = true; + if(_parent) + { + ComPtr parent = _parent; + _parent = NULL; + [self restoreParentWindow]; + parent->BaseEvents->Closed(); + [parent->View onClosed]; + } +} + +-(BOOL)canBecomeKeyWindow +{ + if(_canBecomeKeyWindow) + { + // If the window has a child window being shown as a dialog then don't allow it to become the key window. + for(NSWindow* uch in [self childWindows]) + { + if (![uch conformsToProtocol:@protocol(AvnWindowProtocol)]) + { + continue; + } + + id ch = (id ) uch; + + if(ch.isDialog) + return false; + } + + return true; + } + + return false; +} + +#ifndef IS_NSPANEL +-(BOOL)canBecomeMainWindow +{ + return true; +} +#endif + +-(void)setCanBecomeKeyWindow:(bool)value +{ + _canBecomeKeyWindow = value; +} + +-(bool)shouldTryToHandleEvents +{ + return _isEnabled; +} + +-(void) setEnabled:(bool)enable +{ + _isEnabled = enable; +} + +-(void)becomeKeyWindow +{ + [self showWindowMenuWithAppMenu]; + + if(_parent != nullptr) + { + _parent->BaseEvents->Activated(); + } + + [super becomeKeyWindow]; +} + +-(void) restoreParentWindow; +{ + auto parent = [self parentWindow]; + + if(parent != nil) + { + [parent removeChildWindow:self]; + } +} + +- (void)windowDidMiniaturize:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->WindowStateChanged(); + } +} + +- (void)windowDidDeminiaturize:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->WindowStateChanged(); + } +} + +- (void)windowDidResize:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->WindowStateChanged(); + } +} + +- (void)windowWillExitFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->StartStateTransition(); + } +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->EndStateTransition(); + + if(parent->Decorations() != SystemDecorationsFull && parent->WindowState() == Maximized) + { + NSRect screenRect = [[self screen] visibleFrame]; + [self setFrame:screenRect display:YES]; + } + + if(parent->WindowState() == Minimized) + { + [self miniaturize:nullptr]; + } + + parent->WindowStateChanged(); + } +} + +- (void)windowWillEnterFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->StartStateTransition(); + } +} + +- (void)windowDidEnterFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->EndStateTransition(); + parent->WindowStateChanged(); + } +} + +- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame +{ + return true; +} + +-(void)resignKeyWindow +{ + if(_parent) + _parent->BaseEvents->Deactivated(); + + [self showAppMenuOnly]; + + [super resignKeyWindow]; +} + +- (void)windowDidMove:(NSNotification *)notification +{ + AvnPoint position; + + if(_parent != nullptr) + { + auto cparent = dynamic_cast(_parent.getRaw()); + + if(cparent != nullptr) + { + if(!cparent->IsShown()) + { + return; + } + + if(cparent->WindowState() == Maximized) + { + cparent->SetWindowState(Normal); + } + } + + _parent->GetPosition(&position); + _parent->BaseEvents->PositionChanged(position); + } +} + +- (AvnPoint) translateLocalPoint:(AvnPoint)pt +{ + pt.Y = [self frame].size.height - pt.Y; + return pt; +} + +- (void)sendEvent:(NSEvent *)event +{ + [super sendEvent:event]; + + /// This is to detect non-client clicks. This can only be done on Windows... not popups, hence the dynamic_cast. + if(_parent != nullptr && dynamic_cast(_parent.getRaw()) != nullptr) + { + switch(event.type) + { + case NSEventTypeLeftMouseDown: + { + AvnView* view = _parent->View; + NSPoint windowPoint = [event locationInWindow]; + NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil]; + + if (!NSPointInRect(viewPoint, view.bounds)) + { + auto avnPoint = [AvnView toAvnPoint:windowPoint]; + auto point = [self translateLocalPoint:avnPoint]; + AvnVector delta = { 0, 0 }; + + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, static_cast([event timestamp] * 1000), AvnInputModifiersNone, point, delta); + } + } + break; + + case NSEventTypeMouseEntered: + { + _parent->UpdateCursor(); + } + break; + + case NSEventTypeMouseExited: + { + [[NSCursor arrowCursor] set]; + } + break; + + default: + break; + } + } +} + +- (void)disconnectParent { + _parent = nullptr; +} + +@end + diff --git a/native/Avalonia.Native/src/OSX/INSWindowHolder.h b/native/Avalonia.Native/src/OSX/INSWindowHolder.h new file mode 100644 index 0000000000..ae64a53e7d --- /dev/null +++ b/native/Avalonia.Native/src/OSX/INSWindowHolder.h @@ -0,0 +1,17 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H +#define AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H + +@class AvnView; + +struct INSWindowHolder +{ + virtual NSWindow* _Nonnull GetNSWindow () = 0; + virtual NSView* _Nonnull GetNSView () = 0; +}; + +#endif //AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H diff --git a/native/Avalonia.Native/src/OSX/IWindowStateChanged.h b/native/Avalonia.Native/src/OSX/IWindowStateChanged.h new file mode 100644 index 0000000000..f0905da3ac --- /dev/null +++ b/native/Avalonia.Native/src/OSX/IWindowStateChanged.h @@ -0,0 +1,18 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_IWINDOWSTATECHANGED_H +#define AVALONIA_NATIVE_OSX_IWINDOWSTATECHANGED_H + +struct IWindowStateChanged +{ + virtual void WindowStateChanged () = 0; + virtual void StartStateTransition () = 0; + virtual void EndStateTransition () = 0; + virtual SystemDecorations Decorations () = 0; + virtual AvnWindowState WindowState () = 0; +}; + +#endif //AVALONIA_NATIVE_OSX_IWINDOWSTATECHANGED_H diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.h b/native/Avalonia.Native/src/OSX/PopupImpl.h new file mode 100644 index 0000000000..451019a6a4 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/PopupImpl.h @@ -0,0 +1,9 @@ +// +// Created by Dan Walmsley on 06/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_POPUPIMPL_H +#define AVALONIA_NATIVE_OSX_POPUPIMPL_H + +#endif //AVALONIA_NATIVE_OSX_POPUPIMPL_H diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.mm b/native/Avalonia.Native/src/OSX/PopupImpl.mm new file mode 100644 index 0000000000..3c5afd9424 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/PopupImpl.mm @@ -0,0 +1,61 @@ +// +// Created by Dan Walmsley on 06/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#include "WindowInterfaces.h" +#include "AvnView.h" +#include "WindowImpl.h" +#include "automation.h" +#include "menu.h" +#include "common.h" +#import "WindowBaseImpl.h" +#import "WindowProtocol.h" +#import +#include "PopupImpl.h" + +class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup +{ +private: + BEGIN_INTERFACE_MAP() + INHERIT_INTERFACE_MAP(WindowBaseImpl) + INTERFACE_MAP_ENTRY(IAvnPopup, IID_IAvnPopup) + END_INTERFACE_MAP() + virtual ~PopupImpl(){} + ComPtr WindowEvents; + PopupImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) + { + WindowEvents = events; + } +protected: + virtual NSWindowStyleMask GetStyle() override + { + return NSWindowStyleMaskBorderless; + } + + virtual void OnInitialiseNSWindow () override + { + [Window setLevel:NSPopUpMenuWindowLevel]; + } + +public: + virtual bool ShouldTakeFocusOnShow() override + { + return false; + } + + virtual HRESULT Show(bool activate, bool isDialog) override + { + return WindowBaseImpl::Show(activate, true); + } +}; + + +extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl) +{ + @autoreleasepool + { + IAvnPopup* ptr = dynamic_cast(new PopupImpl(events, gl)); + return ptr; + } +} diff --git a/native/Avalonia.Native/src/OSX/ResizeScope.h b/native/Avalonia.Native/src/OSX/ResizeScope.h new file mode 100644 index 0000000000..9a43c158fe --- /dev/null +++ b/native/Avalonia.Native/src/OSX/ResizeScope.h @@ -0,0 +1,24 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_RESIZESCOPE_H +#define AVALONIA_NATIVE_OSX_RESIZESCOPE_H + +#include "avalonia-native.h" + +@class AvnView; + +class ResizeScope +{ +public: + ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason); + + ~ResizeScope(); +private: + AvnView* _Nonnull _view; + AvnPlatformResizeReason _restore; +}; + +#endif //AVALONIA_NATIVE_OSX_RESIZESCOPE_H diff --git a/native/Avalonia.Native/src/OSX/ResizeScope.mm b/native/Avalonia.Native/src/OSX/ResizeScope.mm new file mode 100644 index 0000000000..9f1177af8b --- /dev/null +++ b/native/Avalonia.Native/src/OSX/ResizeScope.mm @@ -0,0 +1,18 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#include "ResizeScope.h" +#include "AvnView.h" + +ResizeScope::ResizeScope(AvnView *view, AvnPlatformResizeReason reason) { + _view = view; + _restore = [view getResizeReason]; + [view setResizeReason:reason]; +} + +ResizeScope::~ResizeScope() { + [_view setResizeReason:_restore]; +} diff --git a/native/Avalonia.Native/src/OSX/SystemDialogs.mm b/native/Avalonia.Native/src/OSX/SystemDialogs.mm index a47221056b..535b6c3b66 100644 --- a/native/Avalonia.Native/src/OSX/SystemDialogs.mm +++ b/native/Avalonia.Native/src/OSX/SystemDialogs.mm @@ -1,5 +1,5 @@ #include "common.h" -#include "window.h" +#include "INSWindowHolder.h" class SystemDialogs : public ComSingleObject { diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h new file mode 100644 index 0000000000..83850e780c --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -0,0 +1,136 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H +#define AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H + +#include "rendertarget.h" +#include "INSWindowHolder.h" + +@class AutoFitContentView; +@class AvnMenu; +@protocol AvnWindowProtocol; + +class WindowBaseImpl : public virtual ComObject, + public virtual IAvnWindowBase, + public INSWindowHolder { + +public: + FORWARD_IUNKNOWN() + +BEGIN_INTERFACE_MAP() + INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase) + END_INTERFACE_MAP() + + virtual ~WindowBaseImpl(); + + WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl); + + virtual HRESULT ObtainNSWindowHandle(void **ret) override; + + virtual HRESULT ObtainNSWindowHandleRetained(void **ret) override; + + virtual HRESULT ObtainNSViewHandle(void **ret) override; + + virtual HRESULT ObtainNSViewHandleRetained(void **ret) override; + + virtual NSWindow *GetNSWindow() override; + + virtual NSView *GetNSView() override; + + virtual HRESULT Show(bool activate, bool isDialog) override; + + virtual bool IsShown (); + + virtual bool ShouldTakeFocusOnShow(); + + virtual HRESULT Hide() override; + + virtual HRESULT Activate() override; + + virtual HRESULT SetTopMost(bool value) override; + + virtual HRESULT Close() override; + + virtual HRESULT GetClientSize(AvnSize *ret) override; + + virtual HRESULT GetFrameSize(AvnSize *ret) override; + + virtual HRESULT GetScaling(double *ret) override; + + virtual HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize) override; + + virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override; + + virtual HRESULT Invalidate(__attribute__((unused)) AvnRect rect) override; + + virtual HRESULT SetMainMenu(IAvnMenu *menu) override; + + virtual HRESULT BeginMoveDrag() override; + + virtual HRESULT BeginResizeDrag(__attribute__((unused)) AvnWindowEdge edge) override; + + virtual HRESULT GetPosition(AvnPoint *ret) override; + + virtual HRESULT SetPosition(AvnPoint point) override; + + virtual HRESULT PointToClient(AvnPoint point, AvnPoint *ret) override; + + virtual HRESULT PointToScreen(AvnPoint point, AvnPoint *ret) override; + + virtual HRESULT ThreadSafeSetSwRenderedFrame(AvnFramebuffer *fb, IUnknown *dispose) override; + + virtual HRESULT SetCursor(IAvnCursor *cursor) override; + + virtual void UpdateCursor(); + + virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget **ppv) override; + + virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost **retOut) override; + + virtual HRESULT SetBlurEnabled(bool enable) override; + + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, + IAvnClipboard *clipboard, IAvnDndResultCallback *cb, + void *sourceHandle) override; + + virtual bool IsDialog(); + + id GetWindowProtocol (); + +protected: + virtual NSWindowStyleMask GetStyle(); + + void UpdateStyle(); + + virtual void OnInitialiseNSWindow (); + +private: + void CreateNSWindow (bool isDialog); + void CleanNSWindow (); + void InitialiseNSWindow (); + + NSCursor *cursor; + ComPtr _glContext; + bool hasPosition; + NSSize lastSize; + NSSize lastMinSize; + NSSize lastMaxSize; + AvnMenu* lastMenu; + bool _inResize; + +protected: + AvnPoint lastPositionSet; + AutoFitContentView *StandardContainer; + bool _shown; + +public: + NSObject *renderTarget; + NSWindow * Window; + ComPtr BaseEvents; + AvnView *View; +}; + +#endif //AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm new file mode 100644 index 0000000000..6dc59ae4d8 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -0,0 +1,618 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#include "common.h" +#include "AvnView.h" +#include "menu.h" +#include "automation.h" +#include "cursor.h" +#include "ResizeScope.h" +#include "AutoFitContentView.h" +#import "WindowProtocol.h" +#import "WindowInterfaces.h" +#include "WindowBaseImpl.h" + + +WindowBaseImpl::~WindowBaseImpl() { + View = nullptr; + Window = nullptr; +} + +WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) { + _shown = false; + _inResize = false; + BaseEvents = events; + _glContext = gl; + renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext:gl]; + View = [[AvnView alloc] initWithParent:this]; + StandardContainer = [[AutoFitContentView new] initWithContent:View]; + + lastPositionSet = { 0, 0 }; + hasPosition = false; + lastSize = NSSize { 100, 100 }; + lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX}; + lastMinSize = NSSize { 0, 0 }; + + Window = nullptr; + lastMenu = nullptr; +} + +HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge void *) View; + + return S_OK; +} + +HRESULT WindowBaseImpl::ObtainNSViewHandleRetained(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge_retained void *) View; + + return S_OK; +} + +NSWindow *WindowBaseImpl::GetNSWindow() { + return Window; +} + +NSView *WindowBaseImpl::GetNSView() { + return View; +} + +HRESULT WindowBaseImpl::ObtainNSWindowHandleRetained(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge_retained void *) Window; + + return S_OK; +} + +HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) { + START_COM_CALL; + + @autoreleasepool { + CreateNSWindow(isDialog); + InitialiseNSWindow(); + + if(hasPosition) + { + SetPosition(lastPositionSet); + } else + { + [Window center]; + } + + UpdateStyle(); + + if (ShouldTakeFocusOnShow() && activate) { + [Window orderFront:Window]; + [Window makeKeyAndOrderFront:Window]; + [Window makeFirstResponder:View]; + [NSApp activateIgnoringOtherApps:YES]; + } else { + [Window orderFront:Window]; + } + + _shown = true; + + return S_OK; + } +} + +bool WindowBaseImpl::IsShown () +{ + return _shown; +} + +bool WindowBaseImpl::ShouldTakeFocusOnShow() { + return true; +} + +HRESULT WindowBaseImpl::ObtainNSWindowHandle(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge void *) Window; + + return S_OK; +} + +HRESULT WindowBaseImpl::Hide() { + START_COM_CALL; + + @autoreleasepool { + if (Window != nullptr) { + [Window orderOut:Window]; + + [GetWindowProtocol() restoreParentWindow]; + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::Activate() { + START_COM_CALL; + + @autoreleasepool { + if (Window != nullptr) { + [Window makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; + } + } + + return S_OK; +} + +HRESULT WindowBaseImpl::SetTopMost(bool value) { + START_COM_CALL; + + @autoreleasepool { + [Window setLevel:value ? NSFloatingWindowLevel : NSNormalWindowLevel]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::Close() { + START_COM_CALL; + + @autoreleasepool { + if (Window != nullptr) { + auto window = Window; + Window = nullptr; + + try { + // Seems to throw sometimes on application exit. + [window close]; + } + catch (NSException *) {} + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::GetClientSize(AvnSize *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) + return E_POINTER; + + ret->Width = lastSize.width; + ret->Height = lastSize.height; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::GetFrameSize(AvnSize *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) + return E_POINTER; + + if(Window != nullptr){ + auto frame = [Window frame]; + ret->Width = frame.size.width; + ret->Height = frame.size.height; + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::GetScaling(double *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) + return E_POINTER; + + if (Window == nullptr) { + *ret = 1; + return S_OK; + } + + *ret = [Window backingScaleFactor]; + return S_OK; + } +} + +HRESULT WindowBaseImpl::SetMinMaxSize(AvnSize minSize, AvnSize maxSize) { + START_COM_CALL; + + @autoreleasepool { + lastMinSize = ToNSSize(minSize); + lastMaxSize = ToNSSize(maxSize); + + if(Window != nullptr) { + [Window setContentMinSize:lastMinSize]; + [Window setContentMaxSize:lastMaxSize]; + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reason) { + if (_inResize) { + return S_OK; + } + + _inResize = true; + + START_COM_CALL; + auto resizeBlock = ResizeScope(View, reason); + + @autoreleasepool { + auto maxSize = lastMaxSize; + auto minSize = lastMinSize; + + if (x < minSize.width) { + x = minSize.width; + } + + if (y < minSize.height) { + y = minSize.height; + } + + if (x > maxSize.width) { + x = maxSize.width; + } + + if (y > maxSize.height) { + y = maxSize.height; + } + + @try { + lastSize = NSSize {x, y}; + + if (!_shown) { + BaseEvents->Resized(AvnSize{x, y}, reason); + } + + if(Window != nullptr) { + [Window setContentSize:lastSize]; + [Window invalidateShadow]; + } + } + @finally { + _inResize = false; + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::Invalidate(__attribute__((unused)) AvnRect rect) { + START_COM_CALL; + + @autoreleasepool { + [View setNeedsDisplayInRect:[View frame]]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::SetMainMenu(IAvnMenu *menu) { + START_COM_CALL; + + auto nativeMenu = dynamic_cast(menu); + + lastMenu = nativeMenu->GetNative(); + + if(Window != nullptr) { + [GetWindowProtocol() applyMenu:lastMenu]; + + if ([Window isKeyWindow]) { + [GetWindowProtocol() showWindowMenuWithAppMenu]; + } + } + + return S_OK; +} + +HRESULT WindowBaseImpl::BeginMoveDrag() { + START_COM_CALL; + + @autoreleasepool { + auto lastEvent = [View lastMouseDownEvent]; + + if (lastEvent == nullptr) { + return S_OK; + } + + [Window performWindowDragWithEvent:lastEvent]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::BeginResizeDrag(__attribute__((unused)) AvnWindowEdge edge) { + START_COM_CALL; + + return S_OK; +} + +HRESULT WindowBaseImpl::GetPosition(AvnPoint *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + if(Window != nullptr) { + auto frame = [Window frame]; + + ret->X = frame.origin.x; + ret->Y = frame.origin.y + frame.size.height; + + *ret = ConvertPointY(*ret); + } else + { + *ret = lastPositionSet; + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::SetPosition(AvnPoint point) { + START_COM_CALL; + + @autoreleasepool { + lastPositionSet = point; + hasPosition = true; + + if(Window != nullptr) { + [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(point))]; + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::PointToClient(AvnPoint point, AvnPoint *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + point = ConvertPointY(point); + NSRect convertRect = [Window convertRectFromScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; + auto viewPoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y); + + *ret = [View translateLocalPoint:ToAvnPoint(viewPoint)]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::PointToScreen(AvnPoint point, AvnPoint *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + auto cocoaViewPoint = ToNSPoint([View translateLocalPoint:point]); + 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; + } +} + +HRESULT WindowBaseImpl::ThreadSafeSetSwRenderedFrame(AvnFramebuffer *fb, IUnknown *dispose) { + START_COM_CALL; + + [View setSwRenderedFrame:fb dispose:dispose]; + return S_OK; +} + +HRESULT WindowBaseImpl::SetCursor(IAvnCursor *cursor) { + START_COM_CALL; + + @autoreleasepool { + Cursor *avnCursor = dynamic_cast(cursor); + this->cursor = avnCursor->GetNative(); + UpdateCursor(); + + if (avnCursor->IsHidden()) { + [NSCursor hide]; + } else { + [NSCursor unhide]; + } + + return S_OK; + } +} + +void WindowBaseImpl::UpdateCursor() { + if (cursor != nil) { + [cursor set]; + } +} + +HRESULT WindowBaseImpl::CreateGlRenderTarget(IAvnGlSurfaceRenderTarget **ppv) { + START_COM_CALL; + + if (View == NULL) + return E_FAIL; + *ppv = [renderTarget createSurfaceRenderTarget]; + return static_cast(*ppv == nil ? E_FAIL : S_OK); +} + +HRESULT WindowBaseImpl::CreateNativeControlHost(IAvnNativeControlHost **retOut) { + START_COM_CALL; + + if (View == NULL) + return E_FAIL; + *retOut = ::CreateNativeControlHost(View); + return S_OK; +} + +HRESULT WindowBaseImpl::SetBlurEnabled(bool enable) { + START_COM_CALL; + + [StandardContainer ShowBlur:enable]; + + return S_OK; +} + +HRESULT WindowBaseImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard *clipboard, IAvnDndResultCallback *cb, void *sourceHandle) { + START_COM_CALL; + + auto item = TryGetPasteboardItem(clipboard); + [item setString:@"" forType:GetAvnCustomDataType()]; + if (item == nil) + return E_INVALIDARG; + if (View == NULL) + return E_FAIL; + + auto nsevent = [NSApp currentEvent]; + auto nseventType = [nsevent type]; + + // If current event isn't a mouse one (probably due to malfunctioning user app) + // attempt to forge a new one + if (!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited) + || (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged))) { + 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]; + CFRelease(cgevent); + } + + auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:item]; + + auto dragItemImage = [NSImage imageNamed:NSImageNameMultipleDocuments]; + NSRect dragItemRect = {(float) point.X, (float) point.Y, [dragItemImage size].width, [dragItemImage size].height}; + [dragItem setDraggingFrame:dragItemRect contents:dragItemImage]; + + int op = 0; + int ieffects = (int) effects; + if ((ieffects & (int) AvnDragDropEffects::Copy) != 0) + op |= NSDragOperationCopy; + if ((ieffects & (int) AvnDragDropEffects::Link) != 0) + op |= NSDragOperationLink; + if ((ieffects & (int) AvnDragDropEffects::Move) != 0) + op |= NSDragOperationMove; + [View beginDraggingSessionWithItems:@[dragItem] event:nsevent + source:CreateDraggingSource((NSDragOperation) op, cb, sourceHandle)]; + return S_OK; +} + +bool WindowBaseImpl::IsDialog() { + return false; +} + +NSWindowStyleMask WindowBaseImpl::GetStyle() { + return NSWindowStyleMaskBorderless; +} + +void WindowBaseImpl::UpdateStyle() { + [Window setStyleMask:GetStyle()]; +} + +void WindowBaseImpl::CleanNSWindow() { + if(Window != nullptr) { + [GetWindowProtocol() disconnectParent]; + [Window close]; + Window = nullptr; + } +} + +void WindowBaseImpl::CreateNSWindow(bool isDialog) { + if (isDialog) { + if (![Window isKindOfClass:[AvnPanel class]]) { + CleanNSWindow(); + + Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; + } + } else { + if (![Window isKindOfClass:[AvnWindow class]]) { + CleanNSWindow(); + + Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; + } + } +} + +void WindowBaseImpl::OnInitialiseNSWindow() +{ + +} + +void WindowBaseImpl::InitialiseNSWindow() { + if(Window != nullptr) { + [Window setContentView:StandardContainer]; + [Window setStyleMask:NSWindowStyleMaskBorderless]; + [Window setBackingType:NSBackingStoreBuffered]; + + [Window setContentSize:lastSize]; + [Window setContentMinSize:lastMinSize]; + [Window setContentMaxSize:lastMaxSize]; + + [Window setOpaque:false]; + + [Window invalidateShadow]; + + if (lastMenu != nullptr) { + [GetWindowProtocol() applyMenu:lastMenu]; + + if ([Window isKeyWindow]) { + [GetWindowProtocol() showWindowMenuWithAppMenu]; + } + } + + OnInitialiseNSWindow(); + } +} + +id WindowBaseImpl::GetWindowProtocol() { + if(Window == nullptr) + { + return nullptr; + } + + return (id ) Window; +} + +extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl) +{ + @autoreleasepool + { + IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events, gl); + return ptr; + } +} diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h new file mode 100644 index 0000000000..db19497b29 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -0,0 +1,101 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_WINDOWIMPL_H +#define AVALONIA_NATIVE_OSX_WINDOWIMPL_H + +#import "WindowBaseImpl.h" +#include "IWindowStateChanged.h" + +class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged +{ +private: + bool _canResize; + bool _fullScreenActive; + SystemDecorations _decorations; + AvnWindowState _lastWindowState; + AvnWindowState _actualWindowState; + bool _inSetWindowState; + NSRect _preZoomSize; + bool _transitioningWindowState; + bool _isClientAreaExtended; + bool _isDialog; + AvnExtendClientAreaChromeHints _extendClientHints; + + FORWARD_IUNKNOWN() +BEGIN_INTERFACE_MAP() + INHERIT_INTERFACE_MAP(WindowBaseImpl) + INTERFACE_MAP_ENTRY(IAvnWindow, IID_IAvnWindow) + END_INTERFACE_MAP() + virtual ~WindowImpl() + { + } + + ComPtr WindowEvents; + + WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl); + + void HideOrShowTrafficLights (); + + virtual HRESULT Show (bool activate, bool isDialog) override; + + virtual HRESULT SetEnabled (bool enable) override; + + virtual HRESULT SetParent (IAvnWindow* parent) override; + + void StartStateTransition () override ; + + void EndStateTransition () override ; + + SystemDecorations Decorations () override ; + + AvnWindowState WindowState () override ; + + void WindowStateChanged () override ; + + bool UndecoratedIsMaximized (); + + bool IsZoomed (); + + void DoZoom(); + + virtual HRESULT SetCanResize(bool value) override; + + virtual HRESULT SetDecorations(SystemDecorations value) override; + + virtual HRESULT SetTitle (char* utf8title) override; + + virtual HRESULT SetTitleBarColor(AvnColor color) override; + + virtual HRESULT GetWindowState (AvnWindowState*ret) override; + + virtual HRESULT TakeFocusFromChildren () override; + + virtual HRESULT SetExtendClientArea (bool enable) override; + + virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override; + + virtual HRESULT GetExtendTitleBarHeight (double*ret) override; + + virtual HRESULT SetExtendTitleBarHeight (double value) override; + + void EnterFullScreenMode (); + + void ExitFullScreenMode (); + + virtual HRESULT SetWindowState (AvnWindowState state) override; + + virtual bool IsDialog() override; + + virtual void OnInitialiseNSWindow() override; + +protected: + virtual NSWindowStyleMask GetStyle() override; + +private: + NSString *_lastTitle; +}; + +#endif //AVALONIA_NATIVE_OSX_WINDOWIMPL_H diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm new file mode 100644 index 0000000000..d96fe717ab --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -0,0 +1,554 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#include "AutoFitContentView.h" +#include "AvnView.h" +#include "automation.h" +#include "WindowProtocol.h" + +WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBaseImpl(events, gl) { + _isClientAreaExtended = false; + _extendClientHints = AvnDefaultChrome; + _fullScreenActive = false; + _canResize = true; + _decorations = SystemDecorationsFull; + _transitioningWindowState = false; + _inSetWindowState = false; + _lastWindowState = Normal; + _actualWindowState = Normal; + _lastTitle = @""; + WindowEvents = events; +} + +void WindowImpl::HideOrShowTrafficLights() { + if (Window == nil) { + return; + } + + for (id subview in Window.contentView.superview.subviews) { + if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { + NSView *titlebarView = [subview subviews][0]; + for (id button in titlebarView.subviews) { + if ([button isKindOfClass:[NSButton class]]) { + if (_isClientAreaExtended) { + auto wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + [button setHidden:!wantsChrome]; + } else { + [button setHidden:(_decorations != SystemDecorationsFull)]; + } + + [button setWantsLayer:true]; + } + } + } + } +} + +void WindowImpl::OnInitialiseNSWindow(){ + [GetWindowProtocol() setCanBecomeKeyWindow:true]; + [Window disableCursorRects]; + [Window setTabbingMode:NSWindowTabbingModeDisallowed]; + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; + + [Window setTitle:_lastTitle]; + + if(_isClientAreaExtended) + { + [GetWindowProtocol() setIsExtended:true]; + SetExtendClientArea(true); + } +} + +HRESULT WindowImpl::Show(bool activate, bool isDialog) { + START_COM_CALL; + + @autoreleasepool { + _isDialog = isDialog; + + WindowBaseImpl::Show(activate, isDialog); + + HideOrShowTrafficLights(); + + return SetWindowState(_lastWindowState); + } +} + +HRESULT WindowImpl::SetEnabled(bool enable) { + START_COM_CALL; + + @autoreleasepool { + [GetWindowProtocol() setEnabled:enable]; + return S_OK; + } +} + +HRESULT WindowImpl::SetParent(IAvnWindow *parent) { + START_COM_CALL; + + @autoreleasepool { + if (parent == nullptr) + return E_POINTER; + + auto cparent = dynamic_cast(parent); + if (cparent == nullptr) + 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 + // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. + if (cparent->WindowState() == Minimized) + cparent->SetWindowState(Normal); + + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; + [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; + + UpdateStyle(); + + return S_OK; + } +} + +void WindowImpl::StartStateTransition() { + _transitioningWindowState = true; +} + +void WindowImpl::EndStateTransition() { + _transitioningWindowState = false; +} + +SystemDecorations WindowImpl::Decorations() { + return _decorations; +} + +AvnWindowState WindowImpl::WindowState() { + return _lastWindowState; +} + +void WindowImpl::WindowStateChanged() { + if (_shown && !_inSetWindowState && !_transitioningWindowState) { + AvnWindowState state; + GetWindowState(&state); + + if (_lastWindowState != state) { + if (_isClientAreaExtended) { + if (_lastWindowState == FullScreen) { + // we exited fs. + if (_extendClientHints & AvnOSXThickTitleBar) { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } + + [Window setTitlebarAppearsTransparent:true]; + + [StandardContainer setFrameSize:StandardContainer.frame.size]; + } else if (state == FullScreen) { + // we entered fs. + if (_extendClientHints & AvnOSXThickTitleBar) { + Window.toolbar = nullptr; + } + + [Window setTitlebarAppearsTransparent:false]; + + [StandardContainer setFrameSize:StandardContainer.frame.size]; + } + } + + _lastWindowState = state; + _actualWindowState = state; + WindowEvents->WindowStateChanged(state); + } + } +} + +bool WindowImpl::UndecoratedIsMaximized() { + auto windowSize = [Window frame]; + auto available = [Window screen].visibleFrame; + return CGRectEqualToRect(windowSize, available); +} + +bool WindowImpl::IsZoomed() { + return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized(); +} + +void WindowImpl::DoZoom() { + switch (_decorations) { + case SystemDecorationsNone: + case SystemDecorationsBorderOnly: + [Window setFrame:[Window screen].visibleFrame display:true]; + break; + + + case SystemDecorationsFull: + [Window performZoom:Window]; + break; + } +} + +HRESULT WindowImpl::SetCanResize(bool value) { + START_COM_CALL; + + @autoreleasepool { + _canResize = value; + UpdateStyle(); + return S_OK; + } +} + +HRESULT WindowImpl::SetDecorations(SystemDecorations value) { + START_COM_CALL; + + @autoreleasepool { + auto currentWindowState = _lastWindowState; + _decorations = value; + + if (_fullScreenActive) { + return S_OK; + } + + UpdateStyle(); + + HideOrShowTrafficLights(); + + switch (_decorations) { + case SystemDecorationsNone: + [Window setHasShadow:NO]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + + if (currentWindowState == Maximized) { + if (!UndecoratedIsMaximized()) { + DoZoom(); + } + } + break; + + case SystemDecorationsBorderOnly: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + + if (currentWindowState == Maximized) { + if (!UndecoratedIsMaximized()) { + DoZoom(); + } + } + break; + + case SystemDecorationsFull: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + + if (currentWindowState == Maximized) { + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + + [View setFrameSize:newFrame]; + } + break; + } + + return S_OK; + } +} + +HRESULT WindowImpl::SetTitle(char *utf8title) { + START_COM_CALL; + + @autoreleasepool { + _lastTitle = [NSString stringWithUTF8String:(const char *) utf8title]; + [Window setTitle:_lastTitle]; + + return S_OK; + } +} + +HRESULT WindowImpl::SetTitleBarColor(AvnColor color) { + START_COM_CALL; + + @autoreleasepool { + float a = (float) color.Alpha / 255.0f; + float r = (float) color.Red / 255.0f; + float g = (float) color.Green / 255.0f; + float b = (float) color.Blue / 255.0f; + + auto nscolor = [NSColor colorWithSRGBRed:r green:g blue:b alpha:a]; + + // Based on the titlebar color we have to choose either light or dark + // OSX doesnt let you set a foreground color for titlebar. + if ((r * 0.299 + g * 0.587 + b * 0.114) > 186.0f / 255.0f) { + [Window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantLight]]; + } else { + [Window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]]; + } + + [Window setTitlebarAppearsTransparent:true]; + [Window setBackgroundColor:nscolor]; + } + + return S_OK; +} + +HRESULT WindowImpl::GetWindowState(AvnWindowState *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + if (([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) { + *ret = FullScreen; + return S_OK; + } + + if ([Window isMiniaturized]) { + *ret = Minimized; + return S_OK; + } + + if (IsZoomed()) { + *ret = Maximized; + return S_OK; + } + + *ret = Normal; + + return S_OK; + } +} + +HRESULT WindowImpl::TakeFocusFromChildren() { + START_COM_CALL; + + @autoreleasepool { + if (Window == nil) + return S_OK; + if ([Window isKeyWindow]) + [Window makeFirstResponder:View]; + + return S_OK; + } +} + +HRESULT WindowImpl::SetExtendClientArea(bool enable) { + START_COM_CALL; + + @autoreleasepool { + _isClientAreaExtended = enable; + + if(Window != nullptr) { + if (enable) { + Window.titleVisibility = NSWindowTitleHidden; + + [Window setTitlebarAppearsTransparent:true]; + + auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + if (wantsTitleBar) { + [StandardContainer ShowTitleBar:true]; + } else { + [StandardContainer ShowTitleBar:false]; + } + + if (_extendClientHints & AvnOSXThickTitleBar) { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } else { + Window.toolbar = nullptr; + } + } else { + Window.titleVisibility = NSWindowTitleVisible; + Window.toolbar = nullptr; + [Window setTitlebarAppearsTransparent:false]; + View.layer.zPosition = 0; + } + + [GetWindowProtocol() setIsExtended:enable]; + + HideOrShowTrafficLights(); + + UpdateStyle(); + } + + return S_OK; + } +} + +HRESULT WindowImpl::SetExtendClientAreaHints(AvnExtendClientAreaChromeHints hints) { + START_COM_CALL; + + @autoreleasepool { + _extendClientHints = hints; + + SetExtendClientArea(_isClientAreaExtended); + return S_OK; + } +} + +HRESULT WindowImpl::GetExtendTitleBarHeight(double *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + *ret = [GetWindowProtocol() getExtendedTitleBarHeight]; + + return S_OK; + } +} + +HRESULT WindowImpl::SetExtendTitleBarHeight(double value) { + START_COM_CALL; + + @autoreleasepool { + [StandardContainer SetTitleBarHeightHint:value]; + return S_OK; + } +} + +void WindowImpl::EnterFullScreenMode() { + _fullScreenActive = true; + + [Window setTitle:_lastTitle]; + [Window toggleFullScreen:nullptr]; +} + +void WindowImpl::ExitFullScreenMode() { + [Window toggleFullScreen:nullptr]; + + _fullScreenActive = false; + + SetDecorations(_decorations); +} + +HRESULT WindowImpl::SetWindowState(AvnWindowState state) { + START_COM_CALL; + + @autoreleasepool { + auto currentState = _actualWindowState; + _lastWindowState = state; + + if (Window == nullptr) { + return S_OK; + } + + if (_actualWindowState == state) { + return S_OK; + } + + _inSetWindowState = true; + + if (currentState == Normal) { + _preZoomSize = [Window frame]; + } + + if (_shown) { + switch (state) { + case Maximized: + if (currentState == FullScreen) { + ExitFullScreenMode(); + } + + lastPositionSet.X = 0; + lastPositionSet.Y = 0; + + if ([Window isMiniaturized]) { + [Window deminiaturize:Window]; + } + + if (!IsZoomed()) { + DoZoom(); + } + break; + + case Minimized: + if (currentState == FullScreen) { + ExitFullScreenMode(); + } else { + [Window miniaturize:Window]; + } + break; + + case FullScreen: + if ([Window isMiniaturized]) { + [Window deminiaturize:Window]; + } + + EnterFullScreenMode(); + break; + + case Normal: + if ([Window isMiniaturized]) { + [Window deminiaturize:Window]; + } + + if (currentState == FullScreen) { + ExitFullScreenMode(); + } + + if (IsZoomed()) { + if (_decorations == SystemDecorationsFull) { + DoZoom(); + } else { + [Window setFrame:_preZoomSize display:true]; + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + + [View setFrameSize:newFrame]; + } + + } + break; + } + + _actualWindowState = _lastWindowState; + WindowEvents->WindowStateChanged(_actualWindowState); + } + + + _inSetWindowState = false; + + return S_OK; + } +} + +bool WindowImpl::IsDialog() { + return _isDialog; +} + +NSWindowStyleMask WindowImpl::GetStyle() { + unsigned long s = this->_isDialog ? NSWindowStyleMaskDocModalWindow : NSWindowStyleMaskBorderless; + + switch (_decorations) { + case SystemDecorationsNone: + s = s | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsBorderOnly: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsFull: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless; + + if (_canResize) { + s = s | NSWindowStyleMaskResizable; + } + break; + } + + if ([Window parentWindow] == nullptr) { + s |= NSWindowStyleMaskMiniaturizable; + } + + if (_isClientAreaExtended) { + s |= NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskTexturedBackground; + } + return s; +} diff --git a/native/Avalonia.Native/src/OSX/WindowInterfaces.h b/native/Avalonia.Native/src/OSX/WindowInterfaces.h new file mode 100644 index 0000000000..6e6d62e85e --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowInterfaces.h @@ -0,0 +1,17 @@ +// +// Created by Dan Walmsley on 06/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#import +#include "WindowProtocol.h" +#include "WindowBaseImpl.h" + +@interface AvnWindow : NSWindow +-(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; +@end + +@interface AvnPanel : NSPanel +-(AvnPanel* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; +@end \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/WindowProtocol.h b/native/Avalonia.Native/src/OSX/WindowProtocol.h new file mode 100644 index 0000000000..0e5c5869e7 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowProtocol.h @@ -0,0 +1,27 @@ +// +// Created by Dan Walmsley on 06/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#pragma once + +#import + +@class AvnMenu; + +@protocol AvnWindowProtocol +-(void) pollModalSession: (NSModalSession _Nonnull) session; +-(void) restoreParentWindow; +-(bool) shouldTryToHandleEvents; +-(void) setEnabled: (bool) enable; +-(void) showAppMenuOnly; +-(void) showWindowMenuWithAppMenu; +-(void) applyMenu:(AvnMenu* _Nullable)menu; + +-(double) getExtendedTitleBarHeight; +-(void) setIsExtended:(bool)value; +-(void) disconnectParent; +-(bool) isDialog; + +-(void) setCanBecomeKeyWindow:(bool)value; +@end diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 79175d9ff1..14f1f6888c 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -73,22 +73,15 @@ ComPtr _events; _isHandlingSendEvent = true; @try { [super sendEvent: event]; + if ([event type] == NSEventTypeKeyUp && ([event modifierFlags] & NSEventModifierFlagCommand)) + { + [[self keyWindow] sendEvent:event]; + } + } @finally { _isHandlingSendEvent = oldHandling; } } - -// This is needed for certain embedded controls -- (BOOL) isHandlingSendEvent -{ - return _isHandlingSendEvent; -} - -- (void)setHandlingSendEvent:(BOOL)handlingSendEvent -{ - _isHandlingSendEvent = handlingSendEvent; -} - @end extern void InitializeAvnApp(IAvnApplicationEvents* events) diff --git a/native/Avalonia.Native/src/OSX/automation.h b/native/Avalonia.Native/src/OSX/automation.h new file mode 100644 index 0000000000..367df3619d --- /dev/null +++ b/native/Avalonia.Native/src/OSX/automation.h @@ -0,0 +1,12 @@ +#pragma once + +#import +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..d0c8d7a9db --- /dev/null +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -0,0 +1,497 @@ +#include "common.h" +#include "automation.h" +#include "AvnString.h" +#include "INSWindowHolder.h" +#include "AvnView.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..a90a235b9d 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -27,7 +27,7 @@ extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnApplicationCommands* CreateApplicationCommands(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); -extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); +extern void SetAppMenu(IAvnMenu *menu); extern void SetServicesMenu (IAvnMenu* menu); extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); @@ -35,9 +35,9 @@ 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(); extern NSSize ToNSSize (AvnSize s); #ifdef DEBUG #define NSDebugLog(...) NSLog(__VA_ARGS__) 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/cursor.mm b/native/Avalonia.Native/src/OSX/cursor.mm index dc38294a18..8638a03531 100644 --- a/native/Avalonia.Native/src/OSX/cursor.mm +++ b/native/Avalonia.Native/src/OSX/cursor.mm @@ -1,6 +1,5 @@ #include "common.h" #include "cursor.h" -#include class CursorFactory : public ComSingleObject { diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 69f2995847..6ee86b21ae 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -335,14 +335,14 @@ public: return S_OK; } } - + virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override { START_COM_CALL; @autoreleasepool { - ::SetAppMenu(s_appTitle, appMenu); + ::SetAppMenu(appMenu); return S_OK; } } @@ -400,6 +400,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; @@ -418,7 +427,3 @@ AvnPoint ConvertPointY (AvnPoint p) return p; } -CGFloat PrimaryDisplayHeight() -{ - return NSMaxY([[[NSScreen screens] firstObject] frame]); -} diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 186fcf255b..ce46ac11e0 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -31,7 +31,6 @@ private: NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem IAvnActionCallback* _callback; IAvnPredicateCallback* _predicate; - bool _isSeparator; bool _isCheckable; public: diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 2dbe76bc6d..b05588a441 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -1,7 +1,6 @@ #include "common.h" #include "menu.h" -#include "window.h" #include "KeyTransform.h" #include #include /* For kVK_ constants, and TIS functions. */ @@ -74,8 +73,7 @@ AvnAppMenuItem::AvnAppMenuItem(bool isSeparator) { _isCheckable = false; - _isSeparator = isSeparator; - + if(isSeparator) { _native = [NSMenuItem separatorItem]; @@ -460,7 +458,7 @@ extern IAvnMenuItem* CreateAppMenuItemSeparator() static IAvnMenu* s_appMenu = nullptr; static NSMenuItem* s_appMenuItem = nullptr; -extern void SetAppMenu (NSString* appName, IAvnMenu* menu) +extern void SetAppMenu(IAvnMenu *menu) { s_appMenu = menu; diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm index dc5c24e41e..266d0345d1 100644 --- a/native/Avalonia.Native/src/OSX/rendertarget.mm +++ b/native/Avalonia.Native/src/OSX/rendertarget.mm @@ -1,14 +1,10 @@ #include "common.h" #include "rendertarget.h" -#import #import #import -#include -#include #include #include -#include @interface IOSurfaceHolder : NSObject @end diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h deleted file mode 100644 index 1dc091a48d..0000000000 --- a/native/Avalonia.Native/src/OSX/window.h +++ /dev/null @@ -1,76 +0,0 @@ -#ifndef window_h -#define window_h - -class WindowBaseImpl; - -@interface AvnView : NSView --(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; --(NSEvent* _Nonnull) lastMouseDownEvent; --(AvnPoint) translateLocalPoint:(AvnPoint)pt; --(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; --(void) onClosed; --(AvnPixelSize) getPixelSize; --(AvnPlatformResizeReason) getResizeReason; --(void) setResizeReason:(AvnPlatformResizeReason)reason; -+ (AvnPoint)toAvnPoint:(CGPoint)p; -@end - -@interface AutoFitContentView : NSView --(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; --(void) ShowTitleBar: (bool) show; --(void) SetTitleBarHeightHint: (double) height; --(void) SetContent: (NSView* _Nonnull) content; --(void) ShowBlur: (bool) show; -@end - -@interface AvnWindow : NSWindow -+(void) closeAll; --(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; --(void) setCanBecomeKeyAndMain; --(void) pollModalSession: (NSModalSession _Nonnull) session; --(void) restoreParentWindow; --(bool) shouldTryToHandleEvents; --(void) setEnabled: (bool) enable; --(void) showAppMenuOnly; --(void) showWindowMenuWithAppMenu; --(void) applyMenu:(NSMenu* _Nullable)menu; --(double) getScaling; --(double) getExtendedTitleBarHeight; --(void) setIsExtended:(bool)value; --(bool) isDialog; -@end - -struct INSWindowHolder -{ - virtual AvnWindow* _Nonnull GetNSWindow () = 0; -}; - -struct IWindowStateChanged -{ - virtual void WindowStateChanged () = 0; - virtual void StartStateTransition () = 0; - virtual void EndStateTransition () = 0; - virtual SystemDecorations Decorations () = 0; - virtual AvnWindowState WindowState () = 0; -}; - -class ResizeScope -{ -public: - ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason) - { - _view = view; - _restore = [view getResizeReason]; - [view setResizeReason:reason]; - } - - ~ResizeScope() - { - [_view setResizeReason:_restore]; - } -private: - AvnView* _Nonnull _view; - AvnPlatformResizeReason _restore; -}; - -#endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm deleted file mode 100644 index 40180274e1..0000000000 --- a/native/Avalonia.Native/src/OSX/window.mm +++ /dev/null @@ -1,2529 +0,0 @@ -#include "common.h" -#include "window.h" -#include "KeyTransform.h" -#include "cursor.h" -#include "menu.h" -#include -#include "rendertarget.h" - -class WindowBaseImpl : public virtual ComSingleObject, public INSWindowHolder -{ -private: - NSCursor* cursor; - -public: - FORWARD_IUNKNOWN() - virtual ~WindowBaseImpl() - { - View = NULL; - Window = NULL; - } - AutoFitContentView* StandardContainer; - AvnView* View; - AvnWindow* Window; - ComPtr BaseEvents; - ComPtr _glContext; - NSObject* renderTarget; - AvnPoint lastPositionSet; - NSString* _lastTitle; - IAvnMenu* _mainMenu; - - bool _shown; - bool _inResize; - - WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl) - { - _shown = false; - _inResize = false; - _mainMenu = nullptr; - BaseEvents = events; - _glContext = gl; - renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl]; - View = [[AvnView alloc] initWithParent:this]; - StandardContainer = [[AutoFitContentView new] initWithContent:View]; - - Window = [[AvnWindow alloc] initWithParent:this]; - [Window setContentView: StandardContainer]; - - lastPositionSet.X = 100; - lastPositionSet.Y = 100; - _lastTitle = @""; - - [Window setStyleMask:NSWindowStyleMaskBorderless]; - [Window setBackingType:NSBackingStoreBuffered]; - - [Window setOpaque:false]; - } - - virtual HRESULT ObtainNSWindowHandle(void** ret) override - { - START_COM_CALL; - - if (ret == nullptr) - { - return E_POINTER; - } - - *ret = (__bridge void*)Window; - - return S_OK; - } - - virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override - { - START_COM_CALL; - - if (ret == nullptr) - { - return E_POINTER; - } - - *ret = (__bridge_retained void*)Window; - - return S_OK; - } - - virtual HRESULT ObtainNSViewHandle(void** ret) override - { - START_COM_CALL; - - if (ret == nullptr) - { - return E_POINTER; - } - - *ret = (__bridge void*)View; - - return S_OK; - } - - virtual HRESULT ObtainNSViewHandleRetained(void** ret) override - { - START_COM_CALL; - - if (ret == nullptr) - { - return E_POINTER; - } - - *ret = (__bridge_retained void*)View; - - return S_OK; - } - - virtual AvnWindow* GetNSWindow() override - { - return Window; - } - - virtual HRESULT Show(bool activate, bool isDialog) override - { - START_COM_CALL; - - @autoreleasepool - { - SetPosition(lastPositionSet); - UpdateStyle(); - - [Window setTitle:_lastTitle]; - - if(ShouldTakeFocusOnShow() && activate) - { - [Window orderFront: Window]; - [Window makeKeyAndOrderFront:Window]; - [Window makeFirstResponder:View]; - [NSApp activateIgnoringOtherApps:YES]; - } - else - { - [Window orderFront: Window]; - } - - _shown = true; - - return S_OK; - } - } - - virtual bool ShouldTakeFocusOnShow() - { - return true; - } - - virtual HRESULT Hide () override - { - START_COM_CALL; - - @autoreleasepool - { - if(Window != nullptr) - { - [Window orderOut:Window]; - [Window restoreParentWindow]; - } - - return S_OK; - } - } - - virtual HRESULT Activate () override - { - START_COM_CALL; - - @autoreleasepool - { - if(Window != nullptr) - { - [Window makeKeyAndOrderFront:nil]; - [NSApp activateIgnoringOtherApps:YES]; - } - } - - return S_OK; - } - - virtual HRESULT SetTopMost (bool value) override - { - START_COM_CALL; - - @autoreleasepool - { - [Window setLevel: value ? NSFloatingWindowLevel : NSNormalWindowLevel]; - - return S_OK; - } - } - - virtual HRESULT Close() override - { - START_COM_CALL; - - @autoreleasepool - { - if (Window != nullptr) - { - auto window = Window; - Window = nullptr; - - try{ - // Seems to throw sometimes on application exit. - [window close]; - } - catch(NSException*){} - } - - return S_OK; - } - } - - virtual HRESULT GetClientSize(AvnSize* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - return E_POINTER; - - auto frame = [View frame]; - ret->Width = frame.size.width; - ret->Height = frame.size.height; - - return S_OK; - } - } - - virtual HRESULT GetFrameSize(AvnSize* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - return E_POINTER; - - auto frame = [Window frame]; - ret->Width = frame.size.width; - ret->Height = frame.size.height; - - return S_OK; - } - } - - virtual HRESULT GetScaling (double* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - return E_POINTER; - - if(Window == nullptr) - { - *ret = 1; - return S_OK; - } - - *ret = [Window backingScaleFactor]; - return S_OK; - } - } - - virtual HRESULT SetMinMaxSize (AvnSize minSize, AvnSize maxSize) override - { - START_COM_CALL; - - @autoreleasepool - { - [Window setMinSize: ToNSSize(minSize)]; - [Window setMaxSize: ToNSSize(maxSize)]; - - return S_OK; - } - } - - virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override - { - if(_inResize) - { - return S_OK; - } - - _inResize = true; - - START_COM_CALL; - auto resizeBlock = ResizeScope(View, reason); - - @autoreleasepool - { - auto maxSize = [Window maxSize]; - auto minSize = [Window minSize]; - - if (x < minSize.width) - { - x = minSize.width; - } - - if (y < minSize.height) - { - y = minSize.height; - } - - if (x > maxSize.width) - { - x = maxSize.width; - } - - if (y > maxSize.height) - { - y = maxSize.height; - } - - @try - { - if(!_shown) - { - BaseEvents->Resized(AvnSize{x,y}, reason); - } - - [Window setContentSize:NSSize{x,y}]; - [Window invalidateShadow]; - } - @finally - { - _inResize = false; - } - - return S_OK; - } - } - - virtual HRESULT Invalidate (AvnRect rect) override - { - START_COM_CALL; - - @autoreleasepool - { - [View setNeedsDisplayInRect:[View frame]]; - - return S_OK; - } - } - - virtual HRESULT SetMainMenu(IAvnMenu* menu) override - { - START_COM_CALL; - - _mainMenu = menu; - - auto nativeMenu = dynamic_cast(menu); - - auto nsmenu = nativeMenu->GetNative(); - - [Window applyMenu:nsmenu]; - - if ([Window isKeyWindow]) - { - [Window showWindowMenuWithAppMenu]; - } - - return S_OK; - } - - virtual HRESULT BeginMoveDrag () override - { - START_COM_CALL; - - @autoreleasepool - { - auto lastEvent = [View lastMouseDownEvent]; - - if(lastEvent == nullptr) - { - return S_OK; - } - - [Window performWindowDragWithEvent:lastEvent]; - - return S_OK; - } - } - - virtual HRESULT BeginResizeDrag (AvnWindowEdge edge) override - { - START_COM_CALL; - - return S_OK; - } - - virtual HRESULT GetPosition (AvnPoint* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - { - return E_POINTER; - } - - auto frame = [Window frame]; - - ret->X = frame.origin.x; - ret->Y = frame.origin.y + frame.size.height; - - *ret = ConvertPointY(*ret); - - return S_OK; - } - } - - virtual HRESULT SetPosition (AvnPoint point) override - { - START_COM_CALL; - - @autoreleasepool - { - lastPositionSet = point; - [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(point))]; - - return S_OK; - } - } - - virtual HRESULT PointToClient (AvnPoint point, AvnPoint* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - { - return E_POINTER; - } - - point = ConvertPointY(point); - auto viewPoint = [Window convertScreenToBase:ToNSPoint(point)]; - - *ret = [View translateLocalPoint:ToAvnPoint(viewPoint)]; - - return S_OK; - } - } - - virtual HRESULT PointToScreen (AvnPoint point, AvnPoint* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - { - return E_POINTER; - } - - auto cocoaViewPoint = ToNSPoint([View translateLocalPoint:point]); - auto cocoaScreenPoint = [Window convertBaseToScreen:cocoaViewPoint]; - *ret = ConvertPointY(ToAvnPoint(cocoaScreenPoint)); - - return S_OK; - } - } - - virtual HRESULT ThreadSafeSetSwRenderedFrame(AvnFramebuffer* fb, IUnknown* dispose) override - { - START_COM_CALL; - - [View setSwRenderedFrame: fb dispose: dispose]; - return S_OK; - } - - virtual HRESULT SetCursor(IAvnCursor* cursor) override - { - START_COM_CALL; - - @autoreleasepool - { - Cursor* avnCursor = dynamic_cast(cursor); - this->cursor = avnCursor->GetNative(); - UpdateCursor(); - - if(avnCursor->IsHidden()) - { - [NSCursor hide]; - } - else - { - [NSCursor unhide]; - } - - return S_OK; - } - } - - virtual void UpdateCursor() - { - if (cursor != nil) - { - [cursor set]; - } - } - - virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ppv) override - { - START_COM_CALL; - - if(View == NULL) - return E_FAIL; - *ppv = [renderTarget createSurfaceRenderTarget]; - return *ppv == nil ? E_FAIL : S_OK; - } - - virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override - { - START_COM_CALL; - - if(View == NULL) - return E_FAIL; - *retOut = ::CreateNativeControlHost(View); - return S_OK; - } - - virtual HRESULT SetBlurEnabled (bool enable) override - { - START_COM_CALL; - - [StandardContainer ShowBlur:enable]; - - return S_OK; - } - - virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, - IAvnClipboard* clipboard, IAvnDndResultCallback* cb, - void* sourceHandle) override - { - START_COM_CALL; - - auto item = TryGetPasteboardItem(clipboard); - [item setString:@"" forType:GetAvnCustomDataType()]; - if(item == nil) - return E_INVALIDARG; - if(View == NULL) - return E_FAIL; - - auto nsevent = [NSApp currentEvent]; - auto nseventType = [nsevent type]; - - // If current event isn't a mouse one (probably due to malfunctioning user app) - // attempt to forge a new one - if(!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited) - || (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged))) - { - auto nspoint = [Window convertBaseToScreen: ToNSPoint(point)]; - CGPoint cgpoint = NSPointToCGPoint(nspoint); - auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft); - nsevent = [NSEvent eventWithCGEvent: cgevent]; - CFRelease(cgevent); - } - - auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter: item]; - - auto dragItemImage = [NSImage imageNamed:NSImageNameMultipleDocuments]; - NSRect dragItemRect = {(float)point.X, (float)point.Y, [dragItemImage size].width, [dragItemImage size].height}; - [dragItem setDraggingFrame: dragItemRect contents: dragItemImage]; - - int op = 0; int ieffects = (int)effects; - if((ieffects & (int)AvnDragDropEffects::Copy) != 0) - op |= NSDragOperationCopy; - if((ieffects & (int)AvnDragDropEffects::Link) != 0) - op |= NSDragOperationLink; - if((ieffects & (int)AvnDragDropEffects::Move) != 0) - op |= NSDragOperationMove; - [View beginDraggingSessionWithItems: @[dragItem] event: nsevent - source: CreateDraggingSource((NSDragOperation) op, cb, sourceHandle)]; - return S_OK; - } - - virtual bool IsDialog() - { - return false; - } - -protected: - virtual NSWindowStyleMask GetStyle() - { - return NSWindowStyleMaskBorderless; - } - - void UpdateStyle() - { - [Window setStyleMask: GetStyle()]; - } - -public: - virtual void OnResized () - { - - } -}; - -class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged -{ -private: - bool _canResize; - bool _fullScreenActive; - SystemDecorations _decorations; - AvnWindowState _lastWindowState; - AvnWindowState _actualWindowState; - bool _inSetWindowState; - NSRect _preZoomSize; - bool _transitioningWindowState; - bool _isClientAreaExtended; - bool _isDialog; - AvnExtendClientAreaChromeHints _extendClientHints; - - FORWARD_IUNKNOWN() - BEGIN_INTERFACE_MAP() - INHERIT_INTERFACE_MAP(WindowBaseImpl) - INTERFACE_MAP_ENTRY(IAvnWindow, IID_IAvnWindow) - END_INTERFACE_MAP() - virtual ~WindowImpl() - { - } - - ComPtr WindowEvents; - WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) - { - _isClientAreaExtended = false; - _extendClientHints = AvnDefaultChrome; - _fullScreenActive = false; - _canResize = true; - _decorations = SystemDecorationsFull; - _transitioningWindowState = false; - _inSetWindowState = false; - _lastWindowState = Normal; - _actualWindowState = Normal; - WindowEvents = events; - [Window setCanBecomeKeyAndMain]; - [Window disableCursorRects]; - [Window setTabbingMode:NSWindowTabbingModeDisallowed]; - [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; - } - - void HideOrShowTrafficLights () - { - if (Window == nil) - { - return; - } - - for (id subview in Window.contentView.superview.subviews) { - if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { - NSView *titlebarView = [subview subviews][0]; - for (id button in titlebarView.subviews) { - if ([button isKindOfClass:[NSButton class]]) - { - if(_isClientAreaExtended) - { - auto wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - - [button setHidden: !wantsChrome]; - } - else - { - [button setHidden: (_decorations != SystemDecorationsFull)]; - } - - [button setWantsLayer:true]; - } - } - } - } - } - - virtual HRESULT Show (bool activate, bool isDialog) override - { - START_COM_CALL; - - @autoreleasepool - { - _isDialog = isDialog; - WindowBaseImpl::Show(activate, isDialog); - - HideOrShowTrafficLights(); - - return SetWindowState(_lastWindowState); - } - } - - virtual HRESULT SetEnabled (bool enable) override - { - START_COM_CALL; - - @autoreleasepool - { - [Window setEnabled:enable]; - return S_OK; - } - } - - virtual HRESULT SetParent (IAvnWindow* parent) override - { - START_COM_CALL; - - @autoreleasepool - { - if(parent == nullptr) - return E_POINTER; - - auto cparent = dynamic_cast(parent); - if(cparent == nullptr) - 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 - // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. - if (cparent->WindowState() == Minimized) - cparent->SetWindowState(Normal); - - [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; - [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; - - UpdateStyle(); - - return S_OK; - } - } - - void StartStateTransition () override - { - _transitioningWindowState = true; - } - - void EndStateTransition () override - { - _transitioningWindowState = false; - } - - SystemDecorations Decorations () override - { - return _decorations; - } - - AvnWindowState WindowState () override - { - return _lastWindowState; - } - - void WindowStateChanged () override - { - if(_shown && !_inSetWindowState && !_transitioningWindowState) - { - AvnWindowState state; - GetWindowState(&state); - - if(_lastWindowState != state) - { - if(_isClientAreaExtended) - { - if(_lastWindowState == FullScreen) - { - // we exited fs. - if(_extendClientHints & AvnOSXThickTitleBar) - { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; - } - - [Window setTitlebarAppearsTransparent:true]; - - [StandardContainer setFrameSize: StandardContainer.frame.size]; - } - else if(state == FullScreen) - { - // we entered fs. - if(_extendClientHints & AvnOSXThickTitleBar) - { - Window.toolbar = nullptr; - } - - [Window setTitlebarAppearsTransparent:false]; - - [StandardContainer setFrameSize: StandardContainer.frame.size]; - } - } - - _lastWindowState = state; - _actualWindowState = state; - WindowEvents->WindowStateChanged(state); - } - } - } - - bool UndecoratedIsMaximized () - { - auto windowSize = [Window frame]; - auto available = [Window screen].visibleFrame; - return CGRectEqualToRect(windowSize, available); - } - - bool IsZoomed () - { - return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized(); - } - - void DoZoom() - { - switch (_decorations) - { - case SystemDecorationsNone: - case SystemDecorationsBorderOnly: - [Window setFrame:[Window screen].visibleFrame display:true]; - break; - - - case SystemDecorationsFull: - [Window performZoom:Window]; - break; - } - } - - virtual HRESULT SetCanResize(bool value) override - { - START_COM_CALL; - - @autoreleasepool - { - _canResize = value; - UpdateStyle(); - return S_OK; - } - } - - virtual HRESULT SetDecorations(SystemDecorations value) override - { - START_COM_CALL; - - @autoreleasepool - { - auto currentWindowState = _lastWindowState; - _decorations = value; - - if(_fullScreenActive) - { - return S_OK; - } - - UpdateStyle(); - - HideOrShowTrafficLights(); - - switch (_decorations) - { - case SystemDecorationsNone: - [Window setHasShadow:NO]; - [Window setTitleVisibility:NSWindowTitleHidden]; - [Window setTitlebarAppearsTransparent:YES]; - - if(currentWindowState == Maximized) - { - if(!UndecoratedIsMaximized()) - { - DoZoom(); - } - } - break; - - case SystemDecorationsBorderOnly: - [Window setHasShadow:YES]; - [Window setTitleVisibility:NSWindowTitleHidden]; - [Window setTitlebarAppearsTransparent:YES]; - - if(currentWindowState == Maximized) - { - if(!UndecoratedIsMaximized()) - { - DoZoom(); - } - } - break; - - case SystemDecorationsFull: - [Window setHasShadow:YES]; - [Window setTitleVisibility:NSWindowTitleVisible]; - [Window setTitlebarAppearsTransparent:NO]; - [Window setTitle:_lastTitle]; - - if(currentWindowState == Maximized) - { - auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; - - [View setFrameSize:newFrame]; - } - break; - } - - return S_OK; - } - } - - virtual HRESULT SetTitle (char* utf8title) override - { - START_COM_CALL; - - @autoreleasepool - { - _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; - [Window setTitle:_lastTitle]; - - return S_OK; - } - } - - virtual HRESULT SetTitleBarColor(AvnColor color) override - { - START_COM_CALL; - - @autoreleasepool - { - float a = (float)color.Alpha / 255.0f; - float r = (float)color.Red / 255.0f; - float g = (float)color.Green / 255.0f; - float b = (float)color.Blue / 255.0f; - - auto nscolor = [NSColor colorWithSRGBRed:r green:g blue:b alpha:a]; - - // Based on the titlebar color we have to choose either light or dark - // OSX doesnt let you set a foreground color for titlebar. - if ((r*0.299 + g*0.587 + b*0.114) > 186.0f / 255.0f) - { - [Window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantLight]]; - } - else - { - [Window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]]; - } - - [Window setTitlebarAppearsTransparent:true]; - [Window setBackgroundColor:nscolor]; - } - - return S_OK; - } - - virtual HRESULT GetWindowState (AvnWindowState*ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - { - return E_POINTER; - } - - if(([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) - { - *ret = FullScreen; - return S_OK; - } - - if([Window isMiniaturized]) - { - *ret = Minimized; - return S_OK; - } - - if(IsZoomed()) - { - *ret = Maximized; - return S_OK; - } - - *ret = Normal; - - return S_OK; - } - } - - virtual HRESULT TakeFocusFromChildren () override - { - START_COM_CALL; - - @autoreleasepool - { - if(Window == nil) - return S_OK; - if([Window isKeyWindow]) - [Window makeFirstResponder: View]; - - return S_OK; - } - } - - virtual HRESULT SetExtendClientArea (bool enable) override - { - START_COM_CALL; - - @autoreleasepool - { - _isClientAreaExtended = enable; - - if(enable) - { - Window.titleVisibility = NSWindowTitleHidden; - - [Window setTitlebarAppearsTransparent:true]; - - auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - - if (wantsTitleBar) - { - [StandardContainer ShowTitleBar:true]; - } - else - { - [StandardContainer ShowTitleBar:false]; - } - - if(_extendClientHints & AvnOSXThickTitleBar) - { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; - } - else - { - Window.toolbar = nullptr; - } - } - else - { - Window.titleVisibility = NSWindowTitleVisible; - Window.toolbar = nullptr; - [Window setTitlebarAppearsTransparent:false]; - View.layer.zPosition = 0; - } - - [Window setIsExtended:enable]; - - HideOrShowTrafficLights(); - - UpdateStyle(); - - return S_OK; - } - } - - virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override - { - START_COM_CALL; - - @autoreleasepool - { - _extendClientHints = hints; - - SetExtendClientArea(_isClientAreaExtended); - return S_OK; - } - } - - virtual HRESULT GetExtendTitleBarHeight (double*ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - { - return E_POINTER; - } - - *ret = [Window getExtendedTitleBarHeight]; - - return S_OK; - } - } - - virtual HRESULT SetExtendTitleBarHeight (double value) override - { - START_COM_CALL; - - @autoreleasepool - { - [StandardContainer SetTitleBarHeightHint:value]; - return S_OK; - } - } - - void EnterFullScreenMode () - { - _fullScreenActive = true; - - [Window setTitle:_lastTitle]; - [Window toggleFullScreen:nullptr]; - } - - void ExitFullScreenMode () - { - [Window toggleFullScreen:nullptr]; - - _fullScreenActive = false; - - SetDecorations(_decorations); - } - - virtual HRESULT SetWindowState (AvnWindowState state) override - { - START_COM_CALL; - - @autoreleasepool - { - if(Window == nullptr) - { - return S_OK; - } - - if(_actualWindowState == state) - { - return S_OK; - } - - _inSetWindowState = true; - - auto currentState = _actualWindowState; - _lastWindowState = state; - - if(currentState == Normal) - { - _preZoomSize = [Window frame]; - } - - if(_shown) - { - switch (state) { - case Maximized: - if(currentState == FullScreen) - { - ExitFullScreenMode(); - } - - lastPositionSet.X = 0; - lastPositionSet.Y = 0; - - if([Window isMiniaturized]) - { - [Window deminiaturize:Window]; - } - - if(!IsZoomed()) - { - DoZoom(); - } - break; - - case Minimized: - if(currentState == FullScreen) - { - ExitFullScreenMode(); - } - else - { - [Window miniaturize:Window]; - } - break; - - case FullScreen: - if([Window isMiniaturized]) - { - [Window deminiaturize:Window]; - } - - EnterFullScreenMode(); - break; - - case Normal: - if([Window isMiniaturized]) - { - [Window deminiaturize:Window]; - } - - if(currentState == FullScreen) - { - ExitFullScreenMode(); - } - - if(IsZoomed()) - { - if(_decorations == SystemDecorationsFull) - { - DoZoom(); - } - else - { - [Window setFrame:_preZoomSize display:true]; - auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; - - [View setFrameSize:newFrame]; - } - - } - break; - } - - _actualWindowState = _lastWindowState; - WindowEvents->WindowStateChanged(_actualWindowState); - } - - - _inSetWindowState = false; - - return S_OK; - } - } - - virtual void OnResized () override - { - if(_shown && !_inSetWindowState && !_transitioningWindowState) - { - WindowStateChanged(); - } - } - - virtual bool IsDialog() override - { - return _isDialog; - } - -protected: - virtual NSWindowStyleMask GetStyle() override - { - unsigned long s = NSWindowStyleMaskBorderless; - - switch (_decorations) - { - case SystemDecorationsNone: - s = s | NSWindowStyleMaskFullSizeContentView; - break; - - case SystemDecorationsBorderOnly: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; - break; - - case SystemDecorationsFull: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless; - - if(_canResize) - { - s = s | NSWindowStyleMaskResizable; - } - break; - } - - if([Window parentWindow] == nullptr) - { - s |= NSWindowStyleMaskMiniaturizable; - } - - if(_isClientAreaExtended) - { - s |= NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskTexturedBackground; - } - return s; - } -}; - -NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; - -@implementation AutoFitContentView -{ - NSVisualEffectView* _titleBarMaterial; - NSBox* _titleBarUnderline; - NSView* _content; - NSVisualEffectView* _blurBehind; - double _titleBarHeightHint; - bool _settingSize; -} - --(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content -{ - _titleBarHeightHint = -1; - _content = content; - _settingSize = false; - - [self setAutoresizesSubviews:true]; - [self setWantsLayer:true]; - - _titleBarMaterial = [NSVisualEffectView new]; - [_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow]; - [_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar]; - [_titleBarMaterial setWantsLayer:true]; - _titleBarMaterial.hidden = true; - - _titleBarUnderline = [NSBox new]; - _titleBarUnderline.boxType = NSBoxSeparator; - _titleBarUnderline.fillColor = [NSColor underPageBackgroundColor]; - _titleBarUnderline.hidden = true; - - [self addSubview:_titleBarMaterial]; - [self addSubview:_titleBarUnderline]; - - _blurBehind = [NSVisualEffectView new]; - [_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; - [_blurBehind setMaterial:NSVisualEffectMaterialLight]; - [_blurBehind setWantsLayer:true]; - _blurBehind.hidden = true; - - [_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; - [_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; - - [self addSubview:_blurBehind]; - [self addSubview:_content]; - - [self setWantsLayer:true]; - return self; -} - --(void) ShowBlur:(bool)show -{ - _blurBehind.hidden = !show; -} - --(void) ShowTitleBar: (bool) show -{ - _titleBarMaterial.hidden = !show; - _titleBarUnderline.hidden = !show; -} - --(void) SetTitleBarHeightHint: (double) height -{ - _titleBarHeightHint = height; - - [self setFrameSize:self.frame.size]; -} - --(void)setFrameSize:(NSSize)newSize -{ - if(_settingSize) - { - return; - } - - _settingSize = true; - [super setFrameSize:newSize]; - - auto window = objc_cast([self window]); - - // TODO get actual titlebar size - - double height = _titleBarHeightHint == -1 ? [window getExtendedTitleBarHeight] : _titleBarHeightHint; - - NSRect tbar; - tbar.origin.x = 0; - tbar.origin.y = newSize.height - height; - tbar.size.width = newSize.width; - tbar.size.height = height; - - [_titleBarMaterial setFrame:tbar]; - tbar.size.height = height < 1 ? 0 : 1; - [_titleBarUnderline setFrame:tbar]; - - _settingSize = false; -} - --(void) SetContent: (NSView* _Nonnull) content -{ - if(content != nullptr) - { - [content removeFromSuperview]; - [self addSubview:content]; - _content = content; - } -} -@end - -@implementation AvnView -{ - ComPtr _parent; - ComPtr _swRenderedFrame; - AvnFramebuffer _swRenderedFrameBuffer; - bool _queuedDisplayFromThread; - NSTrackingArea* _area; - bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver; - AvnInputModifiers _modifierState; - NSEvent* _lastMouseDownEvent; - bool _lastKeyHandled; - AvnPixelSize _lastPixelSize; - NSObject* _renderTarget; - AvnPlatformResizeReason _resizeReason; -} - -- (void)onClosed -{ - @synchronized (self) - { - _parent = nullptr; - } -} - --(AvnPixelSize) getPixelSize -{ - return _lastPixelSize; -} - -- (NSEvent*) lastMouseDownEvent -{ - return _lastMouseDownEvent; -} - -- (void) updateRenderTarget -{ - [_renderTarget resize:_lastPixelSize withScale: [[self window] backingScaleFactor]]; - [self setNeedsDisplayInRect:[self frame]]; -} - --(AvnView*) initWithParent: (WindowBaseImpl*) parent -{ - self = [super init]; - _renderTarget = parent->renderTarget; - [self setWantsLayer:YES]; - [self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize]; - - _parent = parent; - _area = nullptr; - _lastPixelSize.Height = 100; - _lastPixelSize.Width = 100; - [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; - - _modifierState = AvnInputModifiersNone; - return self; -} - -- (BOOL)isFlipped -{ - return YES; -} - -- (BOOL)wantsUpdateLayer -{ - return YES; -} - -- (void)setLayer:(CALayer *)layer -{ - [_renderTarget setNewLayer: layer]; - [super setLayer: layer]; -} - -- (BOOL)isOpaque -{ - return YES; -} - -- (BOOL)acceptsFirstResponder -{ - return true; -} - -- (BOOL)acceptsFirstMouse:(NSEvent *)event -{ - return true; -} - -- (BOOL)canBecomeKeyView -{ - return true; -} - --(void)setFrameSize:(NSSize)newSize -{ - [super setFrameSize:newSize]; - - if(_area != nullptr) - { - [self removeTrackingArea:_area]; - _area = nullptr; - } - - if (_parent == nullptr) - { - return; - } - - NSRect rect = NSZeroRect; - rect.size = newSize; - - NSTrackingAreaOptions options = NSTrackingActiveAlways | NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingEnabledDuringMouseDrag; - _area = [[NSTrackingArea alloc] initWithRect:rect options:options owner:self userInfo:nullptr]; - [self addTrackingArea:_area]; - - _parent->UpdateCursor(); - - auto fsize = [self convertSizeToBacking: [self frame].size]; - - if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height) - { - _lastPixelSize.Width = (int)fsize.width; - _lastPixelSize.Height = (int)fsize.height; - [self updateRenderTarget]; - - auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); - } -} - -- (void)updateLayer -{ - AvnInsidePotentialDeadlock deadlock; - if (_parent == nullptr) - { - return; - } - - _parent->BaseEvents->RunRenderPriorityJobs(); - - if (_parent == nullptr) - { - return; - } - - _parent->BaseEvents->Paint(); -} - -- (void)drawRect:(NSRect)dirtyRect -{ - return; -} - --(void) setSwRenderedFrame: (AvnFramebuffer*) fb dispose: (IUnknown*) dispose -{ - @autoreleasepool { - [_renderTarget setSwFrame:fb]; - dispose->Release(); - } -} - -- (AvnPoint) translateLocalPoint:(AvnPoint)pt -{ - pt.Y = [self bounds].size.height - pt.Y; - return pt; -} - -+ (AvnPoint)toAvnPoint:(CGPoint)p -{ - AvnPoint result; - - result.X = p.x; - result.Y = p.y; - - return result; -} - -- (void) viewDidChangeBackingProperties -{ - auto fsize = [self convertSizeToBacking: [self frame].size]; - _lastPixelSize.Width = (int)fsize.width; - _lastPixelSize.Height = (int)fsize.height; - [self updateRenderTarget]; - - if(_parent != nullptr) - { - _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); - } - - [super viewDidChangeBackingProperties]; -} - -- (bool) ignoreUserInput:(bool)trigerInputWhenDisabled -{ - auto parentWindow = objc_cast([self window]); - - if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) - { - if(trigerInputWhenDisabled) - { - auto window = dynamic_cast(_parent.getRaw()); - - if(window != nullptr) - { - window->WindowEvents->GotInputWhenDisabled(); - } - } - - return TRUE; - } - - return FALSE; -} - -- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type -{ - bool triggerInputWhenDisabled = type != Move; - - if([self ignoreUserInput: triggerInputWhenDisabled]) - { - return; - } - - auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; - auto avnPoint = [AvnView toAvnPoint:localPoint]; - auto point = [self translateLocalPoint:avnPoint]; - AvnVector delta; - - if(type == Wheel) - { - auto speed = 5; - - if([event hasPreciseScrollingDeltas]) - { - speed = 50; - } - - delta.X = [event scrollingDeltaX] / speed; - delta.Y = [event scrollingDeltaY] / speed; - - if(delta.X == 0 && delta.Y == 0) - { - return; - } - } - else if (type == Magnify) - { - delta.X = delta.Y = [event magnification]; - } - else if (type == Rotate) - { - delta.X = delta.Y = [event rotation]; - } - else if (type == Swipe) - { - delta.X = [event deltaX]; - delta.Y = [event deltaY]; - } - - auto timestamp = [event timestamp] * 1000; - auto modifiers = [self getModifiers:[event modifierFlags]]; - - if(type != AvnRawMouseEventType::Move || - ( - [self window] != nil && - ( - [[self window] firstResponder] == nil - || ![[[self window] firstResponder] isKindOfClass: [NSView class]] - ) - ) - ) - [self becomeFirstResponder]; - - if(_parent != nullptr) - { - _parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta); - } - - [super mouseMoved:event]; -} - -- (BOOL) resignFirstResponder -{ - _parent->BaseEvents->LostFocus(); - return YES; -} - -- (void)mouseMoved:(NSEvent *)event -{ - [self mouseEvent:event withType:Move]; -} - -- (void)mouseDown:(NSEvent *)event -{ - _isLeftPressed = true; - _lastMouseDownEvent = event; - [self mouseEvent:event withType:LeftButtonDown]; -} - -- (void)otherMouseDown:(NSEvent *)event -{ - _lastMouseDownEvent = event; - - switch(event.buttonNumber) - { - case 2: - case 3: - _isMiddlePressed = true; - [self mouseEvent:event withType:MiddleButtonDown]; - break; - case 4: - _isXButton1Pressed = true; - [self mouseEvent:event withType:XButton1Down]; - break; - case 5: - _isXButton2Pressed = true; - [self mouseEvent:event withType:XButton2Down]; - break; - } -} - -- (void)rightMouseDown:(NSEvent *)event -{ - _isRightPressed = true; - _lastMouseDownEvent = event; - [self mouseEvent:event withType:RightButtonDown]; -} - -- (void)mouseUp:(NSEvent *)event -{ - _isLeftPressed = false; - [self mouseEvent:event withType:LeftButtonUp]; -} - -- (void)otherMouseUp:(NSEvent *)event -{ - switch(event.buttonNumber) - { - case 2: - case 3: - _isMiddlePressed = false; - [self mouseEvent:event withType:MiddleButtonUp]; - break; - case 4: - _isXButton1Pressed = false; - [self mouseEvent:event withType:XButton1Up]; - break; - case 5: - _isXButton2Pressed = false; - [self mouseEvent:event withType:XButton2Up]; - break; - } -} - -- (void)rightMouseUp:(NSEvent *)event -{ - _isRightPressed = false; - [self mouseEvent:event withType:RightButtonUp]; -} - -- (void)mouseDragged:(NSEvent *)event -{ - [self mouseEvent:event withType:Move]; - [super mouseDragged:event]; -} - -- (void)otherMouseDragged:(NSEvent *)event -{ - [self mouseEvent:event withType:Move]; - [super otherMouseDragged:event]; -} - -- (void)rightMouseDragged:(NSEvent *)event -{ - [self mouseEvent:event withType:Move]; - [super rightMouseDragged:event]; -} - -- (void)scrollWheel:(NSEvent *)event -{ - [self mouseEvent:event withType:Wheel]; - [super scrollWheel:event]; -} - -- (void)magnifyWithEvent:(NSEvent *)event -{ - [self mouseEvent:event withType:Magnify]; - [super magnifyWithEvent:event]; -} - -- (void)rotateWithEvent:(NSEvent *)event -{ - [self mouseEvent:event withType:Rotate]; - [super rotateWithEvent:event]; -} - -- (void)swipeWithEvent:(NSEvent *)event -{ - [self mouseEvent:event withType:Swipe]; - [super swipeWithEvent:event]; -} - -- (void)mouseEntered:(NSEvent *)event -{ - _isMouseOver = true; - [super mouseEntered:event]; -} - -- (void)mouseExited:(NSEvent *)event -{ - _isMouseOver = false; - [self mouseEvent:event withType:LeaveWindow]; - [super mouseExited:event]; -} - -- (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type -{ - if([self ignoreUserInput: false]) - { - return; - } - - auto key = s_KeyMap[[event keyCode]]; - - auto timestamp = [event timestamp] * 1000; - auto modifiers = [self getModifiers:[event modifierFlags]]; - - if(_parent != nullptr) - { - _lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key); - } -} - -- (BOOL)performKeyEquivalent:(NSEvent *)event -{ - bool result = _lastKeyHandled; - - _lastKeyHandled = false; - - return result; -} - -- (void)flagsChanged:(NSEvent *)event -{ - auto newModifierState = [self getModifiers:[event modifierFlags]]; - - bool isAltCurrentlyPressed = (_modifierState & Alt) == Alt; - bool isControlCurrentlyPressed = (_modifierState & Control) == Control; - bool isShiftCurrentlyPressed = (_modifierState & Shift) == Shift; - bool isCommandCurrentlyPressed = (_modifierState & Windows) == Windows; - - bool isAltPressed = (newModifierState & Alt) == Alt; - bool isControlPressed = (newModifierState & Control) == Control; - bool isShiftPressed = (newModifierState & Shift) == Shift; - bool isCommandPressed = (newModifierState & Windows) == Windows; - - - if (isAltPressed && !isAltCurrentlyPressed) - { - [self keyboardEvent:event withType:KeyDown]; - } - else if (isAltCurrentlyPressed && !isAltPressed) - { - [self keyboardEvent:event withType:KeyUp]; - } - - if (isControlPressed && !isControlCurrentlyPressed) - { - [self keyboardEvent:event withType:KeyDown]; - } - else if (isControlCurrentlyPressed && !isControlPressed) - { - [self keyboardEvent:event withType:KeyUp]; - } - - if (isShiftPressed && !isShiftCurrentlyPressed) - { - [self keyboardEvent:event withType:KeyDown]; - } - else if(isShiftCurrentlyPressed && !isShiftPressed) - { - [self keyboardEvent:event withType:KeyUp]; - } - - if(isCommandPressed && !isCommandCurrentlyPressed) - { - [self keyboardEvent:event withType:KeyDown]; - } - else if(isCommandCurrentlyPressed && ! isCommandPressed) - { - [self keyboardEvent:event withType:KeyUp]; - } - - _modifierState = newModifierState; - - [[self inputContext] handleEvent:event]; - [super flagsChanged:event]; -} - -- (void)keyDown:(NSEvent *)event -{ - [self keyboardEvent:event withType:KeyDown]; - [[self inputContext] handleEvent:event]; - [super keyDown:event]; -} - -- (void)keyUp:(NSEvent *)event -{ - [self keyboardEvent:event withType:KeyUp]; - [super keyUp:event]; -} - -- (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod -{ - unsigned int rv = 0; - - if (mod & NSEventModifierFlagControl) - rv |= Control; - if (mod & NSEventModifierFlagShift) - rv |= Shift; - if (mod & NSEventModifierFlagOption) - rv |= Alt; - if (mod & NSEventModifierFlagCommand) - rv |= Windows; - - if (_isLeftPressed) - rv |= LeftMouseButton; - if (_isMiddlePressed) - rv |= MiddleMouseButton; - if (_isRightPressed) - rv |= RightMouseButton; - if (_isXButton1Pressed) - rv |= XButton1MouseButton; - if (_isXButton2Pressed) - rv |= XButton2MouseButton; - - return (AvnInputModifiers)rv; -} - -- (BOOL)hasMarkedText -{ - return _lastKeyHandled; -} - -- (NSRange)markedRange -{ - return NSMakeRange(NSNotFound, 0); -} - -- (NSRange)selectedRange -{ - return NSMakeRange(NSNotFound, 0); -} - -- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange -{ - -} - -- (void)unmarkText -{ - -} - -- (NSArray *)validAttributesForMarkedText -{ - return [NSArray new]; -} - -- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange -{ - return [NSAttributedString new]; -} - -- (void)insertText:(id)string replacementRange:(NSRange)replacementRange -{ - if(!_lastKeyHandled) - { - if(_parent != nullptr) - { - _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); - } - } -} - -- (NSUInteger)characterIndexForPoint:(NSPoint)point -{ - return 0; -} - -- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange -{ - CGRect result; - - return result; -} - -- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info -{ - auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; - auto avnPoint = [AvnView toAvnPoint:localPoint]; - auto point = [self translateLocalPoint:avnPoint]; - auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; - NSDragOperation nsop = [info draggingSourceOperationMask]; - - auto effects = ConvertDragDropEffects(nsop); - int reffects = (int)_parent->BaseEvents - ->DragEvent(type, point, modifiers, effects, - CreateClipboard([info draggingPasteboard], nil), - GetAvnDataObjectHandleFromDraggingInfo(info)); - - NSDragOperation ret = 0; - - // Ensure that the managed part didn't add any new effects - reffects = (int)effects & (int)reffects; - - // OSX requires exactly one operation - if((reffects & (int)AvnDragDropEffects::Copy) != 0) - ret = NSDragOperationCopy; - else if((reffects & (int)AvnDragDropEffects::Move) != 0) - ret = NSDragOperationMove; - else if((reffects & (int)AvnDragDropEffects::Link) != 0) - ret = NSDragOperationLink; - if(ret == 0) - ret = NSDragOperationNone; - return ret; -} - -- (NSDragOperation)draggingEntered:(id )sender -{ - return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender]; -} - -- (NSDragOperation)draggingUpdated:(id )sender -{ - return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender]; -} - -- (void)draggingExited:(id )sender -{ - [self triggerAvnDragEvent: AvnDragEventType::Leave info:sender]; -} - -- (BOOL)prepareForDragOperation:(id )sender -{ - return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone; -} - -- (BOOL)performDragOperation:(id )sender -{ - return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone; -} - -- (void)concludeDragOperation:(nullable id )sender -{ - -} - -- (AvnPlatformResizeReason)getResizeReason -{ - return _resizeReason; -} - -- (void)setResizeReason:(AvnPlatformResizeReason)reason -{ - _resizeReason = reason; -} - -@end - - -@implementation AvnWindow -{ - ComPtr _parent; - bool _canBecomeKeyAndMain; - bool _closed; - bool _isEnabled; - bool _isExtended; - AvnMenu* _menu; - double _lastScaling; -} - --(void) setIsExtended:(bool)value; -{ - _isExtended = value; -} - --(bool) isDialog -{ - return _parent->IsDialog(); -} - --(double) getScaling -{ - return _lastScaling; -} - --(double) getExtendedTitleBarHeight -{ - if(_isExtended) - { - for (id subview in self.contentView.superview.subviews) - { - if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) - { - NSView *titlebarView = [subview subviews][0]; - - return (double)titlebarView.frame.size.height; - } - } - - return -1; - } - else - { - return 0; - } -} - -+(void)closeAll -{ - [[NSApplication sharedApplication] terminate:self]; -} - -- (void)performClose:(id)sender -{ - if([[self delegate] respondsToSelector:@selector(windowShouldClose:)]) - { - if(![[self delegate] windowShouldClose:self]) return; - } - else if([self respondsToSelector:@selector(windowShouldClose:)]) - { - if(![self windowShouldClose:self]) return; - } - - [self close]; -} - -- (void)pollModalSession:(nonnull NSModalSession)session -{ - auto response = [NSApp runModalSession:session]; - - if(response == NSModalResponseContinue) - { - dispatch_async(dispatch_get_main_queue(), ^{ - [self pollModalSession:session]; - }); - } - else if (!_closed) - { - [self orderOut:self]; - [NSApp endModalSession:session]; - } -} - --(void) showWindowMenuWithAppMenu -{ - if(_menu != nullptr) - { - auto appMenuItem = ::GetAppMenuItem(); - - if(appMenuItem != nullptr) - { - auto appMenu = [appMenuItem menu]; - - [appMenu removeItem:appMenuItem]; - - [_menu insertItem:appMenuItem atIndex:0]; - - [_menu setHasGlobalMenuItem:true]; - } - - [NSApp setMenu:_menu]; - } - else - { - [self showAppMenuOnly]; - } -} - --(void) showAppMenuOnly -{ - auto appMenuItem = ::GetAppMenuItem(); - - if(appMenuItem != nullptr) - { - auto appMenu = ::GetAppMenu(); - - auto nativeAppMenu = dynamic_cast(appMenu); - - [[appMenuItem menu] removeItem:appMenuItem]; - - if(_menu != nullptr) - { - [_menu setHasGlobalMenuItem:false]; - } - - [nativeAppMenu->GetNative() addItem:appMenuItem]; - - [NSApp setMenu:nativeAppMenu->GetNative()]; - } - else - { - [NSApp setMenu:nullptr]; - } -} - --(void) applyMenu:(AvnMenu *)menu -{ - if(menu == nullptr) - { - menu = [AvnMenu new]; - } - - _menu = menu; -} - --(void) setCanBecomeKeyAndMain -{ - _canBecomeKeyAndMain = true; -} - --(AvnWindow*) initWithParent: (WindowBaseImpl*) parent -{ - self = [super init]; - [self setReleasedWhenClosed:false]; - _parent = parent; - [self setDelegate:self]; - _closed = false; - _isEnabled = true; - - _lastScaling = [self backingScaleFactor]; - [self setOpaque:NO]; - [self setBackgroundColor: [NSColor clearColor]]; - _isExtended = false; - return self; -} - -- (BOOL)windowShouldClose:(NSWindow *)sender -{ - auto window = dynamic_cast(_parent.getRaw()); - - if(window != nullptr) - { - return !window->WindowEvents->Closing(); - } - - return true; -} - -- (void)windowDidChangeBackingProperties:(NSNotification *)notification -{ - _lastScaling = [self backingScaleFactor]; -} - -- (void)windowWillClose:(NSNotification *)notification -{ - _closed = true; - if(_parent) - { - ComPtr parent = _parent; - _parent = NULL; - [self restoreParentWindow]; - parent->BaseEvents->Closed(); - [parent->View onClosed]; - } -} - --(BOOL)canBecomeKeyWindow -{ - if (_canBecomeKeyAndMain) - { - // If the window has a child window being shown as a dialog then don't allow it to become the key window. - for(NSWindow* uch in [self childWindows]) - { - auto ch = objc_cast(uch); - if(ch == nil) - continue; - if (ch.isDialog) - return false; - } - - return true; - } - - return false; -} - --(BOOL)canBecomeMainWindow -{ - return _canBecomeKeyAndMain; -} - --(bool)shouldTryToHandleEvents -{ - return _isEnabled; -} - --(void) setEnabled:(bool)enable -{ - _isEnabled = enable; -} - --(void)becomeKeyWindow -{ - [self showWindowMenuWithAppMenu]; - - if(_parent != nullptr) - { - _parent->BaseEvents->Activated(); - } - - [super becomeKeyWindow]; -} - --(void) restoreParentWindow; -{ - auto parent = objc_cast([self parentWindow]); - if(parent != nil) - { - [parent removeChildWindow:self]; - } -} - -- (void)windowDidMiniaturize:(NSNotification *)notification -{ - auto parent = dynamic_cast(_parent.operator->()); - - if(parent != nullptr) - { - parent->WindowStateChanged(); - } -} - -- (void)windowDidDeminiaturize:(NSNotification *)notification -{ - auto parent = dynamic_cast(_parent.operator->()); - - if(parent != nullptr) - { - parent->WindowStateChanged(); - } -} - -- (void)windowDidResize:(NSNotification *)notification -{ - auto parent = dynamic_cast(_parent.operator->()); - - if(parent != nullptr) - { - parent->WindowStateChanged(); - } -} - -- (void)windowWillExitFullScreen:(NSNotification *)notification -{ - auto parent = dynamic_cast(_parent.operator->()); - - if(parent != nullptr) - { - parent->StartStateTransition(); - } -} - -- (void)windowDidExitFullScreen:(NSNotification *)notification -{ - auto parent = dynamic_cast(_parent.operator->()); - - if(parent != nullptr) - { - parent->EndStateTransition(); - - if(parent->Decorations() != SystemDecorationsFull && parent->WindowState() == Maximized) - { - NSRect screenRect = [[self screen] visibleFrame]; - [self setFrame:screenRect display:YES]; - } - - if(parent->WindowState() == Minimized) - { - [self miniaturize:nullptr]; - } - - parent->WindowStateChanged(); - } -} - -- (void)windowWillEnterFullScreen:(NSNotification *)notification -{ - auto parent = dynamic_cast(_parent.operator->()); - - if(parent != nullptr) - { - parent->StartStateTransition(); - } -} - -- (void)windowDidEnterFullScreen:(NSNotification *)notification -{ - auto parent = dynamic_cast(_parent.operator->()); - - if(parent != nullptr) - { - parent->EndStateTransition(); - parent->WindowStateChanged(); - } -} - -- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame -{ - return true; -} - --(void)resignKeyWindow -{ - if(_parent) - _parent->BaseEvents->Deactivated(); - - [self showAppMenuOnly]; - - [super resignKeyWindow]; -} - -- (void)windowDidMove:(NSNotification *)notification -{ - AvnPoint position; - - if(_parent != nullptr) - { - _parent->GetPosition(&position); - _parent->BaseEvents->PositionChanged(position); - } -} - -- (AvnPoint) translateLocalPoint:(AvnPoint)pt -{ - pt.Y = [self frame].size.height - pt.Y; - return pt; -} - -- (void)sendEvent:(NSEvent *)event -{ - [super sendEvent:event]; - - /// This is to detect non-client clicks. This can only be done on Windows... not popups, hence the dynamic_cast. - if(_parent != nullptr && dynamic_cast(_parent.getRaw()) != nullptr) - { - switch(event.type) - { - case NSEventTypeLeftMouseDown: - { - AvnView* view = _parent->View; - NSPoint windowPoint = [event locationInWindow]; - NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil]; - - if (!NSPointInRect(viewPoint, view.bounds)) - { - auto avnPoint = [AvnView toAvnPoint:windowPoint]; - auto point = [self translateLocalPoint:avnPoint]; - AvnVector delta; - - _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); - } - } - break; - - case NSEventTypeMouseEntered: - { - _parent->UpdateCursor(); - } - break; - - case NSEventTypeMouseExited: - { - [[NSCursor arrowCursor] set]; - } - break; - - default: - break; - } - } -} -@end - -class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup -{ -private: - BEGIN_INTERFACE_MAP() - INHERIT_INTERFACE_MAP(WindowBaseImpl) - INTERFACE_MAP_ENTRY(IAvnPopup, IID_IAvnPopup) - END_INTERFACE_MAP() - virtual ~PopupImpl(){} - ComPtr WindowEvents; - PopupImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) - { - WindowEvents = events; - [Window setLevel:NSPopUpMenuWindowLevel]; - } -protected: - virtual NSWindowStyleMask GetStyle() override - { - return NSWindowStyleMaskBorderless; - } - - virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override - { - START_COM_CALL; - - @autoreleasepool - { - if (Window != nullptr) - { - [Window setContentSize:NSSize{x, y}]; - - [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; - } - - return S_OK; - } - } -public: - virtual bool ShouldTakeFocusOnShow() override - { - return false; - } -}; - -extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl) -{ - @autoreleasepool - { - IAvnPopup* ptr = dynamic_cast(new PopupImpl(events, gl)); - return ptr; - } -} - -extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl) -{ - @autoreleasepool - { - IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events, gl); - return ptr; - } -} diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 72d90abbf3..f0f677b844 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -214,19 +214,14 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - RunCoreTest("Avalonia.Animation.UnitTests"); RunCoreTest("Avalonia.Base.UnitTests"); RunCoreTest("Avalonia.Controls.UnitTests"); RunCoreTest("Avalonia.Controls.DataGrid.UnitTests"); - RunCoreTest("Avalonia.Input.UnitTests"); - RunCoreTest("Avalonia.Interactivity.UnitTests"); - RunCoreTest("Avalonia.Layout.UnitTests"); RunCoreTest("Avalonia.Markup.UnitTests"); RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); - RunCoreTest("Avalonia.Styling.UnitTests"); - RunCoreTest("Avalonia.Visuals.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.PlatformSupport.UnitTests"); }); Target RunRenderTests => _ => _ diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 52b60b7d0f..b2c58e2292 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -8,11 +8,10 @@ False CS0649;CS0169 - + - 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/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index 2c6ff74e5e..bd6054327f 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -5,6 +5,7 @@
+ diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 1654d20c80..04c67e84e8 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -23,10 +23,25 @@ True True True + True - + + + False + False + + + + True + + + + + + + -
+ \ No newline at end of file diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 137f280e43..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/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 9729713833..dc292fd37b 100644 --- a/samples/ControlCatalog.Android/SplashActivity.cs +++ b/samples/ControlCatalog.Android/SplashActivity.cs @@ -1,9 +1,6 @@ using Android.App; using Android.Content; using Android.OS; -using Application = Android.App.Application; - -using Avalonia; namespace ControlCatalog.Android { @@ -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..2b45ac1508 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,13 @@ + + + + + + + en diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 0c8fd9465c..4464413e63 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -117,7 +117,13 @@ namespace ControlCatalog.NetCore EnableMultitouch = true }) .UseSkia() - .UseManagedSystemDialogs() + .AfterSetup(builder => + { + builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() + { + StartupScreenIndex = 1, + }); + }) .LogToTrace(); static void SilenceConsole() diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj index 520bbdf32b..b2c9ec72eb 100644 --- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj +++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj @@ -1,15 +1,14 @@  net6.0 - false enable - True + + true + 16777216 + false + false - - - - false @@ -23,19 +22,36 @@ -O3 -O3 false + false + false + false + false + false + true + false + true + true + true + link + true - - + + - - - - - + + + + + + + + + + diff --git a/samples/ControlCatalog.Web/LinkerConfig.xml b/samples/ControlCatalog.Web/LinkerConfig.xml deleted file mode 100644 index 5839a0fe03..0000000000 --- a/samples/ControlCatalog.Web/LinkerConfig.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index db1e16166a..513ac44f83 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -1,186 +1,16 @@ - - + - Debug - iPhoneSimulator - {57E0455D-D565-44BB-B069-EE1AA20F8337} - {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} Exe - ControlCatalog.iOS - Resources - ControlCatalogiOS - true - NSUrlSessionHandler - PackageReference - automatic + manual + net6.0-ios + 10.0 + + True + iossimulator-x64 + - - true - full - false - bin\iPhoneSimulator\Debug - DEBUG - prompt - 4 - false - x86_64 - None - True - 9.1 - False - False - False - False - False - False - False - False - True - Default - HttpClientHandler - False - - - none - true - bin\iPhoneSimulator\Release - prompt - 4 - None - x86_64 - false - - - true - full - false - bin\iPhone\Debug - DEBUG - prompt - 4 - false - ARMv7, ARM64 - Entitlements.plist - iPhone Developer - true - - - none - true - bin\iPhone\Release - prompt - 4 - Entitlements.plist - ARMv7, ARM64 - false - iPhone Developer - - - none - True - bin\iPhone\Ad-Hoc - prompt - 4 - False - ARMv7, ARM64 - Entitlements.plist - True - Automatic:AdHoc - iPhone Distribution - - - none - True - bin\iPhone\AppStore - prompt - 4 - False - ARMv7, ARM64 - Entitlements.plist - Automatic:AppStore - iPhone Distribution - - - - - - - - - - - - - - - - - {4488AD85-1495-4809-9AA4-DDFE0A48527E} - Avalonia.iOS - false - false - - - {3E53A01A-B331-47F3-B828-4A5717E77A24} - Avalonia.Markup.Xaml - - - {6417E941-21BC-467B-A771-0DE389353CE6} - Avalonia.Markup - - - {D211E587-D8BC-45B9-95A4-F297C8FA5200} - Avalonia.Animation - - - {B09B78D8-9B26-48B0-9149-D64A2F120F3F} - Avalonia.Base - - - {D2221C82-4A25-4583-9B43-D791E3F6820C} - Avalonia.Controls - - - {7062AE20-5DCC-4442-9645-8195BDECE63E} - Avalonia.Diagnostics - - - {62024B2D-53EB-4638-B26B-85EEAA54866E} - Avalonia.Input - - - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B} - Avalonia.Interactivity - - - {42472427-4774-4C81-8AFF-9F27B8E31721} - Avalonia.Layout - - - {EB582467-6ABB-43A1-B052-E981BA910E3A} - Avalonia.Visuals - - - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F} - Avalonia.Styling - - - {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F} - Avalonia.Themes.Default - - - {7d2d3083-71dd-4cc9-8907-39a0d86fb322} - Avalonia.Skia - - - {d0a739b9-3c68-4ba6-a328-41606954b6bd} - ControlCatalog - - + + - - - - - + \ No newline at end of file diff --git a/samples/ControlCatalog.iOS/Info.plist b/samples/ControlCatalog.iOS/Info.plist index 216fd9c333..6ffe3ba662 100644 --- a/samples/ControlCatalog.iOS/Info.plist +++ b/samples/ControlCatalog.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleDisplayName ControlCatalog.iOS CFBundleIdentifier - com.companyname.ControlCatalog.iOS + Avalonia.ControlCatalog CFBundleShortVersionString 1.0 CFBundleVersion @@ -13,7 +13,7 @@ LSRequiresIPhoneOS MinimumOSVersion - 8.0 + 10.0 UIDeviceFamily 1 @@ -28,6 +28,7 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight @@ -38,5 +39,9 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIStatusBarHidden + + UIViewControllerBasedStatusBarAppearance + diff --git a/samples/ControlCatalog.iOS/Main.cs b/samples/ControlCatalog.iOS/Main.cs index fe039ba69e..2400115041 100644 --- a/samples/ControlCatalog.iOS/Main.cs +++ b/samples/ControlCatalog.iOS/Main.cs @@ -9,7 +9,7 @@ namespace ControlCatalog.iOS { // if you want to use a different Application Delegate class from "AppDelegate" // you can specify it here. - UIApplication.Main(args, null, "AppDelegate"); + UIApplication.Main(args, null, typeof(AppDelegate)); } } -} \ No newline at end of file +} diff --git a/samples/ControlCatalog.iOS/Properties/AssemblyInfo.cs b/samples/ControlCatalog.iOS/Properties/AssemblyInfo.cs deleted file mode 100644 index 0a5a598651..0000000000 --- a/samples/ControlCatalog.iOS/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("ControlCatalog.iOS")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ControlCatalog.iOS")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("57e0455d-d565-44bb-b069-ee1aa20f8337")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/ControlCatalog.iOS/Resources/LaunchScreen.xib b/samples/ControlCatalog.iOS/Resources/LaunchScreen.xib index be4abb2b43..5d3ccc97db 100644 --- a/samples/ControlCatalog.iOS/Resources/LaunchScreen.xib +++ b/samples/ControlCatalog.iOS/Resources/LaunchScreen.xib @@ -11,7 +11,7 @@ - - + + + TextFormatterPage.axaml + Code + + diff --git a/samples/RenderDemo/ViewModels/Transform3DPageViewModel.cs b/samples/RenderDemo/ViewModels/Transform3DPageViewModel.cs new file mode 100644 index 0000000000..c8d1d40e3a --- /dev/null +++ b/samples/RenderDemo/ViewModels/Transform3DPageViewModel.cs @@ -0,0 +1,55 @@ +using System; +using MiniMvvm; +using Avalonia.Animation; + +namespace RenderDemo.ViewModels +{ + public class Transform3DPageViewModel : ViewModelBase + { + private double _depth = 200; + + private double _centerX = 0; + private double _centerY = 0; + private double _centerZ = 0; + private double _angleX = 0; + private double _angleY = 0; + private double _angleZ = 0; + + public double Depth + { + get => _depth; + set => RaiseAndSetIfChanged(ref _depth, value); + } + + public double CenterX + { + get => _centerX; + set => RaiseAndSetIfChanged(ref _centerX, value); + } + public double CenterY + { + get => _centerY; + set => RaiseAndSetIfChanged(ref _centerY, value); + } + public double CenterZ + { + get => _centerZ; + set => RaiseAndSetIfChanged(ref _centerZ, value); + } + public double AngleX + { + get => _angleX; + set => RaiseAndSetIfChanged(ref _angleX, value); + } + public double AngleY + { + get => _angleY; + set => RaiseAndSetIfChanged(ref _angleY, value); + } + public double AngleZ + { + get => _angleZ; + set => RaiseAndSetIfChanged(ref _angleZ, value); + } + } +} diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs b/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs index bbfd3d87ca..ab61dcde91 100644 --- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs +++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs @@ -43,14 +43,13 @@ namespace ControlSamples _splitView = e.NameScope.Find("PART_NavigationPane"); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == BoundsProperty && _splitView is not null) { - var oldBounds = change.OldValue.GetValueOrDefault(); - var newBounds = change.NewValue.GetValueOrDefault(); + var (oldBounds, newBounds) = change.GetOldAndNewValue(); EnsureSplitViewMode(oldBounds, newBounds); } } diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml index 1aa7f8ea04..1d58c465a0 100644 --- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml +++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml @@ -22,7 +22,7 @@ 40 - 200 + 220 36 36 32 @@ -195,9 +195,9 @@ VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}" - TextBlock.FontFamily="{TemplateBinding FontFamily}" - TextBlock.FontSize="{TemplateBinding FontSize}" - TextBlock.FontWeight="{TemplateBinding FontWeight}" /> + TextElement.FontFamily="{TemplateBinding FontFamily}" + TextElement.FontSize="{TemplateBinding FontSize}" + TextElement.FontWeight="{TemplateBinding FontWeight}" /> @@ -216,25 +216,25 @@ Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" CornerRadius="{TemplateBinding CornerRadius}" - TextBlock.FontFamily="{TemplateBinding FontFamily}" - TextBlock.FontSize="{TemplateBinding FontSize}" - TextBlock.FontWeight="{TemplateBinding FontWeight}" /> + TextElement.FontFamily="{TemplateBinding FontFamily}" + TextElement.FontSize="{TemplateBinding FontSize}" + TextElement.FontWeight="{TemplateBinding FontWeight}" /> + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml new file mode 100644 index 0000000000..19f10201a5 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml new file mode 100644 index 0000000000..78e6da8aa3 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml new file mode 100644 index 0000000000..6d2f979f6e --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml new file mode 100644 index 0000000000..cb764a738c --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml new file mode 100644 index 0000000000..18a081721a --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml new file mode 100644 index 0000000000..ac8e2a9c06 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml new file mode 100644 index 0000000000..c25d79727f --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt b/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt new file mode 100644 index 0000000000..bfcec2960a --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt @@ -0,0 +1,4 @@ +Compat issues with assembly Avalonia.Controls.DataGrid: +MembersMustExist : Member 'protected void Avalonia.Controls.DataGridCheckBoxColumn.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.DataGridTextColumn.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. +Total Issues: 2 diff --git a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj index c89157dc0f..6369961f0f 100644 --- a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj +++ b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj @@ -4,14 +4,8 @@ Avalonia.Controls.DataGrid - - - - - - @@ -23,4 +17,10 @@ + + + + + + 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.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 5d71a499e3..aaac3f8f9c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -32,6 +32,14 @@ namespace Avalonia.Controls /// /// Displays data in a customizable grid. /// + [TemplatePart(DATAGRID_elementBottomRightCornerHeaderName, typeof(IVisual))] + [TemplatePart(DATAGRID_elementColumnHeadersPresenterName, typeof(DataGridColumnHeadersPresenter))] + [TemplatePart(DATAGRID_elementFrozenColumnScrollBarSpacerName, typeof(Control))] + [TemplatePart(DATAGRID_elementHorizontalScrollbarName, typeof(ScrollBar))] + [TemplatePart(DATAGRID_elementRowsPresenterName, typeof(DataGridRowsPresenter))] + [TemplatePart(DATAGRID_elementTopLeftCornerHeaderName, typeof(ContentControl))] + [TemplatePart(DATAGRID_elementTopRightCornerHeaderName, typeof(ContentControl))] + [TemplatePart(DATAGRID_elementVerticalScrollbarName, typeof(ScrollBar))] [PseudoClasses(":invalid", ":empty-rows", ":empty-columns")] public partial class DataGrid : TemplatedControl { @@ -669,8 +677,6 @@ namespace Avalonia.Controls ItemsProperty.Changed.AddClassHandler((x, e) => x.OnItemsPropertyChanged(e)); CanUserResizeColumnsProperty.Changed.AddClassHandler((x, e) => x.OnCanUserResizeColumnsChanged(e)); ColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnColumnWidthChanged(e)); - RowBackgroundProperty.Changed.AddClassHandler((x, e) => x.OnRowBackgroundChanged(e)); - AlternatingRowBackgroundProperty.Changed.AddClassHandler((x, e) => x.OnRowBackgroundChanged(e)); FrozenColumnCountProperty.Changed.AddClassHandler((x, e) => x.OnFrozenColumnCountChanged(e)); GridLinesVisibilityProperty.Changed.AddClassHandler((x, e) => x.OnGridLinesVisibilityChanged(e)); HeadersVisibilityProperty.Changed.AddClassHandler((x, e) => x.OnHeadersVisibilityChanged(e)); @@ -1144,14 +1150,6 @@ namespace Avalonia.Controls InvalidateCellsArrange(); } - private void OnRowBackgroundChanged(AvaloniaPropertyChangedEventArgs e) - { - foreach (DataGridRow row in GetAllRows()) - { - row.EnsureBackground(); - } - } - private void OnColumnWidthChanged(AvaloniaPropertyChangedEventArgs e) { var value = (DataGridLength)e.NewValue; @@ -2060,6 +2058,25 @@ namespace Avalonia.Controls forceHorizontalScroll: true); } } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + if (DataConnection.DataSource != null && !DataConnection.EventsWired) + { + DataConnection.WireEvents(DataConnection.DataSource); + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + // When wired to INotifyCollectionChanged, the DataGrid will be cleaned up by GC + if (DataConnection.DataSource != null && DataConnection.EventsWired) + { + DataConnection.UnWireEvents(DataConnection.DataSource); + } + } /// /// Arranges the content of the . diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index e3f150f5c4..67183781d3 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -13,6 +13,7 @@ namespace Avalonia.Controls /// /// Represents an individual cell. /// + [TemplatePart(DATAGRIDCELL_elementRightGridLine, typeof(Rectangle))] [PseudoClasses(":selected", ":current", ":edited", ":invalid")] public class DataGridCell : ContentControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs index ccf1f3f77a..9826c15598 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs @@ -46,7 +46,7 @@ namespace Avalonia.Controls set => SetValue(IsThreeStateProperty, value); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index a77b482436..f3ea48ff80 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -192,14 +192,14 @@ namespace Avalonia.Controls set => SetValue(IsVisibleProperty, value); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IsVisibleProperty) { OwningGrid?.OnColumnVisibleStateChanging(this); - var isVisible = (change as AvaloniaPropertyChangedEventArgs).NewValue.Value; + var isVisible = change.GetNewValue(); if (_headerCell != null) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs index 922b1d9c08..e7f9a9a6c4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs @@ -12,7 +12,8 @@ namespace Avalonia.Controls { internal class DataGridColumnCollection : ObservableCollection { - private DataGrid _owningGrid; + private readonly Dictionary _columnsMap = new Dictionary(); + private readonly DataGrid _owningGrid; public DataGridColumnCollection(DataGrid owningGrid) { @@ -124,18 +125,8 @@ namespace Avalonia.Controls internal int VisibleColumnCount { - get - { - int visibleColumnCount = 0; - for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++) - { - if (ItemsInternal[columnIndex].IsVisible) - { - visibleColumnCount++; - } - } - return visibleColumnCount; - } + get; + private set; } internal double VisibleEdgedColumnsWidth @@ -287,20 +278,31 @@ namespace Avalonia.Controls { VisibleStarColumnCount = 0; VisibleEdgedColumnsWidth = 0; + VisibleColumnCount = 0; + _columnsMap.Clear(); + for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++) { - if (ItemsInternal[columnIndex].IsVisible) + var item = ItemsInternal[columnIndex]; + _columnsMap[columnIndex] = item.DisplayIndex; + if (item.IsVisible) { - ItemsInternal[columnIndex].EnsureWidth(); - if (ItemsInternal[columnIndex].Width.IsStar) + VisibleColumnCount++; + item.EnsureWidth(); + if (item.Width.IsStar) { VisibleStarColumnCount++; } - VisibleEdgedColumnsWidth += ItemsInternal[columnIndex].ActualWidth; + VisibleEdgedColumnsWidth += item.ActualWidth; } } } + internal int GetColumnDisplayIndex(int columnIndex) + { + return _columnsMap.TryGetValue(columnIndex, out var displayIndex) ? displayIndex : -1; + } + internal DataGridColumn GetColumnAtDisplayIndex(int displayIndex) { if (displayIndex < 0 || displayIndex >= ItemsInternal.Count || displayIndex >= DisplayIndexMap.Count) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs index a4577ee952..52f0ad7537 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs @@ -444,12 +444,11 @@ namespace Avalonia.Controls // We need to explicitly collapse the cells of the invisible column because layout only goes through // visible ones - if (!updatedColumn.IsVisible) + ColumnHeaders?.InvalidateChildIndex(); + foreach (var row in GetAllRows()) { - foreach (DataGridRow row in GetAllRows()) - { - row.Cells[updatedColumn.Index].IsVisible = false; - } + row.Cells[updatedColumn.Index].IsVisible = updatedColumn.IsVisible; + row.InvalidateCellsIndex(); } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs index fade597ca1..ae52e5f970 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs @@ -77,7 +77,7 @@ namespace Avalonia.Controls private set; } - public int Count => GetCount(true); + public int Count => TryGetCount(true, false, out var count) ? count : 0; public bool DataIsPrimitive { @@ -193,22 +193,28 @@ namespace Avalonia.Controls } } - internal bool Any() - { - return GetCount(false) > 0; + /// Try get number of DataSource itmes. + /// When "allowSlow" is false, method will not use Linq.Count() method and will return 0 or 1 instead. + /// If "getAny" is true, method can use Linq.Any() method to speedup. + /// number of DataSource itmes. + /// true if able to retrieve number of DataSource itmes; otherwise, false. + internal bool TryGetCount(bool allowSlow, bool getAny, out int count) + { + bool result; + (result, count) = DataSource switch + { + ICollection collection => (true, collection.Count), + DataGridCollectionView cv => (true, cv.Count), + IEnumerable enumerable when allowSlow && !getAny => (true, enumerable.Cast().Count()), + IEnumerable enumerable when getAny => (true, enumerable.Cast().Any() ? 1 : 0), + _ => (false, 0) + }; + return result; } - /// When "allowSlow" is false, method will not use Linq.Count() method and will return 0 or 1 instead. - private int GetCount(bool allowSlow) + internal bool Any() { - return DataSource switch - { - ICollection collection => collection.Count, - DataGridCollectionView cv => cv.Count, - IEnumerable enumerable when allowSlow => enumerable.Cast().Count(), - IEnumerable enumerable when !allowSlow => enumerable.Cast().Any() ? 1 : 0, - _ => 0 - }; + return TryGetCount(false, true, out var count) && count > 0; } /// @@ -383,7 +389,7 @@ namespace Avalonia.Controls List propertyNames = TypeHelper.SplitPropertyPath(propertyName); for (int i = 0; i < propertyNames.Count; i++) { - propertyInfo = propertyType.GetPropertyOrIndexer(propertyNames[i], out object[] index); + propertyInfo = propertyType.GetPropertyOrIndexer(propertyNames[i], out _); if (propertyInfo == null || propertyType.GetIsReadOnly() || propertyInfo.GetIsReadOnly()) { // Either the data type is read-only, the property doesn't exist, or it does exist but is read-only @@ -391,11 +397,10 @@ namespace Avalonia.Controls } // Check if EditableAttribute is defined on the property and if it indicates uneditable - object[] attributes = propertyInfo.GetCustomAttributes(typeof(EditableAttribute), true); + var attributes = propertyInfo.GetCustomAttributes(typeof(EditableAttribute), true); if (attributes != null && attributes.Length > 0) { - EditableAttribute editableAttribute = attributes[0] as EditableAttribute; - Debug.Assert(editableAttribute != null); + var editableAttribute = (EditableAttribute)attributes[0]; if (!editableAttribute.AllowEdit) { return true; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index 1efce7c0b8..b062a14e39 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -21,6 +21,11 @@ namespace Avalonia.Controls /// /// Represents a row. /// + [TemplatePart(DATAGRIDROW_elementBottomGridLine, typeof(Rectangle))] + [TemplatePart(DATAGRIDROW_elementCells, typeof(DataGridCellsPresenter))] + [TemplatePart(DATAGRIDROW_elementDetails, typeof(DataGridDetailsPresenter))] + [TemplatePart(DATAGRIDROW_elementRoot, typeof(Panel))] + [TemplatePart(DATAGRIDROW_elementRowHeader, typeof(DataGridRowHeader))] [PseudoClasses(":selected", ":editing", ":invalid")] public class DataGridRow : TemplatedControl { @@ -543,7 +548,6 @@ namespace Avalonia.Controls RootElement = e.NameScope.Find(DATAGRIDROW_elementRoot); if (RootElement != null) { - EnsureBackground(); UpdatePseudoClasses(); } @@ -668,43 +672,9 @@ namespace Avalonia.Controls Slot = -1; } - // Make sure the row's background is set to its correct value. It could be explicity set or inherit - // DataGrid.RowBackground or DataGrid.AlternatingRowBackground - internal void EnsureBackground() + internal void InvalidateCellsIndex() { - // Inherit the DataGrid's RowBackground properties only if this row doesn't explicity have a background set - if (RootElement != null && OwningGrid != null) - { - IBrush newBackground = null; - if (Background == null) - { - if (Index % 2 == 0 || OwningGrid.AlternatingRowBackground == null) - { - // Use OwningGrid.RowBackground if the index is even or if the OwningGrid.AlternatingRowBackground is null - if (OwningGrid.RowBackground != null) - { - newBackground = OwningGrid.RowBackground; - } - } - else - { - // Alternate row - if (OwningGrid.AlternatingRowBackground != null) - { - newBackground = OwningGrid.AlternatingRowBackground; - } - } - } - else - { - newBackground = Background; - } - - if (RootElement.Background != newBackground) - { - RootElement.Background = newBackground; - } - } + _cellsElement?.InvalidateChildIndex(); } internal void EnsureFillerVisibility() @@ -1092,7 +1062,7 @@ namespace Avalonia.Controls } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == DataContextProperty) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 49ca23d34c..a3dfa44fc9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -14,6 +14,12 @@ using System.Reactive.Linq; namespace Avalonia.Controls { + [TemplatePart(DATAGRIDROWGROUPHEADER_expanderButton, typeof(ToggleButton))] + [TemplatePart(DATAGRIDROWGROUPHEADER_indentSpacer, typeof(Control))] + [TemplatePart(DATAGRIDROWGROUPHEADER_itemCountElement, typeof(TextBlock))] + [TemplatePart(DATAGRIDROWGROUPHEADER_propertyNameElement, typeof(TextBlock))] + [TemplatePart(DataGridRow.DATAGRIDROW_elementRoot, typeof(Panel))] + [TemplatePart(DataGridRow.DATAGRIDROW_elementRowHeader, typeof(DataGridRowHeader))] [PseudoClasses(":pressed", ":current", ":expanded")] public class DataGridRowGroupHeader : TemplatedControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index 510072174f..03299bbf35 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -13,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents an individual row header. /// + [TemplatePart(DATAGRIDROWHEADER_elementRootName, typeof(Control))] [PseudoClasses(":invalid", ":selected", ":editing", ":current")] public class DataGridRowHeader : ContentControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index 1d5c899993..f3afe2c42d 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -5,6 +5,7 @@ using Avalonia.Collections; using Avalonia.Controls.Utils; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Utilities; using System; @@ -811,7 +812,7 @@ namespace Avalonia.Controls if (row.Slot > slotDeleted) { CorrectRowAfterDeletion(row, wasRow); - row.EnsureBackground(); + _rowsPresenter?.InvalidateChildIndex(row); } } @@ -867,7 +868,7 @@ namespace Avalonia.Controls if (row.Slot >= slotInserted) { DataGrid.CorrectRowAfterInsertion(row, rowInserted); - row.EnsureBackground(); + _rowsPresenter?.InvalidateChildIndex(row); } } @@ -1485,8 +1486,8 @@ namespace Avalonia.Controls // If the row has been recycled, reapply the BackgroundBrush if (row.IsRecycled) { - row.EnsureBackground(); row.ApplyCellsState(); + _rowsPresenter?.InvalidateChildIndex(row); } else if (row == EditingRow) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs index 1cf6ab68ac..bb8637cda2 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs @@ -10,6 +10,7 @@ using System; using System.ComponentModel; using Avalonia.Layout; using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Controls.Documents; namespace Avalonia.Controls { @@ -18,8 +19,6 @@ namespace Avalonia.Controls /// public class DataGridTextColumn : DataGridBoundColumn { - private const string DATAGRID_TextColumnCellTextBlockMarginKey = "DataGridTextColumnCellTextBlockMargin"; - /// /// Initializes a new instance of the class. /// @@ -32,7 +31,7 @@ namespace Avalonia.Controls /// Identifies the FontFamily dependency property. /// public static readonly AttachedProperty FontFamilyProperty = - TextBlock.FontFamilyProperty.AddOwner(); + TextElement.FontFamilyProperty.AddOwner(); /// /// Gets or sets the font name. @@ -47,7 +46,7 @@ namespace Avalonia.Controls /// Identifies the FontSize dependency property. /// public static readonly AttachedProperty FontSizeProperty = - TextBlock.FontSizeProperty.AddOwner(); + TextElement.FontSizeProperty.AddOwner(); /// /// Gets or sets the font size. @@ -64,7 +63,7 @@ namespace Avalonia.Controls /// Identifies the FontStyle dependency property. /// public static readonly AttachedProperty FontStyleProperty = - TextBlock.FontStyleProperty.AddOwner(); + TextElement.FontStyleProperty.AddOwner(); /// /// Gets or sets the font style. @@ -79,7 +78,7 @@ namespace Avalonia.Controls /// Identifies the FontWeight dependency property. /// public static readonly AttachedProperty FontWeightProperty = - TextBlock.FontWeightProperty.AddOwner(); + TextElement.FontWeightProperty.AddOwner(); /// /// Gets or sets the font weight or thickness. @@ -90,11 +89,26 @@ namespace Avalonia.Controls set => SetValue(FontWeightProperty, value); } + /// + /// Identifies the FontStretch dependency property. + /// + public static readonly AttachedProperty FontStretchProperty = + TextElement.FontStretchProperty.AddOwner(); + + /// + /// Gets or sets the font weight or thickness. + /// + public FontStretch FontStretch + { + get => GetValue(FontStretchProperty); + set => SetValue(FontStretchProperty, value); + } + /// /// Identifies the Foreground dependency property. /// public static readonly AttachedProperty ForegroundProperty = - TextBlock.ForegroundProperty.AddOwner(); + TextElement.ForegroundProperty.AddOwner(); /// /// Gets or sets a brush that describes the foreground of the column cells. @@ -105,7 +119,7 @@ namespace Avalonia.Controls set => SetValue(ForegroundProperty, value); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -162,8 +176,7 @@ namespace Avalonia.Controls { TextBlock textBlockElement = new TextBlock { - [!Layoutable.MarginProperty] = new DynamicResourceExtension(DATAGRID_TextColumnCellTextBlockMarginKey), - VerticalAlignment = VerticalAlignment.Center + Name = "CellTextBlock" }; SyncProperties(textBlockElement); diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index c5fe9f0cb2..38d559a031 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -3,12 +3,13 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Utilities; using System; +using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; -using Avalonia.Controls; -using Avalonia.Controls.Utils; namespace Avalonia.Controls.Primitives { @@ -16,9 +17,10 @@ namespace Avalonia.Controls.Primitives /// Used within the template of a /// to specify the location in the control's visual tree where the cells are to be added. /// - public sealed class DataGridCellsPresenter : Panel + public sealed class DataGridCellsPresenter : Panel, IChildIndexProvider { private double _fillerLeftEdge; + private EventHandler _childIndexChanged; // The desired height needs to be cached due to column virtualization; otherwise, the cells // would grow and shrink as the DataGrid scrolls horizontally @@ -42,6 +44,25 @@ namespace Avalonia.Controls.Primitives set; } + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return child is DataGridCell cell + ? OwningGrid.ColumnsInternal.GetColumnDisplayIndex(cell.ColumnIndex) + : throw new InvalidOperationException("Invalid cell type"); + } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + count = OwningGrid.ColumnsInternal.VisibleColumnCount; + return true; + } + /// /// Arranges the content of the . /// @@ -120,6 +141,13 @@ namespace Avalonia.Controls.Primitives } } + protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + { + base.ChildrenChanged(sender, e); + + InvalidateChildIndex(); + } + private static void EnsureCellDisplay(DataGridCell cell, bool displayColumn) { if (cell.IsCurrent) @@ -190,6 +218,8 @@ namespace Avalonia.Controls.Primitives { // No explicit height values were set so we can autosize autoSizeHeight = true; + // We need to invalidate desired height in order to grow or shrink as needed + InvalidateDesiredHeight(); measureHeight = double.PositiveInfinity; } else @@ -304,6 +334,11 @@ namespace Avalonia.Controls.Primitives DesiredHeight = 0; } + internal void InvalidateChildIndex() + { + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + } + private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge) { if (!column.IsVisible) diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs index 4eed119240..108dc8ded7 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -3,8 +3,10 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.LogicalTree; using Avalonia.Media; using System; +using System.Collections.Specialized; using System.Diagnostics; namespace Avalonia.Controls.Primitives @@ -13,10 +15,11 @@ namespace Avalonia.Controls.Primitives /// Used within the template of a to specify the /// location in the control's visual tree where the column headers are to be added. /// - public sealed class DataGridColumnHeadersPresenter : Panel + public sealed class DataGridColumnHeadersPresenter : Panel, IChildIndexProvider { private Control _dragIndicator; private IControl _dropLocationIndicator; + private EventHandler _childIndexChanged; /// /// Tracks which column is currently being dragged. @@ -104,6 +107,25 @@ namespace Avalonia.Controls.Primitives set; } + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return child is DataGridColumnHeader header + ? OwningGrid.ColumnsInternal.GetColumnDisplayIndex(header.ColumnIndex) + : throw new InvalidOperationException("Invalid cell type"); + } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + count = OwningGrid.ColumnsInternal.VisibleColumnCount; + return true; + } + /// /// Arranges the content of the . /// @@ -391,5 +413,17 @@ namespace Avalonia.Controls.Primitives OwningGrid.ColumnsInternal.EnsureVisibleEdgedColumnsWidth(); return new Size(OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, height); } + + protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + { + base.ChildrenChanged(sender, e); + + InvalidateChildIndex(); + } + + internal void InvalidateChildIndex() + { + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + } } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs index 308ebc69d4..5d82689eff 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs @@ -7,8 +7,8 @@ using System; using System.Diagnostics; using Avalonia.Input; -using Avalonia.Input.GestureRecognizers; using Avalonia.Layout; +using Avalonia.LogicalTree; using Avalonia.Media; namespace Avalonia.Controls.Primitives @@ -17,8 +17,10 @@ namespace Avalonia.Controls.Primitives /// Used within the template of a to specify the /// location in the control's visual tree where the rows are to be added. /// - public sealed class DataGridRowsPresenter : Panel + public sealed class DataGridRowsPresenter : Panel, IChildIndexProvider { + private EventHandler _childIndexChanged; + public DataGridRowsPresenter() { AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture); @@ -44,6 +46,29 @@ namespace Avalonia.Controls.Primitives } } + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return child is DataGridRow row + ? row.Index + : throw new InvalidOperationException("Invalid DataGrid child"); + } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + return OwningGrid.DataConnection.TryGetCount(false, true, out count); + } + + internal void InvalidateChildIndex(DataGridRow row) + { + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row)); + } + /// /// Arranges the content of the . /// diff --git a/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs b/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs index d61c05ab6e..64769303d6 100644 --- a/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs @@ -1,9 +1,5 @@ -using System.Runtime.CompilerServices; using Avalonia.Metadata; -[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Collections")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")] diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 6715b7ceb9..0d1fe43eb6 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -12,23 +12,32 @@ - - - - - + + + + + + + + @@ -102,22 +116,35 @@ + + + + + @@ -180,57 +152,58 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - + Fill="{TemplateBinding SeparatorBrush}" + IsVisible="{TemplateBinding AreSeparatorsVisible}" /> + + + + + - + @@ -243,10 +216,10 @@ + + + @@ -314,29 +300,30 @@ @@ -430,9 +417,12 @@ Width="12" Height="12" Margin="12,0,0,0" + BorderBrush="{TemplateBinding BorderBrush}" + BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" - Foreground="{TemplateBinding Foreground}" - Focusable="False" /> + CornerRadius="{TemplateBinding CornerRadius}" + Focusable="False" + Foreground="{TemplateBinding Foreground}" /> - - + CornerRadius="{TemplateBinding CornerRadius}"> + diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 2c206b53f6..33e5efbc15 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -1,9 +1,17 @@ Compat issues with assembly Avalonia.Controls: +MembersMustExist : Member 'protected void Avalonia.Controls.Button.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.ButtonSpinner.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.CalendarDatePicker.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.ContextMenu.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. +TypesMustExist : Type 'Avalonia.Controls.DropDown' does not exist in the implementation but it does exist in the contract. +TypesMustExist : Type 'Avalonia.Controls.DropDownItem' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.Expander.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.IMenuItem.StaysOpenOnClick.set(System.Boolean)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.ItemsRepeater.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.DirectProperty Avalonia.Controls.NumericUpDown.ValueProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.IncrementProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.MaximumProperty' does not exist in the implementation but it does exist in the contract. @@ -28,10 +36,35 @@ MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Value.set MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChangedEventArgs..ctor(Avalonia.Interactivity.RoutedEvent, System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.ProgressBar.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.Slider.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontFamilyProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontStyleProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontWeightProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.ForegroundProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontSizeProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.TextAlignmentProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.TextTrimmingProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.TextWrappingProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.LineHeightProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.MaxLinesProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.FontFamily Avalonia.Controls.TextBlock.GetFontFamily(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.TextBlock.GetFontSize(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.FontStyle Avalonia.Controls.TextBlock.GetFontStyle(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.FontWeight Avalonia.Controls.TextBlock.GetFontWeight(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.IBrush Avalonia.Controls.TextBlock.GetForeground(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontFamily(Avalonia.Controls.Control, Avalonia.Media.FontFamily)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontSize(Avalonia.Controls.Control, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontStyle(Avalonia.Controls.Control, Avalonia.Media.FontStyle)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontWeight(Avalonia.Controls.Control, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetForeground(Avalonia.Controls.Control, Avalonia.Media.IBrush)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.TextBox.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' 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. +MembersMustExist : Member 'protected void Avalonia.Controls.Window.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. @@ -42,13 +75,22 @@ MembersMustExist : Member 'public System.Action Avalonia.Controls MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.Notifications.WindowNotificationManager.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation. +MembersMustExist : Member 'protected void Avalonia.Controls.Presenters.ScrollContentPresenter.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected Avalonia.Media.FormattedText Avalonia.Controls.Presenters.TextPresenter.CreateFormattedText()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.FormattedText Avalonia.Controls.Presenters.TextPresenter.FormattedText.get()' does not exist in the implementation but it does exist in the contract. 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. +MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.ScrollBar.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.Track.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist 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 +109,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: 110 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/ApplicationLifetimes/IApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IApplicationLifetime.cs index 9860d0cb38..b38a539d4a 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IApplicationLifetime.cs @@ -1,5 +1,8 @@ +using Avalonia.Metadata; + namespace Avalonia.Controls.ApplicationLifetimes { + [NotClientImplementable] public interface IApplicationLifetime { diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index 2bd5c1238d..4b88f6b537 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using Avalonia.Metadata; namespace Avalonia.Controls.ApplicationLifetimes { /// /// Controls application lifetime in classic desktop style /// + [NotClientImplementable] public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime { /// diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IControlledApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IControlledApplicationLifetime.cs index 3f61aeb536..d7eda790df 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IControlledApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IControlledApplicationLifetime.cs @@ -1,7 +1,9 @@ using System; +using Avalonia.Metadata; namespace Avalonia.Controls.ApplicationLifetimes { + [NotClientImplementable] public interface IControlledApplicationLifetime : IApplicationLifetime { /// diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ISingleViewApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ISingleViewApplicationLifetime.cs index e25815602e..480c65e5ad 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ISingleViewApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ISingleViewApplicationLifetime.cs @@ -1,5 +1,8 @@ +using Avalonia.Metadata; + namespace Avalonia.Controls.ApplicationLifetimes { + [NotClientImplementable] public interface ISingleViewApplicationLifetime : IApplicationLifetime { Control? MainView { get; set; } diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 930e250334..5c95932c1f 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 { @@ -1342,12 +1346,16 @@ namespace Avalonia.Controls /// enabled. /// /// The property. - /// The new binding value for the property. - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + /// The current data binding state. + /// The current data binding error, if any. + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == TextProperty || property == SelectedItemProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } @@ -2180,7 +2188,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 +2247,7 @@ namespace Avalonia.Controls ClearView(); if (Items != null) { - _items = new List(Items.Cast().ToList()); + _items = new List(Items.Cast()); } } diff --git a/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs b/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs new file mode 100644 index 0000000000..4566cd9db5 --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs @@ -0,0 +1,28 @@ +using Avalonia.Automation.Peers; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as automation property identifiers by UI Automation providers. + /// + public static class AutomationElementIdentifiers + { + /// + /// Identifies the bounding rectangle automation property. The bounding rectangle property + /// value is returned by the method. + /// + public static AutomationProperty BoundingRectangleProperty { get; } = new AutomationProperty(); + + /// + /// Identifies the class name automation property. The class name property value is returned + /// by the method. + /// + public static AutomationProperty ClassNameProperty { get; } = new AutomationProperty(); + + /// + /// Identifies the name automation property. The class name property value is returned + /// by the method. + /// + public static AutomationProperty NameProperty { get; } = new AutomationProperty(); + } +} diff --git a/src/Avalonia.Controls/Automation/AutomationLiveSetting.cs b/src/Avalonia.Controls/Automation/AutomationLiveSetting.cs new file mode 100644 index 0000000000..55de657b32 --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationLiveSetting.cs @@ -0,0 +1,28 @@ +namespace Avalonia.Automation +{ + /// + /// Describes the notification characteristics of a particular live region + /// + public enum AutomationLiveSetting + { + /// + /// The element does not send notifications if the content of the live region has changed. + /// + Off = 0, + + /// + /// The element sends non-interruptive notifications if the content of the live region has + /// changed. With this setting, UI Automation clients and assistive technologies are expected + /// to not interrupt the user to inform of changes to the live region. + /// + Polite = 1, + + /// + /// The element sends interruptive notifications if the content of the live region has changed. + /// With this setting, UI Automation clients and assistive technologies are expected to interrupt + /// the user to inform of changes to the live region. + /// + Assertive = 2, + } +} + diff --git a/src/Avalonia.Controls/Automation/AutomationProperties.cs b/src/Avalonia.Controls/Automation/AutomationProperties.cs new file mode 100644 index 0000000000..c20af148b8 --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationProperties.cs @@ -0,0 +1,630 @@ +using System; +using Avalonia.Automation.Peers; +using Avalonia.Controls; + +namespace Avalonia.Automation +{ + /// + /// Declares how a control should included in different views of the automation tree. + /// + public enum AccessibilityView + { + /// + /// The control is included in the Raw view of the automation tree. + /// + Raw, + + /// + /// The control is included in the Control view of the automation tree. + /// + Control, + + /// + /// The control is included in the Content view of the automation tree. + /// + Content, + } + + public static class AutomationProperties + { + internal const int AutomationPositionInSetDefault = -1; + internal const int AutomationSizeOfSetDefault = -1; + + /// + /// Defines the AutomationProperties.AcceleratorKey attached property. + /// + public static readonly AttachedProperty AcceleratorKeyProperty = + AvaloniaProperty.RegisterAttached( + "AcceleratorKey", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.AccessibilityView attached property. + /// + public static readonly AttachedProperty AccessibilityViewProperty = + AvaloniaProperty.RegisterAttached( + "AccessibilityView", + typeof(AutomationProperties), + defaultValue: AccessibilityView.Content); + + /// + /// Defines the AutomationProperties.AccessKey attached property + /// + public static readonly AttachedProperty AccessKeyProperty = + AvaloniaProperty.RegisterAttached( + "AccessKey", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.AutomationId attached property. + /// + public static readonly AttachedProperty AutomationIdProperty = + AvaloniaProperty.RegisterAttached( + "AutomationId", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.ControlTypeOverride attached property. + /// + public static readonly AttachedProperty ControlTypeOverrideProperty = + AvaloniaProperty.RegisterAttached( + "ControlTypeOverride", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.HelpText attached property. + /// + public static readonly AttachedProperty HelpTextProperty = + AvaloniaProperty.RegisterAttached( + "HelpText", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.IsColumnHeader attached property. + /// + public static readonly AttachedProperty IsColumnHeaderProperty = + AvaloniaProperty.RegisterAttached( + "IsColumnHeader", + typeof(AutomationProperties), + false); + + /// + /// Defines the AutomationProperties.IsRequiredForForm attached property. + /// + public static readonly AttachedProperty IsRequiredForFormProperty = + AvaloniaProperty.RegisterAttached( + "IsRequiredForForm", + typeof(AutomationProperties), + false); + + /// + /// Defines the AutomationProperties.IsRowHeader attached property. + /// + public static readonly AttachedProperty IsRowHeaderProperty = + AvaloniaProperty.RegisterAttached( + "IsRowHeader", + typeof(AutomationProperties), + false); + + /// + /// Defines the AutomationProperties.IsOffscreenBehavior attached property. + /// + public static readonly AttachedProperty IsOffscreenBehaviorProperty = + AvaloniaProperty.RegisterAttached( + "IsOffscreenBehavior", + typeof(AutomationProperties), + IsOffscreenBehavior.Default); + + /// + /// Defines the AutomationProperties.ItemStatus attached property. + /// + public static readonly AttachedProperty ItemStatusProperty = + AvaloniaProperty.RegisterAttached( + "ItemStatus", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.ItemType attached property. + /// + public static readonly AttachedProperty ItemTypeProperty = + AvaloniaProperty.RegisterAttached( + "ItemType", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.LabeledBy attached property. + /// + public static readonly AttachedProperty LabeledByProperty = + AvaloniaProperty.RegisterAttached( + "LabeledBy", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.LiveSetting attached property. + /// + public static readonly AttachedProperty LiveSettingProperty = + AvaloniaProperty.RegisterAttached( + "LiveSetting", + typeof(AutomationProperties), + AutomationLiveSetting.Off); + + /// + /// Defines the AutomationProperties.Name attached attached property. + /// + public static readonly AttachedProperty NameProperty = + AvaloniaProperty.RegisterAttached( + "Name", + typeof(AutomationProperties)); + + /// + /// Defines the AutomationProperties.PositionInSet attached property. + /// + /// + /// The PositionInSet property describes the ordinal location of the element within a set + /// of elements which are considered to be siblings. PositionInSet works in coordination + /// with the SizeOfSet property to describe the ordinal location in the set. + /// + public static readonly AttachedProperty PositionInSetProperty = + AvaloniaProperty.RegisterAttached( + "PositionInSet", + typeof(AutomationProperties), + AutomationPositionInSetDefault); + + /// + /// Defines the AutomationProperties.SizeOfSet attached property. + /// + /// + /// The SizeOfSet property describes the count of automation elements in a group or set + /// that are considered to be siblings. SizeOfSet works in coordination with the PositionInSet + /// property to describe the count of items in the set. + /// + public static readonly AttachedProperty SizeOfSetProperty = + AvaloniaProperty.RegisterAttached( + "SizeOfSet", + typeof(AutomationProperties), + AutomationSizeOfSetDefault); + + /// + /// Helper for setting AcceleratorKey property on a StyledElement. + /// + public static void SetAcceleratorKey(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(AcceleratorKeyProperty, value); + } + + /// + /// Helper for reading AcceleratorKey property from a StyledElement. + /// + public static string GetAcceleratorKey(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(AcceleratorKeyProperty)); + } + + /// + /// Helper for setting AccessibilityView property on a StyledElement. + /// + public static void SetAccessibilityView(StyledElement element, AccessibilityView value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(AccessibilityViewProperty, value); + } + + /// + /// Helper for reading AccessibilityView property from a StyledElement. + /// + public static AccessibilityView GetAccessibilityView(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return element.GetValue(AccessibilityViewProperty); + } + + /// + /// Helper for setting AccessKey property on a StyledElement. + /// + public static void SetAccessKey(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(AccessKeyProperty, value); + } + + /// + /// Helper for reading AccessKey property from a StyledElement. + /// + public static string GetAccessKey(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(AccessKeyProperty)); + } + + /// + /// Helper for setting AutomationId property on a StyledElement. + /// + public static void SetAutomationId(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(AutomationIdProperty, value); + } + + /// + /// Helper for reading AutomationId property from a StyledElement. + /// + public static string GetAutomationId(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return element.GetValue(AutomationIdProperty); + } + + /// + /// Helper for setting ControlTypeOverride property on a StyledElement. + /// + public static void SetControlTypeOverride(StyledElement element, AutomationControlType? value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(ControlTypeOverrideProperty, value); + } + + /// + /// Helper for reading ControlTypeOverride property from a StyledElement. + /// + public static AutomationControlType? GetControlTypeOverride(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return element.GetValue(ControlTypeOverrideProperty); + } + + /// + /// Helper for setting HelpText property on a StyledElement. + /// + public static void SetHelpText(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(HelpTextProperty, value); + } + + /// + /// Helper for reading HelpText property from a StyledElement. + /// + public static string GetHelpText(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(HelpTextProperty)); + } + + /// + /// Helper for setting IsColumnHeader property on a StyledElement. + /// + public static void SetIsColumnHeader(StyledElement element, bool value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(IsColumnHeaderProperty, value); + } + + /// + /// Helper for reading IsColumnHeader property from a StyledElement. + /// + public static bool GetIsColumnHeader(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((bool)element.GetValue(IsColumnHeaderProperty)); + } + + /// + /// Helper for setting IsRequiredForForm property on a StyledElement. + /// + public static void SetIsRequiredForForm(StyledElement element, bool value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(IsRequiredForFormProperty, value); + } + + /// + /// Helper for reading IsRequiredForForm property from a StyledElement. + /// + public static bool GetIsRequiredForForm(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((bool)element.GetValue(IsRequiredForFormProperty)); + } + + /// + /// Helper for reading IsRowHeader property from a StyledElement. + /// + public static bool GetIsRowHeader(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((bool)element.GetValue(IsRowHeaderProperty)); + } + + /// + /// Helper for setting IsRowHeader property on a StyledElement. + /// + public static void SetIsRowHeader(StyledElement element, bool value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(IsRowHeaderProperty, value); + } + + /// + /// Helper for setting IsOffscreenBehavior property on a StyledElement. + /// + public static void SetIsOffscreenBehavior(StyledElement element, IsOffscreenBehavior value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(IsOffscreenBehaviorProperty, value); + } + + /// + /// Helper for reading IsOffscreenBehavior property from a StyledElement. + /// + public static IsOffscreenBehavior GetIsOffscreenBehavior(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((IsOffscreenBehavior)element.GetValue(IsOffscreenBehaviorProperty)); + } + + /// + /// Helper for setting ItemStatus property on a StyledElement. + /// + public static void SetItemStatus(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(ItemStatusProperty, value); + } + + /// + /// Helper for reading ItemStatus property from a StyledElement. + /// + public static string GetItemStatus(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(ItemStatusProperty)); + } + + /// + /// Helper for setting ItemType property on a StyledElement. + /// + public static void SetItemType(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(ItemTypeProperty, value); + } + + /// + /// Helper for reading ItemType property from a StyledElement. + /// + public static string GetItemType(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(ItemTypeProperty)); + } + + /// + /// Helper for setting LabeledBy property on a StyledElement. + /// + public static void SetLabeledBy(StyledElement element, IControl value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(LabeledByProperty, value); + } + + /// + /// Helper for reading LabeledBy property from a StyledElement. + /// + public static IControl GetLabeledBy(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return element.GetValue(LabeledByProperty); + } + + /// + /// Helper for setting LiveSetting property on a StyledElement. + /// + public static void SetLiveSetting(StyledElement element, AutomationLiveSetting value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(LiveSettingProperty, value); + } + + /// + /// Helper for reading LiveSetting property from a StyledElement. + /// + public static AutomationLiveSetting GetLiveSetting(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((AutomationLiveSetting)element.GetValue(LiveSettingProperty)); + } + + /// + /// Helper for setting Name property on a StyledElement. + /// + public static void SetName(StyledElement element, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(NameProperty, value); + } + + /// + /// Helper for reading Name property from a StyledElement. + /// + public static string GetName(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((string)element.GetValue(NameProperty)); + } + + /// + /// Helper for setting PositionInSet property on a StyledElement. + /// + public static void SetPositionInSet(StyledElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(PositionInSetProperty, value); + } + + /// + /// Helper for reading PositionInSet property from a StyledElement. + /// + public static int GetPositionInSet(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((int)element.GetValue(PositionInSetProperty)); + } + + /// + /// Helper for setting SizeOfSet property on a StyledElement. + /// + public static void SetSizeOfSet(StyledElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetValue(SizeOfSetProperty, value); + } + + /// + /// Helper for reading SizeOfSet property from a StyledElement. + /// + public static int GetSizeOfSet(StyledElement element) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + return ((int)element.GetValue(SizeOfSetProperty)); + } + } +} + diff --git a/src/Avalonia.Controls/Automation/AutomationProperty.cs b/src/Avalonia.Controls/Automation/AutomationProperty.cs new file mode 100644 index 0000000000..16968b271d --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationProperty.cs @@ -0,0 +1,11 @@ +namespace Avalonia.Automation +{ + /// + /// Identifies a property of or of a specific + /// control pattern. + /// + public sealed class AutomationProperty + { + internal AutomationProperty() { } + } +} diff --git a/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs b/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs new file mode 100644 index 0000000000..3b7eb70fcb --- /dev/null +++ b/src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs @@ -0,0 +1,21 @@ +using System; + +namespace Avalonia.Automation +{ + public class AutomationPropertyChangedEventArgs : EventArgs + { + public AutomationPropertyChangedEventArgs( + AutomationProperty property, + object? oldValue, + object? newValue) + { + Property = property; + OldValue = oldValue; + NewValue = newValue; + } + + public AutomationProperty Property { get; } + public object? OldValue { get; } + public object? NewValue { get; } + } +} diff --git a/src/Avalonia.Controls/Automation/ElementNotEnabledException.cs b/src/Avalonia.Controls/Automation/ElementNotEnabledException.cs new file mode 100644 index 0000000000..ac73d50603 --- /dev/null +++ b/src/Avalonia.Controls/Automation/ElementNotEnabledException.cs @@ -0,0 +1,10 @@ +using System; + +namespace Avalonia.Automation +{ + public class ElementNotEnabledException : Exception + { + public ElementNotEnabledException() : base("Element not enabled.") { } + public ElementNotEnabledException(string message) : base(message) { } + } +} diff --git a/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs b/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs new file mode 100644 index 0000000000..e2b6782162 --- /dev/null +++ b/src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs @@ -0,0 +1,15 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class ExpandCollapsePatternIdentifiers + { + /// + /// Identifies automation property. + /// + public static AutomationProperty ExpandCollapseStateProperty { get; } = new AutomationProperty(); + } +} diff --git a/src/Avalonia.Controls/Automation/ExpandCollapseState.cs b/src/Avalonia.Controls/Automation/ExpandCollapseState.cs new file mode 100644 index 0000000000..c6b4feeb50 --- /dev/null +++ b/src/Avalonia.Controls/Automation/ExpandCollapseState.cs @@ -0,0 +1,29 @@ +namespace Avalonia.Automation +{ + /// + /// Contains values that specify the of a UI Automation element. + /// + public enum ExpandCollapseState + { + /// + /// No child nodes, controls, or content of the UI Automation element are displayed. + /// + Collapsed, + + /// + /// All child nodes, controls or content of the UI Automation element are displayed. + /// + Expanded, + + /// + /// The UI Automation element has no child nodes, controls, or content to display. + /// + LeafNode, + + /// + /// Some, but not all, child nodes, controls, or content of the UI Automation element are + /// displayed. + /// + PartiallyExpanded + } +} diff --git a/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs b/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs new file mode 100644 index 0000000000..128c1e1dcc --- /dev/null +++ b/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs @@ -0,0 +1,26 @@ +namespace Avalonia.Automation +{ + /// + /// This enum offers different ways of evaluating the IsOffscreen AutomationProperty + /// + public enum IsOffscreenBehavior + { + /// + /// The AutomationProperty IsOffscreen is calculated based on IsVisible. + /// + Default, + /// + /// The AutomationProperty IsOffscreen is false. + /// + Onscreen, + /// + /// The AutomationProperty IsOffscreen if true. + /// + Offscreen, + /// + /// The AutomationProperty IsOffscreen is calculated based on clip regions. + /// + FromClip, + } +} + diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs new file mode 100644 index 0000000000..71421ac136 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Automation.Peers +{ + public enum AutomationControlType + { + None, + Button, + Calendar, + CheckBox, + ComboBox, + ComboBoxItem, + Edit, + Hyperlink, + Image, + ListItem, + List, + Menu, + MenuBar, + MenuItem, + ProgressBar, + RadioButton, + ScrollBar, + Slider, + Spinner, + StatusBar, + Tab, + TabItem, + Text, + ToolBar, + ToolTip, + Tree, + TreeItem, + Custom, + Group, + Thumb, + DataGrid, + DataItem, + Document, + SplitButton, + Window, + Pane, + Header, + HeaderItem, + Table, + TitleBar, + Separator, + } + + /// + /// Provides a base class that exposes an element to UI Automation. + /// + public abstract class AutomationPeer + { + /// + /// Attempts to bring the element associated with the automation peer into view. + /// + public void BringIntoView() => BringIntoViewCore(); + + /// + /// Gets the accelerator key combinations for the element that is associated with the UI + /// Automation peer. + /// + public string? GetAcceleratorKey() => GetAcceleratorKeyCore(); + + /// + /// Gets the access key for the element that is associated with the automation peer. + /// + public string? GetAccessKey() => GetAccessKeyCore(); + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + public AutomationControlType GetAutomationControlType() => GetControlTypeOverrideCore(); + + /// + /// Gets the automation ID of the element that is associated with the UI Automation peer. + /// + public string? GetAutomationId() => GetAutomationIdCore(); + + /// + /// Gets the bounding rectangle of the element that is associated with the automation peer + /// in top-level coordinates. + /// + public Rect GetBoundingRectangle() => GetBoundingRectangleCore(); + + /// + /// Gets the child automation peers. + /// + public IReadOnlyList GetChildren() => GetOrCreateChildrenCore(); + + /// + /// Gets a string that describes the class of the element. + /// + public string GetClassName() => GetClassNameCore() ?? string.Empty; + + /// + /// Gets the automation peer for the label that is targeted to the element. + /// + /// + public AutomationPeer? GetLabeledBy() => GetLabeledByCore(); + + /// + /// Gets a human-readable localized string that represents the type of the control that is + /// associated with this automation peer. + /// + public string GetLocalizedControlType() => GetLocalizedControlTypeCore(); + + /// + /// Gets text that describes the element that is associated with this automation peer. + /// + public string GetName() => GetNameCore() ?? string.Empty; + + /// + /// Gets the that is the parent of this . + /// + /// + public AutomationPeer? GetParent() => GetParentCore(); + + /// + /// Gets a value that indicates whether the element that is associated with this automation + /// peer currently has keyboard focus. + /// + public bool HasKeyboardFocus() => HasKeyboardFocusCore(); + + /// + /// Gets a value that indicates whether the element that is associated with this automation + /// peer contains data that is presented to the user. + /// + public bool IsContentElement() => IsControlElement() && IsContentElementCore(); + + /// + /// Gets a value that indicates whether the element is understood by the user as + /// interactive or as contributing to the logical structure of the control in the GUI. + /// + public bool IsControlElement() => IsControlElementCore(); + + /// + /// Gets a value indicating whether the control is enabled for user interaction. + /// + public bool IsEnabled() => IsEnabledCore(); + + /// + /// Gets a value that indicates whether the element can accept keyboard focus. + /// + /// + public bool IsKeyboardFocusable() => IsKeyboardFocusableCore(); + + /// + /// Sets the keyboard focus on the element that is associated with this automation peer. + /// + public void SetFocus() => SetFocusCore(); + + /// + /// Shows the context menu for the element that is associated with this automation peer. + /// + /// true if a context menu is present for the element; otherwise false. + public bool ShowContextMenu() => ShowContextMenuCore(); + + /// + /// Tries to get a provider of the specified type from the peer. + /// + /// The provider type. + /// The provider, or null if not implemented on this peer. + public T? GetProvider() => (T?)GetProviderCore(typeof(T)); + + /// + /// Occurs when the children of the automation peer have changed. + /// + public event EventHandler? ChildrenChanged; + + /// + /// Occurs when a property value of the automation peer has changed. + /// + public event EventHandler? PropertyChanged; + + /// + /// Raises an event to notify the automation client the the children of the peer have changed. + /// + protected void RaiseChildrenChangedEvent() => ChildrenChanged?.Invoke(this, EventArgs.Empty); + + /// + /// Raises an event to notify the automation client of a changed property value. + /// + /// The property that changed. + /// The previous value of the property. + /// The new value of the property. + public void RaisePropertyChangedEvent( + AutomationProperty property, + object? oldValue, + object? newValue) + { + PropertyChanged?.Invoke(this, new AutomationPropertyChangedEventArgs(property, oldValue, newValue)); + } + + protected virtual string GetLocalizedControlTypeCore() + { + var controlType = GetAutomationControlType(); + + return controlType switch + { + AutomationControlType.CheckBox => "check box", + AutomationControlType.ComboBox => "combo box", + AutomationControlType.ListItem => "list item", + AutomationControlType.MenuBar => "menu bar", + AutomationControlType.MenuItem => "menu item", + AutomationControlType.ProgressBar => "progress bar", + AutomationControlType.RadioButton => "radio button", + AutomationControlType.ScrollBar => "scroll bar", + AutomationControlType.StatusBar => "status bar", + AutomationControlType.TabItem => "tab item", + AutomationControlType.ToolBar => "toolbar", + AutomationControlType.ToolTip => "tooltip", + AutomationControlType.TreeItem => "tree item", + AutomationControlType.Custom => "custom", + AutomationControlType.DataGrid => "data grid", + AutomationControlType.DataItem => "data item", + AutomationControlType.SplitButton => "split button", + AutomationControlType.HeaderItem => "header item", + AutomationControlType.TitleBar => "title bar", + _ => controlType.ToString().ToLowerInvariant(), + }; + } + + protected abstract void BringIntoViewCore(); + protected abstract string? GetAcceleratorKeyCore(); + protected abstract string? GetAccessKeyCore(); + protected abstract AutomationControlType GetAutomationControlTypeCore(); + protected abstract string? GetAutomationIdCore(); + protected abstract Rect GetBoundingRectangleCore(); + protected abstract IReadOnlyList GetOrCreateChildrenCore(); + protected abstract string GetClassNameCore(); + protected abstract AutomationPeer? GetLabeledByCore(); + protected abstract string? GetNameCore(); + protected abstract AutomationPeer? GetParentCore(); + protected abstract bool HasKeyboardFocusCore(); + protected abstract bool IsContentElementCore(); + protected abstract bool IsControlElementCore(); + protected abstract bool IsEnabledCore(); + protected abstract bool IsKeyboardFocusableCore(); + protected abstract void SetFocusCore(); + protected abstract bool ShowContextMenuCore(); + + protected virtual AutomationControlType GetControlTypeOverrideCore() + { + return GetAutomationControlTypeCore(); + } + + protected virtual object? GetProviderCore(Type providerType) + { + return providerType.IsAssignableFrom(this.GetType()) ? this : null; + } + + protected internal abstract bool TrySetParent(AutomationPeer? parent); + + protected void EnsureEnabled() + { + if (!IsEnabled()) + throw new ElementNotEnabledException(); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs new file mode 100644 index 0000000000..4ac07717da --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs @@ -0,0 +1,43 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class ButtonAutomationPeer : ContentControlAutomationPeer, + IInvokeProvider + { + public ButtonAutomationPeer(Button owner) + : base(owner) + { + } + + public new Button Owner => (Button)base.Owner; + + public void Invoke() + { + EnsureEnabled(); + (Owner as Button)?.PerformClick(); + } + + protected override string? GetAcceleratorKeyCore() + { + var result = base.GetAcceleratorKeyCore(); + + if (string.IsNullOrWhiteSpace(result)) + { + result = Owner.HotKey?.ToString(); + } + + return result; + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Button; + } + + protected override bool IsContentElementCore() => true; + protected override bool IsControlElementCore() => true; + } +} + diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs new file mode 100644 index 0000000000..5ff291d972 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class ComboBoxAutomationPeer : SelectingItemsControlAutomationPeer, + IExpandCollapseProvider, + IValueProvider + { + private UnrealizedSelectionPeer[]? _selection; + + public ComboBoxAutomationPeer(ComboBox owner) + : base(owner) + { + } + + public new ComboBox Owner => (ComboBox)base.Owner; + + public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsDropDownOpen); + public bool ShowsMenu => true; + public void Collapse() => Owner.IsDropDownOpen = false; + public void Expand() => Owner.IsDropDownOpen = true; + bool IValueProvider.IsReadOnly => true; + + string? IValueProvider.Value + { + get + { + var selection = GetSelection(); + return selection.Count == 1 ? selection[0].GetName() : null; + } + } + + void IValueProvider.SetValue(string? value) => throw new NotSupportedException(); + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.ComboBox; + } + + protected override IReadOnlyList? GetSelectionCore() + { + if (ExpandCollapseState == ExpandCollapseState.Expanded) + return base.GetSelectionCore(); + + // If the combo box is not open then we won't have an ItemsPresenter so the default + // GetSelectionCore implementation won't work. For this case we create a separate + // peer to represent the unrealized item. + if (Owner.SelectedItem is object selection) + { + _selection ??= new[] { new UnrealizedSelectionPeer(this) }; + _selection[0].Item = selection; + return _selection; + } + + return null; + } + + protected override void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + base.OwnerPropertyChanged(sender, e); + + if (e.Property == ComboBox.IsDropDownOpenProperty) + { + RaisePropertyChangedEvent( + ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, + ToState((bool)e.OldValue!), + ToState((bool)e.NewValue!)); + } + } + + private ExpandCollapseState ToState(bool value) + { + return value ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed; + } + + private class UnrealizedSelectionPeer : UnrealizedElementAutomationPeer + { + private readonly ComboBoxAutomationPeer _owner; + private object? _item; + + public UnrealizedSelectionPeer(ComboBoxAutomationPeer owner) + { + _owner = owner; + } + + public object? Item + { + get => _item; + set + { + if (_item != value) + { + var oldValue = GetNameCore(); + _item = value; + RaisePropertyChangedEvent( + AutomationElementIdentifiers.NameProperty, + oldValue, + GetNameCore()); + } + } + } + + protected override string? GetAcceleratorKeyCore() => null; + protected override string? GetAccessKeyCore() => null; + protected override string? GetAutomationIdCore() => null; + protected override string GetClassNameCore() => typeof(ComboBoxItem).Name; + protected override AutomationPeer? GetLabeledByCore() => null; + protected override AutomationPeer? GetParentCore() => _owner; + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.ListItem; + + protected override string? GetNameCore() + { + if (_item is Control c) + { + var result = AutomationProperties.GetName(c); + + if (result is null && c is ContentControl cc && cc.Presenter?.Child is TextBlock text) + { + result = text.Text; + } + + if (result is null) + { + result = c.GetValue(ContentControl.ContentProperty)?.ToString(); + } + + return result; + } + + return _item?.ToString(); + } + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs new file mode 100644 index 0000000000..df24222a0c --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class ContentControlAutomationPeer : ControlAutomationPeer + { + protected ContentControlAutomationPeer(ContentControl owner) + : base(owner) + { + } + + public new ContentControl Owner => (ContentControl)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Pane; + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (result is null && Owner.Presenter?.Child is TextBlock text) + { + result = text.Text; + } + + if (result is null) + { + result = Owner.Content?.ToString(); + } + + return result; + } + + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs new file mode 100644 index 0000000000..28cb3e34b2 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.VisualTree; + +namespace Avalonia.Automation.Peers +{ + /// + /// An automation peer which represents a element. + /// + public class ControlAutomationPeer : AutomationPeer + { + private IReadOnlyList? _children; + private bool _childrenValid; + private AutomationPeer? _parent; + private bool _parentValid; + + public ControlAutomationPeer(Control owner) + { + Owner = owner ?? throw new ArgumentNullException("owner"); + Initialize(); + } + + public Control Owner { get; } + + public AutomationPeer GetOrCreate(Control element) + { + if (element == Owner) + return this; + return CreatePeerForElement(element); + } + + public static AutomationPeer CreatePeerForElement(Control element) + { + return element.GetOrCreateAutomationPeer(); + } + + protected override void BringIntoViewCore() => Owner.BringIntoView(); + + protected override IReadOnlyList GetOrCreateChildrenCore() + { + var children = _children ?? Array.Empty(); + + if (_childrenValid) + return children; + + var newChildren = GetChildrenCore() ?? Array.Empty(); + + foreach (var peer in children.Except(newChildren)) + peer.TrySetParent(null); + foreach (var peer in newChildren) + peer.TrySetParent(this); + + _childrenValid = true; + return _children = newChildren; + } + + protected virtual IReadOnlyList? GetChildrenCore() + { + var children = ((IVisual)Owner).VisualChildren; + + if (children.Count == 0) + return null; + + var result = new List(); + + foreach (var child in children) + { + if (child is Control c && c.IsVisible) + { + result.Add(GetOrCreate(c)); + } + } + + return result; + } + + protected override AutomationPeer? GetLabeledByCore() + { + var label = AutomationProperties.GetLabeledBy(Owner); + return label is Control c ? GetOrCreate(c) : null; + } + + protected override string? GetNameCore() + { + var result = AutomationProperties.GetName(Owner); + + if (string.IsNullOrWhiteSpace(result) && GetLabeledBy() is AutomationPeer labeledBy) + { + return labeledBy.GetName(); + } + + return null; + } + + protected override AutomationPeer? GetParentCore() + { + EnsureConnected(); + return _parent; + } + + /// + /// Invalidates the peer's children and causes a re-read from . + /// + protected void InvalidateChildren() + { + _childrenValid = false; + RaiseChildrenChangedEvent(); + } + + /// + /// Invalidates the peer's parent. + /// + protected void InvalidateParent() + { + _parent = null; + _parentValid = false; + } + + protected override bool ShowContextMenuCore() + { + var c = Owner; + + while (c is object) + { + if (c.ContextMenu is object) + { + c.ContextMenu.Open(c); + return true; + } + + c = c.Parent as Control; + } + + return false; + } + + protected internal override bool TrySetParent(AutomationPeer? parent) + { + _parent = parent; + return true; + } + + protected override string? GetAcceleratorKeyCore() => AutomationProperties.GetAcceleratorKey(Owner); + protected override string? GetAccessKeyCore() => AutomationProperties.GetAccessKey(Owner); + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom; + protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name; + protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds); + protected override string GetClassNameCore() => Owner.GetType().Name; + protected override bool HasKeyboardFocusCore() => Owner.IsFocused; + protected override bool IsContentElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Content; + protected override bool IsControlElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Control; + protected override bool IsEnabledCore() => Owner.IsEnabled; + protected override bool IsKeyboardFocusableCore() => Owner.Focusable; + protected override void SetFocusCore() => Owner.Focus(); + + protected override AutomationControlType GetControlTypeOverrideCore() + { + return AutomationProperties.GetControlTypeOverride(Owner) ?? GetAutomationControlTypeCore(); + } + + private static Rect GetBounds(TransformedBounds? bounds) + { + return bounds?.Bounds.TransformToAABB(bounds!.Value.Transform) ?? default; + } + + private void Initialize() + { + Owner.PropertyChanged += OwnerPropertyChanged; + var visualChildren = ((IVisual)Owner).VisualChildren; + visualChildren.CollectionChanged += VisualChildrenChanged; + } + + private void VisualChildrenChanged(object? sender, EventArgs e) => InvalidateChildren(); + + private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == Visual.IsVisibleProperty) + { + var parent = Owner.GetVisualParent(); + if (parent is Control c) + (GetOrCreate(c) as ControlAutomationPeer)?.InvalidateChildren(); + } + else if (e.Property == Visual.TransformedBoundsProperty) + { + RaisePropertyChangedEvent( + AutomationElementIdentifiers.BoundingRectangleProperty, + GetBounds((TransformedBounds?)e.OldValue), + GetBounds((TransformedBounds?)e.NewValue)); + } + else if (e.Property == Visual.VisualParentProperty) + { + InvalidateParent(); + } + } + + + private void EnsureConnected() + { + if (!_parentValid) + { + var parent = Owner.GetVisualParent(); + + while (parent is object) + { + if (parent is Control c) + { + var parentPeer = GetOrCreate(c); + parentPeer.GetChildren(); + } + + parent = parent.GetVisualParent(); + } + + _parentValid = true; + } + } + } +} + diff --git a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs new file mode 100644 index 0000000000..db16bf0a53 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs @@ -0,0 +1,54 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class ItemsControlAutomationPeer : ControlAutomationPeer, IScrollProvider + { + private bool _searchedForScrollable; + private IScrollProvider? _scroller; + + public ItemsControlAutomationPeer(ItemsControl owner) + : base(owner) + { + } + + public new ItemsControl Owner => (ItemsControl)base.Owner; + public bool HorizontallyScrollable => _scroller?.HorizontallyScrollable ?? false; + public double HorizontalScrollPercent => _scroller?.HorizontalScrollPercent ?? -1; + public double HorizontalViewSize => _scroller?.HorizontalViewSize ?? 0; + public bool VerticallyScrollable => _scroller?.VerticallyScrollable ?? false; + public double VerticalScrollPercent => _scroller?.VerticalScrollPercent ?? -1; + public double VerticalViewSize => _scroller?.VerticalViewSize ?? 0; + + protected virtual IScrollProvider? Scroller + { + get + { + if (!_searchedForScrollable) + { + if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable) + _scroller = GetOrCreate(scrollable) as IScrollProvider; + _searchedForScrollable = true; + } + + return _scroller; + } + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.List; + } + + public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount) + { + _scroller?.Scroll(horizontalAmount, verticalAmount); + } + + public void SetScrollPercent(double horizontalPercent, double verticalPercent) + { + _scroller?.SetScrollPercent(horizontalPercent, verticalPercent); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs new file mode 100644 index 0000000000..ac23873e6a --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -0,0 +1,82 @@ +using System; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; + +namespace Avalonia.Automation.Peers +{ + public class ListItemAutomationPeer : ContentControlAutomationPeer, + ISelectionItemProvider + { + public ListItemAutomationPeer(ContentControl owner) + : base(owner) + { + } + + public bool IsSelected => Owner.GetValue(ListBoxItem.IsSelectedProperty); + + public ISelectionProvider? SelectionContainer + { + get + { + if (Owner.Parent is Control parent) + { + var parentPeer = GetOrCreate(parent); + return parentPeer as ISelectionProvider; + } + + return null; + } + } + + public void Select() + { + EnsureEnabled(); + + if (Owner.Parent is SelectingItemsControl parent) + { + var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + + if (index != -1) + parent.SelectedIndex = index; + } + } + + void ISelectionItemProvider.AddToSelection() + { + EnsureEnabled(); + + if (Owner.Parent is ItemsControl parent && + parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel) + { + var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + + if (index != -1) + selectionModel.Select(index); + } + } + + void ISelectionItemProvider.RemoveFromSelection() + { + EnsureEnabled(); + + if (Owner.Parent is ItemsControl parent && + parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel) + { + var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + + if (index != -1) + selectionModel.Deselect(index); + } + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.ListItem; + } + + protected override bool IsContentElementCore() => true; + protected override bool IsControlElementCore() => true; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs new file mode 100644 index 0000000000..c98c5c9a22 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs @@ -0,0 +1,59 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public class MenuItemAutomationPeer : ControlAutomationPeer + { + public MenuItemAutomationPeer(MenuItem owner) + : base(owner) + { + } + + public new MenuItem Owner => (MenuItem)base.Owner; + + protected override string? GetAccessKeyCore() + { + var result = base.GetAccessKeyCore(); + + if (string.IsNullOrWhiteSpace(result)) + { + if (Owner.HeaderPresenter?.Child is AccessText accessText) + { + result = accessText.AccessKey.ToString(); + } + } + + return result; + } + + protected override string? GetAcceleratorKeyCore() + { + var result = base.GetAcceleratorKeyCore(); + + if (string.IsNullOrWhiteSpace(result)) + { + result = Owner.InputGesture?.ToString(); + } + + return result; + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.MenuItem; + } + + protected override string? GetNameCore() + { + var result = base.GetNameCore(); + + if (result is null && Owner.Header is string header) + { + result = AccessText.RemoveAccessKeyMarker(header); + } + + return result; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs new file mode 100644 index 0000000000..0f92fed6f3 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs @@ -0,0 +1,25 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + /// + /// An automation peer which represents an element that is exposed to automation as non- + /// interactive or as not contributing to the logical structure of the application. + /// + public class NoneAutomationPeer : ControlAutomationPeer + { + public NoneAutomationPeer(Control owner) + : base(owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.None; + } + + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + } +} + diff --git a/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs new file mode 100644 index 0000000000..25f6ca6e2d --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public class PopupAutomationPeer : ControlAutomationPeer + { + public PopupAutomationPeer(Popup owner) + : base(owner) + { + owner.Opened += PopupOpenedClosed; + owner.Closed += PopupOpenedClosed; + } + + protected override IReadOnlyList? GetChildrenCore() + { + var host = (IPopupHostProvider)Owner; + return host.PopupHost is Control c ? new[] { GetOrCreate(c) } : null; + } + + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + + private void PopupOpenedClosed(object? sender, EventArgs e) + { + // This is golden. We're following WPF's automation peer API here where the + // parent of a peer is set when another peer returns it as a child. We want to + // add the popup root as a child of the popup, so we need to return it as a + // child right? Yeah except invalidating children doesn't automatically cause + // UIA to re-read the children meaning that the parent doesn't get set. So the + // MAIN MECHANISM FOR PARENTING CONTROLS IS BROKEN WITH THE ONLY AUTOMATION API + // IT WAS WRITTEN FOR. Luckily WPF provides an escape-hatch by exposing the + // TrySetParent API internally to work around this. We're exposing it publicly + // to shame whoever came up with this abomination of an API. + GetPopupRoot()?.TrySetParent(this); + InvalidateChildren(); + } + + private AutomationPeer? GetPopupRoot() + { + var popupRoot = ((IPopupHostProvider)Owner).PopupHost as Control; + return popupRoot is object ? GetOrCreate(popupRoot) : null; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs new file mode 100644 index 0000000000..cb65682c06 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs @@ -0,0 +1,40 @@ +using System; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public class PopupRootAutomationPeer : WindowBaseAutomationPeer + { + public PopupRootAutomationPeer(PopupRoot owner) + : base(owner) + { + if (owner.IsVisible) + StartTrackingFocus(); + else + owner.Opened += OnOpened; + owner.Closed += OnClosed; + } + + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + + + protected override AutomationPeer? GetParentCore() + { + var parent = base.GetParentCore(); + return parent; + } + + private void OnOpened(object? sender, EventArgs e) + { + ((PopupRoot)Owner).Opened -= OnOpened; + StartTrackingFocus(); + } + + private void OnClosed(object? sender, EventArgs e) + { + ((PopupRoot)Owner).Closed -= OnClosed; + StopTrackingFocus(); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs new file mode 100644 index 0000000000..39398933fa --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs @@ -0,0 +1,34 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public abstract class RangeBaseAutomationPeer : ControlAutomationPeer, IRangeValueProvider + { + public RangeBaseAutomationPeer(RangeBase owner) + : base(owner) + { + owner.PropertyChanged += OwnerPropertyChanged; + } + + public new RangeBase Owner => (RangeBase)base.Owner; + public virtual bool IsReadOnly => false; + public double Maximum => Owner.Maximum; + public double Minimum => Owner.Minimum; + public double Value => Owner.Value; + public double SmallChange => Owner.SmallChange; + public double LargeChange => Owner.LargeChange; + + public void SetValue(double value) => Owner.Value = value; + + protected virtual void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == RangeBase.MinimumProperty) + RaisePropertyChangedEvent(RangeValuePatternIdentifiers.MinimumProperty, e.OldValue, e.NewValue); + else if (e.Property == RangeBase.MaximumProperty) + RaisePropertyChangedEvent(RangeValuePatternIdentifiers.MaximumProperty, e.OldValue, e.NewValue); + else if (e.Property == RangeBase.ValueProperty) + RaisePropertyChangedEvent(RangeValuePatternIdentifiers.ValueProperty, e.OldValue, e.NewValue); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs new file mode 100644 index 0000000000..835ed1c4af --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs @@ -0,0 +1,172 @@ +using System; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Utilities; + +namespace Avalonia.Automation.Peers +{ + public class ScrollViewerAutomationPeer : ControlAutomationPeer, IScrollProvider + { + public ScrollViewerAutomationPeer(ScrollViewer owner) + : base(owner) + { + } + + public new ScrollViewer Owner => (ScrollViewer)base.Owner; + + public bool HorizontallyScrollable + { + get => MathUtilities.GreaterThan(Owner.Extent.Width, Owner.Viewport.Width); + } + + public double HorizontalScrollPercent + { + get + { + if (!HorizontallyScrollable) + return ScrollPatternIdentifiers.NoScroll; + return (double)(Owner.Offset.X * 100.0 / (Owner.Extent.Width - Owner.Viewport.Width)); + } + } + + public double HorizontalViewSize + { + get + { + if (MathUtilities.IsZero(Owner.Extent.Width)) + return 100; + return Math.Min(100, Owner.Viewport.Width * 100.0 / Owner.Extent.Width); + } + } + + public bool VerticallyScrollable + { + get => MathUtilities.GreaterThan(Owner.Extent.Height, Owner.Viewport.Height); + } + + public double VerticalScrollPercent + { + get + { + if (!VerticallyScrollable) + return ScrollPatternIdentifiers.NoScroll; + return (double)(Owner.Offset.Y * 100.0 / (Owner.Extent.Height - Owner.Viewport.Height)); + } + } + + public double VerticalViewSize + { + get + { + if (MathUtilities.IsZero(Owner.Extent.Height)) + return 100; + return Math.Min(100, Owner.Viewport.Height * 100.0 / Owner.Extent.Height); + } + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Pane; + } + + protected override bool IsContentElementCore() => false; + + protected override bool IsControlElementCore() + { + // Return false if the control is part of a control template. + return Owner.TemplatedParent is null && base.IsControlElementCore(); + } + + public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount) + { + if (!IsEnabled()) + throw new ElementNotEnabledException(); + + var scrollHorizontally = horizontalAmount != ScrollAmount.NoAmount; + var scrollVertically = verticalAmount != ScrollAmount.NoAmount; + + if (scrollHorizontally && !HorizontallyScrollable || scrollVertically && !VerticallyScrollable) + { + throw new InvalidOperationException("Operation cannot be performed"); + } + + switch (horizontalAmount) + { + case ScrollAmount.LargeDecrement: + Owner.PageLeft(); + break; + case ScrollAmount.SmallDecrement: + Owner.LineLeft(); + break; + case ScrollAmount.SmallIncrement: + Owner.LineRight(); + break; + case ScrollAmount.LargeIncrement: + Owner.PageRight(); + break; + case ScrollAmount.NoAmount: + break; + default: + throw new InvalidOperationException("Operation cannot be performed"); + } + + switch (verticalAmount) + { + case ScrollAmount.LargeDecrement: + Owner.PageUp(); + break; + case ScrollAmount.SmallDecrement: + Owner.LineUp(); + break; + case ScrollAmount.SmallIncrement: + Owner.LineDown(); + break; + case ScrollAmount.LargeIncrement: + Owner.PageDown(); + break; + case ScrollAmount.NoAmount: + break; + default: + throw new InvalidOperationException("Operation cannot be performed"); + } + } + + public void SetScrollPercent(double horizontalPercent, double verticalPercent) + { + if (!IsEnabled()) + throw new ElementNotEnabledException(); + + var scrollHorizontally = horizontalPercent != ScrollPatternIdentifiers.NoScroll; + var scrollVertically = verticalPercent != ScrollPatternIdentifiers.NoScroll; + + if (scrollHorizontally && !HorizontallyScrollable || scrollVertically && !VerticallyScrollable) + { + throw new InvalidOperationException("Operation cannot be performed"); + } + + if (scrollHorizontally && (horizontalPercent < 0.0) || (horizontalPercent > 100.0)) + { + throw new ArgumentOutOfRangeException("horizontalPercent"); + } + + if (scrollVertically && (verticalPercent < 0.0) || (verticalPercent > 100.0)) + { + throw new ArgumentOutOfRangeException("verticalPercent"); + } + + var offset = Owner.Offset; + + if (scrollHorizontally) + { + offset = offset.WithX((Owner.Extent.Width - Owner.Viewport.Width) * horizontalPercent * 0.01); + } + + if (scrollVertically) + { + offset = offset.WithY((Owner.Extent.Height - Owner.Viewport.Height) * verticalPercent * 0.01); + } + + Owner.Offset = offset; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs new file mode 100644 index 0000000000..4626e30ff1 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; +using Avalonia.VisualTree; + +namespace Avalonia.Automation.Peers +{ + public abstract class SelectingItemsControlAutomationPeer : ItemsControlAutomationPeer, + ISelectionProvider + { + private ISelectionModel _selection; + + protected SelectingItemsControlAutomationPeer(SelectingItemsControl owner) + : base(owner) + { + _selection = owner.GetValue(ListBox.SelectionProperty); + _selection.SelectionChanged += OwnerSelectionChanged; + owner.PropertyChanged += OwnerPropertyChanged; + } + + public bool CanSelectMultiple => GetSelectionModeCore().HasAllFlags(SelectionMode.Multiple); + public bool IsSelectionRequired => GetSelectionModeCore().HasAllFlags(SelectionMode.AlwaysSelected); + public IReadOnlyList GetSelection() => GetSelectionCore() ?? Array.Empty(); + + protected virtual IReadOnlyList? GetSelectionCore() + { + List? result = null; + + if (Owner is SelectingItemsControl owner) + { + var selection = Owner.GetValue(ListBox.SelectionProperty); + + foreach (var i in selection.SelectedIndexes) + { + var container = owner.ItemContainerGenerator.ContainerFromIndex(i); + + if (container is Control c && ((IVisual)c).IsAttachedToVisualTree) + { + var peer = GetOrCreate(c); + + if (peer is object) + { + result ??= new List(); + result.Add(peer); + } + } + } + + return result; + } + + return result; + } + + protected virtual SelectionMode GetSelectionModeCore() + { + return (Owner as SelectingItemsControl)?.GetValue(ListBox.SelectionModeProperty) ?? SelectionMode.Single; + } + + protected virtual void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == ListBox.SelectionProperty) + { + _selection.SelectionChanged -= OwnerSelectionChanged; + _selection = Owner.GetValue(ListBox.SelectionProperty); + _selection.SelectionChanged += OwnerSelectionChanged; + RaiseSelectionChanged(); + } + } + + protected virtual void OwnerSelectionChanged(object? sender, SelectionModelSelectionChangedEventArgs e) + { + RaiseSelectionChanged(); + } + + private void RaiseSelectionChanged() + { + RaisePropertyChangedEvent(SelectionPatternIdentifiers.SelectionProperty, null, null); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs new file mode 100644 index 0000000000..8a89e38f62 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class TextBlockAutomationPeer : ControlAutomationPeer + { + public TextBlockAutomationPeer(TextBlock owner) + : base(owner) + { + } + + public new TextBlock Owner => (TextBlock)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Text; + } + + protected override string? GetNameCore() => Owner.Text; + + protected override bool IsControlElementCore() + { + // Return false if the control is part of a control template. + return Owner.TemplatedParent is null && base.IsControlElementCore(); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs new file mode 100644 index 0000000000..9be17afa8c --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs @@ -0,0 +1,23 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class TextBoxAutomationPeer : ControlAutomationPeer, IValueProvider + { + public TextBoxAutomationPeer(TextBox owner) + : base(owner) + { + } + + public new TextBox Owner => (TextBox)base.Owner; + public bool IsReadOnly => Owner.IsReadOnly; + public string? Value => Owner.Text; + public void SetValue(string? value) => Owner.Text = value; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Edit; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs new file mode 100644 index 0000000000..979d54f48e --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs @@ -0,0 +1,39 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public class ToggleButtonAutomationPeer : ContentControlAutomationPeer, IToggleProvider + { + public ToggleButtonAutomationPeer(ToggleButton owner) + : base(owner) + { + } + + public new ToggleButton Owner => (ToggleButton)base.Owner; + + ToggleState IToggleProvider.ToggleState + { + get => Owner.IsChecked switch + { + true => ToggleState.On, + false => ToggleState.Off, + null => ToggleState.Indeterminate, + }; + } + + void IToggleProvider.Toggle() + { + EnsureEnabled(); + Owner.PerformClick(); + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Button; + } + + protected override bool IsContentElementCore() => true; + protected override bool IsControlElementCore() => true; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs new file mode 100644 index 0000000000..56d5aa79ae --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Automation.Peers +{ + /// + /// An automation peer which represents an unrealized element + /// + public abstract class UnrealizedElementAutomationPeer : AutomationPeer + { + public void SetParent(AutomationPeer? parent) => TrySetParent(parent); + protected override void BringIntoViewCore() => GetParent()?.BringIntoView(); + protected override Rect GetBoundingRectangleCore() => GetParent()?.GetBoundingRectangle() ?? default; + protected override IReadOnlyList GetOrCreateChildrenCore() => Array.Empty(); + protected override bool HasKeyboardFocusCore() => false; + protected override bool IsContentElementCore() => false; + protected override bool IsControlElementCore() => false; + protected override bool IsEnabledCore() => true; + protected override bool IsKeyboardFocusableCore() => false; + protected override void SetFocusCore() { } + protected override bool ShowContextMenuCore() => false; + protected internal override bool TrySetParent(AutomationPeer? parent) => false; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs new file mode 100644 index 0000000000..1162132d54 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class WindowAutomationPeer : WindowBaseAutomationPeer + { + public WindowAutomationPeer(Window owner) + : base(owner) + { + if (owner.IsVisible) + StartTrackingFocus(); + else + owner.Opened += OnOpened; + owner.Closed += OnClosed; + } + + public new Window Owner => (Window)base.Owner; + + protected override string? GetNameCore() => Owner.Title; + + private void OnOpened(object? sender, EventArgs e) + { + Owner.Opened -= OnOpened; + StartTrackingFocus(); + } + + private void OnClosed(object? sender, EventArgs e) + { + Owner.Closed -= OnClosed; + StopTrackingFocus(); + } + } +} + + diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs new file mode 100644 index 0000000000..30b56bbd96 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -0,0 +1,78 @@ +using System; +using System.ComponentModel; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Automation.Peers +{ + public class WindowBaseAutomationPeer : ControlAutomationPeer, IRootProvider + { + private Control? _focus; + + public WindowBaseAutomationPeer(WindowBase owner) + : base(owner) + { + } + + public new WindowBase Owner => (WindowBase)base.Owner; + public ITopLevelImpl? PlatformImpl => Owner.PlatformImpl; + + public event EventHandler? FocusChanged; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Window; + } + + public AutomationPeer? GetFocus() => _focus is object ? GetOrCreate(_focus) : null; + + public AutomationPeer? GetPeerFromPoint(Point p) + { + var hit = Owner.GetVisualAt(p)?.FindAncestorOfType(includeSelf: true); + return hit is object ? GetOrCreate(hit) : null; + } + + protected void StartTrackingFocus() + { + if (KeyboardDevice.Instance is not null) + { + KeyboardDevice.Instance.PropertyChanged += KeyboardDevicePropertyChanged; + OnFocusChanged(KeyboardDevice.Instance.FocusedElement); + } + } + + protected void StopTrackingFocus() + { + if (KeyboardDevice.Instance is not null) + KeyboardDevice.Instance.PropertyChanged -= KeyboardDevicePropertyChanged; + } + + private void OnFocusChanged(IInputElement? focus) + { + var oldFocus = _focus; + + _focus = focus?.VisualRoot == Owner ? focus as Control : null; + + if (_focus != oldFocus) + { + var peer = _focus is object ? + _focus == Owner ? this : + GetOrCreate(_focus) : null; + FocusChanged?.Invoke(this, EventArgs.Empty); + } + } + + private void KeyboardDevicePropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(KeyboardDevice.FocusedElement)) + { + OnFocusChanged(KeyboardDevice.Instance!.FocusedElement); + } + } + } +} + + diff --git a/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs b/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs new file mode 100644 index 0000000000..a4691180a3 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs @@ -0,0 +1,33 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support UI Automation client access to controls that + /// visually expand to display content and collapse to hide content. + /// + public interface IExpandCollapseProvider + { + /// + /// Gets the state, expanded or collapsed, of the control. + /// + ExpandCollapseState ExpandCollapseState { get; } + + /// + /// Gets a value indicating whether expanding the element shows a menu of items to the user, + /// such as drop-down list. + /// + /// + /// Used in OSX to enable the "Show Menu" action on the element. + /// + bool ShowsMenu { get; } + + /// + /// Displays all child nodes, controls, or content of the control. + /// + void Expand(); + + /// + /// Hides all nodes, controls, or content that are descendants of the control. + /// + void Collapse(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs b/src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs new file mode 100644 index 0000000000..47d7211c92 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support UI Automation client access to controls that + /// initiate or perform a single, unambiguous action and do not maintain state when + /// activated. + /// + public interface IInvokeProvider + { + /// + /// Sends a request to activate a control and initiate its single, unambiguous action. + /// + void Invoke(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs new file mode 100644 index 0000000000..43a877a21a --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs @@ -0,0 +1,47 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support access by a UI Automation client to controls + /// that can be set to a value within a range. + /// + public interface IRangeValueProvider + { + /// + /// Gets a value that indicates whether the value of a control is read-only. + /// + bool IsReadOnly { get; } + + /// + /// Gets the minimum range value that is supported by the control. + /// + double Minimum { get; } + + /// + /// Gets the maximum range value that is supported by the control. + /// + double Maximum { get; } + + /// + /// Gets the value of the control. + /// + double Value { get; } + + /// + /// Gets the value that is added to or subtracted from the Value property when a large + /// change is made, such as with the PAGE DOWN key. + /// + double LargeChange { get; } + + /// + /// Gets the value that is added to or subtracted from the Value property when a small + /// change is made, such as with an arrow key. + /// + double SmallChange { get; } + + /// + /// Sets the value of the control. + /// + /// The value to set. + public void SetValue(double value); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs new file mode 100644 index 0000000000..ce38059559 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Automation.Peers; +using Avalonia.Platform; + +namespace Avalonia.Automation.Provider +{ + public interface IRootProvider + { + ITopLevelImpl? PlatformImpl { get; } + AutomationPeer? GetFocus(); + AutomationPeer? GetPeerFromPoint(Point p); + event EventHandler? FocusChanged; + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs b/src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs new file mode 100644 index 0000000000..1055a2f1e1 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs @@ -0,0 +1,71 @@ +namespace Avalonia.Automation.Provider +{ + public enum ScrollAmount + { + LargeDecrement, + SmallDecrement, + NoAmount, + LargeIncrement, + SmallIncrement, + } + + /// + /// Exposes methods and properties to support access by a UI Automation client to a control + /// that acts as a scrollable container for a collection of child objects. + /// + public interface IScrollProvider + { + /// + /// Gets a value that indicates whether the control can scroll horizontally. + /// + bool HorizontallyScrollable { get; } + + /// + /// Gets the current horizontal scroll position. + /// + double HorizontalScrollPercent { get; } + + /// + /// Gets the current horizontal view size. + /// + double HorizontalViewSize { get; } + + /// + /// Gets a value that indicates whether the control can scroll vertically. + /// + bool VerticallyScrollable { get; } + + /// + /// Gets the current vertical scroll position. + /// + double VerticalScrollPercent { get; } + + /// + /// Gets the vertical view size. + /// + double VerticalViewSize { get; } + + /// + /// Scrolls the visible region of the content area horizontally and vertically. + /// + /// The horizontal increment specific to the control. + /// The vertical increment specific to the control. + void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount); + + /// + /// Sets the horizontal and vertical scroll position as a percentage of the total content + /// area within the control. + /// + /// + /// The horizontal position as a percentage of the content area's total range. + /// should be passed in if the control + /// cannot be scrolled in this direction. + /// + /// + /// The vertical position as a percentage of the content area's total range. + /// should be passed in if the control + /// cannot be scrolled in this direction. + /// + void SetScrollPercent(double horizontalPercent, double verticalPercent); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs b/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs new file mode 100644 index 0000000000..6cea1d1350 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs @@ -0,0 +1,35 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support access by a UI Automation client to individual, + /// selectable child controls of containers that implement . + /// + public interface ISelectionItemProvider + { + /// + /// Gets a value that indicates whether an item is selected. + /// + bool IsSelected { get; } + + /// + /// Gets the UI Automation provider that implements and + /// acts as the container for the calling object. + /// + ISelectionProvider? SelectionContainer { get; } + + /// + /// Adds the current element to the collection of selected items. + /// + void AddToSelection(); + + /// + /// Removes the current element from the collection of selected items. + /// + void RemoveFromSelection(); + + /// + /// Clears any existing selection and then selects the current element. + /// + void Select(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs b/src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs new file mode 100644 index 0000000000..bf21c0151f --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Avalonia.Automation.Peers; + +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support access by a UI Automation client to controls + /// that act as containers for a collection of individual, selectable child items. + /// + public interface ISelectionProvider + { + /// + /// Gets a value that indicates whether the provider allows more than one child element + /// to be selected concurrently. + /// + bool CanSelectMultiple { get; } + + /// + /// Gets a value that indicates whether the provider requires at least one child element + /// to be selected. + /// + bool IsSelectionRequired { get; } + + /// + /// Retrieves a provider for each child element that is selected. + /// + IReadOnlyList GetSelection(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs b/src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs new file mode 100644 index 0000000000..67913e3204 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs @@ -0,0 +1,40 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Contains values that specify the toggle state of a UI Automation element. + /// + public enum ToggleState + { + /// + /// The UI Automation element isn't selected, checked, marked, or otherwise activated. + /// + Off, + + /// + /// The UI Automation element is selected, checked, marked, or otherwise activated. + /// + On, + + /// + /// The UI Automation element is in an indeterminate state. + /// + Indeterminate, + } + + /// + /// Exposes methods and properties to support UI Automation client access to controls that can + /// cycle through a set of states and maintain a particular state. + /// + public interface IToggleProvider + { + /// + /// Gets the toggle state of the control. + /// + ToggleState ToggleState { get; } + + /// + /// Cycles through the toggle states of a control. + /// + void Toggle(); + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs b/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs new file mode 100644 index 0000000000..e025e28782 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IValueProvider.cs @@ -0,0 +1,29 @@ +namespace Avalonia.Automation.Provider +{ + /// + /// Exposes methods and properties to support access by a UI Automation client to controls + /// that have an intrinsic value that does not span a range and that can be represented as + /// a string. + /// + public interface IValueProvider + { + /// + /// Gets a value that indicates whether the value of a control is read-only. + /// + bool IsReadOnly { get; } + + /// + /// Gets the value of the control. + /// + public string? Value { get; } + + /// + /// Sets the value of a control. + /// + /// + /// The value to set. The provider is responsible for converting the value to the + /// appropriate data type. + /// + public void SetValue(string? value); + } +} diff --git a/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs b/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs new file mode 100644 index 0000000000..625b37d001 --- /dev/null +++ b/src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs @@ -0,0 +1,30 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class RangeValuePatternIdentifiers + { + /// + /// Identifies automation property. + /// + public static AutomationProperty IsReadOnlyProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty MinimumProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty MaximumProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty ValueProperty { get; } = new AutomationProperty(); + } +} diff --git a/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs b/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs new file mode 100644 index 0000000000..d9e843e75a --- /dev/null +++ b/src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs @@ -0,0 +1,45 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class ScrollPatternIdentifiers + { + /// + /// Specifies that scrolling should not be performed. + /// + public const double NoScroll = -1; + + /// + /// Identifies automation property. + /// + public static AutomationProperty HorizontallyScrollableProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty HorizontalScrollPercentProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty HorizontalViewSizeProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty VerticallyScrollableProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty VerticalScrollPercentProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty VerticalViewSizeProperty { get; } = new AutomationProperty(); + } +} diff --git a/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs b/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs new file mode 100644 index 0000000000..c3669528cd --- /dev/null +++ b/src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs @@ -0,0 +1,25 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class SelectionPatternIdentifiers + { + /// + /// Identifies automation property. + /// + public static AutomationProperty CanSelectMultipleProperty { get; } = new AutomationProperty(); + + /// + /// Identifies automation property. + /// + public static AutomationProperty IsSelectionRequiredProperty { get; } = new AutomationProperty(); + + /// + /// Identifies the property that gets the selected items in a container. + /// + public static AutomationProperty SelectionProperty { get; } = new AutomationProperty(); + } +} diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 543a513d57..00f094f508 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -6,17 +6,18 @@ - - - - - - + + + + + + + diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index ee3be1d5b3..bc740c133a 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -1,8 +1,10 @@ +using System; using Avalonia.Collections; using Avalonia.Controls.Shapes; using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -69,6 +71,8 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(BorderLineJoin), PenLineJoin.Miter); private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); + private Thickness? _layoutThickness; + private double _scale; /// /// Initializes static members of the class. @@ -88,6 +92,18 @@ namespace Avalonia.Controls AffectsMeasure(BorderThicknessProperty); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + switch (change.Property.Name) + { + case nameof(UseLayoutRounding): + case nameof(BorderThickness): + _layoutThickness = null; + break; + } + } + /// /// Gets or sets a brush with which to paint the background. /// @@ -169,13 +185,43 @@ namespace Avalonia.Controls set => SetValue(BoxShadowProperty, value); } + private Thickness LayoutThickness + { + get + { + VerifyScale(); + + if (_layoutThickness == null) + { + var borderThickness = BorderThickness; + + if (UseLayoutRounding) + borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale, _scale); + + _layoutThickness = borderThickness; + } + + return _layoutThickness.Value; + } + } + + private void VerifyScale() + { + var currentScale = LayoutHelper.GetLayoutScale(this); + if (MathUtilities.AreClose(currentScale, _scale)) + return; + + _scale = currentScale; + _layoutThickness = null; + } + /// /// Renders the control. /// /// The drawing context. public override void Render(DrawingContext context) { - _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush, + _borderRenderHelper.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush, BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray); } diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index a7a4759182..db4ca6bc43 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Windows.Input; +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; @@ -28,11 +29,14 @@ namespace Avalonia.Controls } /// - /// A button control. + /// A standard button control. /// - [PseudoClasses(":pressed")] - public class Button : ContentControl, ICommandSource + [PseudoClasses(pcFlyoutOpen, pcPressed)] + public class Button : ContentControl, ICommandSource, IClickableControl { + protected const string pcPressed = ":pressed"; + protected const string pcFlyoutOpen = ":flyout-open"; + /// /// Defines the property. /// @@ -91,6 +95,7 @@ namespace Avalonia.Controls private ICommand? _command; private bool _commandCanExecute = true; private KeyGesture? _hotkey; + private bool _isFlyoutOpen = false; /// /// Initializes static members of the class. @@ -106,7 +111,6 @@ namespace Avalonia.Controls /// public Button() { - UpdatePseudoClasses(IsPressed); } /// @@ -237,7 +241,7 @@ namespace Avalonia.Controls { HotKey = _hotkey; } - + base.OnAttachedToLogicalTree(e); if (Command != null) @@ -305,6 +309,8 @@ namespace Avalonia.Controls IsPressed = false; e.Handled = true; } + + base.OnKeyUp(e); } /// @@ -327,11 +333,30 @@ namespace Avalonia.Controls } } + /// + /// Opens the button's flyout. + /// protected virtual void OpenFlyout() { Flyout?.ShowAt(this); } + /// + /// Invoked when the button's flyout is opened. + /// + protected virtual void OnFlyoutOpened() + { + // Available for derived types + } + + /// + /// Invoked when the button's flyout is closed. + /// + protected virtual void OnFlyoutClosed() + { + // Available for derived types + } + /// protected override void OnPointerPressed(PointerPressedEventArgs e) { @@ -370,6 +395,8 @@ namespace Avalonia.Controls /// protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { + base.OnPointerCaptureLost(e); + IsPressed = false; } @@ -382,7 +409,17 @@ namespace Avalonia.Controls } /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + UnregisterFlyoutEvents(Flyout); + RegisterFlyoutEvents(Flyout); + UpdatePseudoClasses(); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -390,12 +427,13 @@ namespace Avalonia.Controls { if (((ILogical)this).IsAttachedToLogicalTree) { - if (change.OldValue.GetValueOrDefault() is ICommand oldCommand) + var (oldValue, newValue) = change.GetOldAndNewValue(); + if (oldValue is ICommand oldCommand) { oldCommand.CanExecuteChanged -= CanExecuteChanged; } - if (change.NewValue.GetValueOrDefault() is ICommand newCommand) + if (newValue is ICommand newCommand) { newCommand.CanExecuteChanged += CanExecuteChanged; } @@ -409,7 +447,7 @@ namespace Avalonia.Controls } else if (change.Property == IsCancelProperty) { - var isCancel = change.NewValue.GetValueOrDefault(); + var isCancel = change.GetNewValue(); if (VisualRoot is IInputElement inputRoot) { @@ -425,7 +463,7 @@ namespace Avalonia.Controls } else if (change.Property == IsDefaultProperty) { - var isDefault = change.NewValue.GetValueOrDefault(); + var isDefault = change.GetNewValue(); if (VisualRoot is IInputElement inputRoot) { @@ -441,27 +479,40 @@ namespace Avalonia.Controls } else if (change.Property == IsPressedProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(); } else if (change.Property == FlyoutProperty) { + var (oldFlyout, newFlyout) = change.GetOldAndNewValue(); + // If flyout is changed while one is already open, make sure we // close the old one first - if (change.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout && + if (oldFlyout != null && oldFlyout.IsOpen) { oldFlyout.Hide(); } + + // Must unregister events here while a reference to the old flyout still exists + UnregisterFlyoutEvents(oldFlyout); + + RegisterFlyoutEvents(newFlyout); + UpdatePseudoClasses(); } } + protected override AutomationPeer OnCreateAutomationPeer() => new ButtonAutomationPeer(this); + /// - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { - base.UpdateDataValidation(property, value); + base.UpdateDataValidation(property, state, error); if (property == CommandProperty) { - if (value.Type == BindingValueType.BindingError) + if (state == BindingValueType.BindingError) { if (_commandCanExecute) { @@ -472,6 +523,8 @@ namespace Avalonia.Controls } } + internal void PerformClick() => OnClick(); + /// /// Called when the event fires. /// @@ -488,6 +541,32 @@ namespace Avalonia.Controls } } + /// + /// Registers all flyout events. + /// + /// The flyout to connect events to. + private void RegisterFlyoutEvents(FlyoutBase? flyout) + { + if (flyout != null) + { + flyout.Opened += Flyout_Opened; + flyout.Closed += Flyout_Closed; + } + } + + /// + /// Explicitly unregisters all flyout events. + /// + /// The flyout to disconnect events from. + private void UnregisterFlyoutEvents(FlyoutBase? flyout) + { + if (flyout != null) + { + flyout.Opened -= Flyout_Opened; + flyout.Closed -= Flyout_Closed; + } + } + /// /// Starts listening for the Enter key when the button . /// @@ -534,6 +613,7 @@ namespace Avalonia.Controls if (e.Key == Key.Enter && IsVisible && IsEnabled) { OnClick(); + e.Handled = true; } } @@ -547,17 +627,60 @@ namespace Avalonia.Controls if (e.Key == Key.Escape && IsVisible && IsEnabled) { OnClick(); + e.Handled = true; } } /// /// Updates the visual state of the control by applying latest PseudoClasses. /// - private void UpdatePseudoClasses(bool isPressed) + private void UpdatePseudoClasses() { - PseudoClasses.Set(":pressed", isPressed); + PseudoClasses.Set(pcFlyoutOpen, _isFlyoutOpen); + PseudoClasses.Set(pcPressed, IsPressed); } void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + + void IClickableControl.RaiseClick() => OnClick(); + + /// + /// Event handler for when the button's flyout is opened. + /// + private void Flyout_Opened(object? sender, EventArgs e) + { + var flyout = sender as FlyoutBase; + + // It is possible to share flyouts among multiple controls including Button. + // This can cause a problem here since all controls that share a flyout receive + // the same Opened/Closed events at the same time. + // For Button that means they all would be updating their pseudoclasses accordingly. + // In other words, all Buttons with a shared Flyout would have the backgrounds changed together. + // To fix this, only continue here if the Flyout target matches this Button instance. + if (object.ReferenceEquals(flyout?.Target, this)) + { + _isFlyoutOpen = true; + UpdatePseudoClasses(); + + OnFlyoutOpened(); + } + } + + /// + /// Event handler for when the button's flyout is closed. + /// + private void Flyout_Closed(object? sender, EventArgs e) + { + var flyout = sender as FlyoutBase; + + // See comments in Flyout_Opened + if (object.ReferenceEquals(flyout?.Target, this)) + { + _isFlyoutOpen = false; + UpdatePseudoClasses(); + + OnFlyoutClosed(); + } + } } } diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 31aba024ae..e455c6c6f3 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 { @@ -208,13 +210,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ButtonSpinnerLocationProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } } 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 deleted file mode 100644 index c1f487c32d..0000000000 --- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs +++ /dev/null @@ -1,1136 +0,0 @@ -// (c) Copyright Microsoft Corporation. -// This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. -// All other rights reserved. - -using System; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Globalization; -using Avalonia.Controls.Primitives; -using Avalonia.Data; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Layout; - -namespace Avalonia.Controls -{ - /// - /// Provides data for the - /// - /// event. - /// - public class CalendarDatePickerDateValidationErrorEventArgs : EventArgs - { - private bool _throwException; - - /// - /// Initializes a new instance of the - /// - /// class. - /// - /// - /// The initial exception from the - /// - /// event. - /// - /// - /// The text that caused the - /// - /// event. - /// - public CalendarDatePickerDateValidationErrorEventArgs(Exception exception, string text) - { - this.Text = text; - this.Exception = exception; - } - - /// - /// Gets the initial exception associated with the - /// - /// event. - /// - /// - /// The exception associated with the validation failure. - /// - public Exception Exception { get; private set; } - - /// - /// Gets the text that caused the - /// - /// event. - /// - /// - /// The text that caused the validation failure. - /// - public string Text { get; private set; } - - /// - /// Gets or sets a value indicating whether - /// - /// should be thrown. - /// - /// - /// True if the exception should be thrown; otherwise, false. - /// - /// - /// If set to true and - /// - /// is null. - /// - public bool ThrowException - { - get { return this._throwException; } - set - { - if (value && this.Exception == null) - { - throw new ArgumentException("Cannot Throw Null Exception"); - } - this._throwException = value; - } - } - } - - /// - /// Specifies date formats for a - /// . - /// - public enum CalendarDatePickerFormat - { - /// - /// Specifies that the date should be displayed using unabbreviated days - /// of the week and month names. - /// - Long = 0, - - /// - /// Specifies that the date should be displayed using abbreviated days - /// of the week and month names. - /// - Short = 1, - - /// - /// Specifies that the date should be displayed using a custom format string. - /// - Custom = 2 - } - - public class CalendarDatePicker : TemplatedControl - { - private const string ElementTextBox = "PART_TextBox"; - private const string ElementButton = "PART_Button"; - private const string ElementPopup = "PART_Popup"; - private const string ElementCalendar = "PART_Calendar"; - - private Calendar? _calendar; - private string _defaultText; - private Button? _dropDownButton; - //private Canvas _outsideCanvas; - //private Canvas _outsidePopupCanvas; - private Popup? _popUp; - private TextBox? _textBox; - private IDisposable? _textBoxTextChangedSubscription; - private IDisposable? _buttonPointerPressedSubscription; - - private DateTime? _onOpenSelectedDate; - private bool _settingSelectedDate; - - private DateTime _displayDate; - private DateTime? _displayDateStart; - private DateTime? _displayDateEnd; - private bool _isDropDownOpen; - private DateTime? _selectedDate; - private string? _text; - private bool _suspendTextChangeHandler = false; - private bool _isPopupClosing = false; - private bool _ignoreButtonClick = false; - - /// - /// Gets a collection of dates that are marked as not selectable. - /// - /// - /// A collection of dates that cannot be selected. The default value is - /// an empty collection. - /// - public CalendarBlackoutDatesCollection? BlackoutDates { get; private set; } - - public static readonly DirectProperty DisplayDateProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDate), - o => o.DisplayDate, - (o, v) => o.DisplayDate = v); - public static readonly DirectProperty DisplayDateStartProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateStart), - o => o.DisplayDateStart, - (o, v) => o.DisplayDateStart = v); - public static readonly DirectProperty DisplayDateEndProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateEnd), - o => o.DisplayDateEnd, - (o, v) => o.DisplayDateEnd = v); - public static readonly StyledProperty FirstDayOfWeekProperty = - AvaloniaProperty.Register(nameof(FirstDayOfWeek)); - - public static readonly DirectProperty IsDropDownOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsDropDownOpen), - o => o.IsDropDownOpen, - (o, v) => o.IsDropDownOpen = v); - - public static readonly StyledProperty IsTodayHighlightedProperty = - AvaloniaProperty.Register(nameof(IsTodayHighlighted)); - public static readonly DirectProperty SelectedDateProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectedDate), - o => o.SelectedDate, - (o, v) => o.SelectedDate = v, - enableDataValidation: true); - - public static readonly StyledProperty SelectedDateFormatProperty = - AvaloniaProperty.Register( - nameof(SelectedDateFormat), - defaultValue: CalendarDatePickerFormat.Short, - validate: IsValidSelectedDateFormat); - - public static readonly StyledProperty CustomDateFormatStringProperty = - AvaloniaProperty.Register( - nameof(CustomDateFormatString), - defaultValue: "d", - validate: IsValidDateFormatString); - - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect( - nameof(Text), - o => o.Text, - (o, v) => o.Text = v); - public static readonly StyledProperty WatermarkProperty = - TextBox.WatermarkProperty.AddOwner(); - public static readonly StyledProperty UseFloatingWatermarkProperty = - TextBox.UseFloatingWatermarkProperty.AddOwner(); - - - /// - /// Defines the property. - /// - public static readonly StyledProperty HorizontalContentAlignmentProperty = - ContentControl.HorizontalContentAlignmentProperty.AddOwner(); - - /// - /// Defines the property. - /// - public static readonly StyledProperty VerticalContentAlignmentProperty = - ContentControl.VerticalContentAlignmentProperty.AddOwner(); - - /// - /// Gets or sets the date to display. - /// - /// - /// The date to display. The default - /// . - /// - /// - /// The specified date is not in the range defined by - /// - /// and - /// . - /// - public DateTime DisplayDate - { - get { return _displayDate; } - set { SetAndRaise(DisplayDateProperty, ref _displayDate, value); } - } - - /// - /// Gets or sets the first date to be displayed. - /// - /// The first date to display. - public DateTime? DisplayDateStart - { - get { return _displayDateStart; } - set { SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value); } - } - - /// - /// Gets or sets the last date to be displayed. - /// - /// The last date to display. - public DateTime? DisplayDateEnd - { - get { return _displayDateEnd; } - set { SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value); } - } - - /// - /// Gets or sets the day that is considered the beginning of the week. - /// - /// - /// A representing the beginning of - /// the week. The default is . - /// - public DayOfWeek FirstDayOfWeek - { - get { return GetValue(FirstDayOfWeekProperty); } - set { SetValue(FirstDayOfWeekProperty, value); } - } - - /// - /// Gets or sets a value indicating whether the drop-down - /// is open or closed. - /// - /// - /// True if the is - /// open; otherwise, false. The default is false. - /// - public bool IsDropDownOpen - { - get { return _isDropDownOpen; } - set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); } - } - - /// - /// Gets or sets a value indicating whether the current date will be - /// highlighted. - /// - /// - /// True if the current date is highlighted; otherwise, false. The - /// default is true. - /// - public bool IsTodayHighlighted - { - get { return GetValue(IsTodayHighlightedProperty); } - set { SetValue(IsTodayHighlightedProperty, value); } - } - - /// - /// Gets or sets the currently selected date. - /// - /// - /// The date currently selected. The default is null. - /// - /// - /// The specified date is not in the range defined by - /// - /// and - /// , - /// or the specified date is in the - /// - /// collection. - /// - public DateTime? SelectedDate - { - get { return _selectedDate; } - set { SetAndRaise(SelectedDateProperty, ref _selectedDate, value); } - } - - /// - /// Gets or sets the format that is used to display the selected date. - /// - /// - /// The format that is used to display the selected date. The default is - /// . - /// - /// - /// An specified format is not valid. - /// - public CalendarDatePickerFormat SelectedDateFormat - { - get { return GetValue(SelectedDateFormatProperty); } - set { SetValue(SelectedDateFormatProperty, value); } - } - - public string CustomDateFormatString - { - get { return GetValue(CustomDateFormatStringProperty); } - set { SetValue(CustomDateFormatStringProperty, value); } - } - - /// - /// Gets or sets the text that is displayed by the - /// . - /// - /// - /// The text displayed by the - /// . - /// - /// - /// The text entered cannot be parsed to a valid date, and the exception - /// is not suppressed. - /// - /// - /// The text entered parses to a date that is not selectable. - /// - public string? Text - { - get { return _text; } - set { SetAndRaise(TextProperty, ref _text, value); } - } - - public string? Watermark - { - get { return GetValue(WatermarkProperty); } - set { SetValue(WatermarkProperty, value); } - } - public bool UseFloatingWatermark - { - get { return GetValue(UseFloatingWatermarkProperty); } - set { SetValue(UseFloatingWatermarkProperty, value); } - } - - - /// - /// Gets or sets the horizontal alignment of the content within the control. - /// - public HorizontalAlignment HorizontalContentAlignment - { - get => GetValue(HorizontalContentAlignmentProperty); - set => SetValue(HorizontalContentAlignmentProperty, value); - } - - /// - /// Gets or sets the vertical alignment of the content within the control. - /// - public VerticalAlignment VerticalContentAlignment - { - get => GetValue(VerticalContentAlignmentProperty); - set => SetValue(VerticalContentAlignmentProperty, value); - } - - /// - /// Occurs when the drop-down - /// is closed. - /// - public event EventHandler? CalendarClosed; - - /// - /// Occurs when the drop-down - /// is opened. - /// - public event EventHandler? CalendarOpened; - - /// - /// Occurs when - /// is assigned a value that cannot be interpreted as a date. - /// - public event EventHandler? DateValidationError; - - /// - /// Occurs when the - /// - /// property is changed. - /// - public event EventHandler? SelectedDateChanged; - - static CalendarDatePicker() - { - FocusableProperty.OverrideDefaultValue(true); - - IsDropDownOpenProperty.Changed.AddClassHandler((x,e) => x.OnIsDropDownOpenChanged(e)); - SelectedDateProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateChanged(e)); - SelectedDateFormatProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateFormatChanged(e)); - CustomDateFormatStringProperty.Changed.AddClassHandler((x,e) => x.OnCustomDateFormatStringChanged(e)); - TextProperty.Changed.AddClassHandler((x,e) => x.OnTextChanged(e)); - } - /// - /// Initializes a new instance of the - /// class. - /// - public CalendarDatePicker() - { - FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek; - _defaultText = string.Empty; - DisplayDate = DateTime.Today; - } - - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - if (_calendar != null) - { - _calendar.DayButtonMouseUp -= Calendar_DayButtonMouseUp; - _calendar.DisplayDateChanged -= Calendar_DisplayDateChanged; - _calendar.SelectedDatesChanged -= Calendar_SelectedDatesChanged; - _calendar.PointerReleased -= Calendar_PointerReleased; - _calendar.KeyDown -= Calendar_KeyDown; - } - _calendar = e.NameScope.Find(ElementCalendar); - if (_calendar != null) - { - _calendar.SelectionMode = CalendarSelectionMode.SingleDate; - - _calendar.DayButtonMouseUp += Calendar_DayButtonMouseUp; - _calendar.DisplayDateChanged += Calendar_DisplayDateChanged; - _calendar.SelectedDatesChanged += Calendar_SelectedDatesChanged; - _calendar.PointerReleased += Calendar_PointerReleased; - _calendar.KeyDown += Calendar_KeyDown; - - var currentBlackoutDays = BlackoutDates; - BlackoutDates = _calendar.BlackoutDates; - if(currentBlackoutDays != null) - { - foreach (var range in currentBlackoutDays) - { - BlackoutDates.Add(range); - } - } - } - - if (_popUp != null) - { - _popUp.Child = null; - _popUp.Closed -= PopUp_Closed; - } - _popUp = e.NameScope.Find(ElementPopup); - if(_popUp != null) - { - _popUp.Closed += PopUp_Closed; - if (IsDropDownOpen) - { - OpenDropDown(); - } - } - - if(_dropDownButton != null) - { - _dropDownButton.Click -= DropDownButton_Click; - _buttonPointerPressedSubscription?.Dispose(); - } - _dropDownButton = e.NameScope.Find + [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 { @@ -167,13 +173,13 @@ namespace Avalonia.Controls.Primitives if (MonthView != null) { var childCount = Calendar.RowsPerMonth + Calendar.RowsPerMonth * Calendar.ColumnsPerMonth; - var children = new List(childCount); + using var children = new PooledList(childCount); for (int i = 0; i < Calendar.RowsPerMonth; i++) { if (_dayTitleTemplate != null) { - var cell = _dayTitleTemplate.Build(); + var cell = (Control) _dayTitleTemplate.Build(); cell.DataContext = string.Empty; cell.SetValue(Grid.RowProperty, 0); cell.SetValue(Grid.ColumnProperty, i); @@ -181,11 +187,16 @@ namespace Avalonia.Controls.Primitives } } + EventHandler cellMouseLeftButtonDown = Cell_MouseLeftButtonDown; + EventHandler cellMouseLeftButtonUp = Cell_MouseLeftButtonUp; + EventHandler cellMouseEnter = Cell_MouseEnter; + EventHandler cellClick = Cell_Click; + for (int i = 1; i < Calendar.RowsPerMonth; i++) { for (int j = 0; j < Calendar.ColumnsPerMonth; j++) { - CalendarDayButton cell = new CalendarDayButton(); + var cell = new CalendarDayButton(); if (Owner != null) { @@ -193,10 +204,10 @@ namespace Avalonia.Controls.Primitives } cell.SetValue(Grid.RowProperty, i); cell.SetValue(Grid.ColumnProperty, j); - cell.CalendarDayButtonMouseDown += Cell_MouseLeftButtonDown; - cell.CalendarDayButtonMouseUp += Cell_MouseLeftButtonUp; - cell.PointerEnter += Cell_MouseEnter; - cell.Click += Cell_Click; + cell.CalendarDayButtonMouseDown += cellMouseLeftButtonDown; + cell.CalendarDayButtonMouseUp += cellMouseLeftButtonUp; + cell.PointerEnter += cellMouseEnter; + cell.Click += cellClick; children.Add(cell); } } @@ -209,12 +220,15 @@ namespace Avalonia.Controls.Primitives var childCount = Calendar.RowsPerYear * Calendar.ColumnsPerYear; var children = new List(childCount); - CalendarButton month; + EventHandler monthCalendarButtonMouseDown = Month_CalendarButtonMouseDown; + EventHandler monthCalendarButtonMouseUp = Month_CalendarButtonMouseUp; + EventHandler monthMouseEnter = Month_MouseEnter; + for (int i = 0; i < Calendar.RowsPerYear; i++) { for (int j = 0; j < Calendar.ColumnsPerYear; j++) { - month = new CalendarButton(); + var month = new CalendarButton(); if (Owner != null) { @@ -222,9 +236,9 @@ namespace Avalonia.Controls.Primitives } month.SetValue(Grid.RowProperty, i); month.SetValue(Grid.ColumnProperty, j); - month.CalendarLeftMouseButtonDown += Month_CalendarButtonMouseDown; - month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp; - month.PointerEnter += Month_MouseEnter; + month.CalendarLeftMouseButtonDown += monthCalendarButtonMouseDown; + month.CalendarLeftMouseButtonUp += monthCalendarButtonMouseUp; + month.PointerEnter += monthMouseEnter; children.Add(month); } } diff --git a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs index eb90f6c399..7a5c74a51b 100644 --- a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs +++ b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs @@ -68,10 +68,7 @@ namespace Avalonia.Controls public static DateTime DiscardDayTime(DateTime d) { - int year = d.Year; - int month = d.Month; - DateTime newD = new DateTime(year, month, 1, 0, 0, 0); - return newD; + return new DateTime(d.Year, d.Month, 1, 0, 0, 0); } [return: NotNullIfNotNull("d")] diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs new file mode 100644 index 0000000000..6c2356b411 --- /dev/null +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs @@ -0,0 +1,303 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Layout; + +namespace Avalonia.Controls +{ + /// + public partial class CalendarDatePicker + { + /// + /// Defines the property. + /// + public static readonly DirectProperty DisplayDateProperty = + AvaloniaProperty.RegisterDirect( + nameof(DisplayDate), + o => o.DisplayDate, + (o, v) => o.DisplayDate = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty DisplayDateStartProperty = + AvaloniaProperty.RegisterDirect( + nameof(DisplayDateStart), + o => o.DisplayDateStart, + (o, v) => o.DisplayDateStart = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty DisplayDateEndProperty = + AvaloniaProperty.RegisterDirect( + nameof(DisplayDateEnd), + o => o.DisplayDateEnd, + (o, v) => o.DisplayDateEnd = v); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FirstDayOfWeekProperty = + AvaloniaProperty.Register(nameof(FirstDayOfWeek)); + + /// + /// Defines the property. + /// + public static readonly DirectProperty IsDropDownOpenProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsDropDownOpen), + o => o.IsDropDownOpen, + (o, v) => o.IsDropDownOpen = v); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsTodayHighlightedProperty = + AvaloniaProperty.Register(nameof(IsTodayHighlighted)); + + /// + /// Defines the property. + /// + public static readonly DirectProperty SelectedDateProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedDate), + o => o.SelectedDate, + (o, v) => o.SelectedDate = v, + enableDataValidation: true, + defaultBindingMode:BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SelectedDateFormatProperty = + AvaloniaProperty.Register( + nameof(SelectedDateFormat), + defaultValue: CalendarDatePickerFormat.Short, + validate: IsValidSelectedDateFormat); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CustomDateFormatStringProperty = + AvaloniaProperty.Register( + nameof(CustomDateFormatString), + defaultValue: "d", + validate: IsValidDateFormatString); + + /// + /// Defines the property. + /// + public static readonly DirectProperty TextProperty = + AvaloniaProperty.RegisterDirect( + nameof(Text), + o => o.Text, + (o, v) => o.Text = v); + + /// + /// Defines the property. + /// + public static readonly StyledProperty WatermarkProperty = + TextBox.WatermarkProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty UseFloatingWatermarkProperty = + TextBox.UseFloatingWatermarkProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalContentAlignmentProperty = + ContentControl.HorizontalContentAlignmentProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalContentAlignmentProperty = + ContentControl.VerticalContentAlignmentProperty.AddOwner(); + + /// + /// Gets a collection of dates that are marked as not selectable. + /// + /// + /// A collection of dates that cannot be selected. The default value is + /// an empty collection. + /// + public CalendarBlackoutDatesCollection? BlackoutDates { get; private set; } + + /// + /// Gets or sets the date to display. + /// + /// + /// The date to display. The default is . + /// + /// + /// The specified date is not in the range defined by + /// + /// and + /// . + /// + public DateTime DisplayDate + { + get => _displayDate; + set => SetAndRaise(DisplayDateProperty, ref _displayDate, value); + } + + /// + /// Gets or sets the first date to be displayed. + /// + /// The first date to display. + public DateTime? DisplayDateStart + { + get => _displayDateStart; + set => SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value); + } + + /// + /// Gets or sets the last date to be displayed. + /// + /// The last date to display. + public DateTime? DisplayDateEnd + { + get => _displayDateEnd; + set => SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value); + } + + /// + /// Gets or sets the day that is considered the beginning of the week. + /// + /// + /// A representing the beginning of + /// the week. The default is . + /// + public DayOfWeek FirstDayOfWeek + { + get => GetValue(FirstDayOfWeekProperty); + set => SetValue(FirstDayOfWeekProperty, value); + } + + /// + /// Gets or sets a value indicating whether the drop-down + /// is open or closed. + /// + /// + /// True if the is + /// open; otherwise, false. The default is false. + /// + public bool IsDropDownOpen + { + get => _isDropDownOpen; + set => SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); + } + + /// + /// Gets or sets a value indicating whether the current date will be + /// highlighted. + /// + /// + /// True if the current date is highlighted; otherwise, false. The + /// default is true. + /// + public bool IsTodayHighlighted + { + get => GetValue(IsTodayHighlightedProperty); + set => SetValue(IsTodayHighlightedProperty, value); + } + + /// + /// Gets or sets the currently selected date. + /// + /// + /// The date currently selected. The default is null. + /// + /// + /// The specified date is not in the range defined by + /// + /// and + /// , + /// or the specified date is in the + /// + /// collection. + /// + public DateTime? SelectedDate + { + get => _selectedDate; + set => SetAndRaise(SelectedDateProperty, ref _selectedDate, value); + } + + /// + /// Gets or sets the format that is used to display the selected date. + /// + /// + /// The format that is used to display the selected date. The default is + /// . + /// + /// + /// An specified format is not valid. + /// + public CalendarDatePickerFormat SelectedDateFormat + { + get => GetValue(SelectedDateFormatProperty); + set => SetValue(SelectedDateFormatProperty, value); + } + + public string CustomDateFormatString + { + get => GetValue(CustomDateFormatStringProperty); + set => SetValue(CustomDateFormatStringProperty, value); + } + + /// + /// Gets or sets the text that is displayed by the . + /// + /// + /// The text displayed by the . + /// + /// + /// The text entered cannot be parsed to a valid date, and the exception + /// is not suppressed. + /// + /// + /// The text entered parses to a date that is not selectable. + /// + public string? Text + { + get => _text; + set => SetAndRaise(TextProperty, ref _text, value); + } + + /// + public string? Watermark + { + get => GetValue(WatermarkProperty); + set => SetValue(WatermarkProperty, value); + } + + /// + public bool UseFloatingWatermark + { + get => GetValue(UseFloatingWatermarkProperty); + set => SetValue(UseFloatingWatermarkProperty, value); + } + + /// + /// Gets or sets the horizontal alignment of the content within the control. + /// + public HorizontalAlignment HorizontalContentAlignment + { + get => GetValue(HorizontalContentAlignmentProperty); + set => SetValue(HorizontalContentAlignmentProperty, value); + } + + /// + /// Gets or sets the vertical alignment of the content within the control. + /// + public VerticalAlignment VerticalContentAlignment + { + get => GetValue(VerticalContentAlignmentProperty); + set => SetValue(VerticalContentAlignmentProperty, value); + } + } +} diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs new file mode 100644 index 0000000000..3d592e9ab5 --- /dev/null +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -0,0 +1,927 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; +using System.Reactive.Disposables; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// A date selection control that allows the user to select dates from a drop down calendar. + /// + [TemplatePart(ElementButton, typeof(Button))] + [TemplatePart(ElementCalendar, typeof(Calendar))] + [TemplatePart(ElementPopup, typeof(Popup))] + [TemplatePart(ElementTextBox, typeof(TextBox))] + [PseudoClasses(pcFlyoutOpen, pcPressed)] + public partial class CalendarDatePicker : TemplatedControl + { + protected const string pcPressed = ":pressed"; + protected const string pcFlyoutOpen = ":flyout-open"; + + private const string ElementTextBox = "PART_TextBox"; + private const string ElementButton = "PART_Button"; + private const string ElementPopup = "PART_Popup"; + private const string ElementCalendar = "PART_Calendar"; + + private Calendar? _calendar; + private string _defaultText; + private Button? _dropDownButton; + private Popup? _popUp; + private TextBox? _textBox; + private IDisposable? _textBoxTextChangedSubscription; + private IDisposable? _buttonPointerPressedSubscription; + + private DateTime? _onOpenSelectedDate; + private bool _settingSelectedDate; + + private DateTime _displayDate; + private DateTime? _displayDateStart; + private DateTime? _displayDateEnd; + private bool _isDropDownOpen; + private DateTime? _selectedDate; + private string? _text; + private bool _suspendTextChangeHandler = false; + private bool _isPopupClosing = false; + private bool _ignoreButtonClick = false; + private bool _isFlyoutOpen = false; + private bool _isPressed = false; + + /// + /// Occurs when the drop-down + /// is closed. + /// + public event EventHandler? CalendarClosed; + + /// + /// Occurs when the drop-down + /// is opened. + /// + public event EventHandler? CalendarOpened; + + /// + /// Occurs when + /// is assigned a value that cannot be interpreted as a date. + /// + public event EventHandler? DateValidationError; + + /// + /// Occurs when the + /// + /// property is changed. + /// + public event EventHandler? SelectedDateChanged; + + static CalendarDatePicker() + { + FocusableProperty.OverrideDefaultValue(true); + } + + /// + /// Initializes a new instance of the class. + /// + public CalendarDatePicker() + { + FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek; + _defaultText = string.Empty; + DisplayDate = DateTime.Today; + } + + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// + protected void UpdatePseudoClasses() + { + PseudoClasses.Set(pcFlyoutOpen, _isFlyoutOpen); + PseudoClasses.Set(pcPressed, _isPressed); + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + if (_calendar != null) + { + _calendar.DayButtonMouseUp -= Calendar_DayButtonMouseUp; + _calendar.DisplayDateChanged -= Calendar_DisplayDateChanged; + _calendar.SelectedDatesChanged -= Calendar_SelectedDatesChanged; + _calendar.PointerReleased -= Calendar_PointerReleased; + _calendar.KeyDown -= Calendar_KeyDown; + } + _calendar = e.NameScope.Find(ElementCalendar); + if (_calendar != null) + { + _calendar.SelectionMode = CalendarSelectionMode.SingleDate; + + _calendar.DayButtonMouseUp += Calendar_DayButtonMouseUp; + _calendar.DisplayDateChanged += Calendar_DisplayDateChanged; + _calendar.SelectedDatesChanged += Calendar_SelectedDatesChanged; + _calendar.PointerReleased += Calendar_PointerReleased; + _calendar.KeyDown += Calendar_KeyDown; + + var currentBlackoutDays = BlackoutDates; + BlackoutDates = _calendar.BlackoutDates; + if(currentBlackoutDays != null) + { + foreach (var range in currentBlackoutDays) + { + BlackoutDates.Add(range); + } + } + } + + if (_popUp != null) + { + _popUp.Child = null; + _popUp.Closed -= PopUp_Closed; + } + _popUp = e.NameScope.Find(ElementPopup); + if(_popUp != null) + { + _popUp.Closed += PopUp_Closed; + if (IsDropDownOpen) + { + OpenDropDown(); + } + } + + if(_dropDownButton != null) + { + _dropDownButton.Click -= DropDownButton_Click; + _buttonPointerPressedSubscription?.Dispose(); + } + _dropDownButton = e.NameScope.Find 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..cbf9b35a05 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Avalonia.Automation.Peers; using System.Reactive.Disposables; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; @@ -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 { /// @@ -181,6 +184,25 @@ namespace Avalonia.Controls this.UpdateSelectionBoxItem(SelectedItem); } + // Because the SelectedItem isn't connected to the visual tree + public override void InvalidateMirrorTransform() + { + base.InvalidateMirrorTransform(); + + if (SelectedItem is Control selectedControl) + { + selectedControl.InvalidateMirrorTransform(); + + foreach (var visual in selectedControl.GetVisualDescendants()) + { + if (visual is Control childControl) + { + childControl.InvalidateMirrorTransform(); + } + } + } + } + /// protected override void OnKeyDown(KeyEventArgs e) { @@ -295,6 +317,11 @@ namespace Avalonia.Controls _popup.Closed += PopupClosed; } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ComboBoxAutomationPeer(this); + } + internal void ItemFocused(ComboBoxItem dropDownItem) { if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) @@ -426,42 +453,18 @@ namespace Avalonia.Controls private void SelectNext() { - int next = SelectedIndex + 1; - - if (next >= ItemCount) + if (ItemCount >= 1) { - if (WrapSelection == true) - { - next = 0; - } - else - { - return; - } + MoveSelection(NavigationDirection.Next, WrapSelection); } - - - - SelectedIndex = next; } private void SelectPrev() { - int prev = SelectedIndex - 1; - - if (prev < 0) + if (ItemCount >= 1) { - if (WrapSelection == true) - { - prev = ItemCount - 1; - } - else - { - return; - } + MoveSelection(NavigationDirection.Previous, WrapSelection); } - - SelectedIndex = prev; } } } 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..2b122d4174 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 { @@ -61,7 +63,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty PlacementRectProperty = - AvaloniaProperty.Register(nameof(PlacementRect)); + Popup.PlacementRectProperty.AddOwner(); /// /// Defines the property. @@ -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); } /// @@ -237,13 +241,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == WindowManagerAddShadowHintProperty && _popup != null) { - _popup.WindowManagerAddShadowHint = change.NewValue.GetValueOrDefault(); + _popup.WindowManagerAddShadowHint = change.GetNewValue(); } } @@ -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..d6a5fa0727 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; -using System.Runtime.CompilerServices; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Documents; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; @@ -66,9 +67,10 @@ namespace Avalonia.Controls /// public static readonly AttachedProperty FlowDirectionProperty = AvaloniaProperty.RegisterAttached(nameof(FlowDirection), inherits: true); - + private DataTemplates? _dataTemplates; private IControl? _focusAdorner; + private AutomationPeer? _automationPeer; /// /// Gets or sets the control's focus adorner. @@ -135,9 +137,39 @@ namespace Avalonia.Controls public new IControl? Parent => (IControl?)base.Parent; + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The flow direction. + public static FlowDirection GetFlowDirection(Control control) + { + return control.GetValue(FlowDirectionProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFlowDirection(Control control, FlowDirection value) + { + control.SetValue(FlowDirectionProperty, value); + } + /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; + /// + /// Gets a value indicating whether control bypass FlowDirecton policies. + /// + /// + /// Related to FlowDirection system and returns false as default, so if + /// is RTL then control will get a mirror presentation. + /// For controls that want to avoid this behavior, override this property and return true. + /// + protected virtual bool BypassFlowDirectionPolicies => false; + /// void ISetterValue.Initialize(ISetter setter) { @@ -197,6 +229,14 @@ namespace Avalonia.Controls base.OnDetachedFromVisualTreeCore(e); } + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + InvalidateMirrorTransform(); + } + /// protected override void OnGotFocus(GotFocusEventArgs e) { @@ -242,6 +282,24 @@ namespace Avalonia.Controls } } + protected virtual AutomationPeer OnCreateAutomationPeer() + { + return new NoneAutomationPeer(this); + } + + internal AutomationPeer GetOrCreateAutomationPeer() + { + VerifyAccess(); + + if (_automationPeer is object) + { + return _automationPeer; + } + + _automationPeer = OnCreateAutomationPeer(); + return _automationPeer; + } + protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); @@ -289,5 +347,55 @@ namespace Avalonia.Controls } } } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == FlowDirectionProperty) + { + InvalidateMirrorTransform(); + + foreach (var visual in VisualChildren) + { + if (visual is Control child) + { + child.InvalidateMirrorTransform(); + } + } + } + } + + /// + /// Computes the value according to the + /// and + /// + public virtual void InvalidateMirrorTransform() + { + var flowDirection = this.FlowDirection; + var parentFlowDirection = FlowDirection.LeftToRight; + + bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies; + bool parentBypassFlowDirectionPolicies = false; + + var parent = this.FindAncestorOfType(); + if (parent != null) + { + parentFlowDirection = parent.FlowDirection; + parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies; + } + else if (Parent is Control logicalParent) + { + parentFlowDirection = logicalParent.FlowDirection; + parentBypassFlowDirectionPolicies = logicalParent.BypassFlowDirectionPolicies; + } + + bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies; + bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies; + + bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored; + + HasMirrorTransform = shouldApplyMirrorTransform; + } } } diff --git a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs index 643c30178e..a91f143019 100644 --- a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs +++ b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs @@ -1,27 +1,32 @@ using System; using System.Globalization; - using Avalonia.Data.Converters; namespace Avalonia.Controls.Converters { /// /// Converts an existing CornerRadius struct to a new CornerRadius struct, - /// with filters applied to extract only the specified fields, leaving the others set to 0. + /// with filters applied to extract only the specified corners, leaving the others set to 0. /// public class CornerRadiusFilterConverter : IValueConverter { /// - /// Gets or sets the type of the filter applied to the . + /// Gets or sets the corners to filter by. + /// Only the specified corners will be included in the converted . /// - public CornerRadiusFilterKinds Filter { get; set; } + public Corners Filter { get; set; } /// - /// Gets or sets the scale multiplier applied to the . + /// Gets or sets the scale multiplier applied uniformly to each corner. /// public double Scale { get; set; } = 1; - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { if (!(value is CornerRadius radius)) { @@ -29,13 +34,18 @@ namespace Avalonia.Controls.Converters } return new CornerRadius( - Filter.HasAllFlags(CornerRadiusFilterKinds.TopLeft) ? radius.TopLeft * Scale : 0, - Filter.HasAllFlags(CornerRadiusFilterKinds.TopRight) ? radius.TopRight * Scale : 0, - Filter.HasAllFlags(CornerRadiusFilterKinds.BottomRight) ? radius.BottomRight * Scale : 0, - Filter.HasAllFlags(CornerRadiusFilterKinds.BottomLeft) ? radius.BottomLeft * Scale : 0); + Filter.HasAllFlags(Corners.TopLeft) ? radius.TopLeft * Scale : 0, + Filter.HasAllFlags(Corners.TopRight) ? radius.TopRight * Scale : 0, + Filter.HasAllFlags(Corners.BottomRight) ? radius.BottomRight * Scale : 0, + Filter.HasAllFlags(Corners.BottomLeft) ? radius.BottomLeft * Scale : 0); } - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs b/src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs deleted file mode 100644 index 6a9d0596be..0000000000 --- a/src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; - -namespace Avalonia.Controls.Converters -{ - /// - /// Defines constants that specify the filter type for a instance. - /// - [Flags] - public enum CornerRadiusFilterKinds - { - /// - /// No filter applied. - /// - None, - /// - /// Filters TopLeft value. - /// - TopLeft = 1, - /// - /// Filters TopRight value. - /// - TopRight = 2, - /// - /// Filters BottomLeft value. - /// - BottomLeft = 4, - /// - /// Filters BottomRight value. - /// - BottomRight = 8 - } -} diff --git a/src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs b/src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs new file mode 100644 index 0000000000..6da15b61e6 --- /dev/null +++ b/src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converts one corner of a to its double value. + /// + public class CornerRadiusToDoubleConverter : IValueConverter + { + /// + /// Gets or sets the specific corner of the to convert to double. + /// + public Corners Corner { get; set; } + + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (!(value is CornerRadius cornerRadius)) + { + return AvaloniaProperty.UnsetValue; + } + + switch (Corner) + { + case Corners.TopLeft: + return cornerRadius.TopLeft; + case Corners.TopRight: + return cornerRadius.TopRight; + case Corners.BottomRight: + return cornerRadius.BottomRight; + case Corners.BottomLeft: + return cornerRadius.BottomLeft; + default: + return 0.0; + } + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Controls/Converters/Corners.cs b/src/Avalonia.Controls/Converters/Corners.cs new file mode 100644 index 0000000000..22ac4cb5f9 --- /dev/null +++ b/src/Avalonia.Controls/Converters/Corners.cs @@ -0,0 +1,36 @@ +using System; + +namespace Avalonia.Controls.Converters +{ + /// + /// Defines constants that specify one or more corners of a . + /// + [Flags] + public enum Corners + { + /// + /// No corner. + /// + None, + + /// + /// The TopLeft corner. + /// + TopLeft = 1, + + /// + /// The TopRight corner. + /// + TopRight = 2, + + /// + /// The BottomLeft corner. + /// + BottomLeft = 4, + + /// + /// The BottomRight corner. + /// + BottomRight = 8 + } +} diff --git a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs new file mode 100644 index 0000000000..1a33a82ca4 --- /dev/null +++ b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs @@ -0,0 +1,54 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converter that checks if an enum value is equal to the given parameter enum value. + /// + public class EnumValueEqualsConverter : IValueConverter + { + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + // Note: Unlike string comparisons, null/empty is not supported + // Both 'value' and 'parameter' must exist and if both are missing they are not considered equal + if (value != null && + parameter != null) + { + Type type = value.GetType(); + + if (type.IsEnum) + { + var valueStr = value?.ToString(); + var paramStr = parameter?.ToString(); + + if (string.Equals(valueStr, paramStr, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + /* + // TODO: When .net Standard 2.0 is no longer supported the code can be changed to below + // This is a little more type safe + if (type.IsEnum && + Enum.TryParse(type, value?.ToString(), true, out object? valueEnum) && + Enum.TryParse(type, parameter?.ToString(), true, out object? paramEnum)) + { + return valueEnum == paramEnum; + } + */ + } + + return false; + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs index b0c30ea11f..7931b63d8e 100644 --- a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs +++ b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs @@ -35,7 +35,6 @@ namespace Avalonia.Controls.Converters Bottom ? Indent * thicknessDepth.Bottom : 0); } return new Thickness(0); - } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 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 5948d81810..667f994a1d 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs @@ -1,7 +1,11 @@ using System; using System.Globalization; using System.Linq; +using Avalonia.Controls.Presenters; using Avalonia.Input; +using Avalonia.Input.GestureRecognizers; +using Avalonia.Interactivity; +using Avalonia.Media; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -58,18 +62,18 @@ namespace Avalonia.Controls.Primitives private Vector _offset; private bool _hasInit; private bool _suppressUpdateOffset; - private ListBoxItem? _pressedItem; + private ScrollContentPresenter? _parentScroller; public DateTimePickerPanel() { FormatDate = DateTime.Now; - AddHandler(ListBoxItem.PointerPressedEvent, OnItemPointerDown, Avalonia.Interactivity.RoutingStrategies.Bubble); - AddHandler(ListBoxItem.PointerReleasedEvent, OnItemPointerUp, Avalonia.Interactivity.RoutingStrategies.Bubble); + AddHandler(TappedEvent, OnItemTapped, RoutingStrategies.Bubble); } static DateTimePickerPanel() { FocusableProperty.OverrideDefaultValue(true); + BackgroundProperty.OverrideDefaultValue(Brushes.Transparent); AffectsMeasure(ItemHeightProperty); } @@ -254,6 +258,8 @@ namespace Avalonia.Controls.Primitives _suppressUpdateOffset = true; SelectedValue = (int)newSel * Increment + MinimumValue; _suppressUpdateOffset = false; + + System.Diagnostics.Debug.WriteLine($"Offset: {_offset} ItemHeight: {ItemHeight}"); } } @@ -269,7 +275,7 @@ namespace Avalonia.Controls.Primitives public Size Extent => _extent; - public Size Viewport => new Size(0, ItemHeight); + public Size Viewport => Bounds.Size; public event EventHandler? ScrollInvalidated; @@ -340,6 +346,20 @@ namespace Avalonia.Controls.Primitives return finalSize; } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _parentScroller = this.GetVisualParent() as ScrollContentPresenter; + _parentScroller?.AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _parentScroller?.RemoveHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + _parentScroller = null; + } + protected override void OnKeyDown(KeyEventArgs e) { switch (e.Key) @@ -523,26 +543,13 @@ namespace Avalonia.Controls.Primitives return newValue; } - private void OnItemPointerDown(object? sender, PointerPressedEventArgs e) + private void OnItemTapped(object? sender, TappedEventArgs e) { - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && - e.Source is IVisual source) - { - _pressedItem = GetItemFromSource(source); - e.Handled = true; - } - } - - private void OnItemPointerUp(object? sender, PointerReleasedEventArgs e) - { - if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonReleased && - _pressedItem != null && - e.Source is IVisual source && - GetItemFromSource(source) is ListBoxItem item && - item.Tag is int tag) + if (e.Source is IVisual source && + GetItemFromSource(source) is ListBoxItem listBoxItem && + listBoxItem.Tag is int tag) { SelectedValue = tag; - _pressedItem = null; e.Handled = true; } } @@ -566,5 +573,15 @@ namespace Avalonia.Controls.Primitives { ScrollInvalidated?.Invoke(this, e); } + + private void OnScrollGestureEnded(object? sender, ScrollGestureEndedEventArgs e) + { + var snapY = Math.Round(Offset.Y / ItemHeight) * ItemHeight; + + if (snapY != Offset.Y) + { + Offset = Offset.WithY(snapY); + } + } } } diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index a0a27bd4ed..047667567d 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 { @@ -25,13 +38,13 @@ namespace Avalonia.Controls /// Defines the property /// public static readonly StyledProperty HeaderProperty = - AvaloniaProperty.Register(nameof(Header)); + AvaloniaProperty.Register(nameof(Header)); /// /// Defines the property /// public static readonly StyledProperty HeaderTemplateProperty = - AvaloniaProperty.Register(nameof(HeaderTemplate)); + AvaloniaProperty.Register(nameof(HeaderTemplate)); /// /// Defines the property @@ -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/Diagnostics/IPopupHostProvider.cs b/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs index 45cd1d727e..64978248e5 100644 --- a/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs +++ b/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs @@ -1,11 +1,13 @@ using System; using Avalonia.Controls.Primitives; +using Avalonia.Metadata; namespace Avalonia.Controls.Diagnostics { /// /// Diagnostics interface to retrieve an associated . /// + [NotClientImplementable] public interface IPopupHostProvider { /// diff --git a/src/Avalonia.Controls/DockPanel.cs b/src/Avalonia.Controls/DockPanel.cs index 8e23555c2d..3e3ed509b5 100644 --- a/src/Avalonia.Controls/DockPanel.cs +++ b/src/Avalonia.Controls/DockPanel.cs @@ -34,7 +34,7 @@ namespace Avalonia.Controls /// public static readonly StyledProperty LastChildFillProperty = AvaloniaProperty.Register( - nameof(LastChildFillProperty), + nameof(LastChildFill), defaultValue: true); /// 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/IInlineHost.cs b/src/Avalonia.Controls/Documents/IInlineHost.cs new file mode 100644 index 0000000000..da72c207be --- /dev/null +++ b/src/Avalonia.Controls/Documents/IInlineHost.cs @@ -0,0 +1,11 @@ +using Avalonia.LogicalTree; + +namespace Avalonia.Controls.Documents +{ + internal interface IInlineHost : ILogical + { + void AddVisualChild(IControl child); + + void Invalidate(); + } +} diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs new file mode 100644 index 0000000000..b400625903 --- /dev/null +++ b/src/Avalonia.Controls/Documents/Inline.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Text; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; + +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 void BuildTextRun(IList textRuns); + + internal abstract void 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): + InlineHost?.Invalidate(); + break; + } + } + } +} diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs new file mode 100644 index 0000000000..a76222385e --- /dev/null +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -0,0 +1,149 @@ +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 readonly IInlineHost? _host; + private string? _text = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + public InlineCollection(ILogical parent) : this(parent, null) { } + + /// + /// Initializes a new instance of the class. + /// + internal InlineCollection(ILogical parent, IInlineHost? host = null) : base(0) + { + _host = host; + + ResetBehavior = ResetBehavior.Remove; + + this.ForEachItem( + x => + { + ((ISetLogicalParent)x).SetParent(parent); + x.InlineHost = host; + host?.Invalidate(); + }, + x => + { + ((ISetLogicalParent)x).SetParent(null); + x.InlineHost = host; + host?.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 void Add(IControl child) + { + var implicitRun = new InlineUIContainer(child); + + Add(implicitRun); + } + + public override void Add(Inline item) + { + if (!HasComplexContent) + { + if (!string.IsNullOrEmpty(_text)) + { + 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() + { + if(_host != null) + { + _host.Invalidate(); + } + + Invalidated?.Invoke(this, EventArgs.Empty); + } + + private void Invalidate(object? sender, EventArgs e) => Invalidate(); + } +} diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs new file mode 100644 index 0000000000..5f08c23099 --- /dev/null +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Text; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Metadata; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Documents +{ + /// + /// InlineUIContainer - a wrapper for embedded UIElements in text + /// flow content inline collections + /// + public class InlineUIContainer : Inline + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ChildProperty = + AvaloniaProperty.Register(nameof(Child)); + + static InlineUIContainer() + { + BaselineAlignmentProperty.OverrideDefaultValue(BaselineAlignment.Top); + } + + /// + /// Initializes a new instance of InlineUIContainer element. + /// + /// + /// The purpose of this element is to be a wrapper for UIElements + /// when they are embedded into text flow - as items of + /// InlineCollections. + /// + public InlineUIContainer() + { + } + + /// + /// Initializes an InlineBox specifying its child UIElement + /// + /// + /// UIElement set as a child of this inline item + /// + public InlineUIContainer(IControl child) + { + Child = child; + } + + /// + /// The content spanned by this TextElement. + /// + [Content] + public IControl Child + { + get => GetValue(ChildProperty); + set => SetValue(ChildProperty, value); + } + + internal override void BuildTextRun(IList textRuns) + { + if(InlineHost == null) + { + return; + } + + ((ISetLogicalParent)Child).SetParent(InlineHost); + + InlineHost.AddVisualChild(Child); + + textRuns.Add(new InlineRun(Child, CreateTextRunProperties())); + } + + internal override void AppendText(StringBuilder stringBuilder) + { + } + + private class InlineRun : DrawableTextRun + { + public InlineRun(IControl control, TextRunProperties properties) + { + Control = control; + Properties = properties; + } + + public IControl Control { get; } + + public override TextRunProperties? Properties { get; } + + public override Size Size + { + get + { + if (!Control.IsMeasureValid) + { + Control.Measure(Size.Infinity); + } + + return Control.DesiredSize; + } + } + + public override double Baseline + { + get + { + double baseline = Size.Height; + double baselineOffsetValue = Control.GetValue(TextBlock.BaselineOffsetProperty); + + if (!MathUtilities.IsZero(baselineOffsetValue)) + { + baseline = baselineOffsetValue; + } + + return -baseline; + } + } + + public override void Draw(DrawingContext drawingContext, Point origin) + { + Control.Arrange(new Rect(origin, Size)); + } + } + } +} 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..aeb81f7313 --- /dev/null +++ b/src/Avalonia.Controls/Documents/LineBreak.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.LogicalTree; +using Avalonia.Media.TextFormatting; +using Avalonia.Metadata; + +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 void BuildTextRun(IList textRuns) + { + textRuns.Add(new TextEndOfLine()); + } + + internal override void AppendText(StringBuilder stringBuilder) + { + stringBuilder.Append(Environment.NewLine); + } + } +} + diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs new file mode 100644 index 0000000000..2bd66b8a64 --- /dev/null +++ b/src/Avalonia.Controls/Documents/Run.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Data; +using Avalonia.Media.TextFormatting; +using Avalonia.Metadata; + +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 void BuildTextRun(IList textRuns) + { + var text = (Text ?? "").AsMemory(); + + var textRunProperties = CreateTextRunProperties(); + + var textCharacters = new TextCharacters(text, textRunProperties); + + textRuns.Add(textCharacters); + } + + internal override void AppendText(StringBuilder stringBuilder) + { + var text = Text ?? ""; + + stringBuilder.Append(text); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + switch (change.Property.Name) + { + case nameof(Text): + InlineHost?.Invalidate(); + break; + } + } + } +} diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs new file mode 100644 index 0000000000..bd1b4fc5e1 --- /dev/null +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Media.TextFormatting; +using Avalonia.Metadata; + +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) => InlineHost?.Invalidate(); + } + + /// + /// Gets or sets the inlines. + /// + [Content] + public InlineCollection Inlines { get; } + + internal override void BuildTextRun(IList textRuns) + { + if (Inlines.HasComplexContent) + { + foreach (var inline in Inlines) + { + inline.BuildTextRun(textRuns); + } + } + else + { + if (Inlines.Text is string text) + { + var textRunProperties = CreateTextRunProperties(); + + var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties); + + textRuns.Add(textCharacters); + } + } + } + + internal override void AppendText(StringBuilder stringBuilder) + { + if (Inlines.HasComplexContent) + { + foreach (var inline in Inlines) + { + inline.AppendText(stringBuilder); + } + } + + if (Inlines.Text is string text) + { + stringBuilder.Append(text); + } + } + } +} diff --git a/src/Avalonia.Controls/Documents/TextElement.cs b/src/Avalonia.Controls/Documents/TextElement.cs new file mode 100644 index 0000000000..f228519e60 --- /dev/null +++ b/src/Avalonia.Controls/Documents/TextElement.cs @@ -0,0 +1,273 @@ +using Avalonia.Media; + +namespace Avalonia.Controls.Documents +{ + /// + /// TextElement is an base class for content in text based controls. + /// TextElements span other content, applying property values or providing structural information. + /// + public abstract class TextElement : StyledElement + { + /// + /// Defines the property. + /// + public static readonly StyledProperty BackgroundProperty = + Border.BackgroundProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontFamilyProperty = + AvaloniaProperty.RegisterAttached( + nameof(FontFamily), + defaultValue: FontFamily.Default, + inherits: true); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontSizeProperty = + AvaloniaProperty.RegisterAttached( + nameof(FontSize), + defaultValue: 12, + inherits: true); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontStyleProperty = + AvaloniaProperty.RegisterAttached( + nameof(FontStyle), + inherits: true); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontWeightProperty = + AvaloniaProperty.RegisterAttached( + nameof(FontWeight), + inherits: true, + defaultValue: FontWeight.Normal); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontStretchProperty = + AvaloniaProperty.RegisterAttached( + nameof(FontStretch), + inherits: true, + defaultValue: FontStretch.Normal); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty ForegroundProperty = + AvaloniaProperty.RegisterAttached( + nameof(Foreground), + Brushes.Black, + inherits: true); + + /// + /// Gets or sets a brush used to paint the control's background. + /// + public IBrush? Background + { + get { return GetValue(BackgroundProperty); } + set { SetValue(BackgroundProperty, value); } + } + + /// + /// Gets or sets the font family. + /// + public FontFamily FontFamily + { + get { return GetValue(FontFamilyProperty); } + set { SetValue(FontFamilyProperty, value); } + } + + /// + /// Gets or sets the font size. + /// + public double FontSize + { + get { return GetValue(FontSizeProperty); } + set { SetValue(FontSizeProperty, value); } + } + + /// + /// Gets or sets the font style. + /// + public FontStyle FontStyle + { + get { return GetValue(FontStyleProperty); } + set { SetValue(FontStyleProperty, value); } + } + + /// + /// Gets or sets the font weight. + /// + public FontWeight FontWeight + { + get { return GetValue(FontWeightProperty); } + set { SetValue(FontWeightProperty, value); } + } + + /// + /// Gets or sets the font stretch. + /// + public FontStretch FontStretch + { + get { return GetValue(FontStretchProperty); } + set { SetValue(FontStretchProperty, value); } + } + + /// + /// Gets or sets a brush used to paint the text. + /// + public IBrush? Foreground + { + get { return GetValue(ForegroundProperty); } + set { SetValue(ForegroundProperty, value); } + } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font family. + public static FontFamily GetFontFamily(Control control) + { + return control.GetValue(FontFamilyProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFontFamily(Control control, FontFamily value) + { + control.SetValue(FontFamilyProperty, value); + } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font size. + public static double GetFontSize(Control control) + { + return control.GetValue(FontSizeProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFontSize(Control control, double value) + { + control.SetValue(FontSizeProperty, value); + } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font style. + public static FontStyle GetFontStyle(Control control) + { + return control.GetValue(FontStyleProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFontStyle(Control control, FontStyle value) + { + control.SetValue(FontStyleProperty, value); + } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font weight. + public static FontWeight GetFontWeight(Control control) + { + return control.GetValue(FontWeightProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFontWeight(Control control, FontWeight value) + { + control.SetValue(FontWeightProperty, value); + } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font stretch. + public static FontStretch GetFontStretch(Control control) + { + return control.GetValue(FontStretchProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFontStretch(Control control, FontStretch value) + { + control.SetValue(FontStretchProperty, value); + } + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The foreground. + public static IBrush? GetForeground(Control control) + { + return control.GetValue(ForegroundProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetForeground(Control control, IBrush? value) + { + control.SetValue(ForegroundProperty, value); + } + + internal IInlineHost? InlineHost { get; set; } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + switch (change.Property.Name) + { + case nameof(Background): + case nameof(FontFamily): + case nameof(FontSize): + case nameof(FontStyle): + case nameof(FontWeight): + case nameof(FontStretch): + case nameof(Foreground): + InlineHost?.Invalidate(); + break; + } + } + } +} diff --git a/src/Avalonia.Controls/Documents/Underline.cs b/src/Avalonia.Controls/Documents/Underline.cs new file mode 100644 index 0000000000..fcd46c8439 --- /dev/null +++ b/src/Avalonia.Controls/Documents/Underline.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Controls.Documents +{ + /// + /// Underline element - markup helper for indicating superscript content. + /// Equivalent to a Span with TextDecorations property set to TextDecorations.Underlined. + /// Can contain other inline elements. + /// + public sealed class Underline : Span + { + static Underline() + { + TextDecorationsProperty.OverrideDefaultValue(Media.TextDecorations.Underline); + } + } +} diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs deleted file mode 100644 index 4e17f5bff5..0000000000 --- a/src/Avalonia.Controls/DropDown.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Avalonia.Logging; -using Avalonia.Styling; - -namespace Avalonia.Controls -{ - [Obsolete("Use ComboBox")] - public class DropDown : ComboBox, IStyleable - { - public DropDown() - { - Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "DropDown is deprecated: Use ComboBox"); - } - - Type IStyleable.StyleKey => typeof(ComboBox); - } - - [Obsolete("Use ComboBoxItem")] - public class DropDownItem : ComboBoxItem, IStyleable - { - public DropDownItem() - { - Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "DropDownItem is deprecated: Use ComboBoxItem"); - } - - Type IStyleable.StyleKey => typeof(ComboBoxItem); - } -} diff --git a/src/Avalonia.Controls/DropDownButton.cs b/src/Avalonia.Controls/DropDownButton.cs new file mode 100644 index 0000000000..8dc40f42af --- /dev/null +++ b/src/Avalonia.Controls/DropDownButton.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Controls +{ + /// + /// A button with an added drop-down chevron to visually indicate it has a flyout with additional actions. + /// + public class DropDownButton : Button + { + /// + /// Initializes a new instance of the class. + /// + public DropDownButton() + { + } + } +} diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 08d559a5c1..1c7bdb9b37 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -2,11 +2,13 @@ using System.Collections.Generic; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Rendering; namespace Avalonia.Controls.Embedding.Offscreen { + [Unstable] public abstract class OffscreenTopLevelImplBase : ITopLevelImpl { private double _scaling = 1; diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 020b162864..3ba99d8a67 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -106,13 +106,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ExpandDirectionProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } } 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/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 79a9e16879..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)); } diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs index da13416700..eb60fca367 100644 --- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs +++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs @@ -15,6 +15,7 @@ namespace Avalonia.Controls.Generators /// public class TreeContainerIndex { + private readonly Dictionary> _itemToContainerSet = new Dictionary>(); private readonly Dictionary _itemToContainer = new Dictionary(); private readonly Dictionary _containerToItem = new Dictionary(); @@ -45,14 +46,45 @@ namespace Avalonia.Controls.Generators /// The item container. public void Add(object item, IControl container) { - _itemToContainer.Add(item, container); + _itemToContainer[item] = container; + if (_itemToContainerSet.TryGetValue(item, out var set)) + { + set.Add(container); + } + else + { + _itemToContainerSet.Add(item, new HashSet { container }); + } + _containerToItem.Add(container, item); Materialized?.Invoke( - this, + this, new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } + /// + /// Removes a container from private collections. + /// + /// The item container. + /// The DataContext object + private void RemoveContainer(IControl container, object item) + { + if (_itemToContainerSet.TryGetValue(item, out var set)) + { + set.Remove(container); + if (set.Count == 0) + { + _itemToContainerSet.Remove(item); + _itemToContainer.Remove(item); + } + else + { + _itemToContainer[item] = set.First(); + } + } + } + /// /// Removes a container from the index. /// @@ -61,10 +93,10 @@ namespace Avalonia.Controls.Generators { var item = _containerToItem[container]; _containerToItem.Remove(container); - _itemToContainer.Remove(item); + RemoveContainer(container, item); Dematerialized?.Invoke( - this, + this, new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } @@ -79,7 +111,7 @@ namespace Avalonia.Controls.Generators { var item = _containerToItem[container.ContainerControl]; _containerToItem.Remove(container.ContainerControl); - _itemToContainer.Remove(item); + RemoveContainer(container.ContainerControl, item); } Dematerialized?.Invoke( @@ -97,6 +129,14 @@ namespace Avalonia.Controls.Generators if (item != null) { _itemToContainer.TryGetValue(item, out var result); + if (result == null) + { + _itemToContainerSet.TryGetValue(item, out var set); + if (set?.Count > 0) + { + return set.FirstOrDefault(); + } + } return result; } @@ -113,6 +153,10 @@ namespace Avalonia.Controls.Generators if (container != null) { _containerToItem.TryGetValue(container, out var result); + if (result != null) + { + _itemToContainer[result] = container; + } return result; } diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index d83b996aa0..bde1da509b 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -12,21 +12,61 @@ namespace Avalonia.Controls class HotkeyCommandWrapper : ICommand { - public HotkeyCommandWrapper(ICommandSource? control) + readonly WeakReference reference; + + public HotkeyCommandWrapper(IControl control) { - CommandSource = control; + reference = new WeakReference(control); } - public readonly ICommandSource? CommandSource; + public ICommand? GetCommand() + { + if (reference.Target is { } target) + { + if (target is ICommandSource commandSource && commandSource.Command is { } command) + { + return command; + } + else if (target is IClickableControl { }) + { + return this; + } + } + return null; + } - private ICommand? GetCommand() => CommandSource?.Command; + public bool CanExecute(object? parameter) + { + if (reference.Target is { } target) + { + if (target is ICommandSource commandSource && commandSource.Command is { } command) + { + return commandSource.IsEffectivelyEnabled + && command.CanExecute(commandSource.CommandParameter) == true; + } + else if (target is IClickableControl clickable) + { + return clickable.IsEffectivelyEnabled; + } + } + return false; + } - public bool CanExecute(object? parameter) => - CommandSource?.Command?.CanExecute(CommandSource.CommandParameter) == true - && CommandSource.IsEffectivelyEnabled; + public void Execute(object? parameter) + { + if (reference.Target is { } target) + { + if (target is ICommandSource commandSource && commandSource.Command is { } command) + { + command.Execute(commandSource.CommandParameter); + } + else if (target is IClickableControl { IsEffectivelyEnabled: true } clickable) + { + clickable.RaiseClick(); + } + } + } - public void Execute(object? parameter) => - GetCommand()?.Execute(CommandSource?.CommandParameter); #pragma warning disable 67 // Event not used public event EventHandler? CanExecuteChanged; @@ -47,7 +87,7 @@ namespace Avalonia.Controls public Manager(IControl control) { _control = control; - _wrapper = new HotkeyCommandWrapper(_control as ICommandSource); + _wrapper = new HotkeyCommandWrapper(_control); } public void Init() @@ -104,13 +144,14 @@ namespace Avalonia.Controls { HotKeyProperty.Changed.Subscribe(args => { - if (args.NewValue.Value is null) return; + if (args.NewValue.Value is null) + return; var control = args.Sender as IControl; - if (control is not ICommandSource) + if (control is not IClickableControl) { Logging.Logger.TryGet(Logging.LogEventLevel.Warning, Logging.LogArea.Control)?.Log(control, - $"The element {args.Sender.GetType().Name} does not implement ICommandSource and does not support binding a HotKey ({args.NewValue})."); + $"The element {args.Sender.GetType().Name} does not implement IClickableControl and does not support binding a HotKey ({args.NewValue})."); return; } diff --git a/src/Avalonia.Controls/IContentControl.cs b/src/Avalonia.Controls/IContentControl.cs index d28b0afb25..b4d8d0f574 100644 --- a/src/Avalonia.Controls/IContentControl.cs +++ b/src/Avalonia.Controls/IContentControl.cs @@ -1,5 +1,6 @@ using Avalonia.Controls.Templates; using Avalonia.Layout; +using Avalonia.Metadata; namespace Avalonia.Controls { @@ -7,6 +8,7 @@ namespace Avalonia.Controls /// Defines a control that displays according to a /// . /// + [NotClientImplementable] public interface IContentControl : IControl { /// diff --git a/src/Avalonia.Controls/IControl.cs b/src/Avalonia.Controls/IControl.cs index b501bc15a7..3395fc1059 100644 --- a/src/Avalonia.Controls/IControl.cs +++ b/src/Avalonia.Controls/IControl.cs @@ -1,6 +1,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -8,6 +9,7 @@ namespace Avalonia.Controls /// /// Interface for Avalonia controls. /// + [NotClientImplementable] public interface IControl : IVisual, IDataTemplateHost, ILayoutable, diff --git a/src/Avalonia.Controls/IGlobalDataTemplates.cs b/src/Avalonia.Controls/IGlobalDataTemplates.cs index 92dcd2c189..f4499ddb5e 100644 --- a/src/Avalonia.Controls/IGlobalDataTemplates.cs +++ b/src/Avalonia.Controls/IGlobalDataTemplates.cs @@ -1,10 +1,12 @@ using Avalonia.Controls.Templates; +using Avalonia.Metadata; namespace Avalonia.Controls { /// /// Defines the application-global data templates. /// + [NotClientImplementable] public interface IGlobalDataTemplates : IDataTemplateHost { } diff --git a/src/Avalonia.Controls/IMenu.cs b/src/Avalonia.Controls/IMenu.cs index 0722a22f08..d90c5ea7a8 100644 --- a/src/Avalonia.Controls/IMenu.cs +++ b/src/Avalonia.Controls/IMenu.cs @@ -1,10 +1,12 @@ using Avalonia.Controls.Platform; +using Avalonia.Metadata; namespace Avalonia.Controls { /// /// Represents a or . /// + [NotClientImplementable] public interface IMenu : IMenuElement { /// diff --git a/src/Avalonia.Controls/IMenuElement.cs b/src/Avalonia.Controls/IMenuElement.cs index a3200d2b1b..c13c20b639 100644 --- a/src/Avalonia.Controls/IMenuElement.cs +++ b/src/Avalonia.Controls/IMenuElement.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using Avalonia.Input; +using Avalonia.Metadata; namespace Avalonia.Controls { /// /// Represents an or . /// + [NotClientImplementable] public interface IMenuElement : IControl { /// diff --git a/src/Avalonia.Controls/IMenuItem.cs b/src/Avalonia.Controls/IMenuItem.cs index 35e36eb0f4..9d7ef3c18d 100644 --- a/src/Avalonia.Controls/IMenuItem.cs +++ b/src/Avalonia.Controls/IMenuItem.cs @@ -1,8 +1,11 @@ -namespace Avalonia.Controls +using Avalonia.Metadata; + +namespace Avalonia.Controls { /// /// Represents a . /// + [NotClientImplementable] public interface IMenuItem : IMenuElement { /// diff --git a/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs b/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs index f492e6ca0f..29963e4821 100644 --- a/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs +++ b/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs @@ -1,5 +1,8 @@ +using Avalonia.Metadata; + namespace Avalonia.Controls { + [Unstable] public interface INativeMenuExporterEventsImplBridge { void RaiseNeedsUpdate (); diff --git a/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs b/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs index 6cb68d8ddd..a6c7489971 100644 --- a/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs +++ b/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs @@ -1,5 +1,8 @@ +using Avalonia.Metadata; + namespace Avalonia.Controls { + [Unstable] public interface INativeMenuItemExporterEventsImplBridge { void RaiseClicked (); diff --git a/src/Avalonia.Controls/IPanel.cs b/src/Avalonia.Controls/IPanel.cs index 7b9e2c2074..8f2564ec74 100644 --- a/src/Avalonia.Controls/IPanel.cs +++ b/src/Avalonia.Controls/IPanel.cs @@ -1,8 +1,11 @@ +using Avalonia.Metadata; + namespace Avalonia.Controls { /// /// Interface for controls that can contain multiple children. /// + [NotClientImplementable] public interface IPanel : IControl { /// @@ -10,4 +13,4 @@ namespace Avalonia.Controls /// Controls Children { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/IScrollable.cs b/src/Avalonia.Controls/IScrollable.cs index 2a98b3910a..680088290c 100644 --- a/src/Avalonia.Controls/IScrollable.cs +++ b/src/Avalonia.Controls/IScrollable.cs @@ -1,4 +1,3 @@ - namespace Avalonia.Controls.Primitives { /// diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index c448729643..7408bff902 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -1,3 +1,5 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Metadata; @@ -33,6 +35,7 @@ namespace Avalonia.Controls { AffectsRender(SourceProperty, StretchProperty, StretchDirectionProperty); AffectsMeasure(SourceProperty, StretchProperty, StretchDirectionProperty); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Image); } /// @@ -63,6 +66,8 @@ namespace Avalonia.Controls set { SetValue(StretchDirectionProperty, value); } } + protected override bool BypassFlowDirectionPolicies => true; + /// /// Renders the control. /// diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index ed8f9efb2e..56b0014c05 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; @@ -165,7 +166,7 @@ namespace Avalonia.Controls if (Presenter is IChildIndexProvider innerProvider) { innerProvider.ChildIndexChanged += PresenterChildIndexChanged; - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); } } @@ -335,13 +336,18 @@ namespace Avalonia.Controls base.OnKeyDown(e); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ItemsControlAutomationPeer(this); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ItemCountProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } } @@ -502,7 +508,6 @@ namespace Avalonia.Controls do { result = container.GetControl(direction, c, wrap); - from = from ?? result; if (result != null && result.Focusable && @@ -513,7 +518,7 @@ namespace Avalonia.Controls } c = result; - } while (c != null && c != from); + } while (c != null && c != from && direction != NavigationDirection.First && direction != NavigationDirection.Last); return null; } 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/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs index ad64c61ebe..080326606e 100644 --- a/src/Avalonia.Controls/MaskedTextBox.cs +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -32,7 +32,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(Mask), string.Empty); public static new readonly StyledProperty PasswordCharProperty = - AvaloniaProperty.Register(nameof(PasswordChar), '\0'); + AvaloniaProperty.Register(nameof(PasswordChar), '\0'); public static readonly StyledProperty PromptCharProperty = AvaloniaProperty.Register(nameof(PromptChar), '_'); @@ -280,7 +280,7 @@ namespace Avalonia.Controls base.OnLostFocus(e); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { void UpdateMaskProvider() { diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index cc89677f82..611811f170 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -1,3 +1,5 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -35,6 +37,8 @@ namespace Avalonia.Controls static Menu() { ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); + AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue(AccessibilityView.Control); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Menu); } /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index bdcff5cd09..122d45d033 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -18,8 +18,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty IsOpenProperty = + AvaloniaProperty.RegisterDirect( nameof(IsOpen), o => o.IsOpen); diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 185b834052..58229a1772 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using System.Windows.Input; +using Avalonia.Automation.Peers; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; @@ -19,8 +20,9 @@ namespace Avalonia.Controls /// /// A menu item control. /// + [TemplatePart("PART_Popup", typeof(Popup))] [PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")] - public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource + public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource, IClickableControl { /// /// Defines the property. @@ -83,13 +85,13 @@ namespace Avalonia.Controls /// Defines the event. /// public static readonly RoutedEvent PointerEnterItemEvent = - RoutedEvent.Register(nameof(PointerEnterItem), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(PointerEnterItem), RoutingStrategies.Bubble); /// /// Defines the event. /// public static readonly RoutedEvent PointerLeaveItemEvent = - RoutedEvent.Register(nameof(PointerLeaveItem), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(PointerLeaveItem), RoutingStrategies.Bubble); /// /// Defines the event. @@ -494,12 +496,20 @@ namespace Avalonia.Controls } } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override AutomationPeer OnCreateAutomationPeer() { - base.UpdateDataValidation(property, value); + return new MenuItemAutomationPeer(this); + } + + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) + { + base.UpdateDataValidation(property, state, error); if (property == CommandProperty) { - _commandBindingError = value.Type == BindingValueType.BindingError; + _commandBindingError = state == BindingValueType.BindingError; if (_commandBindingError && _commandCanExecute) { _commandCanExecute = false; @@ -637,7 +647,9 @@ namespace Avalonia.Controls /// The property change event. private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e) { - if ((bool)e.NewValue!) + var parentMenu = Parent as Menu; + + if ((bool)e.NewValue! && (parentMenu is null || parentMenu.IsOpen)) { Focus(); } @@ -698,6 +710,14 @@ namespace Avalonia.Controls void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + void IClickableControl.RaiseClick() + { + if (IsEffectivelyEnabled) + { + RaiseEvent(new RoutedEventArgs(ClickEvent)); + } + } + /// /// A dependency resolver which returns a . /// diff --git a/src/Avalonia.Controls/NativeMenuItemBase.cs b/src/Avalonia.Controls/NativeMenuItemBase.cs index a5e3308e3b..4946d16f01 100644 --- a/src/Avalonia.Controls/NativeMenuItemBase.cs +++ b/src/Avalonia.Controls/NativeMenuItemBase.cs @@ -11,8 +11,8 @@ namespace Avalonia.Controls } - public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); + public static readonly DirectProperty ParentProperty = + AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); public NativeMenu? Parent { diff --git a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs index 977544674d..b2e6e9e80b 100644 --- a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Controls.Notifications +using Avalonia.Metadata; + +namespace Avalonia.Controls.Notifications { /// /// Represents a notification manager that can show arbitrary content. @@ -9,6 +11,7 @@ /// can display arbitrary content, as opposed to notification managers which display notifications /// using the host operating system's notification mechanism. /// + [NotClientImplementable] public interface IManagedNotificationManager : INotificationManager { /// diff --git a/src/Avalonia.Controls/Notifications/INotification.cs b/src/Avalonia.Controls/Notifications/INotification.cs index fa08233097..9ccce5b2c4 100644 --- a/src/Avalonia.Controls/Notifications/INotification.cs +++ b/src/Avalonia.Controls/Notifications/INotification.cs @@ -1,10 +1,12 @@ using System; +using Avalonia.Metadata; namespace Avalonia.Controls.Notifications { /// /// Represents a notification that can be shown in a window or by the host operating system. /// + [NotClientImplementable] public interface INotification { /// diff --git a/src/Avalonia.Controls/Notifications/INotificationManager.cs b/src/Avalonia.Controls/Notifications/INotificationManager.cs index 72fb8e6c08..5fa479f2c3 100644 --- a/src/Avalonia.Controls/Notifications/INotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/INotificationManager.cs @@ -1,9 +1,12 @@ -namespace Avalonia.Controls.Notifications +using Avalonia.Metadata; + +namespace Avalonia.Controls.Notifications { /// /// Represents a notification manager that can be used to show notifications in a window or using /// the host operating system. /// + [NotClientImplementable] public interface INotificationManager { /// diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 9499995da3..2449f4c15c 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 { @@ -138,13 +139,13 @@ namespace Avalonia.Controls.Notifications notificationControl.Close(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == PositionProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index f67377b310..4d86a0f17c 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 { /// @@ -400,12 +403,16 @@ namespace Avalonia.Controls /// enabled. /// /// The property. - /// The new binding value for the property. - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + /// The current data binding state. + /// The current data binding error, if any. + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == TextProperty || property == ValueProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } @@ -1051,7 +1058,7 @@ namespace Avalonia.Controls var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); var textSpecialCharacters = text.Where(c => !char.IsDigit(c)); // same non-digit characters on currentValueText and new text => remove them on new Text to parse it again. - if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0) + if (!currentValueTextSpecialCharacters.Except(textSpecialCharacters).Any()) { foreach (var character in textSpecialCharacters) { diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 482a7fab84..2230b4b0d2 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -147,7 +147,7 @@ namespace Avalonia.Controls throw new NotSupportedException(); } - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); InvalidateMeasureOnChildrenChanged(); } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 23ecdb2e7b..3a6d06f150 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -4,6 +4,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -14,6 +15,7 @@ namespace Avalonia.Controls.Platform /// /// Provides the default keyboard and pointer interaction for menus. /// + [Unstable] public class DefaultMenuInteractionHandler : IMenuInteractionHandler { private readonly bool _isContextMenu; @@ -54,6 +56,7 @@ namespace Avalonia.Controls.Platform Menu.AddHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); Menu.AddHandler(MenuItem.PointerEnterItemEvent, PointerEnter); Menu.AddHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); + Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved); _root = Menu.VisualRoot; @@ -89,6 +92,7 @@ namespace Avalonia.Controls.Platform Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); Menu.RemoveHandler(MenuItem.PointerEnterItemEvent, PointerEnter); Menu.RemoveHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); + Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved); if (_root is InputElement inputRoot) { @@ -149,13 +153,18 @@ namespace Avalonia.Controls.Platform case Key.Up: case Key.Down: { - if (item?.IsTopLevel == true) + if (item?.IsTopLevel == true && item.HasSubMenu) { - if (item.HasSubMenu && !item.IsSubMenuOpen) + if (!item.IsSubMenuOpen) { Open(item, true); - e.Handled = true; } + else + { + item.MoveSelection(NavigationDirection.First, true); + } + + e.Handled = true; } else { @@ -247,7 +256,8 @@ namespace Avalonia.Controls.Platform // new menu. if (item.IsSubMenuOpen && item.Parent is IMenu && - item.Parent.SelectedItem is object) + item.Parent.SelectedItem is object && + item.Parent.SelectedItem != item) { item.Close(); Open(item.Parent.SelectedItem, true); @@ -332,6 +342,22 @@ namespace Avalonia.Controls.Platform } } + protected internal virtual void PointerMoved(object? sender, PointerEventArgs e) + { + // HACK: #8179 needs to be addressed to correctly implement it in the PointerPressed method. + var item = GetMenuItem(e.Source as IControl) as MenuItem; + if (item?.TransformedBounds == null) + { + return; + } + var point = e.GetCurrentPoint(null); + + if (point.Properties.IsLeftButtonPressed && item.TransformedBounds.Value.Contains(point.Position) == false) + { + e.Pointer.Capture(null); + } + } + protected internal virtual void PointerLeave(object? sender, PointerEventArgs e) { var item = GetMenuItem(e.Source as IControl); @@ -411,7 +437,7 @@ namespace Avalonia.Controls.Platform protected internal virtual void MenuOpened(object? sender, RoutedEventArgs e) { - if (e.Source == Menu) + if (e.Source is Menu) { Menu?.MoveSelection(NavigationDirection.First, true); } diff --git a/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs b/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs deleted file mode 100644 index 2a401ad912..0000000000 --- a/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace Avalonia.Platform -{ - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public class ExportWindowingSubsystemAttribute : Attribute - { - public ExportWindowingSubsystemAttribute(OperatingSystemType requiredRuntimePlatform, int priority, string name, Type initializationType, - string initializationMethod, Type? environmentChecker = null) - { - Name = name; - InitializationType = initializationType; - InitializationMethod = initializationMethod; - EnvironmentChecker = environmentChecker; - RequiredOS = requiredRuntimePlatform; - Priority = priority; - } - - public string InitializationMethod { get; private set; } - public Type? EnvironmentChecker { get; } - public Type InitializationType { get; private set; } - public string Name { get; private set; } - public int Priority { get; private set; } - public OperatingSystemType RequiredOS { get; private set; } - } -} diff --git a/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs b/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs index a8d3a3b3ac..99bbb8b56d 100644 --- a/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs +++ b/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs @@ -1,5 +1,8 @@ +using Avalonia.Metadata; + namespace Avalonia.Platform { + [Unstable] public interface IApplicationPlatformEvents { void RaiseUrlsOpened(string[] urls); diff --git a/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs index dd8503f768..47b5a048b0 100644 --- a/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs @@ -1,8 +1,11 @@ -namespace Avalonia.Controls.Platform +using Avalonia.Metadata; + +namespace Avalonia.Controls.Platform { /// /// Handles user interaction for menus. /// + [Unstable] public interface IMenuInteractionHandler { /// diff --git a/src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs b/src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs index 6e10163175..daeb9076e6 100644 --- a/src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs +++ b/src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs @@ -1,13 +1,13 @@ using System; using System.Collections.ObjectModel; -using System.Threading.Tasks; -using Avalonia.Platform; +using Avalonia.Metadata; namespace Avalonia.Controls.Platform { /// /// Defines a platform-specific mount volumes info provider implementation. /// + [Unstable] public interface IMountedVolumeInfoProvider { /// diff --git a/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs b/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs index df13613848..ffa79aa8d6 100644 --- a/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs +++ b/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs @@ -1,10 +1,11 @@ using System; using System.Diagnostics.CodeAnalysis; +using Avalonia.Metadata; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Controls.Platform { + [Unstable] public interface INativeControlHostImpl { INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent); @@ -13,11 +14,13 @@ namespace Avalonia.Controls.Platform bool IsCompatibleWith(IPlatformHandle handle); } + [Unstable] public interface INativeControlHostDestroyableControlHandle : IPlatformHandle { void Destroy(); } + [Unstable] public interface INativeControlHostControlTopLevelAttachment : IDisposable { INativeControlHostImpl? AttachedTo { get; set; } @@ -27,6 +30,7 @@ namespace Avalonia.Controls.Platform void ShowInBounds(Rect rect); } + [Unstable] public interface ITopLevelImplWithNativeControlHost { INativeControlHostImpl? NativeControlHost { get; } diff --git a/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs b/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs index ecbc6d2234..4c844ce30f 100644 --- a/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs +++ b/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs @@ -1,7 +1,9 @@ using System.IO; +using Avalonia.Metadata; namespace Avalonia.Platform { + [Unstable] public interface IPlatformIconLoader { IWindowIconImpl LoadIcon(string fileName); diff --git a/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs b/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs index 4cd6640453..0658f9211c 100644 --- a/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs +++ b/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs @@ -1,9 +1,10 @@ using System; -using System.ComponentModel; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Metadata; namespace Avalonia.Platform { + [Unstable] public interface IPlatformLifetimeEventsImpl { /// diff --git a/src/Avalonia.Controls/Platform/IPlatformNativeSurfaceHandle.cs b/src/Avalonia.Controls/Platform/IPlatformNativeSurfaceHandle.cs new file mode 100644 index 0000000000..6ad07b1b13 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IPlatformNativeSurfaceHandle.cs @@ -0,0 +1,12 @@ +using System; +using Avalonia.Metadata; + +namespace Avalonia.Platform +{ + [Unstable] + public interface IPlatformNativeSurfaceHandle : IPlatformHandle + { + PixelSize Size { get; } + double Scaling { get; } + } +} diff --git a/src/Avalonia.Controls/Platform/IPopupImpl.cs b/src/Avalonia.Controls/Platform/IPopupImpl.cs index 477d5fab43..cd86045dee 100644 --- a/src/Avalonia.Controls/Platform/IPopupImpl.cs +++ b/src/Avalonia.Controls/Platform/IPopupImpl.cs @@ -1,10 +1,12 @@ using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Metadata; namespace Avalonia.Platform { /// /// Defines a platform-specific popup window implementation. /// + [Unstable] public interface IPopupImpl : IWindowBaseImpl { IPopupPositioner PopupPositioner { get; } diff --git a/src/Avalonia.Controls/Platform/IScreenImpl.cs b/src/Avalonia.Controls/Platform/IScreenImpl.cs index 5bd45057d9..fcae3b6493 100644 --- a/src/Avalonia.Controls/Platform/IScreenImpl.cs +++ b/src/Avalonia.Controls/Platform/IScreenImpl.cs @@ -1,11 +1,19 @@ using System.Collections.Generic; +using Avalonia.Metadata; namespace Avalonia.Platform { + [Unstable] public interface IScreenImpl { 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/ISystemDialogImpl.cs b/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs index 1685a6a38c..715eda5cfa 100644 --- a/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs +++ b/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs @@ -1,10 +1,12 @@ using System.Threading.Tasks; +using Avalonia.Metadata; namespace Avalonia.Controls.Platform { /// /// Defines a platform-specific system dialog implementation. /// + [Unstable] public interface ISystemDialogImpl { /// diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 80434882f7..bd0339f525 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -4,6 +4,7 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Layout; +using Avalonia.Metadata; using Avalonia.Rendering; using JetBrains.Annotations; @@ -50,6 +51,7 @@ namespace Avalonia.Platform /// This interface is the common interface to and /// . /// + [Unstable] public interface ITopLevelImpl : IDisposable { /// diff --git a/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs b/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs index bafb973765..a2e426ca08 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs @@ -1,9 +1,11 @@ using Avalonia.Input; using Avalonia.Input.TextInput; +using Avalonia.Metadata; using Avalonia.Platform; namespace Avalonia.Controls.Platform { + [Unstable] public interface ITopLevelImplWithTextInputMethod : ITopLevelImpl { public ITextInputMethodImpl? TextInputMethod { get; } diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs index 9e72a40439..149a978c54 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs @@ -1,13 +1,16 @@ using System; +using Avalonia.Metadata; using Avalonia.Platform; namespace Avalonia.Controls.Platform { + [Unstable] public interface INativeMenuExporter { void SetNativeMenu(NativeMenu? menu); } + [Unstable] public interface ITopLevelNativeMenuExporter : INativeMenuExporter { bool IsNativeMenuExported { get; } @@ -15,11 +18,13 @@ namespace Avalonia.Controls.Platform event EventHandler OnIsNativeMenuExportedChanged; } + [Unstable] public interface INativeMenuExporterProvider { INativeMenuExporter? NativeMenuExporter { get; } } - + + [Unstable] public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl { ITopLevelNativeMenuExporter? NativeMenuExporter { get; } diff --git a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs index 289b569645..4ef9397d04 100644 --- a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs +++ b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs @@ -1,8 +1,10 @@ using System; using Avalonia.Controls.Platform; +using Avalonia.Metadata; namespace Avalonia.Platform { + [Unstable] public interface ITrayIconImpl : IDisposable { /// diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index ff83e007b4..512fad6dfc 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -1,7 +1,10 @@ using System; +using Avalonia.Automation.Peers; +using Avalonia.Metadata; namespace Avalonia.Platform { + [Unstable] public interface IWindowBaseImpl : ITopLevelImpl { /// diff --git a/src/Avalonia.Controls/Platform/IWindowIconImpl.cs b/src/Avalonia.Controls/Platform/IWindowIconImpl.cs index 7086b7651c..4bb8844d97 100644 --- a/src/Avalonia.Controls/Platform/IWindowIconImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowIconImpl.cs @@ -1,7 +1,9 @@ using System.IO; +using Avalonia.Metadata; namespace Avalonia.Platform { + [Unstable] public interface IWindowIconImpl { void Save(Stream outputStream); diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index d4be4f9f45..af9392d440 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -1,12 +1,14 @@ using System; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Metadata; namespace Avalonia.Platform { /// /// Defines a platform-specific window implementation. /// + [Unstable] public interface IWindowImpl : IWindowBaseImpl { /// diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index fa26fe8fdd..5acc5adccd 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -1,5 +1,8 @@ +using Avalonia.Metadata; + namespace Avalonia.Platform { + [Unstable] public interface IWindowingPlatform { IWindowImpl CreateWindow(); diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs index 4e5908456e..630d2d8efb 100644 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs @@ -1,14 +1,13 @@ using System; -using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading; -using System.Threading.Tasks; +using Avalonia.Metadata; using Avalonia.Platform; -using Avalonia.Rendering; using Avalonia.Threading; namespace Avalonia.Controls.Platform { + [Unstable] public class InternalPlatformThreadingInterface : IPlatformThreadingInterface { public InternalPlatformThreadingInterface() diff --git a/src/Avalonia.Controls/Platform/MountedDriveInfo.cs b/src/Avalonia.Controls/Platform/MountedDriveInfo.cs index f3104e4360..620ac9303f 100644 --- a/src/Avalonia.Controls/Platform/MountedDriveInfo.cs +++ b/src/Avalonia.Controls/Platform/MountedDriveInfo.cs @@ -1,10 +1,12 @@ using System; +using Avalonia.Metadata; namespace Avalonia.Controls.Platform { /// /// Describes a Drive's properties. /// + [Unstable] public class MountedVolumeInfo : IEquatable { public string? VolumeLabel { get; set; } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index ee62316922..92f6f1cb52 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -1,9 +1,11 @@ using System; using System.Reactive.Disposables; +using Avalonia.Metadata; using Avalonia.Platform; namespace Avalonia.Controls.Platform { + [Unstable] public static partial class PlatformManager { static bool s_designerMode; 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/Platform/Surfaces/IFramebufferPlatformSurface.cs b/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs index 62cd012d51..0a7daeaa24 100644 --- a/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs +++ b/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs @@ -1,7 +1,9 @@ -using Avalonia.Platform; +using Avalonia.Metadata; +using Avalonia.Platform; namespace Avalonia.Controls.Platform.Surfaces { + [Unstable] public interface IFramebufferPlatformSurface { /// diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 9886dd913a..12a8dd747d 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -1,5 +1,6 @@ using System; +using Avalonia.Controls.Documents; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -8,6 +9,7 @@ using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Utilities; namespace Avalonia.Controls.Presenters { @@ -46,7 +48,73 @@ namespace Avalonia.Controls.Presenters /// public static readonly StyledProperty BoxShadowProperty = Border.BoxShadowProperty.AddOwner(); - + + /// + /// Defines the property. + /// + public static readonly StyledProperty ForegroundProperty = + TextElement.ForegroundProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontFamilyProperty = + TextElement.FontFamilyProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontSizeProperty = + TextElement.FontSizeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontStyleProperty = + TextElement.FontStyleProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontWeightProperty = + TextElement.FontWeightProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontStretchProperty = + TextElement.FontStretchProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty TextAlignmentProperty = + TextBlock.TextAlignmentProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty TextWrappingProperty = + TextBlock.TextWrappingProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty TextTrimmingProperty = + TextBlock.TextTrimmingProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty LineHeightProperty = + TextBlock.LineHeightProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty MaxLinesProperty = + TextBlock.MaxLinesProperty.AddOwner(); + /// /// Defines the property. /// @@ -107,9 +175,6 @@ namespace Avalonia.Controls.Presenters AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty); AffectsArrange(HorizontalContentAlignmentProperty, VerticalContentAlignmentProperty); AffectsMeasure(BorderThicknessProperty, PaddingProperty); - ContentProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e)); - ContentTemplateProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e)); - TemplatedParentProperty.Changed.AddClassHandler((x, e) => x.TemplatedParentChanged(e)); } public ContentPresenter() @@ -162,6 +227,105 @@ namespace Avalonia.Controls.Presenters set => SetValue(BoxShadowProperty, value); } + /// + /// Gets or sets a brush used to paint the text. + /// + public IBrush? Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + /// + /// Gets or sets the font family. + /// + public FontFamily FontFamily + { + get => GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + /// + /// Gets or sets the font size. + /// + public double FontSize + { + get => GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + /// + /// Gets or sets the font style. + /// + public FontStyle FontStyle + { + get => GetValue(FontStyleProperty); + set => SetValue(FontStyleProperty, value); + } + + /// + /// Gets or sets the font weight. + /// + public FontWeight FontWeight + { + get => GetValue(FontWeightProperty); + set => SetValue(FontWeightProperty, value); + } + + /// + /// Gets or sets the font stretch. + /// + public FontStretch FontStretch + { + get => GetValue(FontStretchProperty); + set => SetValue(FontStretchProperty, value); + } + + /// + /// Gets or sets the text alignment + /// + public TextAlignment TextAlignment + { + get => GetValue(TextAlignmentProperty); + set => SetValue(TextAlignmentProperty, value); + } + + /// + /// Gets or sets the text wrapping + /// + public TextWrapping TextWrapping + { + get => GetValue(TextWrappingProperty); + set => SetValue(TextWrappingProperty, value); + } + + /// + /// Gets or sets the text trimming + /// + public TextTrimming TextTrimming + { + get => GetValue(TextTrimmingProperty); + set => SetValue(TextTrimmingProperty, value); + } + + /// + /// Gets or sets the line height + /// + public double LineHeight + { + get => GetValue(LineHeightProperty); + set => SetValue(LineHeightProperty, value); + } + + /// + /// Gets or sets the max lines + /// + public int MaxLines + { + get => GetValue(MaxLinesProperty); + set => SetValue(MaxLinesProperty, value); + } + /// /// Gets the control displayed by the presenter. /// @@ -240,6 +404,25 @@ 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; + case nameof(UseLayoutRounding): + case nameof(BorderThickness): + _layoutThickness = null; + break; + } + } + /// /// Updates the control based on the control's . /// @@ -254,8 +437,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 +460,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 +488,7 @@ namespace Avalonia.Controls.Presenters } _createdChild = true; + } /// @@ -310,10 +500,43 @@ namespace Avalonia.Controls.Presenters InvalidateMeasure(); } + private Thickness? _layoutThickness; + private double _scale; + + private Thickness LayoutThickness + { + get + { + VerifyScale(); + + if (_layoutThickness == null) + { + var borderThickness = BorderThickness; + + if (UseLayoutRounding) + borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale, _scale); + + _layoutThickness = borderThickness; + } + + return _layoutThickness.Value; + } + } + + private void VerifyScale() + { + var currentScale = LayoutHelper.GetLayoutScale(this); + if (MathUtilities.AreClose(currentScale, _scale)) + return; + + _scale = currentScale; + _layoutThickness = null; + } + /// public override void Render(DrawingContext context) { - _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush, + _borderRenderer.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush, BoxShadow); } @@ -325,18 +548,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 ); @@ -376,13 +604,22 @@ namespace Avalonia.Controls.Presenters { if (Child == null) return finalSize; - var padding = Padding + BorderThickness; + var useLayoutRounding = UseLayoutRounding; + var scale = LayoutHelper.GetLayoutScale(this); + var padding = Padding; + var borderThickness = BorderThickness; + + if (useLayoutRounding) + { + padding = LayoutHelper.RoundLayoutThickness(padding, scale, scale); + borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, scale, scale); + } + + padding += borderThickness; var horizontalContentAlignment = HorizontalContentAlignment; var verticalContentAlignment = VerticalContentAlignment; - var useLayoutRounding = UseLayoutRounding; var availableSize = finalSize; var sizeForChild = availableSize; - var scale = LayoutHelper.GetLayoutScale(this); var originX = offset.X; var originY = offset.Y; @@ -446,7 +683,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/IContentPresenter.cs b/src/Avalonia.Controls/Presenters/IContentPresenter.cs index ab4d61e3bd..673de4700b 100644 --- a/src/Avalonia.Controls/Presenters/IContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IContentPresenter.cs @@ -1,4 +1,5 @@ using Avalonia.Controls.Primitives; +using Avalonia.Metadata; namespace Avalonia.Controls.Presenters { @@ -6,6 +7,7 @@ namespace Avalonia.Controls.Presenters /// Interface for controls that present a single item of data inside a /// template. /// + [NotClientImplementable] public interface IContentPresenter : IPresenter { /// diff --git a/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs b/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs index 78c4affe44..562638e94a 100644 --- a/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs +++ b/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs @@ -1,5 +1,6 @@ using Avalonia.Collections; using Avalonia.LogicalTree; +using Avalonia.Metadata; using Avalonia.Styling; namespace Avalonia.Controls.Presenters @@ -15,6 +16,7 @@ namespace Avalonia.Controls.Presenters /// parent control's template is instantiated so they register themselves using this /// interface. /// + [NotClientImplementable] public interface IContentPresenterHost : ITemplatedControl { /// diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index e7da3d4618..7cc72ef0a7 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -1,8 +1,10 @@ using System.Collections; using System.Collections.Specialized; +using Avalonia.Metadata; namespace Avalonia.Controls.Presenters { + [NotClientImplementable] public interface IItemsPresenter : IPresenter { IEnumerable? Items { get; set; } diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs b/src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs index ba9ee0fe31..db11474871 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenterHost.cs @@ -1,3 +1,4 @@ +using Avalonia.Metadata; using Avalonia.Styling; namespace Avalonia.Controls.Presenters @@ -13,6 +14,7 @@ namespace Avalonia.Controls.Presenters /// parent control's template is instantiated so they register themselves using this /// interface. /// + [NotClientImplementable] public interface IItemsPresenterHost : ITemplatedControl { /// diff --git a/src/Avalonia.Controls/Presenters/IPresenter.cs b/src/Avalonia.Controls/Presenters/IPresenter.cs index 5318ea2757..0399983189 100644 --- a/src/Avalonia.Controls/Presenters/IPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IPresenter.cs @@ -1,4 +1,5 @@ using Avalonia.Controls.Primitives; +using Avalonia.Metadata; namespace Avalonia.Controls.Presenters { @@ -12,6 +13,7 @@ namespace Avalonia.Controls.Presenters /// of a then that signals that the visual child /// of the presenter is not a part of the template. /// + [NotClientImplementable] public interface IPresenter : IControl, INamed { } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index f938c8d437..2821fa8cf0 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -158,7 +158,7 @@ namespace Avalonia.Controls.Presenters { ItemsChanged(e); - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); } } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 97fb4c3f43..c526b7ac49 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -389,7 +389,7 @@ namespace Avalonia.Controls.Presenters { var logicalUnits = delta.Y / logicalScrollItemSize.Y; delta = delta.WithY(delta.Y - logicalUnits * logicalScrollItemSize.Y); - dy = logicalUnits * scrollable!.ScrollSize.Height; + dy = logicalUnits; } else dy = delta.Y; @@ -407,7 +407,7 @@ namespace Avalonia.Controls.Presenters { var logicalUnits = delta.X / logicalScrollItemSize.X; delta = delta.WithX(delta.X - logicalUnits * logicalScrollItemSize.X); - dx = logicalUnits * scrollable!.ScrollSize.Width; + dx = logicalUnits; } else dx = delta.X; @@ -469,7 +469,7 @@ namespace Avalonia.Controls.Presenters } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == OffsetProperty && !_arranging) { diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 8629af5243..ea9ae7bb0f 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -8,6 +8,7 @@ using Avalonia.Utilities; using Avalonia.VisualTree; using Avalonia.Layout; using Avalonia.Media.Immutable; +using Avalonia.Controls.Documents; namespace Avalonia.Controls.Presenters { @@ -25,14 +26,14 @@ namespace Avalonia.Controls.Presenters AvaloniaProperty.Register(nameof(PasswordChar)); public static readonly StyledProperty SelectionBrushProperty = - AvaloniaProperty.Register(nameof(SelectionBrushProperty)); + AvaloniaProperty.Register(nameof(SelectionBrush)); public static readonly StyledProperty SelectionForegroundBrushProperty = - AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); + AvaloniaProperty.Register(nameof(SelectionForegroundBrush)); public static readonly StyledProperty CaretBrushProperty = - AvaloniaProperty.Register(nameof(CaretBrushProperty)); - + AvaloniaProperty.Register(nameof(CaretBrush)); + public static readonly DirectProperty SelectionStartProperty = TextBox.SelectionStartProperty.AddOwner( o => o.SelectionStart, @@ -42,7 +43,7 @@ namespace Avalonia.Controls.Presenters TextBox.SelectionEndProperty.AddOwner( o => o.SelectionEnd, (o, v) => o.SelectionEnd = v); - + /// /// Defines the property. /// @@ -64,6 +65,12 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty TextWrappingProperty = TextBlock.TextWrappingProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty LineHeightProperty = + TextBlock.LineHeightProperty.AddOwner(); + /// /// Defines the property. /// @@ -121,8 +128,8 @@ namespace Avalonia.Controls.Presenters /// public FontFamily FontFamily { - get => TextBlock.GetFontFamily(this); - set => TextBlock.SetFontFamily(this, value); + get => TextElement.GetFontFamily(this); + set => TextElement.SetFontFamily(this, value); } /// @@ -130,8 +137,8 @@ namespace Avalonia.Controls.Presenters /// public double FontSize { - get => TextBlock.GetFontSize(this); - set => TextBlock.SetFontSize(this, value); + get => TextElement.GetFontSize(this); + set => TextElement.SetFontSize(this, value); } /// @@ -139,8 +146,8 @@ namespace Avalonia.Controls.Presenters /// public FontStyle FontStyle { - get => TextBlock.GetFontStyle(this); - set => TextBlock.SetFontStyle(this, value); + get => TextElement.GetFontStyle(this); + set => TextElement.SetFontStyle(this, value); } /// @@ -148,8 +155,17 @@ namespace Avalonia.Controls.Presenters /// public FontWeight FontWeight { - get => TextBlock.GetFontWeight(this); - set => TextBlock.SetFontWeight(this, value); + get => TextElement.GetFontWeight(this); + set => TextElement.SetFontWeight(this, value); + } + + /// + /// Gets or sets the font stretch. + /// + public FontStretch FontStretch + { + get => TextElement.GetFontStretch(this); + set => TextElement.SetFontStretch(this, value); } /// @@ -157,8 +173,8 @@ namespace Avalonia.Controls.Presenters /// public IBrush? Foreground { - get => TextBlock.GetForeground(this); - set => TextBlock.SetForeground(this, value); + get => TextElement.GetForeground(this); + set => TextElement.SetForeground(this, value); } /// @@ -169,6 +185,15 @@ namespace Avalonia.Controls.Presenters get => GetValue(TextWrappingProperty); set => SetValue(TextWrappingProperty, value); } + + /// + /// Gets or sets the line height. By default, this is set to , which determines the appropriate height automatically. + /// + public double LineHeight + { + get => GetValue(LineHeightProperty); + set => SetValue(LineHeightProperty, value); + } /// /// Gets or sets the text alignment. @@ -243,7 +268,7 @@ namespace Avalonia.Controls.Presenters get => GetValue(CaretBrushProperty); set => SetValue(CaretBrushProperty, value); } - + public int SelectionStart { get @@ -271,6 +296,8 @@ namespace Avalonia.Controls.Presenters SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); } } + + protected override bool BypassFlowDirectionPolicies => true; /// /// Creates the used to render the text. @@ -289,7 +316,7 @@ namespace Avalonia.Controls.Presenters var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment, TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides, - flowDirection: FlowDirection); + flowDirection: FlowDirection, lineHeight: LineHeight); return textLayout; } @@ -310,7 +337,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) { @@ -344,7 +371,7 @@ namespace Avalonia.Controls.Presenters foreach (var rect in rects) { - context.FillRectangle(selectionBrush, rect); + context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1)); } } @@ -385,10 +412,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 +490,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; @@ -495,28 +526,43 @@ namespace Avalonia.Controls.Presenters InvalidateMeasure(); } - + + protected override Size MeasureOverride(Size availableSize) { - _textLayout = null; + if (string.IsNullOrEmpty(Text)) + { + return new Size(); + } _constraint = availableSize; - return TextLayout.Size; + _textLayout = null; + + InvalidateArrange(); + + var measuredSize = TextLayout.Bounds.Size; + + return measuredSize; } protected override Size ArrangeOverride(Size finalSize) { - if (!double.IsInfinity(_constraint.Width)) + if (finalSize.Width < TextLayout.Bounds.Width) { - return base.ArrangeOverride(finalSize); + finalSize = finalSize.WithWidth(TextLayout.Bounds.Width); } - - _constraint = finalSize; - + + if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) + { + return finalSize; + } + + _constraint = new Size(finalSize.Width, double.PositiveInfinity); + _textLayout = null; - return base.ArrangeOverride(finalSize); + return finalSize; } private int CoerceCaretIndex(int value) @@ -615,19 +661,12 @@ namespace Avalonia.Controls.Presenters CaretChanged(); } - public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward) + public CharacterHit GetNextCharacterHit(LogicalDirection direction = LogicalDirection.Forward) { if (Text is null) { - return; - } - - if (FlowDirection == FlowDirection.RightToLeft) - { - direction = direction == LogicalDirection.Forward ? - LogicalDirection.Backward : - LogicalDirection.Forward; - } + return default; + } var characterHit = _lastCharacterHit; var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -636,7 +675,7 @@ namespace Avalonia.Controls.Presenters if (lineIndex < 0) { - return; + return default; } if (direction == LogicalDirection.Forward) @@ -649,7 +688,7 @@ namespace Avalonia.Controls.Presenters caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - if (textLine.NewLineLength > 0 && caretIndex == textLine.TextRange.Start + textLine.TextRange.Length) + if (textLine.TrailingWhitespaceLength > 0 && caretIndex == textLine.FirstTextSourceIndex + textLine.Length) { characterHit = new CharacterHit(caretIndex); } @@ -661,7 +700,7 @@ namespace Avalonia.Controls.Presenters break; } - if (caretIndex - textLine.NewLineLength == textLine.TextRange.Start + textLine.TextRange.Length) + if (caretIndex - textLine.NewLineLength == textLine.FirstTextSourceIndex + textLine.Length) { break; } @@ -697,6 +736,20 @@ namespace Avalonia.Controls.Presenters } } + return characterHit; + } + + public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward) + { + if (FlowDirection == FlowDirection.RightToLeft) + { + direction = direction == LogicalDirection.Forward ? + LogicalDirection.Backward : + LogicalDirection.Forward; + } + + var characterHit = GetNextCharacterHit(direction); + UpdateCaret(characterHit); _navigationPosition = _caretBounds.Position; @@ -749,25 +802,31 @@ namespace Avalonia.Controls.Presenters _caretTimer.Tick -= CaretTimerTick; } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); switch (change.Property.Name) { - case nameof (TextBlock.Foreground): - case nameof (TextBlock.FontSize): - case nameof (TextBlock.FontStyle): - case nameof (TextBlock.FontWeight): - case nameof (TextBlock.FontFamily): + case nameof (Foreground): + case nameof (FontSize): + case nameof (FontStyle): + case nameof (FontWeight): + case nameof (FontFamily): + case nameof (FontStretch): + case nameof (Text): case nameof (TextAlignment): case nameof (TextWrapping): + case nameof (SelectionStart): case nameof (SelectionEnd): case nameof (SelectionForegroundBrush): + case nameof (PasswordChar): case nameof (RevealPassword): + + case nameof(FlowDirection): { InvalidateTextLayout(); break; diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index a6976721b1..87cf660cad 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using Avalonia.Automation.Peers; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.TextFormatting; @@ -78,9 +79,9 @@ namespace Avalonia.Controls.Primitives } /// - protected override TextLayout? CreateTextLayout(Size constraint, string? text) + protected override TextLayout CreateTextLayout(Size constraint, string? text) { - return base.CreateTextLayout(constraint, StripAccessKey(text)); + return base.CreateTextLayout(constraint, RemoveAccessKeyMarker(text)); } /// @@ -107,29 +108,40 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Returns a string with the first underscore stripped. - /// - /// The text. - /// The text with the first underscore stripped. - [return: NotNullIfNotNull("text")] - private string? StripAccessKey(string? text) + protected override AutomationPeer OnCreateAutomationPeer() { - if (text is null) - { - return null; - } - - var position = text.IndexOf('_'); + return new NoneAutomationPeer(this); + } - if (position == -1) + internal static string? RemoveAccessKeyMarker(string? text) + { + if (!string.IsNullOrEmpty(text)) { - return text; + var accessKeyMarker = "_"; + var doubleAccessKeyMarker = accessKeyMarker + accessKeyMarker; + int index = FindAccessKeyMarker(text); + if (index >= 0 && index < text.Length - 1) + text = text.Remove(index, 1); + text = text.Replace(doubleAccessKeyMarker, accessKeyMarker); } - else + return text; + } + + private static int FindAccessKeyMarker(string text) + { + var length = text.Length; + var startIndex = 0; + while (startIndex < length) { - return text.Substring(0, position) + text.Substring(position + 1); + int index = text.IndexOf('_', startIndex); + if (index == -1) + return -1; + if (index + 1 < length && text[index + 1] != '_') + return index; + startIndex = index + 2; } + + return -1; } /// diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 353f12118f..5ad4e39baf 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -69,15 +69,18 @@ namespace Avalonia.Controls.Primitives { foreach (var child in Children) { - var info = child.GetValue(s_adornedElementInfoProperty); - - if (info != null && info.Bounds.HasValue) - { - child.Measure(info.Bounds.Value.Bounds.Size); - } - else + if (child is AvaloniaObject ao) { - child.Measure(availableSize); + var info = ao.GetValue(s_adornedElementInfoProperty); + + if (info != null && info.Bounds.HasValue) + { + child.Measure(info.Bounds.Value.Bounds.Size); + } + else + { + child.Measure(availableSize); + } } } @@ -88,19 +91,22 @@ namespace Avalonia.Controls.Primitives { foreach (var child in Children) { - var info = child.GetValue(s_adornedElementInfoProperty); - var isClipEnabled = child.GetValue(IsClipEnabledProperty); - - if (info != null && info.Bounds.HasValue) - { - child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform); - child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute); - UpdateClip(child, info.Bounds.Value, isClipEnabled); - child.Arrange(info.Bounds.Value.Bounds); - } - else + if (child is AvaloniaObject ao) { - child.Arrange(new Rect(finalSize)); + var info = ao.GetValue(s_adornedElementInfoProperty); + var isClipEnabled = ao.GetValue(IsClipEnabledProperty); + + if (info != null && info.Bounds.HasValue) + { + child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform); + child.RenderTransformOrigin = new RelativePoint(new Point(0, 0), RelativeUnit.Absolute); + UpdateClip(child, info.Bounds.Value, isClipEnabled); + child.Arrange(info.Bounds.Value.Bounds); + } + else + { + ArrangeChild((Control) child, finalSize); + } } } diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs index 36d2ae9230..bd2aa39621 100644 --- a/src/Avalonia.Controls/Primitives/IPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs @@ -2,6 +2,8 @@ using System; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -14,19 +16,54 @@ namespace Avalonia.Controls.Primitives /// () or an which is created /// on an . /// + [NotClientImplementable] public interface IPopupHost : IDisposable, IFocusScope { /// - /// Sets the control to display in the popup. + /// Gets or sets the fixed width of the popup. /// - /// - void SetChild(IControl? control); + double Width { get; set; } + + /// + /// Gets or sets the minimum width of the popup. + /// + double MinWidth { get; set; } + + /// + /// Gets or sets the maximum width of the popup. + /// + double MaxWidth { get; set; } + + /// + /// Gets or sets the fixed height of the popup. + /// + double Height { get; set; } + + /// + /// Gets or sets the minimum height of the popup. + /// + double MinHeight { get; set; } + + /// + /// Gets or sets the maximum height of the popup. + /// + double MaxHeight { get; set; } /// /// Gets the presenter from the control's template. /// IContentPresenter? Presenter { get; } + /// + /// Gets or sets whether the popup appears on top of all other windows. + /// + bool Topmost { get; set; } + + /// + /// Gets or sets a transform that will be applied to the popup. + /// + Transform? Transform { get; set; } + /// /// Gets the root of the visual tree in the case where the popup is presented using a /// separate visual tree. @@ -57,6 +94,12 @@ namespace Avalonia.Controls.Primitives PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All, Rect? rect = null); + /// + /// Sets the control to display in the popup. + /// + /// + void SetChild(IControl? control); + /// /// Shows the popup. /// @@ -66,14 +109,5 @@ namespace Avalonia.Controls.Primitives /// Hides the popup. /// void Hide(); - - /// - /// Binds the constraints of the popup host to a set of properties, usally those present on - /// . - /// - IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, - StyledProperty minWidthProperty, StyledProperty maxWidthProperty, - StyledProperty heightProperty, StyledProperty minHeightProperty, - StyledProperty maxHeightProperty, StyledProperty topmostProperty); } } diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs index 6a0408d6d1..468879edd1 100644 --- a/src/Avalonia.Controls/Primitives/OverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs @@ -23,7 +23,14 @@ namespace Avalonia.Controls.Primitives } public bool HitTest(Point point) => Children.HitTestCustom(point); - + + protected override Size MeasureOverride(Size availableSize) + { + foreach (Control child in Children) + child.Measure(availableSize); + return availableSize; + } + protected override Size ArrangeOverride(Size finalSize) { // We are saving it here since child controls might need to know the entire size of the overlay diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 6251d5cda7..4765718c3b 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -11,6 +11,12 @@ namespace Avalonia.Controls.Primitives { public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup { + /// + /// Defines the property. + /// + public static readonly StyledProperty TransformProperty = + PopupRoot.TransformProperty.AddOwner(); + private readonly OverlayLayer _overlayLayer; private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters(); private ManagedPopupPositioner _positioner; @@ -29,10 +35,22 @@ namespace Avalonia.Controls.Primitives } public IVisual? HostedVisualTreeRoot => null; - + + public Transform? Transform + { + get => GetValue(TransformProperty); + set => SetValue(TransformProperty, value); + } + /// IInteractive? IInteractive.InteractiveParent => Parent; + bool IPopupHost.Topmost + { + get => false; + set { /* Not currently supported in overlay popups */ } + } + public void Dispose() => Hide(); @@ -48,35 +66,13 @@ namespace Avalonia.Controls.Primitives _shown = false; } - public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, - StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, - StyledProperty maxHeightProperty, StyledProperty topmostProperty) - { - // Topmost property is not supported - var bindings = new List(); - - void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); - Bind(WidthProperty, widthProperty); - Bind(MinWidthProperty, minWidthProperty); - Bind(MaxWidthProperty, maxWidthProperty); - Bind(HeightProperty, heightProperty); - Bind(MinHeightProperty, minHeightProperty); - Bind(MaxHeightProperty, maxHeightProperty); - - return Disposable.Create(() => - { - foreach (var x in bindings) - x.Dispose(); - }); - } - public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, PopupAnchor anchor = PopupAnchor.None, PopupGravity gravity = PopupGravity.None, PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All, Rect? rect = null) { _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot()!, target, placement, offset, anchor, - gravity, constraintAdjustment, rect); + gravity, constraintAdjustment, rect, FlowDirection); UpdatePosition(); } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index f3fec95bcc..95e5e25c42 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; @@ -13,6 +14,8 @@ using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.VisualTree; +using Avalonia.Media; +using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { @@ -24,7 +27,7 @@ namespace Avalonia.Controls.Primitives #pragma warning restore CS0612 // Type or member is obsolete { public static readonly StyledProperty WindowManagerAddShadowHintProperty = - AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), false); + AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), false); /// /// Defines the property. @@ -32,6 +35,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty ChildProperty = AvaloniaProperty.Register(nameof(Child)); + /// + /// Defines the property. + /// + public static readonly StyledProperty InheritsTransformProperty = + AvaloniaProperty.Register(nameof(InheritsTransform)); + /// /// Defines the property. /// @@ -195,6 +204,16 @@ namespace Avalonia.Controls.Primitives set; } + /// + /// Gets or sets a value that determines whether the popup inherits the render transform + /// from its . Defaults to false. + /// + public bool InheritsTransform + { + get => GetValue(InheritsTransformProperty); + set => SetValue(InheritsTransformProperty, value); + } + /// /// Gets or sets a value that determines how the can be dismissed. /// @@ -394,24 +413,29 @@ namespace Avalonia.Controls.Primitives } _isOpenRequested = false; - var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); + var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); var handlerCleanup = new CompositeDisposable(7); - popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, - HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup); - + UpdateHostSizing(popupHost, topLevel, placementTarget); + popupHost.Topmost = Topmost; popupHost.SetChild(Child); ((ISetLogicalParent)popupHost).SetParent(this); - popupHost.ConfigurePosition( - placementTarget, - PlacementMode, - new Point(HorizontalOffset, VerticalOffset), - PlacementAnchor, - PlacementGravity, - PlacementConstraintAdjustment, - PlacementRect); + if (InheritsTransform && placementTarget is Control c) + { + SubscribeToEventHandler>( + c, + PlacementTargetPropertyChanged, + (x, handler) => x.PropertyChanged += handler, + (x, handler) => x.PropertyChanged -= handler).DisposeWith(handlerCleanup); + } + else + { + popupHost.Transform = null; + } + + UpdateHostPosition(popupHost, placementTarget); SubscribeToEventHandler>(popupHost, RootTemplateApplied, (x, handler) => x.TemplateApplied += handler, @@ -493,7 +517,7 @@ namespace Avalonia.Controls.Primitives } } - _openState = new PopupOpenState(topLevel, popupHost, cleanupPopup); + _openState = new PopupOpenState(placementTarget, topLevel, popupHost, cleanupPopup); WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint); @@ -541,7 +565,93 @@ namespace Avalonia.Controls.Primitives base.OnDetachedFromLogicalTree(e); Close(); } - + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (_openState is not null) + { + if (change.Property == WidthProperty || + change.Property == MinWidthProperty || + change.Property == MaxWidthProperty || + change.Property == HeightProperty || + change.Property == MinHeightProperty || + change.Property == MaxHeightProperty) + { + UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget); + } + else if (change.Property == PlacementTargetProperty || + change.Property == PlacementModeProperty || + change.Property == HorizontalOffsetProperty || + change.Property == VerticalOffsetProperty || + change.Property == PlacementAnchorProperty || + change.Property == PlacementConstraintAdjustmentProperty || + change.Property == PlacementRectProperty) + { + if (change.Property == PlacementTargetProperty) + { + var newTarget = change.GetNewValue() ?? this.FindLogicalAncestorOfType(); + + if (newTarget is null || newTarget.GetVisualRoot() != _openState.TopLevel) + { + Close(); + return; + } + + _openState.PlacementTarget = newTarget; + } + + UpdateHostPosition(_openState.PopupHost, _openState.PlacementTarget); + } + else if (change.Property == TopmostProperty) + { + _openState.PopupHost.Topmost = change.GetNewValue(); + } + } + } + + private void UpdateHostPosition(IPopupHost popupHost, IControl placementTarget) + { + popupHost.ConfigurePosition( + placementTarget, + PlacementMode, + new Point(HorizontalOffset, VerticalOffset), + PlacementAnchor, + PlacementGravity, + PlacementConstraintAdjustment, + PlacementRect ?? new Rect(default, placementTarget.Bounds.Size)); + } + + private void UpdateHostSizing(IPopupHost popupHost, TopLevel topLevel, IControl placementTarget) + { + var scaleX = 1.0; + var scaleY = 1.0; + + if (InheritsTransform && placementTarget.TransformToVisual(topLevel) is Matrix m) + { + scaleX = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12); + scaleY = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12); + + // Ideally we'd only assign a ScaleTransform here when the scale != 1, but there's + // an issue with LayoutTransformControl in that it sets its LayoutTransform property + // with LocalValue priority in ArrangeOverride in certain cases when LayoutTransform + // is null, which breaks TemplateBindings to this property. Offending commit/line: + // + // https://github.com/AvaloniaUI/Avalonia/commit/6fbe1c2180ef45a940e193f1b4637e64eaab80ed#diff-5344e793df13f462126a8153ef46c44194f244b6890f25501709bae51df97f82R54 + popupHost.Transform = new ScaleTransform(scaleX, scaleY); + } + else + { + popupHost.Transform = null; + } + + popupHost.Width = Width * scaleX; + popupHost.MinWidth = MinWidth * scaleX; + popupHost.MaxWidth = MaxWidth * scaleX; + popupHost.Height = Height * scaleY; + popupHost.MinHeight = MinHeight * scaleY; + popupHost.MaxHeight = MaxHeight * scaleY; + } + private void HandlePositionChange() { if (_openState != null) @@ -560,6 +670,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 +766,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 { @@ -808,6 +933,14 @@ namespace Avalonia.Controls.Primitives } } + private void PlacementTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (_openState is not null && e.Property == Visual.TransformedBoundsProperty) + { + UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget); + } + } + private void WindowLostFocus() { if (IsLightDismissEnabled) @@ -846,15 +979,16 @@ namespace Avalonia.Controls.Primitives private readonly IDisposable _cleanup; private IDisposable? _presenterCleanup; - public PopupOpenState(TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup) + public PopupOpenState(IControl placementTarget, TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup) { + PlacementTarget = placementTarget; TopLevel = topLevel; PopupHost = popupHost; _cleanup = cleanup; } public TopLevel TopLevel { get; } - + public IControl PlacementTarget { get; set; } public IPopupHost PopupHost { get; } public void SetPresenterSubscription(IDisposable? presenterCleanup) diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 340076a407..8d35a91e00 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -45,7 +45,9 @@ Copyright © 2019 Nikita Tsukanov */ using System; +using Avalonia.Metadata; using Avalonia.VisualTree; +using Avalonia.Media; namespace Avalonia.Controls.Primitives.PopupPositioning { @@ -60,6 +62,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// requirement that a popup must intersect with or be at least partially adjacent to its parent /// surface. /// + [Unstable] public struct PopupPositionerParameters { private PopupGravity _gravity; @@ -428,6 +431,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// managed implementation is provided in for platforms /// on which popups can be arbitrarily positioned. /// + [NotClientImplementable] public interface IPopupPositioner { /// @@ -438,13 +442,15 @@ namespace Avalonia.Controls.Primitives.PopupPositioning void Update(PopupPositionerParameters parameters); } + [Unstable] static class PopupPositionerExtensions { public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters, TopLevel topLevel, IVisual target, PlacementMode placement, Point offset, PopupAnchor anchor, PopupGravity gravity, - PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect) + PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect, + FlowDirection flowDirection) { // We need a better way for tracking the last pointer position #pragma warning disable CS0618 // Type or member is obsolete @@ -503,6 +509,32 @@ namespace Avalonia.Controls.Primitives.PopupPositioning else throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); } + + // Invert coordinate system if FlowDirection is RTL + if (flowDirection == FlowDirection.RightToLeft) + { + if ((positionerParameters.Anchor & PopupAnchor.Right) == PopupAnchor.Right) + { + positionerParameters.Anchor ^= PopupAnchor.Right; + positionerParameters.Anchor |= PopupAnchor.Left; + } + else if ((positionerParameters.Anchor & PopupAnchor.Left) == PopupAnchor.Left) + { + positionerParameters.Anchor ^= PopupAnchor.Left; + positionerParameters.Anchor |= PopupAnchor.Right; + } + + if ((positionerParameters.Gravity & PopupGravity.Right) == PopupGravity.Right) + { + positionerParameters.Gravity ^= PopupGravity.Right; + positionerParameters.Gravity |= PopupGravity.Left; + } + else if ((positionerParameters.Gravity & PopupGravity.Left) == PopupGravity.Left) + { + positionerParameters.Gravity ^= PopupGravity.Left; + positionerParameters.Gravity |= PopupGravity.Right; + } + } } } diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index 0f0dd7311d..a80a60350e 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -106,9 +106,9 @@ namespace Avalonia.Controls.Primitives.PopupPositioning { var screens = _popup.Screens; - var targetScreen = screens.FirstOrDefault(s => s.Bounds.Contains(anchorRect.TopLeft)) + var targetScreen = screens.FirstOrDefault(s => s.Bounds.ContainsExclusive(anchorRect.TopLeft)) ?? screens.FirstOrDefault(s => s.Bounds.Intersects(anchorRect)) - ?? screens.FirstOrDefault(s => s.Bounds.Contains(parentGeometry.TopLeft)) + ?? screens.FirstOrDefault(s => s.Bounds.ContainsExclusive(parentGeometry.TopLeft)) ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry)) ?? screens.FirstOrDefault(); diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs index 91ed5d975d..51a21323d9 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs @@ -23,8 +23,9 @@ namespace Avalonia.Controls.Primitives.PopupPositioning public IReadOnlyList Screens => - _parent.Screen.AllScreens.Select(s => new ManagedPopupPositionerScreenInfo( - s.Bounds.ToRect(1), s.WorkingArea.ToRect(1))).ToList(); + _parent.Screen.AllScreens + .Select(s => new ManagedPopupPositionerScreenInfo(s.Bounds.ToRect(1), s.WorkingArea.ToRect(1))) + .ToArray(); public Rect ParentClientAreaScreenGeometry { diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index be447ea512..f7bf7c1a27 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -1,13 +1,11 @@ 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; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.VisualTree; -using JetBrains.Annotations; namespace Avalonia.Controls.Primitives { @@ -16,7 +14,12 @@ namespace Avalonia.Controls.Primitives /// public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { - private readonly TopLevel _parent; + /// + /// Defines the property. + /// + public static readonly StyledProperty TransformProperty = + AvaloniaProperty.Register(nameof(Transform)); + private PopupPositionerParameters _positionerParameters; /// @@ -44,9 +47,9 @@ namespace Avalonia.Controls.Primitives /// The dependency resolver to use. If null the default dependency resolver will be used. /// public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver? dependencyResolver) - : base(ValidatingPopupImpl.Wrap(impl), dependencyResolver) + : base(impl, dependencyResolver) { - _parent = parent; + ParentTopLevel = parent; } /// @@ -54,6 +57,15 @@ namespace Avalonia.Controls.Primitives /// public new IPopupImpl? PlatformImpl => (IPopupImpl?)base.PlatformImpl; + /// + /// Gets or sets a transform that will be applied to the popup. + /// + public Transform? Transform + { + get => GetValue(TransformProperty); + set => SetValue(TransformProperty, value); + } + /// /// Gets the parent control in the event route. /// @@ -72,6 +84,8 @@ namespace Avalonia.Controls.Primitives /// IStyleHost? IStyleHost.StylingParent => Parent; + public TopLevel ParentTopLevel { get; } + /// public void Dispose() { @@ -90,8 +104,8 @@ namespace Avalonia.Controls.Primitives PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All, Rect? rect = null) { - _positionerParameters.ConfigurePosition(_parent, target, - placement, offset, anchor, gravity, constraintAdjustment, rect); + _positionerParameters.ConfigurePosition(ParentTopLevel, target, + placement, offset, anchor, gravity, constraintAdjustment, rect, FlowDirection); if (_positionerParameters.Size != default) UpdatePosition(); @@ -101,27 +115,6 @@ namespace Avalonia.Controls.Primitives IVisual IPopupHost.HostedVisualTreeRoot => this; - public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, - StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, - StyledProperty maxHeightProperty, StyledProperty topmostProperty) - { - var bindings = new List(); - - void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); - Bind(WidthProperty, widthProperty); - Bind(MinWidthProperty, minWidthProperty); - Bind(MaxWidthProperty, maxWidthProperty); - Bind(HeightProperty, heightProperty); - Bind(MinHeightProperty, minHeightProperty); - Bind(MaxHeightProperty, maxHeightProperty); - Bind(TopmostProperty, topmostProperty); - return Disposable.Create(() => - { - foreach (var x in bindings) - x.Dispose(); - }); - } - protected override Size MeasureOverride(Size availableSize) { var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity; @@ -168,5 +161,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..e5c3392faf 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 { @@ -190,13 +194,13 @@ namespace Avalonia.Controls.Primitives } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == OrientationProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } else if (change.Property == AllowAutoHideProperty) { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index c690726e71..ea20247b4b 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -94,7 +94,7 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty IsTextSearchEnabledProperty = - AvaloniaProperty.Register(nameof(IsTextSearchEnabled), false); + AvaloniaProperty.Register(nameof(IsTextSearchEnabled), false); /// /// Event that should be raised by items that implement to @@ -111,14 +111,14 @@ namespace Avalonia.Controls.Primitives /// public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register( - "SelectionChanged", + nameof(SelectionChanged), RoutingStrategies.Bubble); /// /// Defines the property. /// public static readonly StyledProperty WrapSelectionProperty = - AvaloniaProperty.Register(nameof(WrapSelection), defaultValue: false); + AvaloniaProperty.Register(nameof(WrapSelection), defaultValue: false); private static readonly IList Empty = Array.Empty(); private string _textSearchTerm = string.Empty; @@ -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, @@ -501,12 +501,16 @@ namespace Avalonia.Controls.Primitives /// enabled. /// /// The property. - /// The new binding value for the property. - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + /// The current data binding state. + /// The current data binding error, if any. + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == SelectedItemProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } @@ -533,9 +537,9 @@ namespace Avalonia.Controls.Primitives bool Match(ItemContainerInfo info) { - if (info.ContainerControl.IsSet(TextSearch.TextProperty)) + if (info.ContainerControl is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty)) { - var searchText = info.ContainerControl.GetValue(TextSearch.TextProperty); + var searchText = ao.GetValue(TextSearch.TextProperty); if (searchText?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true) { @@ -585,7 +589,7 @@ namespace Avalonia.Controls.Primitives } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -595,7 +599,7 @@ namespace Avalonia.Controls.Primitives } if (change.Property == ItemsProperty && _updateState is null && _selection is object) { - var newValue = change.NewValue.GetValueOrDefault(); + var newValue = change.GetNewValue(); _selection.Source = newValue; if (newValue is null) @@ -605,7 +609,7 @@ namespace Avalonia.Controls.Primitives } else if (change.Property == SelectionModeProperty && _selection is object) { - var newValue = change.NewValue.GetValueOrDefault(); + var newValue = change.GetNewValue(); _selection.SingleSelect = !newValue.HasAllFlags(SelectionMode.Multiple); } else if (change.Property == WrapSelectionProperty) @@ -845,8 +849,8 @@ namespace Avalonia.Controls.Primitives { var ev = new SelectionChangedEventArgs( SelectionChangedEvent, - e.DeselectedItems.ToList(), - e.SelectedItems.ToList()); + e.DeselectedItems.ToArray(), + e.SelectedItems.ToArray()); RaiseEvent(ev); } } @@ -988,7 +992,7 @@ namespace Avalonia.Controls.Primitives RaiseEvent(new SelectionChangedEventArgs( SelectionChangedEvent, Array.Empty(), - Selection.SelectedItems.ToList())); + Selection.SelectedItems.ToArray())); } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index af681d6930..db029d38c0 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Documents; using Avalonia.Controls.Templates; using Avalonia.Interactivity; using Avalonia.Logging; @@ -41,31 +42,37 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty FontFamilyProperty = - TextBlock.FontFamilyProperty.AddOwner(); + TextElement.FontFamilyProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty FontSizeProperty = - TextBlock.FontSizeProperty.AddOwner(); + TextElement.FontSizeProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty FontStyleProperty = - TextBlock.FontStyleProperty.AddOwner(); + TextElement.FontStyleProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty FontWeightProperty = - TextBlock.FontWeightProperty.AddOwner(); + TextElement.FontWeightProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontStretchProperty = + TextElement.FontStretchProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty ForegroundProperty = - TextBlock.ForegroundProperty.AddOwner(); + TextElement.ForegroundProperty.AddOwner(); /// /// Defines the property. @@ -90,7 +97,7 @@ namespace Avalonia.Controls.Primitives /// public static readonly RoutedEvent TemplateAppliedEvent = RoutedEvent.Register( - "TemplateApplied", + nameof(TemplateApplied), RoutingStrategies.Direct); private IControlTemplate? _appliedTemplate; @@ -185,6 +192,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(FontWeightProperty, value); } } + /// + /// Gets or sets the font stretch used to draw the control's text. + /// + public FontStretch FontStretch + { + get { return GetValue(FontStretchProperty); } + set { SetValue(FontStretchProperty, value); } + } + /// /// Gets or sets the brush used to draw the control's text and other foreground elements. /// @@ -340,6 +356,11 @@ namespace Avalonia.Controls.Primitives base.OnDetachedFromLogicalTree(e); } + /// + /// Called when the control's template is applied. + /// In simple terms, this means the method is called just before the control is displayed. + /// + /// The event args. protected virtual void OnApplyTemplate(TemplateAppliedEventArgs e) { } diff --git a/src/Avalonia.Controls/Primitives/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/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index f8d6046101..14ec7a2849 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -44,7 +44,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(IsDirectionReversed)); public static readonly StyledProperty IgnoreThumbDragProperty = - AvaloniaProperty.Register(nameof(IsThumbDragHandled)); + AvaloniaProperty.Register(nameof(IgnoreThumbDrag)); private double _minimum; private double _maximum = 100.0; @@ -118,7 +118,7 @@ namespace Avalonia.Controls.Primitives set { SetValue(IsDirectionReversedProperty, value); } } - public bool IsThumbDragHandled + public bool IgnoreThumbDrag { get { return GetValue(IgnoreThumbDragProperty); } set { SetValue(IgnoreThumbDragProperty, value); } @@ -291,13 +291,13 @@ namespace Avalonia.Controls.Primitives return arrangeSize; } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == OrientationProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } } @@ -442,7 +442,7 @@ namespace Avalonia.Controls.Primitives private void ThumbDragged(object? sender, VectorEventArgs e) { - if (IsThumbDragHandled) + if (IgnoreThumbDrag) return; Value = MathUtilities.Clamp( diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 017a053c48..1075328c67 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)); @@ -175,17 +178,17 @@ namespace Avalonia.Controls return base.ArrangeOverride(finalSize); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IsIndeterminateProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault(), null); + UpdatePseudoClasses(change.GetNewValue(), null); } else if (change.Property == OrientationProperty) { - UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(null, change.GetNewValue()); } } diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index 05561a38ef..0f3da91107 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -1,11 +1,7 @@ -using System.Runtime.CompilerServices; using Avalonia.Metadata; -[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[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 +10,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/Remote/RemoteServer.cs b/src/Avalonia.Controls/Remote/RemoteServer.cs index f4cc91a0e6..2cf2d2b97d 100644 --- a/src/Avalonia.Controls/Remote/RemoteServer.cs +++ b/src/Avalonia.Controls/Remote/RemoteServer.cs @@ -1,11 +1,12 @@ using System; using Avalonia.Controls.Embedding; using Avalonia.Controls.Remote.Server; -using Avalonia.Platform; +using Avalonia.Metadata; using Avalonia.Remote.Protocol; namespace Avalonia.Controls.Remote { + [Unstable] public class RemoteServer { private EmbeddableControlRoot _topLevel; diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index c9fd1dc3b8..e800f2f4b0 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -6,6 +6,7 @@ using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Layout; +using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Input; @@ -18,6 +19,7 @@ using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat; namespace Avalonia.Controls.Remote.Server { + [Unstable] public class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface { private readonly IAvaloniaRemoteTransportConnection _transport; diff --git a/src/Avalonia.Controls/RepeatButton.cs b/src/Avalonia.Controls/RepeatButton.cs index 0415a78721..6a62172934 100644 --- a/src/Avalonia.Controls/RepeatButton.cs +++ b/src/Avalonia.Controls/RepeatButton.cs @@ -13,13 +13,13 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IntervalProperty = - AvaloniaProperty.Register(nameof(Interval), 100); + AvaloniaProperty.Register(nameof(Interval), 100); /// /// Defines the property. /// public static readonly StyledProperty DelayProperty = - AvaloniaProperty.Register(nameof(Delay), 300); + AvaloniaProperty.Register(nameof(Delay), 300); private DispatcherTimer? _repeatTimer; @@ -70,11 +70,11 @@ namespace Avalonia.Controls _repeatTimer?.Stop(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); - if (change.Property == IsPressedProperty && change.NewValue.GetValueOrDefault() == false) + if (change.Property == IsPressedProperty && change.GetNewValue() == false) { StopTimer(); } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 09c0e58332..3f42d95deb 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber + public class ItemsRepeater : Panel, IChildIndexProvider { /// /// Defines the property. @@ -60,6 +60,7 @@ namespace Avalonia.Controls private readonly ViewManager _viewManager; private readonly ViewportManager _viewportManager; + private readonly TargetWeakEventSubscriber _layoutWeakSubscriber; private IEnumerable? _items; private VirtualizingLayoutContext? _layoutContext; private EventHandler? _childIndexChanged; @@ -74,6 +75,15 @@ namespace Avalonia.Controls /// public ItemsRepeater() { + _layoutWeakSubscriber = new TargetWeakEventSubscriber( + this, static (target, _, ev, _) => + { + if (ev == AttachedLayout.ArrangeInvalidatedWeakEvent) + target.InvalidateArrange(); + else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) + target.InvalidateMeasure(); + }); + _viewManager = new ViewManager(this); _viewportManager = new ViewportManager(this); KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once); @@ -257,10 +267,9 @@ namespace Avalonia.Controls internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); - internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element) + internal static VirtualizationInfo? TryGetVirtualizationInfo(IControl element) { - var value = element.GetValue(VirtualizationInfoProperty); - return value; + return (element as AvaloniaObject)?.GetValue(VirtualizationInfoProperty); } internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element) @@ -277,15 +286,20 @@ namespace Avalonia.Controls internal static VirtualizationInfo GetVirtualizationInfo(IControl element) { - var result = element.GetValue(VirtualizationInfoProperty); - - if (result == null) + if (element is AvaloniaObject ao) { - result = new VirtualizationInfo(); - element.SetValue(VirtualizationInfoProperty, result); + var result = ao.GetValue(VirtualizationInfoProperty); + + if (result == null) + { + result = new VirtualizationInfo(); + ao.SetValue(VirtualizationInfoProperty, result); + } + + return result; } - return result; + throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); } private protected override void InvalidateMeasureOnChildrenChanged() @@ -391,11 +405,7 @@ namespace Avalonia.Controls var newBounds = element.Bounds; virtInfo.ArrangeBounds = newBounds; - if (!virtInfo.IsRegisteredAsAnchorCandidate) - { - _viewportManager.RegisterScrollAnchorCandidate(element); - virtInfo.IsRegisteredAsAnchorCandidate = true; - } + _viewportManager.RegisterScrollAnchorCandidate(element, virtInfo); } } @@ -420,12 +430,11 @@ namespace Avalonia.Controls _viewportManager.ResetScrollers(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == ItemsProperty) { - var oldEnumerable = change.OldValue.GetValueOrDefault(); - var newEnumerable = change.NewValue.GetValueOrDefault(); + var (oldEnumerable, newEnumerable) = change.GetOldAndNewValue(); if (oldEnumerable != newEnumerable) { @@ -440,23 +449,21 @@ namespace Avalonia.Controls } else if (change.Property == ItemTemplateProperty) { - OnItemTemplateChanged( - change.OldValue.GetValueOrDefault(), - change.NewValue.GetValueOrDefault()); + var (oldvalue, newValue) = change.GetOldAndNewValue(); + OnItemTemplateChanged(oldvalue, newValue); } else if (change.Property == LayoutProperty) { - OnLayoutChanged( - change.OldValue.GetValueOrDefault(), - change.NewValue.GetValueOrDefault()); + var (oldvalue, newValue) = change.GetOldAndNewValue(); + OnLayoutChanged(oldvalue, newValue); } else if (change.Property == HorizontalCacheLengthProperty) { - _viewportManager.HorizontalCacheLength = change.NewValue.GetValueOrDefault(); + _viewportManager.HorizontalCacheLength = change.GetNewValue(); } else if (change.Property == VerticalCacheLengthProperty) { - _viewportManager.VerticalCacheLength = change.NewValue.GetValueOrDefault(); + _viewportManager.VerticalCacheLength = change.GetNewValue(); } base.OnPropertyChanged(change); @@ -480,7 +487,7 @@ namespace Avalonia.Controls _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset); _viewManager.ClearElement(element, isClearedDueToCollectionChange); - _viewportManager.OnElementCleared(element); + _viewportManager.OnElementCleared(element, GetVirtualizationInfo(element)); } private int GetElementIndexImpl(IControl element) @@ -491,7 +498,7 @@ namespace Avalonia.Controls if (parent == this) { var virtInfo = TryGetVirtualizationInfo(element); - return _viewManager.GetElementIndex(virtInfo); + return _viewManager.GetElementIndex(virtInfo!); } return -1; @@ -728,8 +735,8 @@ namespace Avalonia.Controls { oldValue.UninitializeForContext(LayoutContext); - AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this); - AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this); + AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber); + AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -747,8 +754,8 @@ namespace Avalonia.Controls { newValue.InitializeForContext(LayoutContext); - AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this); - AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this); + AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); + AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; @@ -798,15 +805,7 @@ namespace Avalonia.Controls { _viewportManager.OnBringIntoViewRequested(e); } - - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) - { - if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent) - InvalidateArrange(); - else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) - InvalidateMeasure(); - } - + private VirtualizingLayoutContext GetLayoutContext() { if (_layoutContext == null) diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls/Repeater/RecyclePool.cs index 9e2da81f99..cf2b40836e 100644 --- a/src/Avalonia.Controls/Repeater/RecyclePool.cs +++ b/src/Avalonia.Controls/Repeater/RecyclePool.cs @@ -80,8 +80,8 @@ namespace Avalonia.Controls return null; } - internal string GetReuseKey(IControl element) => element.GetValue(ReuseKeyProperty); - internal void SetReuseKey(IControl element, string value) => element.SetValue(ReuseKeyProperty, value); + internal string GetReuseKey(IControl element) => ((Control)element).GetValue(ReuseKeyProperty); + internal void SetReuseKey(IControl element, string value) => ((Control)element).SetValue(ReuseKeyProperty, value); private IPanel? EnsureOwnerIsPanelOrNull(IControl? owner) { diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index b28753b518..12db48a2c2 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls if (madeAnchor != null) { var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor); - if (anchorVirtInfo.Index == index) + if (anchorVirtInfo!.Index == index) { element = madeAnchor; } @@ -60,12 +60,12 @@ namespace Avalonia.Controls var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); if (suppressAutoRecycle) { - virtInfo.AutoRecycleCandidate = false; + virtInfo!.AutoRecycleCandidate = false; Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "GetElement: {Index} Not AutoRecycleCandidate:", virtInfo.Index); } else { - virtInfo.AutoRecycleCandidate = true; + virtInfo!.AutoRecycleCandidate = true; virtInfo.KeepAlive = true; Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "GetElement: {Index} AutoRecycleCandidate:", virtInfo.Index); } diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index ec25fcb265..7f03cc575e 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -249,9 +249,10 @@ namespace Avalonia.Controls virtInfo.IsRegisteredAsAnchorCandidate = false; } - public void OnElementCleared(IControl element) + public void OnElementCleared(IControl element, VirtualizationInfo virtInfo) { _scroller?.UnregisterAnchorCandidate(element); + virtInfo.IsRegisteredAsAnchorCandidate = false; } public void OnOwnerMeasuring() @@ -358,9 +359,12 @@ namespace Avalonia.Controls { foreach (var child in _owner.Children) { - if (child != targetChild) + var info = ItemsRepeater.GetVirtualizationInfo(child); + + if (child != targetChild && info.IsRegisteredAsAnchorCandidate) { _scroller.UnregisterAnchorCandidate(child); + info.IsRegisteredAsAnchorCandidate = false; } } } @@ -377,9 +381,13 @@ namespace Avalonia.Controls } } - public void RegisterScrollAnchorCandidate(IControl element) + public void RegisterScrollAnchorCandidate(IControl element, VirtualizationInfo virtInfo) { - _scroller?.RegisterAnchorCandidate(element); + if (!virtInfo.IsRegisteredAsAnchorCandidate) + { + _scroller?.RegisterAnchorCandidate(element); + virtInfo.IsRegisteredAsAnchorCandidate = true; + } } private IControl? GetImmediateChildOfRepeater(IControl descendant) @@ -405,15 +413,18 @@ namespace Avalonia.Controls _isBringIntoViewInProgress = false; _makeAnchorElement = null; + // Undo the anchor deregistrations done by OnBringIntoViewRequested. if (_scroller is object) { foreach (var child in _owner.Children) { var info = ItemsRepeater.GetVirtualizationInfo(child); - if (info.IsRealized && info.IsHeldByLayout) + // The item brought into view is still registered - don't register it more than once. + if (info.IsRealized && info.IsHeldByLayout && !info.IsRegisteredAsAnchorCandidate) { _scroller.RegisterAnchorCandidate(child); + info.IsRegisteredAsAnchorCandidate = true; } } } @@ -430,7 +441,13 @@ namespace Avalonia.Controls { foreach (var child in _owner.Children) { - _scroller.UnregisterAnchorCandidate(child); + var info = ItemsRepeater.GetVirtualizationInfo(child); + + if (info.IsRegisteredAsAnchorCandidate) + { + _scroller.UnregisterAnchorCandidate(child); + info.IsRegisteredAsAnchorCandidate = false; + } } _scroller = null; diff --git a/src/Avalonia.Controls/Screens.cs b/src/Avalonia.Controls/Screens.cs index ef438576a7..a554f82f61 100644 --- a/src/Avalonia.Controls/Screens.cs +++ b/src/Avalonia.Controls/Screens.cs @@ -2,54 +2,45 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Platform; -using Avalonia.Utilities; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls { public class Screens { - private readonly IScreenImpl? _iScreenImpl; + private readonly IScreenImpl _iScreenImpl; public int ScreenCount => _iScreenImpl?.ScreenCount ?? 0; public IReadOnlyList All => _iScreenImpl?.AllScreens ?? Array.Empty(); public Screen? Primary => All.FirstOrDefault(x => x.Primary); - public Screens(IScreenImpl? iScreenImpl) + public Screens(IScreenImpl iScreenImpl) { _iScreenImpl = iScreenImpl; } - public Screen? ScreenFromBounds(PixelRect bounds){ - - Screen? currMaxScreen = null; - double maxAreaSize = 0; - foreach (Screen screen in All) - { - double left = MathUtilities.Clamp(bounds.X, screen.Bounds.X, screen.Bounds.X + screen.Bounds.Width); - double top = MathUtilities.Clamp(bounds.Y, screen.Bounds.Y, screen.Bounds.Y + screen.Bounds.Height); - double right = MathUtilities.Clamp(bounds.X + bounds.Width, screen.Bounds.X, screen.Bounds.X + screen.Bounds.Width); - double bottom = MathUtilities.Clamp(bounds.Y + bounds.Height, screen.Bounds.Y, screen.Bounds.Y + screen.Bounds.Height); - double area = (right - left) * (bottom - top); - if (area > maxAreaSize) - { - maxAreaSize = area; - currMaxScreen = screen; - } - } - - return currMaxScreen; + public Screen? ScreenFromBounds(PixelRect bounds) + { + return _iScreenImpl.ScreenFromRect(bounds); } - public Screen? ScreenFromPoint(PixelPoint point) + public Screen? ScreenFromWindow(IWindowBaseImpl window) { - return All.FirstOrDefault(x => x.Bounds.Contains(point)); + return _iScreenImpl.ScreenFromWindow(window); + } + + public Screen? ScreenFromPoint(PixelPoint point) + { + return _iScreenImpl.ScreenFromPoint(point); } public Screen? ScreenFromVisual(IVisual visual) { var tl = visual.PointToScreen(visual.Bounds.TopLeft); var br = visual.PointToScreen(visual.Bounds.BottomRight); + return ScreenFromBounds(new PixelRect(tl, br)); } } diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 7c4b87e66a..535f9ae43e 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -1,5 +1,7 @@ using System; using System.Reactive.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -8,8 +10,10 @@ using Avalonia.Interactivity; namespace Avalonia.Controls { /// - /// A control scrolls its content if the content is bigger than the space available. + /// A control which scrolls its content if the content is bigger than the space available. /// + [TemplatePart("PART_HorizontalScrollBar", typeof(ScrollBar))] + [TemplatePart("PART_VerticalScrollBar", typeof(ScrollBar))] public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider { /// @@ -762,6 +766,11 @@ namespace Avalonia.Controls _scrollBarExpandSubscription = SubscribeToScrollBars(e); } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ScrollViewerAutomationPeer(this); + } + private IDisposable? SubscribeToScrollBars(TemplateAppliedEventArgs e) { static IObservable? GetExpandedObservable(ScrollBar? scrollBar) diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index 40c6f63ed8..d92ffb0d1a 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -182,8 +182,8 @@ namespace Avalonia.Controls.Selection try { var items = WritableSelectedItems; - var deselected = e.DeselectedItems.ToList(); - var selected = e.SelectedItems.ToList(); + var deselected = e.DeselectedItems.ToArray(); + var selected = e.SelectedItems.ToArray(); _ignoreSelectedItemsChanges = true; diff --git a/src/Avalonia.Controls/Shapes/Rectangle.cs b/src/Avalonia.Controls/Shapes/Rectangle.cs index f3db2644a9..d60180bab4 100644 --- a/src/Avalonia.Controls/Shapes/Rectangle.cs +++ b/src/Avalonia.Controls/Shapes/Rectangle.cs @@ -2,19 +2,139 @@ using Avalonia.Media; namespace Avalonia.Controls.Shapes { + /// + /// Represents a rectangle with optional rounded corners. + /// public class Rectangle : Shape { + private const double PiOver2 = 1.57079633; // 90 deg to rad + + /// + /// Defines the property. + /// + public static readonly StyledProperty RadiusXProperty = + AvaloniaProperty.Register(nameof(RadiusX)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty RadiusYProperty = + AvaloniaProperty.Register(nameof(RadiusY)); + static Rectangle() { - AffectsGeometry(BoundsProperty, StrokeThicknessProperty); + AffectsGeometry( + BoundsProperty, + RadiusXProperty, + RadiusYProperty, + StrokeThicknessProperty); + } + + /// + /// Gets or sets the radius on the X-axis used to round the corners of the rectangle. + /// Corner radii are represented by an ellipse so this is the X-axis width of the ellipse. + /// + public double RadiusX + { + get => GetValue(RadiusXProperty); + set => SetValue(RadiusXProperty, value); + } + + /// + /// Gets or sets the radius on the Y-axis used to round the corners of the rectangle. + /// Corner radii are represented by an ellipse so this is the Y-axis height of the ellipse. + /// + public double RadiusY + { + get => GetValue(RadiusYProperty); + set => SetValue(RadiusYProperty, value); } + /// protected override Geometry CreateDefiningGeometry() { - var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); - return new RectangleGeometry(rect); + // TODO: If RectangleGeometry ever supports RadiusX/Y like in WPF, + // this code can be removed/combined with that implementation + + double x = RadiusX; + double y = RadiusY; + + if (x == 0 && y == 0) + { + // Optimization when there are no corner radii + var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); + return new RectangleGeometry(rect); + } + else + { + var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); + var geometry = new StreamGeometry(); + var arcSize = new Size(x, y); + + using (StreamGeometryContext context = geometry.Open()) + { + // The rectangle is constructed as follows: + // + // (origin) + // Corner 4 Corner 1 + // Top/Left Line 1 Top/Right + // \_ __________ _/ + // | | + // Line 4 | | Line 2 + // _ |__________| _ + // / Line 3 \ + // Corner 3 Corner 2 + // Bottom/Left Bottom/Right + // + // - Lines 1,3 follow the deflated rectangle bounds minus RadiusX + // - Lines 2,4 follow the deflated rectangle bounds minus RadiusY + // - All corners are constructed using elliptical arcs + + // Line 1 + Corner 1 + context.BeginFigure(new Point(rect.Left + x, rect.Top), true); + context.LineTo(new Point(rect.Right - x, rect.Top)); + context.ArcTo( + new Point(rect.Right, rect.Top + y), + arcSize, + rotationAngle: PiOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + // Line 2 + Corner 2 + context.LineTo(new Point(rect.Right, rect.Bottom - y)); + context.ArcTo( + new Point(rect.Right - x, rect.Bottom), + arcSize, + rotationAngle: PiOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + // Line 3 + Corner 3 + context.LineTo(new Point(rect.Left + x, rect.Bottom)); + context.ArcTo( + new Point(rect.Left, rect.Bottom - y), + arcSize, + rotationAngle: PiOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + // Line 4 + Corner 4 + context.LineTo(new Point(rect.Left, rect.Top + y)); + context.ArcTo( + new Point(rect.Left + x, rect.Top), + arcSize, + rotationAngle: PiOver2, + isLargeArc: false, + SweepDirection.Clockwise); + + context.EndFigure(true); + } + + return geometry; + } } + /// protected override Size MeasureOverride(Size availableSize) { return new Size(StrokeThickness, StrokeThickness); diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 87de04f92f..be87705b54 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 { @@ -71,7 +76,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty TickPlacementProperty = - AvaloniaProperty.Register(nameof(TickPlacement), 0d); + AvaloniaProperty.Register(nameof(TickPlacement), 0d); /// /// Defines the property. @@ -105,6 +110,7 @@ namespace Avalonia.Controls RoutingStrategies.Bubble); ValueProperty.OverrideMetadata(new DirectPropertyMetadata(enableDataValidation: true)); + AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Slider); } /// @@ -191,7 +197,7 @@ namespace Avalonia.Controls if (_track != null) { - _track.IsThumbDragHandled = true; + _track.IgnoreThumbDrag = true; } if (_decreaseButton != null) @@ -355,21 +361,24 @@ namespace Avalonia.Controls Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue; } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == ValueProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == OrientationProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } } diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index f1d07b2679..f39064435d 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -1,7 +1,6 @@ using System; using System.Windows.Input; using Avalonia.Controls.Metadata; -using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; @@ -13,6 +12,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 { @@ -227,6 +228,8 @@ namespace Avalonia.Controls /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + base.OnApplyTemplate(e); + UnregisterEvents(); UnregisterFlyoutEvents(Flyout); @@ -275,19 +278,21 @@ namespace Avalonia.Controls } /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (e.Property == CommandProperty) { if (_isAttachedToLogicalTree) { // Must unregister events here while a reference to the old command still exists - if (e.OldValue.GetValueOrDefault() is ICommand oldCommand) + var (oldValue, newValue) = e.GetOldAndNewValue(); + + if (oldValue is ICommand oldCommand) { oldCommand.CanExecuteChanged -= CanExecuteChanged; } - if (e.NewValue.GetValueOrDefault() is ICommand newCommand) + if (newValue is ICommand newCommand) { newCommand.CanExecuteChanged += CanExecuteChanged; } @@ -301,8 +306,7 @@ namespace Avalonia.Controls } else if (e.Property == FlyoutProperty) { - var oldFlyout = e.OldValue.GetValueOrDefault() as FlyoutBase; - var newFlyout = e.NewValue.GetValueOrDefault() as FlyoutBase; + var (oldFlyout, newFlyout) = e.GetOldAndNewValue(); // If flyout is changed while one is already open, make sure we // close the old one first @@ -314,16 +318,9 @@ namespace Avalonia.Controls } // Must unregister events here while a reference to the old flyout still exists - if (oldFlyout != null) - { - UnregisterFlyoutEvents(oldFlyout); - } - - if (newFlyout != null) - { - RegisterFlyoutEvents(newFlyout); - } + UnregisterFlyoutEvents(oldFlyout); + RegisterFlyoutEvents(newFlyout); UpdatePseudoClasses(); } @@ -415,6 +412,22 @@ namespace Avalonia.Controls } } + /// + /// Invoked when the split button's flyout is opened. + /// + protected virtual void OnFlyoutOpened() + { + // Available for derived types + } + + /// + /// Invoked when the split button's flyout is closed. + /// + protected virtual void OnFlyoutClosed() + { + // Available for derived types + } + //////////////////////////////////////////////////////////////////////// // Event Handling //////////////////////////////////////////////////////////////////////// @@ -464,6 +477,8 @@ namespace Avalonia.Controls { _isFlyoutOpen = true; UpdatePseudoClasses(); + + OnFlyoutOpened(); } } @@ -479,6 +494,8 @@ namespace Avalonia.Controls { _isFlyoutOpen = false; UpdatePseudoClasses(); + + OnFlyoutClosed(); } } } diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index 879c1aa6e1..cd34f8060a 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -90,7 +90,7 @@ namespace Avalonia.Controls //////////////////////////////////////////////////////////////////////// /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (e.Property == IsCheckedProperty) { diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index d2161deb6e..532cb1d329 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")] @@ -137,7 +138,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty PaneTemplateProperty = - AvaloniaProperty.Register(nameof(PaneTemplate)); + AvaloniaProperty.Register(nameof(PaneTemplate)); /// /// Defines the property diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index feb425a9c3..50c48d2bb0 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -123,7 +123,7 @@ namespace Avalonia.Controls index = Children.Count - 1; break; case NavigationDirection.Next: - if (index != -1) ++index; + ++index; break; case NavigationDirection.Previous: if (index != -1) --index; diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index 4a9e745e30..093f10be51 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls /// Gets or sets a collection of filters which determine the types of files displayed in an /// or an . /// - public List Filters { get; set; } = new List(); + public List? Filters { get; set; } = new List(); /// /// Gets or sets initial file name that is displayed when the dialog is opened. 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/Templates/IDataTemplateHost.cs b/src/Avalonia.Controls/Templates/IDataTemplateHost.cs index 61986a0661..ce763c3336 100644 --- a/src/Avalonia.Controls/Templates/IDataTemplateHost.cs +++ b/src/Avalonia.Controls/Templates/IDataTemplateHost.cs @@ -1,9 +1,11 @@ - +using Avalonia.Metadata; + namespace Avalonia.Controls.Templates { /// /// Defines an element that has a collection. /// + [NotClientImplementable] public interface IDataTemplateHost { /// diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index ed012bd8b1..bbe6aeb7ee 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -1,16 +1,20 @@ -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 { /// /// A control that displays a block of text. /// - public class TextBlock : Control + public class TextBlock : Control, IInlineHost { /// /// Defines the property. @@ -24,69 +28,69 @@ namespace Avalonia.Controls public static readonly StyledProperty PaddingProperty = Decorator.PaddingProperty.AddOwner(); - // TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner - // them into TextBlock. - /// /// Defines the property. /// - public static readonly AttachedProperty FontFamilyProperty = - AvaloniaProperty.RegisterAttached( - nameof(FontFamily), - defaultValue: FontFamily.Default, - inherits: true); + public static readonly StyledProperty FontFamilyProperty = + TextElement.FontFamilyProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty FontSizeProperty = - AvaloniaProperty.RegisterAttached( - nameof(FontSize), - defaultValue: 12, - inherits: true); + public static readonly StyledProperty FontSizeProperty = + TextElement.FontSizeProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty FontStyleProperty = - AvaloniaProperty.RegisterAttached( - nameof(FontStyle), - inherits: true); + public static readonly StyledProperty FontStyleProperty = + TextElement.FontStyleProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontWeightProperty = + TextElement.FontWeightProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty FontWeightProperty = - AvaloniaProperty.RegisterAttached( - nameof(FontWeight), - inherits: true, - defaultValue: FontWeight.Normal); + public static readonly StyledProperty FontStretchProperty = + TextElement.FontStretchProperty.AddOwner(); /// /// Defines the property. /// - public static readonly AttachedProperty ForegroundProperty = - AvaloniaProperty.RegisterAttached( - nameof(Foreground), - Brushes.Black, - inherits: true); + public static readonly StyledProperty ForegroundProperty = + TextElement.ForegroundProperty.AddOwner(); + + /// + /// DependencyProperty for property. + /// + public static readonly AttachedProperty BaselineOffsetProperty = + AvaloniaProperty.RegisterAttached( + nameof(BaselineOffset), + 0, + true); /// /// Defines the property. /// - public static readonly StyledProperty LineHeightProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty LineHeightProperty = + AvaloniaProperty.RegisterAttached( nameof(LineHeight), double.NaN, - validate: IsValidLineHeight); + validate: IsValidLineHeight, + inherits: true); /// /// Defines the property. /// - public static readonly StyledProperty MaxLinesProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty MaxLinesProperty = + AvaloniaProperty.RegisterAttached( nameof(MaxLines), - validate: IsValidMaxLines); + validate: IsValidMaxLines, + inherits: true); /// /// Defines the property. @@ -97,23 +101,35 @@ namespace Avalonia.Controls o => o.Text, (o, v) => o.Text = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty InlinesProperty = + AvaloniaProperty.RegisterDirect( + nameof(Inlines), + o => o.Inlines); + /// /// Defines the property. /// - public static readonly StyledProperty TextAlignmentProperty = - AvaloniaProperty.Register(nameof(TextAlignment)); + public static readonly AttachedProperty TextAlignmentProperty = + AvaloniaProperty.RegisterAttached(nameof(TextAlignment), + inherits: true); /// /// Defines the property. /// - public static readonly StyledProperty TextWrappingProperty = - AvaloniaProperty.Register(nameof(TextWrapping)); + public static readonly AttachedProperty TextWrappingProperty = + AvaloniaProperty.RegisterAttached(nameof(TextWrapping), + inherits: true); /// /// Defines the property. /// - public static readonly StyledProperty TextTrimmingProperty = - AvaloniaProperty.Register(nameof(TextTrimming)); + public static readonly AttachedProperty TextTrimmingProperty = + AvaloniaProperty.RegisterAttached(nameof(TextTrimming), + defaultValue: TextTrimming.None, + inherits: true); /// /// Defines the property. @@ -121,7 +137,6 @@ namespace Avalonia.Controls public static readonly StyledProperty TextDecorationsProperty = AvaloniaProperty.Register(nameof(TextDecorations)); - private string? _text; private TextLayout? _textLayout; private Size _constraint; @@ -140,13 +155,13 @@ namespace Avalonia.Controls /// public TextBlock() { - _text = string.Empty; + Inlines = new InlineCollection(this, this); } /// /// Gets the used to render the text. /// - public TextLayout? TextLayout + public TextLayout TextLayout { get { @@ -175,15 +190,32 @@ namespace Avalonia.Controls /// /// Gets or sets the text. /// - [Content] public string? Text { - get { return _text; } - set { SetAndRaise(TextProperty, ref _text, value); } + get => Inlines.Text; + set + { + var old = Text; + + if (value == old) + { + return; + } + + Inlines.Text = value; + + RaisePropertyChanged(TextProperty, old, value); + } } /// - /// Gets or sets the font family. + /// Gets the inlines. + /// + [Content] + public InlineCollection Inlines { get; } + + /// + /// Gets or sets the font family used to draw the control's text. /// public FontFamily FontFamily { @@ -192,7 +224,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the font size. + /// Gets or sets the size of the control's text in points. /// public double FontSize { @@ -201,7 +233,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the font style. + /// Gets or sets the font style used to draw the control's text. /// public FontStyle FontStyle { @@ -210,7 +242,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the font weight. + /// Gets or sets the font weight used to draw the control's text. /// public FontWeight FontWeight { @@ -219,7 +251,16 @@ namespace Avalonia.Controls } /// - /// Gets or sets a brush used to paint the text. + /// Gets or sets the font stretch used to draw the control's text. + /// + public FontStretch FontStretch + { + get { return GetValue(FontStretchProperty); } + set { SetValue(FontStretchProperty, value); } + } + + /// + /// Gets or sets the brush used to draw the control's text and other foreground elements. /// public IBrush? Foreground { @@ -280,112 +321,193 @@ namespace Avalonia.Controls get => GetValue(TextDecorationsProperty); set => SetValue(TextDecorationsProperty, value); } + + protected override bool BypassFlowDirectionPolicies => true; /// - /// Gets the value of the attached on a control. + /// The BaselineOffset property provides an adjustment to baseline offset /// - /// The control. - /// The font family. - public static FontFamily GetFontFamily(Control control) + public double BaselineOffset { - return control.GetValue(FontFamilyProperty); + get { return (double)GetValue(BaselineOffsetProperty); } + set { SetValue(BaselineOffsetProperty, value); } } /// - /// Gets the value of the attached on a control. + /// Reads the attached property from the given element /// - /// The control. - /// The font family. - public static double GetFontSize(Control control) + /// The element to which to read the attached property. + public static double GetBaselineOffset(Control control) { - return control.GetValue(FontSizeProperty); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(BaselineOffsetProperty); } /// - /// Gets the value of the attached on a control. + /// Writes the attached property BaselineOffset to the given element. /// - /// The control. - /// The font family. - public static FontStyle GetFontStyle(Control control) + /// The element to which to write the attached property. + /// The property value to set + public static void SetBaselineOffset(Control control, double value) { - return control.GetValue(FontStyleProperty); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(BaselineOffsetProperty, value); } /// - /// Gets the value of the attached on a control. + /// Reads the attached property from the given element /// - /// The control. - /// The font family. - public static FontWeight GetFontWeight(Control control) + /// The element to which to read the attached property. + public static TextAlignment GetTextAlignment(Control control) { - return control.GetValue(FontWeightProperty); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(TextAlignmentProperty); } /// - /// Gets the value of the attached on a control. + /// Writes the attached property BaselineOffset to the given element. /// - /// The control. - /// The foreground. - public static IBrush? GetForeground(Control control) + /// The element to which to write the attached property. + /// The property value to set + public static void SetTextAlignment(Control control, TextAlignment alignment) { - return control.GetValue(ForegroundProperty); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(TextAlignmentProperty, alignment); } /// - /// Sets the value of the attached on a control. + /// Reads the attached property from the given element /// - /// The control. - /// The property value to set. - /// The font family. - public static void SetFontFamily(Control control, FontFamily value) + /// The element to which to read the attached property. + public static TextWrapping GetTextWrapping(Control control) { - control.SetValue(FontFamilyProperty, value); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(TextWrappingProperty); } /// - /// Sets the value of the attached on a control. + /// Writes the attached property BaselineOffset to the given element. /// - /// The control. - /// The property value to set. - /// The font family. - public static void SetFontSize(Control control, double value) + /// The element to which to write the attached property. + /// The property value to set + public static void SetTextWrapping(Control control, TextWrapping wrapping) { - control.SetValue(FontSizeProperty, value); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(TextWrappingProperty, wrapping); } /// - /// Sets the value of the attached on a control. + /// Reads the attached property from the given element /// - /// The control. - /// The property value to set. - /// The font family. - public static void SetFontStyle(Control control, FontStyle value) + /// The element to which to read the attached property. + public static TextTrimming GetTextTrimming(Control control) { - control.SetValue(FontStyleProperty, value); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(TextTrimmingProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetTextTrimming(Control control, TextTrimming trimming) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(TextTrimmingProperty, trimming); + } + + /// + /// Reads the attached property from the given element + /// + /// The element to which to read the attached property. + public static double GetLineHeight(Control control) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(LineHeightProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetLineHeight(Control control, double height) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(LineHeightProperty, height); } /// - /// Sets the value of the attached on a control. + /// Reads the attached property from the given element /// - /// The control. - /// The property value to set. - /// The font family. - public static void SetFontWeight(Control control, FontWeight value) + /// The element to which to read the attached property. + public static int GetMaxLines(Control control) { - control.SetValue(FontWeightProperty, value); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(MaxLinesProperty); } /// - /// Sets the value of the attached on a control. + /// Writes the attached property BaselineOffset to the given element. /// - /// The control. - /// The property value to set. - /// The font family. - public static void SetForeground(Control control, IBrush? value) + /// The element to which to write the attached property. + /// The property value to set + public static void SetMaxLines(Control control, int maxLines) { - control.SetValue(ForegroundProperty, value); + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(MaxLinesProperty, maxLines); } + /// /// Renders the to a drawing context. /// @@ -399,25 +521,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,23 +548,39 @@ 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) + var defaultProperties = new GenericTextRunProperties( + new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), + FontSize, + TextDecorations, + Foreground); + + var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, + defaultProperties, TextWrapping, LineHeight, 0); + + ITextSource textSource; + + if (Inlines.HasComplexContent) { - return null; + var textRuns = new List(); + + foreach (var inline in Inlines) + { + inline.BuildTextRun(textRuns); + } + + textSource = new InlinesTextSource(textRuns); + } + else + { + textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties); } return new TextLayout( - text ?? string.Empty, - new Typeface(FontFamily, FontStyle, FontWeight), - FontSize, - Foreground ?? Brushes.Transparent, - TextAlignment, - TextWrapping, + textSource, + paragraphProperties, TextTrimming, - TextDecorations, - FlowDirection, constraint.Width, constraint.Height, maxLines: MaxLines, @@ -460,43 +593,61 @@ namespace Avalonia.Controls protected void InvalidateTextLayout() { _textLayout = null; - + 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; + var scale = LayoutHelper.GetLayoutScale(this); + + var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); + + _constraint = availableSize.Deflate(padding); + + _textLayout = null; - availableSize = availableSize.Deflate(padding); + InvalidateArrange(); - if (_constraint != availableSize) + var measuredSize = TextLayout.Bounds.Size.Inflate(padding); + + return measuredSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + if(finalSize.Width < TextLayout.Bounds.Width) { - _constraint = availableSize; + finalSize = finalSize.WithWidth(TextLayout.Bounds.Width); + } - InvalidateTextLayout(); + if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) + { + return finalSize; } - var measuredSize = TextLayout?.Size ?? Size.Empty; + _constraint = new Size(finalSize.Width, double.PositiveInfinity); - return measuredSize.Inflate(padding); + _textLayout = null; + + return finalSize; + } + + protected override AutomationPeer OnCreateAutomationPeer() + { + return new TextBlockAutomationPeer(this); } private static bool IsValidMaxLines(int maxLines) => maxLines >= 0; private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0; - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -506,10 +657,12 @@ namespace Avalonia.Controls case nameof (FontWeight): case nameof (FontStyle): case nameof (FontFamily): + case nameof (FontStretch): case nameof (TextWrapping): case nameof (TextTrimming): case nameof (TextAlignment): + case nameof (FlowDirection): case nameof (Padding): @@ -525,5 +678,84 @@ namespace Avalonia.Controls } } } + + private void InlinesChanged(object? sender, EventArgs e) + { + InvalidateTextLayout(); + } + + void IInlineHost.AddVisualChild(IControl child) + { + if (child.VisualParent == null) + { + VisualChildren.Add(child); + } + } + + void IInlineHost.Invalidate() + { + InvalidateTextLayout(); + } + + private readonly struct InlinesTextSource : ITextSource + { + private readonly IReadOnlyList _textRuns; + + public InlinesTextSource(IReadOnlyList textRuns) + { + _textRuns = textRuns; + } + + public TextRun? GetTextRun(int textSourceIndex) + { + var currentPosition = 0; + + foreach (var textRun in _textRuns) + { + if(textRun.TextSourceLength == 0) + { + continue; + } + + if(currentPosition >= textSourceIndex) + { + return textRun; + } + + currentPosition += textRun.TextSourceLength; + } + + return null; + } + } + + private readonly struct SimpleTextSource : ITextSource + { + private readonly ReadOnlySlice _text; + private readonly TextRunProperties _defaultProperties; + + public SimpleTextSource(ReadOnlySlice text, TextRunProperties defaultProperties) + { + _text = text; + _defaultProperties = defaultProperties; + } + + public TextRun? GetTextRun(int textSourceIndex) + { + if (textSourceIndex > _text.Length) + { + return null; + } + + var runText = _text.Skip(textSourceIndex); + + if (runText.IsEmpty) + { + return null; + } + + return new TextCharacters(runText, _defaultProperties); + } + } } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 4ec0c4c5e1..7652b23162 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 { @@ -53,13 +55,13 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(PasswordChar)); public static readonly StyledProperty SelectionBrushProperty = - AvaloniaProperty.Register(nameof(SelectionBrushProperty)); + AvaloniaProperty.Register(nameof(SelectionBrush)); public static readonly StyledProperty SelectionForegroundBrushProperty = - AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); + AvaloniaProperty.Register(nameof(SelectionForegroundBrush)); public static readonly StyledProperty CaretBrushProperty = - AvaloniaProperty.Register(nameof(CaretBrushProperty)); + AvaloniaProperty.Register(nameof(CaretBrush)); public static readonly DirectProperty SelectionStartProperty = AvaloniaProperty.RegisterDirect( @@ -76,6 +78,9 @@ namespace Avalonia.Controls public static readonly StyledProperty MaxLengthProperty = AvaloniaProperty.Register(nameof(MaxLength), defaultValue: 0); + public static readonly StyledProperty MaxLinesProperty = + AvaloniaProperty.Register(nameof(MaxLines), defaultValue: 0); + public static readonly DirectProperty TextProperty = TextBlock.TextProperty.AddOwnerWithDataValidation( o => o.Text, @@ -100,6 +105,12 @@ namespace Avalonia.Controls public static readonly StyledProperty TextWrappingProperty = TextBlock.TextWrappingProperty.AddOwner(); + + /// + /// Defines see property. + /// + public static readonly StyledProperty LineHeightProperty = + TextBlock.LineHeightProperty.AddOwner(); public static readonly StyledProperty WatermarkProperty = AvaloniaProperty.Register(nameof(Watermark)); @@ -149,15 +160,15 @@ namespace Avalonia.Controls public static readonly RoutedEvent CopyingToClipboardEvent = RoutedEvent.Register( - "CopyingToClipboard", RoutingStrategies.Bubble); + nameof(CopyingToClipboard), RoutingStrategies.Bubble); public static readonly RoutedEvent CuttingToClipboardEvent = RoutedEvent.Register( - "CuttingToClipboard", RoutingStrategies.Bubble); + nameof(CuttingToClipboard), RoutingStrategies.Bubble); public static readonly RoutedEvent PastingFromClipboardEvent = RoutedEvent.Register( - "PastingFromClipboard", RoutingStrategies.Bubble); + nameof(PastingFromClipboard), RoutingStrategies.Bubble); readonly struct UndoRedoState : IEquatable { @@ -201,7 +212,10 @@ namespace Avalonia.Controls FocusableProperty.OverrideDefaultValue(typeof(TextBox), true); TextInputMethodClientRequestedEvent.AddClassHandler((tb, e) => { - e.Client = tb._imClient; + if (!tb.IsReadOnly) + { + e.Client = tb._imClient; + } }); } @@ -231,23 +245,19 @@ namespace Avalonia.Controls public bool AcceptsReturn { - get { return GetValue(AcceptsReturnProperty); } - set { SetValue(AcceptsReturnProperty, value); } + get => GetValue(AcceptsReturnProperty); + set => SetValue(AcceptsReturnProperty, value); } public bool AcceptsTab { - get { return GetValue(AcceptsTabProperty); } - set { SetValue(AcceptsTabProperty, value); } + get => GetValue(AcceptsTabProperty); + set => SetValue(AcceptsTabProperty, value); } public int CaretIndex { - get - { - return _caretIndex; - } - + get => _caretIndex; set { value = CoerceCaretIndex(value); @@ -256,13 +266,15 @@ namespace Avalonia.Controls UndoRedoState state; if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) _undoRedoHelper.UpdateLastState(); + + SelectionStart = SelectionEnd = value; } } public bool IsReadOnly { - get { return GetValue(IsReadOnlyProperty); } - set { SetValue(IsReadOnlyProperty, value); } + get => GetValue(IsReadOnlyProperty); + set => SetValue(IsReadOnlyProperty, value); } public char PasswordChar @@ -291,34 +303,27 @@ namespace Avalonia.Controls public int SelectionStart { - get - { - return _selectionStart; - } - + get => _selectionStart; set { 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; } } } public int SelectionEnd { - get - { - return _selectionEnd; - } - + get => _selectionEnd; set { value = CoerceCaretIndex(value); @@ -328,8 +333,8 @@ namespace Avalonia.Controls { UpdateCommandStates(); } - - if (value == SelectionStart) + + if (SelectionStart == value && CaretIndex != value) { CaretIndex = value; } @@ -338,23 +343,40 @@ namespace Avalonia.Controls public int MaxLength { - get { return GetValue(MaxLengthProperty); } - set { SetValue(MaxLengthProperty, value); } + get => GetValue(MaxLengthProperty); + set => SetValue(MaxLengthProperty, value); + } + + public int MaxLines + { + get => GetValue(MaxLinesProperty); + set => SetValue(MaxLinesProperty, value); + } + + /// + /// Gets or sets the line height. + /// + public double LineHeight + { + get { return GetValue(LineHeightProperty); } + set { SetValue(LineHeightProperty, value); } } [Content] public string? Text { - get { return _text; } + get => _text; set { 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) { @@ -367,7 +389,7 @@ namespace Avalonia.Controls public string SelectedText { - get { return GetSelection(); } + get => GetSelection(); set { if (string.IsNullOrEmpty(value)) @@ -388,8 +410,8 @@ namespace Avalonia.Controls /// public HorizontalAlignment HorizontalContentAlignment { - get { return GetValue(HorizontalContentAlignmentProperty); } - set { SetValue(HorizontalContentAlignmentProperty, value); } + get => GetValue(HorizontalContentAlignmentProperty); + set => SetValue(HorizontalContentAlignmentProperty, value); } /// @@ -397,50 +419,58 @@ namespace Avalonia.Controls /// public VerticalAlignment VerticalContentAlignment { - get { return GetValue(VerticalContentAlignmentProperty); } - set { SetValue(VerticalContentAlignmentProperty, value); } + get => GetValue(VerticalContentAlignmentProperty); + set => SetValue(VerticalContentAlignmentProperty, value); } public TextAlignment TextAlignment { - get { return GetValue(TextAlignmentProperty); } - set { SetValue(TextAlignmentProperty, value); } + get => GetValue(TextAlignmentProperty); + set => SetValue(TextAlignmentProperty, value); } + /// + /// Gets or sets the placeholder or descriptive text that is displayed even if the + /// property is not yet set. + /// public string? Watermark { - get { return GetValue(WatermarkProperty); } - set { SetValue(WatermarkProperty, value); } + get => GetValue(WatermarkProperty); + set => SetValue(WatermarkProperty, value); } + /// + /// Gets or sets a value indicating whether the will still be shown above the + /// even after a text value is set. + /// public bool UseFloatingWatermark { - get { return GetValue(UseFloatingWatermarkProperty); } - set { SetValue(UseFloatingWatermarkProperty, value); } + get => GetValue(UseFloatingWatermarkProperty); + set => SetValue(UseFloatingWatermarkProperty, value); } public object InnerLeftContent { - get { return GetValue(InnerLeftContentProperty); } - set { SetValue(InnerLeftContentProperty, value); } + get => GetValue(InnerLeftContentProperty); + set => SetValue(InnerLeftContentProperty, value); } public object InnerRightContent { - get { return GetValue(InnerRightContentProperty); } - set { SetValue(InnerRightContentProperty, value); } + get => GetValue(InnerRightContentProperty); + set => SetValue(InnerRightContentProperty, value); } public bool RevealPassword { - get { return GetValue(RevealPasswordProperty); } - set { SetValue(RevealPasswordProperty, value); } + get => GetValue(RevealPasswordProperty); + set => SetValue(RevealPasswordProperty, value); } public TextWrapping TextWrapping { - get { return GetValue(TextWrappingProperty); } - set { SetValue(TextWrappingProperty, value); } + get => GetValue(TextWrappingProperty); + set => SetValue(TextWrappingProperty, value); } /// @@ -448,8 +478,8 @@ namespace Avalonia.Controls /// public string NewLine { - get { return _newLine; } - set { SetAndRaise(NewLineProperty, ref _newLine, value); } + get => _newLine; + set => SetAndRaise(NewLineProperty, ref _newLine, value); } /// @@ -457,7 +487,7 @@ namespace Avalonia.Controls /// public void ClearSelection() { - SelectionStart = SelectionEnd = CaretIndex; + CaretIndex = SelectionStart; } /// @@ -465,8 +495,8 @@ namespace Avalonia.Controls /// public bool CanCut { - get { return _canCut; } - private set { SetAndRaise(CanCutProperty, ref _canCut, value); } + get => _canCut; + private set => SetAndRaise(CanCutProperty, ref _canCut, value); } /// @@ -474,8 +504,8 @@ namespace Avalonia.Controls /// public bool CanCopy { - get { return _canCopy; } - private set { SetAndRaise(CanCopyProperty, ref _canCopy, value); } + get => _canCopy; + private set => SetAndRaise(CanCopyProperty, ref _canCopy, value); } /// @@ -483,8 +513,8 @@ namespace Avalonia.Controls /// public bool CanPaste { - get { return _canPaste; } - private set { SetAndRaise(CanPasteProperty, ref _canPaste, value); } + get => _canPaste; + private set => SetAndRaise(CanPasteProperty, ref _canPaste, value); } /// @@ -492,13 +522,13 @@ namespace Avalonia.Controls /// public bool IsUndoEnabled { - get { return GetValue(IsUndoEnabledProperty); } - set { SetValue(IsUndoEnabledProperty, value); } + get => GetValue(IsUndoEnabledProperty); + set => SetValue(IsUndoEnabledProperty, value); } public int UndoLimit { - get { return _undoRedoHelper.Limit; } + get => _undoRedoHelper.Limit; set { if (_undoRedoHelper.Limit != value) @@ -566,7 +596,7 @@ namespace Avalonia.Controls _imClient.SetPresenter(null, null); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -575,7 +605,7 @@ namespace Avalonia.Controls UpdatePseudoclasses(); UpdateCommandStates(); } - else if (change.Property == IsUndoEnabledProperty && change.NewValue.GetValueOrDefault() == false) + else if (change.Property == IsUndoEnabledProperty && change.GetNewValue() == false) { // from docs at // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: @@ -661,6 +691,39 @@ namespace Avalonia.Controls _selectedTextChangesMadeSinceLastUndoSnapshot++; SnapshotUndoRedo(ignoreChangeCount: false); + if (_presenter != null && MaxLines > 0) + { + var lineCount = _presenter.TextLayout.TextLines.Count; + + var length = 0; + + var graphemeEnumerator = new GraphemeEnumerator(input.AsMemory()); + + while (graphemeEnumerator.MoveNext()) + { + var grapheme = graphemeEnumerator.Current; + + if (grapheme.FirstCodepoint.IsBreakChar) + { + if(lineCount + 1 > MaxLines) + { + break; + } + else + { + lineCount++; + } + } + + length += grapheme.Text.Length; + } + + if (length < input.Length) + { + input = input.Remove(Math.Max(0, length)); + } + } + var text = Text ?? string.Empty; var newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); @@ -855,6 +918,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheEndOfDocument)) { @@ -862,6 +926,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheStartOfLine)) { @@ -869,7 +934,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; - + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheEndOfLine)) { @@ -877,24 +942,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 +974,9 @@ namespace Avalonia.Controls } else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection)) { + SelectionStart = caretIndex; MoveEnd(false); + SelectionEnd = _presenter.CaretIndex; movement = true; selection = true; handled = true; @@ -941,7 +1015,7 @@ namespace Avalonia.Controls } else { - SelectionStart = SelectionEnd = _presenter.CaretIndex; + CaretIndex = _presenter.CaretIndex; } break; @@ -963,7 +1037,7 @@ namespace Avalonia.Controls } else { - SelectionStart = SelectionEnd = _presenter.CaretIndex; + CaretIndex = _presenter.CaretIndex; } break; @@ -977,34 +1051,28 @@ namespace Avalonia.Controls SetSelectionForControlBackspace(); } - if (!DeleteSelection() && caretIndex > 0) + if (!DeleteSelection()) { - var removedCharacters = 0; - - // \r\n needs special treatment here - if (caretIndex - 1 > 0 && text[caretIndex - 1] == '\n' && text[caretIndex - 2] == '\r') - { - removedCharacters = 2; - } - else - { - Codepoint.ReadAt(text.AsMemory(), caretIndex - 1, out removedCharacters); - } + var characterHit = _presenter.GetNextCharacterHit(LogicalDirection.Backward); - if (removedCharacters == 0) + var backspacePosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + if (caretIndex != backspacePosition) { - return; - } + var start = Math.Min(backspacePosition, caretIndex); + var end = Math.Max(backspacePosition, caretIndex); + + var length = end - start; - var length = Math.Max(0, caretIndex - removedCharacters); + var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); - SetTextInternal(text.Substring(0, length) + - text.Substring(caretIndex)); - - CaretIndex = caretIndex - removedCharacters; - - ClearSelection(); + SetTextInternal(editedText); + + CaretIndex = start; + } } + + SnapshotUndoRedo(); handled = true; break; @@ -1017,16 +1085,21 @@ namespace Avalonia.Controls SetSelectionForControlDelete(); } - if (!DeleteSelection() && caretIndex < text.Length) + if (!DeleteSelection()) { - _presenter.MoveCaretHorizontal(); + var characterHit = _presenter.GetNextCharacterHit(); + + var nextPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - var removedCharacters = Math.Max(0, _presenter.CaretIndex - caretIndex); + if(nextPosition != caretIndex) + { + var start = Math.Min(nextPosition, caretIndex); + var end = Math.Max(nextPosition, caretIndex); - SetTextInternal(text.Substring(0, caretIndex) + - text.Substring(caretIndex + removedCharacters)); + var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); - CaretIndex = caretIndex; + SetTextInternal(editedText); + } } SnapshotUndoRedo(); @@ -1195,11 +1268,19 @@ namespace Avalonia.Controls e.Pointer.Capture(null); } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override AutomationPeer OnCreateAutomationPeer() + { + return new TextBoxAutomationPeer(this); + } + + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == TextProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } @@ -1238,40 +1319,69 @@ namespace Avalonia.Controls private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting) { + if (_presenter == null) + { + return; + } + var text = Text ?? string.Empty; var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; if (!wholeWord) { - if (_presenter == null) - { - return; - } - - _presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward); - if (isSelecting) { + _presenter.MoveCaretToTextPosition(selectionEnd); + + _presenter.MoveCaretHorizontal(direction > 0 ? + LogicalDirection.Forward : + LogicalDirection.Backward); + SelectionEnd = _presenter.CaretIndex; } else { - SelectionStart = SelectionEnd = _presenter.CaretIndex; + if (selectionStart != selectionEnd) + { + _presenter.MoveCaretToTextPosition(direction > 0 ? + Math.Max(selectionStart, selectionEnd) : + Math.Min(selectionStart, selectionEnd)); + } + else + { + _presenter.MoveCaretHorizontal(direction > 0 ? + LogicalDirection.Forward : + LogicalDirection.Backward); + } + + CaretIndex = _presenter.CaretIndex; } } else { + int offset; + if (direction > 0) { - var offset = StringUtils.NextWord(text, selectionStart) - selectionStart; - - CaretIndex += offset; + offset = StringUtils.NextWord(text, selectionEnd) - selectionEnd; } else { - var offset = StringUtils.PreviousWord(text, selectionStart) - selectionStart; - - CaretIndex += offset; + offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd; + } + + SelectionEnd += offset; + + _presenter.MoveCaretToTextPosition(SelectionEnd); + + if (!isSelecting) + { + CaretIndex = SelectionEnd; + } + else + { + SelectionStart = selectionStart; } } } @@ -1283,32 +1393,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; - } + var textLines = _presenter.TextLayout.TextLines; + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false); + var textLine = textLines[lineIndex]; - pos += line.TextRange.Length; - } - - caretIndex = pos; + _presenter.MoveCaretToTextPosition(textLine.FirstTextSourceIndex); } - - CaretIndex = caretIndex; } private void MoveEnd(bool document) @@ -1323,36 +1421,18 @@ namespace Avalonia.Controls if (document) { - caretIndex = text.Length; + _presenter.MoveCaretToTextPosition(text.Length, true); } else { - var lines = _presenter.TextLayout.TextLines; - var pos = 0; + var textLines = _presenter.TextLayout.TextLines; + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false); + var textLine = textLines[lineIndex]; - foreach (var line in lines) - { - pos += line.TextRange.Length; - - if (pos > caretIndex) - { - if (pos < text.Length) - { - --pos; - if (pos > 0 && text[pos - 1] == '\r' && text[pos] == '\n') - { - --pos; - } - } - - break; - } - } + var textPosition = textLine.FirstTextSourceIndex + textLine.Length; - caretIndex = pos; + _presenter.MoveCaretToTextPosition(textPosition, true); } - - CaretIndex = caretIndex; } /// @@ -1362,50 +1442,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 +1517,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() @@ -1452,7 +1550,7 @@ namespace Avalonia.Controls UndoRedoState UndoRedoHelper.IUndoRedoHost.UndoRedoState { - get { return new UndoRedoState(Text, CaretIndex); } + get => new UndoRedoState(Text, CaretIndex); set { Text = value.Text; 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..57fb82485c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -14,7 +15,6 @@ using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; -using JetBrains.Annotations; namespace Avalonia.Controls { @@ -26,6 +26,7 @@ namespace Avalonia.Controls /// It handles scheduling layout, styling and rendering as well as /// tracking the widget's . /// + [TemplatePart("PART_TransparencyFallback", typeof(Border))] public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, @@ -33,8 +34,7 @@ namespace Avalonia.Controls ICloseable, IStyleHost, ILogicalRoot, - ITextInputMethodRoot, - IWeakEventSubscriber + ITextInputMethodRoot { /// /// Defines the property. @@ -85,11 +85,14 @@ namespace Avalonia.Controls private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; private readonly IPlatformRenderInterface? _renderInterface; private readonly IGlobalStyles? _globalStyles; + private readonly PointerOverPreProcessor? _pointerOverPreProcessor; + private readonly IDisposable? _pointerOverPreProcessorSubscription; private Size _clientSize; private Size? _frameSize; private WindowTransparencyLevel _actualTransparencyLevel; private ILayoutManager? _layoutManager; private Border? _transparencyFallbackBorder; + private TargetWeakEventSubscriber? _resourcesChangesSubscriber; /// /// Initializes static members of the class. @@ -134,8 +137,6 @@ namespace Avalonia.Controls "Could not create window implementation: maybe no windowing subsystem was initialized?"); } - impl = ValidatingToplevelImpl.Wrap(impl); - PlatformImpl = impl; _actualTransparencyLevel = PlatformImpl.TransparencyLevel; @@ -191,10 +192,19 @@ namespace Avalonia.Controls if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { - ResourcesChangedWeakEvent.Subscribe(applicationResources, this); + _resourcesChangesSubscriber = new TargetWeakEventSubscriber( + this, static (target, _, _, e) => + { + ((ILogical)target).NotifyResourcesChanged(e); + }); + + ResourcesChangedWeakEvent.Subscribe(applicationResources, _resourcesChangesSubscriber); } impl.LostFocus += PlatformImpl_LostFocus; + + _pointerOverPreProcessor = new PointerOverPreProcessor(this); + _pointerOverPreProcessorSubscription = _inputManager?.PreProcess.Subscribe(_pointerOverPreProcessor); } /// @@ -283,9 +293,7 @@ namespace Avalonia.Controls /// IKeyboardNavigationHandler IInputRoot.KeyboardNavigationHandler => _keyboardNavigationHandler!; - /// - /// Gets or sets the input element that the pointer is currently over. - /// + /// IInputElement? IInputRoot.PointerOverElement { get { return GetValue(PointerOverElementProperty); } @@ -295,11 +303,6 @@ namespace Avalonia.Controls /// IMouseDevice? IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, ResourcesChangedEventArgs e) - { - ((ILogical)this).NotifyResourcesChanged(e); - } - /// /// Gets or sets a value indicating whether access keys are shown in the window. /// @@ -350,6 +353,12 @@ namespace Avalonia.Controls /// protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this); + public override void InvalidateMirrorTransform() + { + } + + protected override bool BypassFlowDirectionPolicies => true; + /// /// Handles a paint notification from . /// @@ -372,10 +381,12 @@ namespace Avalonia.Controls Renderer?.Dispose(); Renderer = null!; - - (this as IInputRoot).MouseDevice?.TopLevelClosed(this); + + _pointerOverPreProcessor?.OnCompleted(); + _pointerOverPreProcessorSubscription?.Dispose(); + PlatformImpl = null; - + var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); @@ -509,12 +520,17 @@ namespace Avalonia.Controls /// The event args. private void HandleInput(RawInputEventArgs e) { + if (e is RawPointerEventArgs pointerArgs) + { + pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position); + } + _inputManager?.ProcessInput(e); } private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) { - (this as IInputRoot).MouseDevice?.SceneInvalidated(this, e.DirtyRect); + _pointerOverPreProcessor?.SceneInvalidated(e.DirtyRect); } void PlatformImpl_LostFocus() diff --git a/src/Avalonia.Controls/TransitioningContentControl.cs b/src/Avalonia.Controls/TransitioningContentControl.cs new file mode 100644 index 0000000000..451e234653 --- /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/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index b8ab48a2b7..2ccb03e447 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -206,7 +206,7 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -216,15 +216,15 @@ namespace Avalonia.Controls } else if (change.Property == IsVisibleProperty) { - _impl?.SetIsVisible(change.NewValue.GetValueOrDefault()); + _impl?.SetIsVisible(change.GetNewValue()); } else if (change.Property == ToolTipTextProperty) { - _impl?.SetToolTipText(change.NewValue.GetValueOrDefault()); + _impl?.SetToolTipText(change.GetNewValue()); } else if (change.Property == MenuProperty) { - _impl?.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault()); + _impl?.MenuExporter?.SetNativeMenu(change.GetNewValue()); } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 9a276e74d2..b2a188a2ea 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -401,7 +401,7 @@ namespace Avalonia.Controls protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() => CreateTreeItemContainerGenerator(); - protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() where TVItem: TreeViewItem, new() + protected ITreeItemContainerGenerator CreateTreeItemContainerGenerator() where TVItem: TreeViewItem, new() { return new TreeItemContainerGenerator( this, @@ -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..490b0b3ce3 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 { @@ -95,7 +96,7 @@ namespace Avalonia.Controls protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator(); /// - protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() + protected ITreeItemContainerGenerator CreateTreeItemContainerGenerator() where TVItem: TreeViewItem, new() { return new TreeItemContainerGenerator( 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..01a41a0157 100644 --- a/src/Avalonia.Controls/Viewbox.cs +++ b/src/Avalonia.Controls/Viewbox.cs @@ -1,18 +1,21 @@ using Avalonia.Media; +using Avalonia.Media.Immutable; +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 readonly ViewboxContainer _containerVisual; + /// /// Defines the property. /// public static readonly StyledProperty StretchProperty = - AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); + AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); /// /// Defines the property. @@ -20,12 +23,28 @@ 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() + { + // The Child control is hosted inside a ViewboxContainer control so that the transform + // can be applied independently of the Viewbox and Child transforms. + _containerVisual = new ViewboxContainer(); + _containerVisual.RenderTransformOrigin = RelativePoint.TopLeft; + VisualChildren.Add(_containerVisual); + } + /// /// Gets or sets the stretch mode, /// which determines how child fits into the available space. @@ -45,9 +64,55 @@ 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) + { + var (oldChild, newChild) = change.GetOldAndNewValue(); + + if (oldChild is not null) + { + ((ISetLogicalParent)oldChild).SetParent(null); + LogicalChildren.Remove(oldChild); + } + + _containerVisual.Child = newChild; + + if (newChild is not null) + { + ((ISetLogicalParent)newChild).SetParent(this); + LogicalChildren.Add(newChild); + } + + InvalidateMeasure(); + } + } + protected override Size MeasureOverride(Size availableSize) { - var child = Child; + var child = _containerVisual; if (child != null) { @@ -57,7 +122,7 @@ namespace Avalonia.Controls var size = Stretch.CalculateSize(availableSize, childSize, StretchDirection); - return size.Constrain(availableSize); + return size; } return new Size(); @@ -65,31 +130,47 @@ 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 ImmutableTransform(Matrix.CreateScale(scale.X, scale.Y)); child.Arrange(new Rect(childSize)); return childSize * scale; } - return new Size(); + return finalSize; + } + + /// + /// A simple container control which hosts its child as a visual but not logical child. + /// + private class ViewboxContainer : Control + { + private IControl? _child; + + public IControl? Child + { + get => _child; + set + { + if (_child != value) + { + if (_child is not null) + VisualChildren.Remove(_child); + + _child = value; + + if (_child is not null) + VisualChildren.Add(_child); + } + } + } } } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index a5f48bd4a5..a4f4534b88 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; +using Avalonia.Automation.Peers; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Interactivity; @@ -11,7 +12,6 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; -using JetBrains.Annotations; namespace Avalonia.Controls { @@ -171,13 +171,13 @@ namespace Avalonia.Controls /// /// Routed event that can be used for global tracking of window destruction /// - public static readonly RoutedEvent WindowClosedEvent = + public static readonly RoutedEvent WindowClosedEvent = RoutedEvent.Register("WindowClosed", RoutingStrategies.Direct); /// /// Routed event that can be used for global tracking of opening windows /// - public static readonly RoutedEvent WindowOpenedEvent = + public static readonly RoutedEvent WindowOpenedEvent = RoutedEvent.Register("WindowOpened", RoutingStrategies.Direct); @@ -237,14 +237,13 @@ namespace Avalonia.Controls /// /// The window implementation. public Window(IWindowImpl impl) - : base(ValidatingWindowImpl.Wrap(impl)) + : base(impl) { - var wrapped = (IWindowImpl)base.PlatformImpl!; - wrapped.Closing = HandleClosing; - wrapped.GotInputWhenDisabled = OnGotInputWhenDisabled; - wrapped.WindowStateChanged = HandleWindowStateChanged; + impl.Closing = HandleClosing; + impl.GotInputWhenDisabled = OnGotInputWhenDisabled; + impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); - wrapped.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; + impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); @@ -255,6 +254,11 @@ namespace Avalonia.Controls /// public new IWindowImpl? PlatformImpl => (IWindowImpl?)base.PlatformImpl; + /// + /// Gets a collection of child windows owned by this window. + /// + public IReadOnlyList OwnedWindows => _children.Select(x => x.child).ToArray(); + /// /// Gets or sets a value indicating how the window will size itself to fit its content. /// @@ -522,7 +526,7 @@ namespace Avalonia.Controls private void CloseInternal() { - foreach (var (child, _) in _children.ToList()) + foreach (var (child, _) in _children.ToArray()) { child.CloseInternal(); } @@ -546,7 +550,7 @@ namespace Avalonia.Controls bool canClose = true; - foreach (var (child, _) in _children.ToList()) + foreach (var (child, _) in _children.ToArray()) { if (child.ShouldCancelClose(args)) { @@ -854,6 +858,17 @@ namespace Avalonia.Controls private void SetWindowStartupLocation(IWindowBaseImpl? owner = null) { + var startupLocation = WindowStartupLocation; + + if (startupLocation == WindowStartupLocation.CenterOwner && + Owner is Window ownerWindow && + ownerWindow.WindowState == WindowState.Minimized) + { + // If startup location is CenterOwner, but owner is minimized then fall back + // to CenterScreen. This behavior is consistent with WPF. + startupLocation = WindowStartupLocation.CenterScreen; + } + var scaling = owner?.DesktopScaling ?? PlatformImpl?.DesktopScaling ?? 1; // TODO: We really need non-client size here. @@ -861,16 +876,28 @@ namespace Avalonia.Controls PixelPoint.Origin, PixelSize.FromSize(ClientSize, scaling)); - if (WindowStartupLocation == WindowStartupLocation.CenterScreen) + if (startupLocation == WindowStartupLocation.CenterScreen) { - var screen = Screens.ScreenFromPoint(owner?.Position ?? Position); + Screen? screen = null; + + if (owner is not null) + { + screen = Screens.ScreenFromWindow(owner); + + screen ??= Screens.ScreenFromPoint(owner.Position); + } + + if (screen is null) + { + screen = Screens.ScreenFromPoint(Position); + } if (screen != null) { Position = screen.WorkingArea.CenterRect(rect).Position; } } - else if (WindowStartupLocation == WindowStartupLocation.CenterOwner) + else if (startupLocation == WindowStartupLocation.CenterOwner) { if (owner != null) { @@ -890,6 +917,15 @@ namespace Avalonia.Controls var constraint = clientSize; var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity; + if (MaxWidth > 0 && MaxWidth < maxAutoSize.Width) + { + maxAutoSize = maxAutoSize.WithWidth(MaxWidth); + } + if (MaxHeight > 0 && MaxHeight < maxAutoSize.Height) + { + maxAutoSize = maxAutoSize.WithHeight(MaxHeight); + } + if (sizeToContent.HasAllFlags(SizeToContent.Width)) { constraint = constraint.WithWidth(maxAutoSize.Width); @@ -992,16 +1028,16 @@ namespace Avalonia.Controls /// protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == SystemDecorationsProperty) { - var typedNewValue = change.NewValue.GetValueOrDefault(); + var (typedOldValue, typedNewValue) = change.GetOldAndNewValue(); PlatformImpl?.SetSystemDecorations(typedNewValue); - var o = change.OldValue.GetValueOrDefault() == SystemDecorations.Full; + var o = typedOldValue == SystemDecorations.Full; var n = typedNewValue == SystemDecorations.Full; if (o != n) @@ -1012,5 +1048,10 @@ namespace Avalonia.Controls } } } + + protected override AutomationPeer OnCreateAutomationPeer() + { + return new WindowAutomationPeer(this); + } } } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 4464491020..12ba143c8a 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using Avalonia.Automation.Peers; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; @@ -57,13 +58,12 @@ namespace Avalonia.Controls { } - public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(ValidatingWindowBaseImpl.Wrap(impl), dependencyResolver) + public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(impl, dependencyResolver) { - Screens = new Screens(PlatformImpl?.Screen); - var wrapped = PlatformImpl!; - wrapped.Activated = HandleActivated; - wrapped.Deactivated = HandleDeactivated; - wrapped.PositionChanged = HandlePositionChanged; + Screens = new Screens(impl.Screen); + impl.Activated = HandleActivated; + impl.Deactivated = HandleDeactivated; + impl.PositionChanged = HandlePositionChanged; } /// diff --git a/src/Avalonia.Controls/WindowTransparencyLevel.cs b/src/Avalonia.Controls/WindowTransparencyLevel.cs index f416b5de91..d463f74a0e 100644 --- a/src/Avalonia.Controls/WindowTransparencyLevel.cs +++ b/src/Avalonia.Controls/WindowTransparencyLevel.cs @@ -22,6 +22,11 @@ /// AcrylicBlur, + /// + /// Force acrylic on some incompatible versions of Windows 10. + /// + ForceAcrylicBlur, + /// /// The window background is based on desktop wallpaper tint with a blur. This will only work on Windows 11 /// diff --git a/src/Avalonia.DesignerSupport/ApiCompatBaseline.txt b/src/Avalonia.DesignerSupport/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Avalonia.DesignerSupport/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index f8a7cdc690..0be695e0a1 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -16,15 +16,8 @@ - - - - - - - diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs index 6b1934ed06..f100be5d5b 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Viewport; +using Avalonia.Utilities; using InputProtocol = Avalonia.Remote.Protocol.Input; namespace Avalonia.DesignerSupport.Remote.HtmlTransport @@ -320,15 +321,13 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport ? null : modifiersText .Split(',') - .Select(x => (InputProtocol.InputModifiers)Enum.Parse( - typeof(InputProtocol.InputModifiers), x, true)) + .Select(x => EnumHelper.Parse(x, true)) .ToArray(); private static InputProtocol.MouseButton ParseMouseButton(string buttonText) => string.IsNullOrWhiteSpace(buttonText) ? InputProtocol.MouseButton.None - : (InputProtocol.MouseButton)Enum.Parse( - typeof(InputProtocol.MouseButton), buttonText, true); + : EnumHelper.Parse(buttonText, true); private static double ParseDouble(string text) => double.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json index 6a5fea5b10..403bb5a59a 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json @@ -377,9 +377,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" @@ -1078,12 +1078,6 @@ "json5": "^2.1.2" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -1636,9 +1630,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "mobx": { @@ -1839,12 +1833,6 @@ "ms": "^2.1.1" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -2592,12 +2580,6 @@ "json5": "^2.1.2" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html index f5847cdd58..cf76de0077 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html @@ -9,6 +9,6 @@
Loading...
- + diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 5cae29cafd..12af602f54 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -1,5 +1,4 @@ using System; -using System.Reactive.Disposables; using Avalonia.Controls; using Avalonia.Controls.Remote.Server; using Avalonia.Input; diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 9dcd4d8e87..a5f478f445 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Reactive.Disposables; using System.Threading.Tasks; +using Avalonia.Automation.Peers; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; @@ -236,5 +237,20 @@ namespace Avalonia.DesignerSupport.Remote public IReadOnlyList AllScreens { get; } = new Screen[] { new Screen(1, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) }; + + public Screen ScreenFromPoint(PixelPoint point) + { + return ScreenHelper.ScreenFromPoint(point, AllScreens); + } + + public Screen ScreenFromRect(PixelRect rect) + { + return ScreenHelper.ScreenFromRect(rect, AllScreens); + } + + public Screen ScreenFromWindow(IWindowBaseImpl window) + { + return ScreenHelper.ScreenFromWindow(window, AllScreens); + } } } diff --git a/src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt b/src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt deleted file mode 100644 index 0493db9ab3..0000000000 --- a/src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt +++ /dev/null @@ -1,3 +0,0 @@ -Compat issues with assembly Avalonia.DesktopRuntime: -TypesMustExist : Type 'Avalonia.Shared.PlatformSupport.AssetLoader' does not exist in the implementation but it does exist in the contract. -Total Issues: 1 diff --git a/src/Avalonia.DesktopRuntime/AppBuilder.cs b/src/Avalonia.DesktopRuntime/AppBuilder.cs deleted file mode 100644 index 2946324c83..0000000000 --- a/src/Avalonia.DesktopRuntime/AppBuilder.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using Avalonia.Controls; -using Avalonia.Platform; -using Avalonia.PlatformSupport; - -namespace Avalonia -{ - /// - /// Initializes platform-specific services for an . - /// - public sealed class AppBuilder : AppBuilderBase - { - /// - /// Initializes a new instance of the class. - /// - public AppBuilder() - : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly)) - { - } - - bool CheckEnvironment(Type checkerType) - { - if (checkerType == null) - return true; - try - { - return ((IModuleEnvironmentChecker) Activator.CreateInstance(checkerType)).IsCompatible; - } - catch - { - return false; - } - } - - /// - /// Instructs the to use the best settings for the platform. - /// - /// An instance. - public AppBuilder UseSubsystemsFromStartupDirectory() - { - var os = RuntimePlatform.GetRuntimeInfo().OperatingSystem; - - LoadAssembliesInDirectory(); - - var windowingSubsystemAttribute = (from assembly in AppDomain.CurrentDomain.GetAssemblies() - from attribute in assembly.GetCustomAttributes() - where attribute.RequiredOS == os && CheckEnvironment(attribute.EnvironmentChecker) - orderby attribute.Priority ascending - select attribute).FirstOrDefault(); - if (windowingSubsystemAttribute == null) - { - throw new InvalidOperationException("No windowing subsystem found. Are you missing assembly references?"); - } - - var renderingSubsystemAttribute = (from assembly in AppDomain.CurrentDomain.GetAssemblies() - from attribute in assembly.GetCustomAttributes() - where attribute.RequiredOS == os && CheckEnvironment(attribute.EnvironmentChecker) - where attribute.RequiresWindowingSubsystem == null - || attribute.RequiresWindowingSubsystem == windowingSubsystemAttribute.Name - orderby attribute.Priority ascending - select attribute).FirstOrDefault(); - - if (renderingSubsystemAttribute == null) - { - throw new InvalidOperationException("No rendering subsystem found. Are you missing assembly references?"); - } - - UseWindowingSubsystem(() => windowingSubsystemAttribute.InitializationType - .GetRuntimeMethod(windowingSubsystemAttribute.InitializationMethod, Type.EmptyTypes).Invoke(null, null), - windowingSubsystemAttribute.Name); - - UseRenderingSubsystem(() => renderingSubsystemAttribute.InitializationType - .GetRuntimeMethod(renderingSubsystemAttribute.InitializationMethod, Type.EmptyTypes).Invoke(null, null), - renderingSubsystemAttribute.Name); - - return this; - } - - private void LoadAssembliesInDirectory() - { - var location = Assembly.GetEntryAssembly().Location; - if (string.IsNullOrWhiteSpace(location)) - return; - var dir = new FileInfo(location).Directory; - if (dir == null) - return; - foreach (var file in dir.EnumerateFiles("*.dll")) - { - try - { - Assembly.LoadFile(file.FullName); - } - catch (Exception) - { - } - } - } - } -} diff --git a/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj b/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj deleted file mode 100644 index 25effae46e..0000000000 --- a/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net6.0;net461;netcoreapp2.0 - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index 1fc3604f70..adddf3f57b 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -13,17 +13,12 @@ + - - - - - - @@ -34,4 +29,5 @@ + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs index cb98fb70f3..ec7e91c8be 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs @@ -92,7 +92,7 @@ namespace Avalonia.Diagnostics.Controls set => SetValue(HighlightProperty, value); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -102,7 +102,7 @@ namespace Avalonia.Diagnostics.Controls { _isUpdatingThickness = true; - var value = change.NewValue.GetValueOrDefault(); + var value = change.GetNewValue(); Left = value.Left; Top = value.Top; 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/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index aa03330cc5..7384daae30 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -1,13 +1,17 @@ +using System; +using Avalonia.Data; +using Avalonia.Media; + namespace Avalonia.Diagnostics.ViewModels { internal class AvaloniaPropertyViewModel : PropertyViewModel { private readonly AvaloniaObject _target; - private System.Type _assignedType; + private Type _assignedType; private object? _value; private string _priority; private string _group; - private readonly System.Type _propertyType; + private readonly Type _propertyType; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -28,13 +32,9 @@ namespace Avalonia.Diagnostics.ViewModels public AvaloniaProperty Property { get; } public override object Key => Property; public override string Name { get; } - public override bool? IsAttached => - Property.IsAttached; - - public override string Priority => - _priority; - - public override System.Type AssignedType => _assignedType; + public override bool? IsAttached => Property.IsAttached; + public override string Priority => _priority; + public override Type AssignedType => _assignedType; public override string? Value { @@ -53,30 +53,58 @@ namespace Avalonia.Diagnostics.ViewModels public override string Group => _group; - public override System.Type? DeclaringType { get; } - public override System.Type PropertyType => _propertyType; + public override Type? DeclaringType { get; } + public override Type PropertyType => _propertyType; // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] public override void Update() { if (Property.IsDirect) { - RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType,_value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); + object? value; + Type? valueType = null; + + try + { + value = _target.GetValue(Property); + valueType = value?.GetType(); + } + catch (Exception e) + { + value = e.GetBaseException(); + } + + RaiseAndSetIfChanged(ref _value, value, nameof(Value)); + RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); _group = "Properties"; } else { - var val = _target.GetDiagnostic(Property); + object? value; + Type? valueType = null; + BindingPriority? priority = null; + + try + { + var diag = _target.GetDiagnostic(Property); + + value = diag.Value; + valueType = value?.GetType(); + priority = diag.Priority; + } + catch (Exception e) + { + value = e.GetBaseException(); + } - RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); + RaiseAndSetIfChanged(ref _value, value, nameof(Value)); + RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); - if (val != null) + if (priority != null) { - RaiseAndSetIfChanged(ref _priority, val.Priority.ToString(), nameof(Priority)); + RaiseAndSetIfChanged(ref _priority, priority.ToString()!, nameof(Priority)); RaiseAndSetIfChanged(ref _group, IsAttached == true ? "Attached Properties" : "Properties", nameof(Group)); } else 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/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index e2d8a30c8a..73fb615b32 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -1,13 +1,15 @@ -using System.Reflection; +using System; +using System.Reflection; +using Avalonia.Media; namespace Avalonia.Diagnostics.ViewModels { internal class ClrPropertyViewModel : PropertyViewModel { private readonly object _target; - private System.Type _assignedType; + private Type _assignedType; private object? _value; - private readonly System.Type _propertyType; + private readonly Type _propertyType; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -25,6 +27,7 @@ namespace Avalonia.Diagnostics.ViewModels { Name = property.DeclaringType.Name + '.' + property.Name; } + DeclaringType = property.DeclaringType; _propertyType = property.PropertyType; @@ -36,10 +39,10 @@ namespace Avalonia.Diagnostics.ViewModels public override string Name { get; } public override string Group => "CLR Properties"; - public override System.Type AssignedType => _assignedType; - public override System.Type PropertyType => _propertyType; + public override Type AssignedType => _assignedType; + public override Type PropertyType => _propertyType; - public override string? Value + public override string? Value { get => ConvertToString(_value); set @@ -54,20 +57,30 @@ namespace Avalonia.Diagnostics.ViewModels } } - public override string Priority => - string.Empty; + public override string Priority => string.Empty; - public override bool? IsAttached => - default; + public override bool? IsAttached => default; - public override System.Type? DeclaringType { get; } + public override Type? DeclaringType { get; } // [MemberNotNull(nameof(_type))] public override void Update() { - var val = Property.GetValue(_target); - RaiseAndSetIfChanged(ref _value, val, nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); + object? value; + Type? valueType = null; + + try + { + value = Property.GetValue(_target); + valueType = value?.GetType(); + } + catch (Exception e) + { + value = e.GetBaseException(); + } + + RaiseAndSetIfChanged(ref _value, value, nameof(Value)); + RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); RaisePropertyChanged(nameof(Type)); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 8f0a4d07b0..e383c160e3 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; @@ -59,7 +60,8 @@ namespace Avalonia.Diagnostics.ViewModels var styleDiagnostics = styledElement.GetStyleDiagnostics(); - foreach (var appliedStyle in styleDiagnostics.AppliedStyles) + // We need to place styles without activator first, such styles will be overwritten by ones with activators. + foreach (var appliedStyle in styleDiagnostics.AppliedStyles.OrderBy(s => s.HasActivator)) { var styleSource = appliedStyle.Source; @@ -87,7 +89,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 +128,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 +156,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 +473,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 +491,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/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs index 4dc0c34c0a..0c0c005122 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs @@ -110,7 +110,7 @@ namespace Avalonia.Diagnostics.ViewModels private void UpdateSizeConstraints() { - if (_control is IAvaloniaObject ao) + if (_control is AvaloniaObject ao) { string? CreateConstraintInfo(StyledProperty minProperty, StyledProperty maxProperty) { @@ -191,7 +191,7 @@ namespace Avalonia.Diagnostics.ViewModels } else { - if (_control is IAvaloniaObject ao) + if (_control is AvaloniaObject ao) { if (e.Property == Layoutable.MarginProperty) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index e08c5bc8dd..9e8a5d8d9b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -7,7 +7,6 @@ using Avalonia.Diagnostics.Models; using Avalonia.Input; using Avalonia.Metadata; using Avalonia.Threading; -using System.Reactive.Linq; using System.Linq; namespace Avalonia.Diagnostics.ViewModels @@ -59,8 +58,8 @@ namespace Avalonia.Diagnostics.ViewModels .Subscribe(e => { PointerOverRoot = e.Root; - PointerOverElement = e.Root.GetInputElementsAt(e.Position).FirstOrDefault(); - }); + PointerOverElement = e.Root.InputHitTest(e.Position); + }); #nullable restore } Console = new ConsoleViewModel(UpdateConsoleContext); @@ -163,8 +162,7 @@ namespace Avalonia.Diagnostics.ViewModels } catch { } }, - TimeSpan.FromMilliseconds(0), - DispatcherPriority.ApplicationIdle); + TimeSpan.FromMilliseconds(0)); } RaiseAndSetIfChanged(ref _content, value); 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/ConsoleView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml index 264a0de359..7dec878e69 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml @@ -11,7 +11,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - > + - > + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index d7acbbd577..cc392853be 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -44,7 +44,9 @@ UseWholeWordFilter="{Binding UseWholeWordFilter}" UseRegexFilter="{Binding UseRegexFilter}"/> - + + + + + + + + + + @@ -148,6 +170,26 @@ + + + + + + + + + + + + + { + + + } + + + + @@ -159,15 +201,15 @@ - + ( - + ) - + @@ -180,11 +222,11 @@ - + - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs index 78919a2105..08ffe2c081 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs @@ -7,9 +7,13 @@ namespace Avalonia.Diagnostics.Views { internal class ControlDetailsView : UserControl { + private DataGrid _dataGrid; + public ControlDetailsView() { InitializeComponent(); + + _dataGrid = this.GetControl("DataGrid"); } private void InitializeComponent() @@ -25,5 +29,25 @@ namespace Avalonia.Diagnostics.Views } } + + private void PropertyNamePressed(object sender, PointerPressedEventArgs e) + { + var mainVm = (ControlDetailsViewModel?) DataContext; + + if (mainVm is null) + { + return; + } + + if (sender is Control control && control.DataContext is SetterViewModel setterVm) + { + mainVm.SelectProperty(setterVm.Property); + + if (mainVm.SelectedProperty is not null) + { + _dataGrid.ScrollIntoView(mainVm.SelectedProperty, null); + } + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml index af6c84a76a..c404a5c382 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml @@ -15,6 +15,7 @@ + @@ -46,6 +47,7 @@ diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index a3cff7f3d3..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 => { @@ -161,13 +162,18 @@ namespace Avalonia.Diagnostics.Views return; } - var root = Root as TopLevel - ?? vm.PointerOverRoot as TopLevel; + var root = vm.PointerOverRoot as TopLevel; + if (root is null) { return; } + if (root is PopupRoot pr && pr.ParentTopLevel != null) + { + root = pr.ParentTopLevel; + } + switch (e.Modifiers) { case RawInputModifiers.Control when (e.Key == Key.LeftShift || e.Key == Key.RightShift): @@ -242,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.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 2553ae90ce..58807b489e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -36,6 +36,7 @@ namespace Avalonia.Diagnostics.Views new Border { BorderBrush = new SolidColorBrush(Colors.Yellow, 0.5) } }, }; + AdornerLayer.SetIsClipEnabled(_adorner, false); } protected void AddAdorner(object? sender, PointerEventArgs e) diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index 770d15ea27..a311efdfb0 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -15,4 +15,5 @@ + diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.cs b/src/Avalonia.Dialogs/ManagedFileChooser.cs index 9058c405a3..199a4d6620 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooser.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; @@ -9,6 +10,8 @@ using Avalonia.LogicalTree; namespace Avalonia.Dialogs { + [TemplatePart("QuickLinks", typeof(Control))] + [TemplatePart("Files", typeof(ListBox))] public class ManagedFileChooser : TemplatedControl { private Control _quickLinksRoot; diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index bcb63783a4..e9d6394aa5 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -8,5 +8,7 @@ - + + + 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.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index a7cc4f4cc2..4a55212de3 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -12,10 +12,6 @@ using Tmds.DBus; [assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -[assembly: - InternalsVisibleTo( - "Avalonia.X11, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - namespace Avalonia.FreeDesktop { internal class DBusTrayIconImpl : ITrayIconImpl diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs index f9737b461d..39ddd9d769 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -36,6 +36,13 @@ namespace Avalonia.FreeDesktop private string GetSymlinkTarget(string x) => Path.GetFullPath(Path.Combine(DevByLabelDir, NativeMethods.ReadLink(x))); + private string UnescapeString(string input, string regexText, int escapeBase) => + new Regex(regexText).Replace(input, m => Convert.ToChar(Convert.ToByte(m.Groups[1].Value, escapeBase)).ToString()); + + private string UnescapePathFromProcMounts(string input) => UnescapeString(input, @"\\(\d{3})", 8); + + private string UnescapeDeviceLabel(string input) => UnescapeString(input, @"\\x([0-9a-f]{2})", 16); + private void Poll(long _) { var fProcPartitions = File.ReadAllLines(ProcPartitionsDir) @@ -47,14 +54,14 @@ namespace Avalonia.FreeDesktop var fProcMounts = File.ReadAllLines(ProcMountsDir) .Select(x => x.Split(' ')) - .Select(x => (x[0], x[1])) + .Select(x => (x[0], UnescapePathFromProcMounts(x[1]))) .Where(x => !x.Item2.StartsWith("/snap/", StringComparison.InvariantCultureIgnoreCase)); var labelDirEnum = Directory.Exists(DevByLabelDir) ? new DirectoryInfo(DevByLabelDir).GetFiles() : Enumerable.Empty(); var labelDevPathPairs = labelDirEnum - .Select(x => (GetSymlinkTarget(x.FullName), x.Name)); + .Select(x => (GetSymlinkTarget(x.FullName), UnescapeDeviceLabel(x.Name))); var q1 = from mount in fProcMounts join device in fProcPartitions on mount.Item1 equals device.Item2 @@ -64,7 +71,7 @@ namespace Avalonia.FreeDesktop { VolumePath = mount.Item2, VolumeSizeBytes = device.Item1, - VolumeLabel = x.Name + VolumeLabel = x.Item2 }; var mountVolInfos = q1.ToArray(); diff --git a/src/Avalonia.FreeDesktop/NativeMethods.cs b/src/Avalonia.FreeDesktop/NativeMethods.cs index 8bbda98bb2..147955b6a3 100644 --- a/src/Avalonia.FreeDesktop/NativeMethods.cs +++ b/src/Avalonia.FreeDesktop/NativeMethods.cs @@ -14,15 +14,15 @@ namespace Avalonia.FreeDesktop public static string ReadLink(string path) { - var symlinkMaxSize = Encoding.ASCII.GetMaxByteCount(path.Length); + var symlinkSize = Encoding.UTF8.GetByteCount(path); var bufferSize = 4097; // PATH_MAX is (usually?) 4096, but we need to know if the result was truncated - var symlink = ArrayPool.Shared.Rent(symlinkMaxSize + 1); + var symlink = ArrayPool.Shared.Rent(symlinkSize + 1); var buffer = ArrayPool.Shared.Rent(bufferSize); try { - var symlinkSize = Encoding.UTF8.GetBytes(path, 0, path.Length, symlink, 0); + Encoding.UTF8.GetBytes(path, 0, path.Length, symlink, 0); symlink[symlinkSize] = 0; var size = readlink(symlink, buffer, bufferSize); diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 90221bb922..addc248d58 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -7,7 +7,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; namespace Avalonia.Headless { diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index b619b9d129..083b16c107 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -133,9 +133,12 @@ namespace Avalonia.Headless class HeadlessTextShaperStub : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, - CultureInfo culture, sbyte bidiLevel) + public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) { + var typeface = options.Typeface; + var fontRenderingEmSize = options.FontRenderingEmSize; + var bidiLevel = options.BidLevel; + return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); } } @@ -157,9 +160,10 @@ namespace Avalonia.Headless return new List { "Arial" }; } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out Typeface typeface) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, + FontFamily fontFamily, CultureInfo culture, out Typeface typeface) { - typeface = new Typeface("Arial", fontStyle, fontWeight); + typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch); return true; } } @@ -199,5 +203,20 @@ namespace Avalonia.Headless new Screen(1, new PixelRect(0, 0, 1920, 1280), new PixelRect(0, 0, 1920, 1280), true), }; + + public Screen ScreenFromPoint(PixelPoint point) + { + return ScreenHelper.ScreenFromPoint(point, AllScreens); + } + + public Screen ScreenFromRect(PixelRect rect) + { + return ScreenHelper.ScreenFromRect(rect, AllScreens); + } + + public Screen ScreenFromWindow(IWindowBaseImpl window) + { + return ScreenHelper.ScreenFromWindow(window, AllScreens); + } } } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 1b582d5775..c976921bc3 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Automation.Peers; using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Controls.Primitives.PopupPositioning; diff --git a/src/Avalonia.Input/ApiCompatBaseline.txt b/src/Avalonia.Input/ApiCompatBaseline.txt deleted file mode 100644 index 270c5305e5..0000000000 --- a/src/Avalonia.Input/ApiCompatBaseline.txt +++ /dev/null @@ -1,14 +0,0 @@ -Compat issues with assembly Avalonia.Input: -MembersMustExist : Member 'public Avalonia.Platform.IPlatformHandle Avalonia.Input.Cursor.PlatformCursor.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.DoubleTappedEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.RightTappedEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.TappedEvent' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.IFocusManager.RemoveFocusScope(Avalonia.Input.IFocusScope)' is present in the implementation but not in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.InputElement.DoubleTappedEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.InputElement.TappedEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_DoubleTapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_Tapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_DoubleTapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_Tapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -TypesMustExist : Type 'Avalonia.Platform.IStandardCursorFactory' does not exist in the implementation but it does exist in the contract. -Total Issues: 12 diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj deleted file mode 100644 index e66d1d7e7c..0000000000 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - net6.0;netstandard2.0 - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs deleted file mode 100644 index 30301788bc..0000000000 --- a/src/Avalonia.Input/FocusManager.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - /// - /// Manages focus for the application. - /// - public class FocusManager : IFocusManager - { - /// - /// The focus scopes in which the focus is currently defined. - /// - private readonly ConditionalWeakTable _focusScopes = - new ConditionalWeakTable(); - - /// - /// Initializes a new instance of the class. - /// - static FocusManager() - { - InputElement.PointerPressedEvent.AddClassHandler( - typeof(IInputElement), - new EventHandler(OnPreviewPointerPressed), - RoutingStrategies.Tunnel); - } - - /// - /// Gets the instance of the . - /// - public static IFocusManager? Instance => AvaloniaLocator.Current.GetService(); - - /// - /// Gets the currently focused . - /// - public IInputElement? Current => KeyboardDevice.Instance?.FocusedElement; - - /// - /// Gets the current focus scope. - /// - public IFocusScope? Scope - { - get; - private set; - } - - /// - /// Focuses a control. - /// - /// The control to focus. - /// The method by which focus was changed. - /// Any key modifiers active at the time of focus. - public void Focus( - IInputElement? control, - NavigationMethod method = NavigationMethod.Unspecified, - KeyModifiers keyModifiers = KeyModifiers.None) - { - if (control != null) - { - var scope = GetFocusScopeAncestors(control) - .FirstOrDefault(); - - if (scope != null) - { - Scope = scope; - SetFocusedElement(scope, control, method, keyModifiers); - } - } - else if (Current != null) - { - // If control is null, set focus to the topmost focus scope. - foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList()) - { - if (scope != Scope && - _focusScopes.TryGetValue(scope, out var element) && - element != null) - { - Focus(element, method); - return; - } - } - - if (Scope is object) - { - // Couldn't find a focus scope, clear focus. - SetFocusedElement(Scope, null); - } - } - } - - public IInputElement? GetFocusedElement(IInputElement e) - { - if (e is IFocusScope scope) - { - _focusScopes.TryGetValue(scope, out var result); - return result; - } - - return null; - } - - /// - /// Sets the currently focused element in the specified scope. - /// - /// The focus scope. - /// The element to focus. May be null. - /// The method by which focus was changed. - /// Any key modifiers active at the time of focus. - /// - /// If the specified scope is the current then the keyboard focus - /// will change. - /// - public void SetFocusedElement( - IFocusScope scope, - IInputElement? element, - NavigationMethod method = NavigationMethod.Unspecified, - KeyModifiers keyModifiers = KeyModifiers.None) - { - scope = scope ?? throw new ArgumentNullException(nameof(scope)); - - if (_focusScopes.TryGetValue(scope, out var existingElement)) - { - if (element != existingElement) - { - _focusScopes.Remove(scope); - _focusScopes.Add(scope, element); - } - } - else - { - _focusScopes.Add(scope, element); - } - - if (Scope == scope) - { - KeyboardDevice.Instance?.SetFocusedElement(element, method, keyModifiers); - } - } - - /// - /// Notifies the focus manager of a change in focus scope. - /// - /// The new focus scope. - public void SetFocusScope(IFocusScope scope) - { - scope = scope ?? throw new ArgumentNullException(nameof(scope)); - - if (!_focusScopes.TryGetValue(scope, out var e)) - { - // TODO: Make this do something useful, i.e. select the first focusable - // control, select a control that the user has specified to have default - // focus etc. - e = scope as IInputElement; - _focusScopes.Add(scope, e); - } - - Scope = scope; - Focus(e); - } - - public void RemoveFocusScope(IFocusScope scope) - { - scope = scope ?? throw new ArgumentNullException(nameof(scope)); - - if (_focusScopes.TryGetValue(scope, out _)) - { - SetFocusedElement(scope, null); - _focusScopes.Remove(scope); - } - - if (Scope == scope) - { - Scope = null; - } - } - - public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope; - - /// - /// Checks if the specified element can be focused. - /// - /// The element. - /// True if the element can be focused. - private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && e.IsVisible; - - /// - /// Gets the focus scope ancestors of the specified control, traversing popups. - /// - /// The control. - /// The focus scopes. - private static IEnumerable GetFocusScopeAncestors(IInputElement control) - { - IInputElement? c = control; - - while (c != null) - { - var scope = c as IFocusScope; - - if (scope != null && c.VisualRoot?.IsVisible == true) - { - yield return scope; - } - - c = c.GetVisualParent() ?? - ((c as IHostedVisualTreeRoot)?.Host as IInputElement); - } - } - - /// - /// Global handler for pointer pressed events. - /// - /// The event sender. - /// The event args. - private static void OnPreviewPointerPressed(object? sender, RoutedEventArgs e) - { - if (sender is null) - return; - - var ev = (PointerPressedEventArgs)e; - var visual = (IVisual)sender; - - if (sender == e.Source && ev.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) - { - IVisual? element = ev.Pointer?.Captured ?? e.Source as IInputElement; - - while (element != null) - { - if (element is IInputElement inputElement && CanFocus(inputElement)) - { - Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.KeyModifiers); - - break; - } - - element = element.VisualParent; - } - } - } - } -} diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs deleted file mode 100644 index 86e6e96a71..0000000000 --- a/src/Avalonia.Input/Gestures.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public static class Gestures - { - private static bool s_isDoubleTapped = false; - public static readonly RoutedEvent TappedEvent = RoutedEvent.Register( - "Tapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent DoubleTappedEvent = RoutedEvent.Register( - "DoubleTapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent RightTappedEvent = RoutedEvent.Register( - "RightTapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent ScrollGestureEvent = - RoutedEvent.Register( - "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent ScrollGestureEndedEvent = - RoutedEvent.Register( - "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = - RoutedEvent.Register( - "PointerMagnifyGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = - RoutedEvent.Register( - "PointerRotateGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = - RoutedEvent.Register( - "PointerSwipeGesture", RoutingStrategies.Bubble, typeof(Gestures)); - -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - private static readonly WeakReference s_lastPress = new WeakReference(null); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - - static Gestures() - { - InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); - InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); - } - - public static void AddTappedHandler(IInteractive element, EventHandler handler) - { - element.AddHandler(TappedEvent, handler); - } - - public static void AddDoubleTappedHandler(IInteractive element, EventHandler handler) - { - element.AddHandler(DoubleTappedEvent, handler); - } - - public static void AddRightTappedHandler(IInteractive element, EventHandler handler) - { - element.AddHandler(RightTappedEvent, handler); - } - - public static void RemoveTappedHandler(IInteractive element, EventHandler handler) - { - element.RemoveHandler(TappedEvent, handler); - } - - public static void RemoveDoubleTappedHandler(IInteractive element, EventHandler handler) - { - element.RemoveHandler(DoubleTappedEvent, handler); - } - - public static void RemoveRightTappedHandler(IInteractive element, EventHandler handler) - { - element.RemoveHandler(RightTappedEvent, handler); - } - - private static void PointerPressed(RoutedEventArgs ev) - { - if (ev.Source is null) - { - return; - } - - if (ev.Route == RoutingStrategies.Bubble) - { - var e = (PointerPressedEventArgs)ev; - var visual = (IVisual)ev.Source; - - if (e.ClickCount <= 1) - { - s_isDoubleTapped = false; - s_lastPress.SetTarget(ev.Source); - } - else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) - { - if (s_lastPress.TryGetTarget(out var target) && target == e.Source) - { - s_isDoubleTapped = true; - e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); - } - } - else - { - s_isDoubleTapped = false; - } - } - } - - private static void PointerReleased(RoutedEventArgs ev) - { - if (ev.Route == RoutingStrategies.Bubble) - { - var e = (PointerReleasedEventArgs)ev; - - if (s_lastPress.TryGetTarget(out var target) && target == e.Source) - { - if (e.InitialPressMouseButton == MouseButton.Left || e.InitialPressMouseButton == MouseButton.Right) - { - if (e.InitialPressMouseButton == MouseButton.Right) - { - e.Source.RaiseEvent(new TappedEventArgs(RightTappedEvent, e)); - } - //s_isDoubleTapped needed here to prevent invoking Tapped event when DoubleTapped is called. - //This behaviour matches UWP behaviour. - else if (s_isDoubleTapped == false) - { - e.Source.RaiseEvent(new TappedEventArgs(TappedEvent, e)); - } - } - } - } - } - } -} diff --git a/src/Avalonia.Input/IAccessKeyHandler.cs b/src/Avalonia.Input/IAccessKeyHandler.cs deleted file mode 100644 index e484d003c7..0000000000 --- a/src/Avalonia.Input/IAccessKeyHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Avalonia.Input -{ - /// - /// Defines the interface for classes that handle access keys for a window. - /// - public interface IAccessKeyHandler - { - /// - /// Gets or sets the window's main menu. - /// - IMainMenu? MainMenu { get; set; } - - /// - /// Sets the owner of the access key handler. - /// - /// The owner. - /// - /// This method can only be called once, typically by the owner itself on creation. - /// - void SetOwner(IInputRoot owner); - - /// - /// Registers an input element to be associated with an access key. - /// - /// The access key. - /// The input element. - void Register(char accessKey, IInputElement element); - - /// - /// Unregisters the access keys associated with the input element. - /// - /// The input element. - void Unregister(IInputElement element); - } -} diff --git a/src/Avalonia.Input/IFocusManager.cs b/src/Avalonia.Input/IFocusManager.cs deleted file mode 100644 index 2510479a8e..0000000000 --- a/src/Avalonia.Input/IFocusManager.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace Avalonia.Input -{ - /// - /// Manages focus for the application. - /// - public interface IFocusManager - { - /// - /// Gets the currently focused . - /// - IInputElement? Current { get; } - - /// - /// Gets the current focus scope. - /// - IFocusScope? Scope { get; } - - /// - /// Focuses a control. - /// - /// The control to focus. - /// The method by which focus was changed. - /// Any key modifiers active at the time of focus. - void Focus( - IInputElement? control, - NavigationMethod method = NavigationMethod.Unspecified, - KeyModifiers keyModifiers = KeyModifiers.None); - - /// - /// Notifies the focus manager of a change in focus scope. - /// - /// The new focus scope. - /// - /// This should not be called by client code. It is called by an - /// when it activates, e.g. when a Window is activated. - /// - void SetFocusScope(IFocusScope scope); - - /// - /// Notifies the focus manager that a focus scope has been removed. - /// - /// The focus scope to be removed. - /// This should not be called by client code. It is called by an - /// when it deactivates or closes, e.g. when a Window is closed. - void RemoveFocusScope(IFocusScope scope); - } -} diff --git a/src/Avalonia.Input/IInputDevice.cs b/src/Avalonia.Input/IInputDevice.cs deleted file mode 100644 index ab0fae65df..0000000000 --- a/src/Avalonia.Input/IInputDevice.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Avalonia.Input.Raw; - -namespace Avalonia.Input -{ - public interface IInputDevice - { - /// - /// Processes raw event. Is called after preprocessing by InputManager - /// - /// - void ProcessRawEvent(RawInputEventArgs ev); - } -} diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs deleted file mode 100644 index d1552a3a2a..0000000000 --- a/src/Avalonia.Input/IInputElement.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -#nullable enable - -namespace Avalonia.Input -{ - /// - /// Defines input-related functionality for a control. - /// - public interface IInputElement : IInteractive, IVisual - { - /// - /// Occurs when the control receives focus. - /// - event EventHandler? GotFocus; - - /// - /// Occurs when the control loses focus. - /// - event EventHandler? LostFocus; - - /// - /// Occurs when a key is pressed while the control has focus. - /// - event EventHandler? KeyDown; - - /// - /// Occurs when a key is released while the control has focus. - /// - event EventHandler? KeyUp; - - /// - /// Occurs when a user typed some text while the control has focus. - /// - event EventHandler? TextInput; - - /// - /// Occurs when the pointer enters the control. - /// - event EventHandler? PointerEnter; - - /// - /// Occurs when the pointer leaves the control. - /// - event EventHandler? PointerLeave; - - /// - /// Occurs when the pointer is pressed over the control. - /// - event EventHandler? PointerPressed; - - /// - /// Occurs when the pointer moves over the control. - /// - event EventHandler? PointerMoved; - - /// - /// Occurs when the pointer is released over the control. - /// - event EventHandler? PointerReleased; - - /// - /// Occurs when the mouse wheel is scrolled over the control. - /// - event EventHandler? PointerWheelChanged; - - /// - /// Gets or sets a value indicating whether the control can receive keyboard focus. - /// - bool Focusable { get; } - - /// - /// Gets or sets a value indicating whether the control is enabled for user interaction. - /// - bool IsEnabled { get; } - - /// - /// Gets or sets the associated mouse cursor. - /// - Cursor? Cursor { get; } - - /// - /// Gets a value indicating whether this control and all its parents are enabled. - /// - /// - /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the - /// value of this control and its parent controls. - /// - bool IsEffectivelyEnabled { get; } - - /// - /// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements. - /// - bool IsKeyboardFocusWithin { get; } - - /// - /// Gets a value indicating whether the control is focused. - /// - bool IsFocused { get; } - - /// - /// Gets a value indicating whether the control is considered for hit testing. - /// - bool IsHitTestVisible { get; } - - /// - /// Gets a value indicating whether the pointer is currently over the control. - /// - bool IsPointerOver { get; } - - /// - /// Focuses the control. - /// - void Focus(); - - /// - /// Gets the key bindings for the element. - /// - List KeyBindings { get; } - } -} diff --git a/src/Avalonia.Input/IInputManager.cs b/src/Avalonia.Input/IInputManager.cs deleted file mode 100644 index 80b71d3e47..0000000000 --- a/src/Avalonia.Input/IInputManager.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using Avalonia.Input.Raw; - -namespace Avalonia.Input -{ - /// - /// Receives input from the windowing subsystem and dispatches it to interested parties - /// for processing. - /// - public interface IInputManager - { - /// - /// Gets an observable that notifies on each input event received before - /// . - /// - IObservable PreProcess { get; } - - /// - /// Gets an observable that notifies on each input event received. - /// - IObservable Process { get; } - - /// - /// Gets an observable that notifies on each input event received after - /// . - /// - IObservable PostProcess { get; } - - /// - /// Processes a raw input event. - /// - /// The raw input event. - void ProcessInput(RawInputEventArgs e); - } -} diff --git a/src/Avalonia.Input/IInputRoot.cs b/src/Avalonia.Input/IInputRoot.cs deleted file mode 100644 index 3e2b8cc477..0000000000 --- a/src/Avalonia.Input/IInputRoot.cs +++ /dev/null @@ -1,36 +0,0 @@ -using JetBrains.Annotations; - -namespace Avalonia.Input -{ - /// - /// Defines the interface for top-level input elements. - /// - public interface IInputRoot : IInputElement - { - /// - /// Gets or sets the access key handler. - /// - IAccessKeyHandler AccessKeyHandler { get; } - - /// - /// Gets or sets the keyboard navigation handler. - /// - IKeyboardNavigationHandler KeyboardNavigationHandler { get; } - - /// - /// Gets or sets the input element that the pointer is currently over. - /// - IInputElement? PointerOverElement { get; set; } - - /// - /// Gets or sets a value indicating whether access keys are shown in the window. - /// - bool ShowAccessKeys { get; set; } - - /// - /// Gets associated mouse device - /// - [CanBeNull] - IMouseDevice? MouseDevice { get; } - } -} diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs deleted file mode 100644 index 9506dc36fb..0000000000 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.ComponentModel; - -namespace Avalonia.Input -{ - [Flags, Obsolete("Use KeyModifiers and PointerPointProperties")] - public enum InputModifiers - { - None = 0, - Alt = 1, - Control = 2, - Shift = 4, - Windows = 8, - LeftMouseButton = 16, - RightMouseButton = 32, - MiddleMouseButton = 64 - } - - [Flags] - public enum KeyModifiers - { - None = 0, - Alt = 1, - Control = 2, - Shift = 4, - Meta = 8, - } - - [Flags] - public enum KeyStates - { - None = 0, - Down = 1, - Toggled = 2, - } - - [Flags] - public enum RawInputModifiers - { - None = 0, - Alt = 1, - Control = 2, - Shift = 4, - Meta = 8, - LeftMouseButton = 16, - RightMouseButton = 32, - MiddleMouseButton = 64, - XButton1MouseButton = 128, - XButton2MouseButton = 256, - KeyboardMask = Alt | Control | Shift | Meta - } - - internal static class KeyModifiersUtils - { - public static KeyModifiers ConvertToKey(RawInputModifiers modifiers) => - (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); - } - - public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged - { - IInputElement? FocusedElement { get; } - - void SetFocusedElement( - IInputElement? element, - NavigationMethod method, - KeyModifiers modifiers); - } -} diff --git a/src/Avalonia.Input/IKeyboardNavigationHandler.cs b/src/Avalonia.Input/IKeyboardNavigationHandler.cs deleted file mode 100644 index 88d00b3b50..0000000000 --- a/src/Avalonia.Input/IKeyboardNavigationHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Avalonia.Input -{ - /// - /// Defines the interface for classes that handle keyboard navigation for a window. - /// - public interface IKeyboardNavigationHandler - { - /// - /// Sets the owner of the keyboard navigation handler. - /// - /// The owner. - /// - /// This method can only be called once, typically by the owner itself on creation. - /// - void SetOwner(IInputRoot owner); - - /// - /// Moves the focus in the specified direction. - /// - /// The current element. - /// The direction to move. - /// Any key modifiers active at the time of focus. - void Move( - IInputElement element, - NavigationDirection direction, - KeyModifiers keyModifiers = KeyModifiers.None); - } -} diff --git a/src/Avalonia.Input/IMainMenu.cs b/src/Avalonia.Input/IMainMenu.cs deleted file mode 100644 index 67b58c0ffc..0000000000 --- a/src/Avalonia.Input/IMainMenu.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - /// - /// Defines the interface for a window's main menu. - /// - public interface IMainMenu : IVisual - { - /// - /// Gets a value indicating whether the menu is open. - /// - bool IsOpen { get; } - - /// - /// Closes the menu. - /// - void Close(); - - /// - /// Opens the menu in response to the Alt/F10 key. - /// - void Open(); - - /// - /// Occurs when the main menu closes. - /// - event EventHandler? MenuClosed; - } -} diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs deleted file mode 100644 index 272d1eb8d7..0000000000 --- a/src/Avalonia.Input/IMouseDevice.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Avalonia.Input -{ - /// - /// Represents a mouse device. - /// - public interface IMouseDevice : IPointerDevice - { - /// - /// Gets the mouse position, in screen coordinates. - /// - [Obsolete("Use PointerEventArgs.GetPosition")] - PixelPoint Position { get; } - - void TopLevelClosed(IInputRoot root); - - void SceneInvalidated(IInputRoot root, Rect rect); - } -} diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs deleted file mode 100644 index 7af48cef82..0000000000 --- a/src/Avalonia.Input/IPointer.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Avalonia.Input -{ - public interface IPointer - { - int Id { get; } - void Capture(IInputElement? control); - IInputElement? Captured { get; } - PointerType Type { get; } - bool IsPrimary { get; } - - } - - public enum PointerType - { - Mouse, - Touch - } -} diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs deleted file mode 100644 index 1f82cb1ed7..0000000000 --- a/src/Avalonia.Input/IPointerDevice.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public interface IPointerDevice : IInputDevice - { - [Obsolete("Use IPointer")] - IInputElement? Captured { get; } - - [Obsolete("Use IPointer")] - void Capture(IInputElement? control); - - [Obsolete("Use PointerEventArgs.GetPosition")] - Point GetPosition(IVisual relativeTo); - } -} diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs deleted file mode 100644 index 5ec0bd6ee4..0000000000 --- a/src/Avalonia.Input/InputElement.cs +++ /dev/null @@ -1,710 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Metadata; -using Avalonia.Data; -using Avalonia.Input.GestureRecognizers; -using Avalonia.Input.TextInput; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -#nullable enable - -namespace Avalonia.Input -{ - /// - /// Implements input-related functionality for a control. - /// - [PseudoClasses(":disabled", ":focus", ":focus-visible", ":focus-within", ":pointerover")] - public class InputElement : Interactive, IInputElement - { - /// - /// Defines the property. - /// - public static readonly StyledProperty FocusableProperty = - AvaloniaProperty.Register(nameof(Focusable)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty IsEnabledProperty = - AvaloniaProperty.Register(nameof(IsEnabled), true); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsEffectivelyEnabledProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsEffectivelyEnabled), - o => o.IsEffectivelyEnabled); - - /// - /// Gets or sets associated mouse cursor. - /// - public static readonly StyledProperty CursorProperty = - AvaloniaProperty.Register(nameof(Cursor), null, true); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsKeyboardFocusWithinProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsKeyboardFocusWithin), - o => o.IsKeyboardFocusWithin); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsFocusedProperty = - AvaloniaProperty.RegisterDirect(nameof(IsFocused), o => o.IsFocused); - - /// - /// Defines the property. - /// - public static readonly StyledProperty IsHitTestVisibleProperty = - AvaloniaProperty.Register(nameof(IsHitTestVisible), true); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsPointerOverProperty = - AvaloniaProperty.RegisterDirect(nameof(IsPointerOver), o => o.IsPointerOver); - - /// - /// Defines the property. - /// - public static readonly StyledProperty IsTabStopProperty = - KeyboardNavigation.IsTabStopProperty.AddOwner(); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent GotFocusEvent = - RoutedEvent.Register(nameof(GotFocus), RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent LostFocusEvent = - RoutedEvent.Register(nameof(LostFocus), RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent KeyDownEvent = - RoutedEvent.Register( - "KeyDown", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent KeyUpEvent = - RoutedEvent.Register( - "KeyUp", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the property. - /// - public static readonly StyledProperty TabIndexProperty = - KeyboardNavigation.TabIndexProperty.AddOwner(); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent TextInputEvent = - RoutedEvent.Register( - "TextInput", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent TextInputMethodClientRequestedEvent = - RoutedEvent.Register( - "TextInputMethodClientRequested", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent TextInputOptionsQueryEvent = - RoutedEvent.Register( - "TextInputOptionsQuery", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerEnterEvent = - RoutedEvent.Register(nameof(PointerEnter), RoutingStrategies.Direct); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerLeaveEvent = - RoutedEvent.Register(nameof(PointerLeave), RoutingStrategies.Direct); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerMovedEvent = - RoutedEvent.Register( - "PointerMove", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerPressedEvent = - RoutedEvent.Register( - "PointerPressed", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerReleasedEvent = - RoutedEvent.Register( - "PointerReleased", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the routed event. - /// - public static readonly RoutedEvent PointerCaptureLostEvent = - RoutedEvent.Register( - "PointerCaptureLost", - RoutingStrategies.Direct); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerWheelChangedEvent = - RoutedEvent.Register( - "PointerWheelChanged", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; - - /// - /// Defines the event. - /// - public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; - - private bool _isEffectivelyEnabled = true; - private bool _isFocused; - private bool _isKeyboardFocusWithin; - private bool _isFocusVisible; - private bool _isPointerOver; - private GestureRecognizerCollection? _gestureRecognizers; - - /// - /// Initializes static members of the class. - /// - static InputElement() - { - IsEnabledProperty.Changed.Subscribe(IsEnabledChanged); - - GotFocusEvent.AddClassHandler((x, e) => x.OnGotFocus(e)); - LostFocusEvent.AddClassHandler((x, e) => x.OnLostFocus(e)); - KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e)); - KeyUpEvent.AddClassHandler((x, e) => x.OnKeyUp(e)); - TextInputEvent.AddClassHandler((x, e) => x.OnTextInput(e)); - PointerEnterEvent.AddClassHandler((x, e) => x.OnPointerEnterCore(e)); - PointerLeaveEvent.AddClassHandler((x, e) => x.OnPointerLeaveCore(e)); - PointerMovedEvent.AddClassHandler((x, e) => x.OnPointerMoved(e)); - PointerPressedEvent.AddClassHandler((x, e) => x.OnPointerPressed(e)); - PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); - PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); - PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); - } - - public InputElement() - { - UpdatePseudoClasses(IsFocused, IsPointerOver); - } - - /// - /// Occurs when the control receives focus. - /// - public event EventHandler? GotFocus - { - add { AddHandler(GotFocusEvent, value); } - remove { RemoveHandler(GotFocusEvent, value); } - } - - /// - /// Occurs when the control loses focus. - /// - public event EventHandler? LostFocus - { - add { AddHandler(LostFocusEvent, value); } - remove { RemoveHandler(LostFocusEvent, value); } - } - - /// - /// Occurs when a key is pressed while the control has focus. - /// - public event EventHandler? KeyDown - { - add { AddHandler(KeyDownEvent, value); } - remove { RemoveHandler(KeyDownEvent, value); } - } - - /// - /// Occurs when a key is released while the control has focus. - /// - public event EventHandler? KeyUp - { - add { AddHandler(KeyUpEvent, value); } - remove { RemoveHandler(KeyUpEvent, value); } - } - - /// - /// Occurs when a user typed some text while the control has focus. - /// - public event EventHandler? TextInput - { - add { AddHandler(TextInputEvent, value); } - remove { RemoveHandler(TextInputEvent, value); } - } - - /// - /// Occurs when an input element gains input focus and input method is looking for the corresponding client - /// - public event EventHandler? TextInputMethodClientRequested - { - add { AddHandler(TextInputMethodClientRequestedEvent, value); } - remove { RemoveHandler(TextInputMethodClientRequestedEvent, value); } - } - - /// - /// Occurs when an input element gains input focus and input method is asking for required content options - /// - public event EventHandler? TextInputOptionsQuery - { - add { AddHandler(TextInputOptionsQueryEvent, value); } - remove { RemoveHandler(TextInputOptionsQueryEvent, value); } - } - - /// - /// Occurs when the pointer enters the control. - /// - public event EventHandler? PointerEnter - { - add { AddHandler(PointerEnterEvent, value); } - remove { RemoveHandler(PointerEnterEvent, value); } - } - - /// - /// Occurs when the pointer leaves the control. - /// - public event EventHandler? PointerLeave - { - add { AddHandler(PointerLeaveEvent, value); } - remove { RemoveHandler(PointerLeaveEvent, value); } - } - - /// - /// Occurs when the pointer moves over the control. - /// - public event EventHandler? PointerMoved - { - add { AddHandler(PointerMovedEvent, value); } - remove { RemoveHandler(PointerMovedEvent, value); } - } - - /// - /// Occurs when the pointer is pressed over the control. - /// - public event EventHandler? PointerPressed - { - add { AddHandler(PointerPressedEvent, value); } - remove { RemoveHandler(PointerPressedEvent, value); } - } - - /// - /// Occurs when the pointer is released over the control. - /// - public event EventHandler? PointerReleased - { - add { AddHandler(PointerReleasedEvent, value); } - remove { RemoveHandler(PointerReleasedEvent, value); } - } - - /// - /// Occurs when the control or its child control loses the pointer capture for any reason, - /// event will not be triggered for a parent control if capture was transferred to another child of that parent control - /// - public event EventHandler? PointerCaptureLost - { - add => AddHandler(PointerCaptureLostEvent, value); - remove => RemoveHandler(PointerCaptureLostEvent, value); - } - - /// - /// Occurs when the mouse is scrolled over the control. - /// - public event EventHandler? PointerWheelChanged - { - add { AddHandler(PointerWheelChangedEvent, value); } - remove { RemoveHandler(PointerWheelChangedEvent, value); } - } - - /// - /// Occurs when a tap gesture occurs on the control. - /// - public event EventHandler? Tapped - { - add { AddHandler(TappedEvent, value); } - remove { RemoveHandler(TappedEvent, value); } - } - - /// - /// Occurs when a double-tap gesture occurs on the control. - /// - public event EventHandler? DoubleTapped - { - add { AddHandler(DoubleTappedEvent, value); } - remove { RemoveHandler(DoubleTappedEvent, value); } - } - - /// - /// Gets or sets a value indicating whether the control can receive focus. - /// - public bool Focusable - { - get { return GetValue(FocusableProperty); } - set { SetValue(FocusableProperty, value); } - } - - /// - /// Gets or sets a value indicating whether the control is enabled for user interaction. - /// - public bool IsEnabled - { - get { return GetValue(IsEnabledProperty); } - set { SetValue(IsEnabledProperty, value); } - } - - /// - /// Gets or sets associated mouse cursor. - /// - public Cursor? Cursor - { - get { return GetValue(CursorProperty); } - set { SetValue(CursorProperty, value); } - } - - /// - /// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements. - /// - public bool IsKeyboardFocusWithin - { - get => _isKeyboardFocusWithin; - internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value); - } - - /// - /// Gets a value indicating whether the control is focused. - /// - public bool IsFocused - { - get { return _isFocused; } - private set { SetAndRaise(IsFocusedProperty, ref _isFocused, value); } - } - - /// - /// Gets or sets a value indicating whether the control is considered for hit testing. - /// - public bool IsHitTestVisible - { - get { return GetValue(IsHitTestVisibleProperty); } - set { SetValue(IsHitTestVisibleProperty, value); } - } - - /// - /// Gets a value indicating whether the pointer is currently over the control. - /// - public bool IsPointerOver - { - get { return _isPointerOver; } - internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); } - } - - /// - /// Gets or sets a value that indicates whether the control is included in tab navigation. - /// - public bool IsTabStop - { - get => GetValue(IsTabStopProperty); - set => SetValue(IsTabStopProperty, value); - } - - /// - public bool IsEffectivelyEnabled - { - get => _isEffectivelyEnabled; - private set - { - SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); - PseudoClasses.Set(":disabled", !value); - } - } - - /// - /// Gets or sets a value that determines the order in which elements receive focus when the - /// user navigates through controls by pressing the Tab key. - /// - public int TabIndex - { - get => GetValue(TabIndexProperty); - set => SetValue(TabIndexProperty, value); - } - - public List KeyBindings { get; } = new List(); - - /// - /// Allows a derived class to override the enabled state of the control. - /// - /// - /// Derived controls may wish to disable the enabled state of the control without overwriting the - /// user-supplied setting. This can be done by overriding this property - /// to return the overridden enabled state. If the value returned from - /// should change, then the derived control should call . - /// - protected virtual bool IsEnabledCore => IsEnabled; - - public GestureRecognizerCollection GestureRecognizers - => _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this)); - - /// - /// Focuses the control. - /// - public void Focus() - { - FocusManager.Instance?.Focus(this); - } - - /// - protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTreeCore(e); - - if (IsFocused) - { - FocusManager.Instance?.Focus(null); - } - } - - /// - protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTreeCore(e); - UpdateIsEffectivelyEnabled(); - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnGotFocus(GotFocusEventArgs e) - { - var isFocused = e.Source == this; - _isFocusVisible = isFocused && (e.NavigationMethod == NavigationMethod.Directional || e.NavigationMethod == NavigationMethod.Tab); - IsFocused = isFocused; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnLostFocus(RoutedEventArgs e) - { - _isFocusVisible = false; - IsFocused = false; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnKeyDown(KeyEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnKeyUp(KeyEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnTextInput(TextInputEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerEnter(PointerEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerLeave(PointerEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerMoved(PointerEventArgs e) - { - if (_gestureRecognizers?.HandlePointerMoved(e) == true) - e.Handled = true; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerPressed(PointerPressedEventArgs e) - { - if (_gestureRecognizers?.HandlePointerPressed(e) == true) - e.Handled = true; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerReleased(PointerReleasedEventArgs e) - { - if (_gestureRecognizers?.HandlePointerReleased(e) == true) - e.Handled = true; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e) - { - _gestureRecognizers?.HandlePointerCaptureLost(e); - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerWheelChanged(PointerWheelEventArgs e) - { - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == IsFocusedProperty) - { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault(), null); - } - else if (change.Property == IsPointerOverProperty) - { - UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); - } - else if (change.Property == IsKeyboardFocusWithinProperty) - { - PseudoClasses.Set(":focus-within", change.NewValue.GetValueOrDefault()); - } - } - - /// - /// Updates the property value according to the parent - /// control's enabled state and . - /// - protected void UpdateIsEffectivelyEnabled() - { - UpdateIsEffectivelyEnabled(this.GetVisualParent()); - } - - private static void IsEnabledChanged(AvaloniaPropertyChangedEventArgs e) - { - ((InputElement)e.Sender).UpdateIsEffectivelyEnabled(); - } - - /// - /// Called before the event occurs. - /// - /// The event args. - private void OnPointerEnterCore(PointerEventArgs e) - { - IsPointerOver = true; - OnPointerEnter(e); - } - - /// - /// Called before the event occurs. - /// - /// The event args. - private void OnPointerLeaveCore(PointerEventArgs e) - { - IsPointerOver = false; - OnPointerLeave(e); - } - - /// - /// Updates the property based on the parent's - /// . - /// - /// The parent control. - private void UpdateIsEffectivelyEnabled(InputElement? parent) - { - IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true); - - // PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ - // will cause extra allocations and overhead. - - var children = VisualChildren; - - // ReSharper disable once ForCanBeConvertedToForeach - for (int i = 0; i < children.Count; ++i) - { - var child = children[i] as InputElement; - - child?.UpdateIsEffectivelyEnabled(this); - } - } - - private void UpdatePseudoClasses(bool? isFocused, bool? isPointerOver) - { - if (isFocused.HasValue) - { - PseudoClasses.Set(":focus", isFocused.Value); - PseudoClasses.Set(":focus-visible", _isFocusVisible); - } - - if (isPointerOver.HasValue) - { - PseudoClasses.Set(":pointerover", isPointerOver.Value); - } - } - } -} diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs deleted file mode 100644 index 0adbe73263..0000000000 --- a/src/Avalonia.Input/KeyGesture.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Input -{ - /// - /// Defines a keyboard input combination. - /// - public sealed class KeyGesture : IEquatable - { - private static readonly Dictionary s_keySynonyms = new Dictionary - { - { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod }, { ",", Key.OemComma } - }; - - [Obsolete("Use constructor taking KeyModifiers")] - public KeyGesture(Key key, InputModifiers modifiers) - { - Key = key; - KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xf); - } - - public KeyGesture(Key key, KeyModifiers modifiers = KeyModifiers.None) - { - Key = key; - KeyModifiers = modifiers; - } - - public bool Equals(KeyGesture? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - - return Key == other.Key && KeyModifiers == other.KeyModifiers; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - - return obj is KeyGesture gesture && Equals(gesture); - } - - public override int GetHashCode() - { - unchecked - { - return ((int)Key * 397) ^ (int)KeyModifiers; - } - } - - public static bool operator ==(KeyGesture? left, KeyGesture? right) - { - return Equals(left, right); - } - - public static bool operator !=(KeyGesture? left, KeyGesture? right) - { - return !Equals(left, right); - } - - public Key Key { get; } - - [Obsolete("Use KeyModifiers")] - public InputModifiers Modifiers => (InputModifiers)KeyModifiers; - - public KeyModifiers KeyModifiers { get; } - - public static KeyGesture Parse(string gesture) - { - // string.Split can't be used here because "Ctrl++" is a perfectly valid key gesture - - var key = Key.None; - var keyModifiers = KeyModifiers.None; - - var cstart = 0; - - for (var c = 0; c <= gesture.Length; c++) - { - var ch = c == gesture.Length ? '\0' : gesture[c]; - bool isLast = c == gesture.Length; - - if (isLast || (ch == '+' && cstart != c)) - { - var partSpan = gesture.AsSpan(cstart, c - cstart).Trim(); - - if (isLast) - { - key = ParseKey(partSpan.ToString()); - } - else - { - keyModifiers |= ParseModifier(partSpan); - } - - cstart = c + 1; - } - } - - - return new KeyGesture(key, keyModifiers); - } - - public override string ToString() - { - var s = new StringBuilder(); - - static void Plus(StringBuilder s) - { - if (s.Length > 0) - { - s.Append("+"); - } - } - - if (KeyModifiers.HasAllFlags(KeyModifiers.Control)) - { - s.Append("Ctrl"); - } - - if (KeyModifiers.HasAllFlags(KeyModifiers.Shift)) - { - Plus(s); - s.Append("Shift"); - } - - if (KeyModifiers.HasAllFlags(KeyModifiers.Alt)) - { - Plus(s); - s.Append("Alt"); - } - - if (KeyModifiers.HasAllFlags(KeyModifiers.Meta)) - { - Plus(s); - s.Append("Cmd"); - } - - Plus(s); - s.Append(Key); - - return s.ToString(); - } - - public bool Matches(KeyEventArgs keyEvent) => - keyEvent != null && - keyEvent.KeyModifiers == KeyModifiers && - ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(Key); - - // TODO: Move that to external key parser - private static Key ParseKey(string key) - { - if (s_keySynonyms.TryGetValue(key.ToLower(), out Key rv)) - return rv; - - return (Key)Enum.Parse(typeof(Key), key, true); - } - - private static KeyModifiers ParseModifier(ReadOnlySpan modifier) - { - if (modifier.Equals("ctrl".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - return KeyModifiers.Control; - } - - if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase) || - modifier.Equals("win".AsSpan(), StringComparison.OrdinalIgnoreCase) || - modifier.Equals("⌘".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - return KeyModifiers.Meta; - } - - return (KeyModifiers)Enum.Parse(typeof(KeyModifiers), modifier.ToString(), true); - } - - private Key ResolveNumPadOperationKey(Key key) - { - switch (key) - { - case Key.Add: - return Key.OemPlus; - case Key.Subtract: - return Key.OemMinus; - case Key.Decimal: - return Key.OemPeriod; - default: - return key; - } - } - } -} diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs deleted file mode 100644 index 438e3fcadb..0000000000 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ /dev/null @@ -1,269 +0,0 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; -using Avalonia.Input.Raw; -using Avalonia.Input.TextInput; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged - { - private IInputElement? _focusedElement; - private IInputRoot? _focusedRoot; - - public event PropertyChangedEventHandler? PropertyChanged; - - public static IKeyboardDevice? Instance => AvaloniaLocator.Current.GetService(); - - public IInputManager? InputManager => AvaloniaLocator.Current.GetService(); - - public IFocusManager? FocusManager => AvaloniaLocator.Current.GetService(); - - // This should live in the FocusManager, but with the current outdated architecture - // the source of truth about the input focus is in KeyboardDevice - private readonly TextInputMethodManager _textInputManager = new TextInputMethodManager(); - - public IInputElement? FocusedElement - { - get - { - return _focusedElement; - } - - private set - { - _focusedElement = value; - - if (_focusedElement != null && _focusedElement.IsAttachedToVisualTree) - { - _focusedRoot = _focusedElement.VisualRoot as IInputRoot; - } - else - { - _focusedRoot = null; - } - - RaisePropertyChanged(); - _textInputManager.SetFocusedElement(value); - } - } - - private void ClearFocusWithinAncestors(IInputElement? element) - { - var el = element; - - while (el != null) - { - if (el is InputElement ie) - { - ie.IsKeyboardFocusWithin = false; - } - - el = (IInputElement?)el.VisualParent; - } - } - - private void ClearFocusWithin(IInputElement element, bool clearRoot) - { - foreach (var visual in element.VisualChildren) - { - if (visual is IInputElement el && el.IsKeyboardFocusWithin) - { - ClearFocusWithin(el, true); - break; - } - } - - if (clearRoot) - { - if (element is InputElement ie) - { - ie.IsKeyboardFocusWithin = false; - } - } - } - - private void SetIsFocusWithin(IInputElement? oldElement, IInputElement? newElement) - { - if (newElement == null && oldElement != null) - { - ClearFocusWithinAncestors(oldElement); - return; - } - - IInputElement? branch = null; - - var el = newElement; - - while (el != null) - { - if (el.IsKeyboardFocusWithin) - { - branch = el; - break; - } - - el = el.VisualParent as IInputElement; - } - - el = oldElement; - - if (el != null && branch != null) - { - ClearFocusWithin(branch, false); - } - - el = newElement; - - while (el != null && el != branch) - { - if (el is InputElement ie) - { - ie.IsKeyboardFocusWithin = true; - } - - el = el.VisualParent as IInputElement; - } - } - - private void ClearChildrenFocusWithin(IInputElement element, bool clearRoot) - { - foreach (var visual in element.VisualChildren) - { - if (visual is IInputElement el && el.IsKeyboardFocusWithin) - { - ClearChildrenFocusWithin(el, true); - break; - } - } - - if (clearRoot && element is InputElement ie) - { - ie.IsKeyboardFocusWithin = false; - } - } - - public void SetFocusedElement( - IInputElement? element, - NavigationMethod method, - KeyModifiers keyModifiers) - { - if (element != FocusedElement) - { - var interactive = FocusedElement as IInteractive; - - if (FocusedElement != null && - (!FocusedElement.IsAttachedToVisualTree || - _focusedRoot != element?.VisualRoot as IInputRoot) && - _focusedRoot != null) - { - ClearChildrenFocusWithin(_focusedRoot, true); - } - - SetIsFocusWithin(FocusedElement, element); - - FocusedElement = element; - - interactive?.RaiseEvent(new RoutedEventArgs - { - RoutedEvent = InputElement.LostFocusEvent, - }); - - interactive = element as IInteractive; - - interactive?.RaiseEvent(new GotFocusEventArgs - { - RoutedEvent = InputElement.GotFocusEvent, - NavigationMethod = method, - KeyModifiers = keyModifiers, - }); - } - } - - protected void RaisePropertyChanged([CallerMemberName] string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - public void ProcessRawEvent(RawInputEventArgs e) - { - if(e.Handled) - return; - - var element = FocusedElement ?? e.Root; - - if (e is RawKeyEventArgs keyInput) - { - switch (keyInput.Type) - { - case RawKeyEventType.KeyDown: - case RawKeyEventType.KeyUp: - var routedEvent = keyInput.Type == RawKeyEventType.KeyDown - ? InputElement.KeyDownEvent - : InputElement.KeyUpEvent; - - KeyEventArgs ev = new KeyEventArgs - { - RoutedEvent = routedEvent, - Device = this, - Key = keyInput.Key, - KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), - Source = element, - }; - - IVisual? currentHandler = element; - while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown) - { - var bindings = (currentHandler as IInputElement)?.KeyBindings; - if (bindings != null) - { - KeyBinding[]? bindingsCopy = null; - - // Create a copy of the KeyBindings list if there's a binding which matches the event. - // If we don't do this the foreach loop will throw an InvalidOperationException when the KeyBindings list is changed. - // This can happen when a new view is loaded which adds its own KeyBindings to the handler. - foreach (var binding in bindings) - { - if (binding.Gesture?.Matches(ev) == true) - { - bindingsCopy = bindings.ToArray(); - break; - } - } - - if (bindingsCopy is object) - { - foreach (var binding in bindingsCopy) - { - if (ev.Handled) - break; - binding.TryHandle(ev); - } - } - } - currentHandler = currentHandler.VisualParent; - } - - element.RaiseEvent(ev); - e.Handled = ev.Handled; - break; - } - } - - if (e is RawTextInputEventArgs text) - { - var ev = new TextInputEventArgs() - { - Device = this, - Text = text.Text, - Source = element, - RoutedEvent = InputElement.TextInputEvent - }; - - element.RaiseEvent(ev); - e.Handled = ev.Handled; - } - } - } -} diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs deleted file mode 100644 index a25aed6811..0000000000 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ /dev/null @@ -1,134 +0,0 @@ -namespace Avalonia.Input -{ - /// - /// Defines attached properties that control keyboard navigation behaviour for a container. - /// - public static class KeyboardNavigation - { - /// - /// Defines the TabIndex attached property. - /// - public static readonly AttachedProperty TabIndexProperty = - AvaloniaProperty.RegisterAttached( - "TabIndex", - typeof(KeyboardNavigation), - int.MaxValue); - - /// - /// Defines the TabNavigation attached property. - /// - /// - /// The TabNavigation attached property defines how pressing the Tab key causes focus to - /// be navigated between the children of the container. - /// - public static readonly AttachedProperty TabNavigationProperty = - AvaloniaProperty.RegisterAttached( - "TabNavigation", - typeof(KeyboardNavigation)); - - /// - /// Defines the TabOnceActiveElement attached property. - /// - /// - /// When focus enters a container which has its - /// attached property set to , this property - /// defines to which child the focus should move. - /// - public static readonly AttachedProperty TabOnceActiveElementProperty = - AvaloniaProperty.RegisterAttached( - "TabOnceActiveElement", - typeof(KeyboardNavigation)); - - /// - /// Defines the IsTabStop attached property. - /// - /// - /// The IsTabStop attached property determines whether the control is focusable by tab navigation. - /// - public static readonly AttachedProperty IsTabStopProperty = - AvaloniaProperty.RegisterAttached( - "IsTabStop", - typeof(KeyboardNavigation), - true); - - /// - /// Gets the for an element. - /// - /// The container. - /// The for the container. - public static int GetTabIndex(IInputElement element) - { - return ((IAvaloniaObject)element).GetValue(TabIndexProperty); - } - - /// - /// Sets the for an element. - /// - /// The element. - /// The tab index. - public static void SetTabIndex(IInputElement element, int value) - { - ((IAvaloniaObject)element).SetValue(TabIndexProperty, value); - } - - /// - /// Gets the for a container. - /// - /// The container. - /// The for the container. - public static KeyboardNavigationMode GetTabNavigation(InputElement element) - { - return element.GetValue(TabNavigationProperty); - } - - /// - /// Sets the for a container. - /// - /// The container. - /// The for the container. - public static void SetTabNavigation(InputElement element, KeyboardNavigationMode value) - { - element.SetValue(TabNavigationProperty, value); - } - - /// - /// Gets the for a container. - /// - /// The container. - /// The active element for the container. - public static IInputElement? GetTabOnceActiveElement(InputElement element) - { - return element.GetValue(TabOnceActiveElementProperty); - } - - /// - /// Sets the for a container. - /// - /// The container. - /// The active element for the container. - public static void SetTabOnceActiveElement(InputElement element, IInputElement? value) - { - element.SetValue(TabOnceActiveElementProperty, value); - } - - /// - /// Sets the for an element. - /// - /// The container. - /// Value indicating whether the container is a tab stop. - public static void SetIsTabStop(InputElement element, bool value) - { - element.SetValue(IsTabStopProperty, value); - } - - /// - /// Gets the for an element. - /// - /// The container. - /// Whether the container is a tab stop. - public static bool GetIsTabStop(InputElement element) - { - return element.GetValue(IsTabStopProperty); - } - } -} diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs deleted file mode 100644 index eed51e6c4a..0000000000 --- a/src/Avalonia.Input/MouseDevice.cs +++ /dev/null @@ -1,569 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Input.Raw; -using Avalonia.Interactivity; -using Avalonia.Platform; -using Avalonia.Utilities; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - /// - /// Represents a mouse device. - /// - public class MouseDevice : IMouseDevice, IDisposable - { - private int _clickCount; - private Rect _lastClickRect; - private ulong _lastClickTime; - - private readonly Pointer _pointer; - private bool _disposed; - private PixelPoint? _position; - - public MouseDevice(Pointer? pointer = null) - { - _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); - } - - /// - /// Gets the control that is currently capturing by the mouse, if any. - /// - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. To set the mouse capture, call the - /// method. - /// - [Obsolete("Use IPointer instead")] - public IInputElement? Captured => _pointer.Captured; - - /// - /// Gets the mouse position, in screen coordinates. - /// - [Obsolete("Use events instead")] - public PixelPoint Position - { - get => _position ?? new PixelPoint(-1, -1); - protected set => _position = value; - } - - /// - /// Captures mouse input to the specified control. - /// - /// The control. - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. The current mouse capture control is exposed - /// by the property. - /// - public void Capture(IInputElement? control) - { - _pointer.Capture(control); - } - - /// - /// Gets the mouse position relative to a control. - /// - /// The control. - /// The mouse position in the control's coordinates. - public Point GetPosition(IVisual relativeTo) - { - relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); - - if (relativeTo.VisualRoot == null) - { - throw new InvalidOperationException("Control is not attached to visual tree."); - } - -#pragma warning disable CS0618 // Type or member is obsolete - var rootPoint = relativeTo.VisualRoot.PointToClient(Position); -#pragma warning restore CS0618 // Type or member is obsolete - var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); - return rootPoint * transform!.Value; - } - - public void ProcessRawEvent(RawInputEventArgs e) - { - if (!e.Handled && e is RawPointerEventArgs margs) - ProcessRawEvent(margs); - } - - public void TopLevelClosed(IInputRoot root) - { - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - } - - public void SceneInvalidated(IInputRoot root, Rect rect) - { - // Pointer is outside of the target area - if (_position == null ) - { - if (root.PointerOverElement != null) - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - return; - } - - - var clientPoint = root.PointToClient(_position.Value); - - if (rect.Contains(clientPoint)) - { - if (_pointer.Captured == null) - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, - PointerPointProperties.None, KeyModifiers.None); - } - else - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, - PointerPointProperties.None, KeyModifiers.None); - } - } - } - - int ButtonCount(PointerPointProperties props) - { - var rv = 0; - if (props.IsLeftButtonPressed) - rv++; - if (props.IsMiddleButtonPressed) - rv++; - if (props.IsRightButtonPressed) - rv++; - if (props.IsXButton1Pressed) - rv++; - if (props.IsXButton2Pressed) - rv++; - return rv; - } - - private void ProcessRawEvent(RawPointerEventArgs e) - { - e = e ?? throw new ArgumentNullException(nameof(e)); - - var mouse = (MouseDevice)e.Device; - if(mouse._disposed) - return; - - _position = e.Root.PointToScreen(e.Position); - var props = CreateProperties(e); - var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); - switch (e.Type) - { - case RawPointerEventType.LeaveWindow: - LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); - break; - case RawPointerEventType.LeftButtonDown: - case RawPointerEventType.RightButtonDown: - case RawPointerEventType.MiddleButtonDown: - case RawPointerEventType.XButton1Down: - case RawPointerEventType.XButton2Down: - if (ButtonCount(props) > 1) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); - else - e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - props, keyModifiers); - break; - case RawPointerEventType.LeftButtonUp: - case RawPointerEventType.RightButtonUp: - case RawPointerEventType.MiddleButtonUp: - case RawPointerEventType.XButton1Up: - case RawPointerEventType.XButton2Up: - if (ButtonCount(props) != 0) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); - else - e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - break; - case RawPointerEventType.Move: - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); - break; - case RawPointerEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); - break; - case RawPointerEventType.Magnify: - e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); - break; - case RawPointerEventType.Rotate: - e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); - break; - case RawPointerEventType.Swipe: - e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); - break; - } - } - - private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - _position = null; - ClearPointerOver(this, timestamp, root, properties, inputModifiers); - } - - - PointerPointProperties CreateProperties(RawPointerEventArgs args) - { - - var kind = PointerUpdateKind.Other; - - if (args.Type == RawPointerEventType.LeftButtonDown) - kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.MiddleButtonDown) - kind = PointerUpdateKind.MiddleButtonPressed; - if (args.Type == RawPointerEventType.RightButtonDown) - kind = PointerUpdateKind.RightButtonPressed; - if (args.Type == RawPointerEventType.XButton1Down) - kind = PointerUpdateKind.XButton1Pressed; - if (args.Type == RawPointerEventType.XButton2Down) - kind = PointerUpdateKind.XButton2Pressed; - if (args.Type == RawPointerEventType.LeftButtonUp) - kind = PointerUpdateKind.LeftButtonReleased; - if (args.Type == RawPointerEventType.MiddleButtonUp) - kind = PointerUpdateKind.MiddleButtonReleased; - if (args.Type == RawPointerEventType.RightButtonUp) - kind = PointerUpdateKind.RightButtonReleased; - if (args.Type == RawPointerEventType.XButton1Up) - kind = PointerUpdateKind.XButton1Released; - if (args.Type == RawPointerEventType.XButton2Up) - kind = PointerUpdateKind.XButton2Released; - - return new PointerPointProperties(args.InputModifiers, kind); - } - - private MouseButton _lastMouseDownButton; - private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - _pointer.Capture(hit); - var source = GetSource(hit); - if (source != null) - { - var settings = AvaloniaLocator.Current.GetService(); - var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; - var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); - - if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) - { - _clickCount = 0; - } - - ++_clickCount; - _lastClickTime = timestamp; - _lastClickRect = new Rect(p, new Size()) - .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); - _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); - source.RaiseEvent(e); - return e.Handled; - } - } - - return false; - } - - private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers, Lazy?>? intermediatePoints) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - IInputElement? source; - - if (_pointer.Captured == null) - { - source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); - } - else - { - SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); - source = _pointer.Captured; - } - - if (source is object) - { - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, - p, timestamp, properties, inputModifiers, intermediatePoints); - - source.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); - - if (source is not null) - { - var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, - _lastMouseDownButton); - - source?.RaiseEvent(e); - _pointer.Capture(null); - return e.Handled; - } - - return false; - } - - private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, - Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); - - // 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); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - var source = GetSource(hit); - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source, - _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - var source = GetSource(hit); - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source, - _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - var source = GetSource(hit); - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source, - _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private IInteractive? GetSource(IVisual? hit) - { - if (hit is null) - return null; - - return _pointer.Captured ?? - (hit as IInteractive) ?? - hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - } - - private IInputElement? HitTest(IInputElement root, Point p) - { - root = root ?? throw new ArgumentNullException(nameof(root)); - - return _pointer.Captured ?? root.InputHitTest(p); - } - - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - return new PointerEventArgs(ev, source, _pointer, null, default, - timestamp, properties, inputModifiers); - } - - private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); - - if (element!=null && !element.IsAttachedToVisualTree) - { - // element has been removed from visual tree so do top down cleanup - if (root.IsPointerOver) - ClearChildrenPointerOver(e, root,true); - } - while (element != null) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - element = (IInputElement?)element.VisualParent; - } - - root.PointerOverElement = null; - } - - private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) - { - foreach (IInputElement el in element.VisualChildren) - { - if (el.IsPointerOver) - { - ClearChildrenPointerOver(e, el, true); - break; - } - } - if(clearRoot) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - } - } - - private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.InputHitTest(p); - - if (element != root.PointerOverElement) - { - if (element != null) - { - SetPointerOver(device, timestamp, root, element, properties, inputModifiers); - } - else - { - ClearPointerOver(device, timestamp, root, properties, inputModifiers); - } - } - - return element; - } - - private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - element = element ?? throw new ArgumentNullException(nameof(element)); - - IInputElement? branch = null; - - IInputElement? el = element; - - while (el != null) - { - if (el.IsPointerOver) - { - branch = el; - break; - } - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement; - - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); - if (el!=null && branch!=null && !el.IsAttachedToVisualTree) - { - ClearChildrenPointerOver(e,branch,false); - } - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement = element; - e.RoutedEvent = InputElement.PointerEnterEvent; - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - } - - public void Dispose() - { - _disposed = true; - _pointer?.Dispose(); - } - } -} diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs deleted file mode 100644 index c8290cb3b7..0000000000 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ /dev/null @@ -1,673 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.VisualTree; - -namespace Avalonia.Input.Navigation -{ - /// - /// The implementation for default tab navigation. - /// - internal static class TabNavigation - { - public static IInputElement? GetNextTab(IInputElement e, bool goDownOnly) - { - return GetNextTab(e, GetGroupParent(e), goDownOnly); - } - - public static IInputElement? GetNextTab(IInputElement? e, IInputElement container, bool goDownOnly) - { - var tabbingType = GetKeyNavigationMode(container); - - if (e == null) - { - if (IsTabStop(container)) - return container; - - // Using ActiveElement if set - var activeElement = GetActiveElement(container); - if (activeElement != null) - return GetNextTab(null, activeElement, true); - } - else - { - if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None) - { - if (container != e) - { - if (goDownOnly) - return null; - var parentContainer = GetGroupParent(container); - return GetNextTab(container, parentContainer, goDownOnly); - } - } - } - - // All groups - IInputElement? loopStartElement = null; - var nextTabElement = e; - var currentTabbingType = tabbingType; - - // Search down inside the container - while ((nextTabElement = GetNextTabInGroup(nextTabElement, container, currentTabbingType)) != null) - { - // Avoid the endless loop here for Cycle groups - if (loopStartElement == nextTabElement) - break; - if (loopStartElement == null) - loopStartElement = nextTabElement; - - var firstTabElementInside = GetNextTab(null, nextTabElement, true); - if (firstTabElementInside != null) - return firstTabElementInside; - - // If we want to continue searching inside the Once groups, we should change the navigation mode - if (currentTabbingType == KeyboardNavigationMode.Once) - currentTabbingType = KeyboardNavigationMode.Contained; - } - - // If there is no next element in the group (nextTabElement == null) - - // Search up in the tree if allowed - // consider: Use original tabbingType instead of currentTabbingType - if (!goDownOnly && currentTabbingType != KeyboardNavigationMode.Contained && GetParent(container) != null) - { - return GetNextTab(container, GetGroupParent(container), false); - } - - return null; - } - - public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e) - { - if (e is IInputElement container) - { - var last = GetLastInTree(container); - - if (last is object) - return GetNextTab(last, false); - } - - return null; - } - - public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly) - { - if (e is null && container is null) - throw new InvalidOperationException("Either 'e' or 'container' must be non-null."); - - if (container is null) - container = GetGroupParent(e!); - - KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container); - - if (e == null) - { - // Using ActiveElement if set - var activeElement = GetActiveElement(container); - if (activeElement != null) - return GetPrevTab(null, activeElement, true); - else - { - // If we Shift+Tab on a container with KeyboardNavigationMode=Once, and ActiveElement is null - // then we want to go to the first item (not last) within the container - if (tabbingType == KeyboardNavigationMode.Once) - { - var firstTabElement = GetNextTabInGroup(null, container, tabbingType); - if (firstTabElement == null) - { - if (IsTabStop(container)) - return container; - if (goDownOnly) - return null; - - return GetPrevTab(container, null, false); - } - else - { - return GetPrevTab(null, firstTabElement, true); - } - } - } - } - else - { - if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None) - { - if (goDownOnly || container == e) - return null; - - // FocusedElement should not be e otherwise we will delegate focus to the same element - if (IsTabStop(container)) - return container; - - return GetPrevTab(container, null, false); - } - } - - // All groups (except Once) - continue - IInputElement? loopStartElement = null; - IInputElement? nextTabElement = e; - - // Look for element with the same TabIndex before the current element - while ((nextTabElement = GetPrevTabInGroup(nextTabElement, container, tabbingType)) != null) - { - if (nextTabElement == container && tabbingType == KeyboardNavigationMode.Local) - break; - - // At this point nextTabElement is TabStop or TabGroup - // In case it is a TabStop only return the element - if (IsTabStop(nextTabElement) && !IsGroup(nextTabElement)) - return nextTabElement; - - // Avoid the endless loop here - if (loopStartElement == nextTabElement) - break; - if (loopStartElement == null) - loopStartElement = nextTabElement; - - // At this point nextTabElement is TabGroup - var lastTabElementInside = GetPrevTab(null, nextTabElement, true); - if (lastTabElementInside != null) - return lastTabElementInside; - } - - if (tabbingType == KeyboardNavigationMode.Contained) - return null; - - if (e != container && IsTabStop(container)) - return container; - - // If end of the subtree is reached or there no other elements above - if (!goDownOnly && GetParent(container) != null) - { - return GetPrevTab(container, null, false); - } - - return null; - } - - public static IInputElement? GetPrevTabOutside(ICustomKeyboardNavigation e) - { - if (e is IInputElement container) - { - var first = GetFirstChild(container); - - if (first is object) - return GetPrevTab(first, null, false); - } - - return null; - } - - private static IInputElement? FocusedElement(IInputElement e) - { - var iie = e; - // Focus delegation is enabled only if keyboard focus is outside the container - if (iie != null && !iie.IsKeyboardFocusWithin) - { - var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e); - if (focusedElement != null) - { - if (!IsFocusScope(e)) - { - // Verify if focusedElement is a visual descendant of e - if (focusedElement is IVisual visualFocusedElement && - visualFocusedElement != e && - e.IsVisualAncestorOf(visualFocusedElement)) - { - return focusedElement; - } - } - } - } - - return null; - } - - private static IInputElement? GetFirstChild(IInputElement e) - { - // If the element has a FocusedElement it should be its first child - if (FocusedElement(e) is IInputElement focusedElement) - return focusedElement; - - // Return the first visible element. - var uiElement = e as InputElement; - - if (uiElement is null || IsVisibleAndEnabled(uiElement)) - { - if (e is IVisual elementAsVisual) - { - var children = elementAsVisual.VisualChildren; - var count = children.Count; - - for (int i = 0; i < count; i++) - { - if (children[i] is InputElement ie) - { - if (IsVisibleAndEnabled(ie)) - return ie; - else - { - var firstChild = GetFirstChild(ie); - if (firstChild != null) - return firstChild; - } - } - } - } - } - - return null; - } - - private static IInputElement? GetLastChild(IInputElement e) - { - // If the element has a FocusedElement it should be its last child - if (FocusedElement(e) is IInputElement focusedElement) - return focusedElement; - - // Return the last visible element. - var uiElement = e as InputElement; - - if (uiElement == null || IsVisibleAndEnabled(uiElement)) - { - var elementAsVisual = e as IVisual; - - if (elementAsVisual != null) - { - var children = elementAsVisual.VisualChildren; - var count = children.Count; - - for (int i = count - 1; i >= 0; i--) - { - if (children[i] is InputElement ie) - { - if (IsVisibleAndEnabled(ie)) - return ie; - else - { - var lastChild = GetLastChild(ie); - if (lastChild != null) - return lastChild; - } - } - } - } - } - - return null; - } - - private static IInputElement? GetFirstTabInGroup(IInputElement container) - { - IInputElement? firstTabElement = null; - int minIndexFirstTab = int.MinValue; - - var currElement = container; - while ((currElement = GetNextInTree(currElement, container)) != null) - { - if (IsTabStopOrGroup(currElement)) - { - int currPriority = KeyboardNavigation.GetTabIndex(currElement); - - if (currPriority < minIndexFirstTab || firstTabElement == null) - { - minIndexFirstTab = currPriority; - firstTabElement = currElement; - } - } - } - return firstTabElement; - } - - private static IInputElement? GetLastInTree(IInputElement container) - { - IInputElement? result; - IInputElement? c = container; - - do - { - result = c; - c = GetLastChild(c); - } while (c != null && !IsGroup(c)); - - if (c != null) - return c; - - return result; - } - - private static IInputElement? GetLastTabInGroup(IInputElement container) - { - IInputElement? lastTabElement = null; - int maxIndexFirstTab = int.MaxValue; - var currElement = GetLastInTree(container); - while (currElement != null && currElement != container) - { - if (IsTabStopOrGroup(currElement)) - { - int currPriority = KeyboardNavigation.GetTabIndex(currElement); - - if (currPriority > maxIndexFirstTab || lastTabElement == null) - { - maxIndexFirstTab = currPriority; - lastTabElement = currElement; - } - } - currElement = GetPreviousInTree(currElement, container); - } - return lastTabElement; - } - - private static IInputElement? GetNextInTree(IInputElement e, IInputElement container) - { - IInputElement? result = null; - - if (e == container || !IsGroup(e)) - result = GetFirstChild(e); - - if (result != null || e == container) - return result; - - IInputElement? parent = e; - do - { - var sibling = GetNextSibling(parent); - if (sibling != null) - return sibling; - - parent = GetParent(parent); - } while (parent != null && parent != container); - - return null; - } - - private static IInputElement? GetNextSibling(IInputElement e) - { - if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual) - { - var children = parentAsVisual.VisualChildren; - var count = children.Count; - var i = 0; - - //go till itself - for (; i < count; i++) - { - var vchild = children[i]; - if (vchild == elementAsVisual) - break; - } - i++; - //search ahead - for (; i < count; i++) - { - var visual = children[i]; - if (visual is IInputElement ie) - return ie; - } - } - - return null; - } - - private static IInputElement? GetNextTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType) - { - // None groups: Tab navigation is not supported - if (tabbingType == KeyboardNavigationMode.None) - return null; - - // e == null or e == container -> return the first TabStopOrGroup - if (e == null || e == container) - { - return GetFirstTabInGroup(container); - } - - if (tabbingType == KeyboardNavigationMode.Once) - return null; - - var nextTabElement = GetNextTabWithSameIndex(e, container); - if (nextTabElement != null) - return nextTabElement; - - return GetNextTabWithNextIndex(e, container, tabbingType); - } - - private static IInputElement? GetNextTabWithSameIndex(IInputElement e, IInputElement container) - { - var elementTabPriority = KeyboardNavigation.GetTabIndex(e); - var currElement = e; - while ((currElement = GetNextInTree(currElement, container)) != null) - { - if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority) - { - return currElement; - } - } - - return null; - } - - private static IInputElement? GetNextTabWithNextIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType) - { - // Find the next min index in the tree - // min (index>currentTabIndex) - IInputElement? nextTabElement = null; - IInputElement? firstTabElement = null; - int minIndexFirstTab = int.MinValue; - int minIndex = int.MinValue; - int elementTabPriority = KeyboardNavigation.GetTabIndex(e); - - IInputElement? currElement = container; - while ((currElement = GetNextInTree(currElement, container)) != null) - { - if (IsTabStopOrGroup(currElement)) - { - int currPriority = KeyboardNavigation.GetTabIndex(currElement); - if (currPriority > elementTabPriority) - { - if (currPriority < minIndex || nextTabElement == null) - { - minIndex = currPriority; - nextTabElement = currElement; - } - } - - if (currPriority < minIndexFirstTab || firstTabElement == null) - { - minIndexFirstTab = currPriority; - firstTabElement = currElement; - } - } - } - - // Cycle groups: if not found - return first element - if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null) - nextTabElement = firstTabElement; - - return nextTabElement; - } - - private static IInputElement? GetPrevTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType) - { - // None groups: Tab navigation is not supported - if (tabbingType == KeyboardNavigationMode.None) - return null; - - // Search the last index inside the group - if (e == null) - { - return GetLastTabInGroup(container); - } - - if (tabbingType == KeyboardNavigationMode.Once) - return null; - - if (e == container) - return null; - - var nextTabElement = GetPrevTabWithSameIndex(e, container); - if (nextTabElement != null) - return nextTabElement; - - return GetPrevTabWithPrevIndex(e, container, tabbingType); - } - - private static IInputElement? GetPrevTabWithSameIndex(IInputElement e, IInputElement container) - { - int elementTabPriority = KeyboardNavigation.GetTabIndex(e); - var currElement = GetPreviousInTree(e, container); - while (currElement != null) - { - if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority && currElement != container) - { - return currElement; - } - currElement = GetPreviousInTree(currElement, container); - } - return null; - } - - private static IInputElement? GetPrevTabWithPrevIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType) - { - // Find the next max index in the tree - // max (index maxIndex || nextTabElement == null) - { - maxIndex = currPriority; - nextTabElement = currElement; - } - } - - if (currPriority > maxIndexFirstTab || lastTabElement == null) - { - maxIndexFirstTab = currPriority; - lastTabElement = currElement; - } - } - - currElement = GetPreviousInTree(currElement, container); - } - - // Cycle groups: if not found - return first element - if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null) - nextTabElement = lastTabElement; - - return nextTabElement; - } - - private static IInputElement? GetPreviousInTree(IInputElement e, IInputElement container) - { - if (e == container) - return null; - - var result = GetPreviousSibling(e); - - if (result != null) - { - if (IsGroup(result)) - return result; - else - return GetLastInTree(result); - } - else - return GetParent(e); - } - - private static IInputElement? GetPreviousSibling(IInputElement e) - { - if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual) - { - var children = parentAsVisual.VisualChildren; - var count = children.Count; - IInputElement? prev = null; - - for (int i = 0; i < count; i++) - { - var vchild = children[i]; - if (vchild == elementAsVisual) - break; - if (vchild is IInputElement ie && IsVisibleAndEnabled(ie)) - prev = ie; - } - return prev; - } - return null; - } - - private static IInputElement? GetActiveElement(IInputElement e) - { - return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabOnceActiveElementProperty); - } - - private static IInputElement GetGroupParent(IInputElement e) => GetGroupParent(e, false); - - private static IInputElement GetGroupParent(IInputElement element, bool includeCurrent) - { - var result = element; // Keep the last non null element - var e = element; - - // If we don't want to include the current element, - // start at the parent of the element. If the element - // is the root, then just return it as the group parent. - if (!includeCurrent) - { - result = e; - e = GetParent(e); - if (e == null) - return result; - } - - while (e != null) - { - if (IsGroup(e)) - return e; - - result = e; - e = GetParent(e); - } - - return result; - } - - private static IInputElement? GetParent(IInputElement e) - { - // For Visual - go up the visual parent chain until we find Visual. - if (e is IVisual v) - return v.FindAncestorOfType(); - - // This will need to be implemented when we have non-visual input elements. - throw new NotSupportedException(); - } - - private static KeyboardNavigationMode GetKeyNavigationMode(IInputElement e) - { - return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabNavigationProperty); - } - private static bool IsFocusScope(IInputElement e) => FocusManager.GetIsFocusScope(e) || GetParent(e) == null; - private static bool IsGroup(IInputElement e) => GetKeyNavigationMode(e) != KeyboardNavigationMode.Continue; - - private static bool IsTabStop(IInputElement e) - { - if (e is InputElement ie) - return ie.Focusable && KeyboardNavigation.GetIsTabStop(ie) && ie.IsVisible && ie.IsEnabled; - return false; - } - - private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e); - private static bool IsVisibleAndEnabled(IInputElement e) => e.IsVisible && e.IsEnabled; - } -} diff --git a/src/Avalonia.Input/Platform/IClipboard.cs b/src/Avalonia.Input/Platform/IClipboard.cs deleted file mode 100644 index eb880904eb..0000000000 --- a/src/Avalonia.Input/Platform/IClipboard.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading.Tasks; - -namespace Avalonia.Input.Platform -{ - public interface IClipboard - { - Task GetTextAsync(); - - Task SetTextAsync(string text); - - Task ClearAsync(); - - Task SetDataObjectAsync(IDataObject data); - - Task GetFormatsAsync(); - - Task GetDataAsync(string format); - } -} diff --git a/src/Avalonia.Input/Platform/ICursorImpl.cs b/src/Avalonia.Input/Platform/ICursorImpl.cs deleted file mode 100644 index 14235869f7..0000000000 --- a/src/Avalonia.Input/Platform/ICursorImpl.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Avalonia.Input; - -#nullable enable - -namespace Avalonia.Platform -{ - /// - /// Represents a platform implementation of a . - /// - public interface ICursorImpl : IDisposable - { - } -} diff --git a/src/Avalonia.Input/Platform/IPlatformDragSource.cs b/src/Avalonia.Input/Platform/IPlatformDragSource.cs deleted file mode 100644 index 30d8ee5337..0000000000 --- a/src/Avalonia.Input/Platform/IPlatformDragSource.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace Avalonia.Input.Platform -{ - public interface IPlatformDragSource - { - Task DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects); - } -} diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs deleted file mode 100644 index 0604d09dc4..0000000000 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Input.Raw; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public class PointerEventArgs : RoutedEventArgs - { - private readonly IVisual? _rootVisual; - private readonly Point _rootVisualPosition; - private readonly PointerPointProperties _properties; - private Lazy?>? _previousPoints; - - public PointerEventArgs(RoutedEvent routedEvent, - IInteractive? source, - IPointer pointer, - IVisual? rootVisual, Point rootVisualPosition, - ulong timestamp, - PointerPointProperties properties, - KeyModifiers modifiers) - : base(routedEvent) - { - Source = source; - _rootVisual = rootVisual; - _rootVisualPosition = rootVisualPosition; - _properties = properties; - Pointer = pointer; - Timestamp = timestamp; - KeyModifiers = modifiers; - } - - public PointerEventArgs(RoutedEvent routedEvent, - IInteractive? source, - IPointer pointer, - IVisual? rootVisual, Point rootVisualPosition, - ulong timestamp, - PointerPointProperties properties, - KeyModifiers modifiers, - Lazy?>? previousPoints) - : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) - { - _previousPoints = previousPoints; - } - - - class EmulatedDevice : IPointerDevice - { - private readonly PointerEventArgs _ev; - - public EmulatedDevice(PointerEventArgs ev) - { - _ev = ev; - } - - public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException(); - - public IInputElement? Captured => _ev.Pointer.Captured; - public void Capture(IInputElement? control) - { - _ev.Pointer.Capture(control); - } - - public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo); - } - - public IPointer Pointer { get; } - public ulong Timestamp { get; } - - private IPointerDevice? _device; - - [Obsolete("Use Pointer to get pointer-specific information")] - public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this)); - - [Obsolete("Use KeyModifiers and PointerPointProperties")] - public InputModifiers InputModifiers - { - get - { - var mods = (InputModifiers)KeyModifiers; - if (_properties.IsLeftButtonPressed) - mods |= InputModifiers.LeftMouseButton; - if (_properties.IsMiddleButtonPressed) - mods |= InputModifiers.MiddleMouseButton; - if (_properties.IsRightButtonPressed) - mods |= InputModifiers.RightMouseButton; - - return mods; - } - } - - public KeyModifiers KeyModifiers { get; } - - private Point GetPosition(Point pt, IVisual? relativeTo) - { - if (_rootVisual == null) - return default; - if (relativeTo == null) - return pt; - return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; - } - - public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); - - [Obsolete("Use GetCurrentPoint")] - public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo); - - /// - /// Returns the PointerPoint associated with the current event - /// - /// The visual which coordinate system to use. Pass null for toplevel coordinate system - /// - public PointerPoint GetCurrentPoint(IVisual? relativeTo) - => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); - - /// - /// Returns the PointerPoint associated with the current event - /// - /// The visual which coordinate system to use. Pass null for toplevel coordinate system - /// - public IReadOnlyList GetIntermediatePoints(IVisual? relativeTo) - { - 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 pt = previousPoints[c]; - points[c] = new PointerPoint(Pointer, GetPosition(pt.Position, relativeTo), _properties); - } - - points[points.Length - 1] = GetCurrentPoint(relativeTo); - return points; - } - - /// - /// Returns the current pointer point properties - /// - protected PointerPointProperties Properties => _properties; - } - - public enum MouseButton - { - None, - Left, - Right, - Middle, - XButton1, - XButton2 - } - - public class PointerPressedEventArgs : PointerEventArgs - { - private readonly int _clickCount; - - public PointerPressedEventArgs( - IInteractive source, - IPointer pointer, - IVisual rootVisual, Point rootVisualPosition, - ulong timestamp, - PointerPointProperties properties, - KeyModifiers modifiers, - int clickCount = 1) - : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, - timestamp, properties, modifiers) - { - _clickCount = clickCount; - } - - public int ClickCount => _clickCount; - - [Obsolete("Use PointerPressedEventArgs.GetCurrentPoint(this).Properties")] - public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton(); - } - - public class PointerReleasedEventArgs : PointerEventArgs - { - public PointerReleasedEventArgs( - IInteractive source, IPointer pointer, - IVisual rootVisual, Point rootVisualPosition, ulong timestamp, - PointerPointProperties properties, KeyModifiers modifiers, - MouseButton initialPressMouseButton) - : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition, - timestamp, properties, modifiers) - { - InitialPressMouseButton = initialPressMouseButton; - } - - /// - /// Gets the mouse button that triggered the corresponding PointerPressed event - /// - public MouseButton InitialPressMouseButton { get; } - - [Obsolete("Use InitialPressMouseButton")] - public MouseButton MouseButton => InitialPressMouseButton; - } - - public class PointerCaptureLostEventArgs : RoutedEventArgs - { - public IPointer Pointer { get; } - - public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) - { - Pointer = pointer; - Source = source; - } - } -} diff --git a/src/Avalonia.Input/Properties/AssemblyInfo.cs b/src/Avalonia.Input/Properties/AssemblyInfo.cs deleted file mode 100644 index 433f821ca3..0000000000 --- a/src/Avalonia.Input/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System.Reflection; -using Avalonia.Metadata; - -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")] diff --git a/src/Avalonia.Input/Raw/IDragDropDevice.cs b/src/Avalonia.Input/Raw/IDragDropDevice.cs deleted file mode 100644 index f7b7914bd1..0000000000 --- a/src/Avalonia.Input/Raw/IDragDropDevice.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Avalonia.Input.Raw -{ - public interface IDragDropDevice : IInputDevice - { - } -} diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs deleted file mode 100644 index 6e9ce20ff1..0000000000 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace Avalonia.Input.Raw -{ - public class RawDragEvent : RawInputEventArgs - { - public Point Location { get; set; } - public IDataObject Data { get; } - public DragDropEffects Effects { get; set; } - public RawDragEventType Type { get; } - [Obsolete("Use KeyModifiers")] - public InputModifiers Modifiers { get; } - public KeyModifiers KeyModifiers { get; } - - public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, - IInputRoot root, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) - :base(inputDevice, 0, root) - { - Type = type; - Location = location; - Data = data; - Effects = effects; - KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers); -#pragma warning disable CS0618 // Type or member is obsolete - Modifiers = (InputModifiers)modifiers; -#pragma warning restore CS0618 // Type or member is obsolete - } - } -} diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs deleted file mode 100644 index c157fa059c..0000000000 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Avalonia.Input.Raw -{ - public enum RawPointerEventType - { - LeaveWindow, - LeftButtonDown, - LeftButtonUp, - RightButtonDown, - RightButtonUp, - MiddleButtonDown, - MiddleButtonUp, - XButton1Down, - XButton1Up, - XButton2Down, - XButton2Up, - Move, - Wheel, - NonClientLeftButtonDown, - TouchBegin, - TouchUpdate, - TouchEnd, - TouchCancel, - Magnify, - Rotate, - Swipe - } - - /// - /// A raw mouse event. - /// - public class RawPointerEventArgs : RawInputEventArgs - { - private RawPointerPoint _point; - - /// - /// Initializes a new instance of the class. - /// - /// The associated device. - /// The event timestamp. - /// The root from which the event originates. - /// The type of the event. - /// The mouse position, in client DIPs. - /// The input modifiers. - public RawPointerEventArgs( - IInputDevice device, - ulong timestamp, - IInputRoot root, - RawPointerEventType type, - Point position, - RawInputModifiers inputModifiers) - : base(device, timestamp, root) - { - Contract.Requires(device != null); - Contract.Requires(root != null); - - Position = position; - Type = type; - InputModifiers = inputModifiers; - } - - /// - /// 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 => _point.Position; - set => _point.Position = value; - } - - /// - /// Gets the type of the event. - /// - public RawPointerEventType Type { get; set; } - - /// - /// Gets the input modifiers. - /// - public RawInputModifiers InputModifiers { get; set; } - - /// - /// Points that were traversed by a pointer since the previous relevant event, - /// only valid for Move and TouchUpdate - /// - public 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 deleted file mode 100644 index 2d24ed30a0..0000000000 --- a/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Avalonia.Input.TextInput -{ - public interface ITextInputMethodImpl - { - void SetActive(bool active); - void SetCursorRect(Rect rect); - void SetOptions(TextInputOptionsQueryEventArgs options); - void Reset(); - } - - public interface ITextInputMethodRoot : IInputRoot - { - ITextInputMethodImpl? InputMethod { get; } - } -} diff --git a/src/Avalonia.Input/TextInput/InputMethodManager.cs b/src/Avalonia.Input/TextInput/InputMethodManager.cs deleted file mode 100644 index 64422a7fdf..0000000000 --- a/src/Avalonia.Input/TextInput/InputMethodManager.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using Avalonia.VisualTree; - -namespace Avalonia.Input.TextInput -{ - internal class TextInputMethodManager - { - private ITextInputMethodImpl? _im; - private IInputElement? _focusedElement; - private ITextInputMethodClient? _client; - private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper(); - - public TextInputMethodManager() - { - _transformTracker.MatrixChanged += UpdateCursorRect; - InputMethod.IsInputMethodEnabledProperty.Changed.Subscribe(OnIsInputMethodEnabledChanged); - } - - private ITextInputMethodClient? Client - { - get => _client; - set - { - if(_client == value) - return; - if (_client != null) - { - _client.CursorRectangleChanged -= OnCursorRectangleChanged; - _client.TextViewVisualChanged -= OnTextViewVisualChanged; - } - - _client = value; - - if (_client != null) - { - _client.CursorRectangleChanged += OnCursorRectangleChanged; - _client.TextViewVisualChanged += OnTextViewVisualChanged; - var optionsQuery = new TextInputOptionsQueryEventArgs - { - RoutedEvent = InputElement.TextInputOptionsQueryEvent - }; - _focusedElement?.RaiseEvent(optionsQuery); - _im?.Reset(); - _im?.SetOptions(optionsQuery); - _transformTracker?.SetVisual(_client?.TextViewVisual); - UpdateCursorRect(); - - _im?.SetActive(true); - } - else - { - _im?.SetActive(false); - _transformTracker.SetVisual(null); - } - } - } - - private void OnIsInputMethodEnabledChanged(AvaloniaPropertyChangedEventArgs obj) - { - if (ReferenceEquals(obj.Sender, _focusedElement)) - { - TryFindAndApplyClient(); - } - } - - private void OnTextViewVisualChanged(object? sender, EventArgs e) - => _transformTracker.SetVisual(_client?.TextViewVisual); - - private void UpdateCursorRect() - { - if (_im == null || _client == null || _focusedElement?.VisualRoot == null) - return; - - var transform = _focusedElement.TransformToVisual(_focusedElement.VisualRoot); - if (transform == null) - _im.SetCursorRect(default); - else - _im.SetCursorRect(_client.CursorRectangle.TransformToAABB(transform.Value)); - } - - private void OnCursorRectangleChanged(object? sender, EventArgs e) - { - if (sender == _client) - UpdateCursorRect(); - } - - public void SetFocusedElement(IInputElement? element) - { - if(_focusedElement == element) - return; - _focusedElement = element; - - var inputMethod = (element?.VisualRoot as ITextInputMethodRoot)?.InputMethod; - if (_im != inputMethod) - _im?.SetActive(false); - - _im = inputMethod; - - TryFindAndApplyClient(); - } - - private void TryFindAndApplyClient() - { - if (_focusedElement is not InputElement focused || - _im == null || - !InputMethod.GetIsInputMethodEnabled(focused)) - { - Client = null; - return; - } - - var clientQuery = new TextInputMethodClientRequestedEventArgs - { - RoutedEvent = InputElement.TextInputMethodClientRequestedEvent - }; - - _focusedElement.RaiseEvent(clientQuery); - Client = clientQuery.Client; - } - } -} diff --git a/src/Avalonia.Input/TextInput/TextInputContentType.cs b/src/Avalonia.Input/TextInput/TextInputContentType.cs deleted file mode 100644 index 5d73fc1552..0000000000 --- a/src/Avalonia.Input/TextInput/TextInputContentType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Avalonia.Input.TextInput -{ - public enum TextInputContentType - { - Normal = 0, - Email = 1, - Phone = 2, - Number = 3, - Url = 4, - Password = 5 - } -} diff --git a/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs b/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs deleted file mode 100644 index 924d0eb166..0000000000 --- a/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Avalonia.Interactivity; - -namespace Avalonia.Input.TextInput -{ - public class TextInputOptionsQueryEventArgs : RoutedEventArgs - { - /// - /// The content type (mostly for determining the shape of the virtual keyboard) - /// - public TextInputContentType ContentType { get; set; } - /// - /// Text is multiline - /// - public bool Multiline { get; set; } - /// - /// Text is in lower case - /// - public bool Lowercase { get; set; } - /// - /// Text is in upper case - /// - public bool Uppercase { get; set; } - /// - /// Automatically capitalize letters at the start of the sentence - /// - public bool AutoCapitalization { get; set; } - /// - /// Text contains sensitive data like card numbers and should not be stored - /// - public bool IsSensitive { get; set; } - } -} diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs deleted file mode 100644 index 12ad182bf8..0000000000 --- a/src/Avalonia.Input/TouchDevice.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Input.Raw; -using Avalonia.Platform; - -namespace Avalonia.Input -{ - /// - /// Handles raw touch events - /// - /// This class is supposed to be used on per-toplevel basis, don't use a shared one - /// - /// - public class TouchDevice : IInputDevice, IDisposable - { - private readonly Dictionary _pointers = new Dictionary(); - private bool _disposed; - private int _clickCount; - private Rect _lastClickRect; - private ulong _lastClickTime; - KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => - (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); - - RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown) - { - var rv = modifiers &= RawInputModifiers.KeyboardMask; - if (isLeftButtonDown) - rv |= RawInputModifiers.LeftMouseButton; - return rv; - } - - public void ProcessRawEvent(RawInputEventArgs ev) - { - if (_disposed) - return; - var args = (RawTouchEventArgs)ev; - if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) - { - if (args.Type == RawPointerEventType.TouchEnd) - return; - var hit = args.Root.InputHitTest(args.Position); - - _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), - PointerType.Touch, _pointers.Count == 0); - pointer.Capture(hit); - } - - - var target = pointer.Captured ?? args.Root; - if (args.Type == RawPointerEventType.TouchBegin) - { - if (_pointers.Count > 1) - { - _clickCount = 1; - _lastClickTime = 0; - _lastClickRect = new Rect(); - } - else - { - var settings = AvaloniaLocator.Current.GetRequiredService(); - - if (!_lastClickRect.Contains(args.Position) - || ev.Timestamp - _lastClickTime > settings.TouchDoubleClickTime.TotalMilliseconds) - { - _clickCount = 0; - } - ++_clickCount; - _lastClickTime = ev.Timestamp; - _lastClickRect = new Rect(args.Position, new Size()) - .Inflate(new Thickness(settings.TouchDoubleClickSize.Width / 2, settings.TouchDoubleClickSize.Height / 2)); - } - - target.RaiseEvent(new PointerPressedEventArgs(target, pointer, - args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, true), - PointerUpdateKind.LeftButtonPressed), - GetKeyModifiers(args.InputModifiers), _clickCount)); - } - - if (args.Type == RawPointerEventType.TouchEnd) - { - _pointers.Remove(args.TouchPointId); - using (pointer) - { - target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, - args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, false), - PointerUpdateKind.LeftButtonReleased), - GetKeyModifiers(args.InputModifiers), MouseButton.Left)); - } - } - - if (args.Type == RawPointerEventType.TouchCancel) - { - _pointers.Remove(args.TouchPointId); - using (pointer) - pointer.Capture(null); - } - - if (args.Type == RawPointerEventType.TouchUpdate) - { - var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); - target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, - args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other), - GetKeyModifiers(args.InputModifiers), args.IntermediatePoints)); - } - - - } - - public void Dispose() - { - if (_disposed) - return; - var values = _pointers.Values.ToList(); - _pointers.Clear(); - _disposed = true; - foreach (var p in values) - p.Dispose(); - } - - } -} diff --git a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj deleted file mode 100644 index 03a53f7d86..0000000000 --- a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - net6.0;netstandard2.0 - - - - - - - - - - - \ No newline at end of file diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs deleted file mode 100644 index afda29e329..0000000000 --- a/src/Avalonia.Interactivity/IInteractive.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; - -#nullable enable - -namespace Avalonia.Interactivity -{ - /// - /// Interface for objects that raise routed events. - /// - public interface IInteractive - { - /// - /// Gets the interactive parent of the object for bubbling and tunneling events. - /// - IInteractive? InteractiveParent { get; } - - /// - /// Adds a handler for the specified routed event. - /// - /// The routed event. - /// The handler. - /// The routing strategies to listen to. - /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - void AddHandler( - RoutedEvent routedEvent, - Delegate handler, - RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, - bool handledEventsToo = false); - - /// - /// Adds a handler for the specified routed event. - /// - /// The type of the event's args. - /// The routed event. - /// The handler. - /// The routing strategies to listen to. - /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - void AddHandler( - RoutedEvent routedEvent, - EventHandler handler, - RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, - bool handledEventsToo = false) where TEventArgs : RoutedEventArgs; - - /// - /// Removes a handler for the specified routed event. - /// - /// The routed event. - /// The handler. - void RemoveHandler(RoutedEvent routedEvent, Delegate handler); - - /// - /// Removes a handler for the specified routed event. - /// - /// The type of the event's args. - /// The routed event. - /// The handler. - void RemoveHandler(RoutedEvent routedEvent, EventHandler handler) - where TEventArgs : RoutedEventArgs; - - /// - /// Adds the object's handlers for a routed event to an event route. - /// - /// The event. - /// The event route. - void AddToEventRoute(RoutedEvent routedEvent, EventRoute route); - - /// - /// Raises a routed event. - /// - /// The event args. - void RaiseEvent(RoutedEventArgs e); - } -} diff --git a/src/Avalonia.Layout/Avalonia.Layout.csproj b/src/Avalonia.Layout/Avalonia.Layout.csproj deleted file mode 100644 index f2c4b0540b..0000000000 --- a/src/Avalonia.Layout/Avalonia.Layout.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net6.0;netstandard2.0 - - - - - - - - - - diff --git a/src/Avalonia.Layout/ILayoutManager.cs b/src/Avalonia.Layout/ILayoutManager.cs deleted file mode 100644 index 614670a53b..0000000000 --- a/src/Avalonia.Layout/ILayoutManager.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; - -#nullable enable - -namespace Avalonia.Layout -{ - /// - /// Manages measuring and arranging of controls. - /// - public interface ILayoutManager : IDisposable - { - /// - /// Raised when the layout manager completes a layout pass. - /// - event EventHandler LayoutUpdated; - - /// - /// Notifies the layout manager that a control requires a measure. - /// - /// The control. - void InvalidateMeasure(ILayoutable control); - - /// - /// Notifies the layout manager that a control requires an arrange. - /// - /// The control. - void InvalidateArrange(ILayoutable control); - - /// - /// Executes a layout pass. - /// - /// - /// You should not usually need to call this method explictly, the layout manager will - /// schedule layout passes itself. - /// - void ExecuteLayoutPass(); - - /// - /// Executes the initial layout pass on a layout root. - /// - /// - /// You should not usually need to call this method explictly, the layout root will call - /// it to carry out the initial layout of the control. - /// - void ExecuteInitialLayoutPass(); - - /// - /// Executes the initial layout pass on a layout root. - /// - /// The control to lay out. - /// - /// You should not usually need to call this method explictly, the layout root will call - /// it to carry out the initial layout of the control. - /// - [Obsolete("Call ExecuteInitialLayoutPass without parameter")] - void ExecuteInitialLayoutPass(ILayoutRoot root); - - /// - /// Registers a control as wanting to receive effective viewport notifications. - /// - /// The control. - void RegisterEffectiveViewportListener(ILayoutable control); - - /// - /// Registers a control as no longer wanting to receive effective viewport notifications. - /// - /// The control. - void UnregisterEffectiveViewportListener(ILayoutable control); - } -} diff --git a/src/Avalonia.Layout/ILayoutRoot.cs b/src/Avalonia.Layout/ILayoutRoot.cs deleted file mode 100644 index e2f16b338a..0000000000 --- a/src/Avalonia.Layout/ILayoutRoot.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Avalonia.Layout -{ - /// - /// Defines the root of a layoutable tree. - /// - public interface ILayoutRoot : ILayoutable - { - /// - /// The size available to lay out the controls. - /// - Size ClientSize { get; } - - /// - /// The scaling factor to use in layout. - /// - double LayoutScaling { get; } - - /// - /// Associated instance of layout manager - /// - ILayoutManager LayoutManager { get; } - } -} diff --git a/src/Avalonia.Layout/ILayoutable.cs b/src/Avalonia.Layout/ILayoutable.cs deleted file mode 100644 index 54d3ba6a11..0000000000 --- a/src/Avalonia.Layout/ILayoutable.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Avalonia.VisualTree; - -#nullable enable - -namespace Avalonia.Layout -{ - /// - /// Defines layout-related functionality for a control. - /// - public interface ILayoutable : IVisual - { - /// - /// Gets the size that this element computed during the measure pass of the layout process. - /// - Size DesiredSize { get; } - - /// - /// Gets the width of the element. - /// - double Width { get; } - - /// - /// Gets the height of the element. - /// - double Height { get; } - - /// - /// Gets the minimum width of the element. - /// - double MinWidth { get; } - - /// - /// Gets the maximum width of the element. - /// - double MaxWidth { get; } - - /// - /// Gets the minimum height of the element. - /// - double MinHeight { get; } - - /// - /// Gets the maximum height of the element. - /// - double MaxHeight { get; } - - /// - /// Gets the margin around the element. - /// - Thickness Margin { get; } - - /// - /// Gets the element's preferred horizontal alignment in its parent. - /// - HorizontalAlignment HorizontalAlignment { get; } - - /// - /// Gets the element's preferred vertical alignment in its parent. - /// - VerticalAlignment VerticalAlignment { get; } - - /// - /// Gets a value indicating whether the control's layout measure is valid. - /// - bool IsMeasureValid { get; } - - /// - /// Gets a value indicating whether the control's layouts arrange is valid. - /// - bool IsArrangeValid { get; } - - /// - /// Gets the available size passed in the previous layout pass, if any. - /// - Size? PreviousMeasure { get; } - - /// - /// Gets the layout rect passed in the previous layout pass, if any. - /// - Rect? PreviousArrange { get; } - - /// - /// Creates the visual children of the control, if necessary - /// - void ApplyTemplate(); - - /// - /// Carries out a measure of the control. - /// - /// The available size for the control. - void Measure(Size availableSize); - - /// - /// Arranges the control and its children. - /// - /// The control's new bounds. - void Arrange(Rect rect); - - /// - /// Invalidates the measurement of the control and queues a new layout pass. - /// - void InvalidateMeasure(); - - /// - /// Invalidates the arrangement of the control and queues a new layout pass. - /// - void InvalidateArrange(); - - /// - /// Called when a child control's desired size changes. - /// - /// The child control. - void ChildDesiredSizeChanged(ILayoutable control); - - /// - /// Used by the to notify the control that its effective - /// viewport is changed. - /// - /// The viewport information. - void EffectiveViewportChanged(EffectiveViewportChangedEventArgs e); - } -} diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs deleted file mode 100644 index d4154a6d0c..0000000000 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System; -using Avalonia.Utilities; -using Avalonia.VisualTree; - -namespace Avalonia.Layout -{ - /// - /// Provides helper methods needed for layout. - /// - public static class LayoutHelper - { - /// - /// Epsilon value used for certain layout calculations. - /// Based on the value in WPF LayoutDoubleUtil. - /// - public static double LayoutEpsilon { get; } = 0.00000153; - - /// - /// Calculates a control's size based on its , - /// , , - /// , and - /// . - /// - /// The control. - /// The space available for the control. - /// The control's size. - public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints) - { - var minmax = new MinMax(control); - - return new Size( - MathUtilities.Clamp(constraints.Width, minmax.MinWidth, minmax.MaxWidth), - MathUtilities.Clamp(constraints.Height, minmax.MinHeight, minmax.MaxHeight)); - } - - public static Size MeasureChild(ILayoutable? control, Size availableSize, Thickness padding, - Thickness borderThickness) - { - return MeasureChild(control, availableSize, padding + borderThickness); - } - - public static Size MeasureChild(ILayoutable? control, Size availableSize, Thickness padding) - { - if (control != null) - { - control.Measure(availableSize.Deflate(padding)); - return control.DesiredSize.Inflate(padding); - } - - return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top); - } - - public static Size ArrangeChild(ILayoutable? child, Size availableSize, Thickness padding, Thickness borderThickness) - { - return ArrangeChild(child, availableSize, padding + borderThickness); - } - - public static Size ArrangeChild(ILayoutable? child, Size availableSize, Thickness padding) - { - child?.Arrange(new Rect(availableSize).Deflate(padding)); - - return availableSize; - } - - /// - /// Invalidates measure for given control and all visual children recursively. - /// - public static void InvalidateSelfAndChildrenMeasure(ILayoutable control) - { - void InnerInvalidateMeasure(IVisual target) - { - if (target is ILayoutable targetLayoutable) - { - targetLayoutable.InvalidateMeasure(); - } - - var visualChildren = target.VisualChildren; - var visualChildrenCount = visualChildren.Count; - - for (int i = 0; i < visualChildrenCount; i++) - { - IVisual child = visualChildren[i]; - - InnerInvalidateMeasure(child); - } - } - - InnerInvalidateMeasure(control); - } - - /// - /// Obtains layout scale of the given control. - /// - /// The control. - /// Thrown when control has no root or returned layout scaling is invalid. - public static double GetLayoutScale(ILayoutable control) - { - var visualRoot = control.VisualRoot; - - var result = (visualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; - - if (result == 0 || double.IsNaN(result) || double.IsInfinity(result)) - { - throw new Exception($"Invalid LayoutScaling returned from {visualRoot!.GetType()}"); - } - - return result; - } - - /// - /// Rounds a size to integer values for layout purposes, compensating for high DPI screen - /// coordinates. - /// - /// Input size. - /// DPI along x-dimension. - /// DPI along y-dimension. - /// Value of size that will be rounded under screen DPI. - /// - /// This is a layout helper method. It takes DPI into account and also does not return - /// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper - /// associated with the UseLayoutRounding property and should not be used as a general rounding - /// utility. - /// - public static Size RoundLayoutSize(Size size, double dpiScaleX, double dpiScaleY) - { - return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY)); - } - - /// - /// Calculates the value to be used for layout rounding at high DPI. - /// - /// Input value to be rounded. - /// Ratio of screen's DPI to layout DPI - /// Adjusted value that will produce layout rounding on screen at high dpi. - /// - /// This is a layout helper method. It takes DPI into account and also does not return - /// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper - /// associated with the UseLayoutRounding property and should not be used as a general rounding - /// utility. - /// - public static double RoundLayoutValue(double value, double dpiScale) - { - double newValue; - - // If DPI == 1, don't use DPI-aware rounding. - if (!MathUtilities.IsOne(dpiScale)) - { - newValue = Math.Round(value * dpiScale) / dpiScale; - - // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), - // use the original value. - if (double.IsNaN(newValue) || - double.IsInfinity(newValue) || - MathUtilities.AreClose(newValue, double.MaxValue)) - { - newValue = value; - } - } - else - { - newValue = Math.Round(value); - } - - return newValue; - } - - - /// - /// Calculates the min and max height for a control. Ported from WPF. - /// - private readonly struct MinMax - { - public MinMax(ILayoutable e) - { - MaxHeight = e.MaxHeight; - MinHeight = e.MinHeight; - double l = e.Height; - - double height = (double.IsNaN(l) ? double.PositiveInfinity : l); - MaxHeight = Math.Max(Math.Min(height, MaxHeight), MinHeight); - - height = (double.IsNaN(l) ? 0 : l); - MinHeight = Math.Max(Math.Min(MaxHeight, height), MinHeight); - - MaxWidth = e.MaxWidth; - MinWidth = e.MinWidth; - l = e.Width; - - double width = (double.IsNaN(l) ? double.PositiveInfinity : l); - MaxWidth = Math.Max(Math.Min(width, MaxWidth), MinWidth); - - width = (double.IsNaN(l) ? 0 : l); - MinWidth = Math.Max(Math.Min(MaxWidth, width), MinWidth); - } - - public double MinWidth { get; } - public double MaxWidth { get; } - public double MinHeight { get; } - public double MaxHeight { get; } - } - } -} diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs deleted file mode 100644 index 09e0c4263a..0000000000 --- a/src/Avalonia.Layout/Layoutable.cs +++ /dev/null @@ -1,837 +0,0 @@ -using System; -using Avalonia.Logging; -using Avalonia.VisualTree; - -#nullable enable - -namespace Avalonia.Layout -{ - /// - /// Defines how a control aligns itself horizontally in its parent control. - /// - public enum HorizontalAlignment - { - /// - /// The control stretches to fill the width of the parent control. - /// - Stretch, - - /// - /// The control aligns itself to the left of the parent control. - /// - Left, - - /// - /// The control centers itself in the parent control. - /// - Center, - - /// - /// The control aligns itself to the right of the parent control. - /// - Right, - } - - /// - /// Defines how a control aligns itself vertically in its parent control. - /// - public enum VerticalAlignment - { - /// - /// The control stretches to fill the height of the parent control. - /// - Stretch, - - /// - /// The control aligns itself to the top of the parent control. - /// - Top, - - /// - /// The control centers itself within the parent control. - /// - Center, - - /// - /// The control aligns itself to the bottom of the parent control. - /// - Bottom, - } - - /// - /// Implements layout-related functionality for a control. - /// - public class Layoutable : Visual, ILayoutable - { - /// - /// Defines the property. - /// - public static readonly DirectProperty DesiredSizeProperty = - AvaloniaProperty.RegisterDirect(nameof(DesiredSize), o => o.DesiredSize); - - /// - /// Defines the property. - /// - public static readonly StyledProperty WidthProperty = - AvaloniaProperty.Register(nameof(Width), double.NaN); - - /// - /// Defines the property. - /// - public static readonly StyledProperty HeightProperty = - AvaloniaProperty.Register(nameof(Height), double.NaN); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinWidthProperty = - AvaloniaProperty.Register(nameof(MinWidth)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MaxWidthProperty = - AvaloniaProperty.Register(nameof(MaxWidth), double.PositiveInfinity); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinHeightProperty = - AvaloniaProperty.Register(nameof(MinHeight)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MaxHeightProperty = - AvaloniaProperty.Register(nameof(MaxHeight), double.PositiveInfinity); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MarginProperty = - AvaloniaProperty.Register(nameof(Margin)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty HorizontalAlignmentProperty = - AvaloniaProperty.Register(nameof(HorizontalAlignment)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty VerticalAlignmentProperty = - AvaloniaProperty.Register(nameof(VerticalAlignment)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty UseLayoutRoundingProperty = - AvaloniaProperty.Register(nameof(UseLayoutRounding), defaultValue: true, inherits: true); - - private bool _measuring; - private Size? _previousMeasure; - private Rect? _previousArrange; - private EventHandler? _effectiveViewportChanged; - private EventHandler? _layoutUpdated; - - /// - /// Initializes static members of the class. - /// - static Layoutable() - { - AffectsMeasure( - IsVisibleProperty, - WidthProperty, - HeightProperty, - MinWidthProperty, - MaxWidthProperty, - MinHeightProperty, - MaxHeightProperty, - MarginProperty, - HorizontalAlignmentProperty, - VerticalAlignmentProperty); - } - - /// - /// Occurs when the element's effective viewport changes. - /// - public event EventHandler? EffectiveViewportChanged - { - add - { - if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r) - { - r.LayoutManager.RegisterEffectiveViewportListener(this); - } - - _effectiveViewportChanged += value; - } - - remove - { - _effectiveViewportChanged -= value; - - if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r) - { - r.LayoutManager.UnregisterEffectiveViewportListener(this); - } - } - } - - /// - /// Occurs when a layout pass completes for the control. - /// - public event EventHandler? LayoutUpdated - { - add - { - if (_layoutUpdated is null && VisualRoot is ILayoutRoot r) - { - r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; - } - - _layoutUpdated += value; - } - - remove - { - _layoutUpdated -= value; - - if (_layoutUpdated is null && VisualRoot is ILayoutRoot r) - { - r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; - } - } - } - - /// - /// Gets or sets the width of the element. - /// - public double Width - { - get { return GetValue(WidthProperty); } - set { SetValue(WidthProperty, value); } - } - - /// - /// Gets or sets the height of the element. - /// - public double Height - { - get { return GetValue(HeightProperty); } - set { SetValue(HeightProperty, value); } - } - - /// - /// Gets or sets the minimum width of the element. - /// - public double MinWidth - { - get { return GetValue(MinWidthProperty); } - set { SetValue(MinWidthProperty, value); } - } - - /// - /// Gets or sets the maximum width of the element. - /// - public double MaxWidth - { - get { return GetValue(MaxWidthProperty); } - set { SetValue(MaxWidthProperty, value); } - } - - /// - /// Gets or sets the minimum height of the element. - /// - public double MinHeight - { - get { return GetValue(MinHeightProperty); } - set { SetValue(MinHeightProperty, value); } - } - - /// - /// Gets or sets the maximum height of the element. - /// - public double MaxHeight - { - get { return GetValue(MaxHeightProperty); } - set { SetValue(MaxHeightProperty, value); } - } - - /// - /// Gets or sets the margin around the element. - /// - public Thickness Margin - { - get { return GetValue(MarginProperty); } - set { SetValue(MarginProperty, value); } - } - - /// - /// Gets or sets the element's preferred horizontal alignment in its parent. - /// - public HorizontalAlignment HorizontalAlignment - { - get { return GetValue(HorizontalAlignmentProperty); } - set { SetValue(HorizontalAlignmentProperty, value); } - } - - /// - /// Gets or sets the element's preferred vertical alignment in its parent. - /// - public VerticalAlignment VerticalAlignment - { - get { return GetValue(VerticalAlignmentProperty); } - set { SetValue(VerticalAlignmentProperty, value); } - } - - /// - /// Gets the size that this element computed during the measure pass of the layout process. - /// - public Size DesiredSize - { - get; - private set; - } - - /// - /// Gets a value indicating whether the control's layout measure is valid. - /// - public bool IsMeasureValid - { - get; - private set; - } - - /// - /// Gets a value indicating whether the control's layouts arrange is valid. - /// - public bool IsArrangeValid - { - get; - private set; - } - - /// - /// Gets or sets a value that determines whether the element should be snapped to pixel - /// boundaries at layout time. - /// - public bool UseLayoutRounding - { - get { return GetValue(UseLayoutRoundingProperty); } - set { SetValue(UseLayoutRoundingProperty, value); } - } - - /// - /// Gets the available size passed in the previous layout pass, if any. - /// - Size? ILayoutable.PreviousMeasure => _previousMeasure; - - /// - /// Gets the layout rect passed in the previous layout pass, if any. - /// - Rect? ILayoutable.PreviousArrange => _previousArrange; - - /// - /// Creates the visual children of the control, if necessary - /// - public virtual void ApplyTemplate() - { - } - - /// - /// Carries out a measure of the control. - /// - /// The available size for the control. - public void Measure(Size availableSize) - { - if (double.IsNaN(availableSize.Width) || double.IsNaN(availableSize.Height)) - { - throw new InvalidOperationException("Cannot call Measure using a size with NaN values."); - } - - if (!IsMeasureValid || _previousMeasure != availableSize) - { - var previousDesiredSize = DesiredSize; - var desiredSize = default(Size); - - IsMeasureValid = true; - - try - { - _measuring = true; - desiredSize = MeasureCore(availableSize); - } - finally - { - _measuring = false; - } - - if (IsInvalidSize(desiredSize)) - { - throw new InvalidOperationException("Invalid size returned for Measure."); - } - - DesiredSize = desiredSize; - _previousMeasure = availableSize; - - Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Measure requested {DesiredSize}", DesiredSize); - - if (DesiredSize != previousDesiredSize) - { - this.GetVisualParent()?.ChildDesiredSizeChanged(this); - } - } - } - - /// - /// Arranges the control and its children. - /// - /// The control's new bounds. - public void Arrange(Rect rect) - { - if (IsInvalidRect(rect)) - { - throw new InvalidOperationException("Invalid Arrange rectangle."); - } - - if (!IsMeasureValid) - { - Measure(_previousMeasure ?? rect.Size); - } - - if (!IsArrangeValid || _previousArrange != rect) - { - Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Arrange to {Rect} ", rect); - - IsArrangeValid = true; - ArrangeCore(rect); - _previousArrange = rect; - } - } - - /// - /// Invalidates the measurement of the control and queues a new layout pass. - /// - public void InvalidateMeasure() - { - if (IsMeasureValid) - { - Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Invalidated measure"); - - IsMeasureValid = false; - IsArrangeValid = false; - - if (((ILayoutable)this).IsAttachedToVisualTree) - { - (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateMeasure(this); - InvalidateVisual(); - } - OnMeasureInvalidated(); - } - } - - /// - /// Invalidates the arrangement of the control and queues a new layout pass. - /// - public void InvalidateArrange() - { - if (IsArrangeValid) - { - Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Invalidated arrange"); - - IsArrangeValid = false; - (VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this); - InvalidateVisual(); - } - } - - /// - void ILayoutable.ChildDesiredSizeChanged(ILayoutable control) - { - if (!_measuring) - { - InvalidateMeasure(); - } - } - - void ILayoutable.EffectiveViewportChanged(EffectiveViewportChangedEventArgs e) - { - _effectiveViewportChanged?.Invoke(this, e); - } - - /// - /// Marks a property as affecting the control's measurement. - /// - /// The properties. - /// - /// After a call to this method in a control's static constructor, any change to the - /// property will cause to be called on the element. - /// - [Obsolete("Use AffectsMeasure and specify the control type.")] - protected static void AffectsMeasure(params AvaloniaProperty[] properties) - { - AffectsMeasure(properties); - } - - /// - /// Marks a property as affecting the control's measurement. - /// - /// The control which the property affects. - /// The properties. - /// - /// After a call to this method in a control's static constructor, any change to the - /// property will cause to be called on the element. - /// - protected static void AffectsMeasure(params AvaloniaProperty[] properties) - where T : class, ILayoutable - { - void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - (e.Sender as T)?.InvalidateMeasure(); - } - - foreach (var property in properties) - { - property.Changed.Subscribe(Invalidate); - } - } - - /// - /// Marks a property as affecting the control's arrangement. - /// - /// The properties. - /// - /// After a call to this method in a control's static constructor, any change to the - /// property will cause to be called on the element. - /// - [Obsolete("Use AffectsArrange and specify the control type.")] - protected static void AffectsArrange(params AvaloniaProperty[] properties) - { - AffectsArrange(properties); - } - - /// - /// Marks a property as affecting the control's arrangement. - /// - /// The control which the property affects. - /// The properties. - /// - /// After a call to this method in a control's static constructor, any change to the - /// property will cause to be called on the element. - /// - protected static void AffectsArrange(params AvaloniaProperty[] properties) - where T : class, ILayoutable - { - void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - (e.Sender as T)?.InvalidateArrange(); - } - - foreach (var property in properties) - { - property.Changed.Subscribe(Invalidate); - } - } - - /// - /// The default implementation of the control's measure pass. - /// - /// The size available to the control. - /// The desired size for the control. - /// - /// This method calls which is probably the method you - /// want to override in order to modify a control's arrangement. - /// - protected virtual Size MeasureCore(Size availableSize) - { - if (IsVisible) - { - var margin = Margin; - - ApplyStyling(); - ApplyTemplate(); - - var constrained = LayoutHelper.ApplyLayoutConstraints( - this, - availableSize.Deflate(margin)); - var measured = MeasureOverride(constrained); - - var width = measured.Width; - var height = measured.Height; - - { - double widthCache = Width; - - if (!double.IsNaN(widthCache)) - { - width = widthCache; - } - } - - width = Math.Min(width, MaxWidth); - width = Math.Max(width, MinWidth); - - { - double heightCache = Height; - - if (!double.IsNaN(heightCache)) - { - height = heightCache; - } - } - - height = Math.Min(height, MaxHeight); - height = Math.Max(height, MinHeight); - - width = Math.Min(width, availableSize.Width); - height = Math.Min(height, availableSize.Height); - - if (UseLayoutRounding) - { - var scale = LayoutHelper.GetLayoutScale(this); - width = LayoutHelper.RoundLayoutValue(width, scale); - height = LayoutHelper.RoundLayoutValue(height, scale); - } - - return NonNegative(new Size(width, height).Inflate(margin)); - } - else - { - return new Size(); - } - } - - /// - /// Measures the control and its child elements as part of a layout pass. - /// - /// The size available to the control. - /// The desired size for the control. - protected virtual Size MeasureOverride(Size availableSize) - { - double width = 0; - double height = 0; - - var visualChildren = VisualChildren; - var visualCount = visualChildren.Count; - - for (var i = 0; i < visualCount; i++) - { - IVisual visual = visualChildren[i]; - - if (visual is ILayoutable layoutable) - { - layoutable.Measure(availableSize); - width = Math.Max(width, layoutable.DesiredSize.Width); - height = Math.Max(height, layoutable.DesiredSize.Height); - } - } - - return new Size(width, height); - } - - /// - /// The default implementation of the control's arrange pass. - /// - /// The control's new bounds. - /// - /// This method calls which is probably the method you - /// want to override in order to modify a control's arrangement. - /// - protected virtual void ArrangeCore(Rect finalRect) - { - if (IsVisible) - { - var margin = Margin; - var originX = finalRect.X + margin.Left; - var originY = finalRect.Y + margin.Top; - var availableSizeMinusMargins = new Size( - Math.Max(0, finalRect.Width - margin.Left - margin.Right), - Math.Max(0, finalRect.Height - margin.Top - margin.Bottom)); - var horizontalAlignment = HorizontalAlignment; - var verticalAlignment = VerticalAlignment; - var size = availableSizeMinusMargins; - var scale = LayoutHelper.GetLayoutScale(this); - var useLayoutRounding = UseLayoutRounding; - - if (horizontalAlignment != HorizontalAlignment.Stretch) - { - size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - margin.Left - margin.Right)); - } - - if (verticalAlignment != VerticalAlignment.Stretch) - { - size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - margin.Top - margin.Bottom)); - } - - size = LayoutHelper.ApplyLayoutConstraints(this, size); - - if (useLayoutRounding) - { - size = LayoutHelper.RoundLayoutSize(size, scale, scale); - availableSizeMinusMargins = LayoutHelper.RoundLayoutSize(availableSizeMinusMargins, scale, scale); - } - - size = ArrangeOverride(size).Constrain(size); - - switch (horizontalAlignment) - { - case HorizontalAlignment.Center: - case HorizontalAlignment.Stretch: - originX += (availableSizeMinusMargins.Width - size.Width) / 2; - break; - case HorizontalAlignment.Right: - originX += availableSizeMinusMargins.Width - size.Width; - break; - } - - switch (verticalAlignment) - { - case VerticalAlignment.Center: - case VerticalAlignment.Stretch: - originY += (availableSizeMinusMargins.Height - size.Height) / 2; - break; - case VerticalAlignment.Bottom: - originY += availableSizeMinusMargins.Height - size.Height; - break; - } - - if (useLayoutRounding) - { - originX = LayoutHelper.RoundLayoutValue(originX, scale); - originY = LayoutHelper.RoundLayoutValue(originY, scale); - } - - Bounds = new Rect(originX, originY, size.Width, size.Height); - } - } - - /// - /// Positions child elements as part of a layout pass. - /// - /// The size available to the control. - /// The actual size used. - protected virtual Size ArrangeOverride(Size finalSize) - { - var arrangeRect = new Rect(finalSize); - - var visualChildren = VisualChildren; - var visualCount = visualChildren.Count; - - for (var i = 0; i < visualCount; i++) - { - IVisual visual = visualChildren[i]; - - if (visual is ILayoutable layoutable) - { - layoutable.Arrange(arrangeRect); - } - } - - return finalSize; - } - - protected sealed override void InvalidateStyles() - { - base.InvalidateStyles(); - InvalidateMeasure(); - } - - protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTreeCore(e); - - if (e.Root is ILayoutRoot r) - { - if (_layoutUpdated is object) - { - r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; - } - - if (_effectiveViewportChanged is object) - { - r.LayoutManager.RegisterEffectiveViewportListener(this); - } - } - } - - protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - if (e.Root is ILayoutRoot r) - { - if (_layoutUpdated is object) - { - r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; - } - - if (_effectiveViewportChanged is object) - { - r.LayoutManager.UnregisterEffectiveViewportListener(this); - } - } - - base.OnDetachedFromVisualTreeCore(e); - } - - /// - /// Called by InvalidateMeasure - /// - protected virtual void OnMeasureInvalidated() - { - } - - /// - protected sealed override void OnVisualParentChanged(IVisual? oldParent, IVisual? newParent) - { - LayoutHelper.InvalidateSelfAndChildrenMeasure(this); - - base.OnVisualParentChanged(oldParent, newParent); - } - - /// - /// Called when the layout manager raises a LayoutUpdated event. - /// - /// The sender. - /// The event args. - private void LayoutManagedLayoutUpdated(object? sender, EventArgs e) => _layoutUpdated?.Invoke(this, e); - - /// - /// Tests whether any of a 's properties include negative values, - /// a NaN or Infinity. - /// - /// The rect. - /// True if the rect is invalid; otherwise false. - private static bool IsInvalidRect(Rect rect) - { - return rect.Width < 0 || rect.Height < 0 || - double.IsInfinity(rect.X) || double.IsInfinity(rect.Y) || - double.IsInfinity(rect.Width) || double.IsInfinity(rect.Height) || - double.IsNaN(rect.X) || double.IsNaN(rect.Y) || - double.IsNaN(rect.Width) || double.IsNaN(rect.Height); - } - - /// - /// Tests whether any of a 's properties include negative values, - /// a NaN or Infinity. - /// - /// The size. - /// True if the size is invalid; otherwise false. - private static bool IsInvalidSize(Size size) - { - return size.Width < 0 || size.Height < 0 || - double.IsInfinity(size.Width) || double.IsInfinity(size.Height) || - double.IsNaN(size.Width) || double.IsNaN(size.Height); - } - - /// - /// Ensures neither component of a is negative. - /// - /// The size. - /// The non-negative size. - private static Size NonNegative(Size size) - { - return new Size(Math.Max(size.Width, 0), Math.Max(size.Height, 0)); - } - } -} diff --git a/src/Avalonia.Layout/Properties/AssemblyInfo.cs b/src/Avalonia.Layout/Properties/AssemblyInfo.cs deleted file mode 100644 index efcbf184b5..0000000000 --- a/src/Avalonia.Layout/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Runtime.CompilerServices; -using Avalonia.Metadata; - -[assembly: InternalsVisibleTo("Avalonia.Layout.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")] - diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs deleted file mode 100644 index c63fe5e405..0000000000 --- a/src/Avalonia.Layout/StackLayout.cs +++ /dev/null @@ -1,363 +0,0 @@ -// This source file is 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.Collections.Specialized; -using Avalonia.Data; -using Avalonia.Logging; - -namespace Avalonia.Layout -{ - /// - /// Arranges elements into a single line (with spacing) that can be oriented horizontally or vertically. - /// - public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates - { - /// - /// Defines the property. - /// - public static readonly StyledProperty DisableVirtualizationProperty = - AvaloniaProperty.Register(nameof(DisableVirtualization)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); - - /// - /// Defines the property. - /// - public static readonly StyledProperty SpacingProperty = - AvaloniaProperty.Register(nameof(Spacing)); - - private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); - - /// - /// Initializes a new instance of the StackLayout class. - /// - public StackLayout() - { - LayoutId = "StackLayout"; - } - - /// - /// Gets or sets a value indicating whether virtualization is disabled on the layout. - /// - public bool DisableVirtualization - { - get => GetValue(DisableVirtualizationProperty); - set => SetValue(DisableVirtualizationProperty, value); - } - - /// - /// Gets or sets the axis along which items are laid out. - /// - /// - /// One of the enumeration values that specifies the axis along which items are laid out. - /// The default is Vertical. - /// - public Orientation Orientation - { - get => GetValue(OrientationProperty); - set => SetValue(OrientationProperty, value); - } - - /// - /// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the - /// direction of the StackLayout's Orientation. - /// - public double Spacing - { - get => GetValue(SpacingProperty); - set => SetValue(SpacingProperty, value); - } - - internal Rect GetExtent( - Size availableSize, - VirtualizingLayoutContext context, - ILayoutable? firstRealized, - int firstRealizedItemIndex, - Rect firstRealizedLayoutBounds, - ILayoutable? lastRealized, - int lastRealizedItemIndex, - Rect lastRealizedLayoutBounds) - { - var extent = new Rect(); - - // Constants - int itemsCount = context.ItemCount; - var stackState = (StackLayoutState)context.LayoutState!; - double averageElementSize = GetAverageElementSize(availableSize, context, stackState) + Spacing; - - _orientation.SetMinorSize(ref extent, stackState.MaxArrangeBounds); - _orientation.SetMajorSize(ref extent, Math.Max(0.0f, itemsCount * averageElementSize - Spacing)); - if (itemsCount > 0) - { - if (firstRealized != null) - { - _orientation.SetMajorStart( - ref extent, - _orientation.MajorStart(firstRealizedLayoutBounds) - firstRealizedItemIndex * averageElementSize); - var remainingItems = itemsCount - lastRealizedItemIndex - 1; - _orientation.SetMajorSize( - ref extent, - _orientation.MajorEnd(lastRealizedLayoutBounds) - - _orientation.MajorStart(extent) + - (remainingItems * averageElementSize)); - } - else - { - Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements", - LayoutId); - } - } - - Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on average {Average}", - LayoutId, extent.Size, averageElementSize); - return extent; - } - - internal void OnElementMeasured( - ILayoutable element, - int index, - Size availableSize, - Size measureSize, - Size desiredSize, - Size provisionalArrangeSize, - VirtualizingLayoutContext context) - { - if (context is VirtualizingLayoutContext virtualContext) - { - var stackState = (StackLayoutState)virtualContext.LayoutState!; - var provisionalArrangeSizeWinRt = provisionalArrangeSize; - stackState.OnElementMeasured( - index, - _orientation.Major(provisionalArrangeSizeWinRt), - _orientation.Minor(provisionalArrangeSizeWinRt)); - } - } - - Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize( - int index, - Size availableSize, - VirtualizingLayoutContext context) => availableSize; - - Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize( - int index, - Size measureSize, - Size desiredSize, - VirtualizingLayoutContext context) - { - var measureSizeMinor = _orientation.Minor(measureSize); - return _orientation.MinorMajorSize( - !double.IsInfinity(measureSizeMinor) ? - Math.Max(measureSizeMinor, _orientation.Minor(desiredSize)) : - _orientation.Minor(desiredSize), - _orientation.Major(desiredSize)); - } - - bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => true; - - FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect( - Size availableSize, - VirtualizingLayoutContext context) => GetAnchorForRealizationRect(availableSize, context); - - FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement( - int targetIndex, - Size availableSize, - VirtualizingLayoutContext context) - { - double offset = double.NaN; - int index = -1; - int itemsCount = context.ItemCount; - - if (targetIndex >= 0 && targetIndex < itemsCount) - { - index = targetIndex; - var state = (StackLayoutState)context.LayoutState!; - double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing; - offset = index * averageElementSize + _orientation.MajorStart(state.FlowAlgorithm.LastExtent); - } - - return new FlowLayoutAnchorInfo { Index = index, Offset = offset }; - } - - Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( - Size availableSize, - VirtualizingLayoutContext context, - ILayoutable? firstRealized, - int firstRealizedItemIndex, - Rect firstRealizedLayoutBounds, - ILayoutable? lastRealized, - int lastRealizedItemIndex, - Rect lastRealizedLayoutBounds) - { - return GetExtent( - availableSize, - context, - firstRealized, - firstRealizedItemIndex, - firstRealizedLayoutBounds, - lastRealized, - lastRealizedItemIndex, - lastRealizedLayoutBounds); - } - - void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) - { - OnElementMeasured( - element, - index, - availableSize, - measureSize, - desiredSize, - provisionalArrangeSize, - context); - } - - void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context) - { - } - - internal FlowLayoutAnchorInfo GetAnchorForRealizationRect( - Size availableSize, - VirtualizingLayoutContext context) - { - int anchorIndex = -1; - double offset = double.NaN; - - // Constants - int itemsCount = context.ItemCount; - if (itemsCount > 0) - { - var realizationRect = context.RealizationRect; - var state = (StackLayoutState)context.LayoutState!; - var lastExtent = state.FlowAlgorithm.LastExtent; - - double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing; - double realizationWindowOffsetInExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); - double majorSize = _orientation.MajorSize(lastExtent) == 0 ? Math.Max(0.0, averageElementSize * itemsCount - Spacing) : _orientation.MajorSize(lastExtent); - if (itemsCount > 0 && - _orientation.MajorSize(realizationRect) >= 0 && - // MajorSize = 0 will account for when a nested repeater is outside the realization rect but still being measured. Also, - // note that if we are measuring this repeater, then we are already realizing an element to figure out the size, so we could - // just keep that element alive. It also helps in XYFocus scenarios to have an element realized for XYFocus to find a candidate - // in the navigating direction. - realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize) - { - anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize); - anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex)); - offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent); - } - } - - return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, }; - } - - protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) - { - var state = context.LayoutState; - var stackState = state as StackLayoutState; - - if (stackState == null) - { - if (state != null) - { - throw new InvalidOperationException("LayoutState must derive from StackLayoutState."); - } - - // Custom deriving layouts could potentially be stateful. - // If that is the case, we will just create the base state required by UniformGridLayout ourselves. - stackState = new StackLayoutState(); - } - - stackState.InitializeForContext(context, this); - } - - protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) - { - var stackState = (StackLayoutState)context.LayoutState!; - stackState.UninitializeForContext(context); - } - - protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) - { - ((StackLayoutState)context.LayoutState!).OnMeasureStart(); - - var desiredSize = GetFlowAlgorithm(context).Measure( - availableSize, - context, - false, - 0, - Spacing, - int.MaxValue, - _orientation.ScrollOrientation, - DisableVirtualization, - LayoutId); - - return new Size(desiredSize.Width, desiredSize.Height); - } - - protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) - { - var value = GetFlowAlgorithm(context).Arrange( - finalSize, - context, - false, - FlowLayoutAlgorithm.LineAlignment.Start, - LayoutId); - - return new Size(value.Width, value.Height); - } - - protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args) - { - GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context); - // Always invalidate layout to keep the view accurate. - InvalidateLayout(); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - if (change.Property == OrientationProperty) - { - var orientation = change.NewValue.GetValueOrDefault(); - - //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. - //Horizontal Orientation means we have a Horizontal ScrollOrientation. - _orientation.ScrollOrientation = orientation == Orientation.Horizontal ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical; - } - - InvalidateLayout(); - } - - private double GetAverageElementSize( - Size availableSize, - VirtualizingLayoutContext context, - StackLayoutState stackLayoutState) - { - double averageElementSize = 0; - - if (context.ItemCount > 0) - { - if (stackLayoutState.TotalElementsMeasured == 0) - { - var tmpElement = context.GetOrCreateElementAt(0, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); - stackLayoutState.FlowAlgorithm.MeasureElement(tmpElement, 0, availableSize, context); - context.RecycleElement(tmpElement); - } - - averageElementSize = Math.Round(stackLayoutState.TotalElementSize / stackLayoutState.TotalElementsMeasured); - } - - return averageElementSize; - } - - private void InvalidateLayout() => InvalidateMeasure(); - - private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((StackLayoutState)context.LayoutState!).FlowAlgorithm; - } -} diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs deleted file mode 100644 index 3b82ece886..0000000000 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ /dev/null @@ -1,561 +0,0 @@ -// This source file is 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.Collections.Specialized; -using Avalonia.Data; -using Avalonia.Logging; - -namespace Avalonia.Layout -{ - /// - /// Defines constants that specify how items are aligned on the non-scrolling or non-virtualizing axis. - /// - public enum UniformGridLayoutItemsJustification - { - /// - /// Items are aligned with the start of the row or column, with extra space at the end. - /// Spacing between items does not change. - /// - Start = 0, - - /// - /// Items are aligned in the center of the row or column, with extra space at the start and - /// end. Spacing between items does not change. - /// - Center = 1, - - /// - /// Items are aligned with the end of the row or column, with extra space at the start. - /// Spacing between items does not change. - /// - End = 2, - - /// - /// Items are aligned so that extra space is added evenly before and after each item. - /// - SpaceAround = 3, - - /// - /// Items are aligned so that extra space is added evenly between adjacent items. No space - /// is added at the start or end. - /// - SpaceBetween = 4, - - SpaceEvenly = 5, - }; - - /// - /// Defines constants that specify how items are sized to fill the available space. - /// - public enum UniformGridLayoutItemsStretch - { - /// - /// The item retains its natural size. Use of extra space is determined by the - /// property. - /// - None = 0, - - /// - /// The item is sized to fill the available space in the non-scrolling direction. Item size - /// in the scrolling direction is not changed. - /// - Fill = 1, - - /// - /// The item is sized to both fill the available space in the non-scrolling direction and - /// maintain its aspect ratio. - /// - Uniform = 2, - }; - - /// - /// Positions elements sequentially from left to right or top to bottom in a wrapping layout. - /// - public class UniformGridLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates - { - /// - /// Defines the property. - /// - public static readonly StyledProperty ItemsJustificationProperty = - AvaloniaProperty.Register(nameof(ItemsJustification)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ItemsStretchProperty = - AvaloniaProperty.Register(nameof(ItemsStretch)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinColumnSpacingProperty = - AvaloniaProperty.Register(nameof(MinColumnSpacing)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinItemHeightProperty = - AvaloniaProperty.Register(nameof(MinItemHeight)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinItemWidthProperty = - AvaloniaProperty.Register(nameof(MinItemWidth)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinRowSpacingProperty = - AvaloniaProperty.Register(nameof(MinRowSpacing)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MaximumRowsOrColumnsProperty = - AvaloniaProperty.Register(nameof(MinItemWidth)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty OrientationProperty = - StackLayout.OrientationProperty.AddOwner(); - - private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); - private double _minItemWidth = double.NaN; - private double _minItemHeight = double.NaN; - private double _minRowSpacing; - private double _minColumnSpacing; - private UniformGridLayoutItemsJustification _itemsJustification; - private UniformGridLayoutItemsStretch _itemsStretch; - private int _maximumRowsOrColumns = int.MaxValue; - - /// - /// Initializes a new instance of the class. - /// - public UniformGridLayout() - { - LayoutId = "UniformGridLayout"; - } - - static UniformGridLayout() - { - OrientationProperty.OverrideDefaultValue(Orientation.Horizontal); - } - - /// - /// Gets or sets a value that indicates how items are aligned on the non-scrolling or non- - /// virtualizing axis. - /// - /// - /// An enumeration value that indicates how items are aligned. The default is Start. - /// - public UniformGridLayoutItemsJustification ItemsJustification - { - get => GetValue(ItemsJustificationProperty); - set => SetValue(ItemsJustificationProperty, value); - } - - /// - /// Gets or sets a value that indicates how items are sized to fill the available space. - /// - /// - /// An enumeration value that indicates how items are sized to fill the available space. - /// The default is None. - /// - /// - /// This property enables adaptive layout behavior where the items are sized to fill the - /// available space along the non-scrolling axis, and optionally maintain their aspect ratio. - /// - public UniformGridLayoutItemsStretch ItemsStretch - { - get => GetValue(ItemsStretchProperty); - set => SetValue(ItemsStretchProperty, value); - } - - /// - /// Gets or sets the minimum space between items on the horizontal axis. - /// - /// - /// The spacing may exceed this minimum value when is set - /// to SpaceEvenly, SpaceAround, or SpaceBetween. - /// - public double MinColumnSpacing - { - get => GetValue(MinColumnSpacingProperty); - set => SetValue(MinColumnSpacingProperty, value); - } - - /// - /// Gets or sets the minimum height of each item. - /// - /// - /// The minimum height (in pixels) of each item. The default is NaN, in which case the - /// height of the first item is used as the minimum. - /// - public double MinItemHeight - { - get => GetValue(MinItemHeightProperty); - set => SetValue(MinItemHeightProperty, value); - } - - /// - /// Gets or sets the minimum width of each item. - /// - /// - /// The minimum width (in pixels) of each item. The default is NaN, in which case the width - /// of the first item is used as the minimum. - /// - public double MinItemWidth - { - get => GetValue(MinItemWidthProperty); - set => SetValue(MinItemWidthProperty, value); - } - - /// - /// Gets or sets the minimum space between items on the vertical axis. - /// - /// - /// The spacing may exceed this minimum value when is set - /// to SpaceEvenly, SpaceAround, or SpaceBetween. - /// - public double MinRowSpacing - { - get => GetValue(MinRowSpacingProperty); - set => SetValue(MinRowSpacingProperty, value); - } - - /// - /// Gets or sets the maximum row or column count. - /// - public int MaximumRowsOrColumns - { - get => GetValue(MaximumRowsOrColumnsProperty); - set => SetValue(MaximumRowsOrColumnsProperty, value); - } - - /// - /// Gets or sets the axis along which items are laid out. - /// - /// - /// One of the enumeration values that specifies the axis along which items are laid out. - /// The default is Vertical. - /// - public Orientation Orientation - { - get => GetValue(OrientationProperty); - set => SetValue(OrientationProperty, value); - } - - internal double LineSpacing => Orientation == Orientation.Horizontal ? _minRowSpacing : _minColumnSpacing; - internal double MinItemSpacing => Orientation == Orientation.Horizontal ? _minColumnSpacing : _minRowSpacing; - - Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize( - int index, - Size availableSize, - VirtualizingLayoutContext context) - { - var gridState = (UniformGridLayoutState)context.LayoutState!; - return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight); - } - - Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize( - int index, - Size measureSize, - Size desiredSize, - VirtualizingLayoutContext context) - { - var gridState = (UniformGridLayoutState)context.LayoutState!; - return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight); - } - - bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => remainingSpace < 0; - - FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect( - Size availableSize, - VirtualizingLayoutContext context) - { - Rect bounds = new Rect(double.NaN, double.NaN, double.NaN, double.NaN); - int anchorIndex = -1; - - int itemsCount = context.ItemCount; - var realizationRect = context.RealizationRect; - if (itemsCount > 0 && _orientation.MajorSize(realizationRect) > 0) - { - var gridState = (UniformGridLayoutState)context.LayoutState!; - var lastExtent = gridState.FlowAlgorithm.LastExtent; - var itemsPerLine = Math.Min( // note use of unsigned ints - Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), - Math.Max(1u, (uint)_maximumRowsOrColumns)); - var majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); - var realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); - if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize) - { - double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent)); - int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context)); - - anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); - bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context); - } - } - - return new FlowLayoutAnchorInfo - { - Index = anchorIndex, - Offset = _orientation.MajorStart(bounds) - }; - } - - FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement( - int targetIndex, - Size availableSize, - VirtualizingLayoutContext context) - { - int index = -1; - double offset = double.NaN; - int count = context.ItemCount; - if (targetIndex >= 0 && targetIndex < count) - { - int itemsPerLine = (int)Math.Min( // note use of unsigned ints - Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), - Math.Max(1u, _maximumRowsOrColumns)); - int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine; - index = indexOfFirstInLine; - var state = (UniformGridLayoutState)context.LayoutState!; - offset = _orientation.MajorStart(GetLayoutRectForDataIndex(availableSize, indexOfFirstInLine, state.FlowAlgorithm.LastExtent, context)); - } - - return new FlowLayoutAnchorInfo - { - Index = index, - Offset = offset - }; - } - - Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( - Size availableSize, - VirtualizingLayoutContext context, - ILayoutable? firstRealized, - int firstRealizedItemIndex, - Rect firstRealizedLayoutBounds, - ILayoutable? lastRealized, - int lastRealizedItemIndex, - Rect lastRealizedLayoutBounds) - { - var extent = new Rect(); - - - // Constants - int itemsCount = context.ItemCount; - double availableSizeMinor = _orientation.Minor(availableSize); - int itemsPerLine = - (int)Math.Min( // note use of unsigned ints - Math.Max(1u, !double.IsInfinity(availableSizeMinor) - ? (uint)(availableSizeMinor / GetMinorSizeWithSpacing(context)) - : (uint)itemsCount), - Math.Max(1u, _maximumRowsOrColumns)); - double lineSize = GetMajorSizeWithSpacing(context); - - if (itemsCount > 0) - { - _orientation.SetMinorSize( - ref extent, - !double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ? - availableSizeMinor : - Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); - _orientation.SetMajorSize( - ref extent, - Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing)); - - if (firstRealized != null) - { - _orientation.SetMajorStart( - ref extent, - _orientation.MajorStart(firstRealizedLayoutBounds) - (firstRealizedItemIndex / itemsPerLine) * lineSize); - int remainingItems = itemsCount - lastRealizedItemIndex - 1; - _orientation.SetMajorSize( - ref extent, - _orientation.MajorEnd(lastRealizedLayoutBounds) - _orientation.MajorStart(extent) + (remainingItems / itemsPerLine) * lineSize); - } - else - { - Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements", LayoutId); - } - } - - Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on lineSize {LineSize} and items per line {ItemsPerLine}", - LayoutId, extent.Size, lineSize, itemsPerLine); - return extent; - } - - void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) - { - } - - void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context) - { - } - - protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) - { - var state = context.LayoutState; - var gridState = state as UniformGridLayoutState; - - if (gridState == null) - { - if (state != null) - { - throw new InvalidOperationException("LayoutState must derive from UniformGridLayoutState."); - } - - // Custom deriving layouts could potentially be stateful. - // If that is the case, we will just create the base state required by UniformGridLayout ourselves. - gridState = new UniformGridLayoutState(); - } - - gridState.InitializeForContext(context, this); - } - - protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) - { - var gridState = (UniformGridLayoutState)context.LayoutState!; - gridState.UninitializeForContext(context); - } - - protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) - { - // Set the width and height on the grid state. If the user already set them then use the preset. - // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items. - var gridState = (UniformGridLayoutState)context.LayoutState!; - gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing, _maximumRowsOrColumns); - - var desiredSize = GetFlowAlgorithm(context).Measure( - availableSize, - context, - true, - MinItemSpacing, - LineSpacing, - _maximumRowsOrColumns, - _orientation.ScrollOrientation, - false, - LayoutId); - - // If after Measure the first item is in the realization rect, then we revoke grid state's ownership, - // and only use the layout when to clear it when it's done. - gridState.EnsureFirstElementOwnership(context); - - return desiredSize; - } - - protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) - { - var value = GetFlowAlgorithm(context).Arrange( - finalSize, - context, - true, - (FlowLayoutAlgorithm.LineAlignment)_itemsJustification, - LayoutId); - return new Size(value.Width, value.Height); - } - - protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args) - { - GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context); - // Always invalidate layout to keep the view accurate. - InvalidateLayout(); - - var gridState = (UniformGridLayoutState)context.LayoutState!; - gridState.ClearElementOnDataSourceChange(context, args); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - if (change.Property == OrientationProperty) - { - var orientation = change.NewValue.GetValueOrDefault(); - - //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. - //i.e. the properties are the inverse of each other. - var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal; - _orientation.ScrollOrientation = scrollOrientation; - } - else if (change.Property == MinColumnSpacingProperty) - { - _minColumnSpacing = change.NewValue.GetValueOrDefault(); - } - else if (change.Property == MinRowSpacingProperty) - { - _minRowSpacing = change.NewValue.GetValueOrDefault(); - } - else if (change.Property == ItemsJustificationProperty) - { - _itemsJustification = change.NewValue.GetValueOrDefault(); - } - else if (change.Property == ItemsStretchProperty) - { - _itemsStretch = change.NewValue.GetValueOrDefault(); - } - else if (change.Property == MinItemWidthProperty) - { - _minItemWidth = change.NewValue.GetValueOrDefault(); - } - else if (change.Property == MinItemHeightProperty) - { - _minItemHeight = change.NewValue.GetValueOrDefault(); - } - else if (change.Property == MaximumRowsOrColumnsProperty) - { - _maximumRowsOrColumns = change.NewValue.GetValueOrDefault(); - } - - InvalidateLayout(); - } - - private double GetMinorSizeWithSpacing(VirtualizingLayoutContext context) - { - var minItemSpacing = MinItemSpacing; - var gridState = (UniformGridLayoutState)context.LayoutState!; - return _orientation.ScrollOrientation == ScrollOrientation.Vertical? - gridState.EffectiveItemWidth + minItemSpacing : - gridState.EffectiveItemHeight + minItemSpacing; - } - - private double GetMajorSizeWithSpacing(VirtualizingLayoutContext context) - { - var lineSpacing = LineSpacing; - var gridState = (UniformGridLayoutState)context.LayoutState!; - return _orientation.ScrollOrientation == ScrollOrientation.Vertical ? - gridState.EffectiveItemHeight + lineSpacing : - gridState.EffectiveItemWidth + lineSpacing; - } - - Rect GetLayoutRectForDataIndex( - Size availableSize, - int index, - Rect lastExtent, - VirtualizingLayoutContext context) - { - int itemsPerLine = (int)Math.Min( //note use of unsigned ints - Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), - Math.Max(1u, _maximumRowsOrColumns)); - int rowIndex = (int)(index / itemsPerLine); - int indexInRow = index - (rowIndex * itemsPerLine); - - var gridState = (UniformGridLayoutState)context.LayoutState!; - Rect bounds = _orientation.MinorMajorRect( - indexInRow * GetMinorSizeWithSpacing(context) + _orientation.MinorStart(lastExtent), - rowIndex * GetMajorSizeWithSpacing(context) + _orientation.MajorStart(lastExtent), - _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight, - _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth); - - return bounds; - } - - private void InvalidateLayout() => InvalidateMeasure(); - - private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((UniformGridLayoutState)context.LayoutState!).FlowAlgorithm; - } -} diff --git a/src/Avalonia.Layout/WrapLayout/WrapLayout.cs b/src/Avalonia.Layout/WrapLayout/WrapLayout.cs deleted file mode 100644 index aab0272f37..0000000000 --- a/src/Avalonia.Layout/WrapLayout/WrapLayout.cs +++ /dev/null @@ -1,336 +0,0 @@ -// 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. - -using Avalonia; -using Avalonia.Controls; -using Avalonia.Data; -using Avalonia.Layout; -using System; -using System.Collections.Specialized; - -namespace Avalonia.Layout -{ - /// - /// Arranges elements by wrapping them to fit the available space. - /// When is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row. - /// When is set to Orientation.Vertical, element are arranged in columns until the available height is reached. - /// - public class WrapLayout : VirtualizingLayout - { - /// - /// Gets or sets a uniform Horizontal distance (in pixels) between items when is set to Horizontal, - /// or between columns of items when is set to Vertical. - /// - public double HorizontalSpacing - { - get { return (double)GetValue(HorizontalSpacingProperty); } - set { SetValue(HorizontalSpacingProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly StyledProperty HorizontalSpacingProperty = - AvaloniaProperty.Register(nameof(HorizontalSpacing), 0); - - /// - /// Gets or sets a uniform Vertical distance (in pixels) between items when is set to Vertical, - /// or between rows of items when is set to Horizontal. - /// - public double VerticalSpacing - { - get { return (double)GetValue(VerticalSpacingProperty); } - set { SetValue(VerticalSpacingProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly StyledProperty VerticalSpacingProperty = - AvaloniaProperty.Register( - nameof(VerticalSpacing), 0d); - - /// - /// Gets or sets the orientation of the WrapLayout. - /// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls. - /// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added. - /// - public Orientation Orientation - { - get { return (Orientation)GetValue(OrientationProperty); } - set { SetValue(OrientationProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register( - nameof(Orientation), - Orientation.Horizontal); - - /// - protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) - { - var state = new WrapLayoutState(context); - context.LayoutState = state; - base.InitializeForContextCore(context); - } - - /// - protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) - { - context.LayoutState = null; - base.UninitializeForContextCore(context); - } - - /// - protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args) - { - var state = (WrapLayoutState)context.LayoutState!; - - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - state.RemoveFromIndex(args.NewStartingIndex); - break; - case NotifyCollectionChangedAction.Move: - int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex); - state.RemoveFromIndex(minIndex); - - state.RecycleElementAt(args.OldStartingIndex); - state.RecycleElementAt(args.NewStartingIndex); - break; - case NotifyCollectionChangedAction.Remove: - state.RemoveFromIndex(args.OldStartingIndex); - break; - case NotifyCollectionChangedAction.Replace: - state.RemoveFromIndex(args.NewStartingIndex); - state.RecycleElementAt(args.NewStartingIndex); - break; - case NotifyCollectionChangedAction.Reset: - state.Clear(); - break; - } - - base.OnItemsChangedCore(context, source, args); - } - - /// - protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) - { - var totalMeasure = UvMeasure.Zero; - var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height); - var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); - var realizationBounds = new UvBounds(Orientation, context.RealizationRect); - var position = UvMeasure.Zero; - - var state = (WrapLayoutState)context.LayoutState!; - if (state.Orientation != Orientation) - { - state.SetOrientation(Orientation); - } - - if (spacingMeasure.Equals(state.Spacing) == false) - { - state.ClearPositions(); - state.Spacing = spacingMeasure; - } - - if (state.AvailableU != parentMeasure.U) - { - state.ClearPositions(); - state.AvailableU = parentMeasure.U; - } - - double currentV = 0; - for (int i = 0; i < context.ItemCount; i++) - { - bool measured = false; - WrapItem item = state.GetItemAt(i); - if (item.Measure == null) - { - item.Element = context.GetOrCreateElementAt(i); - item.Element.Measure(availableSize); - item.Measure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height); - measured = true; - } - - UvMeasure currentMeasure = item.Measure.Value; - if (currentMeasure.U == 0) - { - continue; // ignore collapsed items - } - - if (item.Position == null) - { - if (parentMeasure.U < position.U + currentMeasure.U) - { - // New Row - position.U = 0; - position.V += currentV + spacingMeasure.V; - currentV = 0; - } - - item.Position = position; - } - - position = item.Position.Value; - - double vEnd = position.V + currentMeasure.V; - if (vEnd < realizationBounds.VMin) - { - // Item is "above" the bounds - if (item.Element != null) - { - context.RecycleElement(item.Element); - item.Element = null; - } - } - else if (position.V > realizationBounds.VMax) - { - // Item is "below" the bounds. - if (item.Element != null) - { - context.RecycleElement(item.Element); - item.Element = null; - } - - // We don't need to measure anything below the bounds - break; - } - else if (measured == false) - { - // Always measure elements that are within the bounds - item.Element = context.GetOrCreateElementAt(i); - item.Element.Measure(availableSize); - - currentMeasure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height); - if (currentMeasure.Equals(item.Measure) == false) - { - // this item changed size; we need to recalculate layout for everything after this - state.RemoveFromIndex(i + 1); - item.Measure = currentMeasure; - - // did the change make it go into the new row? - if (parentMeasure.U < position.U + currentMeasure.U) - { - // New Row - position.U = 0; - position.V += currentV + spacingMeasure.V; - currentV = 0; - } - - item.Position = position; - } - } - - position.U += currentMeasure.U + spacingMeasure.U; - currentV = Math.Max(currentMeasure.V, currentV); - } - - // update value with the last line - // if the the last loop is(parentMeasure.U > currentMeasure.U + lineMeasure.U) the total isn't calculated then calculate it - // if the last loop is (parentMeasure.U > currentMeasure.U) the currentMeasure isn't added to the total so add it here - // for the last condition it is zeros so adding it will make no difference - // this way is faster than an if condition in every loop for checking the last item - totalMeasure.U = parentMeasure.U; - - // Propagating an infinite size causes a crash. This can happen if the parent is scrollable and infinite in the opposite - // axis to the panel. Clearing to zero prevents the crash. - // This is likely an incorrect use of the control by the developer, however we need stability here so setting a default that wont crash. - if (double.IsInfinity(totalMeasure.U)) - { - totalMeasure.U = 0.0; - } - - totalMeasure.V = state.GetHeight(); - - totalMeasure.U = Math.Ceiling(totalMeasure.U); - - return Orientation == Orientation.Horizontal ? new Size(totalMeasure.U, totalMeasure.V) : new Size(totalMeasure.V, totalMeasure.U); - } - - /// - protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) - { - if (context.ItemCount > 0) - { - var parentMeasure = new UvMeasure(Orientation, finalSize.Width, finalSize.Height); - var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); - var realizationBounds = new UvBounds(Orientation, context.RealizationRect); - - var state = (WrapLayoutState)context.LayoutState!; - bool Arrange(WrapItem item, bool isLast = false) - { - if (item.Measure.HasValue == false) - { - return false; - } - - if (item.Position == null) - { - return false; - } - - var desiredMeasure = item.Measure.Value; - if (desiredMeasure.U == 0) - { - return true; // if an item is collapsed, avoid adding the spacing - } - - UvMeasure position = item.Position.Value; - - // Stretch the last item to fill the available space - if (isLast) - { - desiredMeasure.U = parentMeasure.U - position.U; - } - - if (((position.V + desiredMeasure.V) >= realizationBounds.VMin) && (position.V <= realizationBounds.VMax)) - { - // place the item - var child = context.GetOrCreateElementAt(item.Index); - if (Orientation == Orientation.Horizontal) - { - child.Arrange(new Rect(position.U, position.V, desiredMeasure.U, desiredMeasure.V)); - } - else - { - child.Arrange(new Rect(position.V, position.U, desiredMeasure.V, desiredMeasure.U)); - } - } - else if (position.V > realizationBounds.VMax) - { - return false; - } - - return true; - } - - for (var i = 0; i < context.ItemCount; i++) - { - bool continueArranging = Arrange(state.GetItemAt(i)); - if (continueArranging == false) - { - break; - } - } - } - - return finalSize; - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == OrientationProperty || change.Property == HorizontalSpacingProperty || change.Property == VerticalSpacingProperty) - { - InvalidateMeasure(); - InvalidateArrange(); - } - } - } -} diff --git a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj index b796e173c4..d7f39f6642 100644 --- a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj +++ b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj @@ -10,6 +10,8 @@ false all + true + TargetFramework=netstandard2.0 diff --git a/src/Avalonia.MicroCom/CallbackBase.cs b/src/Avalonia.MicroCom/CallbackBase.cs new file mode 100644 index 0000000000..6783ebe3dc --- /dev/null +++ b/src/Avalonia.MicroCom/CallbackBase.cs @@ -0,0 +1,53 @@ +namespace Avalonia.MicroCom +{ + public abstract class CallbackBase : IUnknown, IMicroComShadowContainer + { + private readonly object _lock = new object(); + private bool _referencedFromManaged = true; + private bool _referencedFromNative = false; + private bool _destroyed; + + public bool IsDestroyed => _destroyed; + + protected virtual void Destroyed() + { + + } + + public void Dispose() + { + lock (_lock) + { + _referencedFromManaged = false; + DestroyIfNeeded(); + } + } + + void DestroyIfNeeded() + { + if(_destroyed) + return; + if (_referencedFromManaged == false && _referencedFromNative == false) + { + _destroyed = true; + Destroyed(); + } + } + + public MicroComShadow Shadow { get; set; } + public void OnReferencedFromNative() + { + lock (_lock) + _referencedFromNative = true; + } + + public void OnUnreferencedFromNative() + { + lock (_lock) + { + _referencedFromNative = false; + DestroyIfNeeded(); + } + } + } +} diff --git a/src/Avalonia.MicroCom/MicroComRuntime.cs b/src/Avalonia.MicroCom/MicroComRuntime.cs index e0f524146a..b9a56a69ba 100644 --- a/src/Avalonia.MicroCom/MicroComRuntime.cs +++ b/src/Avalonia.MicroCom/MicroComRuntime.cs @@ -12,6 +12,8 @@ namespace Avalonia.MicroCom new ConcurrentDictionary>(); private static ConcurrentDictionary _guids = new ConcurrentDictionary(); private static ConcurrentDictionary _guidsToTypes = new ConcurrentDictionary(); + + internal static readonly Guid ManagedObjectInterfaceGuid = Guid.Parse("cd7687c0-a9c2-4563-b08e-a399df50c633"); static MicroComRuntime() { @@ -82,6 +84,19 @@ namespace Avalonia.MicroCom return shadow.Target; } + public static bool IsComWrapper(IUnknown obj) => obj is MicroComProxyBase; + + public static object TryUnwrapManagedObject(IUnknown obj) + { + if (obj is not MicroComProxyBase proxy) + return null; + if (proxy.QueryInterface(ManagedObjectInterfaceGuid, out _) != 0) + return null; + // Successful QueryInterface always increments ref counter + proxy.Release(); + return GetObjectFromCcw(proxy.NativePointer); + } + public static bool TryGetTypeForGuid(Guid guid, out Type t) => _guidsToTypes.TryGetValue(guid, out t); public static bool GetVtableFor(Type type, out IntPtr ptr) => _vtables.TryGetValue(type, out ptr); diff --git a/src/Avalonia.MicroCom/MicroComShadow.cs b/src/Avalonia.MicroCom/MicroComShadow.cs index a6a0fd519e..765ff3b9ad 100644 --- a/src/Avalonia.MicroCom/MicroComShadow.cs +++ b/src/Avalonia.MicroCom/MicroComShadow.cs @@ -23,6 +23,12 @@ namespace Avalonia.MicroCom { if (MicroComRuntime.TryGetTypeForGuid(*guid, out var type)) return QueryInterface(type, ppv); + else if (*guid == MicroComRuntime.ManagedObjectInterfaceGuid) + { + ccw->RefCount++; + *ppv = ccw; + return 0; + } else return unchecked((int)0x80004002u); } diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 9301fd4d91..2001a2fcbc 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -17,6 +17,10 @@ PreserveNewest + + + + @@ -24,4 +28,6 @@ + + diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index 8084e06d28..ae24d06bad 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -6,7 +6,7 @@ using Avalonia.Platform; namespace Avalonia.Native { - internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents, IPlatformLifetimeEventsImpl + internal class AvaloniaNativeApplicationPlatform : NativeCallbackBase, IAvnApplicationEvents, IPlatformLifetimeEventsImpl { public event EventHandler ShutdownRequested; diff --git a/src/Avalonia.Native/AvaloniaNativeDragSource.cs b/src/Avalonia.Native/AvaloniaNativeDragSource.cs index 80d54d8a10..f91a299b3b 100644 --- a/src/Avalonia.Native/AvaloniaNativeDragSource.cs +++ b/src/Avalonia.Native/AvaloniaNativeDragSource.cs @@ -30,7 +30,7 @@ namespace Avalonia.Native return visual.VisualRoot as TopLevel; } - class DndCallback : CallbackBase, IAvnDndResultCallback + class DndCallback : NativeCallbackBase, IAvnDndResultCallback { private TaskCompletionSource _tcs; diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 1eadf70b13..4802fed554 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -79,7 +79,7 @@ namespace Avalonia.Native _factory = factory; } - class GCHandleDeallocator : CallbackBase, IAvnGCHandleDeallocatorCallback + class GCHandleDeallocator : NativeCallbackBase, IAvnGCHandleDeallocatorCallback { public void FreeGCHandle(IntPtr handle) { diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs new file mode 100644 index 0000000000..6c4e96b31b --- /dev/null +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Native.Interop; + +#nullable enable + +namespace Avalonia.Native +{ + internal class AvnAutomationPeer : NativeCallbackBase, IAvnAutomationPeer + { + private static readonly ConditionalWeakTable s_wrappers = new(); + private readonly AutomationPeer _inner; + + private AvnAutomationPeer(AutomationPeer inner) + { + _inner = inner; + _inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged(); + if (inner is WindowBaseAutomationPeer window) + window.FocusChanged += (_, _) => Node?.FocusChanged(); + } + + ~AvnAutomationPeer() => Node?.Dispose(); + + public IAvnAutomationNode? Node { get; private set; } + public IAvnString? AcceleratorKey => _inner.GetAcceleratorKey().ToAvnString(); + public IAvnString? AccessKey => _inner.GetAccessKey().ToAvnString(); + public AvnAutomationControlType AutomationControlType => (AvnAutomationControlType)_inner.GetAutomationControlType(); + public IAvnString? AutomationId => _inner.GetAutomationId().ToAvnString(); + public AvnRect BoundingRectangle => _inner.GetBoundingRectangle().ToAvnRect(); + public IAvnAutomationPeerArray Children => new AvnAutomationPeerArray(_inner.GetChildren()); + public IAvnString ClassName => _inner.GetClassName().ToAvnString(); + public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy()); + public IAvnString Name => _inner.GetName().ToAvnString(); + public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent()); + + public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool(); + public int IsContentElement() => _inner.IsContentElement().AsComBool(); + public int IsControlElement() => _inner.IsControlElement().AsComBool(); + public int IsEnabled() => _inner.IsEnabled().AsComBool(); + public int IsKeyboardFocusable() => _inner.IsKeyboardFocusable().AsComBool(); + public void SetFocus() => _inner.SetFocus(); + public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool(); + + public IAvnAutomationPeer? RootPeer + { + get + { + var peer = _inner; + var parent = peer.GetParent(); + + while (peer is not IRootProvider && parent is not null) + { + peer = parent; + parent = peer.GetParent(); + } + + return Wrap(peer); + } + } + + public void SetNode(IAvnAutomationNode node) + { + if (Node is not null) + throw new InvalidOperationException("The AvnAutomationPeer already has a node."); + Node = node; + } + + public int IsRootProvider() => (_inner is IRootProvider).AsComBool(); + + public IAvnWindowBase RootProvider_GetWindow() + { + var window = (WindowBase)((ControlAutomationPeer)_inner).Owner; + return ((WindowBaseImpl)window.PlatformImpl!).Native; + } + + public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus()); + + public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point) + { + var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint()); + + if (result is null) + return null; + + // The OSX accessibility APIs expect non-ignored elements when hit-testing. + while (!result.IsControlElement()) + { + var parent = result.GetParent(); + + if (parent is not null) + result = parent; + else + break; + } + + return Wrap(result); + } + + public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool(); + + public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch + { + ExpandCollapseState.Expanded => 1, + ExpandCollapseState.PartiallyExpanded => 1, + _ => 0, + }; + + public int ExpandCollapseProvider_GetShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool(); + public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand(); + public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse(); + + public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool(); + public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke(); + + public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool(); + public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value; + public double RangeValueProvider_GetMinimum() => ((IRangeValueProvider)_inner).Minimum; + public double RangeValueProvider_GetMaximum() => ((IRangeValueProvider)_inner).Maximum; + public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange; + public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange; + public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value); + + public int IsSelectionItemProvider() => (_inner is ISelectionItemProvider).AsComBool(); + public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool(); + + public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool(); + public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState; + public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle(); + + public int IsValueProvider() => (_inner is IValueProvider).AsComBool(); + public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString(); + public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value); + + [return: NotNullIfNotNull("peer")] + public static AvnAutomationPeer? Wrap(AutomationPeer? peer) + { + return peer is null ? null : s_wrappers.GetValue(peer, x => new(peer)); + } + } + + internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray + { + private readonly AvnAutomationPeer[] _items; + + public AvnAutomationPeerArray(IReadOnlyList items) + { + _items = items.Select(x => AvnAutomationPeer.Wrap(x)).ToArray(); + } + + public uint Count => (uint)_items.Length; + public IAvnAutomationPeer Get(uint index) => _items[index]; + } +} diff --git a/src/Avalonia.Native/AvnString.cs b/src/Avalonia.Native/AvnString.cs index dcd473bae3..1c92bf0eec 100644 --- a/src/Avalonia.Native/AvnString.cs +++ b/src/Avalonia.Native/AvnString.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Text; namespace Avalonia.Native.Interop { @@ -13,6 +14,53 @@ namespace Avalonia.Native.Interop { string[] ToStringArray(); } + + internal class AvnString : NativeCallbackBase, IAvnString + { + private IntPtr _native; + private int _nativeLen; + + public AvnString(string s) => String = s; + + public string String { get; } + public byte[] Bytes => Encoding.UTF8.GetBytes(String); + + public unsafe void* Pointer() + { + EnsureNative(); + return _native.ToPointer(); + } + + public int Length() + { + EnsureNative(); + return _nativeLen; + } + + protected override void Destroyed() + { + if (_native != IntPtr.Zero) + { + Marshal.FreeHGlobal(_native); + _native = IntPtr.Zero; + } + } + + private unsafe void EnsureNative() + { + if (string.IsNullOrEmpty(String)) + return; + if (_native == IntPtr.Zero) + { + _nativeLen = Encoding.UTF8.GetByteCount(String); + _native = Marshal.AllocHGlobal(_nativeLen + 1); + var ptr = (byte*)_native.ToPointer(); + fixed (char* chars = String) + Encoding.UTF8.GetBytes(chars, String.Length, ptr, _nativeLen); + ptr[_nativeLen] = 0; + } + } + } } namespace Avalonia.Native.Interop.Impl { diff --git a/src/Avalonia.Native/CallbackBase.cs b/src/Avalonia.Native/CallbackBase.cs index 455ed4b159..56f9505cb4 100644 --- a/src/Avalonia.Native/CallbackBase.cs +++ b/src/Avalonia.Native/CallbackBase.cs @@ -5,19 +5,8 @@ using Avalonia.Platform; namespace Avalonia.Native { - public class CallbackBase : IUnknown, IMicroComShadowContainer, IMicroComExceptionCallback + public abstract class NativeCallbackBase : CallbackBase, IMicroComExceptionCallback { - private readonly object _lock = new object(); - private bool _referencedFromManaged = true; - private bool _referencedFromNative = false; - private bool _destroyed; - - - protected virtual void Destroyed() - { - - } - public void RaiseException(Exception e) { if (AvaloniaLocator.Current.GetService() is PlatformThreadingInterface threadingInterface) @@ -27,41 +16,5 @@ namespace Avalonia.Native threadingInterface.DispatchException(ExceptionDispatchInfo.Capture(e)); } } - - public void Dispose() - { - lock (_lock) - { - _referencedFromManaged = false; - DestroyIfNeeded(); - } - } - - void DestroyIfNeeded() - { - if(_destroyed) - return; - if (_referencedFromManaged == false && _referencedFromNative == false) - { - _destroyed = true; - Destroyed(); - } - } - - public MicroComShadow Shadow { get; set; } - public void OnReferencedFromNative() - { - lock (_lock) - _referencedFromNative = true; - } - - public void OnUnreferencedFromNative() - { - lock (_lock) - { - _referencedFromNative = false; - DestroyIfNeeded(); - } - } } } diff --git a/src/Avalonia.Native/DeferredFramebuffer.cs b/src/Avalonia.Native/DeferredFramebuffer.cs index 950b6a3197..4e0c037154 100644 --- a/src/Avalonia.Native/DeferredFramebuffer.cs +++ b/src/Avalonia.Native/DeferredFramebuffer.cs @@ -27,7 +27,7 @@ namespace Avalonia.Native public Vector Dpi { get; set; } public PixelFormat Format { get; set; } - class Disposer : CallbackBase + class Disposer : NativeCallbackBase { private IntPtr _ptr; diff --git a/src/Avalonia.Native/Helpers.cs b/src/Avalonia.Native/Helpers.cs index 564434a04c..764ff789dc 100644 --- a/src/Avalonia.Native/Helpers.cs +++ b/src/Avalonia.Native/Helpers.cs @@ -1,4 +1,5 @@ using Avalonia.Native.Interop; +using JetBrains.Annotations; namespace Avalonia.Native { @@ -24,11 +25,21 @@ namespace Avalonia.Native return new AvnPoint { X = pt.X, Y = pt.Y }; } + public static AvnRect ToAvnRect (this Rect rect) + { + return new AvnRect() { X = rect.X, Y= rect.Y, Height = rect.Height, Width = rect.Width }; + } + public static AvnSize ToAvnSize (this Size size) { return new AvnSize { Height = size.Height, Width = size.Width }; } + public static IAvnString ToAvnString(this string s) + { + return s != null ? new AvnString(s) : null; + } + public static Size ToAvaloniaSize (this AvnSize size) { return new Size(size.Width, size.Height); diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index 0e6fdd2df0..e413023f6d 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -7,7 +7,7 @@ using Avalonia.Platform.Interop; namespace Avalonia.Native.Interop { - class MenuEvents : CallbackBase, IAvnMenuEvents + class MenuEvents : NativeCallbackBase, IAvnMenuEvents { private IAvnMenu _parent; diff --git a/src/Avalonia.Native/MenuActionCallback.cs b/src/Avalonia.Native/MenuActionCallback.cs index 5318195f30..3acbb9c19c 100644 --- a/src/Avalonia.Native/MenuActionCallback.cs +++ b/src/Avalonia.Native/MenuActionCallback.cs @@ -3,7 +3,7 @@ using Avalonia.Native.Interop; namespace Avalonia.Native { - public class MenuActionCallback : CallbackBase, IAvnActionCallback + public class MenuActionCallback : NativeCallbackBase, IAvnActionCallback { private Action _action; diff --git a/src/Avalonia.Native/PlatformThreadingInterface.cs b/src/Avalonia.Native/PlatformThreadingInterface.cs index 882d5a2ed3..99aa8a5850 100644 --- a/src/Avalonia.Native/PlatformThreadingInterface.cs +++ b/src/Avalonia.Native/PlatformThreadingInterface.cs @@ -9,7 +9,7 @@ namespace Avalonia.Native { internal class PlatformThreadingInterface : IPlatformThreadingInterface { - class TimerCallback : CallbackBase, IAvnActionCallback + class TimerCallback : NativeCallbackBase, IAvnActionCallback { readonly Action _tick; @@ -24,7 +24,7 @@ namespace Avalonia.Native } } - class SignaledCallback : CallbackBase, IAvnSignaledCallback + class SignaledCallback : NativeCallbackBase, IAvnSignaledCallback { readonly PlatformThreadingInterface _parent; diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 4680a2af17..76d3905b47 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -7,7 +7,6 @@ namespace Avalonia.Native { class PopupImpl : WindowBaseImpl, IPopupImpl { - private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; private readonly IWindowBaseImpl _parent; @@ -15,9 +14,8 @@ namespace Avalonia.Native public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature, - IWindowBaseImpl parent) : base(opts, glFeature) + IWindowBaseImpl parent) : base(factory, opts, glFeature) { - _factory = factory; _opts = opts; _glFeature = glFeature; _parent = parent; diff --git a/src/Avalonia.Native/PredicateCallback.cs b/src/Avalonia.Native/PredicateCallback.cs index 19c470bcb3..dbb65791f0 100644 --- a/src/Avalonia.Native/PredicateCallback.cs +++ b/src/Avalonia.Native/PredicateCallback.cs @@ -3,7 +3,7 @@ using Avalonia.Native.Interop; namespace Avalonia.Native { - public class PredicateCallback : CallbackBase, IAvnPredicateCallback + public class PredicateCallback : NativeCallbackBase, IAvnPredicateCallback { private Func _predicate; diff --git a/src/Avalonia.Native/Properties/AssemblyInfo.cs b/src/Avalonia.Native/Properties/AssemblyInfo.cs deleted file mode 100644 index d4766e45dc..0000000000 --- a/src/Avalonia.Native/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -using Avalonia.Native; -using Avalonia.Platform; - -[assembly: ExportWindowingSubsystem(OperatingSystemType.OSX, 1, "AvaloniaNative", typeof(AvaloniaNativePlatform), nameof(AvaloniaNativePlatform.Initialize))] diff --git a/src/Avalonia.Native/ScreenImpl.cs b/src/Avalonia.Native/ScreenImpl.cs index 7b4a001486..83db2e8a28 100644 --- a/src/Avalonia.Native/ScreenImpl.cs +++ b/src/Avalonia.Native/ScreenImpl.cs @@ -48,5 +48,20 @@ namespace Avalonia.Native _native?.Dispose(); _native = null; } + + public Screen ScreenFromPoint(PixelPoint point) + { + return ScreenHelper.ScreenFromPoint(point, AllScreens); + } + + public Screen ScreenFromRect(PixelRect rect) + { + return ScreenHelper.ScreenFromRect(rect, AllScreens); + } + + public Screen ScreenFromWindow(IWindowBaseImpl window) + { + return ScreenHelper.ScreenFromWindow(window, AllScreens); + } } } diff --git a/src/Avalonia.Native/SystemDialogs.cs b/src/Avalonia.Native/SystemDialogs.cs index 8012813644..d1d9c17ae3 100644 --- a/src/Avalonia.Native/SystemDialogs.cs +++ b/src/Avalonia.Native/SystemDialogs.cs @@ -30,7 +30,7 @@ namespace Avalonia.Native ofd.Title ?? "", ofd.Directory ?? "", ofd.InitialFileName ?? "", - string.Join(";", dialog.Filters.SelectMany(f => f.Extensions))); + string.Join(";", dialog.Filters?.SelectMany(f => f.Extensions) ?? Array.Empty())); } else { @@ -39,7 +39,7 @@ namespace Avalonia.Native dialog.Title ?? "", dialog.Directory ?? "", dialog.InitialFileName ?? "", - string.Join(";", dialog.Filters.SelectMany(f => f.Extensions))); + string.Join(";", dialog.Filters?.SelectMany(f => f.Extensions) ?? Array.Empty())); } return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; }); @@ -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..b5af927ea0 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 { @@ -106,6 +107,13 @@ namespace Avalonia.Native private bool _isExtended; public bool IsClientAreaExtendedToDecorations => _isExtended; + public override void Show(bool activate, bool isDialog) + { + base.Show(activate, isDialog); + + InvalidateExtendedMargins(); + } + protected override bool ChromeHitTest (RawPointerEventArgs e) { if(_isExtended) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 1917b1575d..94a3a5ed9b 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 @@ -110,8 +116,12 @@ namespace Avalonia.Native { if (_native != null) { - var s = _native.FrameSize; - return new Size(s.Width, s.Height); + unsafe + { + var s = new AvnSize { Width = -1, Height = -1 }; + _native.GetFrameSize(&s); + return s.Width < 0 && s.Height < 0 ? null : new Size(s.Width, s.Height); + } } return default; @@ -151,7 +161,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 +231,6 @@ namespace Avalonia.Native return _parent.RawTextInputEvent(timeStamp, text).AsComBool(); } - void IAvnWindowBaseEvents.ScalingChanged(double scaling) { _parent._savedScaling = scaling; @@ -256,8 +270,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..a1c73e1f03 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -2,6 +2,7 @@ @clr-access internal @clr-map bool int @cpp-preamble @@ +#pragma once #include "com.h" #include "stddef.h" @@ @@ -218,6 +219,14 @@ enum SystemDecorations { SystemDecorationsFull = 2, } +enum AvnAutomationProperty +{ + AutomationPeer_BoundingRectangle, + AutomationPeer_ClassName, + AutomationPeer_Name, + RangeValueProvider_Value, +} + struct AvnSize { double Width, Height; @@ -412,6 +421,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 { @@ -450,7 +504,7 @@ interface IAvnWindowBase : IUnknown HRESULT Close(); HRESULT Activate(); HRESULT GetClientSize(AvnSize*ret); - HRESULT GetFrameSize(AvnSize*ret); + HRESULT GetFrameSize(AvnSize*result); HRESULT GetScaling(double*ret); HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize); HRESULT Resize(double width, double height, AvnPlatformResizeReason reason); @@ -522,6 +576,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 +820,79 @@ interface IAvnApplicationCommands : IUnknown HRESULT ShowAll(); HRESULT HideOthers(); } + +[uuid(b87016f3-7eec-41de-b385-07844c268dc4)] +interface IAvnAutomationPeer : IUnknown +{ + IAvnAutomationNode* GetNode(); + void SetNode(IAvnAutomationNode* node); + + IAvnString* GetAcceleratorKey(); + IAvnString* GetAccessKey(); + AvnAutomationControlType GetAutomationControlType(); + IAvnString* GetAutomationId(); + AvnRect GetBoundingRectangle(); + IAvnAutomationPeerArray* GetChildren(); + IAvnString* GetClassName(); + IAvnAutomationPeer* GetLabeledBy(); + IAvnString* GetName(); + IAvnAutomationPeer* GetParent(); + bool HasKeyboardFocus(); + bool IsContentElement(); + bool IsControlElement(); + bool IsEnabled(); + bool IsKeyboardFocusable(); + void SetFocus(); + bool ShowContextMenu(); + + IAvnAutomationPeer* GetRootPeer(); + + bool IsRootProvider(); + IAvnWindowBase* RootProvider_GetWindow(); + IAvnAutomationPeer* RootProvider_GetFocus(); + IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point); + + bool IsExpandCollapseProvider(); + bool ExpandCollapseProvider_GetIsExpanded(); + bool ExpandCollapseProvider_GetShowsMenu(); + void ExpandCollapseProvider_Expand(); + void ExpandCollapseProvider_Collapse(); + + bool IsInvokeProvider(); + void InvokeProvider_Invoke(); + + bool IsRangeValueProvider(); + double RangeValueProvider_GetValue(); + double RangeValueProvider_GetMinimum(); + double RangeValueProvider_GetMaximum(); + double RangeValueProvider_GetSmallChange(); + double RangeValueProvider_GetLargeChange(); + void RangeValueProvider_SetValue(double value); + + bool IsSelectionItemProvider(); + bool SelectionItemProvider_IsSelected(); + + bool IsToggleProvider(); + int ToggleProvider_GetToggleState(); + void ToggleProvider_Toggle(); + + bool IsValueProvider(); + IAvnString* ValueProvider_GetValue(); + void ValueProvider_SetValue(char* value); +} + +[uuid(b00af5da-78af-4b33-bfff-4ce13a6239a9)] +interface IAvnAutomationPeerArray : IUnknown +{ + uint GetCount(); + HRESULT Get(uint index, IAvnAutomationPeer**ppv); +} + +[uuid(004dc40b-e435-49dc-bac5-6272ee35382a)] +interface IAvnAutomationNode : IUnknown +{ + void Dispose(); + void ChildrenChanged(); + void PropertyChanged(AvnAutomationProperty property); + void FocusChanged(); +} diff --git a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj index 2b32926008..76924d060f 100644 --- a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj +++ b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj @@ -8,6 +8,7 @@ - - + + + diff --git a/src/Avalonia.OpenGL/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.OpenGL/Imaging/IOpenGlBitmapImpl.cs b/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs index aef4f601be..22f0cebf57 100644 --- a/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs +++ b/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs @@ -1,15 +1,17 @@ using System; -using Avalonia.Media.Imaging; +using Avalonia.Metadata; using Avalonia.Platform; namespace Avalonia.OpenGL.Imaging { + [Unstable] public interface IOpenGlBitmapImpl : IBitmapImpl { IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback); bool SupportsContext(IGlContext context); } + [Unstable] public interface IOpenGlBitmapAttachment : IDisposable { void Present(); diff --git a/src/Avalonia.PlatformSupport/AppBuilder.cs b/src/Avalonia.PlatformSupport/AppBuilder.cs new file mode 100644 index 0000000000..136f1f39b3 --- /dev/null +++ b/src/Avalonia.PlatformSupport/AppBuilder.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.PlatformSupport; + +namespace Avalonia +{ + /// + /// Initializes platform-specific services for an . + /// + public sealed class AppBuilder : AppBuilderBase + { + /// + /// Initializes a new instance of the class. + /// + public AppBuilder() + : base(new StandardRuntimePlatform(), + builder => StandardRuntimePlatformServices.Register(builder.ApplicationType?.Assembly)) + { + } + } +} diff --git a/src/Avalonia.PlatformSupport/AssetLoader.cs b/src/Avalonia.PlatformSupport/AssetLoader.cs index 7220694d7b..0e33c3d4c7 100644 --- a/src/Avalonia.PlatformSupport/AssetLoader.cs +++ b/src/Avalonia.PlatformSupport/AssetLoader.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Reflection; using Avalonia.Platform; +using Avalonia.PlatformSupport.Internal; using Avalonia.Utilities; namespace Avalonia.PlatformSupport @@ -13,12 +14,16 @@ namespace Avalonia.PlatformSupport /// public class AssetLoader : IAssetLoader { - private const string AvaloniaResourceName = "!AvaloniaResources"; - private static readonly Dictionary AssemblyNameCache - = new Dictionary(); + private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver(); private AssemblyDescriptor? _defaultResmAssembly; + /// + /// Introduced for tests. + /// + internal static void SetAssemblyDescriptorResolver(IAssemblyDescriptorResolver resolver) => + s_assemblyDescriptorResolver = resolver; + /// /// Initializes a new instance of the class. /// @@ -109,17 +114,18 @@ namespace Avalonia.PlatformSupport /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset public IEnumerable GetAssets(Uri uri, Uri? baseUri) { - if (uri.IsAbsoluteUri && uri.Scheme == "resm") + if (uri.IsAbsoluteResm()) { var assembly = GetAssembly(uri); - return assembly?.Resources?.Where(x => x.Key.Contains(uri.AbsolutePath)) + return assembly?.Resources? + .Where(x => x.Key.IndexOf(uri.GetUnescapeAbsolutePath(), StringComparison.Ordinal) >= 0) .Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? Enumerable.Empty(); } - uri = EnsureAbsolute(uri, baseUri); - if (uri.Scheme == "avares") + uri = uri.EnsureAbsolute(baseUri); + if (uri.IsAvares()) { var (asm, path) = GetResAsmAndPath(uri); if (asm == null) @@ -129,33 +135,23 @@ namespace Avalonia.PlatformSupport "don't know where to look up for the resource, try specifying assembly explicitly."); } - if (asm?.AvaloniaResources == null) + if (asm.AvaloniaResources == null) return Enumerable.Empty(); - path = path.TrimEnd('/') + '/'; - return asm.AvaloniaResources.Where(r => r.Key.StartsWith(path)) + + if (path[path.Length - 1] != '/') + path += '/'; + + return asm.AvaloniaResources + .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) .Select(x => new Uri($"avares://{asm.Name}{x.Key}")); } return Enumerable.Empty(); } - - private Uri EnsureAbsolute(Uri uri, Uri? baseUri) - { - if (uri.IsAbsoluteUri) - return uri; - if(baseUri == null) - throw new ArgumentException($"Relative uri {uri} without base url"); - if (!baseUri.IsAbsoluteUri) - throw new ArgumentException($"Base uri {baseUri} is relative"); - if (baseUri.Scheme == "resm") - throw new ArgumentException( - $"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm"); - return new Uri(baseUri, uri); - } private IAssetDescriptor? GetAsset(Uri uri, Uri? baseUri) { - if (uri.IsAbsoluteUri && uri.Scheme == "resm") + if (uri.IsAbsoluteResm()) { var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly; @@ -172,9 +168,9 @@ namespace Avalonia.PlatformSupport return rv; } - uri = EnsureAbsolute(uri, baseUri); + uri = uri.EnsureAbsolute(baseUri); - if (uri.Scheme == "avares") + if (uri.IsAvares()) { var (asm, path) = GetResAsmAndPath(uri); if (asm.AvaloniaResources == null) @@ -186,209 +182,32 @@ namespace Avalonia.PlatformSupport throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri)); } - private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) + private (IAssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) { - var asm = GetAssembly(uri.Authority); - return (asm, uri.AbsolutePath); + var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority); + return (asm, uri.GetUnescapeAbsolutePath()); } - private AssemblyDescriptor? GetAssembly(Uri? uri) + private IAssemblyDescriptor? GetAssembly(Uri? uri) { if (uri != null) { if (!uri.IsAbsoluteUri) return null; - if (uri.Scheme == "avares") + if (uri.IsAvares()) return GetResAsmAndPath(uri).asm; - if (uri.Scheme == "resm") + if (uri.IsResm()) { - var qs = ParseQueryString(uri); - - if (qs.TryGetValue("assembly", out var assemblyName)) - { - return GetAssembly(assemblyName); - } + var assemblyName = uri.GetAssemblyNameFromQuery(); + if (assemblyName.Length > 0) + return s_assemblyDescriptorResolver.GetAssembly(assemblyName); } } return null; } - private AssemblyDescriptor GetAssembly(string name) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - if (!AssemblyNameCache.TryGetValue(name, out var rv)) - { - var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - var match = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == name); - if (match != null) - { - AssemblyNameCache[name] = rv = new AssemblyDescriptor(match); - } - else - { - // iOS does not support loading assemblies dynamically! -#if NET6_0_OR_GREATER - if (OperatingSystem.IsIOS()) - { - throw new InvalidOperationException( - $"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); - } -#endif - name = Uri.UnescapeDataString(name); - AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name)); - } - } - - return rv; - } - - private Dictionary ParseQueryString(Uri uri) - { - return uri.Query.TrimStart('?') - .Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries) - .Select(p => p.Split('=')) - .ToDictionary(p => p[0], p => p[1]); - } - - private interface IAssetDescriptor - { - Stream GetStream(); - Assembly Assembly { get; } - } - - private class AssemblyResourceDescriptor : IAssetDescriptor - { - private readonly Assembly _asm; - private readonly string _name; - - public AssemblyResourceDescriptor(Assembly asm, string name) - { - _asm = asm; - _name = name; - } - - public Stream GetStream() - { - var s = _asm.GetManifestResourceStream(_name); - return s ?? throw new InvalidOperationException($"Could not find manifest resource stream '{_name}',"); - } - - public Assembly Assembly => _asm; - } - - private class AvaloniaResourceDescriptor : IAssetDescriptor - { - private readonly int _offset; - private readonly int _length; - public Assembly Assembly { get; } - - public AvaloniaResourceDescriptor(Assembly asm, int offset, int length) - { - _offset = offset; - _length = length; - Assembly = asm; - } - - public Stream GetStream() - { - var s = Assembly.GetManifestResourceStream(AvaloniaResourceName) ?? - throw new InvalidOperationException($"Could not find manifest resource stream '{AvaloniaResourceName}',"); - return new SlicedStream(s, _offset, _length); - } - } - - class SlicedStream : Stream - { - private readonly Stream _baseStream; - private readonly int _from; - - public SlicedStream(Stream baseStream, int from, int length) - { - Length = length; - _baseStream = baseStream; - _from = from; - _baseStream.Position = from; - } - public override void Flush() - { - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _baseStream.Read(buffer, offset, (int)Math.Min(count, Length - Position)); - } - - public override long Seek(long offset, SeekOrigin origin) - { - if (origin == SeekOrigin.Begin) - Position = offset; - if (origin == SeekOrigin.End) - Position = _from + Length + offset; - if (origin == SeekOrigin.Current) - Position = Position + offset; - return Position; - } - - public override void SetLength(long value) => throw new NotSupportedException(); - - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - - public override bool CanRead => true; - public override bool CanSeek => _baseStream.CanRead; - public override bool CanWrite => false; - public override long Length { get; } - public override long Position - { - get => _baseStream.Position - _from; - set => _baseStream.Position = value + _from; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - _baseStream.Dispose(); - } - - public override void Close() => _baseStream.Close(); - } - - private class AssemblyDescriptor - { - public AssemblyDescriptor(Assembly assembly) - { - Assembly = assembly; - - if (assembly != null) - { - Resources = assembly.GetManifestResourceNames() - .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); - Name = assembly.GetName().Name; - using (var resources = assembly.GetManifestResourceStream(AvaloniaResourceName)) - { - if (resources != null) - { - Resources.Remove(AvaloniaResourceName); - - var indexLength = new BinaryReader(resources).ReadInt32(); - var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); - var baseOffset = indexLength + 4; - AvaloniaResources = index.ToDictionary(r => "/" + r.Path!.TrimStart('/'), r => (IAssetDescriptor) - new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); - } - } - } - } - - public Assembly Assembly { get; } - public Dictionary? Resources { get; } - public Dictionary? AvaloniaResources { get; } - public string? Name { get; } - } - public static void RegisterResUriParsers() { if (!UriParser.IsKnownScheme("avares")) diff --git a/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj b/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj index be73d87e2c..98839b7af3 100644 --- a/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj +++ b/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj @@ -6,6 +6,7 @@ + @@ -15,4 +16,9 @@ + + + + + diff --git a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs new file mode 100644 index 0000000000..64ffec8482 --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Avalonia.Utilities; + +namespace Avalonia.PlatformSupport.Internal; + +internal interface IAssemblyDescriptor +{ + Assembly Assembly { get; } + Dictionary? Resources { get; } + Dictionary? AvaloniaResources { get; } + string? Name { get; } +} + +internal class AssemblyDescriptor : IAssemblyDescriptor +{ + public AssemblyDescriptor(Assembly assembly) + { + Assembly = assembly; + + if (assembly != null) + { + Resources = assembly.GetManifestResourceNames() + .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); + Name = assembly.GetName().Name; + using (var resources = assembly.GetManifestResourceStream(Constants.AvaloniaResourceName)) + { + if (resources != null) + { + Resources.Remove(Constants.AvaloniaResourceName); + + var indexLength = new BinaryReader(resources).ReadInt32(); + var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); + var baseOffset = indexLength + 4; + AvaloniaResources = index.ToDictionary(r => GetPathRooted(r), r => (IAssetDescriptor) + new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); + } + } + } + } + + public Assembly Assembly { get; } + public Dictionary? Resources { get; } + public Dictionary? AvaloniaResources { get; } + public string? Name { get; } + private static string GetPathRooted(AvaloniaResourcesIndexEntry r) => + r.Path![0] == '/' ? r.Path : '/' + r.Path; +} diff --git a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs new file mode 100644 index 0000000000..28ae35d57d --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Avalonia.PlatformSupport.Internal; + +internal interface IAssemblyDescriptorResolver +{ + IAssemblyDescriptor GetAssembly(string name); +} + +internal class AssemblyDescriptorResolver: IAssemblyDescriptorResolver +{ + private readonly Dictionary _assemblyNameCache = new(); + + public IAssemblyDescriptor GetAssembly(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (!_assemblyNameCache.TryGetValue(name, out var rv)) + { + var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + var match = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == name); + if (match != null) + { + _assemblyNameCache[name] = rv = new AssemblyDescriptor(match); + } + else + { + // iOS does not support loading assemblies dynamically! +#if NET6_0_OR_GREATER + if (OperatingSystem.IsIOS()) + { + throw new InvalidOperationException( + $"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); + } +#endif + name = Uri.UnescapeDataString(name); + _assemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name)); + } + } + + return rv; + } +} diff --git a/src/Avalonia.PlatformSupport/Internal/AssetDescriptor.cs b/src/Avalonia.PlatformSupport/Internal/AssetDescriptor.cs new file mode 100644 index 0000000000..baae1f99e7 --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/AssetDescriptor.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Avalonia.PlatformSupport.Internal; + +internal interface IAssetDescriptor +{ + Stream GetStream(); + Assembly Assembly { get; } +} + +internal class AssemblyResourceDescriptor : IAssetDescriptor +{ + private readonly Assembly _asm; + private readonly string _name; + + public AssemblyResourceDescriptor(Assembly asm, string name) + { + _asm = asm; + _name = name; + } + + public Stream GetStream() + { + var s = _asm.GetManifestResourceStream(_name); + return s ?? throw new InvalidOperationException($"Could not find manifest resource stream '{_name}',"); + } + + public Assembly Assembly => _asm; +} + +internal class AvaloniaResourceDescriptor : IAssetDescriptor +{ + private readonly int _offset; + private readonly int _length; + public Assembly Assembly { get; } + + public AvaloniaResourceDescriptor(Assembly asm, int offset, int length) + { + _offset = offset; + _length = length; + Assembly = asm; + } + + public Stream GetStream() + { + var s = Assembly.GetManifestResourceStream(Constants.AvaloniaResourceName) ?? + throw new InvalidOperationException($"Could not find manifest resource stream '{Constants.AvaloniaResourceName}',"); + return new SlicedStream(s, _offset, _length); + } +} diff --git a/src/Avalonia.PlatformSupport/Internal/Constants.cs b/src/Avalonia.PlatformSupport/Internal/Constants.cs new file mode 100644 index 0000000000..c8a0f7b1ce --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/Constants.cs @@ -0,0 +1,6 @@ +namespace Avalonia.PlatformSupport.Internal; + +internal static class Constants +{ + public static string AvaloniaResourceName => "!AvaloniaResources"; +} diff --git a/src/Avalonia.PlatformSupport/Internal/SlicedStream.cs b/src/Avalonia.PlatformSupport/Internal/SlicedStream.cs new file mode 100644 index 0000000000..e310db964a --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/SlicedStream.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; + +namespace Avalonia.PlatformSupport.Internal; + +internal class SlicedStream : Stream +{ + private readonly Stream _baseStream; + private readonly int _from; + + public SlicedStream(Stream baseStream, int from, int length) + { + Length = length; + _baseStream = baseStream; + _from = from; + _baseStream.Position = from; + } + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _baseStream.Read(buffer, offset, (int)Math.Min(count, Length - Position)); + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + Position = offset; + if (origin == SeekOrigin.End) + Position = _from + Length + offset; + if (origin == SeekOrigin.Current) + Position = Position + offset; + return Position; + } + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override bool CanRead => true; + public override bool CanSeek => _baseStream.CanRead; + public override bool CanWrite => false; + public override long Length { get; } + public override long Position + { + get => _baseStream.Position - _from; + set => _baseStream.Position = value + _from; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + _baseStream.Dispose(); + } + + public override void Close() => _baseStream.Close(); +} diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index 4987349162..21cdef2634 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -48,6 +48,7 @@ namespace Avalonia.ReactiveUI protected override void OnDataContextChanged(EventArgs e) { + base.OnDataContextChanged(e); ViewModel = DataContext as TViewModel; } diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index a475cf5eac..9269dc70f8 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -62,7 +62,7 @@ namespace Avalonia.ReactiveUI /// for the property. /// public static readonly StyledProperty ViewContractProperty = - AvaloniaProperty.Register(nameof(ViewContract)); + AvaloniaProperty.Register(nameof(ViewContract)); /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index c4dd79f468..d26e90b2da 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -10,6 +10,7 @@ namespace Avalonia.ReactiveUI /// /// A ContentControl that animates the transition when its content is changed. /// + [Obsolete("Use TransitioningContentControl in Avalonia.Controls namespace")] public class TransitioningContentControl : ContentControl, IStyleable { /// diff --git a/src/Avalonia.Remote.Protocol/ApiCompatBaseline.txt b/src/Avalonia.Remote.Protocol/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Avalonia.Remote.Protocol/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj index 5a8301a2e9..d746c6db7e 100644 --- a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj +++ b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj @@ -7,7 +7,7 @@ Avalonia.Remote.Protocol - + \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj new file mode 100644 index 0000000000..97e58f8a64 --- /dev/null +++ b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + enable + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/src/Avalonia.SourceGenerator/IsExternalInit.cs b/src/Avalonia.SourceGenerator/IsExternalInit.cs new file mode 100644 index 0000000000..c6ddf762ad --- /dev/null +++ b/src/Avalonia.SourceGenerator/IsExternalInit.cs @@ -0,0 +1,14 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal static class IsExternalInit + { + } +} diff --git a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs new file mode 100644 index 0000000000..4fc9397e7a --- /dev/null +++ b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Avalonia.SourceGenerator +{ + [Generator(LanguageNames.CSharp)] + public class SubtypesFactoryGenerator : IIncrementalGenerator + { + private record struct MethodTarget(IMethodSymbol Method, string MethodDecl, ITypeSymbol BaseType, string Namespace); + private static readonly string s_attributeName = typeof(SubtypesFactoryAttribute).FullName; + + private static bool IsSubtypeOf(ITypeSymbol type, ITypeSymbol baseType) + { + return type.BaseType is not null && (SymbolEqualityComparer.Default.Equals(type.BaseType, baseType) || IsSubtypeOf(type.BaseType, baseType)); + } + + private static void GenerateSubTypes(SourceProductionContext context, MethodTarget methodTarget, ImmutableArray types) + { + var (method, methodDecl, baseType, @namespace) = methodTarget; + var candidateTypes = types.Where(i => IsSubtypeOf(i, baseType)).Where(i => $"{i.ContainingNamespace}.".StartsWith($"{@namespace}.")).ToArray(); + var type = method.ContainingType; + var isGeneric = type.TypeParameters.Length > 0; + var isClass = type.TypeKind == TypeKind.Class; + + var typeDecl = $"partial {(isClass ? "class" : "struct")} {type.Name}{(isGeneric ? $"<{string.Join(", ", type.TypeParameters)}>" : "")}"; + var source = $@"using System; +using System.Collections.Generic; + +namespace {method.ContainingNamespace} +{{ + {typeDecl} + {{ + {methodDecl} + {{ + var hasMatch = false; + (hasMatch, {method.Parameters[1].Name}) = {method.Parameters[0].Name} switch + {{ +{string.Join("\n", candidateTypes.Select(i => $" \"{i.Name}\" => (true, ({method.Parameters[1].Type})new {i}()),"))} + _ => (false, default({method.Parameters[1].Type})) + }}; + + return hasMatch; + }} + }} +}}"; + + context.AddSource($"{type}.{method.MetadataName}.gen.cs", source); + } + + private static MethodTarget? PopulateMethodTargets(GeneratorSyntaxContext context, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + if (context.Node is MethodDeclarationSyntax method) + { + var attributes = method.AttributeLists.SelectMany(i => i.Attributes); + var semanticModel = context.SemanticModel; + foreach (var attribute in attributes) + { + var attributeTypeInfo = semanticModel.GetTypeInfo(attribute); + if (attributeTypeInfo.Type is null || + attributeTypeInfo.Type.ToString() != s_attributeName || + attribute.ArgumentList is null) + { + continue; + } + + var arguments = attribute.ArgumentList.Arguments; + if (arguments.Count != 2) + { + continue; + } + + if (arguments[0].Expression is not TypeOfExpressionSyntax typeOfExpr || + arguments[1].Expression is not LiteralExpressionSyntax and not IdentifierNameSyntax) + { + continue; + } + + var type = semanticModel.GetTypeInfo(typeOfExpr.Type); + var ns = semanticModel.GetConstantValue(arguments[1].Expression); + var methodDeclInfo = semanticModel.GetDeclaredSymbol(method); + + if (type.Type is not ITypeSymbol baseType || + ns.HasValue is false || + ns.Value is not string nsValue || + methodDeclInfo is not IMethodSymbol methodSymbol || + methodSymbol.Parameters.Length != 2 || + methodSymbol.Parameters[1].RefKind != RefKind.Out) + { + continue; + } + + var parameters = new SeparatedSyntaxList().AddRange(method.ParameterList.Parameters.Select(i => i.WithAttributeLists(new SyntaxList()))); + var methodDecl = method + .WithAttributeLists(new SyntaxList()) + .WithParameterList(method.ParameterList.WithParameters(parameters)) + .WithBody(null) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) + .WithoutTrivia().ToString(); + + return new MethodTarget(methodSymbol, methodDecl, baseType, nsValue); + } + } + + return null; + } + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var typesProvider = context.SyntaxProvider.CreateSyntaxProvider( + static (syntaxNode, token) => + { + token.ThrowIfCancellationRequested(); + return syntaxNode is ClassDeclarationSyntax or StructDeclarationSyntax; + }, + static (syntaxContext, token) => + { + token.ThrowIfCancellationRequested(); + return syntaxContext.Node is ClassDeclarationSyntax or StructDeclarationSyntax && + syntaxContext.SemanticModel.GetDeclaredSymbol(syntaxContext.Node) is ITypeSymbol typeSymbol + ? typeSymbol : null; + }) + .SelectMany((type, token) => + { + token.ThrowIfCancellationRequested(); + return type is null ? Array.Empty() : new ITypeSymbol[] { type }; + }); + + var methodsProvider = context.SyntaxProvider.CreateSyntaxProvider( + static (syntaxNode, token) => + { + token.ThrowIfCancellationRequested(); + return syntaxNode is MethodDeclarationSyntax { AttributeLists.Count: > 0 }; + }, PopulateMethodTargets) + .SelectMany((method, token) => + { + token.ThrowIfCancellationRequested(); + return method is null ? Array.Empty() : new MethodTarget[] { method.Value }; + }); + + var generateContext = methodsProvider.Combine(typesProvider.Collect()); + + context.RegisterSourceOutput(generateContext, static (sourceContext, source) => + { + sourceContext.CancellationToken.ThrowIfCancellationRequested(); + GenerateSubTypes(sourceContext, source.Left, source.Right); + }); + } + } +} diff --git a/src/Avalonia.Styling/ApiCompatBaseline.txt b/src/Avalonia.Styling/ApiCompatBaseline.txt deleted file mode 100644 index 0eedc3e360..0000000000 --- a/src/Avalonia.Styling/ApiCompatBaseline.txt +++ /dev/null @@ -1,4 +0,0 @@ -Compat issues with assembly Avalonia.Styling: -InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Styling.IStyleInstance.IsActive' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Styling.IStyleInstance.IsActive.get()' is present in the implementation but not in the contract. -Total Issues: 2 diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj deleted file mode 100644 index 139ba1bd2e..0000000000 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net6.0;netstandard2.0 - Avalonia.Styling - Avalonia - - - - - - - - diff --git a/src/Avalonia.Styling/Controls/ChildNameScope.cs b/src/Avalonia.Styling/Controls/ChildNameScope.cs deleted file mode 100644 index 58114a57fd..0000000000 --- a/src/Avalonia.Styling/Controls/ChildNameScope.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Threading.Tasks; -using Avalonia.Utilities; - -namespace Avalonia.Controls -{ - public class ChildNameScope : INameScope - { - private readonly INameScope _parentScope; - private readonly NameScope _inner = new NameScope(); - - public ChildNameScope(INameScope parentScope) - { - _parentScope = parentScope; - } - - public void Register(string name, object element) => _inner.Register(name, element); - - public SynchronousCompletionAsyncResult FindAsync(string name) - { - var found = Find(name); - if (found != null) - return new SynchronousCompletionAsyncResult(found); - // Not found and both current and parent scope are in completed state - if(IsCompleted) - return new SynchronousCompletionAsyncResult(null); - return DoFindAsync(name); - } - - public SynchronousCompletionAsyncResult DoFindAsync(string name) - { - var src = new SynchronousCompletionAsyncResultSource(); - - void ParentSearch() - { - var parentSearch = _parentScope.FindAsync(name); - if (parentSearch.IsCompleted) - src.SetResult(parentSearch.GetResult()); - else - parentSearch.OnCompleted(() => src.SetResult(parentSearch.GetResult())); - } - if (!_inner.IsCompleted) - { - // Guaranteed to be incomplete at this point - var innerSearch = _inner.FindAsync(name); - innerSearch.OnCompleted(() => - { - var value = innerSearch.GetResult(); - if (value != null) - src.SetResult(value); - else ParentSearch(); - }); - } - else - ParentSearch(); - - return src.AsyncResult; - } - - public object? Find(string name) - { - var found = _inner.Find(name); - if (found != null) - return found; - if (_inner.IsCompleted) - return _parentScope.Find(name); - return null; - } - - public void Complete() => _inner.Complete(); - - public bool IsCompleted => _inner.IsCompleted && _parentScope.IsCompleted; - } -} diff --git a/src/Avalonia.Styling/Controls/INameScope.cs b/src/Avalonia.Styling/Controls/INameScope.cs deleted file mode 100644 index 1ca7db2f37..0000000000 --- a/src/Avalonia.Styling/Controls/INameScope.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Threading.Tasks; -using Avalonia.Utilities; - -namespace Avalonia.Controls -{ - /// - /// Defines a name scope. - /// - public interface INameScope - { - /// - /// Registers an element in the name scope. - /// - /// The element name. - /// The element. - void Register(string name, object element); - - /// - /// Finds a named element in the name scope, waits for the scope to be completely populated before returning null - /// Returned task is configured to run any continuations synchronously. - /// - /// The name. - /// The element, or null if the name was not found. - SynchronousCompletionAsyncResult FindAsync(string name); - - /// - /// Finds a named element in the name scope, returns immediately, doesn't traverse the name scope stack - /// - /// The name. - /// The element, or null if the name was not found. - object? Find(string name); - - /// - /// Marks the name scope as completed, no further registrations will be allowed - /// - void Complete(); - - /// - /// Returns whether further registrations are allowed on the scope - /// - bool IsCompleted { get; } - - - } -} diff --git a/src/Avalonia.Styling/Controls/IPseudoClasses.cs b/src/Avalonia.Styling/Controls/IPseudoClasses.cs deleted file mode 100644 index 27290716d0..0000000000 --- a/src/Avalonia.Styling/Controls/IPseudoClasses.cs +++ /dev/null @@ -1,21 +0,0 @@ - -namespace Avalonia.Controls -{ - /// - /// Exposes an interface for setting pseudoclasses on a collection. - /// - public interface IPseudoClasses - { - /// - /// Adds a pseudoclass to the collection. - /// - /// The pseudoclass name. - void Add(string name); - - /// - /// Removes a pseudoclass from the collection. - /// - /// The pseudoclass name. - bool Remove(string name); - } -} diff --git a/src/Avalonia.Styling/Controls/IResourceHost.cs b/src/Avalonia.Styling/Controls/IResourceHost.cs deleted file mode 100644 index ea34a8b39a..0000000000 --- a/src/Avalonia.Styling/Controls/IResourceHost.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; - -#nullable enable - -namespace Avalonia.Controls -{ - /// - /// Represents an element which hosts resources. - /// - /// - /// This interface is implemented by and `Application`. - /// - public interface IResourceHost : IResourceNode - { - /// - /// Raised when the resources change on the element or an ancestor of the element. - /// - event EventHandler? ResourcesChanged; - - /// - /// Notifies the resource host that one or more of its hosted resources has changed. - /// - /// The event args. - /// - /// This method will be called automatically by the framework, you should not need to call - /// this method yourself. It is called when the resources hosted by this element have - /// changed, and is usually called by a resource dictionary or style hosted by the element - /// in response to a resource being added or removed. - /// - void NotifyHostedResourcesChanged(ResourcesChangedEventArgs e); - } -} diff --git a/src/Avalonia.Styling/Controls/IResourceNode.cs b/src/Avalonia.Styling/Controls/IResourceNode.cs deleted file mode 100644 index 73bfeaf161..0000000000 --- a/src/Avalonia.Styling/Controls/IResourceNode.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -#nullable enable - -namespace Avalonia.Controls -{ - /// - /// Represents an object that can be queried for resources. - /// - /// - /// The interface represents a common interface for both controls that host resources - /// () and resource providers such as - /// (see ). - /// - public interface IResourceNode - { - /// - /// Gets a value indicating whether the object has resources. - /// - bool HasResources { get; } - - /// - /// Tries to find a resource within the object. - /// - /// The resource key. - /// - /// When this method returns, contains the value associated with the specified key, - /// if the key is found; otherwise, null. - /// - /// - /// True if the resource if found, otherwise false. - /// - bool TryGetResource(object key, out object? value); - } -} diff --git a/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs b/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs deleted file mode 100644 index dbf8c68892..0000000000 --- a/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs +++ /dev/null @@ -1,21 +0,0 @@ -#nullable enable - -namespace Avalonia.Controls -{ - /// - /// Defines an interface through which a 's inheritance parent can be set. - /// - /// - /// You should not usually need to use this interface - it is for advanced scenarios only. - /// Additionally, also sets the inheritance parent; this - /// interface is only needed where the logical and inheritance parents differ. - /// - public interface ISetInheritanceParent - { - /// - /// Sets the control's inheritance parent. - /// - /// The parent. - void SetParent(IAvaloniaObject? parent); - } -} diff --git a/src/Avalonia.Styling/Controls/ISetLogicalParent.cs b/src/Avalonia.Styling/Controls/ISetLogicalParent.cs deleted file mode 100644 index 85bda05961..0000000000 --- a/src/Avalonia.Styling/Controls/ISetLogicalParent.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Avalonia.LogicalTree; - -#nullable enable - -namespace Avalonia.Controls -{ - /// - /// Defines an interface through which a 's logical parent can be set. - /// - /// - /// You should not usually need to use this interface - it is for advanced scenarios only. - /// - public interface ISetLogicalParent - { - /// - /// Sets the control's parent. - /// - /// The parent. - void SetParent(ILogical? parent); - } -} diff --git a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs deleted file mode 100644 index 0060767565..0000000000 --- a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; - -#nullable enable - -namespace Avalonia.Controls.Metadata -{ - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public sealed class PseudoClassesAttribute : Attribute - { - public PseudoClassesAttribute(params string[] pseudoClasses) - { - PseudoClasses = pseudoClasses; - } - - public IReadOnlyList PseudoClasses { get; } - } -} diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs deleted file mode 100644 index 77f98f85c4..0000000000 --- a/src/Avalonia.Styling/Controls/NameScope.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Avalonia.LogicalTree; -using Avalonia.Utilities; - -namespace Avalonia.Controls -{ - /// - /// Implements a name scope. - /// - public class NameScope : INameScope - { - /// - /// Defines the NameScope attached property. - /// - public static readonly AttachedProperty NameScopeProperty = - AvaloniaProperty.RegisterAttached("NameScope"); - - /// - public bool IsCompleted { get; private set; } - - private readonly Dictionary _inner = new Dictionary(); - - private readonly Dictionary> _pendingSearches = - new Dictionary>(); - - /// - /// Gets the value of the attached on a styled element. - /// - /// The styled element. - /// The value of the NameScope attached property. - public static INameScope GetNameScope(StyledElement styled) - { - _ = styled ?? throw new ArgumentNullException(nameof(styled)); - - return styled.GetValue(NameScopeProperty); - } - - /// - /// Sets the value of the attached on a styled element. - /// - /// The styled element. - /// The value to set. - public static void SetNameScope(StyledElement styled, INameScope value) - { - _ = styled ?? throw new ArgumentNullException(nameof(styled)); - - styled.SetValue(NameScopeProperty, value); - } - - /// - public void Register(string name, object element) - { - if (IsCompleted) - throw new InvalidOperationException("NameScope is completed, no further registrations are allowed"); - - _ = name ?? throw new ArgumentNullException(nameof(name)); - _ = element ?? throw new ArgumentNullException(nameof(element)); - - if (_inner.TryGetValue(name, out var existing)) - { - if (existing != element) - { - throw new ArgumentException($"Control with the name '{name}' already registered."); - } - } - else - { - _inner.Add(name, element); - if (_pendingSearches.TryGetValue(name, out var tcs)) - { - _pendingSearches.Remove(name); - tcs.SetResult(element); - } - } - } - - public SynchronousCompletionAsyncResult FindAsync(string name) - { - var found = Find(name); - if (found != null) - return new SynchronousCompletionAsyncResult(found); - if (IsCompleted) - return new SynchronousCompletionAsyncResult(null); - if (!_pendingSearches.TryGetValue(name, out var tcs)) - // We are intentionally running continuations synchronously here - _pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource(); - - return tcs.AsyncResult; - } - - /// - public object? Find(string name) - { - _ = name ?? throw new ArgumentNullException(nameof(name)); - - _inner.TryGetValue(name, out var result); - return result; - } - - public void Complete() - { - IsCompleted = true; - foreach (var kp in _pendingSearches) - kp.Value.TrySetResult(null); - _pendingSearches.Clear(); - } - - - } -} diff --git a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs deleted file mode 100644 index 513b3f2424..0000000000 --- a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using Avalonia.Data.Converters; -using Avalonia.LogicalTree; -using Avalonia.Reactive; - -#nullable enable - -namespace Avalonia.Controls -{ - public static class ResourceNodeExtensions - { - /// - /// Finds the specified resource by searching up the logical tree and then global styles. - /// - /// The control. - /// The resource key. - /// The resource, or if not found. - public static object? FindResource(this IResourceHost control, object key) - { - control = control ?? throw new ArgumentNullException(nameof(control)); - key = key ?? throw new ArgumentNullException(nameof(key)); - - if (control.TryFindResource(key, out var value)) - { - return value; - } - - return AvaloniaProperty.UnsetValue; - } - - /// - /// Tries to the specified resource by searching up the logical tree and then global styles. - /// - /// The control. - /// The resource key. - /// On return, contains the resource if found, otherwise null. - /// True if the resource was found; otherwise false. - public static bool TryFindResource(this IResourceHost control, object key, out object? value) - { - control = control ?? throw new ArgumentNullException(nameof(control)); - key = key ?? throw new ArgumentNullException(nameof(key)); - - IResourceHost? current = control; - - while (current != null) - { - if (current is IResourceHost host) - { - if (host.TryGetResource(key, out value)) - { - return true; - } - } - - current = (current as IStyledElement)?.StylingParent as IResourceHost; - } - - value = null; - return false; - } - - public static IObservable GetResourceObservable( - this IResourceHost control, - object key, - Func? converter = null) - { - control = control ?? throw new ArgumentNullException(nameof(control)); - key = key ?? throw new ArgumentNullException(nameof(key)); - - return new ResourceObservable(control, key, converter); - } - - public static IObservable GetResourceObservable( - this IResourceProvider resourceProvider, - object key, - Func? converter = null) - { - resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); - key = key ?? throw new ArgumentNullException(nameof(key)); - - return new FloatingResourceObservable(resourceProvider, key, converter); - } - - private class ResourceObservable : LightweightObservableBase - { - private readonly IResourceHost _target; - private readonly object _key; - private readonly Func? _converter; - - public ResourceObservable(IResourceHost target, object key, Func? converter) - { - _target = target; - _key = key; - _converter = converter; - } - - protected override void Initialize() - { - _target.ResourcesChanged += ResourcesChanged; - } - - protected override void Deinitialize() - { - _target.ResourcesChanged -= ResourcesChanged; - } - - protected override void Subscribed(IObserver observer, bool first) - { - observer.OnNext(Convert(_target.FindResource(_key))); - } - - private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) - { - PublishNext(Convert(_target.FindResource(_key))); - } - - private object? Convert(object? value) => _converter?.Invoke(value) ?? value; - } - - private class FloatingResourceObservable : LightweightObservableBase - { - private readonly IResourceProvider _target; - private readonly object _key; - private readonly Func? _converter; - private IResourceHost? _owner; - - public FloatingResourceObservable(IResourceProvider target, object key, Func? converter) - { - _target = target; - _key = key; - _converter = converter; - } - - protected override void Initialize() - { - _target.OwnerChanged += OwnerChanged; - _owner = _target.Owner; - } - - protected override void Deinitialize() - { - _target.OwnerChanged -= OwnerChanged; - _owner = null; - } - - protected override void Subscribed(IObserver observer, bool first) - { - if (_target.Owner is object) - { - observer.OnNext(Convert(_target.Owner.FindResource(_key))); - } - } - - private void PublishNext() - { - if (_target.Owner is object) - { - PublishNext(Convert(_target.Owner.FindResource(_key))); - } - } - - private void OwnerChanged(object? sender, EventArgs e) - { - if (_owner is object) - { - _owner.ResourcesChanged -= ResourcesChanged; - } - - _owner = _target.Owner; - - if (_owner is object) - { - _owner.ResourcesChanged += ResourcesChanged; - } - - PublishNext(); - } - - private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) - { - PublishNext(); - } - - private object? Convert(object? value) => _converter?.Invoke(value) ?? value; - } - } -} diff --git a/src/Avalonia.Styling/IDataContextProvider.cs b/src/Avalonia.Styling/IDataContextProvider.cs deleted file mode 100644 index 1172adcaa4..0000000000 --- a/src/Avalonia.Styling/IDataContextProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable enable - -namespace Avalonia -{ - /// - /// Defines an element with a data context that can be used for binding. - /// - public interface IDataContextProvider : IAvaloniaObject - { - /// - /// Gets or sets the element's data context. - /// - object? DataContext { get; set; } - } -} diff --git a/src/Avalonia.Styling/IStyledElement.cs b/src/Avalonia.Styling/IStyledElement.cs deleted file mode 100644 index a068d4a5bf..0000000000 --- a/src/Avalonia.Styling/IStyledElement.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.ComponentModel; -using Avalonia.Controls; -using Avalonia.LogicalTree; -using Avalonia.Styling; - -#nullable enable - -namespace Avalonia -{ - public interface IStyledElement : - IStyleable, - IStyleHost, - ILogical, - IResourceHost, - IDataContextProvider, - ISupportInitialize - { - /// - /// Occurs when the control has finished initialization. - /// - event EventHandler? Initialized; - - /// - /// Gets a value that indicates whether the element has finished initialization. - /// - bool IsInitialized { get; } - - /// - /// Gets or sets the control's styling classes. - /// - new Classes Classes { get; set; } - - /// - /// Gets the control's logical parent. - /// - IStyledElement? Parent { get; } - } -} diff --git a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs deleted file mode 100644 index de41f5292c..0000000000 --- a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -#nullable enable -using System; - -namespace Avalonia.LogicalTree -{ - /// - /// Event args for event. - /// - public class ChildIndexChangedEventArgs : EventArgs - { - public ChildIndexChangedEventArgs() - { - } - - public ChildIndexChangedEventArgs(ILogical child) - { - Child = child; - } - - /// - /// Logical child which index was changed. - /// If null, all children should be reset. - /// - public ILogical? Child { get; } - } -} diff --git a/src/Avalonia.Styling/LogicalTree/ILogical.cs b/src/Avalonia.Styling/LogicalTree/ILogical.cs deleted file mode 100644 index caff3d8150..0000000000 --- a/src/Avalonia.Styling/LogicalTree/ILogical.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using Avalonia.Collections; -using Avalonia.Controls; - -namespace Avalonia.LogicalTree -{ - /// - /// Represents a node in the logical tree. - /// - public interface ILogical - { - /// - /// Raised when the control is attached to a rooted logical tree. - /// - event EventHandler? AttachedToLogicalTree; - - /// - /// Raised when the control is detached from a rooted logical tree. - /// - event EventHandler? DetachedFromLogicalTree; - - /// - /// Gets a value indicating whether the element is attached to a rooted logical tree. - /// - bool IsAttachedToLogicalTree { get; } - - /// - /// Gets the logical parent. - /// - ILogical? LogicalParent { get; } - - /// - /// Gets the logical children. - /// - IAvaloniaReadOnlyList LogicalChildren { get; } - - /// - /// Notifies the control that it is being attached to a rooted logical tree. - /// - /// The event args. - /// - /// This method will be called automatically by the framework, you should not need to call - /// this method yourself. - /// - void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e); - - /// - /// Notifies the control that it is being detached from a rooted logical tree. - /// - /// The event args. - /// - /// This method will be called automatically by the framework, you should not need to call - /// this method yourself. - /// - void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e); - - /// - /// Notifies the control that a change has been made to resources that apply to it. - /// - /// The event args. - /// - /// This method will be called automatically by the framework, you should not need to call - /// this method yourself. - /// - void NotifyResourcesChanged(ResourcesChangedEventArgs e); - } -} diff --git a/src/Avalonia.Styling/LogicalTree/ILogicalRoot.cs b/src/Avalonia.Styling/LogicalTree/ILogicalRoot.cs deleted file mode 100644 index 4a61544a6f..0000000000 --- a/src/Avalonia.Styling/LogicalTree/ILogicalRoot.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Avalonia.LogicalTree -{ - /// - /// Represents a root of a logical tree. - /// - public interface ILogicalRoot : ILogical - { - } -} diff --git a/src/Avalonia.Styling/Properties/AssemblyInfo.cs b/src/Avalonia.Styling/Properties/AssemblyInfo.cs deleted file mode 100644 index ab034740ed..0000000000 --- a/src/Avalonia.Styling/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Runtime.CompilerServices; -using Avalonia.Metadata; - -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.LogicalTree")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Styling")] - -[assembly: InternalsVisibleTo("Avalonia.Styling.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs deleted file mode 100644 index 5f498623e1..0000000000 --- a/src/Avalonia.Styling/StyledElement.cs +++ /dev/null @@ -1,888 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; -using Avalonia.Animation; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Data; -using Avalonia.Diagnostics; -using Avalonia.Logging; -using Avalonia.LogicalTree; -using Avalonia.Styling; - -#nullable enable - -namespace Avalonia -{ - /// - /// Extends an with the following features: - /// - /// - An inherited . - /// - Implements to allow styling to work on the styled element. - /// - Implements to form part of a logical tree. - /// - A collection of class strings for custom styling. - /// - public class StyledElement : Animatable, IDataContextProvider, IStyledElement, ISetLogicalParent, ISetInheritanceParent - { - /// - /// Defines the property. - /// - public static readonly StyledProperty DataContextProperty = - AvaloniaProperty.Register( - nameof(DataContext), - inherits: true, - notifying: DataContextNotifying); - - /// - /// Defines the property. - /// - public static readonly DirectProperty NameProperty = - AvaloniaProperty.RegisterDirect(nameof(Name), o => o.Name, (o, v) => o.Name = v); - - /// - /// Defines the property. - /// - public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); - - /// - /// Defines the property. - /// - public static readonly DirectProperty TemplatedParentProperty = - AvaloniaProperty.RegisterDirect( - nameof(TemplatedParent), - o => o.TemplatedParent, - (o ,v) => o.TemplatedParent = v); - - private int _initCount; - private string? _name; - private readonly Classes _classes = new Classes(); - private ILogicalRoot? _logicalRoot; - private IAvaloniaList? _logicalChildren; - private IResourceDictionary? _resources; - private Styles? _styles; - private bool _styled; - private List? _appliedStyles; - private ITemplatedControl? _templatedParent; - private bool _dataContextUpdating; - - /// - /// Initializes static members of the class. - /// - static StyledElement() - { - DataContextProperty.Changed.AddClassHandler((x,e) => x.OnDataContextChangedCore(e)); - } - - /// - /// Initializes a new instance of the class. - /// - public StyledElement() - { - _logicalRoot = this as ILogicalRoot; - } - - /// - /// Raised when the styled element is attached to a rooted logical tree. - /// - public event EventHandler? AttachedToLogicalTree; - - /// - /// Raised when the styled element is detached from a rooted logical tree. - /// - public event EventHandler? DetachedFromLogicalTree; - - /// - /// Occurs when the property changes. - /// - /// - /// This event will be raised when the property has changed and - /// all subscribers to that change have been notified. - /// - public event EventHandler? DataContextChanged; - - /// - /// Occurs when the styled element has finished initialization. - /// - /// - /// The Initialized event indicates that all property values on the styled element have been set. - /// When loading the styled element from markup, it occurs when - /// is called *and* the styled element - /// is attached to a rooted logical tree. When the styled element is created by code and - /// is not used, it is called when the styled element is attached - /// to the visual tree. - /// - public event EventHandler? Initialized; - - /// - /// Occurs when a resource in this styled element or a parent styled element has changed. - /// - public event EventHandler? ResourcesChanged; - - /// - /// Gets or sets the name of the styled element. - /// - /// - /// An element's name is used to uniquely identify an element within the element's name - /// scope. Once the element is added to a logical tree, its name cannot be changed. - /// - public string? Name - { - get => _name; - - set - { - if (_styled) - { - throw new InvalidOperationException("Cannot set Name : styled element already styled."); - } - - _name = value; - } - } - - /// - /// Gets or sets the styled element's classes. - /// - /// - /// - /// Classes can be used to apply user-defined styling to styled elements, or to allow styled elements - /// that share a common purpose to be easily selected. - /// - /// - /// Even though this property can be set, the setter is only intended for use in object - /// initializers. Assigning to this property does not change the underlying collection, - /// it simply clears the existing collection and adds the contents of the assigned - /// collection. - /// - /// - public Classes Classes - { - get - { - return _classes; - } - - set - { - if (_classes != value) - { - _classes.Replace(value); - } - } - } - - /// - /// Gets or sets the control's data context. - /// - /// - /// The data context is an inherited property that specifies the default object that will - /// be used for data binding. - /// - public object? DataContext - { - get { return GetValue(DataContextProperty); } - set { SetValue(DataContextProperty, value); } - } - - /// - /// Gets a value that indicates whether the element has finished initialization. - /// - /// - /// For more information about when IsInitialized is set, see the - /// event. - /// - public bool IsInitialized { get; private set; } - - /// - /// Gets the styles for the styled element. - /// - /// - /// Styles for the entire application are added to the Application.Styles collection, but - /// each styled element may in addition define its own styles which are applied to the styled element - /// itself and its children. - /// - public Styles Styles => _styles ??= new Styles(this); - - /// - /// Gets or sets the styled element's resource dictionary. - /// - public IResourceDictionary Resources - { - get => _resources ??= new ResourceDictionary(this); - set - { - value = value ?? throw new ArgumentNullException(nameof(value)); - _resources?.RemoveOwner(this); - _resources = value; - _resources.AddOwner(this); - } - } - - /// - /// Gets the styled element whose lookless template this styled element is part of. - /// - public ITemplatedControl? TemplatedParent - { - get => _templatedParent; - internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value); - } - - /// - /// Gets the styled element's logical children. - /// - protected IAvaloniaList LogicalChildren - { - get - { - if (_logicalChildren == null) - { - var list = new AvaloniaList - { - ResetBehavior = ResetBehavior.Remove, - Validate = logical => ValidateLogicalChild(logical) - }; - list.CollectionChanged += LogicalChildrenCollectionChanged; - _logicalChildren = list; - } - - return _logicalChildren; - } - } - - /// - /// Gets the collection in a form that allows adding and removing - /// pseudoclasses. - /// - protected IPseudoClasses PseudoClasses => Classes; - - /// - /// Gets a value indicating whether the element is attached to a rooted logical tree. - /// - bool ILogical.IsAttachedToLogicalTree => _logicalRoot != null; - - /// - /// Gets the styled element's logical parent. - /// - public IStyledElement? Parent { get; private set; } - - /// - /// Gets the styled element's logical parent. - /// - ILogical? ILogical.LogicalParent => Parent; - - /// - /// Gets the styled element's logical children. - /// - IAvaloniaReadOnlyList ILogical.LogicalChildren => LogicalChildren; - - /// - bool IResourceNode.HasResources => (_resources?.HasResources ?? false) || - (((IResourceNode?)_styles)?.HasResources ?? false); - - /// - IAvaloniaReadOnlyList IStyleable.Classes => Classes; - - /// - /// Gets the type by which the styled element is styled. - /// - /// - /// Usually controls are styled by their own type, but there are instances where you want - /// a styled element to be styled by its base type, e.g. creating SpecialButton that - /// derives from Button and adds extra functionality but is still styled as a regular - /// Button. - /// - Type IStyleable.StyleKey => GetType(); - - /// - bool IStyleHost.IsStylesInitialized => _styles != null; - - /// - IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent; - - /// - public virtual void BeginInit() - { - ++_initCount; - } - - /// - public virtual void EndInit() - { - if (_initCount == 0) - { - throw new InvalidOperationException("BeginInit was not called."); - } - - if (--_initCount == 0 && _logicalRoot != null) - { - ApplyStyling(); - InitializeIfNeeded(); - } - } - - /// - /// Applies styling to the control if the control is initialized and styling is not - /// already applied. - /// - /// - /// A value indicating whether styling is now applied to the control. - /// - protected bool ApplyStyling() - { - if (_initCount == 0 && !_styled) - { - try - { - BeginBatchUpdate(); - AvaloniaLocator.Current.GetService()?.ApplyStyles(this); - } - finally - { - EndBatchUpdate(); - } - - _styled = true; - } - - return _styled; - } - - /// - /// Detaches all styles from the element and queues a restyle. - /// - protected virtual void InvalidateStyles() => DetachStyles(); - - protected void InitializeIfNeeded() - { - if (_initCount == 0 && !IsInitialized) - { - IsInitialized = true; - OnInitialized(); - Initialized?.Invoke(this, EventArgs.Empty); - } - } - - internal StyleDiagnostics GetStyleDiagnosticsInternal() - { - IReadOnlyList? appliedStyles = _appliedStyles; - - if (appliedStyles is null) - { - appliedStyles = Array.Empty(); - } - - return new StyleDiagnostics(appliedStyles); - } - - /// - void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - OnAttachedToLogicalTreeCore(e); - } - - /// - void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - OnDetachedFromLogicalTreeCore(e); - } - - /// - void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); - - /// - void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); - - /// - bool IResourceNode.TryGetResource(object key, out object? value) - { - value = null; - return (_resources?.TryGetResource(key, out value) ?? false) || - (_styles?.TryGetResource(key, out value) ?? false); - } - - /// - /// Sets the styled element's logical parent. - /// - /// The parent. - void ISetLogicalParent.SetParent(ILogical? parent) - { - var old = Parent; - - if (parent != old) - { - if (old != null && parent != null) - { - throw new InvalidOperationException("The Control already has a parent."); - } - - if (InheritanceParent == null || parent == null) - { - InheritanceParent = parent as AvaloniaObject; - } - - Parent = (IStyledElement?)parent; - - if (_logicalRoot != null) - { - var e = new LogicalTreeAttachmentEventArgs(_logicalRoot, this, old!); - OnDetachedFromLogicalTreeCore(e); - } - - var newRoot = FindLogicalRoot(this); - - if (newRoot is object) - { - var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent!); - OnAttachedToLogicalTreeCore(e); - } - else if (parent is null) - { - // If we were attached to the logical tree, we piggyback on the tree traversal - // there to raise resources changed notifications. If we're being removed from - // the logical tree, then traverse the tree raising notifications now. - // - // We don't raise resources changed notifications if we're being attached to a - // non-rooted control beacuse it's unlikely that dynamic resources need to be - // correct until the control is added to the tree, and it causes a *lot* of - // notifications. - NotifyResourcesChanged(); - } - -#nullable disable - RaisePropertyChanged( - ParentProperty, - new Optional(old), - new BindingValue(Parent), - BindingPriority.LocalValue); -#nullable enable - } - } - - /// - /// Sets the styled element's inheritance parent. - /// - /// The parent. - void ISetInheritanceParent.SetParent(IAvaloniaObject? parent) - { - InheritanceParent = parent; - } - - void IStyleable.StyleApplied(IStyleInstance instance) - { - instance = instance ?? throw new ArgumentNullException(nameof(instance)); - - _appliedStyles ??= new List(); - _appliedStyles.Add(instance); - } - - void IStyleable.DetachStyles() => DetachStyles(); - - void IStyleable.DetachStyles(IReadOnlyList styles) => DetachStyles(styles); - - void IStyleable.InvalidateStyles() => InvalidateStyles(); - - void IStyleHost.StylesAdded(IReadOnlyList styles) - { - InvalidateStylesOnThisAndDescendents(); - } - - void IStyleHost.StylesRemoved(IReadOnlyList styles) - { - var allStyles = RecurseStyles(styles); - DetachStylesFromThisAndDescendents(allStyles); - } - - protected virtual void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - SetLogicalParent(e.NewItems!); - break; - - case NotifyCollectionChangedAction.Remove: - ClearLogicalParent(e.OldItems!); - break; - - case NotifyCollectionChangedAction.Replace: - ClearLogicalParent(e.OldItems!); - SetLogicalParent(e.NewItems!); - break; - - case NotifyCollectionChangedAction.Reset: - throw new NotSupportedException("Reset should not be signaled on LogicalChildren collection"); - } - } - - /// - /// Notifies child controls that a change has been made to resources that apply to them. - /// - /// The event args. - protected virtual void NotifyChildResourcesChanged(ResourcesChangedEventArgs e) - { - if (_logicalChildren is object) - { - var count = _logicalChildren.Count; - - if (count > 0) - { - e ??= ResourcesChangedEventArgs.Empty; - - for (var i = 0; i < count; ++i) - { - _logicalChildren[i].NotifyResourcesChanged(e); - } - } - } - } - - /// - /// Called when the styled element is added to a rooted logical tree. - /// - /// The event args. - protected virtual void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - } - - /// - /// Called when the styled element is removed from a rooted logical tree. - /// - /// The event args. - protected virtual void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - } - - /// - /// Called when the property changes. - /// - /// The event args. - protected virtual void OnDataContextChanged(EventArgs e) - { - DataContextChanged?.Invoke(this, EventArgs.Empty); - } - - /// - /// Called when the begins updating. - /// - protected virtual void OnDataContextBeginUpdate() - { - } - - /// - /// Called when the finishes updating. - /// - protected virtual void OnDataContextEndUpdate() - { - } - - /// - /// Called when the control finishes initialization. - /// - protected virtual void OnInitialized() - { - } - - private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) - { - if (o is StyledElement element) - { - DataContextNotifying(element, updateStarted); - } - } - - private static void DataContextNotifying(StyledElement element, bool updateStarted) - { - if (updateStarted) - { - if (!element._dataContextUpdating) - { - element._dataContextUpdating = true; - element.OnDataContextBeginUpdate(); - - var logicalChildren = element.LogicalChildren; - var logicalChildrenCount = logicalChildren.Count; - - for (var i = 0; i < logicalChildrenCount; i++) - { - if (element.LogicalChildren[i] is StyledElement s && - s.InheritanceParent == element && - !s.IsSet(DataContextProperty)) - { - DataContextNotifying(s, updateStarted); - } - } - } - } - else - { - if (element._dataContextUpdating) - { - element.OnDataContextEndUpdate(); - element._dataContextUpdating = false; - } - } - } - - private static ILogicalRoot? FindLogicalRoot(IStyleHost? e) - { - while (e != null) - { - if (e is ILogicalRoot root) - { - return root; - } - - e = e.StylingParent; - } - - return null; - } - - private static void ValidateLogicalChild(ILogical c) - { - if (c == null) - { - throw new ArgumentException("Cannot add null to LogicalChildren."); - } - } - - private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e) - { - if (this.GetLogicalParent() == null && !(this is ILogicalRoot)) - { - throw new InvalidOperationException( - $"AttachedToLogicalTreeCore called for '{GetType().Name}' but control has no logical parent."); - } - - // This method can be called when a control is already attached to the logical tree - // in the following scenario: - // - ListBox gets assigned Items containing ListBoxItem - // - ListBox makes ListBoxItem a logical child - // - ListBox template gets applied; making its Panel get attached to logical tree - // - That AttachedToLogicalTree signal travels down to the ListBoxItem - if (_logicalRoot == null) - { - _logicalRoot = e.Root; - - ApplyStyling(); - NotifyResourcesChanged(propagate: false); - - OnAttachedToLogicalTree(e); - AttachedToLogicalTree?.Invoke(this, e); - } - - var logicalChildren = LogicalChildren; - var logicalChildrenCount = logicalChildren.Count; - - for (var i = 0; i < logicalChildrenCount; i++) - { - if (logicalChildren[i] is StyledElement child) - { - child.OnAttachedToLogicalTreeCore(e); - } - } - } - - private void OnDetachedFromLogicalTreeCore(LogicalTreeAttachmentEventArgs e) - { - if (_logicalRoot != null) - { - _logicalRoot = null; - DetachStyles(); - OnDetachedFromLogicalTree(e); - DetachedFromLogicalTree?.Invoke(this, e); - - var logicalChildren = LogicalChildren; - var logicalChildrenCount = logicalChildren.Count; - - for (var i = 0; i < logicalChildrenCount; i++) - { - if (logicalChildren[i] is StyledElement child) - { - child.OnDetachedFromLogicalTreeCore(e); - } - } - -#if DEBUG - if (((INotifyCollectionChangedDebug)Classes).GetCollectionChangedSubscribers()?.Length > 0) - { - Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log( - this, - "{Type} detached from logical tree but still has class listeners", - GetType()); - } -#endif - } - } - - private void OnDataContextChangedCore(AvaloniaPropertyChangedEventArgs e) - { - OnDataContextChanged(EventArgs.Empty); - } - - private void SetLogicalParent(IList children) - { - var count = children.Count; - - for (var i = 0; i < count; i++) - { - var logical = (ILogical) children[i]!; - - if (logical.LogicalParent is null) - { - ((ISetLogicalParent)logical).SetParent(this); - } - } - } - - private void ClearLogicalParent(IList children) - { - var count = children.Count; - - for (var i = 0; i < count; i++) - { - var logical = (ILogical) children[i]!; - - if (logical.LogicalParent == this) - { - ((ISetLogicalParent)logical).SetParent(null); - } - } - } - - private void DetachStyles() - { - if (_appliedStyles is object) - { - BeginBatchUpdate(); - - try - { - foreach (var i in _appliedStyles) - { - i.Dispose(); - } - - _appliedStyles.Clear(); - } - finally - { - EndBatchUpdate(); - } - } - - _styled = false; - } - - private void DetachStyles(IReadOnlyList styles) - { - styles = styles ?? throw new ArgumentNullException(nameof(styles)); - - if (_appliedStyles is null) - { - return; - } - - var count = styles.Count; - - for (var i = 0; i < count; ++i) - { - for (var j = _appliedStyles.Count - 1; j >= 0; --j) - { - var applied = _appliedStyles[j]; - - if (applied.Source == styles[i]) - { - applied.Dispose(); - _appliedStyles.RemoveAt(j); - } - } - } - } - - private void InvalidateStylesOnThisAndDescendents() - { - InvalidateStyles(); - - if (_logicalChildren is object) - { - var childCount = _logicalChildren.Count; - - for (var i = 0; i < childCount; ++i) - { - (_logicalChildren[i] as StyledElement)?.InvalidateStylesOnThisAndDescendents(); - } - } - } - - private void DetachStylesFromThisAndDescendents(IReadOnlyList styles) - { - DetachStyles(styles); - - if (_logicalChildren is object) - { - var childCount = _logicalChildren.Count; - - for (var i = 0; i < childCount; ++i) - { - (_logicalChildren[i] as StyledElement)?.DetachStylesFromThisAndDescendents(styles); - } - } - } - - private void NotifyResourcesChanged( - ResourcesChangedEventArgs? e = null, - bool propagate = true) - { - if (ResourcesChanged is object) - { - e ??= ResourcesChangedEventArgs.Empty; - ResourcesChanged(this, e); - } - - if (propagate) - { - e ??= ResourcesChangedEventArgs.Empty; - NotifyChildResourcesChanged(e); - } - } - - private static IReadOnlyList RecurseStyles(IReadOnlyList styles) - { - var count = styles.Count; - List? result = null; - - for (var i = 0; i < count; ++i) - { - var style = styles[i]; - - if (style.Children.Count > 0) - { - if (result is null) - { - result = new List(styles); - } - - RecurseStyles(style.Children, result); - } - } - - return result ?? styles; - } - - private static void RecurseStyles(IReadOnlyList styles, List result) - { - var count = styles.Count; - - for (var i = 0; i < count; ++i) - { - var style = styles[i]; - result.Add(style); - RecurseStyles(style.Children, result); - } - } - } -} diff --git a/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs b/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs deleted file mode 100644 index 479100ed8a..0000000000 --- a/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs +++ /dev/null @@ -1,33 +0,0 @@ -#nullable enable - -using System; - -namespace Avalonia.Styling.Activators -{ - /// - /// Defines a style activator. - /// - /// - /// A style activator is very similar to an `IObservable{bool}` but is optimized for the - /// particular use-case of activating a style according to a selector. It differs from - /// an observable in two major ways: - /// - /// - Can only have a single subscription - /// - The subscription can have a tag associated with it, allowing a subscriber to index - /// into a list of subscriptions without having to allocate additional objects. - /// - public interface IStyleActivator : IDisposable - { - /// - /// Subscribes to the activator. - /// - /// The listener. - /// An optional tag. - void Subscribe(IStyleActivatorSink sink, int tag = 0); - - /// - /// Unsubscribes from the activator. - /// - void Unsubscribe(IStyleActivatorSink sink); - } -} diff --git a/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs b/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs deleted file mode 100644 index a1a6ef5c28..0000000000 --- a/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs +++ /dev/null @@ -1,17 +0,0 @@ -#nullable enable - -namespace Avalonia.Styling.Activators -{ - /// - /// Receives notifications from an . - /// - public interface IStyleActivatorSink - { - /// - /// Called when the subscribed activator value changes. - /// - /// The new value. - /// The subscription tag. - void OnNext(bool value, int tag); - } -} diff --git a/src/Avalonia.Styling/Styling/ChildSelector.cs b/src/Avalonia.Styling/Styling/ChildSelector.cs deleted file mode 100644 index 5c92182b80..0000000000 --- a/src/Avalonia.Styling/Styling/ChildSelector.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using Avalonia.LogicalTree; - -namespace Avalonia.Styling -{ - internal class ChildSelector : Selector - { - private readonly Selector _parent; - private string? _selectorString; - - public ChildSelector(Selector parent) - { - if (parent == null) - { - throw new InvalidOperationException("Child selector must be preceeded by a selector."); - } - - _parent = parent; - } - - /// - public override bool InTemplate => _parent.InTemplate; - - /// - public override bool IsCombinator => true; - - /// - public override Type? TargetType => null; - - public override string ToString() - { - if (_selectorString == null) - { - _selectorString = _parent.ToString() + " > "; - } - - return _selectorString; - } - - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) - { - var controlParent = ((ILogical)control).LogicalParent; - - if (controlParent != null) - { - var parentMatch = _parent.Match((IStyleable)controlParent, subscribe); - - if (parentMatch.Result == SelectorMatchResult.Sometimes) - { - return parentMatch; - } - else if (parentMatch.IsMatch) - { - return SelectorMatch.AlwaysThisInstance; - } - else - { - return SelectorMatch.NeverThisInstance; - } - } - else - { - return SelectorMatch.NeverThisInstance; - } - } - - protected override Selector? MovePrevious() => null; - } -} diff --git a/src/Avalonia.Styling/Styling/DescendentSelector.cs b/src/Avalonia.Styling/Styling/DescendentSelector.cs deleted file mode 100644 index dde88b3436..0000000000 --- a/src/Avalonia.Styling/Styling/DescendentSelector.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using Avalonia.LogicalTree; -using Avalonia.Styling.Activators; - -#nullable enable - -namespace Avalonia.Styling -{ - internal class DescendantSelector : Selector - { - private readonly Selector _parent; - private string? _selectorString; - - public DescendantSelector(Selector? parent) - { - _parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceeded by a selector."); - } - - /// - public override bool IsCombinator => true; - - /// - public override bool InTemplate => _parent.InTemplate; - - /// - public override Type? TargetType => null; - - public override string ToString() - { - if (_selectorString == null) - { - _selectorString = _parent.ToString() + ' '; - } - - return _selectorString; - } - - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) - { - var c = (ILogical)control; - var descendantMatches = new OrActivatorBuilder(); - - while (c != null) - { - c = c.LogicalParent; - - if (c is IStyleable) - { - var match = _parent.Match((IStyleable)c, subscribe); - - if (match.Result == SelectorMatchResult.Sometimes) - { - descendantMatches.Add(match.Activator); - } - else if (match.IsMatch) - { - return SelectorMatch.AlwaysThisInstance; - } - } - } - - if (descendantMatches.Count > 0) - { - return new SelectorMatch(descendantMatches.Get()); - } - else - { - return SelectorMatch.NeverThisInstance; - } - } - - protected override Selector? MovePrevious() => null; - } -} diff --git a/src/Avalonia.Styling/Styling/IGlobalStyles.cs b/src/Avalonia.Styling/Styling/IGlobalStyles.cs deleted file mode 100644 index ab24e3138c..0000000000 --- a/src/Avalonia.Styling/Styling/IGlobalStyles.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// Defines the style host that provides styles global to the application. - /// - public interface IGlobalStyles : IStyleHost - { - /// - /// Raised when styles are added to or a nested styles collection. - /// - public event Action>? GlobalStylesAdded; - - /// - /// Raised when styles are removed from or a nested styles collection. - /// - public event Action>? GlobalStylesRemoved; - } -} diff --git a/src/Avalonia.Styling/Styling/ISetter.cs b/src/Avalonia.Styling/Styling/ISetter.cs deleted file mode 100644 index d588817be8..0000000000 --- a/src/Avalonia.Styling/Styling/ISetter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// Represents a setter for a . - /// - public interface ISetter - { - /// - /// Instances a setter on a control. - /// - /// The control. - /// An . - /// - /// This method should return an which can be used to apply - /// the setter to the specified control. Note that it should not apply the setter value - /// until is called. - /// - ISetterInstance Instance(IStyleable target); - } -} diff --git a/src/Avalonia.Styling/Styling/ISetterInstance.cs b/src/Avalonia.Styling/Styling/ISetterInstance.cs deleted file mode 100644 index a299a87b64..0000000000 --- a/src/Avalonia.Styling/Styling/ISetterInstance.cs +++ /dev/null @@ -1,40 +0,0 @@ -#nullable enable - -using System; - -namespace Avalonia.Styling -{ - /// - /// Represents a setter that has been instanced on a control. - /// - public interface ISetterInstance : IDisposable - { - /// - /// Starts the setter instance. - /// - /// Whether the parent style has an activator. - /// - /// If is false then the setter should be immediately - /// applied and and should not be called. - /// If true, then bindings etc should be initiated but not produce a value until - /// called. - /// - public void Start(bool hasActivator); - - /// - /// Activates the setter. - /// - /// - /// Should only be called if hasActivator was true when was called. - /// - public void Activate(); - - /// - /// Deactivates the setter. - /// - /// - /// Should only be called if hasActivator was true when was called. - /// - public void Deactivate(); - } -} diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs deleted file mode 100644 index 78fbe0f2b5..0000000000 --- a/src/Avalonia.Styling/Styling/IStyle.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Controls; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// Defines the interface for styles. - /// - public interface IStyle - { - /// - /// Gets a collection of child styles. - /// - IReadOnlyList Children { get; } - - /// - /// Attaches the style and any child styles to a control if the style's selector matches. - /// - /// The control to attach to. - /// The element that hosts the style. - /// - /// A describing how the style matches the control. - /// - SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host); - } -} diff --git a/src/Avalonia.Styling/Styling/IStyleHost.cs b/src/Avalonia.Styling/Styling/IStyleHost.cs deleted file mode 100644 index 360b40d9a1..0000000000 --- a/src/Avalonia.Styling/Styling/IStyleHost.cs +++ /dev/null @@ -1,44 +0,0 @@ - -using System.Collections.Generic; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// Defines an element that has a collection. - /// - public interface IStyleHost - { - /// - /// Gets a value indicating whether is initialized. - /// - /// - /// The property may be lazily initialized, if so this property - /// indicates whether it has been initialized. - /// - bool IsStylesInitialized { get; } - - /// - /// Gets the styles for the element. - /// - Styles Styles { get; } - - /// - /// Gets the parent style host element. - /// - IStyleHost? StylingParent { get; } - - /// - /// Called when styles are added to or a nested styles collection. - /// - /// The added styles. - void StylesAdded(IReadOnlyList styles); - - /// - /// Called when styles are removed from or a nested styles collection. - /// - /// The removed styles. - void StylesRemoved(IReadOnlyList styles); - } -} diff --git a/src/Avalonia.Styling/Styling/IStyleInstance.cs b/src/Avalonia.Styling/Styling/IStyleInstance.cs deleted file mode 100644 index 8ddb989bc0..0000000000 --- a/src/Avalonia.Styling/Styling/IStyleInstance.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// Represents a style that has been instanced on a control. - /// - public interface IStyleInstance : IDisposable - { - /// - /// Gets the source style. - /// - IStyle Source { get; } - - /// - /// Gets a value indicating whether this style is active. - /// - bool IsActive { get; } - - /// - /// Instructs the style to start acting upon the control. - /// - void Start(); - } -} diff --git a/src/Avalonia.Styling/Styling/IStyleable.cs b/src/Avalonia.Styling/Styling/IStyleable.cs deleted file mode 100644 index a3df779057..0000000000 --- a/src/Avalonia.Styling/Styling/IStyleable.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Collections; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// Interface for styleable elements. - /// - public interface IStyleable : IAvaloniaObject, INamed - { - /// - /// Gets the list of classes for the control. - /// - IAvaloniaReadOnlyList Classes { get; } - - /// - /// Gets the type by which the control is styled. - /// - Type StyleKey { get; } - - /// - /// Gets the template parent of this element if the control comes from a template. - /// - ITemplatedControl? TemplatedParent { get; } - - /// - /// Notifies the element that a style has been applied. - /// - /// The style instance. - void StyleApplied(IStyleInstance instance); - - /// - /// Detaches all styles applied to the element. - /// - void DetachStyles(); - - /// - /// Detaches a collection of styles, if applied to the element. - /// - void DetachStyles(IReadOnlyList styles); - - /// - /// Detaches all styles from the element and queues a restyle. - /// - void InvalidateStyles(); - } -} diff --git a/src/Avalonia.Styling/Styling/ITemplatedControl.cs b/src/Avalonia.Styling/Styling/ITemplatedControl.cs deleted file mode 100644 index 5485babb62..0000000000 --- a/src/Avalonia.Styling/Styling/ITemplatedControl.cs +++ /dev/null @@ -1,7 +0,0 @@ - -namespace Avalonia.Styling -{ - public interface ITemplatedControl : IAvaloniaObject - { - } -} diff --git a/src/Avalonia.Styling/Styling/NotSelector.cs b/src/Avalonia.Styling/Styling/NotSelector.cs deleted file mode 100644 index ab4e9d5d7f..0000000000 --- a/src/Avalonia.Styling/Styling/NotSelector.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using Avalonia.Styling.Activators; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// The `:not()` style selector. - /// - internal class NotSelector : Selector - { - private readonly Selector? _previous; - private readonly Selector _argument; - private string? _selectorString; - - /// - /// Initializes a new instance of the class. - /// - /// The previous selector. - /// The selector to be not-ed. - public NotSelector(Selector? previous, Selector argument) - { - _previous = previous; - _argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument."); - } - - /// - public override bool InTemplate => _argument.InTemplate; - - /// - public override bool IsCombinator => false; - - /// - public override Type? TargetType => _previous?.TargetType; - - /// - public override string ToString() - { - if (_selectorString == null) - { - _selectorString = $"{_previous?.ToString()}:not({_argument})"; - } - - return _selectorString; - } - - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) - { - var innerResult = _argument.Match(control, subscribe); - - switch (innerResult.Result) - { - case SelectorMatchResult.AlwaysThisInstance: - return SelectorMatch.NeverThisInstance; - case SelectorMatchResult.AlwaysThisType: - return SelectorMatch.NeverThisType; - case SelectorMatchResult.NeverThisInstance: - return SelectorMatch.AlwaysThisInstance; - case SelectorMatchResult.NeverThisType: - return SelectorMatch.AlwaysThisType; - case SelectorMatchResult.Sometimes: - return new SelectorMatch(new NotActivator(innerResult.Activator!)); - default: - throw new InvalidOperationException("Invalid SelectorMatchResult."); - } - } - - protected override Selector? MovePrevious() => _previous; - } -} diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs deleted file mode 100644 index aff34ea17c..0000000000 --- a/src/Avalonia.Styling/Styling/NthChildSelector.cs +++ /dev/null @@ -1,145 +0,0 @@ -#nullable enable -using System; -using System.Text; -using Avalonia.LogicalTree; -using Avalonia.Styling.Activators; - -namespace Avalonia.Styling -{ - /// - /// The :nth-child() pseudo-class matches elements based on their position in a group of siblings. - /// - /// - /// Element indices are 1-based. - /// - public class NthChildSelector : Selector - { - private const string NthChildSelectorName = "nth-child"; - private const string NthLastChildSelectorName = "nth-last-child"; - private readonly Selector? _previous; - private readonly bool _reversed; - - internal protected NthChildSelector(Selector? previous, int step, int offset, bool reversed) - { - _previous = previous; - Step = step; - Offset = offset; - _reversed = reversed; - } - - /// - /// Creates an instance of - /// - /// Previous selector. - /// Position step. - /// Initial index offset. - public NthChildSelector(Selector? previous, int step, int offset) - : this(previous, step, offset, false) - { - - } - - public override bool InTemplate => _previous?.InTemplate ?? false; - - public override bool IsCombinator => false; - - public override Type? TargetType => _previous?.TargetType; - - public int Step { get; } - public int Offset { get; } - - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) - { - if (!(control is ILogical logical)) - { - return SelectorMatch.NeverThisType; - } - - var controlParent = logical.LogicalParent; - - if (controlParent is IChildIndexProvider childIndexProvider) - { - return subscribe - ? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed)) - : Evaluate(logical, childIndexProvider, Step, Offset, _reversed); - } - else - { - return SelectorMatch.NeverThisInstance; - } - } - - internal static SelectorMatch Evaluate( - ILogical logical, IChildIndexProvider childIndexProvider, - int step, int offset, bool reversed) - { - var index = childIndexProvider.GetChildIndex(logical); - if (index < 0) - { - return SelectorMatch.NeverThisInstance; - } - - if (reversed) - { - if (childIndexProvider.TryGetTotalCount(out var totalCountValue)) - { - index = totalCountValue - index; - } - else - { - return SelectorMatch.NeverThisInstance; - } - } - else - { - // nth child index is 1-based - index += 1; - } - - var n = Math.Sign(step); - - var diff = index - offset; - var match = diff == 0 || (Math.Sign(diff) == n && diff % step == 0); - - return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; - } - - protected override Selector? MovePrevious() => _previous; - - public override string ToString() - { - var expectedCapacity = NthLastChildSelectorName.Length + 8; - var stringBuilder = new StringBuilder(_previous?.ToString(), expectedCapacity); - - stringBuilder.Append(':'); - stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName); - stringBuilder.Append('('); - - var hasStep = false; - if (Step != 0) - { - hasStep = true; - stringBuilder.Append(Step); - stringBuilder.Append('n'); - } - - if (Offset > 0) - { - if (hasStep) - { - stringBuilder.Append('+'); - } - stringBuilder.Append(Offset); - } - else if (Offset < 0) - { - stringBuilder.Append('-'); - stringBuilder.Append(-Offset); - } - - stringBuilder.Append(')'); - - return stringBuilder.ToString(); - } - } -} diff --git a/src/Avalonia.Styling/Styling/OrSelector.cs b/src/Avalonia.Styling/Styling/OrSelector.cs deleted file mode 100644 index 3d6db9b01e..0000000000 --- a/src/Avalonia.Styling/Styling/OrSelector.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Styling.Activators; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// The OR style selector. - /// - internal class OrSelector : Selector - { - private readonly IReadOnlyList _selectors; - private string? _selectorString; - private Type? _targetType; - - /// - /// Initializes a new instance of the class. - /// - /// The selectors to OR. - public OrSelector(IReadOnlyList selectors) - { - if (selectors is null) - { - throw new ArgumentNullException(nameof(selectors)); - } - - if (selectors.Count <= 1) - { - throw new ArgumentException("Need more than one selector to OR."); - } - - _selectors = selectors; - } - - /// - public override bool InTemplate => false; - - /// - public override bool IsCombinator => false; - - /// - public override Type? TargetType - { - get - { - if (_targetType == null) - { - _targetType = EvaluateTargetType(); - } - - return _targetType; - } - } - - /// - public override string ToString() - { - if (_selectorString == null) - { - _selectorString = string.Join(", ", _selectors); - } - - return _selectorString; - } - - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) - { - var activators = new OrActivatorBuilder(); - var neverThisInstance = false; - - foreach (var selector in _selectors) - { - var match = selector.Match(control, subscribe); - - switch (match.Result) - { - case SelectorMatchResult.AlwaysThisType: - case SelectorMatchResult.AlwaysThisInstance: - return match; - case SelectorMatchResult.NeverThisInstance: - neverThisInstance = true; - break; - case SelectorMatchResult.Sometimes: - activators.Add(match.Activator!); - break; - } - } - - if (activators.Count > 0) - { - return new SelectorMatch(activators.Get()); - } - else if (neverThisInstance) - { - return SelectorMatch.NeverThisInstance; - } - else - { - return SelectorMatch.NeverThisType; - } - } - - protected override Selector? MovePrevious() => null; - - private Type? EvaluateTargetType() - { - Type? result = null; - - foreach (var selector in _selectors) - { - if (selector.TargetType == null) - { - return null; - } - else if (result == null) - { - result = selector.TargetType; - } - else - { - while (result is not null && !result.IsAssignableFrom(selector.TargetType)) - { - result = result.BaseType; - } - } - } - - return result; - } - } -} - diff --git a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs deleted file mode 100644 index 1cd1a650ef..0000000000 --- a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; -using System.Text; -using Avalonia.Styling.Activators; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// A selector that matches the common case of a type and/or name followed by a collection of - /// style classes and pseudoclasses. - /// - internal class PropertyEqualsSelector : Selector - { - private readonly Selector? _previous; - private readonly AvaloniaProperty _property; - private readonly object? _value; - private string? _selectorString; - - public PropertyEqualsSelector(Selector? previous, AvaloniaProperty property, object? value) - { - property = property ?? throw new ArgumentNullException(nameof(property)); - - _previous = previous; - _property = property; - _value = value; - } - - /// - public override bool InTemplate => _previous?.InTemplate ?? false; - - /// - public override bool IsCombinator => false; - - /// - public override Type? TargetType => _previous?.TargetType; - - /// - public override string ToString() - { - if (_selectorString == null) - { - var builder = new StringBuilder(); - - if (_previous != null) - { - builder.Append(_previous.ToString()); - } - - builder.Append('['); - - if (_property.IsAttached) - { - builder.Append('('); - builder.Append(_property.OwnerType.Name); - builder.Append('.'); - } - - builder.Append(_property.Name); - if (_property.IsAttached) - { - builder.Append(')'); - } - builder.Append('='); - builder.Append(_value ?? string.Empty); - builder.Append(']'); - - _selectorString = builder.ToString(); - } - - return _selectorString; - } - - /// - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) - { - if (subscribe) - { - return new SelectorMatch(new PropertyEqualsActivator(control, _property, _value)); - } - else - { - return Compare(_property.PropertyType, control.GetValue(_property), _value) - ? SelectorMatch.AlwaysThisInstance - : SelectorMatch.NeverThisInstance; - } - - } - - protected override Selector? MovePrevious() => _previous; - - internal static bool Compare(Type propertyType, object? propertyValue, object? value) - { - if (propertyType == typeof(object) && - propertyValue?.GetType() is Type inferredType) - { - propertyType = inferredType; - } - - var valueType = value?.GetType(); - - if (valueType is null || propertyType.IsAssignableFrom(valueType)) - { - return Equals(propertyValue, value); - } - - var converter = TypeDescriptor.GetConverter(propertyType); - if (converter?.CanConvertFrom(valueType) == true) - { - return Equals(propertyValue, converter.ConvertFrom(null, CultureInfo.InvariantCulture, value!)); - } - - return false; - } - } -} diff --git a/src/Avalonia.Styling/Styling/PropertySetterInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs deleted file mode 100644 index 48f462d006..0000000000 --- a/src/Avalonia.Styling/Styling/PropertySetterInstance.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using Avalonia.Data; -using Avalonia.Reactive; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// A which has been instanced on a control. - /// - /// The target property type. - internal class PropertySetterInstance : SingleSubscriberObservableBase>, - ISetterInstance - { - private readonly IStyleable _target; - private readonly StyledPropertyBase? _styledProperty; - private readonly DirectPropertyBase? _directProperty; - private readonly T _value; - private IDisposable? _subscription; - private bool _isActive; - - public PropertySetterInstance( - IStyleable target, - StyledPropertyBase property, - T value) - { - _target = target; - _styledProperty = property; - _value = value; - } - - public PropertySetterInstance( - IStyleable target, - DirectPropertyBase property, - T value) - { - _target = target; - _directProperty = property; - _value = value; - } - - public void Start(bool hasActivator) - { - if (hasActivator) - { - if (_styledProperty is object) - { - _subscription = _target.Bind(_styledProperty, this, BindingPriority.StyleTrigger); - } - else - { - _subscription = _target.Bind(_directProperty!, this); - } - } - else - { - if (_styledProperty is object) - { - _subscription = _target.SetValue(_styledProperty!, _value, BindingPriority.Style); - } - else - { - _target.SetValue(_directProperty!, _value); - } - } - } - - public void Activate() - { - if (!_isActive) - { - _isActive = true; - PublishNext(); - } - } - - public void Deactivate() - { - if (_isActive) - { - _isActive = false; - PublishNext(); - } - } - - public override void Dispose() - { - if (_subscription is object) - { - var sub = _subscription; - _subscription = null; - sub.Dispose(); - } - else if (_isActive) - { - if (_styledProperty is object) - { - _target.ClearValue(_styledProperty); - } - else - { - _target.ClearValue(_directProperty!); - } - } - - base.Dispose(); - } - - protected override void Subscribed() => PublishNext(); - protected override void Unsubscribed() { } - - private void PublishNext() - { - PublishNext(_isActive ? new BindingValue(_value) : default); - } - } -} diff --git a/src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs deleted file mode 100644 index 92653d0064..0000000000 --- a/src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using Avalonia.Data; -using Avalonia.Reactive; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// A which has been instanced on a control and whose value is lazily - /// evaluated. - /// - /// The target property type. - internal class PropertySetterLazyInstance : SingleSubscriberObservableBase>, - ISetterInstance - { - private readonly IStyleable _target; - private readonly StyledPropertyBase? _styledProperty; - private readonly DirectPropertyBase? _directProperty; - private readonly Func _valueFactory; - private BindingValue _value; - private IDisposable? _subscription; - private bool _isActive; - - public PropertySetterLazyInstance( - IStyleable target, - StyledPropertyBase property, - Func valueFactory) - { - _target = target; - _styledProperty = property; - _valueFactory = valueFactory; - } - - public PropertySetterLazyInstance( - IStyleable target, - DirectPropertyBase property, - Func valueFactory) - { - _target = target; - _directProperty = property; - _valueFactory = valueFactory; - } - - public void Start(bool hasActivator) - { - _isActive = !hasActivator; - - if (_styledProperty is object) - { - var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; - _subscription = _target.Bind(_styledProperty, this, priority); - } - else - { - _subscription = _target.Bind(_directProperty!, this); - } - } - - public void Activate() - { - if (!_isActive) - { - _isActive = true; - PublishNext(); - } - } - - public void Deactivate() - { - if (_isActive) - { - _isActive = false; - PublishNext(); - } - } - - public override void Dispose() - { - if (_subscription is object) - { - var sub = _subscription; - _subscription = null; - sub.Dispose(); - } - else if (_isActive) - { - if (_styledProperty is object) - { - _target.ClearValue(_styledProperty); - } - else - { - _target.ClearValue(_directProperty!); - } - } - - base.Dispose(); - } - - protected override void Subscribed() => PublishNext(); - protected override void Unsubscribed() { } - - private T GetValue() - { - if (_value.HasValue) - { - return _value.Value; - } - - _value = _valueFactory(); - return _value.Value; - } - - private void PublishNext() - { - if (_isActive) - { - GetValue(); - PublishNext(_value); - } - else - { - PublishNext(default); - } - } - } -} diff --git a/src/Avalonia.Styling/Styling/Selector.cs b/src/Avalonia.Styling/Styling/Selector.cs deleted file mode 100644 index 0740e0f891..0000000000 --- a/src/Avalonia.Styling/Styling/Selector.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using Avalonia.Styling.Activators; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// A selector in a . - /// - public abstract class Selector - { - /// - /// Gets a value indicating whether either this selector or a previous selector has moved - /// into a template. - /// - public abstract bool InTemplate { get; } - - /// - /// Gets a value indicating whether this selector is a combinator. - /// - /// - /// A combinator is a selector such as Child or Descendent which links simple selectors. - /// - public abstract bool IsCombinator { get; } - - /// - /// Gets the target type of the selector, if available. - /// - public abstract Type? TargetType { get; } - - /// - /// Tries to match the selector with a control. - /// - /// The control. - /// - /// Whether the match should subscribe to changes in order to track the match over time, - /// or simply return an immediate result. - /// - /// A . - public SelectorMatch Match(IStyleable control, bool subscribe = true) - { - // First match the selector until a combinator is found. Selectors are stored from - // right-to-left, so MatchUntilCombinator reverses this order because the type selector - // will be on the left. - var match = MatchUntilCombinator(control, this, subscribe, out var combinator); - - // If the pre-combinator selector matches, we can now match the combinator, if any. - if (match.IsMatch && combinator is object) - { - match = match.And(combinator.Match(control, subscribe)); - - // If we have a combinator then we can never say that we always match a control of - // this type, because by definition the combinator matches on things outside of the - // control. - match = match.Result switch - { - SelectorMatchResult.AlwaysThisType => SelectorMatch.AlwaysThisInstance, - SelectorMatchResult.NeverThisType => SelectorMatch.NeverThisInstance, - _ => match - }; - } - - return match; - } - - /// - /// Evaluates the selector for a match. - /// - /// The control. - /// - /// Whether the match should subscribe to changes in order to track the match over time, - /// or simply return an immediate result. - /// - /// A . - protected abstract SelectorMatch Evaluate(IStyleable control, bool subscribe); - - /// - /// Moves to the previous selector. - /// - protected abstract Selector? MovePrevious(); - - private static SelectorMatch MatchUntilCombinator( - IStyleable control, - Selector start, - bool subscribe, - out Selector? combinator) - { - combinator = null; - - var activators = new AndActivatorBuilder(); - var result = Match(control, start, subscribe, ref activators, ref combinator); - - return result == SelectorMatchResult.Sometimes ? - new SelectorMatch(activators.Get()) : - new SelectorMatch(result); - } - - private static SelectorMatchResult Match( - IStyleable control, - Selector selector, - bool subscribe, - ref AndActivatorBuilder activators, - ref Selector? combinator) - { - var previous = selector.MovePrevious(); - - // Selectors are stored from right-to-left, so we recurse into the selector in order to - // reverse this order, because the type selector will be on the left and is our best - // opportunity to exit early. - if (previous != null && !previous.IsCombinator) - { - var previousMatch = Match(control, previous, subscribe, ref activators, ref combinator); - - if (previousMatch < SelectorMatchResult.Sometimes) - { - return previousMatch; - } - } - - // Match this selector. - var match = selector.Evaluate(control, subscribe); - - if (!match.IsMatch) - { - combinator = null; - return match.Result; - } - else if (match.Activator is object) - { - activators.Add(match.Activator!); - } - - if (previous?.IsCombinator == true) - { - combinator = previous; - } - - return match.Result; - } - } -} diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs deleted file mode 100644 index 7c66469cf1..0000000000 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Avalonia.Styling -{ - /// - /// Extension methods for . - /// - public static class Selectors - { - /// - /// Returns a selector which matches a previous selector's child. - /// - /// The previous selector. - /// The selector. - public static Selector Child(this Selector previous) - { - return new ChildSelector(previous); - } - - /// - /// Returns a selector which matches a control's style class. - /// - /// The previous selector. - /// The name of the style class. - /// The selector. - public static Selector Class(this Selector? previous, string name) - { - _ = name ?? throw new ArgumentNullException(nameof(name)); - - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Name may not be empty", nameof(name)); - } - - var tac = previous as TypeNameAndClassSelector; - - if (tac != null) - { - tac.Classes.Add(name); - return tac; - } - else - { - return TypeNameAndClassSelector.ForClass(previous, name); - } - } - - /// - /// Returns a selector which matches a descendant of a previous selector. - /// - /// The previous selector. - /// The selector. - public static Selector Descendant(this Selector? previous) - { - return new DescendantSelector(previous); - } - - /// - /// Returns a selector which matches a type or a derived type. - /// - /// The previous selector. - /// The type. - /// The selector. - public static Selector Is(this Selector? previous, Type type) - { - _ = type ?? throw new ArgumentNullException(nameof(type)); - - return TypeNameAndClassSelector.Is(previous, type); - } - - /// - /// Returns a selector which matches a type or a derived type. - /// - /// The type. - /// The previous selector. - /// The selector. - public static Selector Is(this Selector? previous) where T : IStyleable - { - return previous.Is(typeof(T)); - } - - /// - /// Returns a selector which matches a control's Name. - /// - /// The previous selector. - /// The name. - /// The selector. - public static Selector Name(this Selector? previous, string name) - { - _ = name ?? throw new ArgumentNullException(nameof(name)); - - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Name may not be empty", nameof(name)); - } - - var tac = previous as TypeNameAndClassSelector; - - if (tac != null) - { - tac.Name = name; - return tac; - } - else - { - return TypeNameAndClassSelector.ForName(previous, name); - } - } - - /// - /// Returns a selector which inverts the results of selector argument. - /// - /// The previous selector. - /// The selector to be not-ed. - /// The selector. - public static Selector Not(this Selector? previous, Func argument) - { - return new NotSelector(previous, argument(null)); - } - - /// - /// Returns a selector which inverts the results of selector argument. - /// - /// The previous selector. - /// The selector to be not-ed. - /// The selector. - public static Selector Not(this Selector? previous, Selector argument) - { - return new NotSelector(previous, argument); - } - - /// - /// - /// The selector. - public static Selector NthChild(this Selector? previous, int step, int offset) - { - return new NthChildSelector(previous, step, offset); - } - - /// - /// - /// The selector. - public static Selector NthLastChild(this Selector? previous, int step, int offset) - { - return new NthLastChildSelector(previous, step, offset); - } - - /// - /// Returns a selector which matches a type. - /// - /// The previous selector. - /// The type. - /// The selector. - public static Selector OfType(this Selector? previous, Type type) - { - _ = type ?? throw new ArgumentNullException(nameof(type)); - - return TypeNameAndClassSelector.OfType(previous, type); - } - - /// - /// Returns a selector which matches a type. - /// - /// The type. - /// The previous selector. - /// The selector. - public static Selector OfType(this Selector? previous) where T : IStyleable - { - return previous.OfType(typeof(T)); - } - - /// - /// Returns a selector which ORs selectors. - /// - /// The selectors to be OR'd. - /// The selector. - public static Selector Or(params Selector[] selectors) - { - return new OrSelector(selectors); - } - - /// - /// Returns a selector which ORs selectors. - /// - /// The selectors to be OR'd. - /// The selector. - public static Selector Or(IReadOnlyList selectors) - { - return new OrSelector(selectors); - } - - /// - /// Returns a selector which matches a control with the specified property value. - /// - /// The property type. - /// The previous selector. - /// The property. - /// The property value. - /// The selector. - public static Selector PropertyEquals(this Selector? previous, AvaloniaProperty property, object? value) - { - _ = property ?? throw new ArgumentNullException(nameof(property)); - - return new PropertyEqualsSelector(previous, property, value); - } - - /// - /// Returns a selector which matches a control with the specified property value. - /// - /// The previous selector. - /// The property. - /// The property value. - /// The selector. - public static Selector PropertyEquals(this Selector? previous, AvaloniaProperty property, object? value) - { - _ = property ?? throw new ArgumentNullException(nameof(property)); - - return new PropertyEqualsSelector(previous, property, value); - } - - /// - /// Returns a selector which enters a lookless control's template. - /// - /// The previous selector. - /// The selector. - public static Selector Template(this Selector previous) - { - return new TemplateSelector(previous); - } - } -} diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs deleted file mode 100644 index 168a882499..0000000000 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using Avalonia.Animation; -using Avalonia.Data; -using Avalonia.Data.Core; -using Avalonia.Metadata; -using Avalonia.Utilities; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// A setter for a . - /// - /// - /// A is used to set a value on a - /// depending on a condition. - /// - public class Setter : ISetter, IAnimationSetter, IAvaloniaPropertyVisitor - { - private object? _value; - - /// - /// Initializes a new instance of the class. - /// - public Setter() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The property to set. - /// The property value. - public Setter(AvaloniaProperty property, object value) - { - Property = property; - Value = value; - } - - /// - /// Gets or sets the property to set. - /// - public AvaloniaProperty? Property { get; set; } - - /// - /// Gets or sets the property value. - /// - [Content] - [AssignBinding] - [DependsOn(nameof(Property))] - public object? Value - { - get => _value; - set - { - (value as ISetterValue)?.Initialize(this); - _value = value; - } - } - - public ISetterInstance Instance(IStyleable target) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - - if (Property is null) - { - throw new InvalidOperationException("Setter.Property must be set."); - } - - var data = new SetterVisitorData - { - target = target, - value = Value, - }; - - Property.Accept(this, ref data); - return data.result!; - } - - void IAvaloniaPropertyVisitor.Visit( - StyledPropertyBase property, - ref SetterVisitorData data) - { - if (data.value is IBinding binding) - { - data.result = new PropertySetterBindingInstance( - data.target, - property, - binding); - } - else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType)) - { - data.result = new PropertySetterLazyInstance( - data.target, - property, - () => (T)template.Build()); - } - else - { - data.result = new PropertySetterInstance( - data.target, - property, - (T)data.value!); - } - } - - void IAvaloniaPropertyVisitor.Visit( - DirectPropertyBase property, - ref SetterVisitorData data) - { - if (data.value is IBinding binding) - { - data.result = new PropertySetterBindingInstance( - data.target, - property, - binding); - } - else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType)) - { - data.result = new PropertySetterLazyInstance( - data.target, - property, - () => (T)template.Build()); - } - else - { - data.result = new PropertySetterInstance( - data.target, - property, - (T)data.value!); - } - } - - private struct SetterVisitorData - { - public IStyleable target; - public object? value; - public ISetterInstance? result; - } - } -} diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs deleted file mode 100644 index 00819ef7be..0000000000 --- a/src/Avalonia.Styling/Styling/Style.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Animation; -using Avalonia.Controls; -using Avalonia.Metadata; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// Defines a style. - /// - public class Style : AvaloniaObject, IStyle, IResourceProvider - { - private IResourceHost? _owner; - private IResourceDictionary? _resources; - private List? _setters; - private List? _animations; - - /// - /// Initializes a new instance of the class. - /// - public Style() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The style selector. - public Style(Func selector) - { - Selector = selector(null); - } - - public IResourceHost? Owner - { - get => _owner; - private set - { - if (_owner != value) - { - _owner = value; - OwnerChanged?.Invoke(this, EventArgs.Empty); - } - } - } - - /// - /// Gets or sets a dictionary of style resources. - /// - public IResourceDictionary Resources - { - get => _resources ?? (Resources = new ResourceDictionary()); - set - { - value = value ?? throw new ArgumentNullException(nameof(value)); - - var hadResources = _resources?.HasResources ?? false; - - _resources = value; - - if (Owner is object) - { - _resources.AddOwner(Owner); - - if (hadResources || _resources.HasResources) - { - Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); - } - } - } - } - - /// - /// Gets or sets the style's selector. - /// - public Selector? Selector { get; set; } - - /// - /// Gets the style's setters. - /// - [Content] - public IList Setters => _setters ??= new List(); - - /// - /// Gets the style's animations. - /// - public IList Animations => _animations ??= new List(); - - bool IResourceNode.HasResources => _resources?.Count > 0; - IReadOnlyList IStyle.Children => Array.Empty(); - - public event EventHandler? OwnerChanged; - - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - - var match = Selector is object ? Selector.Match(target) : - target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; - - if (match.IsMatch && (_setters is object || _animations is object)) - { - var instance = new StyleInstance(this, target, _setters, _animations, match.Activator); - target.StyleApplied(instance); - instance.Start(); - } - - return match.Result; - } - - public bool TryGetResource(object key, out object? result) - { - result = null; - return _resources?.TryGetResource(key, out result) ?? false; - } - - /// - /// Returns a string representation of the style. - /// - /// A string representation of the style. - public override string ToString() - { - if (Selector != null) - { - return "Style: " + Selector.ToString(); - } - else - { - return "Style"; - } - } - - void IResourceProvider.AddOwner(IResourceHost owner) - { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); - - if (Owner != null) - { - throw new InvalidOperationException("The Style already has a parent."); - } - - Owner = owner; - _resources?.AddOwner(owner); - } - - void IResourceProvider.RemoveOwner(IResourceHost owner) - { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); - - if (Owner == owner) - { - Owner = null; - _resources?.RemoveOwner(owner); - } - } - } -} diff --git a/src/Avalonia.Styling/Styling/StyleInstance.cs b/src/Avalonia.Styling/Styling/StyleInstance.cs deleted file mode 100644 index 830cf49a0d..0000000000 --- a/src/Avalonia.Styling/Styling/StyleInstance.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reactive.Subjects; -using Avalonia.Animation; -using Avalonia.Styling.Activators; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// A which has been instanced on a control. - /// - internal class StyleInstance : IStyleInstance, IStyleActivatorSink - { - private readonly List? _setters; - private readonly List? _animations; - private readonly IStyleActivator? _activator; - private readonly Subject? _animationTrigger; - - public StyleInstance( - IStyle source, - IStyleable target, - IReadOnlyList? setters, - IReadOnlyList? animations, - IStyleActivator? activator = null) - { - Source = source ?? throw new ArgumentNullException(nameof(source)); - Target = target ?? throw new ArgumentNullException(nameof(target)); - _activator = activator; - IsActive = _activator is null; - - if (setters is object) - { - var setterCount = setters.Count; - - _setters = new List(setterCount); - - for (var i = 0; i < setterCount; ++i) - { - _setters.Add(setters[i].Instance(Target)); - } - } - - if (animations is object && target is Animatable animatable) - { - var animationsCount = animations.Count; - - _animations = new List(animationsCount); - _animationTrigger = new Subject(); - - for (var i = 0; i < animationsCount; ++i) - { - _animations.Add(animations[i].Apply(animatable, null, _animationTrigger)); - } - } - } - - public bool IsActive { get; private set; } - public IStyle Source { get; } - public IStyleable Target { get; } - - public void Start() - { - var hasActivator = _activator is object; - - if (_setters is object) - { - foreach (var setter in _setters) - { - setter.Start(hasActivator); - } - } - - if (hasActivator) - { - _activator!.Subscribe(this, 0); - } - else if (_animationTrigger != null) - { - _animationTrigger.OnNext(true); - } - } - - public void Dispose() - { - if (_setters is object) - { - foreach (var setter in _setters) - { - setter.Dispose(); - } - } - - if (_animations is object) - { - foreach (var subscripion in _animations) - { - subscripion.Dispose(); - } - } - - _activator?.Dispose(); - } - - private void ActivatorChanged(bool value) - { - if (IsActive != value) - { - IsActive = value; - - _animationTrigger?.OnNext(value); - - if (_setters is object) - { - if (IsActive) - { - foreach (var setter in _setters) - { - setter.Activate(); - } - } - else - { - foreach (var setter in _setters) - { - setter.Deactivate(); - } - } - } - } - } - - void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(value); - } -} diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs deleted file mode 100644 index 81502f1570..0000000000 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ /dev/null @@ -1,334 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using Avalonia.Collections; -using Avalonia.Controls; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// A style that consists of a number of child styles. - /// - public class Styles : AvaloniaObject, - IAvaloniaList, - IStyle, - IResourceProvider - { - private readonly AvaloniaList _styles = new AvaloniaList(); - private IResourceHost? _owner; - private IResourceDictionary? _resources; - private Dictionary?>? _cache; - - public Styles() - { - _styles.ResetBehavior = ResetBehavior.Remove; - _styles.CollectionChanged += OnCollectionChanged; - } - - public Styles(IResourceHost owner) - : this() - { - Owner = owner; - } - - public event NotifyCollectionChangedEventHandler? CollectionChanged; - public event EventHandler? OwnerChanged; - - public int Count => _styles.Count; - - public IResourceHost? Owner - { - get => _owner; - private set - { - if (_owner != value) - { - _owner = value; - OwnerChanged?.Invoke(this, EventArgs.Empty); - } - } - } - - /// - /// Gets or sets a dictionary of style resources. - /// - public IResourceDictionary Resources - { - get => _resources ?? (Resources = new ResourceDictionary()); - set - { - value = value ?? throw new ArgumentNullException(nameof(Resources)); - - if (Owner is object) - { - _resources?.RemoveOwner(Owner); - } - - _resources = value; - - if (Owner is object) - { - _resources.AddOwner(Owner); - } - } - } - - bool ICollection.IsReadOnly => false; - - bool IResourceNode.HasResources - { - get - { - if (_resources?.Count > 0) - { - return true; - } - - foreach (var i in this) - { - if (i is IResourceProvider p && p.HasResources) - { - return true; - } - } - - return false; - } - } - - IStyle IReadOnlyList.this[int index] => _styles[index]; - - IReadOnlyList IStyle.Children => this; - - public IStyle this[int index] - { - get => _styles[index]; - set => _styles[index] = value; - } - - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) - { - _cache ??= new Dictionary?>(); - - if (_cache.TryGetValue(target.StyleKey, out var cached)) - { - if (cached is object) - { - foreach (var style in cached) - { - style.TryAttach(target, host); - } - - return SelectorMatchResult.AlwaysThisType; - } - else - { - return SelectorMatchResult.NeverThisType; - } - } - else - { - List? matches = null; - - foreach (var child in this) - { - if (child.TryAttach(target, host) != SelectorMatchResult.NeverThisType) - { - matches ??= new List(); - matches.Add(child); - } - } - - _cache.Add(target.StyleKey, matches); - - return matches is null ? - SelectorMatchResult.NeverThisType : - SelectorMatchResult.AlwaysThisType; - } - } - - /// - public bool TryGetResource(object key, out object? value) - { - if (_resources != null && _resources.TryGetResource(key, out value)) - { - return true; - } - - for (var i = Count - 1; i >= 0; --i) - { - if (this[i] is IResourceProvider p && p.TryGetResource(key, out value)) - { - return true; - } - } - - value = null; - return false; - } - - /// - public void AddRange(IEnumerable items) => _styles.AddRange(items); - - /// - public void InsertRange(int index, IEnumerable items) => _styles.InsertRange(index, items); - - /// - public void Move(int oldIndex, int newIndex) => _styles.Move(oldIndex, newIndex); - - /// - public void MoveRange(int oldIndex, int count, int newIndex) => _styles.MoveRange(oldIndex, count, newIndex); - - /// - public void RemoveAll(IEnumerable items) => _styles.RemoveAll(items); - - /// - public void RemoveRange(int index, int count) => _styles.RemoveRange(index, count); - - /// - public int IndexOf(IStyle item) => _styles.IndexOf(item); - - /// - public void Insert(int index, IStyle item) => _styles.Insert(index, item); - - /// - public void RemoveAt(int index) => _styles.RemoveAt(index); - - /// - public void Add(IStyle item) => _styles.Add(item); - - /// - public void Clear() => _styles.Clear(); - - /// - public bool Contains(IStyle item) => _styles.Contains(item); - - /// - public void CopyTo(IStyle[] array, int arrayIndex) => _styles.CopyTo(array, arrayIndex); - - /// - public bool Remove(IStyle item) => _styles.Remove(item); - - public AvaloniaList.Enumerator GetEnumerator() => _styles.GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); - - /// - void IResourceProvider.AddOwner(IResourceHost owner) - { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); - - if (Owner != null) - { - throw new InvalidOperationException("The Styles already has a owner."); - } - - Owner = owner; - _resources?.AddOwner(owner); - - foreach (var child in this) - { - if (child is IResourceProvider r) - { - r.AddOwner(owner); - } - } - } - - /// - void IResourceProvider.RemoveOwner(IResourceHost owner) - { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); - - if (Owner == owner) - { - Owner = null; - _resources?.RemoveOwner(owner); - - foreach (var child in this) - { - if (child is IResourceProvider r) - { - r.RemoveOwner(owner); - } - } - } - } - - private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - static IReadOnlyList ToReadOnlyList(IList list) - { - if (list is IReadOnlyList) - { - return (IReadOnlyList)list; - } - else - { - var result = new T[list.Count]; - list.CopyTo(result, 0); - return result; - } - } - - void Add(IList items) - { - for (var i = 0; i < items.Count; ++i) - { - var style = (IStyle)items[i]!; - - if (Owner is object && style is IResourceProvider resourceProvider) - { - resourceProvider.AddOwner(Owner); - } - - _cache = null; - } - - (Owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); - } - - void Remove(IList items) - { - for (var i = 0; i < items.Count; ++i) - { - var style = (IStyle)items[i]!; - - if (Owner is object && style is IResourceProvider resourceProvider) - { - resourceProvider.RemoveOwner(Owner); - } - - _cache = null; - } - - (Owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); - } - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - Add(e.NewItems!); - break; - case NotifyCollectionChangedAction.Remove: - Remove(e.OldItems!); - break; - case NotifyCollectionChangedAction.Replace: - Remove(e.OldItems!); - Add(e.NewItems!); - break; - case NotifyCollectionChangedAction.Reset: - throw new InvalidOperationException("Reset should not be called on Styles."); - } - - CollectionChanged?.Invoke(this, e); - } - } -} diff --git a/src/Avalonia.Styling/Styling/TemplateSelector.cs b/src/Avalonia.Styling/Styling/TemplateSelector.cs deleted file mode 100644 index e8051efa6d..0000000000 --- a/src/Avalonia.Styling/Styling/TemplateSelector.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; - -namespace Avalonia.Styling -{ - internal class TemplateSelector : Selector - { - private readonly Selector _parent; - private string? _selectorString; - - public TemplateSelector(Selector parent) - { - if (parent == null) - { - throw new InvalidOperationException("Template selector must be preceeded by a selector."); - } - - _parent = parent; - } - - /// - public override bool InTemplate => true; - - /// - public override bool IsCombinator => true; - - /// - public override Type? TargetType => null; - - public override string ToString() - { - if (_selectorString == null) - { - _selectorString = _parent.ToString() + " /template/ "; - } - - return _selectorString; - } - - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) - { - var templatedParent = control.TemplatedParent as IStyleable; - - if (templatedParent == null) - { - return SelectorMatch.NeverThisInstance; - } - - return _parent.Match(templatedParent, subscribe); - } - - protected override Selector? MovePrevious() => null; - } -} diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs deleted file mode 100644 index ef48c4a8cd..0000000000 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Avalonia.Styling.Activators; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// A selector that matches the common case of a type and/or name followed by a collection of - /// style classes and pseudoclasses. - /// - internal class TypeNameAndClassSelector : Selector - { - private readonly Selector? _previous; - private readonly Lazy> _classes = new Lazy>(() => new List()); - private Type? _targetType; - private string? _selectorString; - - public static TypeNameAndClassSelector OfType(Selector? previous, Type targetType) - { - var result = new TypeNameAndClassSelector(previous); - result._targetType = targetType; - result.IsConcreteType = true; - - return result; - } - - public static TypeNameAndClassSelector Is(Selector? previous, Type targetType) - { - var result = new TypeNameAndClassSelector(previous); - result._targetType = targetType; - result.IsConcreteType = false; - - return result; - } - - public static TypeNameAndClassSelector ForName(Selector? previous, string name) - { - var result = new TypeNameAndClassSelector(previous); - result.Name = name; - - return result; - } - - public static TypeNameAndClassSelector ForClass(Selector? previous, string className) - { - var result = new TypeNameAndClassSelector(previous); - result.Classes.Add(className); - - return result; - } - - protected TypeNameAndClassSelector(Selector? previous) - { - _previous = previous; - } - - /// - public override bool InTemplate => _previous?.InTemplate ?? false; - - /// - /// Gets the name of the control to match. - /// - public string? Name { get; set; } - - /// - public override Type? TargetType => _targetType ?? _previous?.TargetType; - - /// - public override bool IsCombinator => false; - - /// - /// Whether the selector matches the concrete or any object which - /// implements . - /// - public bool IsConcreteType { get; private set; } - - /// - /// The style classes which the selector matches. - /// - public IList Classes => _classes.Value; - - /// - public override string ToString() - { - if (_selectorString == null) - { - _selectorString = BuildSelectorString(); - } - - return _selectorString; - } - - /// - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) - { - if (TargetType != null) - { - var controlType = control.StyleKey ?? control.GetType(); - - if (IsConcreteType) - { - if (controlType != TargetType) - { - return SelectorMatch.NeverThisType; - } - } - else - { - if (!TargetType.IsAssignableFrom(controlType)) - { - return SelectorMatch.NeverThisType; - } - } - } - - if (Name != null && control.Name != Name) - { - return SelectorMatch.NeverThisInstance; - } - - if (_classes.IsValueCreated && _classes.Value.Count > 0) - { - if (subscribe) - { - var observable = new StyleClassActivator(control.Classes, _classes.Value); - - return new SelectorMatch(observable); - } - - if (!StyleClassActivator.AreClassesMatching(control.Classes, Classes)) - { - return SelectorMatch.NeverThisInstance; - } - } - - return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; - } - - protected override Selector? MovePrevious() => _previous; - - private string BuildSelectorString() - { - var builder = new StringBuilder(); - - if (_previous != null) - { - builder.Append(_previous.ToString()); - } - - if (TargetType != null) - { - if (IsConcreteType) - { - builder.Append(TargetType.Name); - } - else - { - builder.Append(":is("); - builder.Append(TargetType.Name); - builder.Append(")"); - } - } - - if (Name != null) - { - builder.Append('#'); - builder.Append(Name); - } - - if (_classes.IsValueCreated && _classes.Value.Count > 0) - { - foreach (var c in Classes) - { - if (!c.StartsWith(":")) - { - builder.Append('.'); - } - - builder.Append(c); - } - } - - return builder.ToString(); - } - } -} diff --git a/src/Avalonia.Themes.Default/ApiCompatBaseline.txt b/src/Avalonia.Themes.Default/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Avalonia.Themes.Default/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index ef200b5532..40ed4a0f87 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -3,16 +3,10 @@ net6.0;netstandard2.0 - - - - - - - + diff --git a/src/Avalonia.Themes.Default/Controls/Button.xaml b/src/Avalonia.Themes.Default/Controls/Button.xaml index da36abe7ec..a2971c3ff6 100644 --- a/src/Avalonia.Themes.Default/Controls/Button.xaml +++ b/src/Avalonia.Themes.Default/Controls/Button.xaml @@ -18,7 +18,7 @@ Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" RecognizesAccessKey="True" - TextBlock.Foreground="{TemplateBinding Foreground}" + TextElement.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> diff --git a/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml index cf469eeac5..be6642467f 100644 --- a/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml @@ -4,7 +4,7 @@ - + - + diff --git a/src/Avalonia.Themes.Default/Controls/DatePicker.xaml b/src/Avalonia.Themes.Default/Controls/DatePicker.xaml index c6c117138d..c1ee941416 100644 --- a/src/Avalonia.Themes.Default/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Default/Controls/DatePicker.xaml @@ -36,7 +36,7 @@ @@ -151,7 +151,7 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" - TextBlock.Foreground="{TemplateBinding Foreground}" + TextElement.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/> @@ -196,7 +196,7 @@ diff --git a/src/Avalonia.Themes.Default/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Default/Controls/DropDownButton.xaml new file mode 100644 index 0000000000..662c6379fb --- /dev/null +++ b/src/Avalonia.Themes.Default/Controls/DropDownButton.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Controls/Expander.xaml b/src/Avalonia.Themes.Default/Controls/Expander.xaml index e72ddea163..2f18faf84a 100644 --- a/src/Avalonia.Themes.Default/Controls/Expander.xaml +++ b/src/Avalonia.Themes.Default/Controls/Expander.xaml @@ -1,5 +1,36 @@ - + + + + + + + + + + + + Expanded content + + + + + Expanded content + + + + + Expanded content + + + + + Expanded content + + + + + diff --git a/src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml b/src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml index 301e19d208..07d905ea1d 100644 --- a/src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml +++ b/src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml @@ -1,4 +1,4 @@ - diff --git a/src/Avalonia.Themes.Default/Controls/PopupRoot.xaml b/src/Avalonia.Themes.Default/Controls/PopupRoot.xaml index 9468cc5535..5e8f3337ee 100644 --- a/src/Avalonia.Themes.Default/Controls/PopupRoot.xaml +++ b/src/Avalonia.Themes.Default/Controls/PopupRoot.xaml @@ -10,16 +10,18 @@ - - - - - - + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml index a9a03c8ed5..47398966f7 100644 --- a/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml +++ b/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml @@ -24,7 +24,7 @@ ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" - TextBlock.Foreground="{TemplateBinding Foreground}" + TextElement.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> diff --git a/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml index b357446bfa..aab1b76259 100644 --- a/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml @@ -13,7 +13,7 @@ CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" Content="{TemplateBinding Content}" Extent="{TemplateBinding Extent, Mode=TwoWay}" - Margin="{TemplateBinding Padding}" + Padding="{TemplateBinding Padding}" Offset="{TemplateBinding Offset, Mode=TwoWay}" Viewport="{TemplateBinding Viewport, Mode=TwoWay}" IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"> diff --git a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml index ce20a1a165..0c46ce3724 100644 --- a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml @@ -16,6 +16,7 @@ 24 24 1 + 24 1 @@ -59,8 +60,11 @@ + + + @@ -146,7 +150,7 @@ SplitButton /template/ Button#PART_SecondaryButton:pointerover /template/ ContentPresenter"> - + diff --git a/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml index b14a239c35..17fb2af16c 100644 --- a/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml @@ -18,7 +18,7 @@ Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" RecognizesAccessKey="True" - TextBlock.Foreground="{TemplateBinding Foreground}" + TextElement.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> diff --git a/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml index 305f1b1814..2c831cf360 100644 --- a/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml @@ -254,7 +254,7 @@ + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 009b56852a..468b723f5b 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -3,8 +3,9 @@ x:Class="Avalonia.Themes.Default.DefaultTheme"> + + - @@ -13,6 +14,7 @@ + @@ -22,7 +24,6 @@ - @@ -38,6 +39,7 @@ + diff --git a/src/Avalonia.Themes.Default/IBitmapToImageConverter.cs b/src/Avalonia.Themes.Default/IBitmapToImageConverter.cs new file mode 100644 index 0000000000..9b7fcecf45 --- /dev/null +++ b/src/Avalonia.Themes.Default/IBitmapToImageConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Media.Imaging; + +namespace Avalonia.Themes.Default +{ + internal class IBitmapToImageConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value != null && value is IBitmap bm) + return new Image { Source=bm }; + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Themes.Default/SimpleTheme.cs b/src/Avalonia.Themes.Default/SimpleTheme.cs index 1d9f2d5f9d..6929660757 100644 --- a/src/Avalonia.Themes.Default/SimpleTheme.cs +++ b/src/Avalonia.Themes.Default/SimpleTheme.cs @@ -116,7 +116,7 @@ namespace Avalonia.Themes.Default return false; } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ModeProperty) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 5b86de02d5..c5bb70bed3 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -587,9 +587,24 @@ - + + + + + + + + + + + + + + + + 1 diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index eb68270354..8d38d39bd5 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -581,9 +581,24 @@ - + + + + + + + + + + + + + + + + 1 diff --git a/src/Avalonia.Themes.Fluent/ApiCompatBaseline.txt b/src/Avalonia.Themes.Fluent/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 885661536c..35603fe216 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -3,16 +3,10 @@ net6.0;netstandard2.0 - - - - - - - + diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 533fabfb44..a93fb6831d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -4,12 +4,14 @@ - /// The bounds of the ellipse. - /// An ellipse geometry.. - IGeometryImpl CreateEllipseGeometry(Rect rect); - - /// - /// Creates a line geometry implementation. - /// - /// The start of the line. - /// The end of the line. - /// A line geometry. - IGeometryImpl CreateLineGeometry(Point p1, Point p2); - - /// - /// Creates a rectangle geometry implementation. - /// - /// The bounds of the rectangle. - /// A rectangle. - IGeometryImpl CreateRectangleGeometry(Rect rect); - - /// - /// Creates a stream geometry implementation. - /// - /// An . - IStreamGeometryImpl CreateStreamGeometry(); - - /// - /// Creates a geometry group implementation. - /// - /// The fill rule. - /// The geometries to group. - /// A combined geometry. - IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); - - /// - /// Creates a geometry group implementation. - /// - /// The combine mode - /// The first geometry. - /// The second geometry. - /// A combined geometry. - IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2); - - /// - /// Creates a renderer. - /// - /// - /// The list of native platform surfaces that can be used for output. - /// - /// An . - IRenderTarget CreateRenderTarget(IEnumerable surfaces); - - /// - /// Creates a render target bitmap implementation. - /// - /// The size of the bitmap in device pixels. - /// The DPI of the bitmap. - /// An . - IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi); - - /// - /// Creates a writeable bitmap implementation. - /// - /// The size of the bitmap in device pixels. - /// The DPI of the bitmap. - /// Pixel format. - /// Alpha format . - /// An . - IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat format, AlphaFormat alphaFormat); - - /// - /// Loads a bitmap implementation from a file.. - /// - /// The filename of the bitmap. - /// An . - IBitmapImpl LoadBitmap(string fileName); - - /// - /// Loads a bitmap implementation from a file.. - /// - /// The stream to read the bitmap from. - /// An . - IBitmapImpl LoadBitmap(Stream stream); - - /// - /// Loads a WriteableBitmap implementation from a stream to a specified width maintaining aspect ratio. - /// - /// The stream to read the bitmap from. - /// The desired width of the resulting bitmap. - /// The to use should resizing be required. - /// An . - IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); - - /// - /// Loads a WriteableBitmap implementation from a stream to a specified height maintaining aspect ratio. - /// - /// The stream to read the bitmap from. - /// The desired height of the resulting bitmap. - /// The to use should resizing be required. - /// An . - IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); - - /// - /// Loads a WriteableBitmap implementation from a file. - /// - /// The filename of the bitmap. - /// An . - IWriteableBitmapImpl LoadWriteableBitmap(string fileName); - - /// - /// Loads a WriteableBitmap implementation from a file. - /// - /// The stream to read the bitmap from. - /// An . - IWriteableBitmapImpl LoadWriteableBitmap(Stream stream); - - /// - /// Loads a bitmap implementation from a stream to a specified width maintaining aspect ratio. - /// - /// The stream to read the bitmap from. - /// The desired width of the resulting bitmap. - /// The to use should resizing be required. - /// An . - IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); - - /// - /// Loads a bitmap implementation from a stream to a specified height maintaining aspect ratio. - /// - /// The stream to read the bitmap from. - /// The desired height of the resulting bitmap. - /// The to use should resizing be required. - /// An . - IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); - - IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); - - /// - /// Loads a bitmap implementation from a pixels in memory. - /// - /// The pixel format. - /// The alpha format. - /// The pointer to source bytes. - /// The size of the bitmap in device pixels. - /// The DPI of the bitmap. - /// The number of bytes per row. - /// An . - IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride); - - /// - /// Creates a platform implementation of a glyph run. - /// - /// The glyph run. - /// - IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun); - - bool SupportsIndividualRoundRects { get; } - - /// - /// Default used on this platform. - /// - public AlphaFormat DefaultAlphaFormat { get; } - - /// - /// Default used on this platform. - /// - public PixelFormat DefaultPixelFormat { get; } - } -} diff --git a/src/Avalonia.Visuals/Platform/IPlatformSettings.cs b/src/Avalonia.Visuals/Platform/IPlatformSettings.cs deleted file mode 100644 index e4b28e6575..0000000000 --- a/src/Avalonia.Visuals/Platform/IPlatformSettings.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Avalonia.Platform -{ - public interface IPlatformSettings - { - Size DoubleClickSize { get; } - - TimeSpan DoubleClickTime { get; } - - /// - /// Determines the size of the area within that you should click twice in order for a double click to be counted. - /// - Size TouchDoubleClickSize { get; } - - /// - /// Determines the time span that what will be used to determine the double-click. - /// - TimeSpan TouchDoubleClickTime { get; } - } -} diff --git a/src/Avalonia.Visuals/Platform/IRenderTargetBitmapImpl.cs b/src/Avalonia.Visuals/Platform/IRenderTargetBitmapImpl.cs deleted file mode 100644 index 9add07afe3..0000000000 --- a/src/Avalonia.Visuals/Platform/IRenderTargetBitmapImpl.cs +++ /dev/null @@ -1,11 +0,0 @@ - -namespace Avalonia.Platform -{ - /// - /// Defines the platform-specific interface for a - /// . - /// - public interface IRenderTargetBitmapImpl : IBitmapImpl, IRenderTarget - { - } -} diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs deleted file mode 100644 index 4587979308..0000000000 --- a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Avalonia.Platform -{ - /// - /// Describes a geometry using drawing commands. - /// - public interface IStreamGeometryContextImpl : IGeometryContext - { - } -} diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IStreamGeometryImpl.cs deleted file mode 100644 index 5b070fde02..0000000000 --- a/src/Avalonia.Visuals/Platform/IStreamGeometryImpl.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Avalonia.Platform -{ - /// - /// Defines the platform-specific interface for a . - /// - public interface IStreamGeometryImpl : IGeometryImpl - { - /// - /// Clones the geometry. - /// - /// A cloned geometry. - IStreamGeometryImpl Clone(); - - /// - /// Opens the geometry to start defining it. - /// - /// - /// An which can be used to define the geometry. - /// - IStreamGeometryContextImpl Open(); - } -} diff --git a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs deleted file mode 100644 index aced05c9d8..0000000000 --- a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Globalization; -using Avalonia.Media; -using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; - -namespace Avalonia.Platform -{ - /// - /// An abstraction that is used produce shaped text. - /// - public interface ITextShaperImpl - { - /// - /// Shapes the specified region within the text and returns a shaped buffer. - /// - /// The text. - /// The typeface. - /// The font rendering em size. - /// The culture. - /// The bidi level. - /// A shaped glyph run. - ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, CultureInfo? culture, sbyte bidiLevel); - } -} diff --git a/src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs b/src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs deleted file mode 100644 index 1ed025b571..0000000000 --- a/src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Avalonia.Platform -{ - /// - /// Represents a geometry with a transform applied. - /// - /// - /// An transforms a geometry without transforming its - /// stroke thickness. - /// - public interface ITransformedGeometryImpl : IGeometryImpl - { - /// - /// Gets the source geometry that the is applied to. - /// - IGeometryImpl SourceGeometry { get; } - - /// - /// Gets the applied transform. - /// - Matrix Transform { get; } - } -} diff --git a/src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs b/src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs deleted file mode 100644 index c4e2e4915f..0000000000 --- a/src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Avalonia.Platform -{ - /// - /// Defines the platform-specific interface for a . - /// - public interface IWriteableBitmapImpl : IBitmapImpl - { - ILockedFramebuffer Lock(); - } -} diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs deleted file mode 100644 index 67e7d71fbc..0000000000 --- a/src/Avalonia.Visuals/Point.cs +++ /dev/null @@ -1,297 +0,0 @@ -using System; -using System.Globalization; -#if !BUILDTASK -using Avalonia.Animation.Animators; -#endif -using Avalonia.Utilities; - -namespace Avalonia -{ - /// - /// Defines a point. - /// -#if !BUILDTASK - public -#endif - readonly struct Point : IEquatable - { - static Point() - { -#if !BUILDTASK - Animation.Animation.RegisterAnimator(prop => typeof(Point).IsAssignableFrom(prop.PropertyType)); -#endif - } - - /// - /// The X position. - /// - private readonly double _x; - - /// - /// The Y position. - /// - private readonly double _y; - - /// - /// Initializes a new instance of the structure. - /// - /// The X position. - /// The Y position. - public Point(double x, double y) - { - _x = x; - _y = y; - } - - /// - /// Gets the X position. - /// - public double X => _x; - - /// - /// Gets the Y position. - /// - public double Y => _y; - - /// - /// Converts the to a . - /// - /// The point. - public static implicit operator Vector(Point p) - { - return new Vector(p._x, p._y); - } - - /// - /// Negates a point. - /// - /// The point. - /// The negated point. - public static Point operator -(Point a) - { - return new Point(-a._x, -a._y); - } - - /// - /// Checks for equality between two s. - /// - /// The first point. - /// The second point. - /// True if the points are equal; otherwise false. - public static bool operator ==(Point left, Point right) - { - return left.Equals(right); - } - - /// - /// Checks for inequality between two s. - /// - /// The first point. - /// The second point. - /// True if the points are unequal; otherwise false. - public static bool operator !=(Point left, Point right) - { - return !(left == right); - } - - /// - /// Adds two points. - /// - /// The first point. - /// The second point. - /// A point that is the result of the addition. - public static Point operator +(Point a, Point b) - { - return new Point(a._x + b._x, a._y + b._y); - } - - /// - /// Adds a vector to a point. - /// - /// The point. - /// The vector. - /// A point that is the result of the addition. - public static Point operator +(Point a, Vector b) - { - return new Point(a._x + b.X, a._y + b.Y); - } - - /// - /// Subtracts two points. - /// - /// The first point. - /// The second point. - /// A point that is the result of the subtraction. - public static Point operator -(Point a, Point b) - { - return new Point(a._x - b._x, a._y - b._y); - } - - /// - /// Subtracts a vector from a point. - /// - /// The point. - /// The vector. - /// A point that is the result of the subtraction. - public static Point operator -(Point a, Vector b) - { - return new Point(a._x - b.X, a._y - b.Y); - } - - /// - /// Multiplies a point by a factor coordinate-wise - /// - /// Point to multiply - /// Factor - /// Points having its coordinates multiplied - public static Point operator *(Point p, double k) => new Point(p.X * k, p.Y * k); - - /// - /// Multiplies a point by a factor coordinate-wise - /// - /// Point to multiply - /// Factor - /// Points having its coordinates multiplied - public static Point operator *(double k, Point p) => new Point(p.X * k, p.Y * k); - - /// - /// Divides a point by a factor coordinate-wise - /// - /// Point to divide by - /// Factor - /// Points having its coordinates divided - public static Point operator /(Point p, double k) => new Point(p.X / k, p.Y / k); - - /// - /// Applies a matrix to a point. - /// - /// The point. - /// The matrix. - /// The resulting point. - public static Point operator *(Point point, Matrix matrix) - { - return new Point( - (point.X * matrix.M11) + (point.Y * matrix.M21) + matrix.M31, - (point.X * matrix.M12) + (point.Y * matrix.M22) + matrix.M32); - } - - /// - /// Parses a string. - /// - /// The string. - /// The . - public static Point Parse(string s) - { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point.")) - { - return new Point( - tokenizer.ReadDouble(), - tokenizer.ReadDouble() - ); - } - } - - /// - /// Returns a boolean indicating whether the point is equal to the other given point. - /// - /// The other point to test equality against. - /// True if this point is equal to other; False otherwise. - public bool Equals(Point other) - { - // ReSharper disable CompareOfFloatsByEqualityOperator - return _x == other._x && - _y == other._y; - // ReSharper enable CompareOfFloatsByEqualityOperator - } - - /// - /// Checks for equality between a point and an object. - /// - /// The object. - /// - /// True if is a point that equals the current point. - /// - public override bool Equals(object? obj) => obj is Point other && Equals(other); - - /// - /// Returns a hash code for a . - /// - /// The hash code. - public override int GetHashCode() - { - unchecked - { - int hash = 17; - hash = (hash * 23) + _x.GetHashCode(); - hash = (hash * 23) + _y.GetHashCode(); - return hash; - } - } - - /// - /// Returns the string representation of the point. - /// - /// The string representation of the point. - public override string ToString() - { - return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y); - } - - /// - /// Transforms the point by a matrix. - /// - /// The transform. - /// The transformed point. - public Point Transform(Matrix transform) - { - var x = X; - var y = Y; - var xadd = y * transform.M21 + transform.M31; - var yadd = x * transform.M12 + transform.M32; - x *= transform.M11; - x += xadd; - y *= transform.M22; - y += yadd; - return new Point(x, y); - } - - /// - /// Returns a new point with the specified X coordinate. - /// - /// The X coordinate. - /// The new point. - public Point WithX(double x) - { - return new Point(x, _y); - } - - /// - /// Returns a new point with the specified Y coordinate. - /// - /// The Y coordinate. - /// The new point. - public Point WithY(double y) - { - return new Point(_x, y); - } - - /// - /// Deconstructs the point into its X and Y coordinates. - /// - /// The X coordinate. - /// The Y coordinate. - public void Deconstruct(out double x, out double y) - { - x = this._x; - y = this._y; - } - - /// - /// Gets a value indicating whether the X and Y coordinates are zero. - /// - public bool IsDefault - { - get { return (_x == 0) && (_y == 0); } - } - } -} diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs deleted file mode 100644 index ebff097199..0000000000 --- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Runtime.CompilerServices; -using Avalonia.Metadata; - -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Transformation")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] - -[assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.Web.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs deleted file mode 100644 index 6d7d6c2e54..0000000000 --- a/src/Avalonia.Visuals/Rect.cs +++ /dev/null @@ -1,576 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Animation.Animators; -using Avalonia.Utilities; - -namespace Avalonia -{ - /// - /// Defines a rectangle. - /// - public readonly struct Rect : IEquatable - { - static Rect() - { - Animation.Animation.RegisterAnimator(prop => typeof(Rect).IsAssignableFrom(prop.PropertyType)); - } - - /// - /// An empty rectangle. - /// - public static readonly Rect Empty = default(Rect); - - /// - /// The X position. - /// - private readonly double _x; - - /// - /// The Y position. - /// - private readonly double _y; - - /// - /// The width. - /// - private readonly double _width; - - /// - /// The height. - /// - private readonly double _height; - - /// - /// Initializes a new instance of the structure. - /// - /// The X position. - /// The Y position. - /// The width. - /// The height. - public Rect(double x, double y, double width, double height) - { - _x = x; - _y = y; - _width = width; - _height = height; - } - - /// - /// Initializes a new instance of the structure. - /// - /// The size of the rectangle. - public Rect(Size size) - { - _x = 0; - _y = 0; - _width = size.Width; - _height = size.Height; - } - - /// - /// Initializes a new instance of the structure. - /// - /// The position of the rectangle. - /// The size of the rectangle. - public Rect(Point position, Size size) - { - _x = position.X; - _y = position.Y; - _width = size.Width; - _height = size.Height; - } - - /// - /// Initializes a new instance of the structure. - /// - /// The top left position of the rectangle. - /// The bottom right position of the rectangle. - public Rect(Point topLeft, Point bottomRight) - { - _x = topLeft.X; - _y = topLeft.Y; - _width = bottomRight.X - topLeft.X; - _height = bottomRight.Y - topLeft.Y; - } - - /// - /// Gets the X position. - /// - public double X => _x; - - /// - /// Gets the Y position. - /// - public double Y => _y; - - /// - /// Gets the width. - /// - public double Width => _width; - - /// - /// Gets the height. - /// - public double Height => _height; - - /// - /// Gets the position of the rectangle. - /// - public Point Position => new Point(_x, _y); - - /// - /// Gets the size of the rectangle. - /// - public Size Size => new Size(_width, _height); - - /// - /// Gets the right position of the rectangle. - /// - public double Right => _x + _width; - - /// - /// Gets the bottom position of the rectangle. - /// - public double Bottom => _y + _height; - - /// - /// Gets the left position. - /// - public double Left => _x; - - /// - /// Gets the top position. - /// - public double Top => _y; - - /// - /// Gets the top left point of the rectangle. - /// - public Point TopLeft => new Point(_x, _y); - - /// - /// Gets the top right point of the rectangle. - /// - public Point TopRight => new Point(Right, _y); - - /// - /// Gets the bottom left point of the rectangle. - /// - public Point BottomLeft => new Point(_x, Bottom); - - /// - /// Gets the bottom right point of the rectangle. - /// - public Point BottomRight => new Point(Right, Bottom); - - /// - /// Gets the center point of the rectangle. - /// - public Point Center => new Point(_x + (_width / 2), _y + (_height / 2)); - - /// - /// Gets a value that indicates whether the rectangle is empty. - /// - // ReSharper disable CompareOfFloatsByEqualityOperator - public bool IsEmpty => _width == 0 && _height == 0; - // ReSharper restore CompareOfFloatsByEqualityOperator - - /// - /// Checks for equality between two s. - /// - /// The first rect. - /// The second rect. - /// True if the rects are equal; otherwise false. - public static bool operator ==(Rect left, Rect right) - { - return left.Equals(right); - } - - /// - /// Checks for inequality between two s. - /// - /// The first rect. - /// The second rect. - /// True if the rects are unequal; otherwise false. - public static bool operator !=(Rect left, Rect right) - { - return !(left == right); - } - - /// - /// Multiplies a rectangle by a scaling vector. - /// - /// The rectangle. - /// The vector scale. - /// The scaled rectangle. - public static Rect operator *(Rect rect, Vector scale) - { - return new Rect( - rect.X * scale.X, - rect.Y * scale.Y, - rect.Width * scale.X, - rect.Height * scale.Y); - } - - /// - /// Multiplies a rectangle by a scale. - /// - /// The rectangle. - /// The scale. - /// The scaled rectangle. - public static Rect operator *(Rect rect, double scale) - { - return new Rect( - rect.X * scale, - rect.Y * scale, - rect.Width * scale, - rect.Height * scale); - } - - /// - /// Divides a rectangle by a vector. - /// - /// The rectangle. - /// The vector scale. - /// The scaled rectangle. - public static Rect operator /(Rect rect, Vector scale) - { - return new Rect( - rect.X / scale.X, - rect.Y / scale.Y, - rect.Width / scale.X, - rect.Height / scale.Y); - } - - /// - /// Determines whether a point in in the bounds of the rectangle. - /// - /// The point. - /// true if the point is in the bounds of the rectangle; otherwise false. - public bool Contains(Point p) - { - return p.X >= _x && p.X <= _x + _width && - p.Y >= _y && p.Y <= _y + _height; - } - - /// - /// Determines whether the rectangle fully contains another rectangle. - /// - /// The rectangle. - /// true if the rectangle is fully contained; otherwise false. - public bool Contains(Rect r) - { - return Contains(r.TopLeft) && Contains(r.BottomRight); - } - - /// - /// Centers another rectangle in this rectangle. - /// - /// The rectangle to center. - /// The centered rectangle. - public Rect CenterRect(Rect rect) - { - return new Rect( - _x + ((_width - rect._width) / 2), - _y + ((_height - rect._height) / 2), - rect._width, - rect._height); - } - - /// - /// Inflates the rectangle. - /// - /// The thickness to be subtracted for each side of the rectangle. - /// The inflated rectangle. - public Rect Inflate(double thickness) - { - return Inflate(new Thickness(thickness)); - } - - /// - /// Inflates the rectangle. - /// - /// The thickness to be subtracted for each side of the rectangle. - /// The inflated rectangle. - public Rect Inflate(Thickness thickness) - { - return new Rect( - new Point(_x - thickness.Left, _y - thickness.Top), - Size.Inflate(thickness)); - } - - /// - /// Deflates the rectangle. - /// - /// The thickness to be subtracted for each side of the rectangle. - /// The deflated rectangle. - public Rect Deflate(double thickness) - { - return Deflate(new Thickness(thickness)); - } - - /// - /// Deflates the rectangle by a . - /// - /// The thickness to be subtracted for each side of the rectangle. - /// The deflated rectangle. - public Rect Deflate(Thickness thickness) - { - return new Rect( - new Point(_x + thickness.Left, _y + thickness.Top), - Size.Deflate(thickness)); - } - - /// - /// Returns a boolean indicating whether the rect is equal to the other given rect. - /// - /// The other rect to test equality against. - /// True if this rect is equal to other; False otherwise. - public bool Equals(Rect other) - { - // ReSharper disable CompareOfFloatsByEqualityOperator - return _x == other._x && - _y == other._y && - _width == other._width && - _height == other._height; - // ReSharper enable CompareOfFloatsByEqualityOperator - } - - /// - /// Returns a boolean indicating whether the given object is equal to this rectangle. - /// - /// The object to compare against. - /// True if the object is equal to this rectangle; false otherwise. - public override bool Equals(object? obj) => obj is Rect other && Equals(other); - - /// - /// Returns the hash code for this instance. - /// - /// The hash code. - public override int GetHashCode() - { - unchecked - { - int hash = 17; - hash = (hash * 23) + X.GetHashCode(); - hash = (hash * 23) + Y.GetHashCode(); - hash = (hash * 23) + Width.GetHashCode(); - hash = (hash * 23) + Height.GetHashCode(); - return hash; - } - } - - /// - /// Gets the intersection of two rectangles. - /// - /// The other rectangle. - /// The intersection. - public Rect Intersect(Rect rect) - { - var newLeft = (rect.X > X) ? rect.X : X; - var newTop = (rect.Y > Y) ? rect.Y : Y; - var newRight = (rect.Right < Right) ? rect.Right : Right; - var newBottom = (rect.Bottom < Bottom) ? rect.Bottom : Bottom; - - if ((newRight > newLeft) && (newBottom > newTop)) - { - return new Rect(newLeft, newTop, newRight - newLeft, newBottom - newTop); - } - else - { - return Empty; - } - } - - /// - /// Determines whether a rectangle intersects with this rectangle. - /// - /// The other rectangle. - /// - /// True if the specified rectangle intersects with this one; otherwise false. - /// - public bool Intersects(Rect rect) - { - return (rect.X < Right) && (X < rect.Right) && (rect.Y < Bottom) && (Y < rect.Bottom); - } - - /// - /// Returns the axis-aligned bounding box of a transformed rectangle. - /// - /// The transform. - /// The bounding box - public Rect TransformToAABB(Matrix matrix) - { - ReadOnlySpan points = stackalloc Point[4] - { - TopLeft.Transform(matrix), - TopRight.Transform(matrix), - BottomRight.Transform(matrix), - BottomLeft.Transform(matrix) - }; - - var left = double.MaxValue; - var right = double.MinValue; - var top = double.MaxValue; - var bottom = double.MinValue; - - foreach (var p in points) - { - if (p.X < left) left = p.X; - if (p.X > right) right = p.X; - if (p.Y < top) top = p.Y; - if (p.Y > bottom) bottom = p.Y; - } - - return new Rect(new Point(left, top), new Point(right, bottom)); - } - - /// - /// Translates the rectangle by an offset. - /// - /// The offset. - /// The translated rectangle. - public Rect Translate(Vector offset) - { - return new Rect(Position + offset, Size); - } - - /// - /// Normalizes the rectangle so both the and are positive, without changing the location of the rectangle - /// - /// Normalized Rect - /// - /// Empty rect will be return when Rect contains invalid values. Like NaN. - /// - public Rect Normalize() - { - Rect rect = this; - - if(double.IsNaN(rect.Right) || double.IsNaN(rect.Bottom) || - double.IsNaN(rect.X) || double.IsNaN(rect.Y) || - double.IsNaN(Height) || double.IsNaN(Width)) - { - return Rect.Empty; - } - - if (rect.Width < 0) - { - var x = X + Width; - var width = X - x; - - rect = rect.WithX(x).WithWidth(width); - } - - if (rect.Height < 0) - { - var y = Y + Height; - var height = Y - y; - - rect = rect.WithY(y).WithHeight(height); - } - - return rect; - } - - - /// - /// Gets the union of two rectangles. - /// - /// The other rectangle. - /// The union. - public Rect Union(Rect rect) - { - if (IsEmpty) - { - return rect; - } - else if (rect.IsEmpty) - { - return this; - } - else - { - var x1 = Math.Min(X, rect.X); - var x2 = Math.Max(Right, rect.Right); - var y1 = Math.Min(Y, rect.Y); - var y2 = Math.Max(Bottom, rect.Bottom); - - return new Rect(new Point(x1, y1), new Point(x2, y2)); - } - } - - /// - /// Returns a new with the specified X position. - /// - /// The x position. - /// The new . - public Rect WithX(double x) - { - return new Rect(x, _y, _width, _height); - } - - /// - /// Returns a new with the specified Y position. - /// - /// The y position. - /// The new . - public Rect WithY(double y) - { - return new Rect(_x, y, _width, _height); - } - - /// - /// Returns a new with the specified width. - /// - /// The width. - /// The new . - public Rect WithWidth(double width) - { - return new Rect(_x, _y, width, _height); - } - - /// - /// Returns a new with the specified height. - /// - /// The height. - /// The new . - public Rect WithHeight(double height) - { - return new Rect(_x, _y, _width, height); - } - - /// - /// Returns the string representation of the rectangle. - /// - /// The string representation of the rectangle. - public override string ToString() - { - return string.Format( - CultureInfo.InvariantCulture, - "{0}, {1}, {2}, {3}", - _x, - _y, - _width, - _height); - } - - /// - /// Parses a string. - /// - /// The string. - /// The parsed . - public static Rect Parse(string s) - { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect.")) - { - return new Rect( - tokenizer.ReadDouble(), - tokenizer.ReadDouble(), - tokenizer.ReadDouble(), - tokenizer.ReadDouble() - ); - } - } - } -} diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs deleted file mode 100644 index 4550dbd54b..0000000000 --- a/src/Avalonia.Visuals/RelativePoint.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; -using System.Globalization; - -using Avalonia.Animation.Animators; -using Avalonia.Utilities; - -namespace Avalonia -{ - /// - /// Defines the reference point units of an or - /// . - /// - public enum RelativeUnit - { - /// - /// The point is expressed as a fraction of the containing element's size. - /// - Relative, - - /// - /// The point is absolute (i.e. in pixels). - /// - Absolute, - } - - /// - /// Defines a point that may be defined relative to a containing element. - /// - public readonly struct RelativePoint : IEquatable - { - /// - /// A point at the top left of the containing element. - /// - public static readonly RelativePoint TopLeft = new RelativePoint(0, 0, RelativeUnit.Relative); - - /// - /// A point at the center of the containing element. - /// - public static readonly RelativePoint Center = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); - - /// - /// A point at the bottom right of the containing element. - /// - public static readonly RelativePoint BottomRight = new RelativePoint(1, 1, RelativeUnit.Relative); - - private readonly Point _point; - - private readonly RelativeUnit _unit; - - static RelativePoint() - { - Animation.Animation.RegisterAnimator(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType)); - } - - /// - /// Initializes a new instance of the struct. - /// - /// The X point. - /// The Y point - /// The unit. - public RelativePoint(double x, double y, RelativeUnit unit) - : this(new Point(x, y), unit) - { - } - - /// - /// Initializes a new instance of the struct. - /// - /// The point. - /// The unit. - public RelativePoint(Point point, RelativeUnit unit) - { - _point = point; - _unit = unit; - } - - /// - /// Gets the point. - /// - public Point Point => _point; - - /// - /// Gets the unit. - /// - public RelativeUnit Unit => _unit; - - /// - /// Checks for equality between two s. - /// - /// The first point. - /// The second point. - /// True if the points are equal; otherwise false. - public static bool operator ==(RelativePoint left, RelativePoint right) - { - return left.Equals(right); - } - - /// - /// Checks for inequality between two s. - /// - /// The first point. - /// The second point. - /// True if the points are unequal; otherwise false. - public static bool operator !=(RelativePoint left, RelativePoint right) - { - return !left.Equals(right); - } - - /// - /// Checks if the equals another object. - /// - /// The other object. - /// True if the objects are equal, otherwise false. - public override bool Equals(object? obj) => obj is RelativePoint other && Equals(other); - - /// - /// Checks if the equals another point. - /// - /// The other point. - /// True if the objects are equal, otherwise false. - public bool Equals(RelativePoint p) - { - return Unit == p.Unit && Point == p.Point; - } - - /// - /// Gets a hashcode for a . - /// - /// A hash code. - public override int GetHashCode() - { - unchecked - { - return (_point.GetHashCode() * 397) ^ (int)_unit; - } - } - - /// - /// Converts a into pixels. - /// - /// The size of the visual. - /// The origin point in pixels. - public Point ToPixels(Size size) - { - return _unit == RelativeUnit.Absolute ? - _point : - new Point(_point.X * size.Width, _point.Y * size.Height); - } - - /// - /// Parses a string. - /// - /// The string. - /// The parsed . - public static RelativePoint Parse(string s) - { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint.")) - { - var x = tokenizer.ReadString(); - var y = tokenizer.ReadString(); - - var unit = RelativeUnit.Absolute; - var scale = 1.0; - - if (x.EndsWith("%")) - { - if (!y.EndsWith("%")) - { - throw new FormatException("If one coordinate is relative, both must be."); - } - - x = x.TrimEnd('%'); - y = y.TrimEnd('%'); - unit = RelativeUnit.Relative; - scale = 0.01; - } - - return new RelativePoint( - double.Parse(x, CultureInfo.InvariantCulture) * scale, - double.Parse(y, CultureInfo.InvariantCulture) * scale, - unit); - } - } - - /// - /// Returns a String representing this RelativePoint instance. - /// - /// The string representation. - public override string ToString() - { - return _unit == RelativeUnit.Absolute ? - _point.ToString() : - string.Format(CultureInfo.InvariantCulture, "{0}%, {1}%", _point.X * 100, _point.Y * 100); - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/IDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/IDeferredRendererLock.cs deleted file mode 100644 index eab3dca58e..0000000000 --- a/src/Avalonia.Visuals/Rendering/IDeferredRendererLock.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Avalonia.Rendering -{ - public interface IDeferredRendererLock - { - IDisposable? TryLock(); - } -} diff --git a/src/Avalonia.Visuals/Rendering/IRenderLoop.cs b/src/Avalonia.Visuals/Rendering/IRenderLoop.cs deleted file mode 100644 index dd7442e7f8..0000000000 --- a/src/Avalonia.Visuals/Rendering/IRenderLoop.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Avalonia.Rendering -{ - /// - /// The application render loop. - /// - /// - /// The render loop is responsible for advancing the animation timer and updating the scene - /// graph for visible windows. - /// - public interface IRenderLoop - { - /// - /// Adds an update task. - /// - /// The update task. - /// - /// Registered update tasks will be polled on each tick of the render loop after the - /// animation timer has been pulsed. - /// - void Add(IRenderLoopTask i); - - /// - /// Removes an update task. - /// - /// The update task. - void Remove(IRenderLoopTask i); - } -} diff --git a/src/Avalonia.Visuals/Rendering/IRenderRoot.cs b/src/Avalonia.Visuals/Rendering/IRenderRoot.cs deleted file mode 100644 index 54e58bf39c..0000000000 --- a/src/Avalonia.Visuals/Rendering/IRenderRoot.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering -{ - /// - /// Represents the root of a renderable tree. - /// - public interface IRenderRoot : IVisual - { - /// - /// Gets the client size of the window. - /// - Size ClientSize { get; } - - /// - /// Gets the renderer for the window. - /// - IRenderer Renderer { get; } - - /// - /// The scaling factor to use in rendering. - /// - double RenderScaling { get; } - - /// - /// Creates a render target for the window. - /// - /// An . - IRenderTarget CreateRenderTarget(); - - /// - /// Adds a rectangle to the window's dirty region. - /// - /// The rectangle. - void Invalidate(Rect rect); - - /// - /// Converts a point from screen to client coordinates. - /// - /// The point in screen device coordinates. - /// The point in client coordinates. - Point PointToClient(PixelPoint point); - - /// - /// Converts a point from client to screen coordinates. - /// - /// The point in client coordinates. - /// The point in screen device coordinates. - PixelPoint PointToScreen(Point point); - } -} diff --git a/src/Avalonia.Visuals/Rendering/IRenderTimer.cs b/src/Avalonia.Visuals/Rendering/IRenderTimer.cs deleted file mode 100644 index d333e928a0..0000000000 --- a/src/Avalonia.Visuals/Rendering/IRenderTimer.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Avalonia.Rendering -{ - /// - /// Defines the interface implemented by an application render timer. - /// - public interface IRenderTimer - { - /// - /// Raised when the render timer ticks to signal a new frame should be drawn. - /// - /// - /// This event can be raised on any thread; it is the responsibility of the subscriber to - /// switch execution to the right thread. - /// - event Action Tick; - } -} diff --git a/src/Avalonia.Visuals/Rendering/IVisualBrushInitialize.cs b/src/Avalonia.Visuals/Rendering/IVisualBrushInitialize.cs deleted file mode 100644 index 00449c5344..0000000000 --- a/src/Avalonia.Visuals/Rendering/IVisualBrushInitialize.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Media; - -namespace Avalonia.Rendering -{ - /// - /// Internal interface for initializing controls that are to be used as the visual in a - /// . - /// - public interface IVisualBrushInitialize - { - /// - /// Ensures that the control is ready to use as the visual in a visual brush. - /// - void EnsureInitialized(); - } -} diff --git a/src/Avalonia.Visuals/Rendering/IVisualBrushRenderer.cs b/src/Avalonia.Visuals/Rendering/IVisualBrushRenderer.cs deleted file mode 100644 index 1cd6515635..0000000000 --- a/src/Avalonia.Visuals/Rendering/IVisualBrushRenderer.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Avalonia.Media; -using Avalonia.Platform; - -namespace Avalonia.Rendering -{ - /// - /// Defines a renderer used to render a visual brush to a bitmap. - /// - public interface IVisualBrushRenderer - { - /// - /// Gets the size of the intermediate render target to which the visual brush should be - /// drawn. - /// - /// The visual brush. - /// The size of the intermediate render target to create. - Size GetRenderTargetSize(IVisualBrush brush); - - /// - /// Renders a visual brush to a bitmap. - /// - /// The drawing context to render to. - /// The visual brush. - /// A bitmap containing the rendered brush. - void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush); - } -} diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs deleted file mode 100644 index 23016de148..0000000000 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ /dev/null @@ -1,351 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Logging; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering -{ - /// - /// A renderer which renders the state of the visual tree without an intermediate scene graph - /// representation. - /// - /// - /// The immediate renderer supports only clip-bound-based hit testing; a control's geometry is - /// not taken into account. - /// - public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer - { - private readonly IVisual _root; - private readonly IRenderRoot? _renderRoot; - private bool _updateTransformedBounds = true; - private IRenderTarget? _renderTarget; - - /// - /// Initializes a new instance of the class. - /// - /// The control to render. - public ImmediateRenderer(IVisual root) - { - _root = root ?? throw new ArgumentNullException(nameof(root)); - _renderRoot = root as IRenderRoot; - } - - private ImmediateRenderer(IVisual root, bool updateTransformedBounds) - { - _root = root ?? throw new ArgumentNullException(nameof(root)); - _renderRoot = root as IRenderRoot; - _updateTransformedBounds = updateTransformedBounds; - } - - /// - public bool DrawFps { get; set; } - - /// - public bool DrawDirtyRects { get; set; } - - /// - public event EventHandler? SceneInvalidated; - - /// - public void Paint(Rect rect) - { - if (_renderTarget == null) - { - _renderTarget = ((IRenderRoot)_root).CreateRenderTarget(); - } - - try - { - using (var context = new DrawingContext(_renderTarget.CreateDrawingContext(this))) - { - context.PlatformImpl.Clear(Colors.Transparent); - - using (context.PushTransformContainer()) - { - Render(context, _root, _root.Bounds); - } - - if (DrawDirtyRects) - { - var color = (uint)new Random().Next(0xffffff) | 0x44000000; - context.FillRectangle( - new SolidColorBrush(color), - rect); - } - - if (DrawFps) - { - RenderFps(context, _root.Bounds, null); - } - } - } - catch (RenderTargetCorruptedException ex) - { - Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex); - _renderTarget.Dispose(); - _renderTarget = null; - } - - SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect)); - } - - /// - public void Resized(Size size) - { - } - - /// - /// Renders a visual to a render target. - /// - /// The visual. - /// The render target. - public static void Render(IVisual visual, IRenderTarget target) - { - using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) - using (var context = new DrawingContext(target.CreateDrawingContext(renderer))) - { - renderer.Render(context, visual, visual.Bounds); - } - } - - /// - /// Renders a visual to a drawing context. - /// - /// The visual. - /// The drawing context. - public static void Render(IVisual visual, DrawingContext context) - { - using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) - { - renderer.Render(context, visual, visual.Bounds); - } - } - - /// - public void AddDirty(IVisual visual) - { - if (visual.Bounds != Rect.Empty) - { - var m = visual.TransformToVisual(_root); - - if (m.HasValue) - { - var bounds = new Rect(visual.Bounds.Size).TransformToAABB(m.Value); - - //use transformedbounds as previous render state of the visual bounds - //so we can invalidate old and new bounds of a control in case it moved/shrinked - if (visual.TransformedBounds.HasValue) - { - var trb = visual.TransformedBounds.Value; - var trBounds = trb.Bounds.TransformToAABB(trb.Transform); - - if (trBounds != bounds) - { - _renderRoot?.Invalidate(trBounds); - } - } - - _renderRoot?.Invalidate(bounds); - } - } - } - - /// - /// Ends the operation of the renderer. - /// - public void Dispose() - { - _renderTarget?.Dispose(); - } - - /// - public IEnumerable HitTest(Point p, IVisual root, Func filter) - { - return HitTest(root, p, filter); - } - - public IVisual? HitTestFirst(Point p, IVisual root, Func filter) - { - return HitTest(root, p, filter).FirstOrDefault(); - } - - /// - public void RecalculateChildren(IVisual visual) => AddDirty(visual); - - /// - public void Start() - { - } - - /// - public void Stop() - { - } - - /// - Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) - { - (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized(); - return brush.Visual?.Bounds.Size ?? Size.Empty; - } - - /// - void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) - { - var visual = brush.Visual; - Render(new DrawingContext(context), visual, visual.Bounds); - } - - internal static void Render(IVisual visual, DrawingContext context, bool updateTransformedBounds) - { - using var renderer = new ImmediateRenderer(visual, updateTransformedBounds); - renderer.Render(context, visual, visual.Bounds); - } - - private static void ClearTransformedBounds(IVisual visual) - { - foreach (var e in visual.GetSelfAndVisualDescendants()) - { - visual.TransformedBounds = null; - } - } - - private static Rect GetTransformedBounds(IVisual visual) - { - if (visual.RenderTransform == null) - { - return visual.Bounds; - } - else - { - var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); - var offset = Matrix.CreateTranslation(visual.Bounds.Position + origin); - var m = (-offset) * visual.RenderTransform.Value * (offset); - return visual.Bounds.TransformToAABB(m); - } - } - - private static IEnumerable HitTest( - IVisual visual, - Point p, - Func? filter) - { - _ = visual ?? throw new ArgumentNullException(nameof(visual)); - - if (filter?.Invoke(visual) != false) - { - bool containsPoint; - - if (visual is ICustomSimpleHitTest custom) - { - containsPoint = custom.HitTest(p); - } - else - { - containsPoint = visual.TransformedBounds?.Contains(p) == true; - } - - if ((containsPoint || !visual.ClipToBounds) && visual.VisualChildren.Count > 0) - { - foreach (var child in visual.VisualChildren.SortByZIndex()) - { - foreach (var result in HitTest(child, p, filter)) - { - yield return result; - } - } - } - - if (containsPoint) - { - yield return visual; - } - } - } - - private void Render(DrawingContext context, IVisual visual, Rect clipRect) - { - var opacity = visual.Opacity; - var clipToBounds = visual.ClipToBounds; - var bounds = new Rect(visual.Bounds.Size); - - if (visual.IsVisible && opacity > 0) - { - var m = Matrix.CreateTranslation(visual.Bounds.Position); - - var renderTransform = Matrix.Identity; - - if (visual.RenderTransform != null) - { - var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); - var offset = Matrix.CreateTranslation(origin); - renderTransform = (-offset) * visual.RenderTransform.Value * (offset); - } - - m = renderTransform * m; - - if (clipToBounds) - { - if (visual.RenderTransform != null) - { - clipRect = new Rect(visual.Bounds.Size); - } - else - { - clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size)); - } - } - - using (context.PushPostTransform(m)) - using (context.PushOpacity(opacity)) - using (clipToBounds -#pragma warning disable CS0618 // Type or member is obsolete - ? visual is IVisualWithRoundRectClip roundClipVisual - ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius)) - : context.PushClip(bounds) - : default(DrawingContext.PushedState)) -#pragma warning restore CS0618 // Type or member is obsolete - - using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState)) - using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState)) - using (context.PushTransformContainer()) - { - visual.Render(context); - -#pragma warning disable 0618 - var transformed = - new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform); -#pragma warning restore 0618 - - if (_updateTransformedBounds) - visual.TransformedBounds = transformed; - - foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)) - { - var childBounds = GetTransformedBounds(child); - - if (!child.ClipToBounds || clipRect.Intersects(childBounds)) - { - var childClipRect = child.RenderTransform == null - ? clipRect.Translate(-childBounds.Position) - : clipRect; - Render(context, child, childClipRect); - } - else if (_updateTransformedBounds) - { - ClearTransformedBounds(child); - } - } - } - } - - if (!visual.IsVisible && _updateTransformedBounds) - { - ClearTransformedBounds(visual); - } - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs deleted file mode 100644 index 45b62b843b..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Avalonia.Platform; -using Avalonia.Visuals.Media.Imaging; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents an bitmap blending mode push or pop. - /// - internal class BitmapBlendModeNode : IDrawOperation - { - /// - /// Initializes a new instance of the class that represents an - /// push. - /// - /// The to push. - public BitmapBlendModeNode(BitmapBlendingMode bitmapBlend) - { - BlendingMode = bitmapBlend; - } - - /// - /// Initializes a new instance of the class that represents an - /// pop. - /// - public BitmapBlendModeNode() - { - } - - /// - public Rect Bounds => Rect.Empty; - - /// - /// Gets the BitmapBlend to be pushed or null if the operation represents a pop. - /// - public BitmapBlendingMode? BlendingMode { get; } - - /// - public bool HitTest(Point p) => false; - - /// - /// Determines if this draw operation equals another. - /// - /// the how to compare - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(BitmapBlendingMode? blendingMode) => BlendingMode == blendingMode; - - /// - public void Render(IDrawingContextImpl context) - { - if (BlendingMode.HasValue) - { - context.PushBitmapBlendMode(BlendingMode.Value); - } - else - { - context.PopBitmapBlendMode(); - } - } - - public void Dispose() - { - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs deleted file mode 100644 index 9710ca6c3c..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ /dev/null @@ -1,479 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A drawing context which builds a scene graph. - /// - internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport - { - private readonly ISceneBuilder _sceneBuilder; - private VisualNode? _node; - private int _childIndex; - private int _drawOperationindex; - - /// - /// Initializes a new instance of the class. - /// - /// - /// A scene builder used for constructing child scenes for visual brushes. - /// - /// The scene layers. - public DeferredDrawingContextImpl(ISceneBuilder sceneBuilder, SceneLayers layers) - { - _sceneBuilder = sceneBuilder; - Layers = layers; - } - - /// - public Matrix Transform { get; set; } = Matrix.Identity; - - /// - /// Gets the layers in the scene being built. - /// - public SceneLayers Layers { get; } - - /// - /// Informs the drawing context of the visual node that is about to be rendered. - /// - /// The visual node. - /// - /// An object which when disposed will commit the changes to visual node. - /// - public UpdateState BeginUpdate(VisualNode node) - { - _ = node ?? throw new ArgumentNullException(nameof(node)); - - if (_node != null) - { - if (_childIndex < _node.Children.Count) - { - _node.ReplaceChild(_childIndex, node); - } - else - { - _node.AddChild(node); - } - - ++_childIndex; - } - - var state = new UpdateState(this, _node, _childIndex, _drawOperationindex); - _node = node; - _childIndex = _drawOperationindex = 0; - return state; - } - - /// - public void Clear(Color color) - { - // Cannot clear a deferred scene. - } - - /// - public void Dispose() - { - // Nothing to do here since we allocate no unmanaged resources. - } - - /// - /// Removes any remaining drawing operations from the visual node. - /// - /// - /// Drawing operations are updated in place, overwriting existing drawing operations if - /// they are different. Once drawing has completed for the current visual node, it is - /// possible that there are stale drawing operations at the end of the list. This method - /// trims these stale drawing operations. - /// - public void TrimChildren() - { - _node!.TrimChildren(_childIndex); - } - - /// - public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) - { - Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) - { - Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) - { - // This method is currently only used to composite layers so shouldn't be called here. - throw new NotSupportedException(); - } - - /// - public void DrawLine(IPen pen, Point p1, Point p2) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) - { - Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, - BoxShadows boxShadows = default) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) - { - Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, material, rect)) - { - Add(new ExperimentalAcrylicNode(Transform, material, rect)); - } - else - { - ++_drawOperationindex; - } - } - - public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) - { - Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); - } - else - { - ++_drawOperationindex; - } - } - - public void Custom(ICustomDrawOperation custom) - { - var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, custom)) - Add(new CustomDrawOperation(custom, Transform)); - else - ++_drawOperationindex; - } - - /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) - { - Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); - } - - else - { - ++_drawOperationindex; - } - } - public IDrawingContextLayerImpl CreateLayer(Size size) - { - throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); - } - - /// - public void PopClip() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new ClipNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PopGeometryClip() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new GeometryClipNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PopBitmapBlendMode() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new BitmapBlendModeNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PopOpacity() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new OpacityNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PopOpacityMask() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null, null)) - { - Add(new OpacityMaskNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushClip(Rect clip) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, clip)) - { - Add(new ClipNode(Transform, clip)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushClip(RoundedRect clip) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, clip)) - { - Add(new ClipNode(Transform, clip)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushGeometryClip(IGeometryImpl? clip) - { - if (clip is null) - return; - - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, clip)) - { - Add(new GeometryClipNode(Transform, clip)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushOpacity(double opacity) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(opacity)) - { - Add(new OpacityNode(opacity)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushOpacityMask(IBrush mask, Rect bounds) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(mask, bounds)) - { - Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(blendingMode)) - { - Add(new BitmapBlendModeNode(blendingMode)); - } - else - { - ++_drawOperationindex; - } - } - - public readonly struct UpdateState : IDisposable - { - public UpdateState( - DeferredDrawingContextImpl owner, - VisualNode? node, - int childIndex, - int drawOperationIndex) - { - Owner = owner; - Node = node; - ChildIndex = childIndex; - DrawOperationIndex = drawOperationIndex; - } - - public void Dispose() - { - Owner._node!.TrimDrawOperations(Owner._drawOperationindex); - - var dirty = Owner.Layers.GetOrAdd(Owner._node.LayerRoot!).Dirty; - - var drawOperations = Owner._node.DrawOperations; - var drawOperationsCount = drawOperations.Count; - - for (var i = 0; i < drawOperationsCount; i++) - { - dirty.Add(drawOperations[i].Item.Bounds); - } - - Owner._node = Node; - Owner._childIndex = ChildIndex; - Owner._drawOperationindex = DrawOperationIndex; - } - - public DeferredDrawingContextImpl Owner { get; } - public VisualNode? Node { get; } - public int ChildIndex { get; } - public int DrawOperationIndex { get; } - } - - private void Add(T node) where T : class, IDrawOperation - { - using (var refCounted = RefCountable.Create(node)) - { - Add(refCounted); - } - } - - private void Add(IRef node) - { - if (_drawOperationindex < _node!.DrawOperations.Count) - { - _node.ReplaceDrawOperation(_drawOperationindex, node); - } - else - { - _node.AddDrawOperation(node); - } - - ++_drawOperationindex; - } - - private IRef? NextDrawAs() where T : class, IDrawOperation - { - return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef : null; - } - - private IDictionary? CreateChildScene(IBrush? brush) - { - var visualBrush = brush as VisualBrush; - - if (visualBrush != null) - { - var visual = visualBrush.Visual; - - if (visual != null) - { - (visual as IVisualBrushInitialize)?.EnsureInitialized(); - var scene = new Scene(visual); - _sceneBuilder.UpdateAll(scene); - return new Dictionary { { visualBrush.Visual, scene } }; - } - } - - return null; - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs deleted file mode 100644 index 504256b932..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents an ellipse draw. - /// - internal class EllipseNode : BrushDrawOperation - { - public EllipseNode( - Matrix transform, - IBrush? brush, - IPen? pen, - Rect rect, - IDictionary? childScenes = null) - : base(rect.Inflate(pen?.Thickness ?? 0), transform) - { - Transform = transform; - Brush = brush?.ToImmutable(); - Pen = pen?.ToImmutable(); - Rect = rect; - ChildScenes = childScenes; - } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - - /// - /// Gets the stroke pen. - /// - public ImmutablePen? Pen { get; } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the rect of the ellipse to draw. - /// - public Rect Rect { get; } - - public override IDictionary? ChildScenes { get; } - - public bool Equals(Matrix transform, IBrush? brush, IPen? pen, Rect rect) - { - return transform == Transform && - Equals(brush, Brush) && - Equals(Pen, pen) && - rect.Equals(Rect); - } - - public override void Render(IDrawingContextImpl context) - { - context.DrawEllipse(Brush, Pen, Rect); - } - - public override bool HitTest(Point p) - { - if (!Transform.TryInvert(out Matrix inverted)) - { - return false; - } - - p *= inverted; - - var center = Rect.Center; - - var strokeThickness = Pen?.Thickness ?? 0; - - var rx = Rect.Width / 2 + strokeThickness / 2; - var ry = Rect.Height / 2 + strokeThickness / 2; - - var dx = p.X - center.X; - var dy = p.Y - center.Y; - - if (Math.Abs(dx) > rx || Math.Abs(dy) > ry) - { - return false; - } - - if (Brush != null) - { - return Contains(rx, ry); - } - else if (strokeThickness > 0) - { - bool inStroke = Contains(rx, ry); - - rx = Rect.Width / 2 - strokeThickness / 2; - ry = Rect.Height / 2 - strokeThickness / 2; - - bool inInner = Contains(rx, ry); - - return inStroke && !inInner; - } - - bool Contains(double radiusX, double radiusY) - { - var rx2 = radiusX * radiusX; - var ry2 = radiusY * radiusY; - - var distance = ry2 * dx * dx + rx2 * dy * dy; - - return distance <= rx2 * ry2; - } - - return false; - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs deleted file mode 100644 index 7de1035441..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents a geometry draw. - /// - internal class GeometryNode : BrushDrawOperation - { - /// - /// Initializes a new instance of the class. - /// - /// The transform. - /// The fill brush. - /// The stroke pen. - /// The geometry. - /// Child scenes for drawing visual brushes. - public GeometryNode(Matrix transform, - IBrush? brush, - IPen? pen, - IGeometryImpl geometry, - IDictionary? childScenes = null) - : base(geometry.GetRenderBounds(pen), transform) - { - Transform = transform; - Brush = brush?.ToImmutable(); - Pen = pen?.ToImmutable(); - Geometry = geometry; - ChildScenes = childScenes; - } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - - /// - /// Gets the stroke pen. - /// - public ImmutablePen? Pen { get; } - - /// - /// Gets the geometry to draw. - /// - public IGeometryImpl Geometry { get; } - - /// - public override IDictionary? ChildScenes { get; } - - /// - /// Determines if this draw operation equals another. - /// - /// The transform of the other draw operation. - /// The fill of the other draw operation. - /// The stroke of the other draw operation. - /// The geometry of the other draw operation. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(Matrix transform, IBrush? brush, IPen? pen, IGeometryImpl geometry) - { - return transform == Transform && - Equals(brush, Brush) && - Equals(Pen, pen) && - Equals(geometry, Geometry); - } - - /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawGeometry(Brush, Pen, Geometry); - } - - /// - public override bool HitTest(Point p) - { - if (Transform.HasInverse) - { - p *= Transform.Invert(); - return (Brush != null && Geometry.FillContains(p)) || - (Pen != null && Geometry.StrokeContains(Pen, p)); - } - - return false; - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs deleted file mode 100644 index d3da19d8c9..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Avalonia.Platform; -using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents an image draw. - /// - internal class ImageNode : DrawOperation - { - /// - /// Initializes a new instance of the class. - /// - /// The transform. - /// The image to draw. - /// The draw opacity. - /// The source rect. - /// The destination rect. - /// The bitmap interpolation mode. - public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) - : base(destRect, transform) - { - Transform = transform; - Source = source.Clone(); - Opacity = opacity; - SourceRect = sourceRect; - DestRect = destRect; - BitmapInterpolationMode = bitmapInterpolationMode; - SourceVersion = Source.Item.Version; - } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the image to draw. - /// - public IRef Source { get; } - - /// - /// Source bitmap Version - /// - public int SourceVersion { get; } - - /// - /// Gets the draw opacity. - /// - public double Opacity { get; } - - /// - /// Gets the source rect. - /// - public Rect SourceRect { get; } - - /// - /// Gets the destination rect. - /// - public Rect DestRect { get; } - - /// - /// Gets the bitmap interpolation mode. - /// - /// - /// The scaling mode. - /// - public BitmapInterpolationMode BitmapInterpolationMode { get; } - - /// - /// The bitmap blending mode. - /// - /// - /// The blending mode. - /// - public BitmapBlendingMode BitmapBlendingMode { get; } - - /// - /// Determines if this draw operation equals another. - /// - /// The transform of the other draw operation. - /// The image of the other draw operation. - /// The opacity of the other draw operation. - /// The source rect of the other draw operation. - /// The dest rect of the other draw operation. - /// The bitmap interpolation mode. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) - { - return transform == Transform && - Equals(source.Item, Source.Item) && - source.Item.Version == SourceVersion && - opacity == Opacity && - sourceRect == SourceRect && - destRect == DestRect && - bitmapInterpolationMode == BitmapInterpolationMode; - } - - /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); - } - - /// - public override bool HitTest(Point p) => Bounds.Contains(p); - - public override void Dispose() - { - Source?.Dispose(); - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs deleted file mode 100644 index 63c22efc3f..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ /dev/null @@ -1,466 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Threading; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// Builds a scene graph from a visual tree. - /// - public class SceneBuilder : ISceneBuilder - { - /// - public void UpdateAll(Scene scene) - { - _ = scene ?? throw new ArgumentNullException(nameof(scene)); - Dispatcher.UIThread.VerifyAccess(); - - UpdateSize(scene); - scene.Layers.GetOrAdd(scene.Root.Visual); - - using (var impl = new DeferredDrawingContextImpl(this, scene.Layers)) - using (var context = new DrawingContext(impl)) - { - var clip = new Rect(scene.Root.Visual.Bounds.Size); - Update(context, scene, (VisualNode)scene.Root, clip, true); - } - } - - /// - public bool Update(Scene scene, IVisual visual) - { - _ = scene ?? throw new ArgumentNullException(nameof(scene)); - _ = visual ?? throw new ArgumentNullException(nameof(visual)); - - Dispatcher.UIThread.VerifyAccess(); - - if (!scene.Root.Visual.IsVisible) - { - throw new AvaloniaInternalException("Cannot update the scene for an invisible root visual."); - } - - var node = (VisualNode?)scene.FindNode(visual); - - if (visual == scene.Root.Visual) - { - UpdateSize(scene); - } - - if (visual.VisualRoot == scene.Root.Visual) - { - if (node?.Parent != null && - visual.VisualParent != null && - node.Parent.Visual != visual.VisualParent) - { - // The control has changed parents. Remove the node and recurse into the new parent node. - ((VisualNode)node.Parent).RemoveChild(node); - Deindex(scene, node); - node = (VisualNode?)scene.FindNode(visual.VisualParent); - } - - if (visual.IsVisible) - { - // If the node isn't yet part of the scene, find the nearest ancestor that is. - node = node ?? FindExistingAncestor(scene, visual); - - // We don't need to do anything if this part of the tree has already been fully - // updated. - if (node != null && !node.SubTreeUpdated) - { - // If the control we've been asked to update isn't part of the scene then - // we're carrying out an add operation, so recurse and add all the - // descendents too. - var recurse = node.Visual != visual; - - using (var impl = new DeferredDrawingContextImpl(this, scene.Layers)) - using (var context = new DrawingContext(impl)) - { - var clip = new Rect(scene.Root.Visual.Bounds.Size); - - if (node.Parent != null) - { - context.PushPostTransform(node.Parent.Transform); - clip = node.Parent.ClipBounds; - } - - using (context.PushTransformContainer()) - { - Update(context, scene, node, clip, recurse); - } - } - - return true; - } - } - else - { - if (node != null) - { - // The control has been hidden so remove it from its parent and deindex the - // node and its descendents. - ((VisualNode?)node.Parent)?.RemoveChild(node); - Deindex(scene, node); - return true; - } - } - } - else if (node != null) - { - // The control has been removed so remove it from its parent and deindex the - // node and its descendents. - var trim = FindFirstDeadAncestor(scene, node); - ((VisualNode)trim.Parent!).RemoveChild(trim); - Deindex(scene, trim); - return true; - } - - return false; - } - - private static VisualNode? FindExistingAncestor(Scene scene, IVisual visual) - { - var node = scene.FindNode(visual); - - while (node == null && visual.IsVisible) - { - var parent = visual.VisualParent; - - if (parent is null) - return null; - - visual = parent; - node = scene.FindNode(visual); - } - - return visual.IsVisible ? (VisualNode?)node : null; - } - - private static VisualNode FindFirstDeadAncestor(Scene scene, IVisualNode node) - { - var parent = node.Parent; - - while (parent!.Visual.VisualRoot == null) - { - node = parent; - parent = node.Parent; - } - - return (VisualNode)node; - } - - private static object GetOrCreateChildNode(Scene scene, IVisual child, VisualNode parent) - { - var result = (VisualNode?)scene.FindNode(child); - - if (result != null && result.Parent != parent) - { - Deindex(scene, result); - result = null; - } - - return result ?? CreateNode(scene, child, parent); - } - - private static void Update(DrawingContext context, Scene scene, VisualNode node, Rect clip, bool forceRecurse) - { - var visual = node.Visual; - var opacity = visual.Opacity; - var clipToBounds = visual.ClipToBounds; -#pragma warning disable CS0618 // Type or member is obsolete - var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ? - roundRectClip.ClipToBoundsRadius : - default; -#pragma warning restore CS0618 // Type or member is obsolete - - var bounds = new Rect(visual.Bounds.Size); - var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; - - contextImpl.Layers.Find(node.LayerRoot!)?.Dirty.Add(node.Bounds); - - if (visual.IsVisible) - { - var m = node != scene.Root ? - Matrix.CreateTranslation(visual.Bounds.Position) : - Matrix.Identity; - - var renderTransform = Matrix.Identity; - - if (visual.RenderTransform != null) - { - var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); - var offset = Matrix.CreateTranslation(origin); - renderTransform = (-offset) * visual.RenderTransform.Value * (offset); - } - - m = renderTransform * m; - - using (contextImpl.BeginUpdate(node)) - using (context.PushPostTransform(m)) - using (context.PushTransformContainer()) - { - var globalBounds = bounds.TransformToAABB(contextImpl.Transform); - var clipBounds = clipToBounds ? - globalBounds.Intersect(clip) : - clip; - - forceRecurse = forceRecurse || - node.ClipBounds != clipBounds || - node.Opacity != opacity || - node.Transform != contextImpl.Transform; - - node.Transform = contextImpl.Transform; - node.ClipBounds = clipBounds; - node.ClipToBounds = clipToBounds; - node.LayoutBounds = globalBounds; - node.ClipToBoundsRadius = clipToBoundsRadius; - node.GeometryClip = visual.Clip?.PlatformImpl; - node.Opacity = opacity; - - // TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning. - node.OpacityMask = visual.OpacityMask?.ToImmutable(); - - if (ShouldStartLayer(visual)) - { - if (node.LayerRoot != visual) - { - MakeLayer(scene, node); - } - else - { - UpdateLayer(node, scene.Layers[node.LayerRoot]); - } - } - else if (node.LayerRoot == node.Visual && node.Parent != null) - { - ClearLayer(scene, node); - } - - if (node.ClipToBounds) - { - clip = clip.Intersect(node.ClipBounds); - } - - try - { - visual.Render(context); - } - catch { } - - var transformed = new TransformedBounds(new Rect(visual.Bounds.Size), clip, node.Transform); - visual.TransformedBounds = transformed; - - if (forceRecurse) - { - var visualChildren = (IList) visual.VisualChildren; - - node.TryPreallocateChildren(visualChildren.Count); - - if (visualChildren.Count == 1) - { - var childNode = GetOrCreateChildNode(scene, visualChildren[0], node); - Update(context, scene, (VisualNode)childNode, clip, forceRecurse); - } - else if (visualChildren.Count > 1) - { - var count = visualChildren.Count; - var sortedChildren = new (IVisual visual, int index)[count]; - - for (var i = 0; i < count; i++) - { - sortedChildren[i] = (visualChildren[i], i); - } - - // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. - Array.Sort(sortedChildren, (lhs, rhs) => - { - var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); - - return result == 0 ? lhs.index.CompareTo(rhs.index) : result; - }); - - foreach (var child in sortedChildren) - { - var childNode = GetOrCreateChildNode(scene, child.Item1, node); - Update(context, scene, (VisualNode)childNode, clip, forceRecurse); - } - } - - node.SubTreeUpdated = true; - contextImpl.TrimChildren(); - } - } - } - else - { - contextImpl.BeginUpdate(node).Dispose(); - } - } - - private void UpdateSize(Scene scene) - { - var renderRoot = scene.Root.Visual as IRenderRoot; - var newSize = renderRoot?.ClientSize ?? scene.Root.Visual.Bounds.Size; - - scene.Scaling = renderRoot?.RenderScaling ?? 1; - - if (scene.Size != newSize) - { - var oldSize = scene.Size; - - scene.Size = newSize; - - Rect horizontalDirtyRect = Rect.Empty; - Rect verticalDirtyRect = Rect.Empty; - - if (newSize.Width > oldSize.Width) - { - horizontalDirtyRect = new Rect(oldSize.Width, 0, newSize.Width - oldSize.Width, oldSize.Height); - } - - if (newSize.Height > oldSize.Height) - { - verticalDirtyRect = new Rect(0, oldSize.Height, newSize.Width, newSize.Height - oldSize.Height); - } - - foreach (var layer in scene.Layers) - { - layer.Dirty.Add(horizontalDirtyRect); - layer.Dirty.Add(verticalDirtyRect); - } - } - } - - private static VisualNode CreateNode(Scene scene, IVisual visual, VisualNode parent) - { - var node = new VisualNode(visual, parent); - node.LayerRoot = parent.LayerRoot; - scene.Add(node); - return node; - } - - private static void Deindex(Scene scene, VisualNode node) - { - var nodeChildren = node.Children; - var nodeChildrenCount = nodeChildren.Count; - - for (var i = 0; i < nodeChildrenCount; i++) - { - if (nodeChildren[i] is VisualNode visual) - { - Deindex(scene, visual); - } - } - - scene.Remove(node); - - node.SubTreeUpdated = true; - - scene.Layers[node.LayerRoot!].Dirty.Add(node.Bounds); - - node.Visual.TransformedBounds = null; - - if (node.LayerRoot == node.Visual && node.Visual != scene.Root.Visual) - { - scene.Layers.Remove(node.LayerRoot); - } - } - - private static void ClearLayer(Scene scene, VisualNode node) - { - var parent = (VisualNode)node.Parent!; - var oldLayerRoot = node.LayerRoot; - var newLayerRoot = parent.LayerRoot!; - var existingDirtyRects = scene.Layers[node.LayerRoot!].Dirty; - var newDirtyRects = scene.Layers[newLayerRoot].Dirty; - - existingDirtyRects.Coalesce(); - - foreach (var r in existingDirtyRects) - { - newDirtyRects.Add(r); - } - - var oldLayer = scene.Layers[oldLayerRoot!]; - PropagateLayer(node, scene.Layers[newLayerRoot], oldLayer); - scene.Layers.Remove(oldLayer); - } - - private static void MakeLayer(Scene scene, VisualNode node) - { - var oldLayerRoot = node.LayerRoot!; - var layer = scene.Layers.Add(node.Visual); - var oldLayer = scene.Layers[oldLayerRoot!]; - - UpdateLayer(node, layer); - PropagateLayer(node, layer, scene.Layers[oldLayerRoot]); - } - - private static void UpdateLayer(VisualNode node, SceneLayer layer) - { - layer.Opacity = node.Visual.Opacity; - - if (node.Visual.OpacityMask != null) - { - layer.OpacityMask = node.Visual.OpacityMask?.ToImmutable(); - layer.OpacityMaskRect = node.ClipBounds; - } - else - { - layer.OpacityMask = null; - layer.OpacityMaskRect = Rect.Empty; - } - - layer.GeometryClip = node.HasAncestorGeometryClip ? - CreateLayerGeometryClip(node) : - null; - } - - private static void PropagateLayer(VisualNode node, SceneLayer layer, SceneLayer oldLayer) - { - node.LayerRoot = layer.LayerRoot; - - layer.Dirty.Add(node.Bounds); - oldLayer.Dirty.Add(node.Bounds); - - foreach (VisualNode child in node.Children) - { - // If the child is not the start of a new layer, recurse. - if (child.LayerRoot != child.Visual) - { - PropagateLayer(child, layer, oldLayer); - } - } - } - - // HACK: Disabled layers because they're broken in current renderer. See #2244. - private static bool ShouldStartLayer(IVisual visual) => false; - - private static IGeometryImpl? CreateLayerGeometryClip(VisualNode node) - { - IGeometryImpl? result = null; - VisualNode? n = node; - - for (;;) - { - n = (VisualNode?)n!.Parent; - - if (n == null || (n.GeometryClip == null && !n.HasAncestorGeometryClip)) - { - break; - } - - if (n?.GeometryClip != null) - { - var transformed = n.GeometryClip.WithTransform(n.Transform); - - result = result == null ? transformed : result.Intersect(transformed); - } - } - - return result; - } - } -} diff --git a/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs deleted file mode 100644 index ee85d1e876..0000000000 --- a/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Avalonia.Utilities -{ - /// - /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region. - /// - /// The type of elements in the slice. - [DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))] - public readonly struct ReadOnlySlice : IReadOnlyList where T : struct - { - private readonly int _offset; - - /// - /// Gets an empty - /// - public static ReadOnlySlice Empty => new ReadOnlySlice(Array.Empty()); - - private readonly ReadOnlyMemory _buffer; - - public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { } - - public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length, int offset = 0) - { -#if DEBUG - if (start.CompareTo(0) < 0) - { - throw new ArgumentOutOfRangeException(nameof (start)); - } - - if (length.CompareTo(buffer.Length) > 0) - { - throw new ArgumentOutOfRangeException(nameof (length)); - } -#endif - - _buffer = buffer; - Start = start; - Length = length; - _offset = offset; - } - - /// - /// Gets the start. - /// - /// - /// The start. - /// - public int Start { get; } - - /// - /// Gets the end. - /// - /// - /// The end. - /// - public int End => Start + Length - 1; - - /// - /// Gets the length. - /// - /// - /// The length. - /// - public int Length { get; } - - /// - /// Gets a value that indicates whether this instance of is Empty. - /// - public bool IsEmpty => Length == 0; - - /// - /// The underlying span. - /// - public ReadOnlySpan Span => _buffer.Span.Slice(_offset, Length); - - /// - /// The underlying buffer. - /// - public ReadOnlyMemory Buffer => _buffer; - - /// - /// Returns a value to specified element of the slice. - /// - /// The index of the element to return. - /// The . - /// - /// Thrown when index less than 0 or index greater than or equal to . - /// - public T this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { -#if DEBUG - if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) - { - throw new ArgumentOutOfRangeException(nameof (index)); - } -#endif - return Span[index]; - } - } - - /// - /// Returns a sub slice of elements that start at the specified index and has the specified number of elements. - /// - /// The start of the sub slice. - /// The length of the sub slice. - /// A that contains the specified number of elements from the specified start. - public ReadOnlySlice AsSlice(int start, int length) - { - if (IsEmpty) - { - return this; - } - - if (length == 0) - { - return Empty; - } - - if (start < 0 || _offset + start > _buffer.Length - 1) - { - throw new ArgumentOutOfRangeException(nameof(start)); - } - - if (_offset + start + length > _buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, start, length, _offset); - } - - /// - /// Returns a specified number of contiguous elements from the start of the slice. - /// - /// The number of elements to return. - /// A that contains the specified number of elements from the start of this slice. - public ReadOnlySlice Take(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, Start, length, _offset); - } - - /// - /// Bypasses a specified number of elements in the slice and then returns the remaining elements. - /// - /// The number of elements to skip before returning the remaining elements. - /// A that contains the elements that occur after the specified index in this slice. - public ReadOnlySlice Skip(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, Start + length, Length - length, _offset + length); - } - - /// - /// Returns an enumerator for the slice. - /// - public ImmutableReadOnlyListStructEnumerator GetEnumerator() - { - return new ImmutableReadOnlyListStructEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - int IReadOnlyCollection.Count => Length; - - T IReadOnlyList.this[int index] => this[index]; - - public static implicit operator ReadOnlySlice(T[] array) - { - return new ReadOnlySlice(array); - } - - public static implicit operator ReadOnlySlice(ReadOnlyMemory memory) - { - return new ReadOnlySlice(memory); - } - - internal class ReadOnlySliceDebugView - { - private readonly ReadOnlySlice _readOnlySlice; - - public ReadOnlySliceDebugView(ReadOnlySlice readOnlySlice) - { - _readOnlySlice = readOnlySlice; - } - - public int Start => _readOnlySlice.Start; - - public int End => _readOnlySlice.End; - - public int Length => _readOnlySlice.Length; - - public bool IsEmpty => _readOnlySlice.IsEmpty; - - public ReadOnlySpan Items => _readOnlySlice.Span; - } - } -} diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs deleted file mode 100644 index 324b253a0f..0000000000 --- a/src/Avalonia.Visuals/Visual.cs +++ /dev/null @@ -1,649 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Specialized; -using Avalonia.Collections; -using Avalonia.Data; -using Avalonia.Logging; -using Avalonia.LogicalTree; -using Avalonia.Media; -using Avalonia.Metadata; -using Avalonia.Rendering; -using Avalonia.Utilities; -using Avalonia.VisualTree; - -#nullable enable - -namespace Avalonia -{ - /// - /// Base class for controls that provides rendering and related visual properties. - /// - /// - /// The class represents elements that have a visual on-screen - /// representation and stores all the information needed for an - /// to render the control. To traverse the visual tree, use the - /// extension methods defined in . - /// - [UsableDuringInitialization] - public class Visual : StyledElement, IVisual - { - /// - /// Defines the property. - /// - public static readonly DirectProperty BoundsProperty = - AvaloniaProperty.RegisterDirect(nameof(Bounds), o => o.Bounds); - - public static readonly DirectProperty TransformedBoundsProperty = - AvaloniaProperty.RegisterDirect( - nameof(TransformedBounds), - o => o.TransformedBounds); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ClipToBoundsProperty = - AvaloniaProperty.Register(nameof(ClipToBounds)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ClipProperty = - AvaloniaProperty.Register(nameof(Clip)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty IsVisibleProperty = - AvaloniaProperty.Register(nameof(IsVisible), true); - - /// - /// Defines the property. - /// - public static readonly StyledProperty OpacityProperty = - AvaloniaProperty.Register(nameof(Opacity), 1); - - /// - /// Defines the property. - /// - public static readonly StyledProperty OpacityMaskProperty = - AvaloniaProperty.Register(nameof(OpacityMask)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty RenderTransformProperty = - AvaloniaProperty.Register(nameof(RenderTransform)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty RenderTransformOriginProperty = - AvaloniaProperty.Register(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center); - - /// - /// Defines the property. - /// - public static readonly DirectProperty VisualParentProperty = - AvaloniaProperty.RegisterDirect(nameof(IVisual.VisualParent), o => o._visualParent); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ZIndexProperty = - AvaloniaProperty.Register(nameof(ZIndex)); - - private Rect _bounds; - private TransformedBounds? _transformedBounds; - private IRenderRoot? _visualRoot; - private IVisual? _visualParent; - - /// - /// Initializes static members of the class. - /// - static Visual() - { - AffectsRender( - BoundsProperty, - ClipProperty, - ClipToBoundsProperty, - IsVisibleProperty, - OpacityProperty); - RenderTransformProperty.Changed.Subscribe(RenderTransformChanged); - ZIndexProperty.Changed.Subscribe(ZIndexChanged); - } - - /// - /// Initializes a new instance of the class. - /// - public Visual() - { - // Disable transitions until we're added to the visual tree. - DisableTransitions(); - - var visualChildren = new AvaloniaList(); - visualChildren.ResetBehavior = ResetBehavior.Remove; - visualChildren.Validate = visual => ValidateVisualChild(visual); - visualChildren.CollectionChanged += VisualChildrenChanged; - VisualChildren = visualChildren; - } - - /// - /// Raised when the control is attached to a rooted visual tree. - /// - public event EventHandler? AttachedToVisualTree; - - /// - /// Raised when the control is detached from a rooted visual tree. - /// - public event EventHandler? DetachedFromVisualTree; - - /// - /// Gets the bounds of the control relative to its parent. - /// - public Rect Bounds - { - get { return _bounds; } - protected set { SetAndRaise(BoundsProperty, ref _bounds, value); } - } - - /// - /// Gets the bounds of the control relative to the window, accounting for rendering transforms. - /// - public TransformedBounds? TransformedBounds => _transformedBounds; - - /// - /// Gets or sets a value indicating whether the control should be clipped to its bounds. - /// - public bool ClipToBounds - { - get { return GetValue(ClipToBoundsProperty); } - set { SetValue(ClipToBoundsProperty, value); } - } - - /// - /// Gets or sets the geometry clip for this visual. - /// - public Geometry? Clip - { - get { return GetValue(ClipProperty); } - set { SetValue(ClipProperty, value); } - } - - /// - /// Gets a value indicating whether this control and all its parents are visible. - /// - public bool IsEffectivelyVisible - { - get - { - IVisual? node = this; - - while (node != null) - { - if (!node.IsVisible) - { - return false; - } - - node = node.VisualParent; - } - - return true; - } - } - - /// - /// Gets or sets a value indicating whether this control is visible. - /// - public bool IsVisible - { - get { return GetValue(IsVisibleProperty); } - set { SetValue(IsVisibleProperty, value); } - } - - /// - /// Gets or sets the opacity of the control. - /// - public double Opacity - { - get { return GetValue(OpacityProperty); } - set { SetValue(OpacityProperty, value); } - } - - /// - /// Gets or sets the opacity mask of the control. - /// - public IBrush? OpacityMask - { - get { return GetValue(OpacityMaskProperty); } - set { SetValue(OpacityMaskProperty, value); } - } - - /// - /// Gets or sets the render transform of the control. - /// - public ITransform? RenderTransform - { - get { return GetValue(RenderTransformProperty); } - set { SetValue(RenderTransformProperty, value); } - } - - /// - /// Gets or sets the transform origin of the control. - /// - public RelativePoint RenderTransformOrigin - { - get { return GetValue(RenderTransformOriginProperty); } - set { SetValue(RenderTransformOriginProperty, value); } - } - - /// - /// Gets or sets the Z index of the control. - /// - /// - /// Controls with a higher will appear in front of controls with - /// a lower ZIndex. If two controls have the same ZIndex then the control that appears - /// later in the containing element's children collection will appear on top. - /// - public int ZIndex - { - get { return GetValue(ZIndexProperty); } - set { SetValue(ZIndexProperty, value); } - } - - /// - /// Gets the control's child visuals. - /// - protected IAvaloniaList VisualChildren - { - get; - private set; - } - - /// - /// Gets the root of the visual tree, if the control is attached to a visual tree. - /// - protected IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot); - - /// - /// Gets a value indicating whether this control is attached to a visual root. - /// - bool IVisual.IsAttachedToVisualTree => VisualRoot != null; - - /// - /// Gets the control's child controls. - /// - IAvaloniaReadOnlyList IVisual.VisualChildren => VisualChildren; - - /// - /// Gets the control's parent visual. - /// - IVisual? IVisual.VisualParent => _visualParent; - - /// - /// Gets the root of the visual tree, if the control is attached to a visual tree. - /// - IRenderRoot? IVisual.VisualRoot => VisualRoot; - - TransformedBounds? IVisual.TransformedBounds - { - get { return _transformedBounds; } - set { SetAndRaise(TransformedBoundsProperty, ref _transformedBounds, value); } - } - - /// - /// Invalidates the visual and queues a repaint. - /// - public void InvalidateVisual() - { - VisualRoot?.Renderer?.AddDirty(this); - } - - /// - /// Renders the visual to a . - /// - /// The drawing context. - public virtual void Render(DrawingContext context) - { - Contract.Requires(context != null); - } - - /// - /// Indicates that a property change should cause to be - /// called. - /// - /// The properties. - /// - /// This method should be called in a control's static constructor with each property - /// on the control which when changed should cause a redraw. This is similar to WPF's - /// FrameworkPropertyMetadata.AffectsRender flag. - /// - [Obsolete("Use AffectsRender and specify the control type.")] - protected static void AffectsRender(params AvaloniaProperty[] properties) - { - AffectsRender(properties); - } - - /// - /// Indicates that a property change should cause to be - /// called. - /// - /// The control which the property affects. - /// The properties. - /// - /// This method should be called in a control's static constructor with each property - /// on the control which when changed should cause a redraw. This is similar to WPF's - /// FrameworkPropertyMetadata.AffectsRender flag. - /// - protected static void AffectsRender(params AvaloniaProperty[] properties) - where T : Visual - { - static void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is T sender) - { - sender.InvalidateVisual(); - } - } - - static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is T sender) - { - if (e.OldValue is IAffectsRender oldValue) - { - WeakEventHandlerManager.Unsubscribe(oldValue, nameof(oldValue.Invalidated), sender.AffectsRenderInvalidated); - } - - if (e.NewValue is IAffectsRender newValue) - { - WeakEventHandlerManager.Subscribe(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated); - } - - sender.InvalidateVisual(); - } - } - - foreach (var property in properties) - { - if (property.CanValueAffectRender()) - { - property.Changed.Subscribe(e => InvalidateAndSubscribe(e)); - } - else - { - property.Changed.Subscribe(e => Invalidate(e)); - } - } - } - - protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - base.LogicalChildrenCollectionChanged(sender, e); - VisualRoot?.Renderer?.RecalculateChildren(this); - } - - /// - /// Calls the method - /// for this control and all of its visual descendants. - /// - /// The event args. - protected virtual void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Attached to visual tree"); - - _visualRoot = e.Root; - - if (RenderTransform is IMutableTransform mutableTransform) - { - mutableTransform.Changed += RenderTransformChanged; - } - - EnableTransitions(); - OnAttachedToVisualTree(e); - AttachedToVisualTree?.Invoke(this, e); - InvalidateVisual(); - - var visualChildren = VisualChildren; - - if (visualChildren != null) - { - var visualChildrenCount = visualChildren.Count; - - for (var i = 0; i < visualChildrenCount; i++) - { - if (visualChildren[i] is Visual child) - { - child.OnAttachedToVisualTreeCore(e); - } - } - } - } - - /// - /// Calls the method - /// for this control and all of its visual descendants. - /// - /// The event args. - protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Detached from visual tree"); - - _visualRoot = null; - - if (RenderTransform is IMutableTransform mutableTransform) - { - mutableTransform.Changed -= RenderTransformChanged; - } - - DisableTransitions(); - OnDetachedFromVisualTree(e); - DetachedFromVisualTree?.Invoke(this, e); - e.Root?.Renderer?.AddDirty(this); - - var visualChildren = VisualChildren; - - if (visualChildren != null) - { - var visualChildrenCount = visualChildren.Count; - - for (var i = 0; i < visualChildrenCount; i++) - { - if (visualChildren[i] is Visual child) - { - child.OnDetachedFromVisualTreeCore(e); - } - } - } - } - - /// - /// Called when the control is added to a rooted visual tree. - /// - /// The event args. - protected virtual void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - } - - /// - /// Called when the control is removed from a rooted visual tree. - /// - /// The event args. - protected virtual void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - } - - /// - /// Called when the control's visual parent changes. - /// - /// The old visual parent. - /// The new visual parent. - protected virtual void OnVisualParentChanged(IVisual? oldParent, IVisual? newParent) - { - RaisePropertyChanged( - VisualParentProperty, - new Optional(oldParent), - new BindingValue(newParent), - BindingPriority.LocalValue); - } - - protected internal sealed override void LogBindingError(AvaloniaProperty property, Exception e) - { - // Don't log a binding error unless the control is attached to a logical tree. - if (((ILogical)this).IsAttachedToLogicalTree) - { - if (e is BindingChainException b && - string.IsNullOrEmpty(b.ExpressionErrorPoint) && - DataContext == null) - { - // The error occurred at the root of the binding chain and DataContext is null; - // don't log this - the DataContext probably hasn't been set up yet. - return; - } - - Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log( - this, - "Error in binding to {Target}.{Property}: {Message}", - this, - property, - e.Message); - } - } - - /// - /// Called when a visual's changes. - /// - /// The event args. - private static void RenderTransformChanged(AvaloniaPropertyChangedEventArgs e) - { - var sender = e.Sender as Visual; - - if (sender?.VisualRoot != null) - { - var oldValue = e.OldValue as Transform; - var newValue = e.NewValue as Transform; - - if (oldValue != null) - { - oldValue.Changed -= sender.RenderTransformChanged; - } - - if (newValue != null) - { - newValue.Changed += sender.RenderTransformChanged; - } - - sender.InvalidateVisual(); - } - } - - /// - /// Ensures a visual child is not null and not already parented. - /// - /// The visual child. - private static void ValidateVisualChild(IVisual c) - { - if (c == null) - { - throw new ArgumentNullException(nameof(c), "Cannot add null to VisualChildren."); - } - - if (c.VisualParent != null) - { - throw new InvalidOperationException("The control already has a visual parent."); - } - } - - /// - /// Called when the property changes on any control. - /// - /// The event args. - private static void ZIndexChanged(AvaloniaPropertyChangedEventArgs e) - { - var sender = e.Sender as IVisual; - var parent = sender?.VisualParent; - sender?.InvalidateVisual(); - parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); - } - - /// - /// Called when the 's event - /// is fired. - /// - /// The sender. - /// The event args. - private void RenderTransformChanged(object? sender, EventArgs e) - { - InvalidateVisual(); - } - - /// - /// Sets the visual parent of the Visual. - /// - /// The visual parent. - private void SetVisualParent(Visual? value) - { - if (_visualParent == value) - { - return; - } - - var old = _visualParent; - _visualParent = value; - - if (_visualRoot != null) - { - var e = new VisualTreeAttachmentEventArgs(old!, _visualRoot); - OnDetachedFromVisualTreeCore(e); - } - - if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) - { - var root = this.FindAncestorOfType() ?? - throw new AvaloniaInternalException("Visual is atached to visual tree but root could not be found."); - var e = new VisualTreeAttachmentEventArgs(_visualParent, root); - OnAttachedToVisualTreeCore(e); - } - - OnVisualParentChanged(old, value); - } - - private void AffectsRenderInvalidated(object? sender, EventArgs e) => InvalidateVisual(); - - /// - /// Called when the collection changes. - /// - /// The sender. - /// The event args. - private void VisualChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - SetVisualParent(e.NewItems!, this); - break; - - case NotifyCollectionChangedAction.Remove: - SetVisualParent(e.OldItems!, null); - break; - - case NotifyCollectionChangedAction.Replace: - SetVisualParent(e.OldItems!, null); - SetVisualParent(e.NewItems!, this); - break; - } - } - - private static void SetVisualParent(IList children, Visual? parent) - { - var count = children.Count; - - for (var i = 0; i < count; i++) - { - var visual = (Visual) children[i]!; - - visual.SetVisualParent(parent); - } - } - } -} diff --git a/src/Avalonia.Visuals/VisualExtensions.cs b/src/Avalonia.Visuals/VisualExtensions.cs deleted file mode 100644 index ff8a515db3..0000000000 --- a/src/Avalonia.Visuals/VisualExtensions.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using Avalonia.VisualTree; - -namespace Avalonia -{ - /// - /// Extension methods for . - /// - public static class VisualExtensions - { - /// - /// Converts a point from screen to client coordinates. - /// - /// The visual. - /// The point in screen coordinates. - /// The point in client coordinates. - public static Point PointToClient(this IVisual visual, PixelPoint point) - { - var root = visual.VisualRoot ?? - throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); - var rootPoint = root.PointToClient(point); - return root.TranslatePoint(rootPoint, visual)!.Value; - } - - /// - /// Converts a point from client to screen coordinates. - /// - /// The visual. - /// The point in client coordinates. - /// The point in screen coordinates. - public static PixelPoint PointToScreen(this IVisual visual, Point point) - { - var root = visual.VisualRoot ?? - throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); - var p = visual.TranslatePoint(point, root); - return visual.VisualRoot.PointToScreen(p!.Value); - } - - /// - /// Returns a transform that transforms the visual's coordinates into the coordinates - /// of the specified . - /// - /// The visual whose coordinates are to be transformed. - /// The visual to translate the coordinates to. - /// - /// A containing the transform or null if the visuals don't share a - /// common ancestor. - /// - public static Matrix? TransformToVisual(this IVisual from, IVisual to) - { - var common = from.FindCommonVisualAncestor(to); - - if (common != null) - { - var thisOffset = GetOffsetFrom(common, from); - var thatOffset = GetOffsetFrom(common, to); - - if (!thatOffset.TryInvert(out var thatOffsetInverted)) - { - return null; - } - - return thatOffsetInverted * thisOffset; - } - - return null; - } - - /// - /// Translates a point relative to this visual to coordinates that are relative to the specified visual. - /// - /// The visual. - /// The point value, as relative to this visual. - /// The visual to translate the given point into. - /// - /// A point value, now relative to the target visual rather than this source element, or null if the - /// two elements have no common ancestor. - /// - public static Point? TranslatePoint(this IVisual visual, Point point, IVisual relativeTo) - { - var transform = visual.TransformToVisual(relativeTo); - - if (transform.HasValue) - { - return point.Transform(transform.Value); - } - - return null; - } - - /// - /// Gets a transform from an ancestor to a descendent. - /// - /// The ancestor visual. - /// The visual. - /// The transform. - private static Matrix GetOffsetFrom(IVisual ancestor, IVisual visual) - { - var result = Matrix.Identity; - IVisual? v = visual; - - while (v != ancestor) - { - if (v.RenderTransform?.Value != null) - { - var origin = v.RenderTransformOrigin.ToPixels(v.Bounds.Size); - var offset = Matrix.CreateTranslation(origin); - var renderTransform = (-offset) * v.RenderTransform.Value * (offset); - - result *= renderTransform; - } - - var topLeft = v.Bounds.TopLeft; - - if (topLeft != default) - { - result *= Matrix.CreateTranslation(topLeft); - } - - v = v.VisualParent; - - if (v == null) - { - throw new ArgumentException("'visual' is not a descendant of 'ancestor'."); - } - } - - return result; - } - } -} diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs deleted file mode 100644 index 97c4554de6..0000000000 --- a/src/Avalonia.Visuals/VisualTree/IVisual.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using Avalonia.Collections; -using Avalonia.Media; -using Avalonia.Rendering; - -#nullable enable - -namespace Avalonia.VisualTree -{ - /// - /// Represents control that has a visual on-screen representation. - /// - /// - /// The interface defines the interface required for a renderer to - /// render a control. You should not usually need to reference this interface unless - /// you are writing a renderer; instead use the extension methods defined in - /// to traverse the visual tree. This interface is - /// implemented by . It should not be necessary to implement it - /// anywhere else. - /// - public interface IVisual - { - /// - /// Raised when the control is attached to a rooted visual tree. - /// - event EventHandler? AttachedToVisualTree; - - /// - /// Raised when the control is detached from a rooted visual tree. - /// - event EventHandler? DetachedFromVisualTree; - - /// - /// Gets the bounds of the control relative to its parent. - /// - Rect Bounds { get; } - - /// - /// Gets the bounds of the control relative to the window, accounting for rendering transforms. - /// - TransformedBounds? TransformedBounds { get; set; } - - /// - /// Gets a value indicating whether the control should be clipped to its bounds. - /// - bool ClipToBounds { get; set; } - - /// - /// Gets or sets the geometry clip for this visual. - /// - Geometry? Clip { get; set; } - - /// - /// Gets a value indicating whether this control is attached to a visual root. - /// - bool IsAttachedToVisualTree { get; } - - /// - /// Gets a value indicating whether this control and all its parents are visible. - /// - bool IsEffectivelyVisible { get; } - - /// - /// Gets or sets a value indicating whether this control is visible. - /// - bool IsVisible { get; set; } - - /// - /// Gets or sets the opacity of the control. - /// - double Opacity { get; set; } - - /// - /// Gets or sets the opacity mask for the control. - /// - IBrush? OpacityMask { get; set; } - - /// - /// Gets or sets the render transform of the control. - /// - ITransform? RenderTransform { get; set; } - - /// - /// Gets or sets the render transform origin of the control. - /// - RelativePoint RenderTransformOrigin { get; set; } - - /// - /// Gets the control's child visuals. - /// - IAvaloniaReadOnlyList VisualChildren { get; } - - /// - /// Gets the control's parent visual. - /// - IVisual? VisualParent { get; } - - /// - /// Gets the root of the visual tree, if the control is attached to a visual tree. - /// - IRenderRoot? VisualRoot { get; } - - /// - /// Gets or sets the Z index of the node. - /// - int ZIndex { get; set; } - - /// - /// Invalidates the visual and queues a repaint. - /// - void InvalidateVisual(); - - /// - /// Renders the control to a . - /// - /// The context. - void Render(DrawingContext context); - } -} diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index fcdc10e999..1e70608168 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -9,7 +9,7 @@ namespace Avalonia.X11.Glx unsafe class GlxDisplay { private readonly X11Info _x11; - private readonly List _probeProfiles; + private readonly GlVersion[] _probeProfiles; private readonly IntPtr _fbconfig; private readonly XVisualInfo* _visual; private string[] _displayExtensions; @@ -21,7 +21,7 @@ namespace Avalonia.X11.Glx public GlxDisplay(X11Info x11, IList probeProfiles) { _x11 = x11; - _probeProfiles = probeProfiles.ToList(); + _probeProfiles = probeProfiles.ToArray(); _displayExtensions = Glx.GetExtensions(_x11.Display); var baseAttribs = new[] @@ -76,10 +76,10 @@ namespace Avalonia.X11.Glx if (Glx.GetFBConfigAttrib(_x11.Display, _fbconfig, GLX_STENCIL_SIZE, out var stencil) == 0) stencilSize = stencil; - var pbuffers = Enumerable.Range(0, 2).Select(_ => Glx.CreatePbuffer(_x11.Display, _fbconfig, new[] - { - GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 - })).ToList(); + var attributes = new[] { GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 }; + + Glx.CreatePbuffer(_x11.Display, _fbconfig, attributes); + Glx.CreatePbuffer(_x11.Display, _fbconfig, attributes); XLib.XFlush(_x11.Display); @@ -104,7 +104,6 @@ namespace Avalonia.X11.Glx $"Renderer '{glInterface.Renderer}' is blacklisted by '{item}'"); } } - } IntPtr CreatePBuffer() diff --git a/src/Avalonia.X11/ICELib.cs b/src/Avalonia.X11/ICELib.cs index 8ef21dd000..19c973a5e0 100644 --- a/src/Avalonia.X11/ICELib.cs +++ b/src/Avalonia.X11/ICELib.cs @@ -46,7 +46,7 @@ namespace Avalonia.X11 IntPtr iceConn, bool swap, int offendingMinorOpcode, - ulong offendingSequence, + nuint offendingSequence, int errorClass, int severity, IntPtr values diff --git a/src/Avalonia.X11/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/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index 6632573ba2..36939f4103 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -2,12 +2,9 @@ using System; using System.IO; using System.Runtime.InteropServices; using Avalonia.Controls.Platform.Surfaces; -using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; -using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; -using static Avalonia.X11.XLib; + namespace Avalonia.X11 { class X11IconLoader : IPlatformIconLoader diff --git a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs index 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/Avalonia.Markup.Xaml.Loader.csproj b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj index f75c96c759..b89ea8399a 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj @@ -12,5 +12,6 @@ - + + diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs index ece90762cb..1e2a77c34d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs @@ -177,11 +177,13 @@ namespace Avalonia.Markup.Xaml.XamlIl var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); var clrPropertyBuilder = tb.DefineNestedType("ClrProperties_" + Guid.NewGuid().ToString("N")); var indexerClosureType = _sreBuilder.DefineType("IndexerClosure_" + Guid.NewGuid().ToString("N")); + var trampolineBuilder = _sreBuilder.DefineType("Trampolines_" + Guid.NewGuid().ToString("N")); var compiler = new AvaloniaXamlIlCompiler(new AvaloniaXamlIlCompilerConfiguration(_sreTypeSystem, asm, _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter, new XamlIlClrPropertyInfoEmitter(_sreTypeSystem.CreateTypeBuilder(clrPropertyBuilder)), - new XamlIlPropertyInfoAccessorFactoryEmitter(_sreTypeSystem.CreateTypeBuilder(indexerClosureType))), + new XamlIlPropertyInfoAccessorFactoryEmitter(_sreTypeSystem.CreateTypeBuilder(indexerClosureType)), + new XamlIlTrampolineBuilder(_sreTypeSystem.CreateTypeBuilder(trampolineBuilder))), _sreEmitMappings, _sreContextType) { EnableIlVerification = true }; @@ -196,6 +198,7 @@ namespace Avalonia.Markup.Xaml.XamlIl compiler.ParseAndCompile(xaml, uri?.ToString(), null, _sreTypeSystem.CreateTypeBuilder(tb), overrideType); var created = tb.CreateTypeInfo(); clrPropertyBuilder.CreateTypeInfo(); + trampolineBuilder.CreateTypeInfo(); return LoadOrPopulate(created, rootInstance); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs index f6f47dce0d..9fc6b5d517 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs @@ -7,6 +7,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { public XamlIlClrPropertyInfoEmitter ClrPropertyEmitter { get; } public XamlIlPropertyInfoAccessorFactoryEmitter AccessorFactoryEmitter { get; } + public XamlIlTrampolineBuilder TrampolineBuilder { get; } public AvaloniaXamlIlCompilerConfiguration(IXamlTypeSystem typeSystem, IXamlAssembly defaultAssembly, @@ -15,13 +16,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XamlValueConverter customValueConverter, XamlIlClrPropertyInfoEmitter clrPropertyEmitter, XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter, + XamlIlTrampolineBuilder trampolineBuilder, IXamlIdentifierGenerator identifierGenerator = null) : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter, identifierGenerator) { ClrPropertyEmitter = clrPropertyEmitter; AccessorFactoryEmitter = accessorFactoryEmitter; + TrampolineBuilder = trampolineBuilder; AddExtra(ClrPropertyEmitter); AddExtra(AccessorFactoryEmitter); + AddExtra(TrampolineBuilder); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index 1db0208310..8ed94f6b20 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -39,6 +39,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { typeSystem.GetType("Avalonia.Metadata.ContentAttribute") }, + WhitespaceSignificantCollectionAttributes = + { + typeSystem.GetType("Avalonia.Metadata.WhitespaceSignificantCollectionAttribute") + }, + TrimSurroundingWhitespaceAttributes = + { + typeSystem.GetType("Avalonia.Metadata.TrimSurroundingWhitespaceAttribute") + }, ProvideValueTarget = typeSystem.GetType("Avalonia.Markup.Xaml.IProvideValueTarget"), RootObjectProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IRootObjectProvider"), RootObjectProviderIntermediateRootPropertyName = "IntermediateRootObject", diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 4592b9c8b4..d907bcbef9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -160,6 +160,29 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return true; } + if (type.Equals(types.RelativePoint)) + { + try + { + var relativePoint = RelativePoint.Parse(text); + + var relativePointTypeRef = new XamlAstClrTypeReference(node, types.RelativePoint, false); + + result = new XamlAstNewClrObjectNode(node, relativePointTypeRef, types.RelativePointFullConstructor, new List + { + new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.X), + new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.Y), + new XamlConstantNode(node, types.RelativeUnit, (int) relativePoint.Unit), + }); + + return true; + } + catch + { + throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a relative point", node); + } + } + if (type.Equals(types.GridLength)) { try @@ -201,7 +224,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions if (type.Equals(types.Classes)) { var classes = text.Split(' '); - var classNodes = classes.Select(c => new XamlAstTextNode(node, c, types.XamlIlTypes.String)).ToArray(); + var classNodes = classes.Select(c => new XamlAstTextNode(node, c, type: types.XamlIlTypes.String)).ToArray(); result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, types.Classes, types.XamlIlTypes.String, classNodes); return true; @@ -221,6 +244,32 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } + if (type.Equals(types.TextTrimming)) + { + foreach (var property in types.TextTrimming.Properties) + { + if (property.PropertyType == types.TextTrimming && property.Name.Equals(text, StringComparison.OrdinalIgnoreCase)) + { + result = new XamlStaticOrTargetedReturnMethodCallNode(node, property.Getter, Enumerable.Empty()); + + return true; + } + } + } + + if (type.Equals(types.TextDecorationCollection)) + { + foreach (var property in types.TextDecorations.Properties) + { + if (property.PropertyType == types.TextDecorationCollection && property.Name.Equals(text, StringComparison.OrdinalIgnoreCase)) + { + result = new XamlStaticOrTargetedReturnMethodCallNode(node, property.Getter, Enumerable.Empty()); + + return true; + } + } + } + result = null; return false; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs index 048a6220c5..3cc3504e16 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -117,7 +117,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return parentDataContextNode.DataContextType; }; - XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startTypeResolver, context.ParentNodes().OfType().First().Type.GetClrType()); + var selfType = context.ParentNodes().OfType().First().Type.GetClrType(); + + // When using self bindings with setters we need to change target type to resolved selector type. + if (context.GetAvaloniaTypes().ISetter.IsAssignableFrom(selfType)) + { + selfType = context.ParentNodes().OfType().First().TargetType.GetClrType(); + } + + XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startTypeResolver, selfType); } return node; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index cf691db860..d4dfcf9c4b 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -49,6 +49,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } else if (child is XamlPropertyAssignmentNode pa) { + var templateDataTypeAttribute = context.GetAvaloniaTypes().DataTypeAttribute; + if (pa.Property.Name == "DataContext" && pa.Property.DeclaringType.Equals(context.GetAvaloniaTypes().StyledElement) && pa.Values[0] is XamlMarkupExtensionNode ext @@ -56,8 +58,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { inferredDataContextTypeNode = ParseDataContext(context, on, obj); } - else if(context.GetAvaloniaTypes().DataTemplate.IsAssignableFrom(on.Type.GetClrType()) - && pa.Property.Name == "DataType" + else if(pa.Property.CustomAttributes.Any(a => a.Type == templateDataTypeAttribute) && pa.Values[0] is XamlTypeExtensionNode dataTypeNode) { inferredDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, dataTypeNode.Value.GetClrType()); 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..70209fb3ad 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()}", @@ -151,6 +151,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers results.Add(result); result = initialNode; break; + case SelectorGrammar.NestingSyntax: + var parentTargetType = context.ParentNodes().OfType().FirstOrDefault(); + + if (parentTargetType is null) + throw new XamlParseException($"Cannot find parent style for nested selector.", node); + + result = new XamlIlNestingSelector(result, parentTargetType.TargetType.GetClrType()); + break; default: throw new XamlParseException($"Unsupported selector grammar '{i.GetType()}'.", node); } @@ -474,4 +482,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers m => m.Name == "Or" && m.Parameters.Count == 1 && m.Parameters[0].Name.StartsWith("IReadOnlyList")); } } + + class XamlIlNestingSelector : XamlIlSelectorNode + { + public XamlIlNestingSelector(XamlIlSelectorNode previous, IXamlType targetType) + : base(previous) + { + TargetType = targetType; + } + + public override IXamlType TargetType { get; } + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) + { + EmitCall(context, codeGen, + m => m.Name == "Nesting" && m.Parameters.Count == 1); + } + } } 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..28787d9b84 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,13 @@ 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 DataTypeAttribute { get; } public IXamlType UnsetValueType { get; } public IXamlType StyledElement { get; } public IXamlType IStyledElement { get; } @@ -69,6 +72,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlConstructor MatrixFullConstructor { get; } public IXamlType CornerRadius { get; } public IXamlConstructor CornerRadiusFullConstructor { get; } + public IXamlType RelativeUnit { get; } + public IXamlType RelativePoint { get; } + public IXamlConstructor RelativePointFullConstructor { get; } public IXamlType GridLength { get; } public IXamlConstructor GridLengthConstructorValueType { get; } public IXamlType Color { get; } @@ -85,6 +91,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 +109,11 @@ 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"); + DataTypeAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DataTypeAttribute"); AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, IAvaloniaObject, AvaloniaProperty, IBinding, cfg.WellKnownTypes.Object); @@ -126,7 +140,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers NameScopeSetNameScope = NameScope.GetMethod(new FindMethodMethodSignature("SetNameScope", XamlIlTypes.Void, StyledElement, INameScope) { IsStatic = true }); - AvaloniaObjectSetValueMethod = AvaloniaObject.FindMethod("SetValue", XamlIlTypes.Void, + AvaloniaObjectSetValueMethod = AvaloniaObject.FindMethod("SetValue", IDisposable, false, AvaloniaProperty, XamlIlTypes.Object, BindingPriority); IPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.IPropertyInfo"); ClrPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.ClrPropertyInfo"); @@ -166,6 +180,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers (Matrix, MatrixFullConstructor) = GetNumericTypeInfo("Avalonia.Matrix", XamlIlTypes.Double, 6); (CornerRadius, CornerRadiusFullConstructor) = GetNumericTypeInfo("Avalonia.CornerRadius", XamlIlTypes.Double, 4); + RelativeUnit = cfg.TypeSystem.GetType("Avalonia.RelativeUnit"); + RelativePoint = cfg.TypeSystem.GetType("Avalonia.RelativePoint"); + RelativePointFullConstructor = RelativePoint.GetConstructor(new List { XamlIlTypes.Double, XamlIlTypes.Double, RelativeUnit }); + GridLength = cfg.TypeSystem.GetType("Avalonia.Controls.GridLength"); GridLengthConstructorValueType = GridLength.GetConstructor(new List { XamlIlTypes.Double, cfg.TypeSystem.GetType("Avalonia.Controls.GridUnitType") }); Color = cfg.TypeSystem.GetType("Avalonia.Media.Color"); @@ -187,6 +205,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/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs index 871a2a2045..e76e2cd46e 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs @@ -55,7 +55,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return cached.get; } - var name = lst.Count == 0 ? key : key + "_" + Guid.NewGuid().ToString("N"); + var name = lst.Count == 0 ? key : key + "_" + context.Configuration.IdentifierGenerator.GenerateIdentifierPart(); var field = _builder.DefineField(types.IPropertyInfo, name + "!Field", false, true); 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..30d321426f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -18,8 +18,10 @@ + + @@ -54,16 +56,15 @@ - - - - - - + + + + + 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.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index db33b88cc3..add97a660b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -39,17 +39,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions targetType = setter.Property.PropertyType; } - // Look upwards though the ambient context for IResourceHosts and IResourceProviders + // Look upwards though the ambient context for IResourceNodes // which might be able to give us the resource. - foreach (var e in stack.Parents) + foreach (var parent in stack.Parents) { - object value; - - if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value)) - { - return ColorToBrushConverter.Convert(value, targetType); - } - else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value)) + if (parent is IResourceNode node && node.TryGetResource(ResourceKey, out var value)) { return ColorToBrushConverter.Convert(value, targetType); } @@ -58,7 +52,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (provideTarget.TargetObject is IControl target && provideTarget.TargetProperty is PropertyInfo property) { - DelayedBinding.Add(target, property, x => GetValue(x, targetType)); + // This is stored locally to avoid allocating closure in the outer scope. + var localTargetType = targetType; + var localInstance = this; + + DelayedBinding.Add(target, property, x => localInstance.GetValue(x, localTargetType)); return AvaloniaProperty.UnsetValue; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Properties/AssemblyInfo.cs b/src/Markup/Avalonia.Markup.Xaml/Properties/AssemblyInfo.cs index cee4d90917..7e5b3159d4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Properties/AssemblyInfo.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Properties/AssemblyInfo.cs @@ -1,9 +1,6 @@ using Avalonia.Metadata; -using System.Runtime.CompilerServices; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Xaml.MarkupExtensions")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Xaml.Styling")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Xaml.Templates")] -[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 607b552c28..fa4a27fc50 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -60,7 +60,7 @@ namespace Avalonia.Markup.Xaml.Styling } } - bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; + bool IResourceNode.HasResources => Loaded?.HasResources ?? false; IReadOnlyList IStyle.Children => _loaded ?? Array.Empty(); @@ -86,9 +86,9 @@ namespace Avalonia.Markup.Xaml.Styling public bool TryGetResource(object key, out object? value) { - if (!_isLoading && Loaded is IResourceProvider p) + if (!_isLoading) { - return p.TryGetResource(key, out value); + return Loaded.TryGetResource(key, out value); } value = null; diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index b7db1a3fbb..d2b24979cc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -7,6 +7,7 @@ namespace Avalonia.Markup.Xaml.Templates { public class DataTemplate : IRecyclingDataTemplate { + [DataType] public Type DataType { get; set; } [Content] diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index 7b065c7f47..10061c3d48 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -11,6 +11,7 @@ namespace Avalonia.Markup.Xaml.Templates { public class TreeDataTemplate : ITreeDataTemplate { + [DataType] public Type DataType { get; set; } [Content] diff --git a/src/Markup/Avalonia.Markup/ApiCompatBaseline.txt b/src/Markup/Avalonia.Markup/ApiCompatBaseline.txt new file mode 100644 index 0000000000..fcc74cf864 --- /dev/null +++ b/src/Markup/Avalonia.Markup/ApiCompatBaseline.txt @@ -0,0 +1 @@ +Total Issues: 0 diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 21370b3bf5..6711c3dd3d 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -11,11 +11,14 @@ - - + + + + + diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index a9fc18474c..16856e674d 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -46,7 +46,7 @@ namespace Avalonia.Markup.Parsers switch (state) { case State.Start: - state = ParseStart(ref r); + (state, syntax) = ParseStart(ref r); break; case State.Middle: (state, syntax) = ParseMiddle(ref r, end); @@ -93,27 +93,31 @@ namespace Avalonia.Markup.Parsers return selector; } - private static State ParseStart(ref CharacterReader r) + private static (State, ISyntax?) ParseStart(ref CharacterReader r) { r.SkipWhitespace(); if (r.End) { - return State.End; + return (State.End, null); } if (r.TakeIf(':')) { - return State.Colon; + return (State.Colon, null); } else if (r.TakeIf('.')) { - return State.Class; + return (State.Class, null); } else if (r.TakeIf('#')) { - return State.Name; + return (State.Name, null); + } + else if (r.TakeIf('^')) + { + return (State.CanHaveType, new NestingSyntax()); } - return State.TypeName; + return (State.TypeName, null); } private static (State, ISyntax?) ParseMiddle(ref CharacterReader r, char? end) @@ -142,6 +146,10 @@ namespace Avalonia.Markup.Parsers { return (State.Start, new CommaSyntax()); } + else if (r.TakeIf('^')) + { + return (State.CanHaveType, new NestingSyntax()); + } else if (end.HasValue && !r.End && r.Peek == end.Value) { return (State.End, null); @@ -635,5 +643,13 @@ namespace Avalonia.Markup.Parsers return obj is CommaSyntax or; } } + + public class NestingSyntax : ISyntax + { + public override bool Equals(object? obj) + { + return obj is NestingSyntax; + } + } } } diff --git a/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs b/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs index 46e2925f92..36ca2784da 100644 --- a/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs +++ b/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs @@ -1,8 +1,5 @@ using Avalonia.Metadata; -using System.Runtime.CompilerServices; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Data")] -[assembly: InternalsVisibleTo("Avalonia.Markup.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - diff --git a/src/Shared/ModuleInitializer.cs b/src/Shared/ModuleInitializer.cs index c14b150c1a..b5bb31ed81 100644 --- a/src/Shared/ModuleInitializer.cs +++ b/src/Shared/ModuleInitializer.cs @@ -3,7 +3,7 @@ namespace System.Runtime.CompilerServices #if !NET5_0_OR_GREATER internal class ModuleInitializerAttribute : Attribute { - + } #endif } diff --git a/src/Shared/SourceGeneratorAttributes.cs b/src/Shared/SourceGeneratorAttributes.cs new file mode 100644 index 0000000000..fdb5977d23 --- /dev/null +++ b/src/Shared/SourceGeneratorAttributes.cs @@ -0,0 +1,17 @@ +using System; + +namespace Avalonia.SourceGenerator +{ + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class SubtypesFactoryAttribute : Attribute + { + public SubtypesFactoryAttribute(Type baseType, string @namespace) + { + BaseType = baseType; + Namespace = @namespace; + } + + public string Namespace { get; } + public Type BaseType { get; } + } +} diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 50ace8209c..413c2ba4d4 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -16,5 +16,15 @@ - + + + + + + + + + + + diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index e695a9cb41..8293769138 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -9,7 +9,7 @@ using Avalonia.Rendering; using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia @@ -126,6 +126,7 @@ namespace Avalonia.Skia SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas; SKSurface ISkiaDrawingContextImpl.SkSurface => Surface; GRContext ISkiaDrawingContextImpl.GrContext => _grContext; + double ISkiaDrawingContextImpl.CurrentOpacity => _currentOpacity; /// public void Clear(Color color) @@ -180,7 +181,8 @@ namespace Avalonia.Skia var size = geometry.Bounds.Size; using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper)) - using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper)) + using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, + size.Inflate(new Thickness(pen?.Thickness / 2 ?? 0))) : default(PaintWrapper)) { if (fill.Paint != null) { @@ -397,7 +399,7 @@ namespace Avalonia.Skia if (pen?.Brush != null) { - using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size)) + using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size.Inflate(new Thickness(pen?.Thickness / 2 ?? 0)))) { if (paint.Paint is object) { @@ -432,7 +434,7 @@ namespace Avalonia.Skia if (pen?.Brush != null) { - using (var paint = CreatePaint(_strokePaint, pen, rect.Size)) + using (var paint = CreatePaint(_strokePaint, pen, rect.Size.Inflate(new Thickness(pen?.Thickness / 2 ?? 0)))) { if (paint.Paint is object) { @@ -614,10 +616,25 @@ 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 + { + var transformOrigin = linearGradient.TransformOrigin.ToPixels(targetSize); + var offset = Matrix.CreateTranslation(transformOrigin); + var transform = (-offset) * linearGradient.Transform.Value * (offset); + + using (var shader = + SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode, transform.ToSKMatrix())) + { + paintWrapper.Paint.Shader = shader; + } } break; @@ -632,10 +649,25 @@ 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 + { + var transformOrigin = radialGradient.TransformOrigin.ToPixels(targetSize); + var offset = Matrix.CreateTranslation(transformOrigin); + var transform = (-offset) * radialGradient.Transform.Value * (offset); + + using (var shader = + SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode, transform.ToSKMatrix())) + { + paintWrapper.Paint.Shader = shader; + } } } else @@ -659,12 +691,30 @@ 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 + { + + var transformOrigin = radialGradient.TransformOrigin.ToPixels(targetSize); + var offset = Matrix.CreateTranslation(transformOrigin); + var transform = (-offset) * radialGradient.Transform.Value * (offset); + + using (var shader = SKShader.CreateCompose( + SKShader.CreateColor(reversedColors[0]), + SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode, transform.ToSKMatrix()) + )) + { + paintWrapper.Paint.Shader = shader; + } } } @@ -679,6 +729,16 @@ namespace Avalonia.Skia var angle = (float)(conicGradient.Angle - 90); var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y); + if (conicGradient.Transform is { }) + { + + var transformOrigin = conicGradient.TransformOrigin.ToPixels(targetSize); + var offset = Matrix.CreateTranslation(transformOrigin); + var transform = (-offset) * conicGradient.Transform.Value * (offset); + + rotation = rotation.PreConcat(transform.ToSKMatrix()); + } + using (var shader = SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation)) { @@ -751,6 +811,15 @@ namespace Avalonia.Skia tileTransform, SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); + if (tileBrush.Transform is { }) + { + var origin = tileBrush.TransformOrigin.ToPixels(targetSize); + var offset = Matrix.CreateTranslation(origin); + var transform = (-offset) * tileBrush.Transform.Value * (offset); + + paintTransform = paintTransform.PreConcat(transform.ToSKMatrix()); + } + using (var shader = image.ToShader(tileX, tileY, paintTransform)) { paintWrapper.Paint.Shader = shader; diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 6b560ac739..125dd0e455 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -29,27 +29,27 @@ namespace Avalonia.Skia [ThreadStatic] private static string[] t_languageTagBuffer; public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, + FontWeight fontWeight, FontStretch fontStretch, FontFamily fontFamily, CultureInfo culture, out Typeface fontKey) { SKFontStyle skFontStyle; switch (fontWeight) { - case FontWeight.Normal when fontStyle == FontStyle.Normal: + case FontWeight.Normal when fontStyle == FontStyle.Normal && fontStretch == FontStretch.Normal: skFontStyle = SKFontStyle.Normal; break; - case FontWeight.Normal when fontStyle == FontStyle.Italic: + case FontWeight.Normal when fontStyle == FontStyle.Italic && fontStretch == FontStretch.Normal: skFontStyle = SKFontStyle.Italic; break; - case FontWeight.Bold when fontStyle == FontStyle.Normal: + case FontWeight.Bold when fontStyle == FontStyle.Normal && fontStretch == FontStretch.Normal: skFontStyle = SKFontStyle.Bold; break; - case FontWeight.Bold when fontStyle == FontStyle.Italic: + case FontWeight.Bold when fontStyle == FontStyle.Italic && fontStretch == FontStretch.Normal: skFontStyle = SKFontStyle.BoldItalic; break; default: - skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle); + skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, (SKFontStyleWidth)fontStretch, (SKFontStyleSlant)fontStyle); break; } @@ -80,7 +80,7 @@ namespace Avalonia.Skia continue; } - fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight); + fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight, fontStretch); return true; } @@ -91,7 +91,7 @@ namespace Avalonia.Skia if (skTypeface != null) { - fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight); + fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight, fontStretch); return true; } @@ -109,15 +109,20 @@ namespace Avalonia.Skia if (typeface.FontFamily.Key == null) { var defaultName = SKTypeface.Default.FamilyName; - var fontStyle = new SKFontStyle((SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); + + var fontStyle = new SKFontStyle((SKFontStyleWeight)typeface.Weight, (SKFontStyleWidth)typeface.Stretch, + (SKFontStyleSlant)typeface.Style); foreach (var familyName in typeface.FontFamily.FamilyNames) { + if(familyName == FontFamily.DefaultFontFamilyName) + { + continue; + } + skTypeface = _skFontManager.MatchFamily(familyName, fontStyle); - if (skTypeface is null - || (!skTypeface.FamilyName.Equals(familyName, StringComparison.Ordinal) - && defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal))) + if (skTypeface is null || defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal)) { continue; } @@ -125,7 +130,10 @@ namespace Avalonia.Skia break; } - skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle); + // MatchTypeface can return "null" if matched typeface wasn't found for the style + // Fallback to the default typeface and styles instead. + skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle) + ?? SKTypeface.Default; } else { diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index f59a0a32c2..bdc3d075cf 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Metadata; using Avalonia.Platform; using JetBrains.Annotations; using SkiaSharp; @@ -6,6 +7,7 @@ using SkiaSharp; namespace Avalonia.Skia { /// + [Unstable] public class GlyphRunImpl : IGlyphRunImpl { public GlyphRunImpl([NotNull] SKTextBlob textBlob) diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index 5b6e5af60f..dcb4eac7ca 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -1,11 +1,13 @@ using System; using System.Runtime.InteropServices; +using Avalonia.Metadata; using Avalonia.Platform; using HarfBuzzSharp; using SkiaSharp; namespace Avalonia.Skia { + [Unstable] public class GlyphTypefaceImpl : IGlyphTypefaceImpl { private bool _isDisposed; 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/ISkiaDrawingContextImpl.cs b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs index 38fa5a5253..1b60154d46 100644 --- a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs @@ -1,12 +1,15 @@ +using Avalonia.Metadata; using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia { + [Unstable] public interface ISkiaDrawingContextImpl : IDrawingContextImpl { SKCanvas SkCanvas { get; } GRContext GrContext { get; } SKSurface SkSurface { get; } + double CurrentOpacity { get; } } } diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index 3010f49420..5628ae177c 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -5,7 +5,7 @@ using System.Security.Cryptography; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Skia.Helpers; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index af3b570fd7..d3c3585cd0 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -10,7 +10,7 @@ using Avalonia.Media; using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; using Avalonia.Platform; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia @@ -35,6 +35,9 @@ namespace Avalonia.Skia var gl = AvaloniaLocator.Current.GetService(); if (gl != null) _skiaGpu = new GlSkiaGpu(gl, maxResourceBytes); + + //TODO: SKFont crashes when disposed in finalizer so we keep it alive + GC.SuppressFinalize(s_font); } public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); diff --git a/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs b/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs deleted file mode 100644 index a7e556ee84..0000000000 --- a/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 2b9d0b103e..1f3f20730f 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -1,6 +1,4 @@ using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using Avalonia.Media; using SkiaSharp; @@ -18,48 +16,183 @@ namespace Avalonia.Skia public SKTypeface Get(Typeface typeface) { - return GetNearestMatch(_typefaces, typeface); + return GetNearestMatch(typeface); } - private static SKTypeface GetNearestMatch(IDictionary typefaces, Typeface key) + private SKTypeface GetNearestMatch(Typeface key) { - if (typefaces.TryGetValue(key, out var typeface)) + if (_typefaces.Count == 0) { - return typeface; + return null; } - var initialWeight = (int)key.Weight; + if (_typefaces.TryGetValue(key, out var typeface)) + { + return typeface; + } + + if(key.Style != FontStyle.Normal) + { + key = new Typeface(key.FontFamily, FontStyle.Normal, key.Weight, key.Stretch); + } + + if(key.Stretch != FontStretch.Normal) + { + if(TryFindStretchFallback(key, out typeface)) + { + return typeface; + } + + if(key.Weight != FontWeight.Normal) + { + if (TryFindStretchFallback(new Typeface(key.FontFamily, key.Style, FontWeight.Normal, key.Stretch), out typeface)) + { + return typeface; + } + } + + key = new Typeface(key.FontFamily, key.Style, key.Weight, FontStretch.Normal); + } + + if(TryFindWeightFallback(key, out typeface)) + { + return typeface; + } + if (TryFindStretchFallback(key, out typeface)) + { + return typeface; + } + + //Nothing was found so we try some regular typeface. + if (_typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface)) + { + return typeface; + } + + SKTypeface skTypeface = null; + + foreach(var pair in _typefaces) + { + skTypeface = pair.Value; + + if (skTypeface.FamilyName.Contains(key.FontFamily.Name)) + { + return skTypeface; + } + } + + return skTypeface; + } + + private bool TryFindStretchFallback(Typeface key, out SKTypeface typeface) + { + typeface = null; + var stretch = (int)key.Stretch; + + if (stretch < 5) + { + for (var i = 0; stretch + i < 9; i++) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch + i)), out typeface)) + { + return true; + } + } + } + else + { + for (var i = 0; stretch - i > 1; i++) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch - i)), out typeface)) + { + return true; + } + } + } + + return false; + } + + private bool TryFindWeightFallback(Typeface key, out SKTypeface typeface) + { + typeface = null; var weight = (int)key.Weight; - weight -= weight % 50; // make sure we start at a full weight + //If the target weight given is between 400 and 500 inclusive + if (weight >= 400 && weight <= 500) + { + //Look for available weights between the target and 500, in ascending order. + for (var i = 0; weight + i <= 500; i += 50) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights greater than 500, in ascending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) + { + return true; + } + } + } - for (var i = 0; i < 2; i++) + //If a weight less than 400 is given, look for available weights less than the target, in descending order. + if (weight < 400) { - for (var j = 0; j < initialWeight; j += 50) + for (var i = 0; weight - i >= 100; i += 50) { - if (weight - j >= 100) + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) { - if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) - { - return typeface; - } + return true; } + } - if (weight + j > 900) + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) { - continue; + return true; } + } + } - if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) + //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order. + if (weight > 500) + { + for (var i = 0; weight + i <= 900; i += 50) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) { - return typeface; + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) + { + return true; } } } - //Nothing was found so we try to get a regular typeface. - return typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface) ? typeface : null; + return false; } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 2816146806..5ca7f40d17 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -51,13 +51,13 @@ namespace Avalonia.Skia if (typeface == null) throw new InvalidOperationException("Typeface could not be loaded."); - if (typeface.FamilyName != fontFamily.Name) + if (!typeface.FamilyName.Contains(fontFamily.Name)) { continue; } var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(), - (FontWeight)typeface.FontWeight); + (FontWeight)typeface.FontWeight, (FontStretch)typeface.FontWidth); typeFaceCollection.AddTypeface(key, typeface); } diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 75b4231640..d584216f17 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -1,7 +1,7 @@ using System; using Avalonia.Media; using Avalonia.Platform; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia @@ -103,9 +103,9 @@ namespace Avalonia.Skia SkewY = (float)m.M12, ScaleY = (float)m.M22, TransY = (float)m.M32, - Persp0 = 0, - Persp1 = 0, - Persp2 = 1 + Persp0 = (float)m.M13, + Persp1 = (float)m.M23, + Persp2 = (float)m.M33 }; return sm; diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index c4d11f4613..908b0ffa47 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -13,18 +12,22 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, - CultureInfo culture, sbyte bidiLevel) + public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) { + var typeface = options.Typeface; + var fontRenderingEmSize = options.FontRenderingEmSize; + var bidiLevel = options.BidLevel; + var culture = options.Culture; + using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); + buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length); MergeBreakPair(buffer); buffer.GuessSegmentProperties(); - buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; + buffer.Direction = Direction.LeftToRight; //Always shape LeftToRight buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); @@ -32,11 +35,6 @@ namespace Avalonia.Skia font.Shape(buffer); - if (buffer.Direction == Direction.RightToLeft) - { - buffer.Reverse(); - } - font.GetScale(out var scaleX, out _); var textScale = fontRenderingEmSize / scaleX; @@ -55,12 +53,21 @@ namespace Avalonia.Skia var glyphIndex = (ushort)sourceInfo.Codepoint; - var glyphCluster = (int)sourceInfo.Cluster; + var glyphCluster = (int)(sourceInfo.Cluster); var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); + if(glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t') + { + glyphIndex = typeface.GetGlyph(' '); + + glyphAdvance = options.IncrementalTabWidth > 0 ? + options.IncrementalTabWidth : + 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; + } + var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); shapedBuffer[i] = targetInfo; diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index 63a1e8f869..506edf0627 100644 --- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -4,7 +4,7 @@ using System.Threading; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Skia.Helpers; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SkiaSharp; namespace Avalonia.Skia diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 07c776efb4..1ccf53943a 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -224,7 +224,7 @@ namespace Avalonia.Web.Blazor private void OnKeyDown(KeyboardEventArgs e) { - _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Key, GetModifiers(e)); + _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Code, GetModifiers(e)); } private void OnKeyUp(KeyboardEventArgs e) @@ -367,10 +367,12 @@ namespace Avalonia.Web.Blazor } } - public void SetActive(bool active) + public void SetClient(ITextInputMethodClient? client) { _inputHelper.Clear(); + var active = client is { }; + if (active) { _inputHelper.Show(); @@ -386,7 +388,7 @@ namespace Avalonia.Web.Blazor { } - public void SetOptions(TextInputOptionsQueryEventArgs options) + public void SetOptions(TextInputOptions options) { } diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts index ff6dc4a8f8..932dfa1e1f 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts @@ -6,7 +6,7 @@ // Here be dragons! // This is community-maintained definition file intended to ease the process of developing -// high quality JavaScript interop code to be used in Blazor application from your C# .Net code. +// high quality JavaScript interop code to be used in Blazor application from your C# .NET code. // Could be removed without a notice in case official definition types ships with Blazor itself. // tslint:disable:no-unnecessary-generics diff --git a/src/Web/Avalonia.Web.Blazor/WinStubs.cs b/src/Web/Avalonia.Web.Blazor/WinStubs.cs index 7b2bff6bfd..7c30a96d35 100644 --- a/src/Web/Avalonia.Web.Blazor/WinStubs.cs +++ b/src/Web/Avalonia.Web.Blazor/WinStubs.cs @@ -55,5 +55,20 @@ namespace Avalonia.Web.Blazor public IReadOnlyList AllScreens { get; } = new[] { new Screen(96, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) }; + + public Screen? ScreenFromPoint(PixelPoint point) + { + return ScreenHelper.ScreenFromPoint(point, AllScreens); + } + + public Screen? ScreenFromRect(PixelRect rect) + { + return ScreenHelper.ScreenFromRect(rect, AllScreens); + } + + public Screen? ScreenFromWindow(IWindowBaseImpl window) + { + return ScreenHelper.ScreenFromWindow(window, AllScreens); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index a1d8a60cb9..03b3ebec0d 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -18,4 +18,15 @@ + + + + + + + + + + + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index c32c58605f..d9e992bb80 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -9,7 +9,7 @@ using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; using TextAlignment = Avalonia.Media.TextAlignment; diff --git a/src/Windows/Avalonia.Direct2D1/Media/BrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/BrushImpl.cs index ad609a0810..602ea9b568 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/BrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/BrushImpl.cs @@ -1,7 +1,9 @@ using System; +using Avalonia.Metadata; namespace Avalonia.Direct2D1.Media { + [Unstable] public abstract class BrushImpl : IDisposable { public SharpDX.Direct2D1.Brush PlatformBrush { get; set; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index 78bf25d607..792bf2d0be 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -5,6 +5,7 @@ using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; using FontStyle = SharpDX.DirectWrite.FontStyle; using FontWeight = SharpDX.DirectWrite.FontWeight; +using FontStretch = SharpDX.DirectWrite.FontStretch; namespace Avalonia.Direct2D1.Media { @@ -32,7 +33,7 @@ namespace Avalonia.Direct2D1.Media { return fontCollection.GetFontFamily(index).GetFirstMatchingFont( (FontWeight)typeface.Weight, - FontStretch.Normal, + (FontStretch)typeface.Stretch, (FontStyle)typeface.Style); } } @@ -41,7 +42,7 @@ namespace Avalonia.Direct2D1.Media return InstalledFontCollection.GetFontFamily(index).GetFirstMatchingFont( (FontWeight)typeface.Weight, - FontStretch.Normal, + (FontStretch)typeface.Stretch, (FontStyle)typeface.Style); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index b62a6fa5a6..a0f98bbbc9 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -5,17 +5,19 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; using SharpDX; using SharpDX.Direct2D1; using SharpDX.Mathematics.Interop; -using BitmapInterpolationMode = Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode; +using BitmapInterpolationMode = Avalonia.Media.Imaging.BitmapInterpolationMode; +using Avalonia.Metadata; namespace Avalonia.Direct2D1.Media { /// /// Draws using Direct2D1. /// + [Unstable] public class DrawingContextImpl : IDrawingContextImpl { private readonly IVisualBrushRenderer _visualBrushRenderer; diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 6d95d759ec..c996337520 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -4,6 +4,7 @@ using Avalonia.Media; using Avalonia.Platform; using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; +using FontStretch = Avalonia.Media.FontStretch; using FontStyle = Avalonia.Media.FontStyle; using FontWeight = Avalonia.Media.FontWeight; @@ -32,7 +33,7 @@ namespace Avalonia.Direct2D1.Media } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, + FontWeight fontWeight, FontStretch fontStretch, FontFamily fontFamily, CultureInfo culture, out Typeface typeface) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -40,7 +41,8 @@ namespace Avalonia.Direct2D1.Media for (var i = 0; i < familyCount; i++) { var font = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i) - .GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, FontStretch.Normal, + .GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, + (SharpDX.DirectWrite.FontStretch)fontStretch, (SharpDX.DirectWrite.FontStyle)fontStyle).GetFont(0); if (!font.HasCharacter(codepoint)) @@ -50,7 +52,7 @@ namespace Avalonia.Direct2D1.Media var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - typeface = new Typeface(fontFamilyName, fontStyle, fontWeight); + typeface = new Typeface(fontFamilyName, fontStyle, fontWeight, fontStretch); return true; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index ec88347a17..c84c14daac 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -1,4 +1,5 @@ using Avalonia.Logging; +using Avalonia.Metadata; using Avalonia.Platform; using SharpDX.Direct2D1; @@ -7,6 +8,7 @@ namespace Avalonia.Direct2D1.Media /// /// The platform-specific interface for . /// + [Unstable] public abstract class GeometryImpl : IGeometryImpl { private const float ContourApproximation = 0.0001f; diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs index 4f2ed22a25..4154b44702 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs @@ -1,11 +1,13 @@ using System; using Avalonia.Media; +using Avalonia.Metadata; using Avalonia.Platform; using HarfBuzzSharp; using SharpDX.DirectWrite; namespace Avalonia.Direct2D1.Media { + [Unstable] public class GlyphTypefaceImpl : IGlyphTypefaceImpl { private bool _isDisposed; diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index 09b900b0c2..17dc359ed7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -1,15 +1,17 @@ using Avalonia.Media; +using Avalonia.Metadata; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; using SharpDX.Direct2D1; namespace Avalonia.Direct2D1.Media { + [Unstable] public sealed class ImageBrushImpl : BrushImpl { private readonly OptionalDispose _bitmap; - private readonly Visuals.Media.Imaging.BitmapInterpolationMode _bitmapInterpolationMode; + private readonly Avalonia.Media.Imaging.BitmapInterpolationMode _bitmapInterpolationMode; public ImageBrushImpl( ITileBrush brush, diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs index af6d5c5e7b..843efe2cc4 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs @@ -1,10 +1,12 @@ using System; using System.IO; +using Avalonia.Metadata; using Avalonia.Platform; using D2DBitmap = SharpDX.Direct2D1.Bitmap; namespace Avalonia.Direct2D1.Media { + [Unstable] public abstract class BitmapImpl : IBitmapImpl, IDisposable { public abstract Vector Dpi { get; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs index 63676e30b5..2656ab4c58 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using Avalonia.Metadata; using SharpDX.WIC; using Bitmap = SharpDX.Direct2D1.Bitmap; @@ -8,6 +9,7 @@ namespace Avalonia.Direct2D1.Media /// /// A Direct2D Bitmap implementation that uses a GPU memory bitmap as its image. /// + [Unstable] public class D2DBitmapImpl : BitmapImpl { private readonly Bitmap _direct2DBitmap; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 9a0e2ec00c..357e472d34 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Utilities; @@ -9,6 +10,7 @@ using D2DBitmap = SharpDX.Direct2D1.Bitmap; namespace Avalonia.Direct2D1.Media.Imaging { + [Unstable] public class D2DRenderTargetBitmapImpl : D2DBitmapImpl, IDrawingContextLayerImpl, ILayerFactory { private readonly BitmapRenderTarget _renderTarget; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index cac35aa4ae..1156246b29 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -5,31 +5,33 @@ using SharpDX.WIC; using APixelFormat = Avalonia.Platform.PixelFormat; using AlphaFormat = Avalonia.Platform.AlphaFormat; using D2DBitmap = SharpDX.Direct2D1.Bitmap; +using Avalonia.Metadata; namespace Avalonia.Direct2D1.Media { /// /// A WIC implementation of a . /// + [Unstable] public class WicBitmapImpl : BitmapImpl { private readonly BitmapDecoder _decoder; - private static BitmapInterpolationMode ConvertInterpolationMode(Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) + private static BitmapInterpolationMode ConvertInterpolationMode(Avalonia.Media.Imaging.BitmapInterpolationMode interpolationMode) { switch (interpolationMode) { - case Visuals.Media.Imaging.BitmapInterpolationMode.Default: + case Avalonia.Media.Imaging.BitmapInterpolationMode.Default: return BitmapInterpolationMode.Fant; - case Visuals.Media.Imaging.BitmapInterpolationMode.LowQuality: + case Avalonia.Media.Imaging.BitmapInterpolationMode.LowQuality: return BitmapInterpolationMode.NearestNeighbor; - case Visuals.Media.Imaging.BitmapInterpolationMode.MediumQuality: + case Avalonia.Media.Imaging.BitmapInterpolationMode.MediumQuality: return BitmapInterpolationMode.Fant; default: - case Visuals.Media.Imaging.BitmapInterpolationMode.HighQuality: + case Avalonia.Media.Imaging.BitmapInterpolationMode.HighQuality: return BitmapInterpolationMode.HighQualityCubic; } @@ -118,7 +120,7 @@ namespace Avalonia.Direct2D1.Media } } - public WicBitmapImpl(Stream stream, int decodeSize, bool horizontal, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) + public WicBitmapImpl(Stream stream, int decodeSize, bool horizontal, Avalonia.Media.Imaging.BitmapInterpolationMode interpolationMode) { _decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, stream, DecodeOptions.CacheOnLoad); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index 1265a7bdf0..8c9d01f37d 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -1,10 +1,12 @@ using System; +using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Rendering; using SharpDX.Direct2D1; namespace Avalonia.Direct2D1.Media { + [Unstable] public class WicRenderTargetBitmapImpl : WicBitmapImpl, IDrawingContextLayerImpl { private readonly WicRenderTarget _renderTarget; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs index fef6a9aa82..885be132a4 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Direct2D1.Media.Imaging class WriteableWicBitmapImpl : WicBitmapImpl, IWriteableBitmapImpl { public WriteableWicBitmapImpl(Stream stream, int decodeSize, bool horizontal, - Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode) + Avalonia.Media.Imaging.BitmapInterpolationMode interpolationMode) : base(stream, decodeSize, horizontal, interpolationMode) { } diff --git a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs index 0e63d4cc03..5dfe683f59 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs @@ -1,8 +1,10 @@ using System.Linq; using Avalonia.Media; +using Avalonia.Metadata; namespace Avalonia.Direct2D1.Media { + [Unstable] public class LinearGradientBrushImpl : BrushImpl { public LinearGradientBrushImpl( diff --git a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs index 1fca6d4e33..0069e47001 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs @@ -1,8 +1,10 @@ using System.Linq; using Avalonia.Media; +using Avalonia.Metadata; namespace Avalonia.Direct2D1.Media { + [Unstable] public class RadialGradientBrushImpl : BrushImpl { public RadialGradientBrushImpl( diff --git a/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs index fea1ca9157..b85494e2c1 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs @@ -1,7 +1,9 @@ using Avalonia.Media; +using Avalonia.Metadata; namespace Avalonia.Direct2D1.Media { + [Unstable] public class SolidColorBrushImpl : BrushImpl { public SolidColorBrushImpl(ISolidColorBrush brush, SharpDX.Direct2D1.RenderTarget target) diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs index e1f7aad1b2..ec8f82556d 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Logging; using Avalonia.Media; +using Avalonia.Metadata; using Avalonia.Platform; using SharpDX.Direct2D1; using D2D = SharpDX.Direct2D1; @@ -8,6 +9,7 @@ using SweepDirection = SharpDX.Direct2D1.SweepDirection; namespace Avalonia.Direct2D1.Media { + [Unstable] public class StreamGeometryContextImpl : IStreamGeometryContextImpl { private readonly GeometrySink _sink; diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs index 2bc2b2db71..e1677c0ed1 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs @@ -1,3 +1,4 @@ +using Avalonia.Metadata; using Avalonia.Platform; using SharpDX.Direct2D1; @@ -6,6 +7,7 @@ namespace Avalonia.Direct2D1.Media /// /// A Direct2D implementation of a . /// + [Unstable] public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl { /// diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 62cf031f86..f4e4b00147 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -11,33 +10,31 @@ using GlyphInfo = HarfBuzzSharp.GlyphInfo; namespace Avalonia.Direct2D1.Media { - -internal class TextShaperImpl : ITextShaperImpl + internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, - CultureInfo culture, sbyte bidiLevel) + public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) { + var typeface = options.Typeface; + var fontRenderingEmSize = options.FontRenderingEmSize; + var bidiLevel = options.BidLevel; + var culture = options.Culture; + using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); + buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length); MergeBreakPair(buffer); - + buffer.GuessSegmentProperties(); - buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; + buffer.Direction = Direction.LeftToRight; //Always shape LeftToRight - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); var font = ((GlyphTypefaceImpl)typeface.PlatformImpl).Font; font.Shape(buffer); - if (buffer.Direction == Direction.RightToLeft) - { - buffer.Reverse(); - } - font.GetScale(out var scaleX, out _); var textScale = fontRenderingEmSize / scaleX; @@ -56,15 +53,22 @@ internal class TextShaperImpl : ITextShaperImpl var glyphIndex = (ushort)sourceInfo.Codepoint; - var glyphCluster = (int)sourceInfo.Cluster; + var glyphCluster = (int)(sourceInfo.Cluster); var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - var targetInfo = - new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, - glyphOffset); + if (glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t') + { + glyphIndex = typeface.GetGlyph(' '); + + glyphAdvance = options.IncrementalTabWidth > 0 ? + options.IncrementalTabWidth : + 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; + } + + var targetInfo = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); shapedBuffer[i] = targetInfo; } @@ -78,7 +82,7 @@ internal class TextShaperImpl : ITextShaperImpl var length = buffer.Length; var glyphInfos = buffer.GetGlyphInfoSpan(); - + var second = glyphInfos[length - 1]; if (!new Codepoint((int)second.Codepoint).IsBreakChar) @@ -89,7 +93,7 @@ internal class TextShaperImpl : ITextShaperImpl if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') { var first = glyphInfos[length - 2]; - + first.Codepoint = '\u200C'; second.Codepoint = '\u200C'; second.Cluster = first.Cluster; @@ -100,7 +104,7 @@ internal class TextShaperImpl : ITextShaperImpl { *p = first; } - + fixed (GlyphInfo* p = &glyphInfos[length - 1]) { *p = second; @@ -135,7 +139,7 @@ internal class TextShaperImpl : ITextShaperImpl private static double GetGlyphAdvance(ReadOnlySpan glyphPositions, int index, double textScale) { // Depends on direction of layout - // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale; + // glyphPositions[index].YAdvance * textScale; return glyphPositions[index].XAdvance * textScale; } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs index fe274701bf..3ecdb49e46 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs @@ -1,8 +1,10 @@ +using Avalonia.Metadata; using Avalonia.Platform; using SharpDX.Direct2D1; namespace Avalonia.Direct2D1.Media { + [Unstable] public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl { /// diff --git a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs deleted file mode 100644 index cdeb675bcd..0000000000 --- a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Runtime.CompilerServices; -using Avalonia.Platform; -using Avalonia.Direct2D1; - -[assembly: ExportRenderingSubsystem(OperatingSystemType.WinNT, 1, "Direct2D1", typeof(Direct2D1Platform), nameof(Direct2D1Platform.Initialize), - typeof(Direct2DChecker))] - -[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] diff --git a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj index 9f49808422..cc8a40e2d4 100644 --- a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj +++ b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj @@ -18,4 +18,5 @@ + diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs new file mode 100644 index 0000000000..5f3f863493 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs @@ -0,0 +1,19 @@ +using Avalonia.Automation; +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IExpandCollapseProvider + { + ExpandCollapseState UIA.IExpandCollapseProvider.ExpandCollapseState + { + get => InvokeSync(x => x.ExpandCollapseState); + } + + void UIA.IExpandCollapseProvider.Expand() => InvokeSync(x => x.Expand()); + void UIA.IExpandCollapseProvider.Collapse() => InvokeSync(x => x.Collapse()); + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs new file mode 100644 index 0000000000..b91cb76888 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs @@ -0,0 +1,19 @@ +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IRangeValueProvider + { + double UIA.IRangeValueProvider.Value => InvokeSync(x => x.Value); + bool UIA.IRangeValueProvider.IsReadOnly => InvokeSync(x => x.IsReadOnly); + double UIA.IRangeValueProvider.Maximum => InvokeSync(x => x.Maximum); + double UIA.IRangeValueProvider.Minimum => InvokeSync(x => x.Minimum); + double UIA.IRangeValueProvider.LargeChange => 1; + double UIA.IRangeValueProvider.SmallChange => 1; + + public void SetValue(double value) => InvokeSync(x => x.SetValue(value)); + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs new file mode 100644 index 0000000000..4f2d4ae269 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs @@ -0,0 +1,32 @@ +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IScrollProvider, UIA.IScrollItemProvider + { + bool UIA.IScrollProvider.HorizontallyScrollable => InvokeSync(x => x.HorizontallyScrollable); + double UIA.IScrollProvider.HorizontalScrollPercent => InvokeSync(x => x.HorizontalScrollPercent); + double UIA.IScrollProvider.HorizontalViewSize => InvokeSync(x => x.HorizontalViewSize); + bool UIA.IScrollProvider.VerticallyScrollable => InvokeSync(x => x.VerticallyScrollable); + double UIA.IScrollProvider.VerticalScrollPercent => InvokeSync(x => x.VerticalScrollPercent); + double UIA.IScrollProvider.VerticalViewSize => InvokeSync(x => x.VerticalViewSize); + + void UIA.IScrollProvider.Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount) + { + InvokeSync(x => x.Scroll(horizontalAmount, verticalAmount)); + } + + void UIA.IScrollProvider.SetScrollPercent(double horizontalPercent, double verticalPercent) + { + InvokeSync(x => x.SetScrollPercent(horizontalPercent, verticalPercent)); + } + + void UIA.IScrollItemProvider.ScrollIntoView() + { + InvokeSync(() => Peer.BringIntoView()); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs new file mode 100644 index 0000000000..61903ab5b0 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.ISelectionProvider, UIA.ISelectionItemProvider + { + bool UIA.ISelectionProvider.CanSelectMultiple => InvokeSync(x => x.CanSelectMultiple); + bool UIA.ISelectionProvider.IsSelectionRequired => InvokeSync(x => x.IsSelectionRequired); + bool UIA.ISelectionItemProvider.IsSelected => InvokeSync(x => x.IsSelected); + + UIA.IRawElementProviderSimple? UIA.ISelectionItemProvider.SelectionContainer + { + get + { + var peer = InvokeSync(x => x.SelectionContainer); + return GetOrCreate(peer as AutomationPeer); + } + } + + UIA.IRawElementProviderSimple[] UIA.ISelectionProvider.GetSelection() + { + var peers = InvokeSync>(x => x.GetSelection()); + return peers?.Select(x => (UIA.IRawElementProviderSimple)GetOrCreate(x)!).ToArray() ?? + Array.Empty(); + } + + void UIA.ISelectionItemProvider.AddToSelection() => InvokeSync(x => x.AddToSelection()); + void UIA.ISelectionItemProvider.RemoveFromSelection() => InvokeSync(x => x.RemoveFromSelection()); + void UIA.ISelectionItemProvider.Select() => InvokeSync(x => x.Select()); + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs new file mode 100644 index 0000000000..38f4d80946 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs @@ -0,0 +1,13 @@ +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IToggleProvider + { + ToggleState UIA.IToggleProvider.ToggleState => InvokeSync(x => x.ToggleState); + void UIA.IToggleProvider.Toggle() => InvokeSync(x => x.Toggle()); + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs new file mode 100644 index 0000000000..34f5dfe0b9 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; +using Avalonia.Automation.Provider; +using UIA = Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal partial class AutomationNode : UIA.IValueProvider + { + bool UIA.IValueProvider.IsReadOnly => InvokeSync(x => x.IsReadOnly); + string? UIA.IValueProvider.Value => InvokeSync(x => x.Value); + + void UIA.IValueProvider.SetValue([MarshalAs(UnmanagedType.LPWStr)] string? value) + { + InvokeSync(x => x.SetValue(value)); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs new file mode 100644 index 0000000000..70e415aff1 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Threading; +using Avalonia.Win32.Interop.Automation; +using AAP = Avalonia.Automation.Provider; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + [ComVisible(true)] + internal partial class AutomationNode : MarshalByRefObject, + IRawElementProviderSimple, + IRawElementProviderSimple2, + IRawElementProviderFragment, + IRawElementProviderAdviseEvents, + IInvokeProvider + { + private static Dictionary s_propertyMap = new Dictionary() + { + { AutomationElementIdentifiers.BoundingRectangleProperty, UiaPropertyId.BoundingRectangle }, + { AutomationElementIdentifiers.ClassNameProperty, UiaPropertyId.ClassName }, + { AutomationElementIdentifiers.NameProperty, UiaPropertyId.Name }, + { ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, UiaPropertyId.ExpandCollapseExpandCollapseState }, + { RangeValuePatternIdentifiers.IsReadOnlyProperty, UiaPropertyId.RangeValueIsReadOnly}, + { RangeValuePatternIdentifiers.MaximumProperty, UiaPropertyId.RangeValueMaximum }, + { RangeValuePatternIdentifiers.MinimumProperty, UiaPropertyId.RangeValueMinimum }, + { RangeValuePatternIdentifiers.ValueProperty, UiaPropertyId.RangeValueValue }, + { ScrollPatternIdentifiers.HorizontallyScrollableProperty, UiaPropertyId.ScrollHorizontallyScrollable }, + { ScrollPatternIdentifiers.HorizontalScrollPercentProperty, UiaPropertyId.ScrollHorizontalScrollPercent }, + { ScrollPatternIdentifiers.HorizontalViewSizeProperty, UiaPropertyId.ScrollHorizontalViewSize }, + { ScrollPatternIdentifiers.VerticallyScrollableProperty, UiaPropertyId.ScrollVerticallyScrollable }, + { ScrollPatternIdentifiers.VerticalScrollPercentProperty, UiaPropertyId.ScrollVerticalScrollPercent }, + { ScrollPatternIdentifiers.VerticalViewSizeProperty, UiaPropertyId.ScrollVerticalViewSize }, + { SelectionPatternIdentifiers.CanSelectMultipleProperty, UiaPropertyId.SelectionCanSelectMultiple }, + { SelectionPatternIdentifiers.IsSelectionRequiredProperty, UiaPropertyId.SelectionIsSelectionRequired }, + { SelectionPatternIdentifiers.SelectionProperty, UiaPropertyId.SelectionSelection }, + }; + + private static ConditionalWeakTable s_nodes = + new ConditionalWeakTable(); + + private readonly int[] _runtimeId; + private int _raiseFocusChanged; + private int _raisePropertyChanged; + + public AutomationNode(AutomationPeer peer) + { + _runtimeId = new int[] { 3, GetHashCode() }; + Peer = peer; + s_nodes.Add(peer, this); + } + + public AutomationPeer Peer { get; protected set; } + + public Rect BoundingRectangle + { + get => InvokeSync(() => + { + if (GetRoot() is RootAutomationNode root) + return root.ToScreen(Peer.GetBoundingRectangle()); + return default; + }); + } + + public virtual IRawElementProviderFragmentRoot? FragmentRoot + { + get => InvokeSync(() => GetRoot()) as IRawElementProviderFragmentRoot; + } + + public virtual IRawElementProviderSimple? HostRawElementProvider => null; + public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider; + + public void ChildrenChanged() + { + UiaCoreProviderApi.UiaRaiseStructureChangedEvent( + this, + StructureChangeType.ChildrenInvalidated, + null, + 0); + } + + public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) + { + if (_raisePropertyChanged > 0 && s_propertyMap.TryGetValue(property, out var id)) + { + UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(this, (int)id, oldValue, newValue); + } + } + + [return: MarshalAs(UnmanagedType.IUnknown)] + public virtual object? GetPatternProvider(int patternId) + { + AutomationNode? ThisIfPeerImplementsProvider() => Peer.GetProvider() is object ? this : null; + + return (UiaPatternId)patternId switch + { + UiaPatternId.ExpandCollapse => ThisIfPeerImplementsProvider(), + UiaPatternId.Invoke => ThisIfPeerImplementsProvider(), + UiaPatternId.RangeValue => ThisIfPeerImplementsProvider(), + UiaPatternId.Scroll => ThisIfPeerImplementsProvider(), + UiaPatternId.ScrollItem => this, + UiaPatternId.Selection => ThisIfPeerImplementsProvider(), + UiaPatternId.SelectionItem => ThisIfPeerImplementsProvider(), + UiaPatternId.Toggle => ThisIfPeerImplementsProvider(), + UiaPatternId.Value => ThisIfPeerImplementsProvider(), + _ => null, + }; + } + + public virtual object? GetPropertyValue(int propertyId) + { + return (UiaPropertyId)propertyId switch + { + UiaPropertyId.AcceleratorKey => InvokeSync(() => Peer.GetAcceleratorKey()), + UiaPropertyId.AccessKey => InvokeSync(() => Peer.GetAccessKey()), + UiaPropertyId.AutomationId => InvokeSync(() => Peer.GetAutomationId()), + UiaPropertyId.ClassName => InvokeSync(() => Peer.GetClassName()), + UiaPropertyId.ClickablePoint => new[] { BoundingRectangle.Center.X, BoundingRectangle.Center.Y }, + UiaPropertyId.ControlType => InvokeSync(() => ToUiaControlType(Peer.GetAutomationControlType())), + UiaPropertyId.Culture => CultureInfo.CurrentCulture.LCID, + UiaPropertyId.FrameworkId => "Avalonia", + UiaPropertyId.HasKeyboardFocus => InvokeSync(() => Peer.HasKeyboardFocus()), + UiaPropertyId.IsContentElement => InvokeSync(() => Peer.IsContentElement()), + UiaPropertyId.IsControlElement => InvokeSync(() => Peer.IsControlElement()), + UiaPropertyId.IsEnabled => InvokeSync(() => Peer.IsEnabled()), + UiaPropertyId.IsKeyboardFocusable => InvokeSync(() => Peer.IsKeyboardFocusable()), + UiaPropertyId.LocalizedControlType => InvokeSync(() => Peer.GetLocalizedControlType()), + UiaPropertyId.Name => InvokeSync(() => Peer.GetName()), + UiaPropertyId.ProcessId => Process.GetCurrentProcess().Id, + UiaPropertyId.RuntimeId => _runtimeId, + _ => null, + }; + } + + public int[]? GetRuntimeId() => _runtimeId; + + public virtual IRawElementProviderFragment? Navigate(NavigateDirection direction) + { + AutomationNode? GetSibling(int direction) + { + var children = Peer.GetParent()?.GetChildren(); + + for (var i = 0; i < (children?.Count ?? 0); ++i) + { + if (ReferenceEquals(children![i], Peer)) + { + var j = i + direction; + if (j >= 0 && j < children.Count) + return GetOrCreate(children[j]); + } + } + + return null; + } + + return InvokeSync(() => + { + return direction switch + { + NavigateDirection.Parent => GetOrCreate(Peer.GetParent()), + NavigateDirection.NextSibling => GetSibling(1), + NavigateDirection.PreviousSibling => GetSibling(-1), + NavigateDirection.FirstChild => GetOrCreate(Peer.GetChildren().FirstOrDefault()), + NavigateDirection.LastChild => GetOrCreate(Peer.GetChildren().LastOrDefault()), + _ => null, + }; + }) as IRawElementProviderFragment; + } + + public void SetFocus() => InvokeSync(() => Peer.SetFocus()); + + public static AutomationNode? GetOrCreate(AutomationPeer? peer) + { + return peer is null ? null : s_nodes.GetValue(peer, Create); + } + + public static void Release(AutomationPeer peer) => s_nodes.Remove(peer); + + IRawElementProviderSimple[]? IRawElementProviderFragment.GetEmbeddedFragmentRoots() => null; + void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu()); + void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke()); + + void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties) + { + switch ((UiaEventId)eventId) + { + case UiaEventId.AutomationPropertyChanged: + ++_raisePropertyChanged; + break; + case UiaEventId.AutomationFocusChanged: + ++_raiseFocusChanged; + break; + } + } + + void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties) + { + switch ((UiaEventId)eventId) + { + case UiaEventId.AutomationPropertyChanged: + --_raisePropertyChanged; + break; + case UiaEventId.AutomationFocusChanged: + --_raiseFocusChanged; + break; + } + } + + protected void InvokeSync(Action action) + { + if (Dispatcher.UIThread.CheckAccess()) + action(); + else + Dispatcher.UIThread.InvokeAsync(action).Wait(); + } + + protected T InvokeSync(Func func) + { + if (Dispatcher.UIThread.CheckAccess()) + return func(); + else + return Dispatcher.UIThread.InvokeAsync(func).Result; + } + + protected void InvokeSync(Action action) + { + if (Peer.GetProvider() is TInterface i) + { + try + { + InvokeSync(() => action(i)); + } + catch (AggregateException e) when (e.InnerException is ElementNotEnabledException) + { + throw new COMException(e.Message, UiaCoreProviderApi.UIA_E_ELEMENTNOTENABLED); + } + } + else + { + throw new NotSupportedException(); + } + } + + protected TResult InvokeSync(Func func) + { + if (Peer.GetProvider() is TInterface i) + { + try + { + return InvokeSync(() => func(i)); + } + catch (AggregateException e) when (e.InnerException is ElementNotEnabledException) + { + throw new COMException(e.Message, UiaCoreProviderApi.UIA_E_ELEMENTNOTENABLED); + } + } + + throw new NotSupportedException(); + } + + protected void RaiseFocusChanged(AutomationNode? focused) + { + if (_raiseFocusChanged > 0) + { + UiaCoreProviderApi.UiaRaiseAutomationEvent( + focused, + (int)UiaEventId.AutomationFocusChanged); + } + } + + private AutomationNode? GetRoot() + { + Dispatcher.UIThread.VerifyAccess(); + + var peer = Peer; + var parent = peer.GetParent(); + + while (peer.GetProvider() is null && parent is object) + { + peer = parent; + parent = peer.GetParent(); + } + + return peer is object ? GetOrCreate(peer) : null; + } + + private static AutomationNode Create(AutomationPeer peer) + { + return peer.GetProvider() is object ? + new RootAutomationNode(peer) : + new AutomationNode(peer); + } + + private static UiaControlTypeId ToUiaControlType(AutomationControlType role) + { + return role switch + { + AutomationControlType.None => UiaControlTypeId.Group, + AutomationControlType.Button => UiaControlTypeId.Button, + AutomationControlType.Calendar => UiaControlTypeId.Calendar, + AutomationControlType.CheckBox => UiaControlTypeId.CheckBox, + AutomationControlType.ComboBox => UiaControlTypeId.ComboBox, + AutomationControlType.ComboBoxItem => UiaControlTypeId.ListItem, + AutomationControlType.Edit => UiaControlTypeId.Edit, + AutomationControlType.Hyperlink => UiaControlTypeId.Hyperlink, + AutomationControlType.Image => UiaControlTypeId.Image, + AutomationControlType.ListItem => UiaControlTypeId.ListItem, + AutomationControlType.List => UiaControlTypeId.List, + AutomationControlType.Menu => UiaControlTypeId.Menu, + AutomationControlType.MenuBar => UiaControlTypeId.MenuBar, + AutomationControlType.MenuItem => UiaControlTypeId.MenuItem, + AutomationControlType.ProgressBar => UiaControlTypeId.ProgressBar, + AutomationControlType.RadioButton => UiaControlTypeId.RadioButton, + AutomationControlType.ScrollBar => UiaControlTypeId.ScrollBar, + AutomationControlType.Slider => UiaControlTypeId.Slider, + AutomationControlType.Spinner => UiaControlTypeId.Spinner, + AutomationControlType.StatusBar => UiaControlTypeId.StatusBar, + AutomationControlType.Tab => UiaControlTypeId.Tab, + AutomationControlType.TabItem => UiaControlTypeId.TabItem, + AutomationControlType.Text => UiaControlTypeId.Text, + AutomationControlType.ToolBar => UiaControlTypeId.ToolBar, + AutomationControlType.ToolTip => UiaControlTypeId.ToolTip, + AutomationControlType.Tree => UiaControlTypeId.Tree, + AutomationControlType.TreeItem => UiaControlTypeId.TreeItem, + AutomationControlType.Custom => UiaControlTypeId.Custom, + AutomationControlType.Group => UiaControlTypeId.Group, + AutomationControlType.Thumb => UiaControlTypeId.Thumb, + AutomationControlType.DataGrid => UiaControlTypeId.DataGrid, + AutomationControlType.DataItem => UiaControlTypeId.DataItem, + AutomationControlType.Document => UiaControlTypeId.Document, + AutomationControlType.SplitButton => UiaControlTypeId.SplitButton, + AutomationControlType.Window => UiaControlTypeId.Window, + AutomationControlType.Pane => UiaControlTypeId.Pane, + AutomationControlType.Header => UiaControlTypeId.Header, + AutomationControlType.HeaderItem => UiaControlTypeId.HeaderItem, + AutomationControlType.Table => UiaControlTypeId.Table, + AutomationControlType.TitleBar => UiaControlTypeId.TitleBar, + AutomationControlType.Separator => UiaControlTypeId.Separator, + _ => UiaControlTypeId.Custom, + }; + } + } +} diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs new file mode 100644 index 0000000000..1085aa1b42 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -0,0 +1,73 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Platform; +using Avalonia.Win32.Interop.Automation; + +#nullable enable + +namespace Avalonia.Win32.Automation +{ + internal class RootAutomationNode : AutomationNode, + IRawElementProviderFragmentRoot + { + public RootAutomationNode(AutomationPeer peer) + : base(peer) + { + Peer = base.Peer.GetProvider() ?? throw new AvaloniaInternalException( + "Attempt to create RootAutomationNode from peer which does not implement IRootProvider."); + Peer.FocusChanged += FocusChanged; + } + + public override IRawElementProviderFragmentRoot? FragmentRoot => this; + public new IRootProvider Peer { get; } + public IWindowBaseImpl? WindowImpl => Peer.PlatformImpl as IWindowBaseImpl; + + public IRawElementProviderFragment? ElementProviderFromPoint(double x, double y) + { + if (WindowImpl is null) + return null; + + var p = WindowImpl.PointToClient(new PixelPoint((int)x, (int)y)); + var peer = (WindowBaseAutomationPeer)Peer; + var found = InvokeSync(() => peer.GetPeerFromPoint(p)); + var result = GetOrCreate(found) as IRawElementProviderFragment; + return result; + } + + public IRawElementProviderFragment? GetFocus() + { + var focus = InvokeSync(() => Peer.GetFocus()); + return GetOrCreate(focus); + } + + public void FocusChanged(object sender, EventArgs e) + { + RaiseFocusChanged(GetOrCreate(Peer.GetFocus())); + } + + public Rect ToScreen(Rect rect) + { + if (WindowImpl is null) + return default; + return new PixelRect( + WindowImpl.PointToScreen(rect.TopLeft), + WindowImpl.PointToScreen(rect.BottomRight)) + .ToRect(1); + } + + public override IRawElementProviderSimple? HostRawElementProvider + { + get + { + var handle = WindowImpl?.Handle.Handle ?? IntPtr.Zero; + if (handle == IntPtr.Zero) + return null; + var hr = UiaCoreProviderApi.UiaHostProviderFromHwnd(handle, out var result); + Marshal.ThrowExceptionForHR(hr); + return result; + } + } + } +} diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index d727acdc22..b9a1880435 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -7,11 +7,15 @@ + + + - + + diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs index a18116aade..7538dedfca 100644 --- a/src/Windows/Avalonia.Win32/ClipboardFormats.cs +++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs @@ -14,11 +14,11 @@ namespace Avalonia.Win32 class ClipboardFormat { - public short Format { get; private set; } + public ushort Format { get; private set; } public string Name { get; private set; } - public short[] Synthesized { get; private set; } + public ushort[] Synthesized { get; private set; } - public ClipboardFormat(string name, short format, params short[] synthesized) + public ClipboardFormat(string name, ushort format, params ushort[] synthesized) { Format = format; Name = name; @@ -28,12 +28,12 @@ namespace Avalonia.Win32 private static readonly List FormatList = new List() { - new ClipboardFormat(DataFormats.Text, (short)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (short)UnmanagedMethods.ClipboardFormat.CF_TEXT), - new ClipboardFormat(DataFormats.FileNames, (short)UnmanagedMethods.ClipboardFormat.CF_HDROP), + new ClipboardFormat(DataFormats.Text, (ushort)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (ushort)UnmanagedMethods.ClipboardFormat.CF_TEXT), + new ClipboardFormat(DataFormats.FileNames, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP), }; - private static string QueryFormatName(short format) + private static string QueryFormatName(ushort format) { StringBuilder sb = new StringBuilder(MAX_FORMAT_NAME_LENGTH); if (UnmanagedMethods.GetClipboardFormatName(format, sb, sb.Capacity) > 0) @@ -41,7 +41,7 @@ namespace Avalonia.Win32 return null; } - public static string GetFormat(short format) + public static string GetFormat(ushort format) { lock (FormatList) { @@ -58,7 +58,7 @@ namespace Avalonia.Win32 } } - public static short GetFormat(string format) + public static ushort GetFormat(string format) { lock (FormatList) { @@ -68,7 +68,7 @@ namespace Avalonia.Win32 int id = UnmanagedMethods.RegisterClipboardFormat(format); if (id == 0) throw new Win32Exception(); - pd = new ClipboardFormat(format, (short)id); + pd = new ClipboardFormat(format, (ushort)id); FormatList.Add(pd); } return pd.Format; diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 047b7c361f..7cf8b14bed 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -78,12 +78,13 @@ namespace Avalonia.Win32 public async Task SetDataObjectAsync(IDataObject data) { Dispatcher.UIThread.VerifyAccess(); - var wrapper = new DataObject(data); + using var wrapper = new DataObject(data); var i = OleRetryCount; while (true) { - var hr = UnmanagedMethods.OleSetClipboard(wrapper); + var ptr = MicroCom.MicroComRuntime.GetNativeIntPtr(wrapper); + var hr = UnmanagedMethods.OleSetClipboard(ptr); if (hr == 0) break; @@ -106,9 +107,9 @@ namespace Avalonia.Win32 if (hr == 0) { - var wrapper = new OleDataObject(dataObject); + using var proxy = MicroCom.MicroComRuntime.CreateProxyFor(dataObject, true); + using var wrapper = new OleDataObject(proxy); var formats = wrapper.GetDataFormats().ToArray(); - Marshal.ReleaseComObject(dataObject); return formats; } @@ -130,9 +131,9 @@ namespace Avalonia.Win32 if (hr == 0) { - var wrapper = new OleDataObject(dataObject); + using var proxy = MicroCom.MicroComRuntime.CreateProxyFor(dataObject, true); + using var wrapper = new OleDataObject(proxy); var rv = wrapper.Get(format); - Marshal.ReleaseComObject(dataObject); return rv; } diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index 3066403186..7a96ca9ee0 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -8,22 +8,25 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization.Formatters.Binary; using Avalonia.Input; +using Avalonia.MicroCom; using Avalonia.Win32.Interop; + +using FORMATETC = Avalonia.Win32.Interop.FORMATETC; using IDataObject = Avalonia.Input.IDataObject; namespace Avalonia.Win32 { - class DataObject : IDataObject, IOleDataObject + internal sealed class DataObject : CallbackBase, IDataObject, Win32Com.IDataObject { // Compatibility with WinForms + WPF... internal static readonly byte[] SerializedObjectGUID = new Guid("FD9EA796-3B13-4370-A679-56106BB288FB").ToByteArray(); - class FormatEnumerator : IEnumFORMATETC + class FormatEnumerator : CallbackBase, Win32Com.IEnumFORMATETC { private FORMATETC[] _formats; - private int _current; + private uint _current; - private FormatEnumerator(FORMATETC[] formats, int current) + private FormatEnumerator(FORMATETC[] formats, uint current) { _formats = formats; _current = current; @@ -46,59 +49,70 @@ namespace Avalonia.Win32 return result; } - public void Clone(out IEnumFORMATETC newEnum) - { - newEnum = new FormatEnumerator(_formats, _current); - } - - public int Next(int celt, FORMATETC[] rgelt, int[] pceltFetched) + public unsafe uint Next(uint celt, FORMATETC* rgelt, uint* results) { if (rgelt == null) - return unchecked((int)UnmanagedMethods.HRESULT.E_INVALIDARG); + return (uint)UnmanagedMethods.HRESULT.E_INVALIDARG; - int i = 0; + uint i = 0; while (i < celt && _current < _formats.Length) { rgelt[i] = _formats[_current]; _current++; i++; } - if (pceltFetched != null) - pceltFetched[0] = i; if (i != celt) - return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE); - return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + return (uint)UnmanagedMethods.HRESULT.S_FALSE; + + // "results" parameter can be NULL if celt is 1. + if (celt != 1 || results != default) + *results = i; + return 0; + } + + public uint Skip(uint celt) + { + _current += Math.Min(celt, int.MaxValue - _current); + if (_current >= _formats.Length) + return (uint)UnmanagedMethods.HRESULT.S_FALSE; + return 0; } - public int Reset() + public void Reset() { _current = 0; - return unchecked((int)UnmanagedMethods.HRESULT.S_OK); } - public int Skip(int celt) + public Win32Com.IEnumFORMATETC Clone() { - _current += Math.Min(celt, int.MaxValue - _current); - if (_current >= _formats.Length) - return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE); - return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + return new FormatEnumerator(_formats, _current); } } - private const int DV_E_TYMED = unchecked((int)0x80040069); - private const int DV_E_DVASPECT = unchecked((int)0x8004006B); - private const int DV_E_FORMATETC = unchecked((int)0x80040064); - private const int OLE_E_ADVISENOTSUPPORTED = unchecked((int)0x80040003); - private const int STG_E_MEDIUMFULL = unchecked((int)0x80030070); + private const uint DV_E_TYMED = 0x80040069; + private const uint DV_E_DVASPECT = 0x8004006B; + private const uint DV_E_FORMATETC = 0x80040064; + private const uint OLE_E_ADVISENOTSUPPORTED = 0x80040003; + private const uint STG_E_MEDIUMFULL = 0x80030070; + private const int GMEM_ZEROINIT = 0x0040; private const int GMEM_MOVEABLE = 0x0002; - IDataObject _wrapped; - + private IDataObject _wrapped; + public DataObject(IDataObject wrapped) { + if (wrapped == null) + { + throw new ArgumentNullException(nameof(wrapped)); + } + if (_wrapped is DataObject || _wrapped is OleDataObject) + { + throw new InvalidOperationException(); + } + _wrapped = wrapped; } @@ -131,123 +145,123 @@ namespace Avalonia.Win32 #region IOleDataObject - int IOleDataObject.DAdvise(ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection) + unsafe int Win32Com.IDataObject.DAdvise(FORMATETC* pFormatetc, int advf, void* adviseSink) { - if (_wrapped is IOleDataObject ole) - return ole.DAdvise(ref pFormatetc, advf, adviseSink, out connection); - connection = 0; - return OLE_E_ADVISENOTSUPPORTED; + if (_wrapped is Win32Com.IDataObject ole) + return ole.DAdvise(pFormatetc, advf, adviseSink); + return 0; } - void IOleDataObject.DUnadvise(int connection) + void Win32Com.IDataObject.DUnadvise(int connection) { - if (_wrapped is IOleDataObject ole) + if (_wrapped is Win32Com.IDataObject ole) ole.DUnadvise(connection); - Marshal.ThrowExceptionForHR(OLE_E_ADVISENOTSUPPORTED); + throw new COMException(nameof(OLE_E_ADVISENOTSUPPORTED), unchecked((int)OLE_E_ADVISENOTSUPPORTED)); } - int IOleDataObject.EnumDAdvise(out IEnumSTATDATA enumAdvise) + unsafe void* Win32Com.IDataObject.EnumDAdvise() { - if (_wrapped is IOleDataObject ole) - return ole.EnumDAdvise(out enumAdvise); + if (_wrapped is Win32Com.IDataObject ole) + return ole.EnumDAdvise(); - enumAdvise = null; - return OLE_E_ADVISENOTSUPPORTED; + return null; } - IEnumFORMATETC IOleDataObject.EnumFormatEtc(DATADIR direction) + Win32Com.IEnumFORMATETC Win32Com.IDataObject.EnumFormatEtc(int direction) { - if (_wrapped is IOleDataObject ole) + if (_wrapped is Win32Com.IDataObject ole) return ole.EnumFormatEtc(direction); - if (direction == DATADIR.DATADIR_GET) + if ((DATADIR)direction == DATADIR.DATADIR_GET) return new FormatEnumerator(_wrapped); - throw new NotSupportedException(); + throw new COMException(nameof(UnmanagedMethods.HRESULT.E_NOTIMPL), unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL)); } - int IOleDataObject.GetCanonicalFormatEtc(ref FORMATETC formatIn, out FORMATETC formatOut) + unsafe FORMATETC Win32Com.IDataObject.GetCanonicalFormatEtc(FORMATETC* formatIn) { - if (_wrapped is IOleDataObject ole) - return ole.GetCanonicalFormatEtc(ref formatIn, out formatOut); + if (_wrapped is Win32Com.IDataObject ole) + return ole.GetCanonicalFormatEtc(formatIn); - formatOut = new FORMATETC(); + var formatOut = new FORMATETC(); formatOut.ptd = IntPtr.Zero; - return unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL); + + throw new COMException(nameof(UnmanagedMethods.HRESULT.E_NOTIMPL), unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL)); } - void IOleDataObject.GetData(ref FORMATETC format, out STGMEDIUM medium) + unsafe uint Win32Com.IDataObject.GetData(FORMATETC* format, Interop.STGMEDIUM* medium) { - if (_wrapped is IOleDataObject ole) + if (_wrapped is Win32Com.IDataObject ole) { - ole.GetData(ref format, out medium); - return; + return ole.GetData(format, medium); } - if(!format.tymed.HasAllFlags(TYMED.TYMED_HGLOBAL)) - Marshal.ThrowExceptionForHR(DV_E_TYMED); - if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) - Marshal.ThrowExceptionForHR(DV_E_DVASPECT); + if (!format->tymed.HasAllFlags(TYMED.TYMED_HGLOBAL)) + return DV_E_TYMED; + + if (format->dwAspect != DVASPECT.DVASPECT_CONTENT) + return DV_E_DVASPECT; - string fmt = ClipboardFormats.GetFormat(format.cfFormat); + string fmt = ClipboardFormats.GetFormat(format->cfFormat); if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt)) - Marshal.ThrowExceptionForHR(DV_E_FORMATETC); + return DV_E_FORMATETC; - medium = default(STGMEDIUM); - medium.tymed = TYMED.TYMED_HGLOBAL; - int result = WriteDataToHGlobal(fmt, ref medium.unionmember); - Marshal.ThrowExceptionForHR(result); + * medium = default(Interop.STGMEDIUM); + medium->tymed = TYMED.TYMED_HGLOBAL; + return WriteDataToHGlobal(fmt, ref medium->unionmember); } - void IOleDataObject.GetDataHere(ref FORMATETC format, ref STGMEDIUM medium) + unsafe uint Win32Com.IDataObject.GetDataHere(FORMATETC* format, Interop.STGMEDIUM* medium) { - if (_wrapped is IOleDataObject ole) + if (_wrapped is Win32Com.IDataObject ole) { - ole.GetDataHere(ref format, ref medium); - return; + return ole.GetDataHere(format, medium); } - if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasAllFlags(TYMED.TYMED_HGLOBAL)) - Marshal.ThrowExceptionForHR(DV_E_TYMED); + if (medium->tymed != TYMED.TYMED_HGLOBAL || !format->tymed.HasAllFlags(TYMED.TYMED_HGLOBAL)) + return DV_E_TYMED; - if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) - Marshal.ThrowExceptionForHR(DV_E_DVASPECT); + if (format->dwAspect != DVASPECT.DVASPECT_CONTENT) + return DV_E_DVASPECT; - string fmt = ClipboardFormats.GetFormat(format.cfFormat); + string fmt = ClipboardFormats.GetFormat(format->cfFormat); if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt)) - Marshal.ThrowExceptionForHR(DV_E_FORMATETC); + return DV_E_FORMATETC; - if (medium.unionmember == IntPtr.Zero) - Marshal.ThrowExceptionForHR(STG_E_MEDIUMFULL); + if (medium->unionmember == IntPtr.Zero) + return STG_E_MEDIUMFULL; - int result = WriteDataToHGlobal(fmt, ref medium.unionmember); - Marshal.ThrowExceptionForHR(result); + return WriteDataToHGlobal(fmt, ref medium->unionmember); } - int IOleDataObject.QueryGetData(ref FORMATETC format) + unsafe uint Win32Com.IDataObject.QueryGetData(FORMATETC* format) { - if (_wrapped is IOleDataObject ole) - return ole.QueryGetData(ref format); - if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) + if (_wrapped is Win32Com.IDataObject ole) + { + return ole.QueryGetData(format); + } + + if (format->dwAspect != DVASPECT.DVASPECT_CONTENT) return DV_E_DVASPECT; - if (!format.tymed.HasAllFlags(TYMED.TYMED_HGLOBAL)) + if (!format->tymed.HasAllFlags(TYMED.TYMED_HGLOBAL)) return DV_E_TYMED; - string dataFormat = ClipboardFormats.GetFormat(format.cfFormat); - if (!string.IsNullOrEmpty(dataFormat) && _wrapped.Contains(dataFormat)) - return unchecked((int)UnmanagedMethods.HRESULT.S_OK); - return DV_E_FORMATETC; + var dataFormat = ClipboardFormats.GetFormat(format->cfFormat); + + if (string.IsNullOrEmpty(dataFormat) || !_wrapped.Contains(dataFormat)) + return DV_E_FORMATETC; + + return 0; } - void IOleDataObject.SetData(ref FORMATETC formatIn, ref STGMEDIUM medium, bool release) + unsafe uint Win32Com.IDataObject.SetData(FORMATETC* pformatetc, Interop.STGMEDIUM* pmedium, int fRelease) { - if (_wrapped is IOleDataObject ole) + if (_wrapped is Win32Com.IDataObject ole) { - ole.SetData(ref formatIn, ref medium, release); - return; + return ole.SetData(pformatetc, pmedium, fRelease); } - Marshal.ThrowExceptionForHR(unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL)); + return (uint)UnmanagedMethods.HRESULT.E_NOTIMPL; } - private int WriteDataToHGlobal(string dataFormat, ref IntPtr hGlobal) + private uint WriteDataToHGlobal(string dataFormat, ref IntPtr hGlobal) { object data = _wrapped.Get(dataFormat); if (dataFormat == DataFormats.Text || data is string) @@ -288,7 +302,7 @@ namespace Avalonia.Win32 } } - private unsafe int WriteBytesToHGlobal(ref IntPtr hGlobal, ReadOnlySpan data) + private unsafe uint WriteBytesToHGlobal(ref IntPtr hGlobal, ReadOnlySpan data) { int required = data.Length; if (hGlobal == IntPtr.Zero) @@ -312,7 +326,7 @@ namespace Avalonia.Win32 } } - private int WriteFileListToHGlobal(ref IntPtr hGlobal, IEnumerable files) + private uint WriteFileListToHGlobal(ref IntPtr hGlobal, IEnumerable files) { if (!files?.Any() ?? false) return unchecked((int)UnmanagedMethods.HRESULT.S_OK); @@ -344,7 +358,7 @@ namespace Avalonia.Win32 } } - private int WriteStringToHGlobal(ref IntPtr hGlobal, string data) + private uint WriteStringToHGlobal(ref IntPtr hGlobal, string data) { int required = (data.Length + 1) * sizeof(char); if (hGlobal == IntPtr.Zero) @@ -367,6 +381,15 @@ namespace Avalonia.Win32 } } + protected override void Destroyed() + { + ReleaseWrapped(); + } + + public void ReleaseWrapped() + { + _wrapped = null; + } #endregion } } diff --git a/src/Windows/Avalonia.Win32/DragSource.cs b/src/Windows/Avalonia.Win32/DragSource.cs index f74e59a8f8..1159c5bfc9 100644 --- a/src/Windows/Avalonia.Win32/DragSource.cs +++ b/src/Windows/Avalonia.Win32/DragSource.cs @@ -8,17 +8,26 @@ namespace Avalonia.Win32 { class DragSource : IPlatformDragSource { - public Task DoDragDrop(PointerEventArgs triggerEvent, + public unsafe Task DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects) { Dispatcher.UIThread.VerifyAccess(); + triggerEvent.Pointer.Capture(null); - OleDragSource src = new OleDragSource(); - DataObject dataObject = new DataObject(data); - int allowed = (int)OleDropTarget.ConvertDropEffect(allowedEffects); + + using var dataObject = new DataObject(data); + using var src = new OleDragSource(); + var allowed = OleDropTarget.ConvertDropEffect(allowedEffects); + + var objPtr = MicroCom.MicroComRuntime.GetNativeIntPtr(dataObject); + var srcPtr = MicroCom.MicroComRuntime.GetNativeIntPtr(src); + + UnmanagedMethods.DoDragDrop(objPtr, srcPtr, (int)allowed, out var finalEffect); + + // Force releasing of internal wrapper to avoid memory leak, if drop target keeps com reference. + dataObject.ReleaseWrapped(); - UnmanagedMethods.DoDragDrop(dataObject, src, allowed, out var finalEffect); - return Task.FromResult(OleDropTarget.ConvertDropEffect((DropEffect)finalEffect)); + return Task.FromResult(OleDropTarget.ConvertDropEffect((Win32Com.DropEffect)finalEffect)); } } } diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index 71e33554f1..3adefd965f 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -37,6 +37,24 @@ namespace Avalonia.Win32.Input IsComposing = false; } + public void ClearLanguageAndWindow() + { + if (HWND != IntPtr.Zero && _defaultImc != IntPtr.Zero) + { + ImmReleaseContext(HWND, _defaultImc); + } + + _defaultImc = IntPtr.Zero; + HWND = IntPtr.Zero; + _parent = null; + _active = false; + _langId = 0; + _showCompositionWindow = false; + _showCandidateList = false; + + IsComposing = false; + } + //Dependant on CurrentThread. When Avalonia will support Multiple Dispatchers - //every Dispatcher should have their own InputMethod. public static Imm32InputMethod Current { get; } = new Imm32InputMethod(); @@ -74,12 +92,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 +234,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..b1e4d8ca01 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 }; @@ -906,9 +914,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumDelegate lpfnEnum, IntPtr dwData); - + public delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData); - + [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr GetDC(IntPtr hWnd); @@ -999,7 +1007,7 @@ namespace Avalonia.Win32.Interop public static uint GetWindowLong(IntPtr hWnd, int nIndex) { - if(IntPtr.Size == 4) + if (IntPtr.Size == 4) { return GetWindowLong32b(hWnd, nIndex); } @@ -1026,7 +1034,7 @@ namespace Avalonia.Win32.Interop return (uint)SetWindowLong64b(hWnd, nIndex, new IntPtr((uint)value)).ToInt32(); } } - + public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr handle) { if (IntPtr.Size == 4) @@ -1060,14 +1068,14 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool InvalidateRect(IntPtr hWnd, RECT* lpRect, bool bErase); - - + + [DllImport("user32.dll")] public static extern bool ValidateRect(IntPtr hWnd, IntPtr lpRect); [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr hWnd); - + [DllImport("user32.dll")] public static extern bool IsWindowEnabled(IntPtr hWnd); @@ -1094,22 +1102,25 @@ namespace Avalonia.Win32.Interop [DllImport("user32")] public static extern IntPtr GetMessageExtraInfo(); - + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "RegisterClassExW")] public static extern ushort RegisterClassEx(ref WNDCLASSEX lpwcx); [DllImport("user32.dll")] public static extern void RegisterTouchWindow(IntPtr hWnd, int flags); - + [DllImport("user32.dll")] public static extern bool ReleaseCapture(); + [DllImport("user32.dll", SetLastError = true)] + public static extern uint RegisterWindowMessage(string lpString); + [DllImport("user32.dll")] public static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr GetActiveWindow(); - + [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr SetActiveWindow(IntPtr hWnd); @@ -1277,10 +1288,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); @@ -1296,7 +1307,7 @@ namespace Avalonia.Win32.Interop [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr LoadLibrary(string fileName); - + [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr LoadLibraryEx(string fileName, IntPtr hFile, int flags); @@ -1340,7 +1351,7 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr MonitorFromWindow(IntPtr hwnd, MONITOR dwFlags); - + [DllImport("user32", EntryPoint = "GetMonitorInfoW", ExactSpelling = true, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetMonitorInfo([In] IntPtr hMonitor, ref MONITORINFO lpmi); @@ -1348,14 +1359,14 @@ namespace Avalonia.Win32.Interop [DllImport("user32")] public static extern unsafe bool GetTouchInputInfo( IntPtr hTouchInput, - uint cInputs, + uint cInputs, TOUCHINPUT* pInputs, - int cbSize + int cbSize ); - + [DllImport("user32")] public static extern bool CloseTouchInputHandle(IntPtr hTouchInput); - + [return: MarshalAs(UnmanagedType.Bool)] [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")] public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); @@ -1364,7 +1375,7 @@ namespace Avalonia.Win32.Interop public static extern int SetDIBitsToDevice(IntPtr hdc, int XDest, int YDest, uint dwWidth, uint dwHeight, int XSrc, int YSrc, uint uStartScan, uint cScanLines, IntPtr lpvBits, [In] ref BITMAPINFOHEADER lpbmi, uint fuColorUse); - + [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool CloseHandle(IntPtr hObject); @@ -1379,27 +1390,27 @@ namespace Avalonia.Win32.Interop [DllImport("gdi32.dll")] public static extern int ChoosePixelFormat(IntPtr hdc, ref PixelFormatDescriptor pfd); - + [DllImport("gdi32.dll")] public static extern int DescribePixelFormat(IntPtr hdc, ref PixelFormatDescriptor pfd); [DllImport("gdi32.dll")] public static extern int SetPixelFormat(IntPtr hdc, int iPixelFormat, ref PixelFormatDescriptor pfd); - - + + [DllImport("gdi32.dll")] public static extern int DescribePixelFormat(IntPtr hdc, int iPixelFormat, int bytes, ref PixelFormatDescriptor pfd); - + [DllImport("gdi32.dll")] public static extern bool SwapBuffers(IntPtr hdc); [DllImport("opengl32.dll")] public static extern IntPtr wglCreateContext(IntPtr hdc); - + [DllImport("opengl32.dll")] public static extern bool wglDeleteContext(IntPtr context); - + [DllImport("opengl32.dll")] public static extern bool wglMakeCurrent(IntPtr hdc, IntPtr context); @@ -1420,11 +1431,11 @@ namespace Avalonia.Win32.Interop uint dwMaximumSizeLow, string lpName); - [DllImport("msvcrt.dll", EntryPoint="memcpy", SetLastError = false, CallingConvention=CallingConvention.Cdecl)] - public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count); - + [DllImport("msvcrt.dll", EntryPoint = "memcpy", SetLastError = false, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count); + [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] - public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target); + 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 +1459,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,15 +1467,18 @@ 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); [DllImport("dwmapi.dll")] public static extern void DwmFlush(); - + [DllImport("dwmapi.dll")] public static extern bool DwmDefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult); - + [DllImport("dwmapi.dll")] public static extern void DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind); @@ -1531,8 +1545,8 @@ namespace Avalonia.Win32.Interop throw new Exception("RtlGetVersion failed!"); } } - - [DllImport("kernel32", EntryPoint="WaitForMultipleObjectsEx", SetLastError = true, CharSet = CharSet.Auto)] + + [DllImport("kernel32", EntryPoint = "WaitForMultipleObjectsEx", SetLastError = true, CharSet = CharSet.Auto)] private static extern int IntWaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable); public const int WAIT_FAILED = unchecked((int)0xFFFFFFFF); @@ -1540,7 +1554,7 @@ namespace Avalonia.Win32.Interop internal static int WaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable) { int result = IntWaitForMultipleObjectsEx(nCount, pHandles, bWaitAll, dwMilliseconds, bAlertable); - if(result == WAIT_FAILED) + if (result == WAIT_FAILED) { throw new Win32Exception(); } @@ -1688,7 +1702,7 @@ namespace Avalonia.Win32.Interop DrawLeftBorder = 0x20, DrawTopBorder = 0x40, DrawRightBorder = 0x80, - DrawBottomBorder = 0x100, + DrawBottomBorder = 0x100, } [StructLayout(LayoutKind.Sequential)] @@ -1756,9 +1770,9 @@ namespace Avalonia.Win32.Interop MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2, MDT_DEFAULT = MDT_EFFECTIVE_DPI - } + } - public enum ClipboardFormat + public enum ClipboardFormat { /// /// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. Use this format for ANSI text. @@ -1809,7 +1823,7 @@ namespace Avalonia.Win32.Interop public int X; public int Y; } - + public struct SIZE { public int X; @@ -2010,7 +2024,7 @@ namespace Avalonia.Win32.Interop OFN_NOREADONLYRETURN = 0x00008000, OFN_OVERWRITEPROMPT = 0x00000002 } - + public enum HRESULT : uint { S_FALSE = 0x0001, @@ -2064,64 +2078,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 +2088,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..f3754cd58f 100644 --- a/src/Windows/Avalonia.Win32/ScreenImpl.cs +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; +using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 { + [Unstable] public class ScreenImpl : IScreenImpl { public int ScreenCount @@ -70,5 +73,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..1c2dd92219 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -5,6 +5,7 @@ using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.LogicalTree; +using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Win32.Interop; @@ -14,6 +15,7 @@ using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 { + [Unstable] public class TrayIconImpl : ITrayIconImpl { private readonly int _uniqueId; @@ -24,6 +26,7 @@ namespace Avalonia.Win32 private readonly Win32NativeToManagedMenuExporter _exporter; private static readonly Dictionary s_trayIcons = new Dictionary(); private bool _disposedValue; + private static readonly uint WM_TASKBARCREATED = UnmanagedMethods.RegisterWindowMessage("TaskbarCreated"); public TrayIconImpl() { @@ -44,6 +47,18 @@ namespace Avalonia.Win32 { s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam); } + + if (msg == WM_TASKBARCREATED) + { + foreach (var tray in s_trayIcons.Values) + { + if (tray._iconAdded) + { + tray.UpdateIcon(true); + tray.UpdateIcon(); + } + } + } } public void SetIcon(IWindowIconImpl? icon) @@ -145,7 +160,7 @@ namespace Avalonia.Win32 private enum CustomWindowsMessage : uint { WM_TRAYICON = WindowsMessage.WM_APP + 1024, - WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 + WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024, } private class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable @@ -217,8 +232,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/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 0376a41f8c..2a3f4a3384 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -24,9 +24,23 @@ namespace Avalonia.Win32 { var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); - if (egl != null && opts.UseWindowsUIComposition) + if (egl != null) { - WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); + if (opts.EglRendererBlacklist != null) + { + foreach (var item in opts.EglRendererBlacklist) + { + if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) + { + return null; + } + } + } + + if (opts.UseWindowsUIComposition) + { + WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); + } } return egl; diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 5cfbab40e4..dc5e5324c4 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -56,6 +56,11 @@ namespace Avalonia /// GPU rendering will not be enabled if this is set to false. /// public bool? AllowEglInitialization { get; set; } + + public IList EglRendererBlacklist { get; set; } = new List + { + "Microsoft Basic Render" + }; /// /// Enables multitouch support. The default value is true. diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 57b0f71306..76af12e8ca 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -17,11 +17,11 @@ namespace Avalonia.Win32.WinRT.Composition { class WinUICompositorConnection : IRenderTimer { + public static readonly Version MinHostBackdropVersion = new Version(10, 0, 22000); private readonly float? _backdropCornerRadius; private readonly EglContext _syncContext; private readonly ICompositionBrush _micaBrush; private ICompositor _compositor; - private ICompositor2 _compositor2; private ICompositor5 _compositor5; private ICompositorInterop _compositorInterop; private AngleWin32EglDisplay _angle; @@ -39,12 +39,11 @@ namespace Avalonia.Win32.WinRT.Composition _syncContext = _gl.PrimaryEglContext; _angle = (AngleWin32EglDisplay)_gl.Display; _compositor = NativeWinRTMethods.CreateInstance("Windows.UI.Composition.Compositor"); - _compositor2 = _compositor.QueryInterface(); _compositor5 = _compositor.QueryInterface(); _compositorInterop = _compositor.QueryInterface(); _compositorDesktopInterop = _compositor.QueryInterface(); using var device = MicroComRuntime.CreateProxyFor(_angle.GetDirect3DDevice(), true); - + _device = _compositorInterop.CreateGraphicsDevice(device); _blurBrush = CreateAcrylicBlurBackdropBrush(); _micaBrush = CreateMicaBackdropBrush(); @@ -71,7 +70,7 @@ namespace Avalonia.Win32.WinRT.Composition AvaloniaLocator.CurrentMutable.BindToSelf(connect); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); tcs.SetResult(true); - + } catch (Exception e) { @@ -99,7 +98,7 @@ namespace Avalonia.Win32.WinRT.Composition } public void Dispose() { - + } public void Invoke(IAsyncAction asyncInfo, AsyncStatus asyncStatus) @@ -118,12 +117,12 @@ namespace Avalonia.Win32.WinRT.Composition { } } - + private void RunLoop() { { var st = Stopwatch.StartNew(); - using (var act = _compositor5.RequestCommitAsync()) + using (var act = _compositor5.RequestCommitAsync()) act.SetCompleted(new RunLoopHandler(this)); while (true) { @@ -172,12 +171,12 @@ namespace Avalonia.Win32.WinRT.Composition using var sc = _syncContext.EnsureLocked(); using var desktopTarget = _compositorDesktopInterop.CreateDesktopWindowTarget(hWnd, 0); using var target = desktopTarget.QueryInterface(); - + using var drawingSurface = _device.CreateDrawingSurface(new UnmanagedMethods.SIZE(), DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); using var surface = drawingSurface.QueryInterface(); using var surfaceInterop = drawingSurface.QueryInterface(); - + using var surfaceBrush = _compositor.CreateSurfaceBrushWithSurface(surface); using var brush = surfaceBrush.QueryInterface(); @@ -190,7 +189,7 @@ namespace Avalonia.Win32.WinRT.Composition using var containerVisual2 = container.QueryInterface(); containerVisual2.SetRelativeSizeAdjustment(new Vector2(1, 1)); using var containerChildren = container.Children; - + target.SetRoot(containerVisual); using var blur = CreateBlurVisual(_blurBrush); @@ -202,10 +201,10 @@ namespace Avalonia.Win32.WinRT.Composition } var compositionRoundedRectangleGeometry = ClipVisual(blur, mica); - + containerChildren.InsertAtTop(blur); containerChildren.InsertAtTop(visual); - + return new WinUICompositedWindow(_syncContext, _compositor, _pumpLock, target, surfaceInterop, visual, blur, mica, compositionRoundedRectangleGeometry); } @@ -234,10 +233,8 @@ namespace Avalonia.Win32.WinRT.Composition var blurEffect = new WinUIGaussianBlurEffect(backDropParameterAsSource); using var blurEffectFactory = _compositor.CreateEffectFactory(blurEffect); using var compositionEffectBrush = blurEffectFactory.CreateBrush(); - using var backdrop = _compositor2.CreateBackdropBrush(); - using var backdropBrush = backdrop.QueryInterface(); - - + using var backdropBrush = CreateBackdropBrush(); + var saturateEffect = new SaturationEffect(blurEffect); using var satEffectFactory = _compositor.CreateEffectFactory(saturateEffect); using var sat = satEffectFactory.CreateBrush(); @@ -261,8 +258,8 @@ namespace Avalonia.Win32.WinRT.Composition foreach (var visual in containerVisuals) { visual?.SetClip(geometricClipWithGeometry.QueryInterface()); - } - + } + return roundedRectangleGeometry.CloneReference(); } @@ -271,8 +268,8 @@ namespace Avalonia.Win32.WinRT.Composition using var spriteVisual = _compositor.CreateSpriteVisual(); using var visual = spriteVisual.QueryInterface(); using var visual2 = spriteVisual.QueryInterface(); - - + + spriteVisual.SetBrush(compositionBrush); visual.SetIsVisible(0); visual2.SetRelativeSizeAdjustment(new Vector2(1.0f, 1.0f)); @@ -280,6 +277,29 @@ namespace Avalonia.Win32.WinRT.Composition return visual.CloneReference(); } + private ICompositionBrush CreateBackdropBrush() + { + ICompositionBackdropBrush brush = null; + try + { + if (Win32Platform.WindowsVersion >= MinHostBackdropVersion) + { + using var compositor3 = _compositor.QueryInterface(); + brush = compositor3.CreateHostBackdropBrush(); + } + else + { + using var compositor2 = _compositor.QueryInterface(); + brush = compositor2.CreateBackdropBrush(); + } + + return brush.QueryInterface(); + } + finally + { + brush?.Dispose(); + } + } public event Action Tick; } diff --git a/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs b/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs index 5026fbaaba..89cde01ff4 100644 --- a/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs +++ b/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs @@ -23,7 +23,7 @@ namespace Avalonia.Win32.WinRT [DllImport("api-ms-win-core-winrt-string-l1-1-0.dll", CallingConvention = CallingConvention.StdCall, PreserveSig = false)] - internal static extern unsafe IntPtr WindowsDeleteString(IntPtr hString); + internal static extern unsafe void WindowsDeleteString(IntPtr hString); [DllImport("Windows.UI.Composition", EntryPoint = "DllGetActivationFactory", CallingConvention = CallingConvention.StdCall, PreserveSig = false)] 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..cae8834550 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,14 @@ namespace Avalonia.Win32 case WindowsMessage.WM_DESTROY: { + UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null); + + // We need to release IMM context and state to avoid leaks. + if (Imm32InputMethod.Current.HWND == _hwnd) + { + Imm32InputMethod.Current.ClearLanguageAndWindow(); + } + //Window doesn't exist anymore _hwnd = IntPtr.Zero; //Remove root reference to this class, so unmanaged delegate can be collected @@ -503,6 +515,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..8d836ef452 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,18 +14,21 @@ 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; using Avalonia.Win32.WinRT; using Avalonia.Win32.WinRT.Composition; using static Avalonia.Win32.Interop.UnmanagedMethods; +using Avalonia.Metadata; namespace Avalonia.Win32 { /// /// Window implementation for Win32 platform. /// + [Unstable] public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithNativeControlHost, ITopLevelImplWithTextInputMethod @@ -84,7 +88,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 +176,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 +249,7 @@ namespace Avalonia.Win32 { get { - if(_isFullScreenActive) + if (_isFullScreenActive) { return WindowState.FullScreen; } @@ -268,7 +272,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 +280,7 @@ namespace Avalonia.Win32 protected IntPtr Hwnd => _hwnd; - public void SetTransparencyLevelHint (WindowTransparencyLevel transparencyLevel) + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { TransparencyLevel = EnableBlur(transparencyLevel); } @@ -316,12 +320,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 +381,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 +435,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 +461,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 +502,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 +550,7 @@ namespace Avalonia.Win32 if (_dropTarget != null) { OleContext.Current?.UnregisterDragDrop(Handle); + _dropTarget.Dispose(); _dropTarget = null; } @@ -540,7 +562,7 @@ namespace Avalonia.Win32 { BeforeCloseCleanup(true); } - + DestroyWindow(_hwnd); _hwnd = IntPtr.Zero; } @@ -606,7 +628,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 +652,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 +742,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 +788,7 @@ namespace Avalonia.Win32 throw new Win32Exception(); } - Handle = new PlatformHandle(_hwnd, PlatformConstants.WindowHandleType); + Handle = new WindowImplPlatformHandle(this); _multitouch = Win32Platform.Options.EnableMultitouch ?? true; @@ -773,7 +798,7 @@ namespace Avalonia.Win32 } if (ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8) - { + { var monitor = MonitorFromWindow( _hwnd, MONITOR.MONITOR_DEFAULTTONEAREST); @@ -856,14 +881,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 +911,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 +936,7 @@ namespace Avalonia.Win32 { return; } - + if (DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled) { _isClientAreaExtended = false; @@ -939,11 +964,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 +984,12 @@ namespace Avalonia.Win32 private void ShowWindow(WindowState state, bool activate) { _shown = true; - + if (_isClientAreaExtended) { ExtendClientArea(); } - + ShowWindowCommand? command; var newWindowProperties = _windowProperties; @@ -982,7 +1007,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 +1038,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 +1056,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 +1083,7 @@ namespace Avalonia.Win32 SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); } } - } + } private WindowStyles GetWindowStateStyles() { @@ -1235,7 +1260,7 @@ namespace Avalonia.Win32 SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); } - } + } } private const int MF_BYCOMMAND = 0x0; @@ -1285,9 +1310,9 @@ namespace Avalonia.Win32 public void SetExtendClientAreaToDecorationsHint(bool hint) { _isClientAreaExtended = hint; - - ExtendClientArea(); - } + + ExtendClientArea(); + } public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) { @@ -1295,7 +1320,7 @@ namespace Avalonia.Win32 ExtendClientArea(); } - + /// public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) { @@ -1309,7 +1334,7 @@ namespace Avalonia.Win32 /// public Action ExtendClientAreaToDecorationsChanged { get; set; } - + /// public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome); @@ -1348,7 +1373,7 @@ namespace Avalonia.Win32 { private readonly WindowImpl _owner; private readonly PlatformResizeReason _restore; - + public ResizeReasonScope(WindowImpl owner, PlatformResizeReason restore) { _owner = owner; @@ -1359,5 +1384,17 @@ namespace Avalonia.Win32 } public ITextInputMethodImpl TextInputMethod => Imm32InputMethod.Current; + + private class WindowImplPlatformHandle : IPlatformNativeSurfaceHandle + { + private readonly WindowImpl _owner; + public WindowImplPlatformHandle(WindowImpl owner) => _owner = owner; + public IntPtr Handle => _owner.Hwnd; + public string HandleDescriptor => PlatformConstants.WindowHandleType; + + public PixelSize Size => PixelSize.FromSize(_owner.ClientSize, Scaling); + + public double Scaling => _owner.RenderScaling; + } } } diff --git a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj index e9015d857c..ea3a79941d 100644 --- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj +++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj @@ -1,17 +1,13 @@ - + - xamarin.ios10 - true - latest + net6.0-ios + 10.0 + true - - - - - - TargetFramework=netstandard2.0 - + + + diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs index b75aad17cf..f976b2feb4 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs @@ -1,6 +1,7 @@ +using Foundation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -using Foundation; + using UIKit; namespace Avalonia.iOS @@ -8,7 +9,18 @@ namespace Avalonia.iOS public class AvaloniaAppDelegate : UIResponder, IUIApplicationDelegate where TApp : Application, new() { - protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder; + class SingleViewLifetime : ISingleViewApplicationLifetime + { + public AvaloniaView View; + + public Control MainView + { + get => View.Content; + set => View.Content = value; + } + } + + protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseiOS(); [Export("window")] public UIWindow Window { get; set; } @@ -18,7 +30,9 @@ namespace Avalonia.iOS { var builder = AppBuilder.Configure(); CustomizeAppBuilder(builder); - var lifetime = new Lifetime(); + + var lifetime = new SingleViewLifetime(); + builder.AfterSetup(_ => { Window = new UIWindow(); @@ -35,15 +49,5 @@ namespace Avalonia.iOS Window.Hidden = false; return true; } - - class Lifetime : ISingleViewApplicationLifetime - { - public AvaloniaView View; - public Control MainView - { - get => View.Content; - set => View.Content = value; - } - } } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs index dc963726b0..d49ce5310c 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs @@ -1,32 +1,146 @@ -using Avalonia.Input; -using Avalonia.Input.Raw; using Foundation; using ObjCRuntime; +using Avalonia.Input.TextInput; +using Avalonia.Input; +using Avalonia.Input.Raw; using UIKit; -namespace Avalonia.iOS +namespace Avalonia.iOS; + +#nullable enable + +[Adopts("UITextInputTraits")] +[Adopts("UIKeyInput")] +public partial class AvaloniaView : ITextInputMethodImpl { - [Adopts("UIKeyInput")] - public partial class AvaloniaView + private ITextInputMethodClient? _currentClient; + + public override bool CanResignFirstResponder => true; + public override bool CanBecomeFirstResponder => true; + + [Export("hasText")] + public bool HasText { - public override bool CanBecomeFirstResponder => true; + get + { + if (_currentClient is { } && _currentClient.SupportsSurroundingText && + _currentClient.SurroundingText.Text.Length > 0) + { + return true; + } + + return false; + } + } + + [Export("keyboardType")] public UIKeyboardType KeyboardType { get; private set; } = UIKeyboardType.Default; - [Export("hasText")] public bool HasText => false; + [Export("isSecureTextEntry")] public bool IsSecureEntry { get; private set; } - [Export("insertText:")] - public void InsertText(string text) => + [Export("insertText:")] + public void InsertText(string text) + { + if (KeyboardDevice.Instance is { }) + { _topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance, 0, InputRoot, text)); + } + } - [Export("deleteBackward")] - public void DeleteBackward() + [Export("deleteBackward")] + public void DeleteBackward() + { + if (KeyboardDevice.Instance is { }) { // TODO: pass this through IME infrastructure instead of emulating a backspace press _topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance, 0, InputRoot, RawKeyEventType.KeyDown, Key.Back, RawInputModifiers.None)); - + _topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance, 0, InputRoot, RawKeyEventType.KeyUp, Key.Back, RawInputModifiers.None)); } } -} \ No newline at end of file + + void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) + { + _currentClient = client; + + if (client is { }) + { + BecomeFirstResponder(); + } + else + { + ResignFirstResponder(); + } + } + + void ITextInputMethodImpl.SetCursorRect(Rect rect) + { + + } + + void ITextInputMethodImpl.SetOptions(TextInputOptions options) + { + IsSecureEntry = false; + + switch (options.ContentType) + { + case TextInputContentType.Normal: + KeyboardType = UIKeyboardType.Default; + break; + + case TextInputContentType.Alpha: + KeyboardType = UIKeyboardType.AsciiCapable; + break; + + case TextInputContentType.Digits: + KeyboardType = UIKeyboardType.PhonePad; + break; + + case TextInputContentType.Pin: + KeyboardType = UIKeyboardType.NumberPad; + IsSecureEntry = true; + break; + + case TextInputContentType.Number: + KeyboardType = UIKeyboardType.PhonePad; + break; + + case TextInputContentType.Email: + KeyboardType = UIKeyboardType.EmailAddress; + break; + + case TextInputContentType.Url: + KeyboardType = UIKeyboardType.Url; + break; + + case TextInputContentType.Name: + KeyboardType = UIKeyboardType.NamePhonePad; + break; + + case TextInputContentType.Password: + KeyboardType = UIKeyboardType.Default; + IsSecureEntry = true; + break; + + case TextInputContentType.Social: + KeyboardType = UIKeyboardType.Twitter; + break; + + case TextInputContentType.Search: + KeyboardType = UIKeyboardType.WebSearch; + break; + } + + if (options.IsSensitive) + { + IsSecureEntry = true; + } + } + + void ITextInputMethodImpl.Reset() + { + ResignFirstResponder(); + } +} diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 5bb2f64879..e8108dd3de 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -2,12 +2,13 @@ using System; using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Controls.Embedding; +using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.Platform; using Avalonia.Rendering; using CoreAnimation; -using CoreGraphics; using Foundation; using ObjCRuntime; using OpenGLES; @@ -42,7 +43,7 @@ namespace Avalonia.iOS MultipleTouchEnabled = true; } - internal class TopLevelImpl : ITopLevelImpl + internal class TopLevelImpl : ITopLevelImplWithTextInputMethod { private readonly AvaloniaView _view; public AvaloniaView View => _view; @@ -109,6 +110,8 @@ namespace Avalonia.iOS public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(); + + public ITextInputMethodImpl? TextInputMethod => _view; } [Export("layerClass")] diff --git a/src/iOS/Avalonia.iOS/Boilerplate/AppBuilder.cs b/src/iOS/Avalonia.iOS/Boilerplate/AppBuilder.cs deleted file mode 100644 index d5830510f6..0000000000 --- a/src/iOS/Avalonia.iOS/Boilerplate/AppBuilder.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Avalonia.Controls; -using Avalonia.PlatformSupport; - -namespace Avalonia -{ - public class AppBuilder : AppBuilderBase - { - public AppBuilder() : base(new StandardRuntimePlatform(), - b => StandardRuntimePlatformServices.Register(b.ApplicationType.Assembly)) - { - this.UseSkia().UseWindowingSubsystem(iOS.Platform.Register); - } - } -} diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index f9c787b6a8..bd1969081d 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -2,7 +2,6 @@ using System; using System.Reactive.Disposables; using Avalonia.OpenGL; using OpenGLES; -using OpenTK.Graphics.ES30; namespace Avalonia.iOS { @@ -75,7 +74,21 @@ namespace Avalonia.iOS public GlVersion Version { get; } = new GlVersion(GlProfileType.OpenGLES, 3, 0); public GlInterface GlInterface { get; } - public int SampleCount { get; } = 0; - public int StencilSize { get; } = 9; + public int SampleCount + { + get + { + GlInterface.GetIntegerv(GlConsts.GL_SAMPLES, out var samples); + return samples; + } + } + public int StencilSize + { + get + { + GlInterface.GetIntegerv(GlConsts.GL_STENCIL_BITS, out var stencil); + return stencil; + } + } } } diff --git a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs index 5e5e1da949..0e8945d921 100644 --- a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs +++ b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs @@ -4,7 +4,6 @@ using System.Threading; using Avalonia.OpenGL; using Avalonia.OpenGL.Surfaces; using CoreAnimation; -using OpenTK.Graphics.ES30; namespace Avalonia.iOS { @@ -35,7 +34,7 @@ namespace Avalonia.iOS public void Dispose() { - GL.Finish(); + _ctx.GlInterface.Finish(); _fbo.Present(); _restoreContext.Dispose(); } @@ -85,7 +84,7 @@ namespace Avalonia.iOS var ctx = Platform.GlFeature.Context; using (ctx.MakeCurrent()) { - var fbo = new SizeSynchronizedLayerFbo(ctx.Context, _layer); + var fbo = new SizeSynchronizedLayerFbo(ctx.Context, ctx.GlInterface, _layer); if (!fbo.Sync()) throw new InvalidOperationException("Unable to create render target"); return new RenderTarget(ctx, fbo); diff --git a/src/iOS/Avalonia.iOS/Extensions.cs b/src/iOS/Avalonia.iOS/Extensions.cs index bf6262e5c5..80f6b419c9 100644 --- a/src/iOS/Avalonia.iOS/Extensions.cs +++ b/src/iOS/Avalonia.iOS/Extensions.cs @@ -12,7 +12,7 @@ namespace Avalonia.iOS public static Point ToAvalonia(this CGPoint point) => new Point(point.X, point.Y); - static nfloat ColorComponent(byte c) => ((float) c) / 255; + static float ColorComponent(byte c) => (float) c / 255; public static UIColor ToUiColor(this Color color) => new UIColor( ColorComponent(color.R), @@ -20,4 +20,4 @@ namespace Avalonia.iOS ColorComponent(color.B), ColorComponent(color.A)); } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/LayerFbo.cs b/src/iOS/Avalonia.iOS/LayerFbo.cs index 907af58c7e..955aaef59f 100644 --- a/src/iOS/Avalonia.iOS/LayerFbo.cs +++ b/src/iOS/Avalonia.iOS/LayerFbo.cs @@ -1,64 +1,71 @@ using System; +using Avalonia.OpenGL; using CoreAnimation; using OpenGLES; -using OpenTK.Graphics.ES20; namespace Avalonia.iOS { public class LayerFbo { private readonly EAGLContext _context; + private readonly GlInterface _gl; private readonly CAEAGLLayer _layer; - private int _framebuffer; - private int _renderbuffer; - private int _depthBuffer; + private int[] _framebuffer; + private int[] _renderbuffer; + private int[] _depthBuffer; private bool _disposed; - private LayerFbo(EAGLContext context, CAEAGLLayer layer, in int framebuffer, in int renderbuffer, in int depthBuffer) + private LayerFbo(EAGLContext context, GlInterface gl, CAEAGLLayer layer, int[] framebuffer, int[] renderbuffer, int[] depthBuffer) { _context = context; + _gl = gl; _layer = layer; _framebuffer = framebuffer; _renderbuffer = renderbuffer; _depthBuffer = depthBuffer; } - public static LayerFbo TryCreate(EAGLContext context, CAEAGLLayer layer) + public static LayerFbo TryCreate(EAGLContext context, GlInterface gl, CAEAGLLayer layer) { if (context != EAGLContext.CurrentContext) return null; - GL.GenFramebuffers(1, out int fb); - GL.GenRenderbuffers(1, out int rb); - GL.BindFramebuffer(FramebufferTarget.Framebuffer, fb); - GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, rb); - context.RenderBufferStorage((uint) All.Renderbuffer, layer); + + var fb = new int[2]; + var rb = new int[2]; + var db = new int[2]; - GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferSlot.ColorAttachment0, RenderbufferTarget.Renderbuffer, rb); + gl.GenRenderbuffers(1, rb); + gl.BindRenderbuffer(GlConsts.GL_RENDERBUFFER, rb[0]); + context.RenderBufferStorage(GlConsts.GL_RENDERBUFFER, layer); + + gl.GenFramebuffers(1, fb); + gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, fb[0]); + gl.FramebufferRenderbuffer(GlConsts.GL_FRAMEBUFFER, GlConsts.GL_COLOR_ATTACHMENT0, GlConsts.GL_RENDERBUFFER, rb[0]); - int w; - int h; - GL.GetRenderbufferParameter(RenderbufferTarget.Renderbuffer, RenderbufferParameterName.RenderbufferWidth, out w); - GL.GetRenderbufferParameter(RenderbufferTarget.Renderbuffer, RenderbufferParameterName.RenderbufferHeight, out h); + int[] w = new int[1]; + int[] h = new int[1]; + gl.GetRenderbufferParameteriv(GlConsts.GL_RENDERBUFFER, GlConsts.GL_RENDERBUFFER_WIDTH, w); + gl.GetRenderbufferParameteriv(GlConsts.GL_RENDERBUFFER, GlConsts.GL_RENDERBUFFER_HEIGHT, h); - GL.GenRenderbuffers(1, out int depthBuffer); + gl.GenRenderbuffers(1, db); //GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, depthBuffer); //GL.RenderbufferStorage(RenderbufferTarget.Renderbuffer, RenderbufferInternalFormat.DepthComponent16, w, h); - GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferSlot.DepthAttachment, RenderbufferTarget.Renderbuffer, depthBuffer); + gl.FramebufferRenderbuffer(GlConsts.GL_FRAMEBUFFER, GlConsts.GL_DEPTH_ATTACHMENT, GlConsts.GL_RENDERBUFFER, db[0]); - var frameBufferError = GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer); - if(frameBufferError != FramebufferErrorCode.FramebufferComplete) + var frameBufferError = gl.CheckFramebufferStatus(GlConsts.GL_FRAMEBUFFER); + if(frameBufferError != GlConsts.GL_FRAMEBUFFER_COMPLETE) { - GL.DeleteFramebuffers(1, ref fb); - GL.DeleteRenderbuffers(1, ref depthBuffer); - GL.DeleteRenderbuffers(1, ref rb); + gl.DeleteFramebuffers(1, fb); + gl.DeleteRenderbuffers(1, db); + gl.DeleteRenderbuffers(1, rb); return null; } - return new LayerFbo(context, layer, fb, rb, depthBuffer) + return new LayerFbo(context, gl, layer, fb, rb, db) { - Width = w, - Height = h + Width = w[0], + Height = h[0] }; } @@ -67,13 +74,13 @@ namespace Avalonia.iOS public void Bind() { - GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer); + _gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, _framebuffer[0]); } public void Present() { Bind(); - var success = _context.PresentRenderBuffer((uint) All.Renderbuffer); + var success = _context.PresentRenderBuffer(GlConsts.GL_RENDERBUFFER); } public void Dispose() @@ -81,9 +88,9 @@ namespace Avalonia.iOS if(_disposed) return; _disposed = true; - GL.DeleteFramebuffers(1, ref _framebuffer); - GL.DeleteRenderbuffers(1, ref _depthBuffer); - GL.DeleteRenderbuffers(1, ref _renderbuffer); + _gl.DeleteFramebuffers(1, _framebuffer); + _gl.DeleteRenderbuffers(1, _depthBuffer); + _gl.DeleteRenderbuffers(1, _renderbuffer); if (_context != EAGLContext.CurrentContext) throw new InvalidOperationException("Associated EAGLContext is not current"); } @@ -92,15 +99,16 @@ namespace Avalonia.iOS class SizeSynchronizedLayerFbo : IDisposable { private readonly EAGLContext _context; + private readonly GlInterface _gl; private readonly CAEAGLLayer _layer; private LayerFbo _fbo; - private nfloat _oldLayerWidth, _oldLayerHeight, _oldLayerScale; + private double _oldLayerWidth, _oldLayerHeight, _oldLayerScale; - public SizeSynchronizedLayerFbo(EAGLContext context, CAEAGLLayer layer) + public SizeSynchronizedLayerFbo(EAGLContext context, GlInterface gl, CAEAGLLayer layer) { _context = context; + _gl = gl; _layer = layer; - } public bool Sync() @@ -112,7 +120,7 @@ namespace Avalonia.iOS return true; _fbo?.Dispose(); _fbo = null; - _fbo = LayerFbo.TryCreate(_context, _layer); + _fbo = LayerFbo.TryCreate(_context, _gl, _layer); _oldLayerWidth = _layer.Bounds.Width; _oldLayerHeight = _layer.Bounds.Height; _oldLayerScale = _layer.ContentsScale; @@ -140,4 +148,4 @@ namespace Avalonia.iOS public int Height => _fbo?.Height ?? 0; public double Scaling => _oldLayerScale; } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 88f60ace1f..2738e502de 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -1,10 +1,25 @@ using System; + +using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +namespace Avalonia +{ + public static class IOSApplicationExtensions + { + public static T UseiOS(this T builder) where T : AppBuilderBase, new() + { + return builder + .UseWindowingSubsystem(iOS.Platform.Register, "iOS") + .UseSkia(); + } + } +} + namespace Avalonia.iOS { static class Platform @@ -29,7 +44,7 @@ namespace Avalonia.iOS GlFeature ??= new EaglFeature(); Timer ??= new DisplayLinkTimer(); var keyboard = new KeyboardDevice(); - var softKeyboard = new SoftKeyboardHelper(); + AvaloniaLocator.CurrentMutable .Bind().ToConstant(GlFeature) .Bind().ToConstant(new CursorFactoryStub()) @@ -42,11 +57,6 @@ namespace Avalonia.iOS .Bind().ToConstant(Timer) .Bind().ToConstant(new PlatformThreadingInterface()) .Bind().ToConstant(keyboard); - keyboard.PropertyChanged += (_, changed) => - { - if (changed.PropertyName == nameof(KeyboardDevice.FocusedElement)) - softKeyboard.UpdateKeyboard(keyboard.FocusedElement); - }; } diff --git a/src/iOS/Avalonia.iOS/SingleViewLifetime.cs b/src/iOS/Avalonia.iOS/SingleViewLifetime.cs deleted file mode 100644 index 914f0ba548..0000000000 --- a/src/iOS/Avalonia.iOS/SingleViewLifetime.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Avalonia.iOS -{ - public class SingleViewLifetime - { - - } -} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/SoftKeyboardHelper.cs b/src/iOS/Avalonia.iOS/SoftKeyboardHelper.cs deleted file mode 100644 index b05ab280d2..0000000000 --- a/src/iOS/Avalonia.iOS/SoftKeyboardHelper.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Input; - -namespace Avalonia.iOS -{ - public class SoftKeyboardHelper - { - private AvaloniaView _oldView; - - public void UpdateKeyboard(IInputElement focusedElement) - { - if (_oldView?.IsFirstResponder == true) - _oldView?.ResignFirstResponder(); - _oldView = null; - - //TODO: Raise a routed event to determine if any control wants to become the text input handler - if (focusedElement is TextBox) - { - var view = ((focusedElement.VisualRoot as TopLevel)?.PlatformImpl as AvaloniaView.TopLevelImpl)?.View; - view?.BecomeFirstResponder(); - } - } - } -} \ No newline at end of file diff --git a/src/iOS/Avalonia.iOS/TouchHandler.cs b/src/iOS/Avalonia.iOS/TouchHandler.cs index 43b19c85af..959a660d8a 100644 --- a/src/iOS/Avalonia.iOS/TouchHandler.cs +++ b/src/iOS/Avalonia.iOS/TouchHandler.cs @@ -41,7 +41,7 @@ namespace Avalonia.iOS _ => RawPointerEventType.TouchUpdate }, pt, RawInputModifiers.None, id); - _device.ProcessRawEvent(ev); + _tl.Input?.Invoke(ev); if (t.Phase == UITouchPhase.Cancelled || t.Phase == UITouchPhase.Ended) _knownTouches.Remove(t); @@ -49,4 +49,4 @@ namespace Avalonia.iOS } } -} \ No newline at end of file +} diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index a2d27fd579..1cf68c1605 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -7,15 +7,9 @@ - - - - - - diff --git a/src/tools/DevAnalyzers/DevAnalyzers.csproj b/src/tools/DevAnalyzers/DevAnalyzers.csproj new file mode 100644 index 0000000000..53e3e74e76 --- /dev/null +++ b/src/tools/DevAnalyzers/DevAnalyzers.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + 10 + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs b/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs new file mode 100644 index 0000000000..8ecd9119f6 --- /dev/null +++ b/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs @@ -0,0 +1,40 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace DevAnalyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class GenericVirtualAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "AVADEV1001"; + + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId, + "Do not use generic virtual methods", + "Method '{0}' is a generic virtual method", + "Performance", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Generic virtual methods affect JIT startup time adversly and should be avoided."); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + } + + private static void AnalyzeMethod(SymbolAnalysisContext context) + { + var symbol = (IMethodSymbol)context.Symbol; + + if (symbol.IsGenericMethod && + (symbol.IsVirtual || symbol.ContainingType.TypeKind == TypeKind.Interface)) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name)); + } + } +} diff --git a/src/tools/DevAnalyzers/GlobalSuppressions.cs b/src/tools/DevAnalyzers/GlobalSuppressions.cs new file mode 100644 index 0000000000..9428b904b8 --- /dev/null +++ b/src/tools/DevAnalyzers/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")] diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs deleted file mode 100644 index 1d5296bebd..0000000000 --- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs +++ /dev/null @@ -1,515 +0,0 @@ -using System; -using Avalonia.Animation.Animators; -using Avalonia.Controls; -using Avalonia.Controls.Shapes; -using Avalonia.Data; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Styling; -using Avalonia.UnitTests; -using Moq; -using Xunit; - -namespace Avalonia.Animation.UnitTests -{ - public class AnimatableTests - { - [Fact] - public void Transition_Is_Not_Applied_When_Not_Attached_To_Visual_Tree() - { - var target = CreateTarget(); - var control = new Control - { - Transitions = new Transitions { target.Object }, - }; - - control.Opacity = 0.5; - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5), - Times.Never); - } - - [Fact] - public void Transition_Is_Not_Applied_To_Initial_Style() - { - using (Start()) - { - var target = CreateTarget(); - var control = new Control - { - Transitions = new Transitions { target.Object }, - }; - - var root = new TestRoot - { - Styles = - { - new Style(x => x.OfType()) - { - Setters = - { - new Setter(Visual.OpacityProperty, 0.8), - } - } - } - }; - - root.Child = control; - - Assert.Equal(0.8, control.Opacity); - - target.Verify(x => x.Apply( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Never); - } - } - - [Fact] - public void Transition_Is_Applied_When_Local_Value_Changes() - { - using var app = Start(); - var target = CreateTarget(); - var control = CreateControl(target.Object); - - control.Opacity = 0.5; - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5)); - } - - [Fact] - public void Transition_Is_Not_Applied_When_Animated_Value_Changes() - { - var target = CreateTarget(); - var control = CreateControl(target.Object); - - control.SetValue(Visual.OpacityProperty, 0.5, BindingPriority.Animation); - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5), - Times.Never); - } - - - [Theory] - [InlineData(null)] //null value - [InlineData("stringValue")] //string value - public void Invalid_Values_In_Animation_Should_Not_Crash_Animations(object invalidValue) - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Layoutable.WidthProperty, 1d), - }, - KeyTime = TimeSpan.FromSeconds(0) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Layoutable.WidthProperty, 2d), - }, - KeyTime = TimeSpan.FromSeconds(2), - }; - - var keyframe3 = new KeyFrame() - { - Setters = - { - new Setter(Layoutable.WidthProperty, invalidValue), - }, - KeyTime = TimeSpan.FromSeconds(3), - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(3), - Children = - { - keyframe1, - keyframe2, - keyframe3 - }, - IterationCount = new IterationCount(5), - PlaybackDirection = PlaybackDirection.Alternate, - }; - - var rect = new Rectangle() - { - Width = 11, - }; - - var originalValue = rect.Width; - - var clock = new TestClock(); - var animationRun = animation.RunAsync(rect, clock); - - clock.Step(TimeSpan.Zero); - Assert.Equal(rect.Width, 1); - clock.Step(TimeSpan.FromSeconds(2)); - Assert.Equal(rect.Width, 2); - clock.Step(TimeSpan.FromSeconds(3)); - //here we have invalid value so value should be expected and set to initial original value - Assert.Equal(rect.Width, originalValue); - } - - [Fact] - public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present() - { - using var app = Start(); - var target = CreateTarget(); - var control = CreateControl(target.Object); - - control.SetValue(Visual.OpacityProperty, 0.5); - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5)); - target.Invocations.Clear(); - - control.SetValue(Visual.OpacityProperty, 0.8, BindingPriority.StyleTrigger); - - target.Verify(x => x.Apply( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Never); - } - - [Fact] - public void Transition_Is_Disposed_When_Local_Value_Changes() - { - using var app = Start(); - var target = CreateTarget(); - var control = CreateControl(target.Object); - var sub = new Mock(); - - target.Setup(x => x.Apply(control, It.IsAny(), 1.0, 0.5)).Returns(sub.Object); - - control.Opacity = 0.5; - sub.Invocations.Clear(); - control.Opacity = 0.4; - - sub.Verify(x => x.Dispose()); - } - - [Fact] - public void New_Transition_Is_Applied_When_Local_Value_Changes() - { - using var app = Start(); - var target = CreateTarget(); - var control = CreateControl(target.Object); - - target.Setup(x => x.Property).Returns(Visual.OpacityProperty); - target.Setup(x => x.Apply(control, It.IsAny(), 1.0, 0.5)) - .Callback(() => - { - control.SetValue(Visual.OpacityProperty, 0.9, BindingPriority.Animation); - }) - .Returns(Mock.Of()); - - control.Opacity = 0.5; - - Assert.Equal(0.9, control.Opacity); - target.Invocations.Clear(); - - control.Opacity = 0.4; - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 0.9, - 0.4)); - } - - [Fact] - public void Transition_Is_Not_Applied_When_Removed_From_Visual_Tree() - { - using var app = Start(); - var target = CreateTarget(); - var control = CreateControl(target.Object); - - control.Opacity = 0.5; - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5)); - target.Invocations.Clear(); - - var root = (TestRoot)control.Parent; - root.Child = null; - control.Opacity = 0.8; - - target.Verify(x => x.Apply( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Never); - } - - [Fact] - public void Animation_Is_Cancelled_When_Transition_Removed() - { - using var app = Start(); - var target = CreateTarget(); - var control = CreateControl(target.Object); - var sub = new Mock(); - - target.Setup(x => x.Apply( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())).Returns(sub.Object); - - control.Opacity = 0.5; - control.Transitions.RemoveAt(0); - - sub.Verify(x => x.Dispose()); - } - - [Fact] - public void Animation_Is_Cancelled_When_New_Style_Activates() - { - using (Start()) - { - var target = CreateTarget(); - var control = CreateStyledControl(target.Object); - var sub = new Mock(); - - target.Setup(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5)).Returns(sub.Object); - - control.Opacity = 0.5; - - target.Verify(x => x.Apply( - control, - It.IsAny(), - 1.0, - 0.5), - Times.Once); - - control.Classes.Add("foo"); - - sub.Verify(x => x.Dispose()); - } - } - - [Fact] - public void Transition_From_Style_Trigger_Is_Applied() - { - using (Start()) - { - var target = CreateTransition(Control.WidthProperty); - var control = CreateStyledControl(transition2: target.Object); - var sub = new Mock(); - - control.Classes.Add("foo"); - control.Width = 100; - - target.Verify(x => x.Apply( - control, - It.IsAny(), - double.NaN, - 100.0), - Times.Once); - } - } - - [Fact] - public void Replacing_Transitions_During_Animation_Does_Not_Throw_KeyNotFound() - { - // Issue #4059 - using (Start()) - { - Border target; - var clock = new TestClock(); - var root = new TestRoot - { - Clock = clock, - Styles = - { - new Style(x => x.OfType()) - { - Setters = - { - new Setter(Border.TransitionsProperty, - new Transitions - { - new DoubleTransition - { - Property = Border.OpacityProperty, - Duration = TimeSpan.FromSeconds(1), - }, - }), - }, - }, - new Style(x => x.OfType().Class("foo")) - { - Setters = - { - new Setter(Border.TransitionsProperty, - new Transitions - { - new DoubleTransition - { - Property = Border.OpacityProperty, - Duration = TimeSpan.FromSeconds(1), - }, - }), - new Setter(Border.OpacityProperty, 0.0), - }, - }, - }, - Child = target = new Border - { - Background = Brushes.Red, - } - }; - - root.Measure(Size.Infinity); - root.Arrange(new Rect(root.DesiredSize)); - - target.Classes.Add("foo"); - clock.Step(TimeSpan.FromSeconds(0)); - clock.Step(TimeSpan.FromSeconds(0.5)); - - Assert.Equal(0.5, target.Opacity); - - target.Classes.Remove("foo"); - } - } - - [Fact] - public void Transitions_Can_Be_Changed_To_Collection_That_Contains_The_Same_Transitions() - { - var target = CreateTarget(); - var control = CreateControl(target.Object); - - control.Transitions = new Transitions { target.Object }; - } - - [Fact] - public void Transitions_Can_Re_Set_During_Batch_Update() - { - var target = CreateTarget(); - var control = CreateControl(target.Object); - - // Assigning and then clearing Transitions ensures we have a transition state - // collection created. - control.Transitions = null; - - control.BeginBatchUpdate(); - - // Setting opacity then Transitions means that we receive the Transitions change - // after the Opacity change when EndBatchUpdate is called. - control.Opacity = 0.5; - control.Transitions = new Transitions { target.Object }; - - // Which means that the transition state hasn't been initialized with the new - // Transitions when the Opacity change notification gets raised here. - control.EndBatchUpdate(); - } - - private static IDisposable Start() - { - var clock = new MockGlobalClock(); - var services = TestServices.RealStyler.With(globalClock: clock); - return UnitTestApplication.Start(services); - } - - private static Mock CreateTarget() - { - return CreateTransition(Visual.OpacityProperty); - } - - private static Control CreateControl(ITransition transition) - { - var control = new Control - { - Transitions = new Transitions { transition }, - }; - - var root = new TestRoot(control); - return control; - } - - private static Control CreateStyledControl( - ITransition transition1 = null, - ITransition transition2 = null) - { - transition1 = transition1 ?? CreateTarget().Object; - transition2 = transition2 ?? CreateTransition(Control.WidthProperty).Object; - - var control = new Control - { - Styles = - { - new Style(x => x.OfType()) - { - Setters = - { - new Setter - { - Property = Control.TransitionsProperty, - Value = new Transitions { transition1 }, - } - } - }, - new Style(x => x.OfType().Class("foo")) - { - Setters = - { - new Setter - { - Property = Control.TransitionsProperty, - Value = new Transitions { transition2 }, - } - } - } - } - }; - - var root = new TestRoot(control); - return control; - } - - private static Mock CreateTransition(AvaloniaProperty property) - { - var target = new Mock(); - var sub = new Mock(); - - target.Setup(x => x.Property).Returns(property); - target.Setup(x => x.Apply( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())).Returns(sub.Object); - - return target; - } - } -} diff --git a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs deleted file mode 100644 index 58bd7a42c3..0000000000 --- a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs +++ /dev/null @@ -1,448 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Animation; -using Avalonia.Controls; -using Avalonia.Styling; -using Avalonia.UnitTests; -using Avalonia.Data; -using Xunit; -using Avalonia.Animation.Easings; -using System.Threading; -using System.Reactive.Linq; - -namespace Avalonia.Animation.UnitTests -{ - public class AnimationIterationTests - { - [Fact] - public void Check_KeyTime_Correctly_Converted_To_Cue() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 100d), - }, - KeyTime = TimeSpan.FromSeconds(0.5) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 0d), - }, - KeyTime = TimeSpan.FromSeconds(0) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(1), - Children = - { - keyframe2, - keyframe1 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 100d - }; - - var clock = new TestClock(); - var animationRun = animation.RunAsync(border, clock); - - clock.Step(TimeSpan.Zero); - Assert.Equal(border.Width, 0d); - - clock.Step(TimeSpan.FromSeconds(1)); - Assert.Equal(border.Width, 100d); - - } - - - [Fact] - public void Check_Initial_Inter_and_Trailing_Delay_Values() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 200d), - }, - Cue = new Cue(1d) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 100d), - }, - Cue = new Cue(0d) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(3), - Delay = TimeSpan.FromSeconds(3), - DelayBetweenIterations = TimeSpan.FromSeconds(3), - IterationCount = new IterationCount(2), - Children = - { - keyframe2, - keyframe1 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 100d - }; - - var clock = new TestClock(); - var animationRun = animation.RunAsync(border, clock); - - clock.Step(TimeSpan.Zero); - - // Initial Delay. - clock.Step(TimeSpan.FromSeconds(1)); - Assert.Equal(border.Width, 0d); - - clock.Step(TimeSpan.FromSeconds(6)); - - // First Inter-Iteration delay. - clock.Step(TimeSpan.FromSeconds(8)); - Assert.Equal(border.Width, 200d); - - // Trailing Delay should be non-existent. - clock.Step(TimeSpan.FromSeconds(14)); - Assert.True(animationRun.Status == TaskStatus.RanToCompletion); - Assert.Equal(border.Width, 100d); - } - - [Fact] - public void Check_FillModes_Start_and_End_Values_if_Retained() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 0d), - }, - Cue = new Cue(0.0d) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 300d), - }, - Cue = new Cue(1.0d) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(0.05d), - Delay = TimeSpan.FromSeconds(0.05d), - Easing = new SineEaseInOut(), - FillMode = FillMode.Both, - Children = - { - keyframe1, - keyframe2 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 100d, - }; - - var clock = new TestClock(); - var animationRun = animation.RunAsync(border, clock); - - clock.Step(TimeSpan.FromSeconds(0d)); - Assert.Equal(border.Width, 0d); - - clock.Step(TimeSpan.FromSeconds(0.050d)); - Assert.Equal(border.Width, 0d); - - clock.Step(TimeSpan.FromSeconds(0.100d)); - Assert.Equal(border.Width, 300d); - } - - [Fact(Skip = "See #6111")] - public void Dispose_Subscription_Should_Stop_Animation() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 200d), - }, - Cue = new Cue(1d) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 100d), - }, - Cue = new Cue(0d) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(10), - Delay = TimeSpan.FromSeconds(0), - DelayBetweenIterations = TimeSpan.FromSeconds(0), - IterationCount = new IterationCount(1), - Children = - { - keyframe2, - keyframe1 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 50d - }; - var propertyChangedCount = 0; - var animationCompletedCount = 0; - border.PropertyChanged += (sender, e) => - { - if (e.Property == Control.WidthProperty) - { - propertyChangedCount++; - } - }; - - var clock = new TestClock(); - var disposable = animation.Apply(border, clock, Observable.Return(true), () => animationCompletedCount++); - - Assert.Equal(0, propertyChangedCount); - - clock.Step(TimeSpan.FromSeconds(0)); - Assert.Equal(0, animationCompletedCount); - Assert.Equal(1, propertyChangedCount); - - disposable.Dispose(); - - // Clock ticks should be ignored after Dispose - clock.Step(TimeSpan.FromSeconds(5)); - clock.Step(TimeSpan.FromSeconds(6)); - clock.Step(TimeSpan.FromSeconds(7)); - - // On animation disposing (cancellation) on completed is not invoked (is it expected?) - Assert.Equal(0, animationCompletedCount); - // Initial property changed before cancellation + animation value removal. - Assert.Equal(2, propertyChangedCount); - } - - [Fact] - public void Do_Not_Run_Cancelled_Animation() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 200d), - }, - Cue = new Cue(1d) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 100d), - }, - Cue = new Cue(0d) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(10), - Delay = TimeSpan.FromSeconds(0), - DelayBetweenIterations = TimeSpan.FromSeconds(0), - IterationCount = new IterationCount(1), - Children = - { - keyframe2, - keyframe1 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 100d - }; - var propertyChangedCount = 0; - border.PropertyChanged += (sender, e) => - { - if (e.Property == Control.WidthProperty) - { - propertyChangedCount++; - } - }; - - var clock = new TestClock(); - var cancellationTokenSource = new CancellationTokenSource(); - cancellationTokenSource.Cancel(); - var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token); - - clock.Step(TimeSpan.FromSeconds(10)); - Assert.Equal(0, propertyChangedCount); - Assert.True(animationRun.IsCompleted); - } - - [Fact(Skip = "See #6111")] - public void Cancellation_Should_Stop_Animation() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 200d), - }, - Cue = new Cue(1d) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 100d), - }, - Cue = new Cue(0d) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(10), - Delay = TimeSpan.FromSeconds(0), - DelayBetweenIterations = TimeSpan.FromSeconds(0), - IterationCount = new IterationCount(1), - Children = - { - keyframe2, - keyframe1 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 50d - }; - var propertyChangedCount = 0; - border.PropertyChanged += (sender, e) => - { - if (e.Property == Control.WidthProperty) - { - propertyChangedCount++; - } - }; - - var clock = new TestClock(); - var cancellationTokenSource = new CancellationTokenSource(); - var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token); - - Assert.Equal(0, propertyChangedCount); - - clock.Step(TimeSpan.FromSeconds(0)); - Assert.False(animationRun.IsCompleted); - Assert.Equal(1, propertyChangedCount); - - cancellationTokenSource.Cancel(); - clock.Step(TimeSpan.FromSeconds(1)); - clock.Step(TimeSpan.FromSeconds(2)); - clock.Step(TimeSpan.FromSeconds(3)); - //Assert.Equal(2, propertyChangedCount); - - animationRun.Wait(); - - clock.Step(TimeSpan.FromSeconds(6)); - Assert.True(animationRun.IsCompleted); - Assert.Equal(2, propertyChangedCount); - } - - [Fact] - public void Cancellation_Of_Completed_Animation_Does_Not_Fail() - { - var keyframe1 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 200d), - }, - Cue = new Cue(1d) - }; - - var keyframe2 = new KeyFrame() - { - Setters = - { - new Setter(Border.WidthProperty, 100d), - }, - Cue = new Cue(0d) - }; - - var animation = new Animation() - { - Duration = TimeSpan.FromSeconds(10), - Delay = TimeSpan.FromSeconds(0), - DelayBetweenIterations = TimeSpan.FromSeconds(0), - IterationCount = new IterationCount(1), - Children = - { - keyframe2, - keyframe1 - } - }; - - var border = new Border() - { - Height = 100d, - Width = 50d - }; - var propertyChangedCount = 0; - border.PropertyChanged += (sender, e) => - { - if (e.Property == Control.WidthProperty) - { - propertyChangedCount++; - } - }; - - var clock = new TestClock(); - var cancellationTokenSource = new CancellationTokenSource(); - var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token); - - Assert.Equal(0, propertyChangedCount); - - clock.Step(TimeSpan.FromSeconds(0)); - Assert.False(animationRun.IsCompleted); - Assert.Equal(1, propertyChangedCount); - - clock.Step(TimeSpan.FromSeconds(10)); - Assert.True(animationRun.IsCompleted); - Assert.Equal(2, propertyChangedCount); - - cancellationTokenSource.Cancel(); - animationRun.Wait(); - } - } -} diff --git a/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj b/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj deleted file mode 100644 index e07ece5460..0000000000 --- a/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - net6.0 - Library - true - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs deleted file mode 100644 index fa2ed61e65..0000000000 --- a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System; -using Avalonia.Animation.Easings; -using Avalonia.Controls.Shapes; -using Avalonia.Media; -using Avalonia.Styling; -using Xunit; - -namespace Avalonia.Animation.UnitTests -{ - public class KeySplineTests - { - [Theory] - [InlineData("1,2 3,4")] - [InlineData("1 2 3 4")] - [InlineData("1 2,3 4")] - [InlineData("1,2,3,4")] - public void Can_Parse_KeySpline_Via_TypeConverter(string input) - { - var conv = new KeySplineTypeConverter(); - - var keySpline = (KeySpline)conv.ConvertFrom(input); - - Assert.Equal(1, keySpline.ControlPointX1); - Assert.Equal(2, keySpline.ControlPointY1); - Assert.Equal(3, keySpline.ControlPointX2); - Assert.Equal(4, keySpline.ControlPointY2); - } - - [Theory] - [InlineData("1,2F,3,4")] - [InlineData("Foo,Bar,Fee,Buzz")] - public void Can_Handle_Invalid_String_KeySpline_Via_TypeConverter(string input) - { - var conv = new KeySplineTypeConverter(); - - Assert.ThrowsAny(() => (KeySpline)conv.ConvertFrom(input)); - } - - [Theory] - [InlineData(0.00)] - [InlineData(0.50)] - [InlineData(1.00)] - public void KeySpline_X_Values_In_Range_Do_Not_Throw(double input) - { - var keySpline = new KeySpline(); - keySpline.ControlPointX1 = input; // no exception will be thrown -- test will fail if exception thrown - keySpline.ControlPointX2 = input; // no exception will be thrown -- test will fail if exception thrown - } - - [Theory] - [InlineData(-0.01)] - [InlineData(1.01)] - public void KeySpline_X_Values_Cannot_Be_Out_Of_Range(double input) - { - var keySpline = new KeySpline(); - Assert.Throws(() => keySpline.ControlPointX1 = input); - Assert.Throws(() => keySpline.ControlPointX2 = input); - } - - [Fact] - public void SplineEasing_Can_Be_Mutated() - { - var easing = new SplineEasing(); - - Assert.Equal(0, easing.Ease(0)); - Assert.Equal(1, easing.Ease(1)); - - easing.X1 = 0.25; - easing.Y1 = 0.5; - easing.X2 = 0.75; - easing.Y2 = 1.0; - - Assert.NotEqual(0.5, easing.Ease(0.5)); - } - - /* - To get the test values for the KeySpline test, you can: - 1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations - 2) Add the following xaml somewhere: -