diff --git a/Avalonia.sln b/Avalonia.sln index 0354e20d4f..f1bdfdbbc3 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -77,7 +77,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Android", "src\Android\Avalonia.Android\Avalonia.Android.csproj", "{7B92AF71-6287-4693-9DCB-BD5B6E927E23}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.AndroidTestApplication", "src\Android\Avalonia.AndroidTestApplication\Avalonia.AndroidTestApplication.csproj", "{FF69B927-C545-49AE-8E16-3D14D621AA12}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.AndroidTestApplication", "src\Android\Avalonia.AndroidTestApplication\Avalonia.AndroidTestApplication.csproj", "{FF69B927-C545-49AE-8E16-3D14D621AA12}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "iOS", "iOS", "{0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}" EndProject @@ -107,7 +107,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interop", "Interop", "{A0CC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderDemo", "samples\RenderDemo\RenderDemo.csproj", "{F1FDC5B0-4654-416F-AE69-E3E9BBD87801}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia", "src\Skia\Avalonia.Skia\Avalonia.Skia.csproj", "{7D2D3083-71DD-4CC9-8907-39A0D86FB322}" EndProject @@ -115,7 +115,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}" ProjectSection(SolutionItems) = preProject - build\AndroidWorkarounds.props = build\AndroidWorkarounds.props build\ApiDiff.props = build\ApiDiff.props build\Base.props = build\Base.props build\Binding.props = build\Binding.props diff --git a/Directory.Build.props b/Directory.Build.props index c6610695c4..835decc672 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,5 +4,6 @@ $(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netcoreapp2.0\Avalonia.Designer.HostApp.dll false + false diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 40669f4f53..ede5aec0e8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,6 +1,3 @@ -variables: - MSBuildEnableWorkloadResolver: 'false' - jobs: - job: GetPRNumber @@ -146,6 +143,12 @@ jobs: inputs: version: 6.0.100 + - task: CmdLine@2 + displayName: 'Install Workloads' + inputs: + script: | + dotnet workload install android + - task: CmdLine@2 displayName: 'Install Nuke' inputs: diff --git a/build/AndroidWorkarounds.props b/build/AndroidWorkarounds.props deleted file mode 100644 index de86acc6de..0000000000 --- a/build/AndroidWorkarounds.props +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - false - - diff --git a/build/System.Drawing.Common.props b/build/System.Drawing.Common.props index 2b3707d38a..108a0f41e0 100644 --- a/build/System.Drawing.Common.props +++ b/build/System.Drawing.Common.props @@ -1,5 +1,6 @@  - + + diff --git a/dirs.proj b/dirs.proj index 594f2c22d3..74d7b482cc 100644 --- a/dirs.proj +++ b/dirs.proj @@ -1,5 +1,7 @@ + + @@ -8,11 +10,8 @@ - - - - + @@ -23,6 +22,10 @@ + + + + diff --git a/global.json b/global.json index b160e4561d..1f93ed27c3 100644 --- a/global.json +++ b/global.json @@ -4,6 +4,7 @@ }, "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/nukebuild/Build.cs b/nukebuild/Build.cs index f0b894b596..72d90abbf3 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -87,7 +87,8 @@ partial class Build : NukeBuild Console.WriteLine(preamble); Process.Start(new ProcessStartInfo(command, args) {UseShellExecute = false}).WaitForExit(); } - ExecWait("dotnet version:", "dotnet", "--version"); + ExecWait("dotnet version:", "dotnet", "--info"); + ExecWait("dotnet workloads:", "dotnet", "workload list"); } IReadOnlyCollection MsBuildCommon( @@ -99,7 +100,7 @@ partial class Build : NukeBuild // This is required for VS2019 image on Azure Pipelines .When(Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure, _ => _ - .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_8_X64"))) + .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_11_X64"))) .AddProperty("PackageVersion", Parameters.Version) .AddProperty("iOSRoslynPathHackRequired", true) .SetProcessToolPath(MsBuildExe.Value) diff --git a/readme.md b/readme.md index f8bbc02d07..96c7937559 100644 --- a/readme.md +++ b/readme.md @@ -5,7 +5,7 @@ ## 📖 About -Avalonia is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, MacOs. Avalonia is mature and production ready. We also have in beta release support for iOS, Andriod and in early stages support for browser via WASM. +Avalonia is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, MacOs. Avalonia is mature and production ready. We also have in beta release support for iOS, Android and in early stages support for browser via WASM. ![image](https://user-images.githubusercontent.com/4672627/152126443-932966cf-57e7-4e77-9be6-62463a66b9f8.png) diff --git a/samples/ControlCatalog.Android/Assets/AboutAssets.txt b/samples/ControlCatalog.Android/Assets/AboutAssets.txt deleted file mode 100644 index a9b0638eb1..0000000000 --- a/samples/ControlCatalog.Android/Assets/AboutAssets.txt +++ /dev/null @@ -1,19 +0,0 @@ -Any raw assets you want to be deployed with your application can be placed in -this directory (and child directories) and given a Build Action of "AndroidAsset". - -These files will be deployed with your package and will be accessible using Android's -AssetManager, like this: - -public class ReadAsset : Activity -{ - protected override void OnCreate (Bundle bundle) - { - base.OnCreate (bundle); - - InputStream input = Assets.Open ("my_asset.txt"); - } -} - -Additionally, some Android functions will automatically load asset files: - -Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 617b6b6ab0..1654d20c80 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -1,165 +1,32 @@ - - + - Debug - AnyCPU - 8.0.30703 - 2.0 - {29132311-1848-4FD6-AE0C-4FF841151BD3} - {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - Properties - ControlCatalog.Android - ControlCatalog.Android - 512 - true - Resources\Resource.Designer.cs - Off - False - v11.0 - Properties\AndroidManifest.xml + net6.0-android + 21 + Exe + enable + com.Avalonia.ControlCatalog + 1 + 1.0 + apk + true - - True - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - True - None - True - False - False - armeabi-v7a;x86;x86_64 - Xamarin - False - False - False - False - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - False - Full - True - False - False - armeabi-v7a,x86;x86_64 - Xamarin - False - False - False - False - False - - - - - - - - - - - - - - - - - - - - - - - - + Resources\drawable\Icon.png + + + True + True + True + + - - - - - {7B92AF71-6287-4693-9DCB-BD5B6E927E23} - Avalonia.Android - - - {d211e587-d8bc-45b9-95a4-f297c8fa5200} - Avalonia.Animation - - - {b09b78d8-9b26-48b0-9149-d64a2f120f3f} - Avalonia.Base - - - {d2221c82-4a25-4583-9b43-d791e3f6820c} - Avalonia.Controls - - - {7062ae20-5dcc-4442-9645-8195bdece63e} - Avalonia.Diagnostics - - - {62024b2d-53eb-4638-b26b-85eeaa54866e} - Avalonia.Input - - - {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} - Avalonia.Interactivity - - - {42472427-4774-4c81-8aff-9f27b8e31721} - Avalonia.Layout - - - {c42d2fc1-a531-4ed4-84b9-89aec7c962fc} - Avalonia.Themes.Fluent - - - {eb582467-6abb-43a1-b052-e981ba910e3a} - Avalonia.Visuals - - - {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} - Avalonia.Styling - - - {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f} - Avalonia.Themes.Default - - - {3e53a01a-b331-47f3-b828-4a5717e77a24} - Avalonia.Markup.Xaml - - - {6417e941-21bc-467b-a771-0de389353ce6} - Avalonia.Markup - - - {7d2d3083-71dd-4cc9-8907-39a0d86fb322} - Avalonia.Skia - - - {d0a739b9-3c68-4ba6-a328-41606954b6bd} - ControlCatalog - + + - - - - diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 2ab03551b6..137f280e43 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -8,7 +8,7 @@ namespace ControlCatalog.Android [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance)] public class MainActivity : AvaloniaActivity { - protected override void OnCreate(Bundle savedInstanceState) + protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml index 9effda7e79..aa570ec504 100644 --- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml +++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml @@ -1,5 +1,4 @@  - - - - \ No newline at end of file + + + diff --git a/samples/ControlCatalog.Android/Properties/AssemblyInfo.cs b/samples/ControlCatalog.Android/Properties/AssemblyInfo.cs deleted file mode 100644 index baeec94648..0000000000 --- a/samples/ControlCatalog.Android/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Android.App; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("ControlCatalog.Android")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ControlCatalog.Android")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: ComVisible(false)] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs deleted file mode 100644 index dccc3f7159..0000000000 --- a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs +++ /dev/null @@ -1,101 +0,0 @@ -#pragma warning disable 1591 -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -[assembly: global::Android.Runtime.ResourceDesignerAttribute("ControlCatalog.Android.Resource", IsApplication=true)] - -namespace ControlCatalog.Android -{ - - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "12.1.99.62")] - public partial class Resource - { - - static Resource() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - public static void UpdateIdValues() - { - } - - public partial class Attribute - { - - static Attribute() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Attribute() - { - } - } - - public partial class Color - { - - // aapt resource value: 0x7F010000 - public const int splash_background = 2130771968; - - static Color() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Color() - { - } - } - - public partial class Drawable - { - - // aapt resource value: 0x7F020000 - public const int Icon = 2130837504; - - // aapt resource value: 0x7F020001 - public const int splash_screen = 2130837505; - - static Drawable() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Drawable() - { - } - } - - public partial class Style - { - - // aapt resource value: 0x7F030000 - public const int MyTheme = 2130903040; - - // aapt resource value: 0x7F030001 - public const int MyTheme_NoActionBar = 2130903041; - - // aapt resource value: 0x7F030002 - public const int MyTheme_Splash = 2130903042; - - static Style() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Style() - { - } - } - } -} -#pragma warning restore 1591 diff --git a/samples/ControlCatalog.Android/SplashActivity.cs b/samples/ControlCatalog.Android/SplashActivity.cs index 6d7c6bc116..9729713833 100644 --- a/samples/ControlCatalog.Android/SplashActivity.cs +++ b/samples/ControlCatalog.Android/SplashActivity.cs @@ -10,7 +10,7 @@ namespace ControlCatalog.Android [Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)] public class SplashActivity : Activity { - protected override void OnCreate(Bundle savedInstanceState) + protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); } diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj index 199fa85ad2..520bbdf32b 100644 --- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj +++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj @@ -1,6 +1,7 @@  net6.0 + false enable True diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 3d869bb723..7beb496ab5 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -22,8 +22,8 @@ - - + + diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml deleted file mode 100644 index 2ce1469118..0000000000 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ /dev/null @@ -1,45 +0,0 @@ - - - A button control - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml b/samples/ControlCatalog/Pages/ButtonsPage.xaml new file mode 100644 index 0000000000..7de0872eae --- /dev/null +++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A standard button control + + + + + + + + + + + + + + + + + + + + + + + + + + + + A button control with multiple states: checked, unchecked or indeterminate. + + + + + + + + + + + + + + A button control that raises its Click event repeatedly when it is pressed and held. + + + + + + + + + + + + + + + + A button with primary and secondary parts that can each be pressed separately. The primary part behaves like a Button and the secondary part opens a flyout. + + + + + + + + + + + + + + + + + + + + + + + + A button with primary and secondary parts that can each be pressed separately. The primary part behaves like a ToggleButton with two states and the secondary part opens a flyout. + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs similarity index 90% rename from samples/ControlCatalog/Pages/ButtonPage.xaml.cs rename to samples/ControlCatalog/Pages/ButtonsPage.xaml.cs index 5e555c8c91..5594bab176 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs @@ -3,11 +3,11 @@ using Avalonia.Markup.Xaml; namespace ControlCatalog.Pages { - public class ButtonPage : UserControl + public class ButtonsPage : UserControl { private int repeatButtonClickCount = 0; - public ButtonPage() + public ButtonsPage() { InitializeComponent(); diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index decbd763a1..64e80a8e11 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -1,77 +1,95 @@ - - - A drop-down list. + + + A drop-down list. - - - - - - Inline Items - Inline Item 2 - Inline Item 3 - Inline Item 4 - + + + + + - - - - - Hello - World - - - - - - - - - - - + + Inline Items + Inline Item 2 + Inline Item 3 + Inline Item 4 + - - - - - Control Items - - - - - - - - - + + + + + Hello + World + + + + + + + + + + + - - - - - - - - - - Inline Items - Inline Item 2 - Inline Item 3 - Inline Item 4 - - - - - + + + + + Control Items + + + + + + + + + - + + + + + + + + + + Inline Items + Inline Item 2 + Inline Item 3 + Inline Item 4 + + + + + + + WrapSelection + + + diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index d50b051d9f..d304bf227d 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -2,6 +2,7 @@ using System.Linq; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Media; +using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { @@ -10,6 +11,7 @@ namespace ControlCatalog.Pages public ComboBoxPage() { this.InitializeComponent(); + DataContext = new ComboBoxPageViewModel(); } private void InitializeComponent() diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 41658329df..433592345a 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -21,6 +21,7 @@ Toggle AlwaysSelected AutoScrollToSelectedItem + WrapSelection @@ -30,6 +31,7 @@ + SelectionMode="{Binding SelectionMode^}" + WrapSelection="{Binding WrapSelection}"/> diff --git a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml index 4db755b6fc..6afe6dd135 100644 --- a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml +++ b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml @@ -11,7 +11,7 @@ - + diff --git a/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs new file mode 100644 index 0000000000..bbe970afd6 --- /dev/null +++ b/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using Avalonia.Controls; +using Avalonia.Controls.Selection; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ComboBoxPageViewModel : ViewModelBase + { + private bool _wrapSelection; + + public bool WrapSelection + { + get => _wrapSelection; + set => this.RaiseAndSetIfChanged(ref _wrapSelection, value); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs index 7f2d6e9572..59489ebcc0 100644 --- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -14,6 +14,7 @@ namespace ControlCatalog.ViewModels private bool _toggle; private bool _alwaysSelected; private bool _autoScrollToSelectedItem = true; + private bool _wrapSelection; private int _counter; private IObservable _selectionMode; @@ -85,6 +86,12 @@ namespace ControlCatalog.ViewModels set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value); } + public bool WrapSelection + { + get => _wrapSelection; + set => this.RaiseAndSetIfChanged(ref _wrapSelection, value); + } + public MiniCommand AddItemCommand { get; } public MiniCommand RemoveItemCommand { get; } public MiniCommand SelectRandomItemCommand { get; } diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 2d4f6a305f..308be1325c 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -10,7 +10,6 @@ using Avalonia.Input.Platform; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; -using Avalonia.PlatformSupport; using Avalonia.Skia; namespace Avalonia @@ -20,9 +19,9 @@ namespace Avalonia public static T UseAndroid(this T builder) where T : AppBuilderBase, new() { var options = AvaloniaLocator.Current.GetService() ?? new AndroidPlatformOptions(); - builder.UseWindowingSubsystem(() => AndroidPlatform.Initialize(builder.ApplicationType, options), "Android"); - builder.UseSkia(); - return builder; + return builder + .UseWindowingSubsystem(() => AndroidPlatform.Initialize(options), "Android") + .UseSkia(); } } } @@ -44,7 +43,7 @@ namespace Avalonia.Android public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); - public static void Initialize(Type appType, AndroidPlatformOptions options) + public static void Initialize(AndroidPlatformOptions options) { Options = options; diff --git a/src/Android/Avalonia.Android/AppBuilder.cs b/src/Android/Avalonia.Android/AppBuilder.cs deleted file mode 100644 index 04f1ff00d0..0000000000 --- a/src/Android/Avalonia.Android/AppBuilder.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Avalonia.Controls; -using Avalonia.PlatformSupport; - -namespace Avalonia -{ - public sealed class AppBuilder : AppBuilderBase - { - public AppBuilder() : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType()?.Assembly)) - { - - } - } -} diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 5c33dbcea6..b774938a1d 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,15 +1,12 @@ - + - monoandroid11.0 + net6.0-android;monoandroid11.0 + 21 true + true - - TargetFramework=netstandard2.0 - - - diff --git a/src/Android/Avalonia.AndroidTestApplication/Assets/AboutAssets.txt b/src/Android/Avalonia.AndroidTestApplication/Assets/AboutAssets.txt deleted file mode 100644 index ee39886295..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Assets/AboutAssets.txt +++ /dev/null @@ -1,19 +0,0 @@ -Any raw assets you want to be deployed with your application can be placed in -this directory (and child directories) and given a Build Action of "AndroidAsset". - -These files will be deployed with you package and will be accessible using Android's -AssetManager, like this: - -public class ReadAsset : Activity -{ - protected override void OnCreate (Bundle bundle) - { - base.OnCreate (bundle); - - InputStream input = Assets.Open ("my_asset.txt"); - } -} - -Additionally, some Android functions will automatically load asset files: - -Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); \ No newline at end of file diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index 9104f1618c..f1e2379853 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -1,153 +1,29 @@ - - + - Debug - AnyCPU - 8.0.30703 - 2.0 - {FF69B927-C545-49AE-8E16-3D14D621AA12} - {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - Properties - Avalonia.AndroidTestApplication - Avalonia.AndroidTestApplication - 512 - true - Resources\Resource.Designer.cs - Off - False - v11.0 - Properties\AndroidManifest.xml + net6.0-android + 21 + Exe + enable + com.Avalonia.AndroidTestApplication + 1 + 1.0 + apk + true - + + True - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - True - None - True - False - False - armeabi-v7a;x86 - Xamarin - False - True - False - False - False + True + True + - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - False - Full - True - False - False - armeabi-v7a,x86 - Xamarin - False - False - False - False - False - False - - - - - - - - - - - - - - - - - - - - - - Designer - - - - - + - + + - - {7b92af71-6287-4693-9dcb-bd5b6e927e23} - Avalonia.Android - - - {3e53a01a-b331-47f3-b828-4a5717e77a24} - Avalonia.Markup.Xaml - - - {d211e587-d8bc-45b9-95a4-f297c8fa5200} - Avalonia.Animation - - - {b09b78d8-9b26-48b0-9149-d64a2f120f3f} - Avalonia.Base - - - {d2221c82-4a25-4583-9b43-d791e3f6820c} - Avalonia.Controls - - - {7062ae20-5dcc-4442-9645-8195bdece63e} - Avalonia.Diagnostics - - - {62024b2d-53eb-4638-b26b-85eeaa54866e} - Avalonia.Input - - - {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} - Avalonia.Interactivity - - - {42472427-4774-4c81-8aff-9f27b8e31721} - Avalonia.Layout - - - {eb582467-6abb-43a1-b052-e981ba910e3a} - Avalonia.Visuals - - - {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} - Avalonia.Styling - - - {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f} - Avalonia.Themes.Default - - - {7d2d3083-71dd-4cc9-8907-39a0d86fb322} - Avalonia.Skia - + + - - - - - - diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml index 57ee503005..ad8134f628 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml +++ b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml @@ -1,6 +1,4 @@  - - + - - \ No newline at end of file + diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AssemblyInfo.cs b/src/Android/Avalonia.AndroidTestApplication/Properties/AssemblyInfo.cs deleted file mode 100644 index 2528202974..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("Avalonia.AndroidTestApplication")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Avalonia.AndroidTestApplication")] -[assembly: AssemblyCopyright("Copyright © 2015")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: ComVisible(false)] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs deleted file mode 100644 index 87fd47df25..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs +++ /dev/null @@ -1,79 +0,0 @@ -#pragma warning disable 1591 -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -[assembly: global::Android.Runtime.ResourceDesignerAttribute("Avalonia.AndroidTestApplication.Resource", IsApplication=true)] - -namespace Avalonia.AndroidTestApplication -{ - - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "12.1.99.62")] - public partial class Resource - { - - static Resource() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - public static void UpdateIdValues() - { - } - - public partial class Attribute - { - - static Attribute() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Attribute() - { - } - } - - public partial class Drawable - { - - // aapt resource value: 0x7F010000 - public const int Icon = 2130771968; - - static Drawable() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Drawable() - { - } - } - - public partial class String - { - - // aapt resource value: 0x7F020000 - public const int ApplicationName = 2130837504; - - // aapt resource value: 0x7F020001 - public const int Hello = 2130837505; - - static String() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private String() - { - } - } - } -} -#pragma warning restore 1591 diff --git a/src/Android/Avalonia.AndroidTestApplication/app.config b/src/Android/Avalonia.AndroidTestApplication/app.config deleted file mode 100644 index fc064cedfb..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index a2efc7fba0..a7a4759182 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -85,8 +85,8 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly StyledProperty FlyoutProperty = - AvaloniaProperty.Register(nameof(Flyout)); + public static readonly StyledProperty FlyoutProperty = + AvaloniaProperty.Register(nameof(Flyout)); private ICommand? _command; private bool _commandCanExecute = true; @@ -186,7 +186,7 @@ namespace Avalonia.Controls /// /// Gets or sets the Flyout that should be shown with this button. /// - public FlyoutBase Flyout + public FlyoutBase? Flyout { get => GetValue(FlyoutProperty); set => SetValue(FlyoutProperty, value); diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index e9eca97e13..72b09b7a3c 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -10,9 +10,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; -using Avalonia.LogicalTree; using Avalonia.Media; -using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -91,7 +89,7 @@ namespace Avalonia.Controls { ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); FocusableProperty.OverrideDefaultValue(true); - SelectedItemProperty.Changed.AddClassHandler((x,e) => x.SelectedItemChanged(e)); + SelectedItemProperty.Changed.AddClassHandler((x, e) => x.SelectedItemChanged(e)); KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); IsTextSearchEnabledProperty.OverrideDefaultValue(true); } @@ -221,8 +219,9 @@ namespace Avalonia.Controls e.Handled = true; } } + // This part of code is needed just to acquire initial focus, subsequent focus navigation will be done by ItemsControl. else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 && - (e.Key == Key.Up || e.Key == Key.Down)) + (e.Key == Key.Up || e.Key == Key.Down) && IsFocused == true) { var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c)); if (firstChild != null) @@ -430,7 +429,18 @@ namespace Avalonia.Controls int next = SelectedIndex + 1; if (next >= ItemCount) - next = 0; + { + if (WrapSelection == true) + { + next = 0; + } + else + { + return; + } + } + + SelectedIndex = next; } @@ -440,7 +450,16 @@ namespace Avalonia.Controls int prev = SelectedIndex - 1; if (prev < 0) - prev = ItemCount - 1; + { + if (WrapSelection == true) + { + prev = ItemCount - 1; + } + else + { + return; + } + } SelectedIndex = prev; } diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs index 10b7f9bdf9..5948d81810 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs @@ -560,7 +560,7 @@ namespace Avalonia.Controls.Primitives public bool BringIntoView(IControl target, Rect targetRect) { return false; } - public IControl? GetControlInDirection(NavigationDirection direction, IControl from) { return null; } + public IControl? GetControlInDirection(NavigationDirection direction, IControl? from) { return null; } public void RaiseScrollInvalidated(EventArgs e) { diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index dd4c6561c2..70d9dbec08 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -97,6 +97,6 @@ namespace Avalonia.Controls.Generators /// /// The container. /// The index of the container, or -1 if not found. - int IndexFromContainer(IControl container); + int IndexFromContainer(IControl? container); } } diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index d02eaffeb2..79a9e16879 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -158,7 +158,7 @@ namespace Avalonia.Controls.Generators } /// - public int IndexFromContainer(IControl container) + public int IndexFromContainer(IControl? container) { foreach (var i in _containers) { diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 10e12a1ae0..ed8f9efb2e 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -143,6 +143,8 @@ namespace Avalonia.Controls protected set; } + private protected bool WrapFocus { get; set; } + event EventHandler? IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -315,7 +317,7 @@ namespace Avalonia.Controls { if (current.VisualParent == container && current is IInputElement inputElement) { - var next = GetNextControl(container, direction.Value, inputElement, false); + var next = GetNextControl(container, direction.Value, inputElement, WrapFocus); if (next != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 343c72f742..4c412e9a41 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -254,7 +254,7 @@ namespace Avalonia.Controls.Presenters /// The movement direction. /// The control from which movement begins. /// The control. - public virtual IControl? GetControlInDirection(NavigationDirection direction, IControl from) + public virtual IControl? GetControlInDirection(NavigationDirection direction, IControl? from) { return null; } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index a34e5d6438..39a512a773 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -226,18 +226,13 @@ namespace Avalonia.Controls.Presenters InvalidateScroll(); } - public override IControl? GetControlInDirection(NavigationDirection direction, IControl from) + public override IControl? GetControlInDirection(NavigationDirection direction, IControl? from) { var generator = Owner.ItemContainerGenerator; var panel = VirtualizingPanel; var itemIndex = generator.IndexFromContainer(from); var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical; - if (itemIndex == -1) - { - return null; - } - var newItemIndex = -1; switch (direction) @@ -250,6 +245,16 @@ namespace Avalonia.Controls.Presenters newItemIndex = ItemCount - 1; break; + default: + if (itemIndex == -1) + { + return null; + } + break; + } + + switch (direction) + { case NavigationDirection.Up: if (vertical) { diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 2c04b03faf..265704ceaa 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -117,7 +117,7 @@ namespace Avalonia.Controls.Presenters } /// - IControl? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from) + IControl? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl? from) { return Virtualizer?.GetControlInDirection(direction, from); } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 1c60c0d906..8629af5243 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Reactive.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; @@ -26,14 +24,14 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); - public static readonly StyledProperty SelectionBrushProperty = - AvaloniaProperty.Register(nameof(SelectionBrushProperty)); + public static readonly StyledProperty SelectionBrushProperty = + AvaloniaProperty.Register(nameof(SelectionBrushProperty)); - public static readonly StyledProperty SelectionForegroundBrushProperty = - AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); + public static readonly StyledProperty SelectionForegroundBrushProperty = + AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); - public static readonly StyledProperty CaretBrushProperty = - AvaloniaProperty.Register(nameof(CaretBrushProperty)); + public static readonly StyledProperty CaretBrushProperty = + AvaloniaProperty.Register(nameof(CaretBrushProperty)); public static readonly DirectProperty SelectionStartProperty = TextBox.SelectionStartProperty.AddOwner( @@ -48,8 +46,8 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty TextProperty = + AvaloniaProperty.RegisterDirect( nameof(Text), o => o.Text, (o, v) => o.Text = v); @@ -77,16 +75,14 @@ namespace Avalonia.Controls.Presenters private int _selectionStart; private int _selectionEnd; private bool _caretBlink; - private string _text; + private string? _text; private TextLayout? _textLayout; - private Size _constraint = Size.Infinity; + private Size _constraint; private CharacterHit _lastCharacterHit; private Rect _caretBounds; private Point _navigationPosition; - private ScrollViewer? _scrollViewer; - static TextPresenter() { AffectsRender(CaretBrushProperty, SelectionBrushProperty); @@ -114,7 +110,7 @@ namespace Avalonia.Controls.Presenters /// Gets or sets the text. /// [Content] - public string Text + public string? Text { get => _text; set => SetAndRaise(TextProperty, ref _text, value); @@ -186,7 +182,7 @@ namespace Avalonia.Controls.Presenters /// /// Gets the used to render the text. /// - public TextLayout? TextLayout + public TextLayout TextLayout { get { @@ -230,19 +226,19 @@ namespace Avalonia.Controls.Presenters set => SetValue(RevealPasswordProperty, value); } - public IBrush SelectionBrush + public IBrush? SelectionBrush { get => GetValue(SelectionBrushProperty); set => SetValue(SelectionBrushProperty, value); } - public IBrush SelectionForegroundBrush + public IBrush? SelectionForegroundBrush { get => GetValue(SelectionForegroundBrushProperty); set => SetValue(SelectionForegroundBrushProperty, value); } - public IBrush CaretBrush + public IBrush? CaretBrush { get => GetValue(CaretBrushProperty); set => SetValue(CaretBrushProperty, value); @@ -284,20 +280,14 @@ namespace Avalonia.Controls.Presenters /// /// /// A object. - private TextLayout? CreateTextLayoutInternal(Size constraint, string text, Typeface typeface, + private TextLayout CreateTextLayoutInternal(Size constraint, string? text, Typeface typeface, IReadOnlyList>? textStyleOverrides) { var foreground = Foreground; - - if (foreground == null) - { - return null; - } - var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width; var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height; - var textLayout = new TextLayout(text ?? string.Empty, typeface, FontSize, foreground, TextAlignment, + var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment, TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides, flowDirection: FlowDirection); @@ -317,11 +307,6 @@ namespace Avalonia.Controls.Presenters context.FillRectangle(background, new Rect(Bounds.Size)); } - if (TextLayout == null) - { - return; - } - var top = 0d; var left = 0.0; @@ -346,17 +331,11 @@ namespace Avalonia.Controls.Presenters public override void Render(DrawingContext context) { - if (double.IsPositiveInfinity (_constraint.Width)) - { - _constraint = _scrollViewer?.Viewport ?? Size.Infinity; - - InvalidateTextLayout(); - } - var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; + var selectionBrush = SelectionBrush; - if (selectionStart != selectionEnd && TextLayout != null) + if (selectionStart != selectionEnd && selectionBrush != null) { var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; @@ -365,7 +344,7 @@ namespace Avalonia.Controls.Presenters foreach (var rect in rects) { - context.FillRectangle(SelectionBrush, rect); + context.FillRectangle(selectionBrush, rect); } } @@ -431,38 +410,40 @@ namespace Avalonia.Controls.Presenters internal void CaretChanged() { - if (this.GetVisualParent() != null) + if (this.GetVisualParent() == null) { - if (_caretTimer.IsEnabled) - { - _caretBlink = true; - _caretTimer.Stop(); - _caretTimer.Start(); - InvalidateVisual(); - } - else - { - _caretTimer.Start(); - InvalidateVisual(); - _caretTimer.Stop(); - } + return; + } - if (IsMeasureValid) - { - this.BringIntoView(_caretBounds); - } - else - { - // The measure is currently invalid so there's no point trying to bring the - // current char into view until a measure has been carried out as the scroll - // viewer extents may not be up-to-date. - Dispatcher.UIThread.Post( - () => - { - this.BringIntoView(_caretBounds); - }, - DispatcherPriority.Render); - } + if (_caretTimer.IsEnabled) + { + _caretBlink = true; + _caretTimer.Stop(); + _caretTimer.Start(); + InvalidateVisual(); + } + else + { + _caretTimer.Start(); + InvalidateVisual(); + _caretTimer.Stop(); + } + + if (IsMeasureValid) + { + this.BringIntoView(_caretBounds); + } + else + { + // The measure is currently invalid so there's no point trying to bring the + // current char into view until a measure has been carried out as the scroll + // viewer extents may not be up-to-date. + Dispatcher.UIThread.Post( + () => + { + this.BringIntoView(_caretBounds); + }, + DispatcherPriority.Render); } } @@ -470,9 +451,9 @@ namespace Avalonia.Controls.Presenters /// Creates the used to render the text. /// /// A object. - protected virtual TextLayout? CreateTextLayout() + protected virtual TextLayout CreateTextLayout() { - TextLayout? result; + TextLayout result; var text = Text; @@ -517,14 +498,25 @@ namespace Avalonia.Controls.Presenters protected override Size MeasureOverride(Size availableSize) { - if (!double.IsInfinity(availableSize.Width) && availableSize != _constraint) + _textLayout = null; + + _constraint = availableSize; + + return TextLayout.Size; + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (!double.IsInfinity(_constraint.Width)) { - _constraint = availableSize; - - InvalidateTextLayout(); + return base.ArrangeOverride(finalSize); } + + _constraint = finalSize; + + _textLayout = null; - return TextLayout?.Size ?? default; + return base.ArrangeOverride(finalSize); } private int CoerceCaretIndex(int value) @@ -537,16 +529,12 @@ namespace Avalonia.Controls.Presenters private void CaretTimerTick(object? sender, EventArgs e) { _caretBlink = !_caretBlink; + InvalidateVisual(); } public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false) { - if (TextLayout == null) - { - return; - } - var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, trailingEdge); var textLine = TextLayout.TextLines[lineIndex]; @@ -569,29 +557,23 @@ namespace Avalonia.Controls.Presenters } _navigationPosition = _caretBounds.Position; + + CaretChanged(); } public void MoveCaretToPoint(Point point) { - if (TextLayout == null) - { - return; - } - var hit = TextLayout.HitTestPoint(point); UpdateCaret(hit.CharacterHit); _navigationPosition = _caretBounds.Position; + + CaretChanged(); } public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward) { - if (TextLayout == null) - { - return; - } - var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(CaretIndex, _lastCharacterHit.TrailingLength > 0); if (lineIndex < 0) @@ -629,14 +611,23 @@ namespace Avalonia.Controls.Presenters MoveCaretToPoint(new Point(currentX, currentY)); _navigationPosition = navigationPosition.WithY(_caretBounds.Y); + + CaretChanged(); } public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward) { - if (TextLayout == null) + if (Text is null) { return; } + + if (FlowDirection == FlowDirection.RightToLeft) + { + direction = direction == LogicalDirection.Forward ? + LogicalDirection.Backward : + LogicalDirection.Forward; + } var characterHit = _lastCharacterHit; var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -709,15 +700,12 @@ namespace Avalonia.Controls.Presenters UpdateCaret(characterHit); _navigationPosition = _caretBounds.Position; + + CaretChanged(); } private void UpdateCaret(CharacterHit characterHit) { - if (TextLayout == null) - { - return; - } - _lastCharacterHit = characterHit; var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -743,9 +731,7 @@ namespace Avalonia.Controls.Presenters CaretBoundsChanged?.Invoke(this, EventArgs.Empty); } - - CaretChanged(); - + SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); } @@ -754,19 +740,10 @@ namespace Avalonia.Controls.Presenters return _caretBounds; } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - _scrollViewer = this.FindAncestorOfType(); - } - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - _scrollViewer = null; - _caretTimer.Stop(); _caretTimer.Tick -= CaretTimerTick; diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs index 2be91c046e..0b3791be3a 100644 --- a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs +++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives /// The movement direction. /// The control from which movement begins. /// The control. - IControl? GetControlInDirection(NavigationDirection direction, IControl from); + IControl? GetControlInDirection(NavigationDirection direction, IControl? from); /// /// Raises the event. diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 840a5ac1dc..c690726e71 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -114,6 +114,12 @@ namespace Avalonia.Controls.Primitives "SelectionChanged", RoutingStrategies.Bubble); + /// + /// Defines the property. + /// + public static readonly StyledProperty WrapSelectionProperty = + AvaloniaProperty.Register(nameof(WrapSelection), defaultValue: false); + private static readonly IList Empty = Array.Empty(); private string _textSearchTerm = string.Empty; private DispatcherTimer? _textSearchTimer; @@ -321,6 +327,16 @@ namespace Avalonia.Controls.Primitives set { SetValue(IsTextSearchEnabledProperty, value); } } + /// + /// Gets or sets a value which indicates whether to wrap around when the first + /// or last item is reached. + /// + public bool WrapSelection + { + get { return GetValue(WrapSelectionProperty); } + set { SetValue(WrapSelectionProperty, value); } + } + /// /// Gets or sets the selection mode. /// @@ -515,11 +531,23 @@ namespace Avalonia.Controls.Primitives _textSearchTerm += e.Text; - bool match(ItemContainerInfo info) => - info.ContainerControl is IContentControl control && - control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + bool Match(ItemContainerInfo info) + { + if (info.ContainerControl.IsSet(TextSearch.TextProperty)) + { + var searchText = info.ContainerControl.GetValue(TextSearch.TextProperty); - var info = ItemContainerGenerator?.Containers.FirstOrDefault(match); + if (searchText?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true) + { + return true; + } + } + + return info.ContainerControl is IContentControl control && + control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + } + + var info = ItemContainerGenerator?.Containers.FirstOrDefault(Match); if (info != null) { @@ -580,6 +608,10 @@ namespace Avalonia.Controls.Primitives var newValue = change.NewValue.GetValueOrDefault(); _selection.SingleSelect = !newValue.HasAllFlags(SelectionMode.Multiple); } + else if (change.Property == WrapSelectionProperty) + { + WrapFocus = WrapSelection; + } } /// diff --git a/src/Avalonia.Controls/Primitives/TextSearch.cs b/src/Avalonia.Controls/Primitives/TextSearch.cs new file mode 100644 index 0000000000..949532cb16 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/TextSearch.cs @@ -0,0 +1,37 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Allows to customize text searching in . + /// + public static class TextSearch + { + /// + /// Defines the Text attached property. + /// This text will be considered during text search in (such as ) + /// + public static readonly AttachedProperty TextProperty + = AvaloniaProperty.RegisterAttached("Text", typeof(TextSearch)); + + /// + /// Sets the for a control. + /// + /// The control + /// The search text to set + public static void SetText(Control control, string text) + { + control.SetValue(TextProperty, text); + } + + /// + /// Gets the of a control. + /// + /// The control + /// The property value + public static string GetText(Control control) + { + return control.GetValue(TextProperty); + } + } +} diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs new file mode 100644 index 0000000000..f1d07b2679 --- /dev/null +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -0,0 +1,485 @@ +using System; +using System.Windows.Input; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; + +namespace Avalonia.Controls +{ + /// + /// A button with primary and secondary parts that can each be pressed separately. + /// The primary part behaves like a and the secondary part opens a flyout. + /// + [PseudoClasses(pcFlyoutOpen, pcPressed)] + public class SplitButton : ContentControl, ICommandSource + { + protected const string pcChecked = ":checked"; + protected const string pcPressed = ":pressed"; + protected const string pcFlyoutOpen = ":flyout-open"; + + /// + /// Raised when the user presses the primary part of the . + /// + public event EventHandler Click + { + add => AddHandler(ClickEvent, value); + remove => RemoveHandler(ClickEvent, value); + } + + /// + /// Defines the event. + /// + public static readonly RoutedEvent ClickEvent = + RoutedEvent.Register(nameof(Click), RoutingStrategies.Bubble); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CommandProperty = + Button.CommandProperty.AddOwner( + splitButton => splitButton.Command, + (splitButton, command) => splitButton.Command = command); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandParameterProperty = + Button.CommandParameterProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty FlyoutProperty = + Button.FlyoutProperty.AddOwner(); + + private ICommand? _Command; + + private Button? _primaryButton = null; + private Button? _secondaryButton = null; + + private bool _commandCanExecute = true; + private bool _isAttachedToLogicalTree = false; + private bool _isFlyoutOpen = false; + private bool _isKeyboardPressed = false; + + private IDisposable? _flyoutPropertyChangedDisposable; + + //////////////////////////////////////////////////////////////////////// + // Constructor / Destructors + //////////////////////////////////////////////////////////////////////// + + /// + /// Initializes a new instance of the class. + /// + public SplitButton() + { + } + + //////////////////////////////////////////////////////////////////////// + // Properties + //////////////////////////////////////////////////////////////////////// + + /// + /// Gets or sets the invoked when the primary part is pressed. + /// + public ICommand? Command + { + get => _Command; + set => SetAndRaise(CommandProperty, ref _Command, value); + } + + /// + /// Gets or sets a parameter to be passed to the . + /// + public object? CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + + /// + /// Gets or sets the that is shown when the secondary part is pressed. + /// + public FlyoutBase? Flyout + { + get => GetValue(FlyoutProperty); + set => SetValue(FlyoutProperty, value); + } + + /// + /// Gets a value indicating whether the button is currently checked. + /// + /// + /// This property exists only for the derived and is + /// unused (set to false) within . Doing this allows the + /// two controls to share a default style. + /// + internal virtual bool InternalIsChecked => false; + + /// + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + + //////////////////////////////////////////////////////////////////////// + // Methods + //////////////////////////////////////////////////////////////////////// + + /// + void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + + /// + private void CanExecuteChanged(object? sender, EventArgs e) + { + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } + } + + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// + protected void UpdatePseudoClasses() + { + PseudoClasses.Set(pcFlyoutOpen, _isFlyoutOpen); + PseudoClasses.Set(pcPressed, _isKeyboardPressed); + PseudoClasses.Set(pcChecked, InternalIsChecked); + } + + /// + /// Opens the secondary button's flyout. + /// + protected void OpenFlyout() + { + if (Flyout != null) + { + Flyout.ShowAt(this); + } + } + + /// + /// Closes the secondary button's flyout. + /// + protected void CloseFlyout() + { + if (Flyout != null) + { + Flyout.Hide(); + } + } + + /// + /// Registers all flyout events. + /// + /// The flyout to connect events to. + private void RegisterFlyoutEvents(FlyoutBase? flyout) + { + if (flyout != null) + { + flyout.Opened += Flyout_Opened; + flyout.Closed += Flyout_Closed; + + _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(FlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged); + } + } + + /// + /// Explicitly unregisters all flyout events. + /// + /// The flyout to disconnect events from. + private void UnregisterFlyoutEvents(FlyoutBase? flyout) + { + if (flyout != null) + { + flyout.Opened -= Flyout_Opened; + flyout.Closed -= Flyout_Closed; + + _flyoutPropertyChangedDisposable?.Dispose(); + _flyoutPropertyChangedDisposable = null; + } + } + + /// + /// Explicitly unregisters all events related to the two buttons in OnApplyTemplate(). + /// + private void UnregisterEvents() + { + if (_primaryButton != null) + { + _primaryButton.Click -= PrimaryButton_Click; + } + + if (_secondaryButton != null) + { + _secondaryButton.Click -= SecondaryButton_Click; + } + } + + //////////////////////////////////////////////////////////////////////// + // OnEvent Overridable Methods + //////////////////////////////////////////////////////////////////////// + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + UnregisterEvents(); + UnregisterFlyoutEvents(Flyout); + + _primaryButton = e.NameScope.Find public string? DefaultExtension { get; set; } + /// + /// Gets or sets a value indicating whether to display a warning if the user specifies the name of a file that already exists. + /// + public bool? ShowOverwritePrompt { get; set; } + /// /// Shows the save file dialog. /// diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c0f5980321..ed012bd8b1 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -492,13 +492,6 @@ namespace Avalonia.Controls return measuredSize.Inflate(padding); } - protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - base.OnAttachedToLogicalTree(e); - - InvalidateTextLayout(); - } - private static bool IsValidMaxLines(int maxLines) => maxLines >= 0; private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index dc6b8ffa4c..4ec0c4c5e1 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -540,14 +540,19 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); + + _imClient.SetPresenter(_presenter, this); + + if (IsFocused) + { + _presenter?.ShowCaret(); + } } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - _imClient.SetPresenter(_presenter, this); - if (IsFocused) { _presenter?.ShowCaret(); @@ -607,6 +612,8 @@ namespace Avalonia.Controls } UpdateCommandStates(); + + _imClient.SetPresenter(_presenter, this); _presenter?.ShowCaret(); } @@ -625,6 +632,8 @@ namespace Avalonia.Controls UpdateCommandStates(); _presenter?.HideCaret(); + + _imClient.SetPresenter(null, null); } protected override void OnTextInput(TextInputEventArgs e) @@ -930,6 +939,10 @@ namespace Avalonia.Controls { SelectionEnd = _presenter.CaretIndex; } + else + { + SelectionStart = SelectionEnd = _presenter.CaretIndex; + } break; } @@ -948,6 +961,10 @@ namespace Avalonia.Controls { SelectionEnd = _presenter.CaretIndex; } + else + { + SelectionStart = SelectionEnd = _presenter.CaretIndex; + } break; } @@ -1273,7 +1290,7 @@ namespace Avalonia.Controls { caretIndex = 0; } - else if (_presenter.TextLayout is not null) + else { var lines = _presenter.TextLayout.TextLines; var pos = 0; @@ -1308,7 +1325,7 @@ namespace Avalonia.Controls { caretIndex = text.Length; } - else if (_presenter.TextLayout is not null) + else { var lines = _presenter.TextLayout.TextLines; var pos = 0; diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 34b774e23f..93fbf6d17b 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -36,7 +36,7 @@ namespace Avalonia.Controls { get { - var bounds = Orientation == Orientation.Horizontal ? + var bounds = Orientation == Orientation.Horizontal ? _availableSpace.Width : _availableSpace.Height; return Math.Max(0, _takenSpace - bounds); } @@ -129,9 +129,6 @@ namespace Avalonia.Controls protected override IInputElement? GetControlInDirection(NavigationDirection direction, IControl? from) { - if (from == null) - return null; - var logicalScrollable = Parent as ILogicalScrollable; if (logicalScrollable?.IsLogicalScrollEnabled == true) @@ -145,7 +142,7 @@ namespace Avalonia.Controls } internal override void ArrangeChild( - IControl child, + IControl child, Rect rect, Size panelSize, Orientation orientation) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 819a203d62..4a5cb0fd3f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -84,7 +84,8 @@ namespace Avalonia.Diagnostics.ViewModels c.GetObservable(Control.ContextMenuProperty), c.GetObservable(FlyoutBase.AttachedFlyoutProperty), c.GetObservable(ToolTipDiagnostics.ToolTipProperty), - (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) => + c.GetObservable(Button.FlyoutProperty), + (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip, ButtonFlyout) => { if (ContextMenu != null) //Note: ContextMenus are special since all the items are added as visual children. @@ -100,6 +101,9 @@ namespace Avalonia.Diagnostics.ViewModels if (ToolTip != null) return GetPopupHostObservable(ToolTip, "ToolTip"); + if (ButtonFlyout != null) + return GetPopupHostObservable(ButtonFlyout, "Flyout"); + return Observable.Return(null); }) .Switch(), diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 7a894d96fb..a3cff7f3d3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -147,6 +147,7 @@ namespace Avalonia.Diagnostics.Views ProcessProperty(control, ContextMenuProperty); ProcessProperty(control, FlyoutBase.AttachedFlyoutProperty); ProcessProperty(control, ToolTipDiagnostics.ToolTipProperty); + ProcessProperty(control, Button.FlyoutProperty); } return popupRoots; diff --git a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs index 28d40f13b9..405a248caf 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs @@ -17,6 +17,7 @@ namespace Avalonia.Dialogs private readonly ManagedFileDialogOptions _options; public event Action CancelRequested; public event Action CompleteRequested; + public event Action OverwritePrompt; public AvaloniaList QuickLinks { get; } = new AvaloniaList(); @@ -39,6 +40,7 @@ namespace Avalonia.Dialogs private bool _scheduledSelectionValidation; private bool _alreadyCancelled = false; private string _defaultExtension; + private bool _overwritePrompt; private CompositeDisposable _disposables; public string Location @@ -167,6 +169,7 @@ namespace Avalonia.Dialogs { _savingFile = true; _defaultExtension = sfd.DefaultExtension; + _overwritePrompt = sfd.ShowOverwritePrompt ?? true; FileName = sfd.InitialFileName; } @@ -360,7 +363,16 @@ namespace Avalonia.Dialogs FileName = Path.ChangeExtension(FileName, _defaultExtension); } - CompleteRequested?.Invoke(new[] { Path.Combine(Location, FileName) }); + var fullName = Path.Combine(Location, FileName); + + if (_overwritePrompt && File.Exists(fullName)) + { + OverwritePrompt?.Invoke(fullName); + } + else + { + CompleteRequested?.Invoke(new[] { fullName }); + } } } else diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index f9e62d905b..1970c5557d 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; @@ -31,6 +32,75 @@ namespace Avalonia.Dialogs dialog.Close(); }; + model.OverwritePrompt += async (filename) => + { + Window overwritePromptDialog = new Window() + { + Title = "Confirm Save As", + SizeToContent = SizeToContent.WidthAndHeight, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Padding = new Thickness(10), + MinWidth = 270 + }; + + string name = Path.GetFileName(filename); + + var panel = new DockPanel() + { + HorizontalAlignment = Layout.HorizontalAlignment.Stretch + }; + + var label = new Label() + { + Content = $"{name} already exists.\nDo you want to replace it?" + }; + + panel.Children.Add(label); + DockPanel.SetDock(label, Dock.Top); + + var buttonPanel = new StackPanel() + { + HorizontalAlignment = Layout.HorizontalAlignment.Right, + Orientation = Layout.Orientation.Horizontal, + Spacing = 10 + }; + + var button = new Button() + { + Content = "Yes", + HorizontalAlignment = Layout.HorizontalAlignment.Right + }; + + button.Click += (sender, args) => + { + result = new string[1] { filename }; + overwritePromptDialog.Close(); + dialog.Close(); + }; + + buttonPanel.Children.Add(button); + + button = new Button() + { + Content = "No", + HorizontalAlignment = Layout.HorizontalAlignment.Right + }; + + button.Click += (sender, args) => + { + overwritePromptDialog.Close(); + }; + + buttonPanel.Children.Add(button); + + panel.Children.Add(buttonPanel); + DockPanel.SetDock(buttonPanel, Dock.Bottom); + + overwritePromptDialog.Content = panel; + + await overwritePromptDialog.ShowDialog(dialog); + }; + model.CancelRequested += dialog.Close; await dialog.ShowDialog(parent); diff --git a/src/Avalonia.Input/ICommandSource.cs b/src/Avalonia.Input/ICommandSource.cs index 410b3a2e47..a6b5189440 100644 --- a/src/Avalonia.Input/ICommandSource.cs +++ b/src/Avalonia.Input/ICommandSource.cs @@ -20,12 +20,11 @@ namespace Avalonia.Input /// object? CommandParameter { get; } - /// - /// Bor the behavior CanExecuteChanged + /// Called for the CanExecuteChanged event when changes are detected. /// - /// - /// + /// The event sender. + /// The event args. void CanExecuteChanged(object sender, System.EventArgs e); /// diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index a17f31498e..eed51e6c4a 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -274,7 +274,7 @@ namespace Avalonia.Input } private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers, IReadOnlyList? intermediatePoints) + KeyModifiers inputModifiers, Lazy?>? intermediatePoints) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 40495a2f0a..0604d09dc4 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -11,7 +11,7 @@ namespace Avalonia.Input private readonly IVisual? _rootVisual; private readonly Point _rootVisualPosition; private readonly PointerPointProperties _properties; - private readonly IReadOnlyList? _previousPoints; + private Lazy?>? _previousPoints; public PointerEventArgs(RoutedEvent routedEvent, IInteractive? source, @@ -38,7 +38,7 @@ namespace Avalonia.Input ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, - IReadOnlyList? previousPoints) + Lazy?>? previousPoints) : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { _previousPoints = previousPoints; @@ -121,13 +121,14 @@ namespace Avalonia.Input /// public IReadOnlyList GetIntermediatePoints(IVisual? relativeTo) { - if (_previousPoints == null || _previousPoints.Count == 0) + var previousPoints = _previousPoints?.Value; + if (previousPoints == null || previousPoints.Count == 0) return new[] { GetCurrentPoint(relativeTo) }; - var points = new PointerPoint[_previousPoints.Count + 1]; - for (var c = 0; c < _previousPoints.Count; c++) + var points = new PointerPoint[previousPoints.Count + 1]; + for (var c = 0; c < previousPoints.Count; c++) { - var pt = _previousPoints[c]; - points[c] = new PointerPoint(Pointer, GetPosition(pt, relativeTo), _properties); + var pt = previousPoints[c]; + points[c] = new PointerPoint(Pointer, GetPosition(pt.Position, relativeTo), _properties); } points[points.Length - 1] = GetCurrentPoint(relativeTo); diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 1fe19d9c55..c157fa059c 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -33,6 +33,8 @@ namespace Avalonia.Input.Raw /// public class RawPointerEventArgs : RawInputEventArgs { + private RawPointerPoint _point; + /// /// Initializes a new instance of the class. /// @@ -58,11 +60,50 @@ namespace Avalonia.Input.Raw Type = type; InputModifiers = inputModifiers; } + + /// + /// Initializes a new instance of the class. + /// + /// The associated device. + /// The event timestamp. + /// The root from which the event originates. + /// The type of the event. + /// The point properties and position, in client DIPs. + /// The input modifiers. + public RawPointerEventArgs( + IInputDevice device, + ulong timestamp, + IInputRoot root, + RawPointerEventType type, + RawPointerPoint point, + RawInputModifiers inputModifiers) + : base(device, timestamp, root) + { + Contract.Requires(device != null); + Contract.Requires(root != null); + + Point = point; + Type = type; + InputModifiers = inputModifiers; + } + + /// + /// Gets the pointer properties and position, in client DIPs. + /// + public RawPointerPoint Point + { + get => _point; + set => _point = value; + } /// /// Gets the mouse position, in client DIPs. /// - public Point Position { get; set; } + public Point Position + { + get => _point.Position; + set => _point.Position = value; + } /// /// Gets the type of the event. @@ -78,6 +119,19 @@ namespace Avalonia.Input.Raw /// Points that were traversed by a pointer since the previous relevant event, /// only valid for Move and TouchUpdate /// - public IReadOnlyList? IntermediatePoints { get; set; } + public Lazy?>? IntermediatePoints { get; set; } + } + + public struct RawPointerPoint + { + /// + /// Pointer position, in client DIPs. + /// + public Point Position { get; set; } + + public RawPointerPoint() + { + Position = default; + } } } diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index 33773ed8e2..b3469c212b 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -89,7 +89,9 @@ namespace Avalonia.OpenGL.Controls gl.BindTexture(GL_TEXTURE_2D, 0); gl.BindFramebuffer(GL_FRAMEBUFFER, 0); gl.DeleteFramebuffers(1, new[] { _fb }); + _fb = 0; gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); + _depthBuffer = 0; _attachment?.Dispose(); _attachment = null; _bitmap?.Dispose(); diff --git a/src/Avalonia.PlatformSupport/StandardRuntimePlatform.cs b/src/Avalonia.PlatformSupport/StandardRuntimePlatform.cs index 768966ba2d..4eeb9232cf 100644 --- a/src/Avalonia.PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Avalonia.PlatformSupport/StandardRuntimePlatform.cs @@ -201,7 +201,7 @@ namespace Avalonia.PlatformSupport #if NETCOREAPP IsCoreClr = true, #elif NETFRAMEWORK - IsDotNetFramework = false, + IsDotNetFramework = true, #endif IsDesktop = os == OperatingSystemType.Linux || os == OperatingSystemType.OSX || os == OperatingSystemType.WinNT, IsMono = os == OperatingSystemType.Android || os == OperatingSystemType.iOS || os == OperatingSystemType.Browser, diff --git a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml new file mode 100644 index 0000000000..ce20a1a165 --- /dev/null +++ b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml @@ -0,0 +1,271 @@ + + + + + + + + + + + + 24 + 24 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 702fcc7c7a..009b56852a 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -57,6 +57,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index a1f423c059..5b86de02d5 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -556,6 +556,33 @@ 1 32 + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index f75af76144..eb68270354 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -550,6 +550,33 @@ 1 32 + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index f07fc6f2e0..857d70084c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -150,7 +150,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 51836255ef..065278521e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -56,6 +56,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml new file mode 100644 index 0000000000..e0dd0180b3 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml @@ -0,0 +1,247 @@ + + + + + + + + Hello + + + + + Hello + + + + + + + + 32 + 32 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index e3b14a5ac3..64f0eaab53 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -194,6 +194,7 @@ namespace Avalonia.Media.TextFormatting var currentY = 0d; var currentPosition = 0; + var currentRect = Rect.Empty; foreach (var textLine in TextLines) { @@ -209,7 +210,10 @@ namespace Avalonia.Media.TextFormatting //The whole line is covered. if (currentPosition >= start && start + length > currentPosition + textLine.TextRange.Length) { - result.Add(new Rect(textLine.Start, currentY, textLine.WidthIncludingTrailingWhitespace, textLine.Height)); + currentRect = new Rect(textLine.Start, currentY, textLine.WidthIncludingTrailingWhitespace, + textLine.Height); + + result.Add(currentRect); currentY += textLine.Height; currentPosition += textLine.TextRange.Length; @@ -317,13 +321,23 @@ namespace Avalonia.Media.TextFormatting var width = endX - startX; - result.Add(new Rect(startX, currentY, width, textLine.Height)); - + if (result.Count > 0 && MathUtilities.AreClose(currentRect.Top, currentY) && + MathUtilities.AreClose(currentRect.Right, startX)) + { + result[result.Count - 1] = currentRect.WithWidth(currentRect.Width + width); + } + else + { + currentRect = new Rect(startX, currentY, width, textLine.Height); + + result.Add(currentRect); + } + if (currentRun.ShapedBuffer.IsLeftToRight) { if (nextRun != null) { - if (nextRun.Text.Start > currentRun.Text.Start) + if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= start + length) { break; } @@ -397,7 +411,7 @@ namespace Avalonia.Media.TextFormatting return new TextHitTestResult(); } - characterHit = currentLine.GetNextCaretCharacterHit(new CharacterHit(currentLine.TextRange.End)); + characterHit = currentLine.GetCharacterHitFromDistance(point.X); return GetHitTestResult(currentLine, characterHit, point); } @@ -424,7 +438,7 @@ namespace Avalonia.Media.TextFormatting continue; } - if (charIndex >= textLine.Start && charIndex <= textLine.TextRange.End + (trailingEdge ? 1 : 0)) + if (charIndex >= textLine.TextRange.Start && charIndex <= textLine.TextRange.End + (trailingEdge ? 1 : 0)) { return index; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs index 9bbc4a8a9d..130d0e9c39 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Avalonia.Media.TextFormatting { @@ -210,10 +211,10 @@ namespace Avalonia.Media.TextFormatting switch (textAlignment) { case TextAlignment.Center: - return (paragraphWidth - width) / 2; + return Math.Max(0, (paragraphWidth - width) / 2); case TextAlignment.Right: - return paragraphWidth - widthIncludingTrailingWhitespace; + return Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace); default: return 0; @@ -223,13 +224,13 @@ namespace Avalonia.Media.TextFormatting switch (textAlignment) { case TextAlignment.Center: - return (paragraphWidth - width) / 2; + return Math.Max(0, (paragraphWidth - width) / 2); case TextAlignment.Right: return 0; default: - return paragraphWidth - widthIncludingTrailingWhitespace; + return Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace); } } } diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index 77e410162f..a0f146afa8 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -63,7 +63,7 @@ namespace Avalonia.X11.NativeDialogs public static IDisposable ConnectSignal(IntPtr obj, string name, T handler) { var handle = GCHandle.Alloc(handler); - var ptr = Marshal.GetFunctionPointerForDelegate((Delegate)(object)handler); + var ptr = Marshal.GetFunctionPointerForDelegate(handler); using (var utf = new Utf8Buffer(name)) { var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0); @@ -179,6 +179,9 @@ namespace Avalonia.X11.NativeDialogs [DllImport(GtkName)] public static extern void gtk_file_chooser_set_select_multiple(IntPtr chooser, bool allow); + + [DllImport(GtkName)] + public static extern void gtk_file_chooser_set_do_overwrite_confirmation(IntPtr chooser, bool do_overwrite_confirmation); [DllImport(GtkName)] public static extern void diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index 6cf6c6f35f..1a6514eb03 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -16,7 +16,7 @@ namespace Avalonia.X11.NativeDialogs { private Task _initialized; private unsafe Task ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action, - bool multiSelect, string initialFileName, IEnumerable filters, string defaultExtension) + bool multiSelect, string initialFileName, IEnumerable filters, string defaultExtension, bool overwritePrompt) { IntPtr dlg; using (var name = new Utf8Buffer(title)) @@ -109,6 +109,8 @@ namespace Avalonia.X11.NativeDialogs gtk_file_chooser_set_filename(dlg, fn); } + gtk_file_chooser_set_do_overwrite_confirmation(dlg, overwritePrompt); + gtk_window_present(dlg); return tcs.Task; } @@ -148,7 +150,7 @@ namespace Avalonia.X11.NativeDialogs (dialog as OpenFileDialog)?.AllowMultiple ?? false, Path.Combine(string.IsNullOrEmpty(dialog.Directory) ? "" : dialog.Directory, string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters, - (dialog as SaveFileDialog)?.DefaultExtension)); + (dialog as SaveFileDialog)?.DefaultExtension, (dialog as SaveFileDialog)?.ShowOverwritePrompt ?? false)); } public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) @@ -160,7 +162,7 @@ namespace Avalonia.X11.NativeDialogs return await await RunOnGlibThread(async () => { var res = await ShowDialog(dialog.Title, platformImpl, - GtkFileChooserAction.SelectFolder, false, dialog.Directory, null, null); + GtkFileChooserAction.SelectFolder, false, dialog.Directory, null, null, false); return res?.FirstOrDefault(); }); } diff --git a/src/Avalonia.X11/SMLib.cs b/src/Avalonia.X11/SMLib.cs index 6ffcad60bf..e2b39cfcff 100644 --- a/src/Avalonia.X11/SMLib.cs +++ b/src/Avalonia.X11/SMLib.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Runtime.InteropServices; @@ -7,20 +8,19 @@ namespace Avalonia.X11 { private const string LibSm = "libSM.so.6"; - [DllImport(LibSm, CharSet = CharSet.Ansi)] + [DllImport(LibSm, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] public static extern IntPtr SmcOpenConnection( - [MarshalAs(UnmanagedType.LPWStr)] string networkId, + [MarshalAs(UnmanagedType.LPStr)] string? networkId, IntPtr content, int xsmpMajorRev, int xsmpMinorRev, - ulong mask, + nuint mask, ref SmcCallbacks callbacks, - [MarshalAs(UnmanagedType.LPWStr)] [Out] - out string previousId, - [MarshalAs(UnmanagedType.LPWStr)] [Out] - out string clientIdRet, + [MarshalAs(UnmanagedType.LPStr)] string? previousId, + ref IntPtr clientIdRet, int errorLength, - [Out] char[] errorStringRet); + [Out] byte[] errorStringRet + ); [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] public static extern int SmcCloseConnection( diff --git a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs index 06e1d8879f..1a17a018e8 100644 --- a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs +++ b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Text; using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Threading; @@ -14,10 +15,10 @@ namespace Avalonia.X11 internal unsafe class X11PlatformLifetimeEvents : IDisposable, IPlatformLifetimeEventsImpl { private readonly AvaloniaX11Platform _platform; - private const ulong SmcSaveYourselfProcMask = 1L; - private const ulong SmcDieProcMask = 2L; - private const ulong SmcSaveCompleteProcMask = 4L; - private const ulong SmcShutdownCancelledProcMask = 8L; + private const nuint SmcSaveYourselfProcMask = 1; + private const nuint SmcDieProcMask = 2; + private const nuint SmcSaveCompleteProcMask = 4; + private const nuint SmcShutdownCancelledProcMask = 8; private static readonly ConcurrentDictionary s_nativeToManagedMapper = new ConcurrentDictionary(); @@ -62,24 +63,24 @@ namespace Avalonia.X11 return; } - var errorBuf = new char[255]; - - var smcConn = SMLib.SmcOpenConnection(null!, + byte[] errorBuf = new byte[255]; + IntPtr clientIdRet = IntPtr.Zero; + var smcConn = SMLib.SmcOpenConnection(null, IntPtr.Zero, 1, 0, SmcSaveYourselfProcMask | SmcSaveCompleteProcMask | SmcShutdownCancelledProcMask | SmcDieProcMask, ref s_callbacks, - out _, - out _, + null, + ref clientIdRet, errorBuf.Length, errorBuf); if (smcConn == IntPtr.Zero) { Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, - $"SMLib/ICELib reported a new error: {new string(errorBuf)}"); + $"SMLib/ICELib reported a new error: {Encoding.ASCII.GetString(errorBuf)}"); return; } diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs index 25b4b41e56..084593ffc6 100644 --- a/src/Shared/RawEventGrouping.cs +++ b/src/Shared/RawEventGrouping.cs @@ -53,7 +53,7 @@ internal class RawEventGrouper : IDisposable _eventCallback?.Invoke(ev); - if (ev is RawPointerEventArgs { IntermediatePoints: PooledList list }) + if (ev is RawPointerEventArgs { IntermediatePoints.Value: PooledList list }) list.Dispose(); if (Dispatcher.UIThread.HasJobsWithPriority(DispatcherPriority.Input + 1)) @@ -110,10 +110,14 @@ internal class RawEventGrouper : IDisposable AddToQueue(args); } + private static IReadOnlyList GetPooledList() => new PooledList(); + private static readonly Func> s_getPooledListDelegate = GetPooledList; + private static void MergeEvents(RawPointerEventArgs last, RawPointerEventArgs current) { - last.IntermediatePoints ??= new PooledList(); - ((PooledList)last.IntermediatePoints).Add(last.Position); + + last.IntermediatePoints ??= new Lazy?>(s_getPooledListDelegate); + ((PooledList)last.IntermediatePoints.Value!).Add(new RawPointerPoint { Position = last.Position }); last.Position = current.Position; last.Timestamp = current.Timestamp; last.InputModifiers = current.InputModifiers; diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs index 72438609d5..d0b45b7c5d 100644 --- a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs @@ -1,10 +1,11 @@ -using Avalonia.Platform; +using System; +using Avalonia.Platform; using Avalonia.Rendering; using SkiaSharp; namespace Avalonia.Skia.Helpers { - public class DrawingContextHelper + public static class DrawingContextHelper { /// /// Wrap Skia canvas in drawing context so we can use Avalonia api to render to external skia canvas @@ -27,5 +28,71 @@ namespace Avalonia.Skia.Helpers return new DrawingContextImpl(createInfo); } + + /// + /// Unsupported - Wraps a GPU Backed SkiaSurface in an Avalonia DrawingContext. + /// + [Obsolete] + public static ISkiaDrawingContextImpl WrapSkiaSurface(this SKSurface surface, GRContext grContext, Vector dpi, params IDisposable[] disposables) + { + var createInfo = new DrawingContextImpl.CreateInfo + { + GrContext = grContext, + Surface = surface, + Dpi = dpi, + DisableTextLcdRendering = false, + }; + + return new DrawingContextImpl(createInfo, disposables); + } + + /// + /// Unsupported - Wraps a non-GPU Backed SkiaSurface in an Avalonia DrawingContext. + /// + [Obsolete] + public static ISkiaDrawingContextImpl WrapSkiaSurface(this SKSurface surface, Vector dpi, params IDisposable[] disposables) + { + var createInfo = new DrawingContextImpl.CreateInfo + { + Surface = surface, + Dpi = dpi, + DisableTextLcdRendering = false, + }; + + return new DrawingContextImpl(createInfo, disposables); + } + + [Obsolete] + public static ISkiaDrawingContextImpl CreateDrawingContext(Size size, Vector dpi, GRContext grContext = null) + { + if (grContext is null) + { + var surface = SKSurface.Create( + new SKImageInfo( + (int)Math.Ceiling(size.Width), + (int)Math.Ceiling(size.Height), + SKImageInfo.PlatformColorType, + SKAlphaType.Premul)); + + return WrapSkiaSurface(surface, dpi, surface); + } + else + { + var surface = SKSurface.Create(grContext, false, + new SKImageInfo( + (int)Math.Ceiling(size.Width), + (int)Math.Ceiling(size.Height), + SKImageInfo.PlatformColorType, + SKAlphaType.Premul)); + + return WrapSkiaSurface(surface, grContext, dpi, surface); + } + } + + [Obsolete] + public static void DrawTo(this ISkiaDrawingContextImpl source, ISkiaDrawingContextImpl destination, SKPaint paint = null) + { + destination.SkCanvas.DrawSurface(source.SkSurface, new SKPoint(0, 0), paint); + } } } diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj index cc604a9753..98fdccfe83 100644 --- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj +++ b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj @@ -5,6 +5,7 @@ enable Avalonia.Web.Blazor preview + false true diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index c74c5fbc01..1809fcf98b 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -862,7 +862,7 @@ namespace Avalonia.Win32.Interop public void Init() { - biSize = (uint)Marshal.SizeOf(this); + biSize = (uint)sizeof(BITMAPINFOHEADER); } } @@ -1521,7 +1521,7 @@ namespace Avalonia.Win32.Interop internal static Version RtlGetVersion() { RTL_OSVERSIONINFOEX v = new RTL_OSVERSIONINFOEX(); - v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(v); + v.dwOSVersionInfoSize = (uint)Marshal.SizeOf(); if (RtlGetVersion(ref v) == 0) { return new Version((int)v.dwMajorVersion, (int)v.dwMinorVersion, (int)v.dwBuildNumber); @@ -1914,7 +1914,7 @@ namespace Avalonia.Win32.Interop get { WINDOWPLACEMENT result = new WINDOWPLACEMENT(); - result.Length = Marshal.SizeOf(result); + result.Length = Marshal.SizeOf(); return result; } } diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index 29844368db..410ba55627 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -40,6 +40,16 @@ namespace Avalonia.Win32 { options |= FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT; } + + if (dialog is SaveFileDialog saveFileDialog) + { + var overwritePrompt = saveFileDialog.ShowOverwritePrompt ?? true; + + if (!overwritePrompt) + { + options &= ~FILEOPENDIALOGOPTIONS.FOS_OVERWRITEPROMPT; + } + } frm.SetOptions(options); var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? ""; @@ -79,7 +89,16 @@ namespace Avalonia.Win32 } } - frm.Show(hWnd); + var showResult = frm.Show(hWnd); + + if ((uint)showResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) + { + return result; + } + else if ((uint)showResult != (uint)UnmanagedMethods.HRESULT.S_OK) + { + throw new Win32Exception(showResult); + } if (openDialog?.AllowMultiple == true) { @@ -108,10 +127,6 @@ namespace Avalonia.Win32 } catch (COMException ex) { - if ((uint)ex.HResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) - { - return result; - } throw new Win32Exception(ex.HResult); } })!; @@ -151,7 +166,17 @@ namespace Avalonia.Win32 } } - frm.Show(hWnd); + var showResult = frm.Show(hWnd); + + if ((uint)showResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) + { + return result; + } + else if ((uint)showResult != (uint)UnmanagedMethods.HRESULT.S_OK) + { + throw new Win32Exception(showResult); + } + if (frm.Result is not null) { result = GetAbsoluteFilePath(frm.Result); @@ -161,10 +186,6 @@ namespace Avalonia.Win32 } catch (COMException ex) { - if ((uint)ex.HResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) - { - return result; - } throw new Win32Exception(ex.HResult); } }); diff --git a/src/Windows/Avalonia.Win32/Win32Com/win32.idl b/src/Windows/Avalonia.Win32/Win32Com/win32.idl index 904ac41ff2..54e9aa583f 100644 --- a/src/Windows/Avalonia.Win32/Win32Com/win32.idl +++ b/src/Windows/Avalonia.Win32/Win32Com/win32.idl @@ -112,7 +112,7 @@ interface IShellItemArray : IUnknown interface IModalWindow : IUnknown { [local] - HRESULT Show( + int Show( [in, unique] HWND hwndOwner); } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e4f5268285..d1945e6c85 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -228,7 +228,7 @@ namespace Avalonia.Win32 return new Size(rcWindow.Width, rcWindow.Height) / RenderScaling; } - DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, Marshal.SizeOf(typeof(RECT))); + DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, Marshal.SizeOf()); return new Size(rect.Width, rect.Height) / RenderScaling; } } @@ -265,7 +265,7 @@ namespace Avalonia.Win32 { if (IsWindowVisible(_hwnd)) { - ShowWindow(value, true); + ShowWindow(value, value != WindowState.Minimized); // If the window is minimized, it shouldn't be activated } _showWindowState = value; @@ -337,7 +337,7 @@ namespace Avalonia.Win32 private WindowTransparencyLevel Win8xEnableBlur(WindowTransparencyLevel transparencyLevel) { var accent = new AccentPolicy(); - var accentStructSize = Marshal.SizeOf(accent); + var accentStructSize = Marshal.SizeOf(); if (transparencyLevel == WindowTransparencyLevel.AcrylicBlur) { @@ -392,7 +392,7 @@ namespace Avalonia.Win32 bool canUseAcrylic = Win32Platform.WindowsVersion.Major > 10 || Win32Platform.WindowsVersion.Build >= 19628; var accent = new AccentPolicy(); - var accentStructSize = Marshal.SizeOf(accent); + var accentStructSize = Marshal.SizeOf(); if (transparencyLevel == WindowTransparencyLevel.AcrylicBlur && !canUseAcrylic) { @@ -973,7 +973,7 @@ namespace Avalonia.Win32 { case WindowState.Minimized: newWindowProperties.IsFullScreen = false; - command = activate ? ShowWindowCommand.Minimize : ShowWindowCommand.ShowMinNoActive; + command = ShowWindowCommand.Minimize; break; case WindowState.Maximized: newWindowProperties.IsFullScreen = false; diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 7c57e22933..aa63e18691 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -613,5 +613,54 @@ namespace Avalonia.Controls.UnitTests Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); } + + private void RaiseKeyEvent(ListBox listBox, Key key, KeyModifiers inputModifiers = 0) + { + listBox.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + KeyModifiers = inputModifiers, + Key = key + }); + } + + [Fact] + public void WrapSelection_Should_Wrap() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray(); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), + WrapSelection = true + }; + + Prepare(target); + + var lbItems = target.GetLogicalChildren().OfType().ToArray(); + + var first = lbItems.First(); + var last = lbItems.Last(); + + first.Focus(); + + RaisePressedEvent(target, first, MouseButton.Left); + Assert.Equal(true, first.IsSelected); + + RaiseKeyEvent(target, Key.Up); + Assert.Equal(true, last.IsSelected); + + RaiseKeyEvent(target, Key.Down); + Assert.Equal(true, first.IsSelected); + + target.WrapSelection = false; + RaiseKeyEvent(target, Key.Up); + + Assert.Equal(true, first.IsSelected); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 514d3b5475..0e0ca7cd25 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1,9 +1,11 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using System.Reactive.Disposables; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; @@ -13,7 +15,9 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Data; +using Avalonia.Platform; using Avalonia.Styling; +using Avalonia.Threading; using Avalonia.UnitTests; using Moq; using Xunit; @@ -1895,6 +1899,54 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, carouselRaised); } + [Fact] + public void Setting_IsTextSearchEnabled_Enables_Or_Disables_Text_Search() + { + var pti = Mock.Of(x => x.CurrentThreadIsLoopThread == true); + + Mock.Get(pti) + .Setup(v => v.StartTimer(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Disposable.Empty); + + using (UnitTestApplication.Start(TestServices.StyledWindow.With(threadingInterface: pti))) + { + var items = new[] + { + new Item { [TextSearch.TextProperty] = "Foo" }, + new Item { [TextSearch.TextProperty] = "Bar" } + }; + + var target = new SelectingItemsControl + { + Items = items, + Template = Template(), + IsTextSearchEnabled = false + }; + + Prepare(target); + + target.RaiseEvent(new TextInputEventArgs + { + RoutedEvent = InputElement.TextInputEvent, + Device = KeyboardDevice.Instance, + Text = "Foo" + }); + + Assert.Null(target.SelectedItem); + + target.IsTextSearchEnabled = true; + + target.RaiseEvent(new TextInputEventArgs + { + RoutedEvent = InputElement.TextInputEvent, + Device = KeyboardDevice.Instance, + Text = "Foo" + }); + + Assert.Equal(items[0], target.SelectedItem); + } + } + private static void Prepare(SelectingItemsControl target) { var root = new TestRoot diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index b525931061..595bcd4009 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -735,27 +735,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = layout.TextLines[0]; - var firstStart = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1)); - - var firstEnd = textLine.GetDistanceFromCharacterHit(new CharacterHit(0)); - - var secondStart = textLine.GetDistanceFromCharacterHit(new CharacterHit(6)); + var start = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1)); - var secondEnd = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1)); + var end = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1)); var rects = layout.HitTestTextRange(0, 7).ToArray(); - Assert.Equal(2, rects.Length); - - var firstExpected = rects[0]; - - Assert.Equal(firstExpected.Left, firstStart); - Assert.Equal(firstExpected.Right, firstEnd); + Assert.Equal(1, rects.Length); - var secondExpected = rects[1]; + var expected = rects[0]; - Assert.Equal(secondExpected.Left, secondStart); - Assert.Equal(secondExpected.Right, secondEnd); + Assert.Equal(expected.Left, start); + Assert.Equal(expected.Right, end); } }