diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 7221fe4657..dec94a44d5 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -15,28 +15,40 @@ jobs: version: 7.0.101 - script: system_profiler SPDisplaysDataType |grep Resolution + displayName: 'Get Resolution' - script: | + arch="x64" + if [[ $(uname -m) == 'arm64' ]]; then + arch="arm64" + fi sudo xcode-select -s /Applications/Xcode.app/Contents/Developer pkill node - appium & + appium > appium.out & pkill IntegrationTestApp ./build.sh CompileNative rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")") pkill IntegrationTestApp ./samples/IntegrationTestApp/bundle.sh - open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-arm64/publish/IntegrationTestApp.app + open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-$arch/publish/IntegrationTestApp.app pkill IntegrationTestApp + displayName: 'Build IntegrationTestApp' - task: DotNetCoreCLI@2 + displayName: 'Run Integration Tests' inputs: command: 'test' projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' + arguments: '-l "console;verbosity=detailed"' - script: | pkill IntegrationTestApp pkill node + displayName: 'Stop Appium' + - publish: appium.out + displayName: 'Publish appium logs on failure' + condition: failed() - job: Windows pool: @@ -60,11 +72,13 @@ jobs: displayName: 'Start WinAppDriver' - task: DotNetCoreCLI@2 + displayName: 'Build IntegrationTestApp' inputs: command: 'build' projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj' - task: DotNetCoreCLI@2 + displayName: 'Run Integration Tests' retryCountOnTaskFailure: 3 inputs: command: 'test' diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 4ae6ad5a00..bcfdc23053 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -127,11 +127,8 @@ [self updateRenderTarget]; auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; - - if(_parent->IsShown()) - { - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); - } + + _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); } } diff --git a/native/Avalonia.Native/src/OSX/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm index 83ab1bfd01..85f4b7c50a 100644 --- a/native/Avalonia.Native/src/OSX/Screens.mm +++ b/native/Avalonia.Native/src/OSX/Screens.mm @@ -41,7 +41,7 @@ public: ret->WorkingArea.X = [screen visibleFrame].origin.x; ret->WorkingArea.Y = ConvertPointY(ToAvnPoint([screen visibleFrame].origin)).Y - ret->WorkingArea.Height; - ret->Scaling = [screen backingScaleFactor]; + ret->Scaling = 1; ret->IsPrimary = index == 0; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 59102e15a6..b579920c6b 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -4,6 +4,7 @@ // #import +#import #include "common.h" #include "AvnView.h" #include "menu.h" @@ -293,15 +294,24 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso } @try { - if(x != lastSize.width || y != lastSize.height) { - lastSize = NSSize{x, y}; - + if(x != lastSize.width || y != lastSize.height) + { if (!_shown) { - BaseEvents->Resized(AvnSize{x, y}, reason); - } else if (Window != nullptr) { - [Window setContentSize:lastSize]; - [Window invalidateShadow]; + auto screenSize = [Window screen].visibleFrame.size; + + if (x > screenSize.width) { + x = screenSize.width; + } + + if (y > screenSize.height) { + y = screenSize.height; + } } + + lastSize = NSSize{x, y}; + + [Window setContentSize:lastSize]; + [Window invalidateShadow]; } } @finally { diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index cf1ee6943d..840f2c9e88 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -54,6 +54,11 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { WindowBaseImpl::Show(activate, isDialog); GetWindowState(&_actualWindowState); + + if(IsZoomed()) { + _lastWindowState = _actualWindowState; + } + return SetWindowState(_lastWindowState); } } diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index a8913fdb5e..40232947d9 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -165,10 +165,10 @@ partial class Build : NukeBuild foreach (var fw in targetFrameworks) { if (fw.StartsWith("net4") - && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") { - Information($"Skipping {projectName} ({fw}) tests on Linux - https://github.com/mono/mono/issues/13969"); + Information($"Skipping {projectName} ({fw}) tests on *nix - https://github.com/mono/mono/issues/13969"); continue; } diff --git a/packages/Avalonia/Avalonia.props b/packages/Avalonia/Avalonia.props index 6f21971d3d..26908b1081 100644 --- a/packages/Avalonia/Avalonia.props +++ b/packages/Avalonia/Avalonia.props @@ -6,4 +6,9 @@ false + + + + + diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index 056d3bf552..f094c0081c 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -5,6 +5,7 @@ + diff --git a/samples/ControlCatalog.Android/Resources/values/styles.xml b/samples/ControlCatalog.Android/Resources/values/styles.xml index 49e079a719..3e1270256d 100644 --- a/samples/ControlCatalog.Android/Resources/values/styles.xml +++ b/samples/ControlCatalog.Android/Resources/values/styles.xml @@ -4,7 +4,7 @@ - diff --git a/samples/ControlCatalog.Browser/app.css b/samples/ControlCatalog.Browser/app.css index 27680f6e1a..0e6ab12461 100644 --- a/samples/ControlCatalog.Browser/app.css +++ b/samples/ControlCatalog.Browser/app.css @@ -1,4 +1,11 @@ -#out { +:root { + --sat: env(safe-area-inset-top); + --sar: env(safe-area-inset-right); + --sab: env(safe-area-inset-bottom); + --sal: env(safe-area-inset-left); +} + +#out { height: 100vh; width: 100vw } diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index e55f003133..85c159467b 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Fonts.Inter; using Avalonia.Headless; using Avalonia.LogicalTree; using Avalonia.Threading; @@ -124,6 +125,7 @@ namespace ControlCatalog.NetCore EnableIme = true }) .UseSkia() + .WithInterFont() .AfterSetup(builder => { builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index 74d5b2fd8c..b4dac5399c 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -3,7 +3,7 @@ Exe manual net6.0-ios - 10.0 + 13.0 True iossimulator-x64 @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog.iOS/Info.plist b/samples/ControlCatalog.iOS/Info.plist index 6ffe3ba662..1dd4416c28 100644 --- a/samples/ControlCatalog.iOS/Info.plist +++ b/samples/ControlCatalog.iOS/Info.plist @@ -13,7 +13,7 @@ LSRequiresIPhoneOS MinimumOSVersion - 10.0 + 13.0 UIDeviceFamily 1 @@ -39,9 +39,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIStatusBarHidden - - UIViewControllerBasedStatusBarAppearance - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index d71d51f068..246fe4385f 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -44,11 +44,11 @@ namespace ControlCatalog { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) { - desktopLifetime.MainWindow = new MainWindow(); + desktopLifetime.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() }; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) { - singleViewLifetime.MainView = new MainView(); + singleViewLifetime.MainView = new MainView { DataContext = new MainWindowViewModel() }; } base.OnFrameworkInitializationCompleted(); diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 6f31d22677..9c439c874f 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -12,6 +13,7 @@ using Avalonia.VisualTree; using Avalonia.Styling; using ControlCatalog.Models; using ControlCatalog.Pages; +using ControlCatalog.ViewModels; namespace ControlCatalog { @@ -99,13 +101,47 @@ namespace ControlCatalog }; } + internal MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!; + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); + var decorations = this.Get("Decorations"); if (VisualRoot is Window window) decorations.SelectedIndex = (int)window.SystemDecorations; - + + var insets = TopLevel.GetTopLevel(this)!.InsetsManager; + if (insets != null) + { + // In real life application these events should be unsubscribed to avoid memory leaks. + ViewModel.SafeAreaPadding = insets.SafeAreaPadding; + insets.SafeAreaChanged += (sender, args) => + { + ViewModel.SafeAreaPadding = insets.SafeAreaPadding; + }; + + ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; + + ViewModel.PropertyChanged += async (sender, args) => + { + if (args.PropertyName == nameof(ViewModel.DisplayEdgeToEdge)) + { + insets.DisplayEdgeToEdge = ViewModel.DisplayEdgeToEdge; + } + else if (args.PropertyName == nameof(ViewModel.IsSystemBarVisible)) + { + insets.IsSystemBarVisible = ViewModel.IsSystemBarVisible; + } + + // Give the OS some time to apply new values and refresh the view model. + await Task.Delay(100); + ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; + }; + } + _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); } diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index c589f41442..10ff94d25c 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -17,7 +17,6 @@ namespace ControlCatalog { this.InitializeComponent(); - DataContext = new MainWindowViewModel(); _recentMenu = ((NativeMenu.GetMenu(this)?.Items[0] as NativeMenuItem)?.Menu?.Items[2] as NativeMenuItem)?.Menu; } diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 6d624c9a07..6d759597b5 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); var fontComboBox = this.Get("fontComboBox"); - fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); + fontComboBox.Items = FontManager.Current.SystemFonts; fontComboBox.SelectedIndex = 0; } } diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index d690058b27..bcc1a71243 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -5,11 +5,21 @@ xmlns:viewModels="using:ControlCatalog.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ControlCatalog.Pages.WindowCustomizationsPage" - x:DataType="viewModels:MainWindowViewModel"> - - - - - + x:DataType="viewModels:MainWindowViewModel" + x:CompileBindings="True"> + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 3628a9b8a7..8c6f0a2bd6 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -6,6 +6,7 @@ using Avalonia.Platform; using Avalonia.Reactive; using System; using System.ComponentModel.DataAnnotations; +using Avalonia; using MiniMvvm; namespace ControlCatalog.ViewModels @@ -20,6 +21,9 @@ namespace ControlCatalog.ViewModels private bool _systemTitleBarEnabled; private bool _preferSystemChromeEnabled; private double _titleBarHeight; + private bool _isSystemBarVisible; + private bool _displayEdgeToEdge; + private Thickness _safeAreaPadding; public MainWindowViewModel() { @@ -78,25 +82,25 @@ namespace ControlCatalog.ViewModels { get { return _chromeHints; } set { this.RaiseAndSetIfChanged(ref _chromeHints, value); } - } + } public bool ExtendClientAreaEnabled { get { return _extendClientAreaEnabled; } set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } - } + } public bool SystemTitleBarEnabled { get { return _systemTitleBarEnabled; } set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); } - } + } public bool PreferSystemChromeEnabled { get { return _preferSystemChromeEnabled; } set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); } - } + } public double TitleBarHeight { @@ -122,6 +126,24 @@ namespace ControlCatalog.ViewModels set { this.RaiseAndSetIfChanged(ref _isMenuItemChecked, value); } } + public bool IsSystemBarVisible + { + get { return _isSystemBarVisible; } + set { this.RaiseAndSetIfChanged(ref _isSystemBarVisible, value); } + } + + public bool DisplayEdgeToEdge + { + get { return _displayEdgeToEdge; } + set { this.RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); } + } + + public Thickness SafeAreaPadding + { + get { return _safeAreaPadding; } + set { this.RaiseAndSetIfChanged(ref _safeAreaPadding, value); } + } + public MiniCommand AboutCommand { get; } public MiniCommand ExitCommand { get; } diff --git a/samples/GpuInterop/GpuInterop.csproj b/samples/GpuInterop/GpuInterop.csproj index 88e6d3d283..161821d92a 100644 --- a/samples/GpuInterop/GpuInterop.csproj +++ b/samples/GpuInterop/GpuInterop.csproj @@ -15,6 +15,7 @@ + diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index 00987429d0..bd6910dd4d 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -1,41 +1,48 @@ - - - - - - - - - - - - - - - - - - - - - - Normal - Minimized - Maximized - FullScreen - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + Normal + Minimized + Maximized + FullScreen + + + + + + + diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs index 43875dd990..f0be34fdaa 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -7,6 +7,25 @@ using Avalonia.Threading; namespace IntegrationTestApp { + public class MeasureBorder : Border + { + protected override Size MeasureOverride(Size availableSize) + { + MeasuredWith = availableSize; + + return base.MeasureOverride(availableSize); + } + + public static readonly StyledProperty MeasuredWithProperty = AvaloniaProperty.Register( + nameof(MeasuredWith)); + + public Size MeasuredWith + { + get => GetValue(MeasuredWithProperty); + set => SetValue(MeasuredWithProperty, value); + } + } + public class ShowWindowTest : Window { private readonly DispatcherTimer? _timer; @@ -16,11 +35,11 @@ namespace IntegrationTestApp { InitializeComponent(); DataContext = this; - PositionChanged += (s, e) => this.GetControl("Position").Text = $"{Position}"; + PositionChanged += (s, e) => this.GetControl("CurrentPosition").Text = $"{Position}"; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - _orderTextBox = this.GetControl("Order"); + _orderTextBox = this.GetControl("CurrentOrder"); _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; _timer.Tick += TimerOnTick; _timer.Start(); @@ -36,13 +55,13 @@ namespace IntegrationTestApp { base.OnOpened(e); var scaling = PlatformImpl!.DesktopScaling; - this.GetControl("Position").Text = $"{Position}"; - this.GetControl("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; - this.GetControl("Scaling").Text = $"{scaling}"; + this.GetControl("CurrentPosition").Text = $"{Position}"; + this.GetControl("CurrentScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; + this.GetControl("CurrentScaling").Text = $"{scaling}"; if (Owner is not null) { - var ownerRect = this.GetControl("OwnerRect"); + var ownerRect = this.GetControl("CurrentOwnerRect"); var owner = (Window)Owner; ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}"; } diff --git a/samples/IntegrationTestApp/bundle.sh b/samples/IntegrationTestApp/bundle.sh index 505991582e..e52968215b 100755 --- a/samples/IntegrationTestApp/bundle.sh +++ b/samples/IntegrationTestApp/bundle.sh @@ -1,5 +1,12 @@ #!/usr/bin/env bash cd $(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) -dotnet restore -r osx-arm64 -dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-arm64 -p:_AvaloniaUseExternalMSBuild=false \ No newline at end of file + +arch="x64" + +if [[ $(uname -m) == 'arm64' ]]; then +arch="arm64" +fi + +dotnet restore -r osx-$arch +dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-$arch -p:_AvaloniaUseExternalMSBuild=false \ No newline at end of file diff --git a/samples/MobileSandbox/MobileSandbox.csproj b/samples/MobileSandbox/MobileSandbox.csproj index 02e4c43960..0d7c7859f8 100644 --- a/samples/MobileSandbox/MobileSandbox.csproj +++ b/samples/MobileSandbox/MobileSandbox.csproj @@ -28,6 +28,7 @@ + diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 5f61a08f3c..40efe818d8 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -7,6 +7,7 @@ + diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index 76c1ba7b69..b572c3e4f8 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -10,6 +10,7 @@ + diff --git a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj index 9650068434..0e054d6c36 100644 --- a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj +++ b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj @@ -7,6 +7,7 @@ + diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj index 3c62af1eaf..b37ecf01b9 100644 --- a/samples/RenderDemo/RenderDemo.csproj +++ b/samples/RenderDemo/RenderDemo.csproj @@ -12,6 +12,7 @@ + diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index eab654acb6..f23e391a2a 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -10,6 +10,7 @@ + diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index b27cfe77e8..81b30c6cbe 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -5,6 +5,7 @@ + diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index 247008c503..eb4b6bf6a0 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Android.App; using Android.Content; using Android.Content.PM; @@ -32,6 +33,9 @@ namespace Avalonia.Android lifetime.View = View; } + Window?.ClearFlags(WindowManagerFlags.TranslucentStatus); + Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds); + base.OnCreate(savedInstanceState); SetContentView(View); @@ -55,6 +59,17 @@ namespace Avalonia.Android } } + protected override void OnResume() + { + base.OnResume(); + + // Android only respects LayoutInDisplayCutoutMode value if it has been set once before window becomes visible. + if (Build.VERSION.SdkInt >= BuildVersionCodes.P) + { + Window.Attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges; + } + } + public event EventHandler BackRequested; public override void OnBackPressed() diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 2a345a857c..27481a598e 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -8,6 +8,7 @@ using Avalonia.Android.Platform; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; using Avalonia.Controls.Embedding; +using Avalonia.Controls.Platform; using Avalonia.Platform; using Avalonia.Rendering; @@ -67,6 +68,11 @@ namespace Avalonia.Android } _root.Renderer.Start(); + + if (_view.TryGetFeature(out var insetsManager) == true) + { + (insetsManager as AndroidInsetsManager)?.ApplyStatusBarState(); + } } else { diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs new file mode 100644 index 0000000000..35d1b06e6a --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using Android.OS; +using Android.Views; +using AndroidX.AppCompat.App; +using AndroidX.Core.View; +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Controls.Platform; +using static Avalonia.Controls.Platform.IInsetsManager; + +namespace Avalonia.Android.Platform +{ + internal class AndroidInsetsManager : Java.Lang.Object, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener + { + private readonly AvaloniaMainActivity _activity; + private readonly TopLevelImpl _topLevel; + private readonly InsetsAnimationCallback _callback; + private bool _displayEdgeToEdge; + private bool _usesLegacyLayouts; + private bool? _systemUiVisibility; + private SystemBarTheme? _statusBarTheme; + private bool? _isDefaultSystemBarLightTheme; + + public event EventHandler SafeAreaChanged; + + public bool DisplayEdgeToEdge + { + get => _displayEdgeToEdge; + set + { + _displayEdgeToEdge = value; + + if(Build.VERSION.SdkInt >= BuildVersionCodes.P) + { + _activity.Window.Attributes.LayoutInDisplayCutoutMode = value ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default; + } + + WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value); + } + } + + public AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel) + { + _activity = activity; + _topLevel = topLevel; + _callback = new InsetsAnimationCallback(WindowInsetsAnimationCompat.Callback.DispatchModeStop); + + _callback.InsetsManager = this; + + ViewCompat.SetOnApplyWindowInsetsListener(_activity.Window.DecorView, this); + + ViewCompat.SetWindowInsetsAnimationCallback(_activity.Window.DecorView, _callback); + + if(Build.VERSION.SdkInt < BuildVersionCodes.R) + { + _usesLegacyLayouts = true; + _activity.Window.DecorView.ViewTreeObserver.AddOnGlobalLayoutListener(this); + } + + DisplayEdgeToEdge = false; + } + + public Thickness SafeAreaPadding + { + get + { + var insets = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); + + if (insets != null) + { + var renderScaling = _topLevel.RenderScaling; + + var inset = insets.GetInsets( + (DisplayEdgeToEdge ? + WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | + WindowInsetsCompat.Type.DisplayCutout() : + 0) | WindowInsetsCompat.Type.Ime()); + var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); + var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); + + return new Thickness(inset.Left / renderScaling, + inset.Top / renderScaling, + inset.Right / renderScaling, + (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ? + imeInset.Bottom - navBarInset.Bottom : + inset.Bottom) / renderScaling); + } + + return default; + } + } + + public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets) + { + NotifySafeAreaChanged(SafeAreaPadding); + return insets; + } + + private void NotifySafeAreaChanged(Thickness safeAreaPadding) + { + SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(safeAreaPadding)); + } + + public void OnGlobalLayout() + { + NotifySafeAreaChanged(SafeAreaPadding); + } + + public SystemBarTheme? SystemBarTheme + { + get + { + try + { + var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View); + + return compat.AppearanceLightStatusBars ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark; + } + catch (Exception _) + { + return Controls.Platform.SystemBarTheme.Light; + } + } + set + { + _statusBarTheme = value; + + var isDefault = _statusBarTheme == null; + + if (!_topLevel.View.IsShown) + { + return; + } + + var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View); + + if (_isDefaultSystemBarLightTheme == null) + { + _isDefaultSystemBarLightTheme = compat.AppearanceLightStatusBars; + } + + if (value == null && _isDefaultSystemBarLightTheme != null) + { + value = (bool)_isDefaultSystemBarLightTheme ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark; + } + + compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light; + compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light; + + AppCompatDelegate.DefaultNightMode = isDefault ? AppCompatDelegate.ModeNightFollowSystem : compat.AppearanceLightStatusBars ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes; + } + } + + public bool? IsSystemBarVisible + { + get + { + if(_activity.Window == null) + { + return true; + } + var compat = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); + + return compat?.IsVisible(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); + } + set + { + _systemUiVisibility = value; + + if (!_topLevel.View.IsShown) + { + return; + } + + var compat = WindowCompat.GetInsetsController(_activity.Window, _topLevel.View); + + if (value == null || value.Value) + { + compat?.Show(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); + } + else + { + compat?.Hide(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); + + if (compat != null) + { + compat.SystemBarsBehavior = WindowInsetsControllerCompat.BehaviorShowTransientBarsBySwipe; + } + } + } + } + + internal void ApplyStatusBarState() + { + IsSystemBarVisible = _systemUiVisibility; + SystemBarTheme = _statusBarTheme; + } + + private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback + { + public InsetsAnimationCallback(int dispatchMode) : base(dispatchMode) + { + } + + public AndroidInsetsManager InsetsManager { get; set; } + + public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList runningAnimations) + { + foreach (var anim in runningAnimations) + { + if ((anim.TypeMask & WindowInsetsCompat.Type.Ime()) != 0) + { + var renderScaling = InsetsManager._topLevel.RenderScaling; + + var inset = insets.GetInsets((InsetsManager.DisplayEdgeToEdge ? WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime()); + var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); + var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); + + + var bottomPadding = (imeInset.Bottom > 0 && !InsetsManager.DisplayEdgeToEdge ? imeInset.Bottom - navBarInset.Bottom : inset.Bottom); + bottomPadding = (int)(bottomPadding * anim.InterpolatedFraction); + + var padding = new Thickness(inset.Left / renderScaling, + inset.Top / renderScaling, + inset.Right / renderScaling, + bottomPadding / renderScaling); + InsetsManager?.NotifySafeAreaChanged(padding); + break; + } + } + return insets; + } + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index e511ed9a8b..b8d80a50ff 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -3,9 +3,7 @@ using System.Collections.Generic; using Android.App; using Android.Content; using Android.Graphics; -using Android.OS; using Android.Runtime; -using Android.Text; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.Platform.Specific; @@ -24,11 +22,13 @@ using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Java.Lang; +using Java.Util; using Math = System.Math; using AndroidRect = Android.Graphics.Rect; using Window = Android.Views.Window; using Android.Graphics.Drawables; -using Java.Util; +using Android.OS; +using Android.Text; namespace Avalonia.Android.Platform.SkiaPlatform { @@ -43,6 +43,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; private readonly ISystemNavigationManagerImpl _systemNavigationManager; + private readonly AndroidInsetsManager _insetsManager; private ViewImpl _view; public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false) @@ -59,6 +60,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); + if (avaloniaView.Context is AvaloniaMainActivity mainActivity) + { + _insetsManager = new AndroidInsetsManager(mainActivity, this); + } + _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context); @@ -70,21 +76,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IInputRoot InputRoot { get; private set; } - public virtual Size ClientSize - { - get - { - AndroidRect rect = new AndroidRect(); - AndroidRect intersection = new AndroidRect(); - - _view.GetWindowVisibleDisplayFrame(intersection); - _view.GetGlobalVisibleRect(rect); - - intersection.Intersect(rect); - - return new PixelSize(intersection.Right - intersection.Left, intersection.Bottom - intersection.Top).ToSize(RenderScaling); - } - } + public virtual Size ClientSize => _view.Size.ToSize(RenderScaling); public Size? FrameSize => null; @@ -285,7 +277,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { - // TODO adjust status bar depending on full screen mode. + if(_insetsManager != null) + { + _insetsManager.SystemBarTheme = themeVariant switch + { + PlatformThemeVariant.Light => SystemBarTheme.Light, + PlatformThemeVariant.Dark => SystemBarTheme.Dark, + _ => null, + }; + } } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); @@ -403,6 +403,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform return _nativeControlHost; } + if (featureType == typeof(IInsetsManager)) + { + return _insetsManager; + } + return null; } } diff --git a/src/Avalonia.Base/AttachedProperty.cs b/src/Avalonia.Base/AttachedProperty.cs index 31b6cad8ab..4a09f2a80a 100644 --- a/src/Avalonia.Base/AttachedProperty.cs +++ b/src/Avalonia.Base/AttachedProperty.cs @@ -32,9 +32,14 @@ namespace Avalonia /// /// The owner type. /// The property. - public new AttachedProperty AddOwner() where TOwner : AvaloniaObject + public new AttachedProperty AddOwner(StyledPropertyMetadata? metadata = null) where TOwner : AvaloniaObject { AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this); + if (metadata != null) + { + OverrideMetadata(metadata); + } + return this; } } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs index a202d6b5bc..3b9b2d0de6 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -2,7 +2,7 @@ using System.Collections; using System.Collections.Generic; using Avalonia.Controls; using Avalonia.LogicalTree; -using Avalonia.Styling; +using Avalonia.Reactive; namespace Avalonia.Input.GestureRecognizers { @@ -11,13 +11,13 @@ namespace Avalonia.Input.GestureRecognizers private readonly IInputElement _inputElement; private List? _recognizers; private Dictionary? _pointerGrabs; - - + + public GestureRecognizerCollection(IInputElement inputElement) { _inputElement = inputElement; } - + public void Add(IGestureRecognizer recognizer) { if (_recognizers == null) @@ -31,14 +31,13 @@ namespace Avalonia.Input.GestureRecognizers recognizer.Initialize(_inputElement, this); // Hacks to make bindings work - + if (_inputElement is ILogical logicalParent && recognizer is ISetLogicalParent logical) { logical.SetParent(logicalParent); if (recognizer is StyledElement styleableRecognizer && _inputElement is StyledElement styleableParent) - styleableRecognizer.Bind(StyledElement.TemplatedParentProperty, - styleableParent.GetObservable(StyledElement.TemplatedParentProperty)); + styleableParent.GetObservable(StyledElement.TemplatedParentProperty).Subscribe(parent => styleableRecognizer.TemplatedParent = parent); } } @@ -58,7 +57,7 @@ namespace Avalonia.Input.GestureRecognizers return false; foreach (var r in _recognizers) { - if(e.Handled) + if (e.Handled) break; r.PointerPressed(e); } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs index 991694cc60..6784677520 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs @@ -13,22 +13,18 @@ namespace Avalonia.Input private Point _initialPosition; private int _gestureId; private IPointer? _tracking; - private PullDirection _pullDirection; private bool _pullInProgress; /// /// Defines the property. /// - public static readonly DirectProperty PullDirectionProperty = - AvaloniaProperty.RegisterDirect( - nameof(PullDirection), - o => o.PullDirection, - (o, v) => o.PullDirection = v); + public static readonly StyledProperty PullDirectionProperty = + AvaloniaProperty.Register(nameof(PullDirection)); public PullDirection PullDirection { - get => _pullDirection; - set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value); + get => GetValue(PullDirectionProperty); + set => SetValue(PullDirectionProperty, value); } public PullGestureRecognizer(PullDirection pullDirection) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 7c1ee13eed..1ad2f292ca 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -17,61 +17,45 @@ namespace Avalonia.Input.GestureRecognizers private IPointer? _tracking; private IInputElement? _target; private IGestureRecognizerActionsDispatcher? _actions; - private bool _canHorizontallyScroll; - private bool _canVerticallyScroll; private int _gestureId; - private int _scrollStartDistance = 30; private Point _pointerPressedPoint; private VelocityTracker? _velocityTracker; // Movement per second private Vector _inertia; private ulong? _lastMoveTimestamp; - private bool _isScrollInertiaEnabled; /// /// Defines the property. /// - public static readonly DirectProperty CanHorizontallyScrollProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanHorizontallyScroll), - o => o.CanHorizontallyScroll, - (o, v) => o.CanHorizontallyScroll = v); + public static readonly StyledProperty CanHorizontallyScrollProperty = + AvaloniaProperty.Register(nameof(CanHorizontallyScroll)); /// /// Defines the property. /// - public static readonly DirectProperty CanVerticallyScrollProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanVerticallyScroll), - o => o.CanVerticallyScroll, - (o, v) => o.CanVerticallyScroll = v); + public static readonly StyledProperty CanVerticallyScrollProperty = + AvaloniaProperty.Register(nameof(CanVerticallyScroll)); /// /// Defines the property. /// - public static readonly DirectProperty IsScrollInertiaEnabledProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsScrollInertiaEnabled), - o => o.IsScrollInertiaEnabled, - (o, v) => o.IsScrollInertiaEnabled = v); + public static readonly StyledProperty IsScrollInertiaEnabledProperty = + AvaloniaProperty.Register(nameof(IsScrollInertiaEnabled)); /// /// Defines the property. /// - public static readonly DirectProperty ScrollStartDistanceProperty = - AvaloniaProperty.RegisterDirect( - nameof(ScrollStartDistance), - o => o.ScrollStartDistance, - (o, v) => o.ScrollStartDistance = v); + public static readonly StyledProperty ScrollStartDistanceProperty = + AvaloniaProperty.Register(nameof(ScrollStartDistance), 30); /// /// Gets or sets a value indicating whether the content can be scrolled horizontally. /// public bool CanHorizontallyScroll { - get => _canHorizontallyScroll; - set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); + get => GetValue(CanHorizontallyScrollProperty); + set => SetValue(CanHorizontallyScrollProperty, value); } /// @@ -79,8 +63,8 @@ namespace Avalonia.Input.GestureRecognizers /// public bool CanVerticallyScroll { - get => _canVerticallyScroll; - set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); + get => GetValue(CanVerticallyScrollProperty); + set => SetValue(CanVerticallyScrollProperty, value); } /// @@ -88,8 +72,8 @@ namespace Avalonia.Input.GestureRecognizers /// public bool IsScrollInertiaEnabled { - get => _isScrollInertiaEnabled; - set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value); + get => GetValue(IsScrollInertiaEnabledProperty); + set => SetValue(IsScrollInertiaEnabledProperty, value); } /// @@ -97,8 +81,8 @@ namespace Avalonia.Input.GestureRecognizers /// public int ScrollStartDistance { - get => _scrollStartDistance; - set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value); + get => GetValue(ScrollStartDistanceProperty); + set => SetValue(ScrollStartDistanceProperty, value); } @@ -137,8 +121,8 @@ namespace Avalonia.Input.GestureRecognizers // Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance _trackedRootPoint = new Point( - _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? _scrollStartDistance : -_scrollStartDistance), - _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? _scrollStartDistance : -_scrollStartDistance)); + _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? ScrollStartDistance : -ScrollStartDistance), + _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? ScrollStartDistance : -ScrollStartDistance)); _actions!.Capture(e.Pointer, this); } diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 2dabb29e76..595a2f3474 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using Avalonia.Media.Fonts; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -13,9 +15,11 @@ namespace Avalonia.Media /// public sealed class FontManager { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - private readonly FontFamily _defaultFontFamily; + internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts"); + + public const string FontCollectionScheme = "fonts"; + + private readonly ConcurrentDictionary _fontCollections = new ConcurrentDictionary(); private readonly IReadOnlyList? _fontFallbacks; public FontManager(IFontManagerImpl platformImpl) @@ -33,9 +37,12 @@ namespace Avalonia.Media throw new InvalidOperationException("Default font family name can't be null or empty."); } - _defaultFontFamily = new FontFamily(DefaultFontFamilyName); + AddFontCollection(new SystemFontCollection(this)); } + /// + /// Get the current font manager instance. + /// public static FontManager Current { get @@ -57,11 +64,6 @@ namespace Avalonia.Media } } - /// - /// - /// - public IFontManagerImpl PlatformImpl { get; } - /// /// Gets the system's default font family's name. /// @@ -71,41 +73,109 @@ namespace Avalonia.Media } /// - /// Get all installed font family names. + /// Get all system fonts. /// - /// If true the font collection is updated. - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); + public IFontCollection SystemFonts => _fontCollections[SystemFontsKey]; + + internal IFontManagerImpl PlatformImpl { get; } /// - /// Returns a new , or an existing one if a matching exists. + /// Tries to get a glyph typeface for specified typeface. /// /// The typeface. + /// The created glyphTypeface /// - /// The . + /// True, if the could create the glyph typeface, False otherwise. /// - public IGlyphTypeface GetOrAddGlyphTypeface(Typeface typeface) + public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - while (true) + glyphTypeface = null; + + var fontFamily = typeface.FontFamily; + + if (fontFamily.Key is FontFamilyKey key) { - if (_glyphTypefaceCache.TryGetValue(typeface, out var glyphTypeface)) + var source = key.Source; + + if (!source.IsAbsoluteUri) { - return glyphTypeface; + if (key.BaseUri == null) + { + throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null."); + } + + source = new Uri(key.BaseUri, source); } - glyphTypeface = PlatformImpl.CreateGlyphTypeface(typeface); + if (!_fontCollections.TryGetValue(source, out var fontCollection)) + { + var embeddedFonts = new EmbeddedFontCollection(source, source); + + embeddedFonts.Initialize(PlatformImpl); - if (_glyphTypefaceCache.TryAdd(typeface, glyphTypeface)) + if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts)) + { + fontCollection = embeddedFonts; + } + } + + if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, + typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { - return glyphTypeface; + return true; } - if (typeface.FontFamily == _defaultFontFamily) + if (!fontFamily.FamilyNames.HasFallbacks) { - throw new InvalidOperationException($"Could not create glyph typeface for: {typeface.FontFamily.Name}."); + return false; } + } - typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight); + foreach (var familyName in fontFamily.FamilyNames) + { + if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) + { + return true; + } + } + + return SystemFonts.TryGetGlyphTypeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface); + } + + /// + /// Add a font collection to the manager. + /// + /// The font collection. + /// + /// If a font collection's key is already present the collection is replaced. + public void AddFontCollection(IFontCollection fontCollection) + { + var key = fontCollection.Key; + + if (!fontCollection.Key.IsFontCollection()) + { + throw new ArgumentException("Font collection Key should follow the fonts: scheme.", nameof(fontCollection)); + } + + _fontCollections.AddOrUpdate(key, fontCollection, (_, oldCollection) => + { + oldCollection.Dispose(); + + return fontCollection; + }); + + fontCollection.Initialize(PlatformImpl); + } + + /// + /// Removes the font collection that corresponds to specified key. + /// + /// The font collection's key. + public void RemoveFontCollection(Uri key) + { + if (_fontCollections.TryRemove(key, out var fontCollection)) + { + fontCollection.Dispose(); } } @@ -123,18 +193,16 @@ namespace Avalonia.Media /// True, if the could match the character to specified parameters, False otherwise. /// public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, - FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface) + FontStretch fontStretch, FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface) { - if(_fontFallbacks != null) + if (_fontFallbacks != null) { foreach (var fallback in _fontFallbacks) { typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); - var glyphTypeface = GetOrAddGlyphTypeface(typeface); - - if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){ + if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { return true; } } diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs new file mode 100644 index 0000000000..f2fb490592 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + public class EmbeddedFontCollection : IFontCollection + { + private readonly ConcurrentDictionary> _glyphTypefaceCache = new(); + + private readonly List _fontFamilies = new List(1); + + private readonly Uri _key; + + private readonly Uri _source; + + public EmbeddedFontCollection(Uri key, Uri source) + { + _key = key; + + _source = source; + } + + public Uri Key => _key; + + public FontFamily this[int index] => _fontFamilies[index]; + + public int Count => _fontFamilies.Count; + + public void Initialize(IFontManagerImpl fontManager) + { + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + var fontAssets = FontFamilyLoader.LoadFontAssets(_source); + + foreach (var fontAsset in fontAssets) + { + var stream = assetLoader.Open(fontAsset); + + if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface)) + { + if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) + { + glyphTypefaces = new ConcurrentDictionary(); + + if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces)) + { + _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName)); + } + } + + var key = new FontCollectionKey( + glyphTypeface.Style, + glyphTypeface.Weight, + glyphTypeface.Stretch); + + glyphTypefaces.TryAdd(key, glyphTypeface); + } + } + } + + public void Dispose() + { + foreach (var fontFamily in _fontFamilies) + { + if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces)) + { + foreach (var glyphTypeface in glyphTypefaces.Values) + { + glyphTypeface.Dispose(); + } + } + } + + GC.SuppressFinalize(this); + } + + public IEnumerator GetEnumerator() => _fontFamilies.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + var key = new FontCollectionKey(style, weight, stretch); + + if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + { + if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + } + + //Try to find a partially matching font + for (var i = 0; i < Count; i++) + { + var fontFamily = _fontFamilies[i]; + + if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture))) + { + if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) && + TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + } + } + + glyphTypeface = null; + + return false; + } + + private static bool TryGetNearestMatch( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) + { + return true; + } + + if (key.Style != FontStyle.Normal) + { + key = key with { Style = FontStyle.Normal }; + } + + if (key.Stretch != FontStretch.Normal) + { + if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + if (key.Weight != FontWeight.Normal) + { + if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface)) + { + return true; + } + } + + key = key with { Stretch = FontStretch.Normal }; + } + + if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + //Take the first glyph typeface we can find. + foreach (var typeface in glyphTypefaces.Values) + { + glyphTypeface = typeface; + + return true; + } + + return false; + } + + private static bool TryFindStretchFallback( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + glyphTypeface = null; + + var stretch = (int)key.Stretch; + + if (stretch < 5) + { + for (var i = 0; stretch + i < 9; i++) + { + if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface)) + { + return true; + } + } + } + else + { + for (var i = 0; stretch - i > 1; i++) + { + if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface)) + { + return true; + } + } + } + + return false; + } + + private static bool TryFindWeightFallback( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? typeface) + { + typeface = null; + var weight = (int)key.Weight; + + //If the target weight given is between 400 and 500 inclusive + if (weight >= 400 && weight <= 500) + { + //Look for available weights between the target and 500, in ascending order. + for (var i = 0; weight + i <= 500; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights greater than 500, in ascending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) + { + return true; + } + } + } + + //If a weight less than 400 is given, look for available weights less than the target, in descending order. + if (weight < 400) + { + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) + { + return true; + } + } + } + + //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order. + if (weight > 500) + { + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) + { + return true; + } + } + } + + return false; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs new file mode 100644 index 0000000000..0d0dc3016e --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs @@ -0,0 +1,4 @@ +namespace Avalonia.Media.Fonts +{ + public readonly record struct FontCollectionKey(FontStyle Style, FontWeight Weight, FontStretch Stretch); +} diff --git a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs index 365fb6e412..37992c895e 100644 --- a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs +++ b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs @@ -11,22 +11,30 @@ namespace Avalonia.Media.Fonts /// /// Loads all font assets that belong to the specified /// - /// + /// /// - public static IEnumerable LoadFontAssets(FontFamilyKey fontFamilyKey) => - IsFontTtfOrOtf(fontFamilyKey.Source) ? - GetFontAssetsByExpression(fontFamilyKey) : - GetFontAssetsBySource(fontFamilyKey); + public static IEnumerable LoadFontAssets(Uri source) + { + if (source.IsAvares() || source.IsAbsoluteResm()) + { + return IsFontTtfOrOtf(source) ? + GetFontAssetsByExpression(source) : + GetFontAssetsBySource(source); + } + + return Enumerable.Empty(); + } + /// /// Searches for font assets at a given location and returns a quantity of found assets /// - /// + /// /// - private static IEnumerable GetFontAssetsBySource(FontFamilyKey fontFamilyKey) + private static IEnumerable GetFontAssetsBySource(Uri source) { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); + var availableAssets = assetLoader.GetAssets(source, null); return availableAssets.Where(x => IsFontTtfOrOtf(x)); } @@ -34,60 +42,50 @@ namespace Avalonia.Media.Fonts /// Searches for font assets at a given location and only accepts assets that fit to a given filename expression. /// File names can target multiple files with * wildcard. For example "FontFile*.ttf" /// - /// + /// /// - private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey) + private static IEnumerable GetFontAssetsByExpression(Uri source) { - var (fileNameWithoutExtension, extension) = GetFileName(fontFamilyKey, out var location); - var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension); + var (fileNameWithoutExtension, extension) = GetFileName(source, out var location); + var filePattern = CreateFilePattern(source, location, fileNameWithoutExtension); var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri); + var availableResources = assetLoader.GetAssets(location, null); return availableResources.Where(x => IsContainsFile(x, filePattern, extension)); } private static (string fileNameWithoutExtension, string extension) GetFileName( - FontFamilyKey fontFamilyKey, out Uri location) + Uri source, out Uri location) { - if (fontFamilyKey.Source.IsAbsoluteResm()) + if (source.IsAbsoluteResm()) { - var fileName = GetFileNameAndExtension(fontFamilyKey.Source.GetUnescapeAbsolutePath(), '.'); + var fileName = GetFileNameAndExtension(source.GetUnescapeAbsolutePath(), '.'); - var uriLocation = fontFamilyKey.Source.GetUnescapeAbsoluteUri() + var uriLocation = source.GetUnescapeAbsoluteUri() .Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty); location = new Uri(uriLocation, UriKind.RelativeOrAbsolute); return fileName; } - var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString); + var filename = GetFileNameAndExtension(source.OriginalString); var fullFilename = filename.fileNameWithoutExtension + filename.extension; - if (fontFamilyKey.BaseUri != null) - { - var relativePath = fontFamilyKey.Source.OriginalString - .Replace(fullFilename, string.Empty); - - location = new Uri(fontFamilyKey.BaseUri, relativePath); - } - else - { - var uriString = fontFamilyKey.Source - .GetUnescapeAbsoluteUri() - .Replace(fullFilename, string.Empty); - location = new Uri(uriString); - } + var uriString = source + .GetUnescapeAbsoluteUri() + .Replace(fullFilename, string.Empty); + location = new Uri(uriString); return filename; } private static string CreateFilePattern( - FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension) + Uri source, Uri location, string fileNameWithoutExtension) { var path = location.GetUnescapeAbsolutePath(); var file = GetSubString(fileNameWithoutExtension, '*'); - return fontFamilyKey.Source.IsAbsoluteResm() + return source.IsAbsoluteResm() ? path + "." + file : path + file; } diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs new file mode 100644 index 0000000000..814230bcf3 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + public interface IFontCollection : IReadOnlyList, IDisposable + { + /// + /// Get the font collection's key. + /// + Uri Key { get; } + + /// + /// Initializes the font collection. + /// + /// The font manager the collection is registered with. + void Initialize(IFontManagerImpl fontManager); + + /// + /// Try to get a glyph typeface for given parameters. + /// + /// The family name. + /// The font style. + /// The font weight. + /// The font stretch. + /// The glyph typeface. + /// Returns true if a glyph typface can be found; otherwise, false + bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + } +} diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs new file mode 100644 index 0000000000..fd332c6ebe --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + internal class SystemFontCollection : IFontCollection + { + private readonly ConcurrentDictionary> _glyphTypefaceCache = new(); + + private readonly FontManager _fontManager; + private readonly string[] _familyNames; + + public SystemFontCollection(FontManager fontManager) + { + _fontManager = fontManager; + _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames(); + } + + public Uri Key => FontManager.SystemFontsKey; + + public FontFamily this[int index] + { + get + { + var familyName = _familyNames[index]; + + return new FontFamily(familyName); + } + } + + public int Count => _familyNames.Length; + + public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (familyName == FontFamily.DefaultFontFamilyName) + { + familyName = _fontManager.DefaultFontFamilyName; + } + + var key = new FontCollectionKey(style, weight, stretch); + + if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) + { + return true; + } + else + { + if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) && + glyphTypefaces.TryAdd(key, glyphTypeface)) + { + return true; + } + } + } + + if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) + { + glyphTypefaces = new ConcurrentDictionary(); + + if (glyphTypefaces.TryAdd(key, glyphTypeface) && _glyphTypefaceCache.TryAdd(familyName, glyphTypefaces)) + { + return true; + } + } + + return false; + } + + public void Initialize(IFontManagerImpl fontManager) + { + //We initialize the system font collection during construction. + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + foreach (var familyName in _familyNames) + { + yield return new FontFamily(familyName); + } + } + + void IDisposable.Dispose() + { + foreach (var glyphTypefaces in _glyphTypefaceCache.Values) + { + foreach (var pair in glyphTypefaces) + { + pair.Value.Dispose(); + } + } + + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 2966ceee8d..d795cca894 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -151,9 +151,9 @@ namespace Avalonia.Media } /// - /// Gets or sets the conservative bounding box of the . + /// Gets the conservative bounding box of the . /// - public Size Size => PlatformImpl.Item.Size; + public Rect Bounds => PlatformImpl.Item.Bounds; /// /// @@ -252,7 +252,7 @@ namespace Avalonia.Media if (characterIndex > Metrics.LastCluster) { - return Size.Width; + return Bounds.Width; } var glyphIndex = FindGlyphIndex(characterIndex); @@ -287,7 +287,7 @@ namespace Avalonia.Media if (characterIndex <= Metrics.FirstCluster) { - return Size.Width; + return Bounds.Width; } for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++) @@ -295,7 +295,7 @@ namespace Avalonia.Media distance += _glyphInfos[i].GlyphAdvance; } - return Size.Width - distance; + return Bounds.Width - distance; } } @@ -321,7 +321,7 @@ namespace Avalonia.Media } //After - if (distance >= Size.Width) + if (distance >= Bounds.Width) { isInside = false; @@ -354,7 +354,7 @@ namespace Avalonia.Media } else { - currentX = Size.Width; + currentX = Bounds.Width; for (var index = _glyphInfos.Count - 1; index >= 0; index--) { diff --git a/src/Avalonia.Base/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs index 06d92fd81c..961203e30e 100644 --- a/src/Avalonia.Base/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Base/Media/GlyphRunDrawing.cs @@ -32,7 +32,7 @@ public override Rect GetBounds() { - return GlyphRun != null ? new Rect(GlyphRun.Size) : default; + return GlyphRun != null ? GlyphRun.Bounds : default; } } } diff --git a/src/Avalonia.Base/Media/IGlyphTypeface.cs b/src/Avalonia.Base/Media/IGlyphTypeface.cs index 9e1e52cb73..09740aac81 100644 --- a/src/Avalonia.Base/Media/IGlyphTypeface.cs +++ b/src/Avalonia.Base/Media/IGlyphTypeface.cs @@ -6,6 +6,26 @@ namespace Avalonia.Media [Unstable] public interface IGlyphTypeface : IDisposable { + /// + /// Gets the family name for the object. + /// + string FamilyName { get; } + + /// + /// Gets the designed weight of the font represented by the object. + /// + FontWeight Weight { get; } + + /// + /// Gets the style for the object. + /// + FontStyle Style { get; } + + /// + /// Gets the value for the object. + /// + FontStretch Stretch { get; } + /// /// Gets the number of glyphs held by this glyph typeface. /// diff --git a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs index 525a543b70..8e57f9a902 100644 --- a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs @@ -48,8 +48,6 @@ namespace Avalonia.Media.Imaging public CroppedBitmap() { - Source = null; - SourceRect = default; } public CroppedBitmap(IImage source, PixelRect sourceRect) diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index b74b7df9c5..e89a7d8826 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -223,7 +223,7 @@ namespace Avalonia.Media if (intersections.Count > 0) { var last = baselineOrigin.X; - var finalPos = last + glyphRun.Size.Width; + var finalPos = last + glyphRun.Bounds.Width; var end = last; var points = new List(); diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index c5dd30b620..2f28c3f954 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -38,14 +38,14 @@ namespace Avalonia.Media.TextFormatting public override double Baseline => -TextMetrics.Ascent; - public override Size Size => GlyphRun.Size; + public override Size Size => GlyphRun.Bounds.Size; public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun(); /// public override void Draw(DrawingContext drawingContext, Point origin) { - using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) + using (drawingContext.PushTransform(Matrix.CreateTranslation(origin))) { if (GlyphRun.GlyphInfos.Count == 0) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index b4734d702b..253c7075fa 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -122,13 +122,14 @@ namespace Avalonia.Media.TextFormatting if (matchFound) { // Fallback found - var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); - - if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) - { - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), - biDiLevel); - } + if(fontManager.TryGetGlyphTypeface(fallbackTypeface, out var fallbackGlyphTypeface)) + { + if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) + { + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), + biDiLevel); + } + } } // no fallback found diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 6422f23dcd..8b6d576c6e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -19,7 +19,7 @@ namespace Avalonia.Media.TextFormatting var collapsedLength = 0; var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight); - if (properties.Width < shapedSymbol.GlyphRun.Size.Width) + if (properties.Width < shapedSymbol.GlyphRun.Bounds.Width) { //Not enough space to fit in the symbol return Array.Empty(); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index a21a5d45e9..41d451b9e3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -60,7 +60,7 @@ namespace Avalonia.Media.TextFormatting var currentWidth = 0.0; var shapedSymbol = TextFormatterImpl.CreateSymbol(Symbol, FlowDirection.LeftToRight); - if (Width < shapedSymbol.GlyphRun.Size.Width) + if (Width < shapedSymbol.GlyphRun.Bounds.Width) { return Array.Empty(); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index b3321d4d9f..f426a20b2c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -423,7 +423,7 @@ namespace Avalonia.Media.TextFormatting { if (currentGlyphRun != null) { - currentDistance -= currentGlyphRun.Size.Width; + currentDistance -= currentGlyphRun.Bounds.Width; } return currentDistance + distance; @@ -477,7 +477,7 @@ namespace Avalonia.Media.TextFormatting { if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft) { - distance = currentGlyphRun.Size.Width; + distance = currentGlyphRun.Bounds.Width; } return true; @@ -1483,7 +1483,7 @@ namespace Avalonia.Media.TextFormatting trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength; - var whitespaceWidth = glyphRun.Size.Width - glyphRunMetrics.Width; + var whitespaceWidth = glyphRun.Bounds.Width - glyphRunMetrics.Width; width -= whitespaceWidth; } diff --git a/src/Avalonia.Base/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs index 1e744c30c8..e2729c9158 100644 --- a/src/Avalonia.Base/Media/Typeface.cs +++ b/src/Avalonia.Base/Media/Typeface.cs @@ -80,7 +80,18 @@ namespace Avalonia.Media /// /// The glyph typeface. /// - public IGlyphTypeface GlyphTypeface => FontManager.Current.GetOrAddGlyphTypeface(this); + public IGlyphTypeface GlyphTypeface + { + get + { + if(FontManager.Current.TryGetGlyphTypeface(this, out var glyphTypeface)) + { + return glyphTypeface; + } + + throw new InvalidOperationException("Could not create glyphTypeface."); + } + } public static bool operator !=(Typeface a, Typeface b) { diff --git a/src/Avalonia.Base/Platform/AssetLoader.cs b/src/Avalonia.Base/Platform/AssetLoader.cs index 659cfb75df..7df446e854 100644 --- a/src/Avalonia.Base/Platform/AssetLoader.cs +++ b/src/Avalonia.Base/Platform/AssetLoader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -62,7 +63,7 @@ namespace Avalonia.Platform /// True if the asset could be found; otherwise false. public bool Exists(Uri uri, Uri? baseUri = null) { - return GetAsset(uri, baseUri) != null; + return TryGetAsset(uri, baseUri, out _); } /// @@ -94,21 +95,27 @@ namespace Avalonia.Platform /// public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null) { - var asset = GetAsset(uri, baseUri); - - if (asset == null) + if (TryGetAsset(uri, baseUri, out var assetDescriptor)) { - throw new FileNotFoundException($"The resource {uri} could not be found."); + return (assetDescriptor.GetStream(), assetDescriptor.Assembly); } - return (asset.GetStream(), asset.Assembly); + throw new FileNotFoundException($"The resource {uri} could not be found."); } public Assembly? GetAssembly(Uri uri, Uri? baseUri) { if (!uri.IsAbsoluteUri && baseUri != null) + { uri = new Uri(baseUri, uri); - return GetAssembly(uri)?.Assembly; + } + + if (TryGetAssembly(uri, out var assemblyDescriptor)) + { + return assemblyDescriptor.Assembly; + } + + return null; } /// @@ -121,99 +128,145 @@ namespace Avalonia.Platform { if (uri.IsAbsoluteResm()) { - var assembly = GetAssembly(uri); + if (!TryGetAssembly(uri, out var assembly)) + { + assembly = _defaultResmAssembly; + } return assembly?.Resources? - .Where(x => x.Key.IndexOf(uri.GetUnescapeAbsolutePath(), StringComparison.Ordinal) >= 0) - .Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? - Enumerable.Empty(); + .Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath())) + .Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? + Enumerable.Empty(); } uri = uri.EnsureAbsolute(baseUri); + if (uri.IsAvares()) { - var (asm, path) = GetResAsmAndPath(uri); - if (asm == null) + if (!TryGetResAsmAndPath(uri, out var assembly, out var path)) { - throw new ArgumentException( - "No default assembly, entry assembly or explicit assembly specified; " + - "don't know where to look up for the resource, try specifying assembly explicitly."); + return Enumerable.Empty(); } - if (asm.AvaloniaResources == null) + if (assembly?.AvaloniaResources == null) + { return Enumerable.Empty(); + } - if (path[path.Length - 1] != '/') + if (path.Length > 0 && path[path.Length - 1] != '/') + { path += '/'; + } - return asm.AvaloniaResources + return assembly.AvaloniaResources .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) - .Select(x => new Uri($"avares://{asm.Name}{x.Key}")); + .Select(x => new Uri($"avares://{assembly.Name}{x.Key}")); } return Enumerable.Empty(); } - - private IAssetDescriptor? GetAsset(Uri uri, Uri? baseUri) - { + + private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor) + { + assetDescriptor = null; + if (uri.IsAbsoluteResm()) { - var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly; - - if (asm == null) + if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly)) { - throw new ArgumentException( - "No default assembly, entry assembly or explicit assembly specified; " + - "don't know where to look up for the resource, try specifying assembly explicitly."); + assembly = _defaultResmAssembly; } - var resourceKey = uri.AbsolutePath; - IAssetDescriptor? rv = null; - asm.Resources?.TryGetValue(resourceKey, out rv); - return rv; + if (assembly?.Resources != null) + { + var resourceKey = uri.AbsolutePath; + + if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor)) + { + return true; + } + } } uri = uri.EnsureAbsolute(baseUri); if (uri.IsAvares()) { - var (asm, path) = GetResAsmAndPath(uri); - if (asm.AvaloniaResources == null) - return null; - asm.AvaloniaResources.TryGetValue(path, out var desc); - return desc; + if (TryGetResAsmAndPath(uri, out var assembly, out var path)) + { + if (assembly.AvaloniaResources == null) + { + return false; + } + + if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor)) + { + return true; + } + } } - throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri)); + return false; } - private static (IAssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) + private static bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path) { - var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority); - return (asm, uri.GetUnescapeAbsolutePath()); + path = uri.GetUnescapeAbsolutePath(); + + if (TryLoadAssembly(uri.Authority, out assembly)) + { + return true; + } + + return false; } - - private static IAssemblyDescriptor? GetAssembly(Uri? uri) + + private static bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) { + assembly = null; + if (uri != null) { if (!uri.IsAbsoluteUri) - return null; - if (uri.IsAvares()) - return GetResAsmAndPath(uri).asm; + { + return false; + } + + if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _)) + { + return true; + } if (uri.IsResm()) { var assemblyName = uri.GetAssemblyNameFromQuery(); - if (assemblyName.Length > 0) - return s_assemblyDescriptorResolver.GetAssembly(assemblyName); + + if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly)) + { + return true; + } } } - return null; + return false; + } + + private static bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) + { + assembly = null; + + try + { + assembly = s_assemblyDescriptorResolver.GetAssembly(assemblyName); + + return true; + } + catch (Exception) { } + + return false; } #endif - + public static void RegisterResUriParsers() { if (!UriParser.IsKnownScheme("avares")) diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index cd6e64abaf..116f7cd6e2 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using Avalonia.Media; using Avalonia.Metadata; @@ -17,7 +18,7 @@ namespace Avalonia.Platform /// Get all installed fonts in the system. /// If true the font collection is updated. /// - IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + string[] GetInstalledFontFamilyNames(bool checkForUpdates = false); /// /// Tries to match a specified character to a typeface that supports specified font properties. @@ -37,12 +38,27 @@ namespace Avalonia.Platform FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface); /// - /// Creates a glyph typeface. + /// Tries to get a glyph typeface for specified parameters. /// - /// The typeface. - /// 0 - /// The created glyph typeface. Can be Null if it was not possible to create a glyph typeface. + /// The family name. + /// The font style. + /// The font weiht. + /// The font stretch. + /// The created glyphTypeface + /// + /// True, if the could create the glyph typeface, False otherwise. + /// + bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); + + /// + /// Tries to create a glyph typeface from specified stream. + /// + /// A stream that holds the font's data. + /// The created glyphTypeface + /// + /// True, if the could create the glyph typeface, False otherwise. /// - IGlyphTypeface CreateGlyphTypeface(Typeface typeface); + bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); } } diff --git a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs index 46b065b04e..fccea27c43 100644 --- a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs +++ b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs @@ -10,11 +10,10 @@ namespace Avalonia.Platform [Unstable] public interface IGlyphRunImpl : IDisposable { - /// /// Gets the conservative bounding box of the glyph run./>. /// - Size Size { get; } + Rect Bounds { get; } /// /// Gets the baseline origin of the glyph run./>. diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 0887f11ec9..53cd3ff307 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -924,7 +924,7 @@ namespace Avalonia.PropertyStore { _effectiveValues.GetKeyValue(i, out var key, out var e); - if (e.Priority == BindingPriority.Unset) + if (e.Priority == BindingPriority.Unset && !e.IsOverridenCurrentValue) { RemoveEffectiveValue(key, i); e.DisposeAndRaiseUnset(this, key); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs index b01fb46aa3..04e40e8744 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering.Composition.Server for (var c = FirstChar; c <= LastChar; c++) { - var height = _runs[c - FirstChar].Size.Height; + var height = _runs[c - FirstChar].Bounds.Height; if (height > maxHeight) { maxHeight = height; @@ -51,8 +51,8 @@ namespace Avalonia.Rendering.Composition.Server { var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; var run = _runs[effectiveChar - FirstChar]; - width += run.Size.Width; - height = Math.Max(height, run.Size.Height); + width += run.Bounds.Width; + height = Math.Max(height, run.Bounds.Height); } return new Size(width, height); @@ -69,7 +69,7 @@ namespace Avalonia.Rendering.Composition.Server var run = _runs[effectiveChar - FirstChar]; context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0); context.DrawGlyphRun(foreground, run.PlatformImpl); - offset += run.Size.Width; + offset += run.Bounds.Width; } context.Transform = originalTransform; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 4d8759f545..381c63f430 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -16,12 +16,11 @@ namespace Avalonia.Rendering.SceneGraph /// The transform. /// The foreground brush. /// The glyph run to draw. - /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, IImmutableBrush foreground, IRef glyphRun) - : base(new Rect(glyphRun.Item.Size), transform, foreground) + : base(glyphRun.Item.Bounds, transform, foreground) { GlyphRun = glyphRun.Clone(); } @@ -54,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) => new Rect(GlyphRun.Item.Size).ContainsExclusive(p); + public override bool HitTest(Point p) => GlyphRun.Item.Bounds.ContainsExclusive(p); public override void Dispose() { diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index cbdf3c3c1e..b51093b40c 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -67,8 +67,7 @@ namespace Avalonia public static readonly DirectProperty TemplatedParentProperty = AvaloniaProperty.RegisterDirect( nameof(TemplatedParent), - o => o.TemplatedParent, - (o ,v) => o.TemplatedParent = v); + o => o.TemplatedParent); /// /// Defines the property. diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 8695918c18..5052840013 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -56,9 +56,14 @@ namespace Avalonia /// /// The type of the additional owner. /// The property. - public StyledProperty AddOwner() where TOwner : AvaloniaObject + public StyledProperty AddOwner(StyledPropertyMetadata? metadata = null) where TOwner : AvaloniaObject { AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this); + if (metadata != null) + { + OverrideMetadata(metadata); + } + return this; } diff --git a/src/Avalonia.Base/Utilities/UriExtensions.cs b/src/Avalonia.Base/Utilities/UriExtensions.cs index c706f72a63..1f9c694eab 100644 --- a/src/Avalonia.Base/Utilities/UriExtensions.cs +++ b/src/Avalonia.Base/Utilities/UriExtensions.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media; namespace Avalonia.Utilities; @@ -10,7 +11,9 @@ internal static class UriExtensions public static bool IsResm(this Uri uri) => uri.Scheme == "resm"; public static bool IsAvares(this Uri uri) => uri.Scheme == "avares"; - + + public static bool IsFontCollection(this Uri uri) => uri.Scheme == FontManager.FontCollectionScheme; + public static Uri EnsureAbsolute(this Uri uri, Uri? baseUri) { if (uri.IsAbsoluteUri) diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index cf79fcd1a8..64bf92b7cd 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -4,6 +4,8 @@ using System.Reflection; using System.Linq; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; +using Avalonia.Media.Fonts; +using Avalonia.Media; namespace Avalonia { @@ -205,6 +207,19 @@ namespace Avalonia return Self; } + /// + /// Registers an action that is executed with the current font manager. + /// + /// The action. + /// An instance. + public AppBuilder ConfigureFonts(Action action) + { + return AfterSetup(appBuilder => + { + action?.Invoke(FontManager.Current); + }); + } + /// /// Sets up the platform-specific services for the . /// diff --git a/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs new file mode 100644 index 0000000000..86cd0d5b14 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs @@ -0,0 +1,34 @@ +using Avalonia.Automation.Peers; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls.Automation.Peers +{ + public class LabelAutomationPeer : ControlAutomationPeer + { + public LabelAutomationPeer(Label owner) : base(owner) + { + } + + override protected string GetClassNameCore() + { + return "Text"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Text; + } + + override protected string? GetNameCore() + { + var content = ((Label)Owner).Content as string; + + if (string.IsNullOrEmpty(content)) + { + return base.GetNameCore(); + } + + return AccessText.RemoveAccessKeyMarker(content) ?? string.Empty; + } + } +} diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 1ec6f8dabc..f48d7a7cc1 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,11 +1,9 @@ using System; -using System.Diagnostics; using System.Linq; using System.Windows.Input; using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -48,9 +46,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = - AvaloniaProperty.RegisterDirect(nameof(Command), - button => button.Command, (button, command) => button.Command = command, enableDataValidation: true); + public static readonly StyledProperty CommandProperty = + AvaloniaProperty.Register(nameof(Command), enableDataValidation: true); /// /// Defines the property. @@ -85,8 +82,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty IsPressedProperty = - AvaloniaProperty.Register(nameof(IsPressed)); + public static readonly DirectProperty IsPressedProperty = + AvaloniaProperty.RegisterDirect(nameof(IsPressed), b => b.IsPressed); /// /// Defines the property @@ -94,10 +91,10 @@ namespace Avalonia.Controls public static readonly StyledProperty FlyoutProperty = AvaloniaProperty.Register(nameof(Flyout)); - private ICommand? _command; private bool _commandCanExecute = true; private KeyGesture? _hotkey; private bool _isFlyoutOpen = false; + private bool _isPressed = false; /// /// Initializes static members of the class. @@ -138,8 +135,8 @@ namespace Avalonia.Controls /// public ICommand? Command { - get => _command; - set => SetAndRaise(CommandProperty, ref _command, value); + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// @@ -185,8 +182,8 @@ namespace Avalonia.Controls /// public bool IsPressed { - get => GetValue(IsPressedProperty); - private set => SetValue(IsPressedProperty, value); + get => _isPressed; + private set => SetAndRaise(IsPressedProperty, ref _isPressed, value); } /// @@ -248,7 +245,7 @@ namespace Avalonia.Controls { if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control { - HotKey = _hotkey; + SetCurrentValue(HotKeyProperty, _hotkey); } base.OnAttachedToLogicalTree(e); @@ -267,7 +264,7 @@ namespace Avalonia.Controls if (HotKey != null) { _hotkey = HotKey; - HotKey = null; + SetCurrentValue(HotKeyProperty, null); } base.OnDetachedFromLogicalTree(e); @@ -291,17 +288,17 @@ namespace Avalonia.Controls break; case Key.Space: - { - if (ClickMode == ClickMode.Press) { - OnClick(); + if (ClickMode == ClickMode.Press) + { + OnClick(); + } + + IsPressed = true; + e.Handled = true; + break; } - IsPressed = true; - e.Handled = true; - break; - } - case Key.Escape when Flyout != null: // If Flyout doesn't have focusable content, close the flyout here CloseFlyout(); @@ -592,7 +589,7 @@ namespace Avalonia.Controls { flyout.Opened -= Flyout_Opened; flyout.Closed -= Flyout_Closed; - } + } } /// @@ -671,7 +668,7 @@ namespace Avalonia.Controls void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); void IClickableControl.RaiseClick() => OnClick(); - + /// /// Event handler for when the button's flyout is opened. /// diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 3300292857..10aadfa759 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -232,14 +232,9 @@ namespace Avalonia.Controls internal const int RowsPerYear = 3; internal const int ColumnsPerYear = 4; - private DateTime? _selectedDate; private DateTime _selectedMonth; private DateTime _selectedYear; - private DateTime _displayDate = DateTime.Today; - private DateTime? _displayDateStart; - private DateTime? _displayDateEnd; - private bool _isShiftPressed; private bool _displayDateIsChanging; @@ -396,13 +391,13 @@ namespace Avalonia.Controls } case CalendarMode.Year: { - DisplayDate = SelectedMonth; + SetCurrentValue(DisplayDateProperty, SelectedMonth); SelectedYear = SelectedMonth; break; } case CalendarMode.Decade: { - DisplayDate = SelectedYear; + SetCurrentValue(DisplayDateProperty, SelectedYear); SelectedMonth = SelectedYear; break; } @@ -472,7 +467,7 @@ namespace Avalonia.Controls if (IsValidSelectionMode(e.NewValue!)) { _displayDateIsChanging = true; - SelectedDate = null; + SetCurrentValue(SelectedDateProperty, null); _displayDateIsChanging = false; SelectedDates.Clear(); } @@ -497,11 +492,8 @@ namespace Avalonia.Controls || mode == CalendarSelectionMode.None; } - public static readonly DirectProperty SelectedDateProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectedDate), - o => o.SelectedDate, - (o, v) => o.SelectedDate = v, + public static readonly StyledProperty SelectedDateProperty = + AvaloniaProperty.Register(nameof(SelectedDate), defaultBindingMode: BindingMode.TwoWay); /// @@ -529,8 +521,8 @@ namespace Avalonia.Controls /// public DateTime? SelectedDate { - get { return _selectedDate; } - set { SetAndRaise(SelectedDateProperty, ref _selectedDate, value); } + get => GetValue(SelectedDateProperty); + set => SetValue(SelectedDateProperty, value); } private void OnSelectedDateChanged(AvaloniaPropertyChangedEventArgs e) { @@ -726,11 +718,8 @@ namespace Avalonia.Controls } } - public static readonly DirectProperty DisplayDateProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDate), - o => o.DisplayDate, - (o, v) => o.DisplayDate = v, + public static readonly StyledProperty DisplayDateProperty = + AvaloniaProperty.Register(nameof(DisplayDate), defaultBindingMode: BindingMode.TwoWay); /// @@ -760,8 +749,8 @@ namespace Avalonia.Controls /// public DateTime DisplayDate { - get { return _displayDate; } - set { SetAndRaise(DisplayDateProperty, ref _displayDate, value); } + get => GetValue(DisplayDateProperty); + set => SetValue(DisplayDateProperty, value); } internal DateTime DisplayDateInternal { get; private set; } @@ -796,11 +785,8 @@ namespace Avalonia.Controls DisplayDateChanged?.Invoke(this, e); } - public static readonly DirectProperty DisplayDateStartProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateStart), - o => o.DisplayDateStart, - (o, v) => o.DisplayDateStart = v, + public static readonly StyledProperty DisplayDateStartProperty = + AvaloniaProperty.Register(nameof(DisplayDateStart), defaultBindingMode: BindingMode.TwoWay); /// /// Gets or sets the first date to be displayed. @@ -814,8 +800,8 @@ namespace Avalonia.Controls /// public DateTime? DisplayDateStart { - get { return _displayDateStart; } - set { SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value); } + get => GetValue(DisplayDateStartProperty); + set => SetValue(DisplayDateStartProperty, value); } private void OnDisplayDateStartChanged(AvaloniaPropertyChangedEventArgs e) { @@ -831,7 +817,7 @@ namespace Avalonia.Controls if (selectedDateMin.HasValue && DateTime.Compare(selectedDateMin.Value, newValue.Value) < 0) { - DisplayDateStart = selectedDateMin.Value; + SetCurrentValue(DisplayDateStartProperty, selectedDateMin.Value); return; } @@ -839,14 +825,14 @@ namespace Avalonia.Controls // DisplayDateEnd = DisplayDateStart if (DateTime.Compare(newValue.Value, DisplayDateRangeEnd) > 0) { - DisplayDateEnd = DisplayDateStart; + SetCurrentValue(DisplayDateEndProperty, DisplayDateStart); } // If DisplayDate < DisplayDateStart, // DisplayDate = DisplayDateStart if (DateTimeHelper.CompareYearMonth(newValue.Value, DisplayDateInternal) > 0) { - DisplayDate = newValue.Value; + SetCurrentValue(DisplayDateProperty, newValue.Value); } } UpdateMonths(); @@ -905,11 +891,8 @@ namespace Avalonia.Controls get { return DisplayDateStart.GetValueOrDefault(DateTime.MinValue); } } - public static readonly DirectProperty DisplayDateEndProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateEnd), - o => o.DisplayDateEnd, - (o, v) => o.DisplayDateEnd = v, + public static readonly StyledProperty DisplayDateEndProperty = + AvaloniaProperty.Register(nameof(DisplayDateEnd), defaultBindingMode: BindingMode.TwoWay); /// @@ -924,8 +907,8 @@ namespace Avalonia.Controls /// public DateTime? DisplayDateEnd { - get { return _displayDateEnd; } - set { SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value); } + get => GetValue(DisplayDateEndProperty); + set => SetValue(DisplayDateEndProperty, value); } private void OnDisplayDateEndChanged(AvaloniaPropertyChangedEventArgs e) @@ -942,7 +925,7 @@ namespace Avalonia.Controls if (selectedDateMax.HasValue && DateTime.Compare(selectedDateMax.Value, newValue.Value) > 0) { - DisplayDateEnd = selectedDateMax.Value; + SetCurrentValue(DisplayDateEndProperty, selectedDateMax.Value); return; } @@ -950,7 +933,7 @@ namespace Avalonia.Controls // DisplayDateEnd = DisplayDateStart if (DateTime.Compare(newValue.Value, DisplayDateRangeStart) < 0) { - DisplayDateEnd = DisplayDateStart; + SetCurrentValue(DisplayDateEndProperty, DisplayDateStart); return; } @@ -958,7 +941,7 @@ namespace Avalonia.Controls // DisplayDate = DisplayDateEnd if (DateTimeHelper.CompareYearMonth(newValue.Value, DisplayDateInternal) < 0) { - DisplayDate = newValue.Value; + SetCurrentValue(DisplayDateProperty, newValue.Value); } } UpdateMonths(); @@ -1284,7 +1267,7 @@ namespace Avalonia.Controls { LastSelectedDate = d.Value; } - DisplayDate = d.Value; + SetCurrentValue(DisplayDateProperty, d.Value); } } else @@ -1332,7 +1315,7 @@ namespace Avalonia.Controls { LastSelectedDate = d.Value; } - DisplayDate = d.Value; + SetCurrentValue(DisplayDateProperty, d.Value); } } else @@ -1719,7 +1702,7 @@ namespace Avalonia.Controls if (ctrl) { SelectedMonth = DisplayDateInternal; - DisplayMode = CalendarMode.Year; + SetCurrentValue(DisplayModeProperty, CalendarMode.Year); } else { @@ -1733,7 +1716,7 @@ namespace Avalonia.Controls if (ctrl) { SelectedYear = SelectedMonth; - DisplayMode = CalendarMode.Decade; + SetCurrentValue(DisplayModeProperty, CalendarMode.Decade); } else { @@ -1770,8 +1753,8 @@ namespace Avalonia.Controls { if (ctrl) { - DisplayDate = SelectedMonth; - DisplayMode = CalendarMode.Month; + SetCurrentValue(DisplayDateProperty, SelectedMonth); + SetCurrentValue(DisplayModeProperty, CalendarMode.Month); } else { @@ -1785,7 +1768,7 @@ namespace Avalonia.Controls if (ctrl) { SelectedMonth = SelectedYear; - DisplayMode = CalendarMode.Year; + SetCurrentValue(DisplayModeProperty, CalendarMode.Year); } else { @@ -1850,14 +1833,14 @@ namespace Avalonia.Controls { case CalendarMode.Year: { - DisplayDate = SelectedMonth; - DisplayMode = CalendarMode.Month; + SetCurrentValue(DisplayDateProperty, SelectedMonth); + SetCurrentValue(DisplayModeProperty, CalendarMode.Month); return true; } case CalendarMode.Decade: { SelectedMonth = SelectedYear; - DisplayMode = CalendarMode.Year; + SetCurrentValue(DisplayModeProperty, CalendarMode.Year); return true; } } @@ -2103,7 +2086,8 @@ namespace Avalonia.Controls /// public Calendar() { - UpdateDisplayDate(this, this.DisplayDate, DateTime.MinValue); + SetCurrentValue(DisplayDateProperty, DateTime.Today); + UpdateDisplayDate(this, DisplayDate, DateTime.MinValue); BlackoutDates = new CalendarBlackoutDatesCollection(this); SelectedDates = new SelectedDatesCollection(this); RemovedItems = new Collection(); diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 3d436b4485..2e3f1f96ce 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -41,7 +41,6 @@ namespace Avalonia.Controls.Primitives private Button? _headerButton; private Button? _nextButton; private Button? _previousButton; - private ITemplate? _dayTitleTemplate; private DateTime _currentMonth; private bool _isMouseLeftButtonDown; @@ -61,17 +60,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(HeaderBackgroundProperty, value); } } - public static readonly DirectProperty?> DayTitleTemplateProperty = - AvaloniaProperty.RegisterDirect?>( + public static readonly StyledProperty?> DayTitleTemplateProperty = + AvaloniaProperty.Register?>( nameof(DayTitleTemplate), - o => o.DayTitleTemplate, - (o,v) => o.DayTitleTemplate = v, defaultBindingMode: BindingMode.OneTime); public ITemplate? DayTitleTemplate { - get { return _dayTitleTemplate; } - set { SetAndRaise(DayTitleTemplateProperty, ref _dayTitleTemplate, value); } + get => GetValue(DayTitleTemplateProperty); + set => SetValue(DayTitleTemplateProperty, value); } /// @@ -176,9 +173,8 @@ namespace Avalonia.Controls.Primitives for (int i = 0; i < Calendar.RowsPerMonth; i++) { - if (_dayTitleTemplate != null) + if (DayTitleTemplate?.Build() is Control cell) { - var cell = _dayTitleTemplate.Build(); cell.DataContext = string.Empty; cell.SetValue(Grid.RowProperty, 0); cell.SetValue(Grid.ColumnProperty, i); diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs index 6c2356b411..1454b4ab6c 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs @@ -11,29 +11,22 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty DisplayDateProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDate), - o => o.DisplayDate, - (o, v) => o.DisplayDate = v); + public static readonly StyledProperty DisplayDateProperty = + AvaloniaProperty.Register(nameof(DisplayDate)); /// /// Defines the property. /// - public static readonly DirectProperty DisplayDateStartProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateStart), - o => o.DisplayDateStart, - (o, v) => o.DisplayDateStart = v); + public static readonly StyledProperty DisplayDateStartProperty = + AvaloniaProperty.Register( + nameof(DisplayDateStart)); /// /// Defines the property. /// - public static readonly DirectProperty DisplayDateEndProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateEnd), - o => o.DisplayDateEnd, - (o, v) => o.DisplayDateEnd = v); + public static readonly StyledProperty DisplayDateEndProperty = + AvaloniaProperty.Register( + nameof(DisplayDateEnd)); /// /// Defines the property. @@ -44,11 +37,9 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty IsDropDownOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsDropDownOpen), - o => o.IsDropDownOpen, - (o, v) => o.IsDropDownOpen = v); + public static readonly StyledProperty IsDropDownOpenProperty = + AvaloniaProperty.Register( + nameof(IsDropDownOpen)); /// /// Defines the property. @@ -59,11 +50,9 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty SelectedDateProperty = - AvaloniaProperty.RegisterDirect( + public static readonly StyledProperty SelectedDateProperty = + AvaloniaProperty.Register( nameof(SelectedDate), - o => o.SelectedDate, - (o, v) => o.SelectedDate = v, enableDataValidation: true, defaultBindingMode:BindingMode.TwoWay); @@ -88,11 +77,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect( - nameof(Text), - o => o.Text, - (o, v) => o.Text = v); + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text)); /// /// Defines the property. @@ -141,8 +127,8 @@ namespace Avalonia.Controls /// public DateTime DisplayDate { - get => _displayDate; - set => SetAndRaise(DisplayDateProperty, ref _displayDate, value); + get => GetValue(DisplayDateProperty); + set => SetValue(DisplayDateProperty, value); } /// @@ -151,8 +137,8 @@ namespace Avalonia.Controls /// The first date to display. public DateTime? DisplayDateStart { - get => _displayDateStart; - set => SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value); + get => GetValue(DisplayDateStartProperty); + set => SetValue(DisplayDateStartProperty, value); } /// @@ -161,8 +147,8 @@ namespace Avalonia.Controls /// The last date to display. public DateTime? DisplayDateEnd { - get => _displayDateEnd; - set => SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value); + get => GetValue(DisplayDateEndProperty); + set => SetValue(DisplayDateEndProperty, value); } /// @@ -188,8 +174,8 @@ namespace Avalonia.Controls /// public bool IsDropDownOpen { - get => _isDropDownOpen; - set => SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); + get => GetValue(IsDropDownOpenProperty); + set => SetValue(IsDropDownOpenProperty, value); } /// @@ -223,8 +209,8 @@ namespace Avalonia.Controls /// public DateTime? SelectedDate { - get => _selectedDate; - set => SetAndRaise(SelectedDateProperty, ref _selectedDate, value); + get => GetValue(SelectedDateProperty); + set => SetValue(SelectedDateProperty, value); } /// @@ -264,8 +250,8 @@ namespace Avalonia.Controls /// public string? Text { - get => _text; - set => SetAndRaise(TextProperty, ref _text, value); + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); } /// diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index 869bdeabea..c091d07632 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -45,12 +45,6 @@ namespace Avalonia.Controls private DateTime? _onOpenSelectedDate; private bool _settingSelectedDate; - private DateTime _displayDate; - private DateTime? _displayDateStart; - private DateTime? _displayDateEnd; - private bool _isDropDownOpen; - private DateTime? _selectedDate; - private string? _text; private bool _suspendTextChangeHandler; private bool _isPopupClosing; private bool _ignoreButtonClick; @@ -92,9 +86,9 @@ namespace Avalonia.Controls /// public CalendarDatePicker() { - FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek; + SetCurrentValue(FirstDayOfWeekProperty, DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek); _defaultText = string.Empty; - DisplayDate = DateTime.Today; + SetCurrentValue(DisplayDateProperty, DateTime.Today); } /// @@ -257,7 +251,7 @@ namespace Avalonia.Controls Threading.Dispatcher.UIThread.InvokeAsync(() => { _settingSelectedDate = true; - Text = DateTimeToString(day); + SetCurrentValue(TextProperty, DateTimeToString(day)); _settingSelectedDate = false; OnDateSelected(addedDate, removedDate); }); @@ -268,7 +262,7 @@ namespace Avalonia.Controls // be changed by the Calendar if ((day.Month != DisplayDate.Month || day.Year != DisplayDate.Year) && (_calendar == null || !_calendar.CalendarDatePickerDisplayDateFlag)) { - DisplayDate = day; + SetCurrentValue(DisplayDateProperty, day); } if(_calendar != null) @@ -317,7 +311,7 @@ namespace Avalonia.Controls if (!_settingSelectedDate) { _settingSelectedDate = true; - SelectedDate = null; + SetCurrentValue(SelectedDateProperty, null); _settingSelectedDate = false; } } @@ -400,7 +394,7 @@ namespace Avalonia.Controls DateTime? newDate = DateTimeHelper.AddDays(selectedDate, e.Delta.Y > 0 ? -1 : 1); if (newDate.HasValue && Calendar.IsValidDateSelection(_calendar, newDate.Value)) { - SelectedDate = newDate; + SetCurrentValue(SelectedDateProperty, newDate); e.Handled = true; } } @@ -478,7 +472,7 @@ namespace Avalonia.Controls { if (SelectedDate.HasValue) { - Text = DateTimeToString(SelectedDate.Value); + SetCurrentValue(TextProperty, DateTimeToString(SelectedDate.Value)); } else if (string.IsNullOrEmpty(_textBox.Text)) { @@ -491,7 +485,7 @@ namespace Avalonia.Controls if (date != null) { string? s = DateTimeToString((DateTime)date); - Text = s; + SetCurrentValue(TextProperty, s); } } } @@ -547,7 +541,7 @@ namespace Avalonia.Controls private void Calendar_DayButtonMouseUp(object? sender, PointerReleasedEventArgs e) { Focus(); - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); } private void Calendar_DisplayDateChanged(object? sender, CalendarDateChangedEventArgs e) @@ -564,13 +558,13 @@ namespace Avalonia.Controls if (e.AddedItems.Count > 0 && SelectedDate.HasValue && DateTime.Compare((DateTime)e.AddedItems[0]!, SelectedDate.Value) != 0) { - SelectedDate = (DateTime?)e.AddedItems[0]; + SetCurrentValue(SelectedDateProperty, (DateTime?)e.AddedItems[0]); } else { if (e.AddedItems.Count == 0) { - SelectedDate = null; + SetCurrentValue(SelectedDateProperty, null); return; } @@ -578,7 +572,7 @@ namespace Avalonia.Controls { if (e.AddedItems.Count > 0) { - SelectedDate = (DateTime?)e.AddedItems[0]; + SetCurrentValue(SelectedDateProperty, (DateTime?)e.AddedItems[0]); } } } @@ -600,18 +594,18 @@ namespace Avalonia.Controls && (e.Key == Key.Enter || e.Key == Key.Space || e.Key == Key.Escape)) { Focus(); - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); if (e.Key == Key.Escape) { - SelectedDate = _onOpenSelectedDate; + SetCurrentValue(SelectedDateProperty, _onOpenSelectedDate); } } } private void TextBox_GotFocus(object? sender, RoutedEventArgs e) { - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); } private void TextBox_KeyDown(object? sender, KeyEventArgs e) @@ -627,7 +621,7 @@ namespace Avalonia.Controls if (_textBox != null) { _suspendTextChangeHandler = true; - Text = _textBox.Text; + SetCurrentValue(TextProperty, _textBox.Text); _suspendTextChangeHandler = false; } } @@ -660,7 +654,7 @@ namespace Avalonia.Controls private void PopUp_Closed(object? sender, EventArgs e) { - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); if(!_isPopupClosing) { @@ -678,12 +672,12 @@ namespace Avalonia.Controls if (IsDropDownOpen) { Focus(); - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); } else { SetSelectedDate(); - IsDropDownOpen = true; + SetCurrentValue(IsDropDownOpenProperty, true); _calendar!.Focus(); } } @@ -821,14 +815,14 @@ namespace Avalonia.Controls if (SelectedDate != d) { - SelectedDate = d; + SetCurrentValue(SelectedDateProperty, d); } } else { if (SelectedDate != null) { - SelectedDate = null; + SetCurrentValue(SelectedDateProperty, null); } } } @@ -838,7 +832,7 @@ namespace Avalonia.Controls if (SelectedDate != d) { - SelectedDate = d; + SetCurrentValue(SelectedDateProperty, d); } } } @@ -884,7 +878,7 @@ namespace Avalonia.Controls if (string.IsNullOrEmpty(Watermark) && !UseFloatingWatermark) { DateTimeFormatInfo dtfi = DateTimeHelper.GetCurrentDateFormat(); - Text = string.Empty; + SetCurrentValue(TextProperty, string.Empty); _defaultText = string.Empty; var watermarkFormat = "<{0}>"; string watermarkText; diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 17a6ad7a09..f6e4b32d6b 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -35,11 +35,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty IsDropDownOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsDropDownOpen), - o => o.IsDropDownOpen, - (o, v) => o.IsDropDownOpen = v); + public static readonly StyledProperty IsDropDownOpenProperty = + AvaloniaProperty.Register(nameof(IsDropDownOpen)); /// /// Defines the property. @@ -77,7 +74,6 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); - private bool _isDropDownOpen; private Popup? _popup; private object? _selectionBoxItem; private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable(); @@ -107,8 +103,8 @@ namespace Avalonia.Controls /// public bool IsDropDownOpen { - get => _isDropDownOpen; - set => SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); + get => GetValue(IsDropDownOpenProperty); + set => SetValue(IsDropDownOpenProperty, value); } /// @@ -123,10 +119,10 @@ namespace Avalonia.Controls /// /// Gets or sets the item to display as the control's content. /// - protected object? SelectionBoxItem + public object? SelectionBoxItem { get => _selectionBoxItem; - set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); + protected set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); } /// @@ -191,23 +187,23 @@ namespace Avalonia.Controls if ((e.Key == Key.F4 && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) == false) || ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt))) { - IsDropDownOpen = !IsDropDownOpen; + SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen); e.Handled = true; } else if (IsDropDownOpen && e.Key == Key.Escape) { - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); e.Handled = true; } else if (!IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space)) { - IsDropDownOpen = true; + SetCurrentValue(IsDropDownOpenProperty, true); e.Handled = true; } else if (IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space)) { SelectFocusedItem(); - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); e.Handled = true; } else if (!IsDropDownOpen) @@ -291,7 +287,7 @@ namespace Avalonia.Controls } else { - IsDropDownOpen = !IsDropDownOpen; + SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen); e.Handled = true; } } @@ -390,7 +386,7 @@ namespace Avalonia.Controls { if (!isVisible && IsDropDownOpen) { - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); } } diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index bb05cd1b1f..118183102a 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -1,7 +1,6 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; -using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Layout; @@ -29,65 +28,56 @@ namespace Avalonia.Controls /// /// Define the Property /// - public static readonly DirectProperty DayFormatProperty = - AvaloniaProperty.RegisterDirect(nameof(DayFormat), - x => x.DayFormat, (x, v) => x.DayFormat = v); + public static readonly StyledProperty DayFormatProperty = + AvaloniaProperty.Register(nameof(DayFormat), "%d"); /// /// Defines the Property /// - public static readonly DirectProperty DayVisibleProperty = - AvaloniaProperty.RegisterDirect(nameof(DayVisible), - x => x.DayVisible, (x, v) => x.DayVisible = v); + public static readonly StyledProperty DayVisibleProperty = + AvaloniaProperty.Register(nameof(DayVisible), true); /// /// Defines the Property /// - public static readonly DirectProperty MaxYearProperty = - AvaloniaProperty.RegisterDirect(nameof(MaxYear), - x => x.MaxYear, (x, v) => x.MaxYear = v); + public static readonly StyledProperty MaxYearProperty = + AvaloniaProperty.Register(nameof(MaxYear), DateTimeOffset.MaxValue, coerce: CoerceMaxYear); /// /// Defines the Property /// - public static readonly DirectProperty MinYearProperty = - AvaloniaProperty.RegisterDirect(nameof(MinYear), - x => x.MinYear, (x, v) => x.MinYear = v); + public static readonly StyledProperty MinYearProperty = + AvaloniaProperty.Register(nameof(MinYear), DateTimeOffset.MinValue, coerce: CoerceMinYear); /// /// Defines the Property /// - public static readonly DirectProperty MonthFormatProperty = - AvaloniaProperty.RegisterDirect(nameof(MonthFormat), - x => x.MonthFormat, (x, v) => x.MonthFormat = v); + public static readonly StyledProperty MonthFormatProperty = + AvaloniaProperty.Register(nameof(MonthFormat), "MMMM"); /// /// Defines the Property /// - public static readonly DirectProperty MonthVisibleProperty = - AvaloniaProperty.RegisterDirect(nameof(MonthVisible), - x => x.MonthVisible, (x, v) => x.MonthVisible = v); + public static readonly StyledProperty MonthVisibleProperty = + AvaloniaProperty.Register(nameof(MonthVisible), true); /// /// Defines the Property /// - public static readonly DirectProperty YearFormatProperty = - AvaloniaProperty.RegisterDirect(nameof(YearFormat), - x => x.YearFormat, (x, v) => x.YearFormat = v); + public static readonly StyledProperty YearFormatProperty = + AvaloniaProperty.Register(nameof(YearFormat), "yyyy"); /// /// Defines the Property /// - public static readonly DirectProperty YearVisibleProperty = - AvaloniaProperty.RegisterDirect(nameof(YearVisible), - x => x.YearVisible, (x, v) => x.YearVisible = v); + public static readonly StyledProperty YearVisibleProperty = + AvaloniaProperty.Register(nameof(YearVisible), true); /// /// Defines the Property /// - public static readonly DirectProperty SelectedDateProperty = - AvaloniaProperty.RegisterDirect(nameof(SelectedDate), - x => x.SelectedDate, (x, v) => x.SelectedDate = v, + public static readonly StyledProperty SelectedDateProperty = + AvaloniaProperty.Register(nameof(SelectedDate), defaultBindingMode: BindingMode.TwoWay); // Template Items @@ -103,28 +93,20 @@ namespace Avalonia.Controls private bool _areControlsAvailable; - private string _dayFormat = "%d"; - private bool _dayVisible = true; - private DateTimeOffset _maxYear; - private DateTimeOffset _minYear; - private string _monthFormat = "MMMM"; - private bool _monthVisible = true; - private string _yearFormat = "yyyy"; - private bool _yearVisible = true; - private DateTimeOffset? _selectedDate; - public DatePicker() { PseudoClasses.Set(":hasnodate", true); var now = DateTimeOffset.Now; - _minYear = new DateTimeOffset(now.Date.Year - 100, 1, 1, 0, 0, 0, now.Offset); - _maxYear = new DateTimeOffset(now.Date.Year + 100, 12, 31, 0, 0, 0, now.Offset); + SetCurrentValue(MinYearProperty, new DateTimeOffset(now.Date.Year - 100, 1, 1, 0, 0, 0, now.Offset)); + SetCurrentValue(MaxYearProperty, new DateTimeOffset(now.Date.Year + 100, 12, 31, 0, 0, 0, now.Offset)); } + private static void OnGridVisibilityChanged(DatePicker sender, AvaloniaPropertyChangedEventArgs e) => sender.SetGrid(); + public string DayFormat { - get => _dayFormat; - set => SetAndRaise(DayFormatProperty, ref _dayFormat, value); + get => GetValue(DayFormatProperty); + set => SetValue(DayFormatProperty, value); } /// @@ -132,12 +114,8 @@ namespace Avalonia.Controls /// public bool DayVisible { - get => _dayVisible; - set - { - SetAndRaise(DayVisibleProperty, ref _dayVisible, value); - SetGrid(); - } + get => GetValue(DayVisibleProperty); + set => SetValue(DayVisibleProperty, value); } /// @@ -145,16 +123,24 @@ namespace Avalonia.Controls /// public DateTimeOffset MaxYear { - get => _maxYear; - set - { - if (value < MinYear) - throw new InvalidOperationException("MaxDate cannot be less than MinDate"); - SetAndRaise(MaxYearProperty, ref _maxYear, value); + get => GetValue(MaxYearProperty); + set => SetValue(MaxYearProperty, value); + } - if (SelectedDate.HasValue && SelectedDate.Value > value) - SelectedDate = value; + private static DateTimeOffset CoerceMaxYear(AvaloniaObject sender, DateTimeOffset value) + { + if (value < sender.GetValue(MinYearProperty)) + { + throw new InvalidOperationException($"{MaxYearProperty.Name} cannot be less than {MinYearProperty.Name}"); } + + return value; + } + + private void OnMaxYearChanged(DateTimeOffset? value) + { + if (SelectedDate.HasValue && SelectedDate.Value > value) + SetCurrentValue(SelectedDateProperty, value); } /// @@ -162,16 +148,24 @@ namespace Avalonia.Controls /// public DateTimeOffset MinYear { - get => _minYear; - set - { - if (value > MaxYear) - throw new InvalidOperationException("MinDate cannot be greater than MaxDate"); - SetAndRaise(MinYearProperty, ref _minYear, value); + get => GetValue(MinYearProperty); + set => SetValue(MinYearProperty, value); + } - if (SelectedDate.HasValue && SelectedDate.Value < value) - SelectedDate = value; + private static DateTimeOffset CoerceMinYear(AvaloniaObject sender, DateTimeOffset value) + { + if (value > sender.GetValue(MaxYearProperty)) + { + throw new InvalidOperationException($"{MinYearProperty.Name} cannot be greater than {MaxYearProperty.Name}"); } + + return value; + } + + private void OnMinYearChanged(DateTimeOffset? value) + { + if (SelectedDate.HasValue && SelectedDate.Value < value) + SetCurrentValue(SelectedDateProperty, value); } /// @@ -179,8 +173,8 @@ namespace Avalonia.Controls /// public string MonthFormat { - get => _monthFormat; - set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value); + get => GetValue(MonthFormatProperty); + set => SetValue(MonthFormatProperty, value); } /// @@ -188,12 +182,8 @@ namespace Avalonia.Controls /// public bool MonthVisible { - get => _monthVisible; - set - { - SetAndRaise(MonthVisibleProperty, ref _monthVisible, value); - SetGrid(); - } + get => GetValue(MonthVisibleProperty); + set => SetValue(MonthVisibleProperty, value); } /// @@ -201,8 +191,8 @@ namespace Avalonia.Controls /// public string YearFormat { - get => _yearFormat; - set => SetAndRaise(YearFormatProperty, ref _yearFormat, value); + get => GetValue(YearFormatProperty); + set => SetValue(YearFormatProperty, value); } /// @@ -210,12 +200,8 @@ namespace Avalonia.Controls /// public bool YearVisible { - get => _yearVisible; - set - { - SetAndRaise(YearVisibleProperty, ref _yearVisible, value); - SetGrid(); - } + get => GetValue(YearVisibleProperty); + set => SetValue(YearVisibleProperty, value); } /// @@ -223,14 +209,8 @@ namespace Avalonia.Controls /// public DateTimeOffset? SelectedDate { - get => _selectedDate; - set - { - var old = _selectedDate; - SetAndRaise(SelectedDateProperty, ref _selectedDate, value); - SetSelectedDateText(); - OnSelectedDateChanged(this, new DatePickerSelectedValueChangedEventArgs(old, value)); - } + get => GetValue(SelectedDateProperty); + set => SetValue(SelectedDateProperty, value); } /// @@ -287,6 +267,31 @@ namespace Avalonia.Controls } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DayVisibleProperty || change.Property == MonthVisibleProperty || change.Property == YearVisibleProperty) + { + SetGrid(); + } + else if (change.Property == MaxYearProperty) + { + OnMaxYearChanged(change.GetNewValue()); + } + else if (change.Property == MinYearProperty) + { + OnMinYearChanged(change.GetNewValue()); + } + else if (change.Property == SelectedDateProperty) + { + SetSelectedDateText(); + + var (oldValue, newValue) = change.GetOldAndNewValue(); + OnSelectedDateChanged(this, new DatePickerSelectedValueChangedEventArgs(oldValue, newValue)); + } + } + private void OnDismissPicker(object? sender, EventArgs e) { _popup!.Close(); @@ -296,7 +301,7 @@ namespace Avalonia.Controls private void OnConfirmed(object? sender, EventArgs e) { _popup!.Close(); - SelectedDate = _presenter!.Date; + SetCurrentValue(SelectedDateProperty, _presenter!.Date); } private void SetGrid() diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs index 2057480490..0ae743f30a 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs @@ -35,65 +35,72 @@ namespace Avalonia.Controls /// /// Defines the Property /// - public static readonly DirectProperty DateProperty = - AvaloniaProperty.RegisterDirect(nameof(Date), - x => x.Date, (x, v) => x.Date = v); + public static readonly StyledProperty DateProperty = + AvaloniaProperty.Register(nameof(Date), coerce: CoerceDate); + + private static DateTimeOffset CoerceDate(AvaloniaObject sender, DateTimeOffset value) + { + var max = sender.GetValue(MaxYearProperty); + if (value > max) + { + return max; + } + var min = sender.GetValue(MinYearProperty); + if (value < min) + { + return min; + } + + return value; + } /// /// Defines the Property /// - public static readonly DirectProperty DayFormatProperty = - DatePicker.DayFormatProperty.AddOwner(x => - x.DayFormat, (x, v) => x.DayFormat = v); + public static readonly StyledProperty DayFormatProperty = + DatePicker.DayFormatProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty DayVisibleProperty = - DatePicker.DayVisibleProperty.AddOwner(x => - x.DayVisible, (x, v) => x.DayVisible = v); + public static readonly StyledProperty DayVisibleProperty = + DatePicker.DayVisibleProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty MaxYearProperty = - DatePicker.MaxYearProperty.AddOwner(x => - x.MaxYear, (x, v) => x.MaxYear = v); + public static readonly StyledProperty MaxYearProperty = + DatePicker.MaxYearProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty MinYearProperty = - DatePicker.MinYearProperty.AddOwner(x => - x.MinYear, (x, v) => x.MinYear = v); + public static readonly StyledProperty MinYearProperty = + DatePicker.MinYearProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty MonthFormatProperty = - DatePicker.MonthFormatProperty.AddOwner(x => - x.MonthFormat, (x, v) => x.MonthFormat = v); + public static readonly StyledProperty MonthFormatProperty = + DatePicker.MonthFormatProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty MonthVisibleProperty = - DatePicker.MonthVisibleProperty.AddOwner(x => - x.MonthVisible, (x, v) => x.MonthVisible = v); + public static readonly StyledProperty MonthVisibleProperty = + DatePicker.MonthVisibleProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty YearFormatProperty = - DatePicker.YearFormatProperty.AddOwner(x => - x.YearFormat, (x, v) => x.YearFormat = v); + public static readonly StyledProperty YearFormatProperty = + DatePicker.YearFormatProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty YearVisibleProperty = - DatePicker.YearVisibleProperty.AddOwner(x => - x.YearVisible, (x, v) => x.YearVisible = v); + public static readonly StyledProperty YearVisibleProperty = + DatePicker.YearVisibleProperty.AddOwner(); // Template Items private Grid? _pickerContainer; @@ -114,15 +121,6 @@ namespace Avalonia.Controls private Button? _dayDownButton; private Button? _yearDownButton; - private DateTimeOffset _date; - private string _dayFormat = "%d"; - private bool _dayVisible = true; - private DateTimeOffset _maxYear; - private DateTimeOffset _minYear; - private string _monthFormat = "MMMM"; - private bool _monthVisible = true; - private string _yearFormat = "yyyy"; - private bool _yearVisible = true; private DateTimeOffset _syncDate; private readonly GregorianCalendar _calendar; @@ -131,11 +129,20 @@ namespace Avalonia.Controls public DatePickerPresenter() { var now = DateTimeOffset.Now; - _minYear = new DateTimeOffset(now.Year - 100, 1, 1, 0, 0, 0, now.Offset); - _maxYear = new DateTimeOffset(now.Year + 100, 12, 31, 0, 0, 0, now.Offset); - _date = now; + SetCurrentValue(MinYearProperty, new DateTimeOffset(now.Year - 100, 1, 1, 0, 0, 0, now.Offset)); + SetCurrentValue(MaxYearProperty, new DateTimeOffset(now.Year + 100, 12, 31, 0, 0, 0, now.Offset)); + SetCurrentValue(DateProperty, now); _calendar = new GregorianCalendar(); - KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle); + } + + static DatePickerPresenter() + { + KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); + } + + private static void OnDateRangeChanged(DatePickerPresenter sender, AvaloniaPropertyChangedEventArgs e) + { + sender.CoerceValue(DateProperty); } /// @@ -143,13 +150,14 @@ namespace Avalonia.Controls /// public DateTimeOffset Date { - get => _date; - set - { - SetAndRaise(DateProperty, ref _date, value); - _syncDate = Date; - InitPicker(); - } + get => GetValue(DateProperty); + set => SetValue(DateProperty, value); + } + + private void OnDateChanged(DateTimeOffset newValue) + { + _syncDate = newValue; + InitPicker(); } /// @@ -157,8 +165,8 @@ namespace Avalonia.Controls /// public string DayFormat { - get => _dayFormat; - set => SetAndRaise(DayFormatProperty, ref _dayFormat, value); + get => GetValue(DayFormatProperty); + set => SetValue(DayFormatProperty, value); } /// @@ -166,11 +174,8 @@ namespace Avalonia.Controls /// public bool DayVisible { - get => _dayVisible; - set - { - SetAndRaise(DayVisibleProperty, ref _dayVisible, value); - } + get => GetValue(DayVisibleProperty); + set => SetValue(DayVisibleProperty, value); } /// @@ -178,16 +183,8 @@ namespace Avalonia.Controls /// public DateTimeOffset MaxYear { - get => _maxYear; - set - { - if (value < MinYear) - throw new InvalidOperationException("MaxDate cannot be less than MinDate"); - SetAndRaise(MaxYearProperty, ref _maxYear, value); - - if (Date > value) - Date = value; - } + get => GetValue(MaxYearProperty); + set => SetValue(MaxYearProperty, value); } /// @@ -195,16 +192,8 @@ namespace Avalonia.Controls /// public DateTimeOffset MinYear { - get => _minYear; - set - { - if (value > MaxYear) - throw new InvalidOperationException("MinDate cannot be greater than MaxDate"); - SetAndRaise(MinYearProperty, ref _minYear, value); - - if (Date < value) - Date = value; - } + get => GetValue(MinYearProperty); + set => SetValue(MinYearProperty, value); } /// @@ -212,8 +201,8 @@ namespace Avalonia.Controls /// public string MonthFormat { - get => _monthFormat; - set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value); + get => GetValue(MonthFormatProperty); + set => SetValue(MonthFormatProperty, value); } /// @@ -221,11 +210,8 @@ namespace Avalonia.Controls /// public bool MonthVisible { - get => _monthVisible; - set - { - SetAndRaise(MonthVisibleProperty, ref _monthVisible, value); - } + get => GetValue(MonthVisibleProperty); + set => SetValue(MonthVisibleProperty, value); } /// @@ -233,8 +219,8 @@ namespace Avalonia.Controls /// public string YearFormat { - get => _yearFormat; - set => SetAndRaise(YearFormatProperty, ref _yearFormat, value); + get => GetValue(YearFormatProperty); + set => SetValue(YearFormatProperty, value); } /// @@ -242,11 +228,8 @@ namespace Avalonia.Controls /// public bool YearVisible { - get => _yearVisible; - set - { - SetAndRaise(YearVisibleProperty, ref _yearVisible, value); - } + get => GetValue(YearVisibleProperty); + set => SetValue(YearVisibleProperty, value); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) @@ -317,6 +300,20 @@ namespace Avalonia.Controls InitPicker(); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DateProperty) + { + OnDateChanged(change.GetNewValue()); + } + else if (change.Property == MaxYearProperty || change.Property == MinYearProperty) + { + OnDateRangeChanged(this, change); + } + } + protected override void OnKeyDown(KeyEventArgs e) { switch (e.Key) @@ -334,7 +331,7 @@ namespace Avalonia.Controls } break; case Key.Enter: - Date = _syncDate; + SetCurrentValue(DateProperty, _syncDate); OnConfirmed(); e.Handled = true; break; @@ -381,13 +378,13 @@ namespace Avalonia.Controls _monthSelector.SelectedValue = dt.Month; _monthSelector.FormatDate = dt.Date; } - + if (YearVisible) { _yearSelector.SelectedValue = dt.Year; _yearSelector.FormatDate = dt.Date; } - + _suppressUpdateSelection = false; SetInitialFocus(); @@ -471,7 +468,7 @@ namespace Avalonia.Controls private void OnAcceptButtonClicked(object? sender, RoutedEventArgs e) { - Date = _syncDate; + SetCurrentValue(DateProperty, _syncDate); OnConfirmed(); } diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index a7a6881fe5..2f49a44b8c 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -1,7 +1,6 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; -using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Layout; using System; @@ -30,23 +29,20 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly DirectProperty MinuteIncrementProperty = - AvaloniaProperty.RegisterDirect(nameof(MinuteIncrement), - x => x.MinuteIncrement, (x, v) => x.MinuteIncrement = v); + public static readonly StyledProperty MinuteIncrementProperty = + AvaloniaProperty.Register(nameof(MinuteIncrement), 1, coerce: CoerceMinuteIncrement); /// /// Defines the property /// - public static readonly DirectProperty ClockIdentifierProperty = - AvaloniaProperty.RegisterDirect(nameof(ClockIdentifier), - x => x.ClockIdentifier, (x, v) => x.ClockIdentifier = v); + public static readonly StyledProperty ClockIdentifierProperty = + AvaloniaProperty.Register(nameof(ClockIdentifier), "12HourClock", coerce: CoerceClockIdentifier); /// /// Defines the property /// - public static readonly DirectProperty SelectedTimeProperty = - AvaloniaProperty.RegisterDirect(nameof(SelectedTime), - x => x.SelectedTime, (x, v) => x.SelectedTime = v, + public static readonly StyledProperty SelectedTimeProperty = + AvaloniaProperty.Register(nameof(SelectedTime), defaultBindingMode: BindingMode.TwoWay); // Template Items @@ -63,17 +59,13 @@ namespace Avalonia.Controls private Grid? _contentGrid; private Popup? _popup; - private TimeSpan? _selectedTime; - private int _minuteIncrement = 1; - private string _clockIdentifier = "12HourClock"; - public TimePicker() { PseudoClasses.Set(":hasnotime", true); var timePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern; if (timePattern.IndexOf("H") != -1) - _clockIdentifier = "24HourClock"; + SetCurrentValue(ClockIdentifierProperty, "24HourClock"); } /// @@ -81,14 +73,16 @@ namespace Avalonia.Controls /// public int MinuteIncrement { - get => _minuteIncrement; - set - { - if (value < 1 || value > 59) - throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59"); - SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value); - SetSelectedTimeText(); - } + get => GetValue(MinuteIncrementProperty); + set => SetValue(MinuteIncrementProperty, value); + } + + private static int CoerceMinuteIncrement(AvaloniaObject sender, int value) + { + if (value < 1 || value > 59) + throw new ArgumentOutOfRangeException(null, "1 >= MinuteIncrement <= 59"); + + return value; } /// @@ -96,15 +90,17 @@ namespace Avalonia.Controls /// public string ClockIdentifier { - get => _clockIdentifier; - set - { - if (!(string.IsNullOrEmpty(value) || value == "12HourClock" || value == "24HourClock")) - throw new ArgumentException("Invalid ClockIdentifier"); - SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value); - SetGrid(); - SetSelectedTimeText(); - } + + get => GetValue(ClockIdentifierProperty); + set => SetValue(ClockIdentifierProperty, value); + } + + private static string CoerceClockIdentifier(AvaloniaObject sender, string value) + { + if (!(string.IsNullOrEmpty(value) || value == "12HourClock" || value == "24HourClock")) + throw new ArgumentException("Invalid ClockIdentifier", default(string)); + + return value; } /// @@ -112,14 +108,8 @@ namespace Avalonia.Controls /// public TimeSpan? SelectedTime { - get => _selectedTime; - set - { - var old = _selectedTime; - SetAndRaise(SelectedTimeProperty, ref _selectedTime, value); - OnSelectedTimeChanged(old, value); - SetSelectedTimeText(); - } + get => GetValue(SelectedTimeProperty); + set => SetValue(SelectedTimeProperty, value); } /// @@ -173,6 +163,27 @@ namespace Avalonia.Controls } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == MinuteIncrementProperty) + { + SetSelectedTimeText(); + } + else if (change.Property == ClockIdentifierProperty) + { + SetGrid(); + SetSelectedTimeText(); + } + else if (change.Property == SelectedTimeProperty) + { + var (oldValue, newValue) = change.GetOldAndNewValue(); + OnSelectedTimeChanged(oldValue, newValue); + SetSelectedTimeText(); + } + } + private void SetGrid() { if (_contentGrid == null) @@ -270,7 +281,7 @@ namespace Avalonia.Controls private void OnConfirmed(object? sender, EventArgs e) { _popup!.Close(); - SelectedTime = _presenter!.Time; + SetCurrentValue(SelectedTimeProperty, _presenter!.Time); } } } diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs index d6599c9f18..ba06e1b5e6 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs @@ -30,28 +30,29 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly DirectProperty MinuteIncrementProperty = - TimePicker.MinuteIncrementProperty.AddOwner(x => x.MinuteIncrement, - (x, v) => x.MinuteIncrement = v); + public static readonly StyledProperty MinuteIncrementProperty = + TimePicker.MinuteIncrementProperty.AddOwner(); /// /// Defines the property /// - public static readonly DirectProperty ClockIdentifierProperty = - TimePicker.ClockIdentifierProperty.AddOwner(x => x.ClockIdentifier, - (x, v) => x.ClockIdentifier = v); + public static readonly StyledProperty ClockIdentifierProperty = + TimePicker.ClockIdentifierProperty.AddOwner(); /// /// Defines the property /// - public static readonly DirectProperty TimeProperty = - AvaloniaProperty.RegisterDirect(nameof(Time), - x => x.Time, (x, v) => x.Time = v); + public static readonly StyledProperty TimeProperty = + AvaloniaProperty.Register(nameof(Time)); public TimePickerPresenter() { - Time = DateTime.Now.TimeOfDay; - KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle); + SetCurrentValue(TimeProperty, DateTime.Now.TimeOfDay); + } + + static TimePickerPresenter() + { + KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); } // TemplateItems @@ -70,24 +71,13 @@ namespace Avalonia.Controls private Button? _minuteDownButton; private Button? _periodDownButton; - // Backing Fields - private TimeSpan _time; - private int _minuteIncrement = 1; - private string _clockIdentifier = "12HourClock"; - /// /// Gets or sets the minute increment in the selector /// public int MinuteIncrement { - get => _minuteIncrement; - set - { - if (value < 1 || value > 59) - throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59"); - SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value); - InitPicker(); - } + get => GetValue(MinuteIncrementProperty); + set => SetValue(MinuteIncrementProperty, value); } /// @@ -95,14 +85,8 @@ namespace Avalonia.Controls /// public string ClockIdentifier { - get => _clockIdentifier; - set - { - if (string.IsNullOrEmpty(value) || !(value == "12HourClock" || value == "24HourClock")) - throw new ArgumentException("Invalid ClockIdentifier"); - SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value); - InitPicker(); - } + get => GetValue(ClockIdentifierProperty); + set => SetValue(ClockIdentifierProperty, value); } /// @@ -110,12 +94,8 @@ namespace Avalonia.Controls /// public TimeSpan Time { - get => _time; - set - { - SetAndRaise(TimeProperty, ref _time, value); - InitPicker(); - } + get => GetValue(TimeProperty); + set => SetValue(TimeProperty, value); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) @@ -162,6 +142,16 @@ namespace Avalonia.Controls InitPicker(); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == MinuteIncrementProperty || change.Property == ClockIdentifierProperty || change.Property == TimeProperty) + { + InitPicker(); + } + } + protected override void OnKeyDown(KeyEventArgs e) { switch (e.Key) @@ -197,7 +187,7 @@ namespace Avalonia.Controls hr = per == 1 ? (hr == 12) ? 12 : hr + 12 : per == 0 && hr == 12 ? 0 : hr; } - Time = new TimeSpan(hr, min, 0); + SetCurrentValue(TimeProperty, new TimeSpan(hr, min, 0)); base.OnConfirmed(); } diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 12a096b105..fe9f5e64a8 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls.Documents this.ForEachItem( x => - { + { x.InlineHost = InlineHost; LogicalChildren?.Add(x); Invalidate(); @@ -92,10 +92,10 @@ namespace Avalonia.Controls.Documents public override void Add(Inline inline) { if (InlineHost is TextBlock textBlock && !string.IsNullOrEmpty(textBlock._text)) - { + { base.Add(new Run(textBlock._text)); - textBlock._text = null; + textBlock._text = null; } base.Add(inline); @@ -159,7 +159,7 @@ namespace Avalonia.Controls.Documents oldParent.Remove(child); } - if(newParent != null) + if (newParent != null) { newParent.Add(child); } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 9d4abec549..36f8371702 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -35,17 +35,14 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property /// - public static readonly DirectProperty ShowModeProperty = - AvaloniaProperty.RegisterDirect(nameof(ShowMode), - x => x.ShowMode, (x, v) => x.ShowMode = v); + public static readonly StyledProperty ShowModeProperty = + AvaloniaProperty.Register(nameof(ShowMode)); /// /// Defines the property /// - public static readonly DirectProperty OverlayInputPassThroughElementProperty = - Popup.OverlayInputPassThroughElementProperty.AddOwner( - o => o._overlayInputPassThroughElement, - (o, v) => o._overlayInputPassThroughElement = v); + public static readonly StyledProperty OverlayInputPassThroughElementProperty = + Popup.OverlayInputPassThroughElementProperty.AddOwner(); /// /// Defines the AttachedFlyout property @@ -56,12 +53,10 @@ namespace Avalonia.Controls.Primitives private readonly Lazy _popupLazy; private bool _isOpen; private Control? _target; - private FlyoutShowMode _showMode = FlyoutShowMode.Standard; private Rect? _enlargedPopupRect; private PixelRect? _enlargePopupRectScreenPixelRect; private IDisposable? _transientDisposable; private Action? _popupHostChangedHandler; - private IInputElement? _overlayInputPassThroughElement; static FlyoutBase() { @@ -98,8 +93,8 @@ namespace Avalonia.Controls.Primitives /// public FlyoutShowMode ShowMode { - get => _showMode; - set => SetAndRaise(ShowModeProperty, ref _showMode, value); + get => GetValue(ShowModeProperty); + set => SetValue(ShowModeProperty, value); } /// @@ -117,8 +112,8 @@ namespace Avalonia.Controls.Primitives /// public IInputElement? OverlayInputPassThroughElement { - get => _overlayInputPassThroughElement; - set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + get => GetValue(OverlayInputPassThroughElementProperty); + set => SetValue(OverlayInputPassThroughElementProperty, value); } IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; @@ -244,7 +239,7 @@ namespace Avalonia.Controls.Primitives { Popup.PlacementTarget = Target = placementTarget; ((ISetLogicalParent)Popup).SetParent(placementTarget); - Popup.SetValue(StyledElement.TemplatedParentProperty, placementTarget.TemplatedParent); + Popup.TemplatedParent = placementTarget.TemplatedParent; } if (Popup.Child == null) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 9483f98881..bebf4a38f6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -383,14 +383,7 @@ namespace Avalonia.Controls { hic.Header = item; hic.HeaderTemplate = itemTemplate; - - itemTemplate ??= hic.FindDataTemplate(item) ?? this.FindDataTemplate(item); - - if (itemTemplate is ITreeDataTemplate treeTemplate) - { - if (item is not null && treeTemplate.ItemsSelector(item) is { } itemsBinding) - BindingOperations.Apply(hic, ItemsProperty, itemsBinding, null); - } + hic.PrepareItemContainer(); } } diff --git a/src/Avalonia.Controls/Label.cs b/src/Avalonia.Controls/Label.cs index 5c8a6e0a5b..94ea66c4c1 100644 --- a/src/Avalonia.Controls/Label.cs +++ b/src/Avalonia.Controls/Label.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Data; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Automation.Peers; using Avalonia.Input; using Avalonia.Interactivity; @@ -18,13 +13,8 @@ namespace Avalonia.Controls /// /// Defines the Direct property /// - public static readonly DirectProperty TargetProperty = - AvaloniaProperty.RegisterDirect(nameof(Target), lbl => lbl.Target, (lbl, inp) => lbl.Target = inp); - - /// - /// Label focus target storage field - /// - private IInputElement? _target; + public static readonly StyledProperty TargetProperty = + AvaloniaProperty.Register(nameof(Target)); /// /// Label focus Target @@ -32,8 +22,8 @@ namespace Avalonia.Controls [ResolveByName] public IInputElement? Target { - get => _target; - set => SetAndRaise(TargetProperty, ref _target, value); + get => GetValue(TargetProperty); + set => SetValue(TargetProperty, value); } static Label() @@ -71,5 +61,10 @@ namespace Avalonia.Controls } base.OnPointerPressed(e); } + + protected override AutomationPeer OnCreateAutomationPeer() + { + return new LabelAutomationPeer(this); + } } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 1670e496b4..03e3444d71 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -27,11 +27,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = - Button.CommandProperty.AddOwner( - menuItem => menuItem.Command, - (menuItem, command) => menuItem.Command = command, - enableDataValidation: true); + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(new(enableDataValidation: true)); /// /// Defines the property. @@ -113,7 +110,6 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); - private ICommand? _command; private bool _commandCanExecute = true; private bool _commandBindingError; private Popup? _popup; @@ -217,8 +213,8 @@ namespace Avalonia.Controls /// public ICommand? Command { - get { return _command; } - set { SetAndRaise(CommandProperty, ref _command, value); } + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// @@ -337,7 +333,7 @@ namespace Avalonia.Controls /// /// This has the same effect as setting to true. /// - public void Open() => IsSubMenuOpen = true; + public void Open() => SetCurrentValue(IsSubMenuOpenProperty, true); /// /// Closes the submenu. @@ -345,7 +341,7 @@ namespace Avalonia.Controls /// /// This has the same effect as setting to false. /// - public void Close() => IsSubMenuOpen = false; + public void Close() => SetCurrentValue(IsSubMenuOpenProperty, false); /// void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent)); @@ -369,7 +365,7 @@ namespace Avalonia.Controls { if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control { - HotKey = _hotkey; + SetCurrentValue(HotKeyProperty, _hotkey); } base.OnAttachedToLogicalTree(e); @@ -397,7 +393,7 @@ namespace Avalonia.Controls if (HotKey != null) { _hotkey = HotKey; - HotKey = null; + SetCurrentValue(HotKeyProperty, null); } base.OnDetachedFromLogicalTree(e); @@ -663,7 +659,7 @@ namespace Avalonia.Controls } RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent)); - IsSelected = true; + SetCurrentValue(IsSelectedProperty, true); PseudoClasses.Add(":open"); } else diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 5ff4148e5a..7b03c607b6 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -79,12 +79,12 @@ namespace Avalonia.Controls } public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); + AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); public NativeMenuItem? Parent { get => _parent; - set => SetAndRaise(ParentProperty, ref _parent, value); + internal set => SetAndRaise(ParentProperty, ref _parent, value); } public void Add(NativeMenuItemBase item) => _items.Add(item); diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 32fa574ee6..9b5f756887 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -4,36 +4,13 @@ using Avalonia.Input; using Avalonia.Media.Imaging; using Avalonia.Metadata; using Avalonia.Utilities; -using Avalonia.Reactive; namespace Avalonia.Controls { public class NativeMenuItem : NativeMenuItemBase, INativeMenuItemExporterEventsImplBridge { - private string? _header; - private KeyGesture? _gesture; - private bool _isEnabled = true; - private ICommand? _command; - private bool _isChecked = false; - private NativeMenuItemToggleType _toggleType; - private IBitmap? _icon; private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber; - private NativeMenu? _menu; - - static NativeMenuItem() - { - MenuProperty.Changed.Subscribe(args => - { - var item = (NativeMenuItem)args.Sender; - var value = args.NewValue.GetValueOrDefault()!; - if (value.Parent != null && value.Parent != item) - throw new InvalidOperationException("NativeMenu already has a parent"); - value.Parent = item; - }); - } - - class CanExecuteChangedSubscriber : IWeakEventSubscriber { private readonly NativeMenuItem _parent; @@ -60,78 +37,70 @@ namespace Avalonia.Controls Header = header; } - public static readonly DirectProperty MenuProperty = - AvaloniaProperty.RegisterDirect(nameof(Menu), o => o.Menu, (o, v) => o.Menu = v); + public static readonly StyledProperty MenuProperty = + AvaloniaProperty.Register(nameof(Menu), coerce: CoerceMenu); [Content] public NativeMenu? Menu { - get => _menu; - set - { - if (value != null && value.Parent != null && value.Parent != this) - throw new InvalidOperationException("NativeMenu already has a parent"); - SetAndRaise(MenuProperty, ref _menu, value); - } + get => GetValue(MenuProperty); + set => SetValue(MenuProperty, value); } - public static readonly DirectProperty IconProperty = - AvaloniaProperty.RegisterDirect(nameof(Icon), o => o.Icon, (o, v) => o.Icon = v); + private static NativeMenu? CoerceMenu(AvaloniaObject sender, NativeMenu? value) + { + if (value != null && value.Parent != null && value.Parent != sender) + throw new InvalidOperationException("NativeMenu already has a parent"); + return value; + } + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); public IBitmap? Icon { - get => _icon; - set => SetAndRaise(IconProperty, ref _icon, value); + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); } - public static readonly DirectProperty HeaderProperty = - AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, (o, v) => o.Header = v); + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); public string? Header { - get => _header; - set => SetAndRaise(HeaderProperty, ref _header, value); + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); } - public static readonly DirectProperty GestureProperty = - AvaloniaProperty.RegisterDirect(nameof(Gesture), o => o.Gesture, (o, v) => o.Gesture = v); + public static readonly StyledProperty GestureProperty = + AvaloniaProperty.Register(nameof(Gesture)); public KeyGesture? Gesture { - get => _gesture; - set => SetAndRaise(GestureProperty, ref _gesture, value); + get => GetValue(GestureProperty); + set => SetValue(GestureProperty, value); } - public static readonly DirectProperty IsCheckedProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsChecked), - o => o.IsChecked, - (o, v) => o.IsChecked = v); + public static readonly StyledProperty IsCheckedProperty = + AvaloniaProperty.Register(nameof(IsChecked)); public bool IsChecked { - get => _isChecked; - set => SetAndRaise(IsCheckedProperty, ref _isChecked, value); + get => GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); } - public static readonly DirectProperty ToggleTypeProperty = - AvaloniaProperty.RegisterDirect( - nameof(ToggleType), - o => o.ToggleType, - (o, v) => o.ToggleType = v); + public static readonly StyledProperty ToggleTypeProperty = + AvaloniaProperty.Register(nameof(ToggleType)); public NativeMenuItemToggleType ToggleType { - get => _toggleType; - set => SetAndRaise(ToggleTypeProperty, ref _toggleType, value); + get => GetValue(ToggleTypeProperty); + set => SetValue(ToggleTypeProperty, value); } - public static readonly DirectProperty CommandProperty = - Button.CommandProperty.AddOwner( - menuItem => menuItem.Command, - (menuItem, command) => menuItem.Command = command, - enableDataValidation: true); + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(new(enableDataValidation: true)); /// /// Defines the property. @@ -139,37 +108,26 @@ namespace Avalonia.Controls public static readonly StyledProperty CommandParameterProperty = Button.CommandParameterProperty.AddOwner(); - public static readonly DirectProperty IsEnabledProperty = - AvaloniaProperty.RegisterDirect(nameof(IsEnabled), o => o.IsEnabled, (o, v) => o.IsEnabled = v, true); + public static readonly StyledProperty IsEnabledProperty = + AvaloniaProperty.Register(nameof(IsEnabled), true); public bool IsEnabled { - get => _isEnabled; - set => SetAndRaise(IsEnabledProperty, ref _isEnabled, value); + get => GetValue(IsEnabledProperty); + set => SetValue(IsEnabledProperty, value); } void CanExecuteChanged() { - IsEnabled = _command?.CanExecute(CommandParameter) ?? true; + SetCurrentValue(IsEnabledProperty, Command?.CanExecute(CommandParameter) ?? true); } public bool HasClickHandlers => Click != null; public ICommand? Command { - get => _command; - set - { - if (_command != null) - WeakEvents.CommandCanExecuteChanged.Unsubscribe(_command, _canExecuteChangedSubscriber); - - SetAndRaise(CommandProperty, ref _command, value); - - if (_command != null) - WeakEvents.CommandCanExecuteChanged.Subscribe(_command, _canExecuteChangedSubscriber); - - CanExecuteChanged(); - } + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// @@ -196,8 +154,28 @@ namespace Avalonia.Controls Command.Execute(CommandParameter); } } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == MenuProperty && change.NewValue is NativeMenu newMenu) + { + if (newMenu.Parent != null && newMenu.Parent != this) + throw new InvalidOperationException("NativeMenu already has a parent"); + newMenu.Parent = this; + } + else if (change.Property == CommandProperty) + { + if (change.OldValue is ICommand oldCommand) + WeakEvents.CommandCanExecuteChanged.Unsubscribe(oldCommand, _canExecuteChangedSubscriber); + if (change.NewValue is ICommand newCommand) + WeakEvents.CommandCanExecuteChanged.Subscribe(newCommand, _canExecuteChangedSubscriber); + CanExecuteChanged(); + } + } } - + public enum NativeMenuItemToggleType { None, diff --git a/src/Avalonia.Controls/NativeMenuItemBase.cs b/src/Avalonia.Controls/NativeMenuItemBase.cs index 4946d16f01..70cb2b806e 100644 --- a/src/Avalonia.Controls/NativeMenuItemBase.cs +++ b/src/Avalonia.Controls/NativeMenuItemBase.cs @@ -12,12 +12,12 @@ namespace Avalonia.Controls } public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); + AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); public NativeMenu? Parent { get => _parent; - set => SetAndRaise(ParentProperty, ref _parent, value); + internal set => SetAndRaise(ParentProperty, ref _parent, value); } } } diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index 663bd3358a..705d40380e 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -13,7 +13,6 @@ namespace Avalonia.Controls.Notifications [PseudoClasses(":error", ":information", ":success", ":warning")] public class NotificationCard : ContentControl { - private bool _isClosed; private bool _isClosing; static NotificationCard() @@ -84,15 +83,15 @@ namespace Avalonia.Controls.Notifications /// public bool IsClosed { - get { return _isClosed; } - set { SetAndRaise(IsClosedProperty, ref _isClosed, value); } + get => GetValue(IsClosedProperty); + set => SetValue(IsClosedProperty, value); } /// /// Defines the property. /// - public static readonly DirectProperty IsClosedProperty = - AvaloniaProperty.RegisterDirect(nameof(IsClosed), o => o.IsClosed, (o, v) => o.IsClosed = v); + public static readonly StyledProperty IsClosedProperty = + AvaloniaProperty.Register(nameof(IsClosed)); /// /// Defines the event. diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index ac4f699313..885a8af5d1 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -43,16 +43,14 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ClipValueToMinMaxProperty = - AvaloniaProperty.RegisterDirect(nameof(ClipValueToMinMax), - updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b); + public static readonly StyledProperty ClipValueToMinMaxProperty = + AvaloniaProperty.Register(nameof(ClipValueToMinMax)); /// /// Defines the property. /// - public static readonly DirectProperty NumberFormatProperty = - AvaloniaProperty.RegisterDirect(nameof(NumberFormat), o => o.NumberFormat, - (o, v) => o.NumberFormat = v, NumberFormatInfo.CurrentInfo); + public static readonly StyledProperty NumberFormatProperty = + AvaloniaProperty.Register(nameof(NumberFormat), NumberFormatInfo.CurrentInfo); /// /// Defines the property. @@ -87,30 +85,28 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ParsingNumberStyleProperty = - AvaloniaProperty.RegisterDirect(nameof(ParsingNumberStyle), - updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style); + public static readonly StyledProperty ParsingNumberStyleProperty = + AvaloniaProperty.Register(nameof(ParsingNumberStyle), NumberStyles.Any); /// /// Defines the property. /// - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect(nameof(Text), o => o.Text, (o, v) => o.Text = v, + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text), defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// /// Defines the property. /// - public static readonly DirectProperty TextConverterProperty = - AvaloniaProperty.RegisterDirect(nameof(TextConverter), - updown => updown.TextConverter, (o, v) => o.TextConverter = v, null, BindingMode.OneWay, false); + public static readonly StyledProperty TextConverterProperty = + AvaloniaProperty.Register(nameof(TextConverter), defaultBindingMode: BindingMode.OneWay); /// /// Defines the property. /// - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, - (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register(nameof(Value), coerce: (s,v) => ((NumericUpDown)s).OnCoerceValue(v), + defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// /// Defines the property. @@ -132,15 +128,9 @@ namespace Avalonia.Controls private IDisposable? _textBoxTextChangedSubscription; - private decimal? _value; - private string? _text; - private IValueConverter? _textConverter; private bool _internalValueSet; - private bool _clipValueToMinMax; private bool _isSyncingTextAndValueProperties; private bool _isTextChangedFromUI; - private NumberStyles _parsingNumberStyle = NumberStyles.Any; - private NumberFormatInfo? _numberFormat; /// /// Gets the Spinner template part. @@ -184,8 +174,8 @@ namespace Avalonia.Controls /// public bool ClipValueToMinMax { - get { return _clipValueToMinMax; } - set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); } + get => GetValue(ClipValueToMinMaxProperty); + set => SetValue(ClipValueToMinMaxProperty, value); } /// @@ -193,8 +183,8 @@ namespace Avalonia.Controls /// public NumberFormatInfo? NumberFormat { - get { return _numberFormat; } - set { SetAndRaise(NumberFormatProperty, ref _numberFormat, value); } + get => GetValue(NumberFormatProperty); + set => SetValue(NumberFormatProperty, value); } /// @@ -249,8 +239,8 @@ namespace Avalonia.Controls /// public NumberStyles ParsingNumberStyle { - get { return _parsingNumberStyle; } - set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); } + get => GetValue(ParsingNumberStyleProperty); + set => SetValue(ParsingNumberStyleProperty, value); } /// @@ -258,8 +248,8 @@ namespace Avalonia.Controls /// public string? Text { - get { return _text; } - set { SetAndRaise(TextProperty, ref _text, value); } + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); } /// @@ -269,8 +259,8 @@ namespace Avalonia.Controls /// public IValueConverter? TextConverter { - get { return _textConverter; } - set { SetAndRaise(TextConverterProperty, ref _textConverter, value); } + get => GetValue(TextConverterProperty); + set => SetValue(TextConverterProperty, value); } /// @@ -278,12 +268,8 @@ namespace Avalonia.Controls /// public decimal? Value { - get { return _value; } - set - { - value = OnCoerceValue(value); - SetAndRaise(ValueProperty, ref _value, value); - } + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); } /// @@ -475,7 +461,7 @@ namespace Avalonia.Controls } if (ClipValueToMinMax && Value.HasValue) { - Value = MathUtilities.Clamp(Value.Value, Minimum, Maximum); + SetCurrentValue(ValueProperty, MathUtilities.Clamp(Value.Value, Minimum, Maximum)); } } @@ -492,7 +478,7 @@ namespace Avalonia.Controls } if (ClipValueToMinMax && Value.HasValue) { - Value = MathUtilities.Clamp(Value.Value, Minimum, Maximum); + SetCurrentValue(ValueProperty, MathUtilities.Clamp(Value.Value, Minimum, Maximum)); } } @@ -508,7 +494,7 @@ namespace Avalonia.Controls SyncTextAndValueProperties(true, Text); } } - + /// /// Called when the property value changed. /// @@ -675,8 +661,8 @@ namespace Avalonia.Controls { result = Minimum; } - - Value = MathUtilities.Clamp(result, Minimum, Maximum); + + SetCurrentValue(ValueProperty, MathUtilities.Clamp(result, Minimum, Maximum)); } /// @@ -685,7 +671,7 @@ namespace Avalonia.Controls private void OnDecrement() { decimal result; - + if (Value.HasValue) { result = Value.Value - Increment; @@ -694,8 +680,8 @@ namespace Avalonia.Controls { result = Maximum; } - - Value = MathUtilities.Clamp(result, Minimum, Maximum); + + SetCurrentValue(ValueProperty, MathUtilities.Clamp(result, Minimum, Maximum)); } /// @@ -712,7 +698,7 @@ namespace Avalonia.Controls { validDirections = ValidSpinDirections.Increase | ValidSpinDirections.Decrease; } - + if (Value < Maximum) { validDirections = validDirections | ValidSpinDirections.Increase; @@ -862,7 +848,7 @@ namespace Avalonia.Controls _internalValueSet = true; try { - Value = value; + SetCurrentValue(ValueProperty, value); } finally { @@ -907,7 +893,7 @@ namespace Avalonia.Controls _isTextChangedFromUI = true; if (TextBox != null) { - Text = TextBox.Text; + SetCurrentValue(TextProperty, TextBox.Text); } } finally @@ -1026,7 +1012,7 @@ namespace Avalonia.Controls var newText = ConvertValueToText(); if (!Equals(Text, newText)) { - Text = newText; + SetCurrentValue(TextProperty, newText); } } @@ -1066,7 +1052,7 @@ namespace Avalonia.Controls { return null; } - + if (TextConverter != null) { var valueFromText = TextConverter.Convert(text, typeof(decimal?), null, CultureInfo.CurrentCulture); diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index fa18ee468c..eff6603727 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -1,13 +1,12 @@ using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using Avalonia.Controls.Presenters; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Reactive; -using Avalonia.Styling; namespace Avalonia.Controls { @@ -59,6 +58,11 @@ namespace Avalonia.Controls set { SetValue(BackgroundProperty, value); } } + /// + /// Gets whether the hosts the items created by an . + /// + public bool IsItemsHost { get; internal set; } + event EventHandler? IChildIndexProvider.ChildIndexChanged { add @@ -129,24 +133,29 @@ namespace Avalonia.Controls /// The event args. protected virtual void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e) { - List controls; - switch (e.Action) { case NotifyCollectionChangedAction.Add: - controls = e.NewItems!.OfType().ToList(); - LogicalChildren.InsertRange(e.NewStartingIndex, controls); + if (!IsItemsHost) + { + LogicalChildren.InsertRange(e.NewStartingIndex, e.NewItems!.OfType().ToList()); + } VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems!.OfType()); break; case NotifyCollectionChangedAction.Move: - LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex); - VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems.Count, e.NewStartingIndex); + if (!IsItemsHost) + { + LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex); + } + VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex); break; case NotifyCollectionChangedAction.Remove: - controls = e.OldItems!.OfType().ToList(); - LogicalChildren.RemoveAll(controls); + if (!IsItemsHost) + { + LogicalChildren.RemoveAll(e.OldItems!.OfType().ToList()); + } VisualChildren.RemoveAll(e.OldItems!.OfType()); break; @@ -155,7 +164,10 @@ namespace Avalonia.Controls { var index = i + e.OldStartingIndex; var child = (Control)e.NewItems![i]!; - LogicalChildren[index] = child; + if (!IsItemsHost) + { + LogicalChildren[index] = child; + } VisualChildren[index] = child; } break; @@ -200,6 +212,7 @@ namespace Avalonia.Controls return child is Control control ? Children.IndexOf(control) : -1; } + /// public bool TryGetTotalCount(out int count) { count = Children.Count; diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs new file mode 100644 index 0000000000..6288142805 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Metadata; + +#nullable enable +namespace Avalonia.Controls.Platform +{ + [Unstable] + [NotClientImplementable] + public interface IInsetsManager + { + /// + /// Gets or sets whether the system bars are visible. + /// + bool? IsSystemBarVisible { get; set; } + + /// + /// Gets or sets whether the window draws edge to edge. behind any visibile system bars. + /// + bool DisplayEdgeToEdge { get; set; } + + /// + /// Gets the current safe area padding. + /// + Thickness SafeAreaPadding { get; } + + /// + /// Occurs when safe area for the current window changes. + /// + event EventHandler? SafeAreaChanged; + } + + public class SafeAreaChangedArgs : EventArgs + { + public SafeAreaChangedArgs(Thickness safeArePadding) + { + SafeAreaPadding = safeArePadding; + } + + /// + public Thickness SafeAreaPadding { get; } + } + + public enum SystemBarTheme + { + /// + /// Light system bar theme, with light background and a dark foreground + /// + Light, + + /// + /// Bark system bar theme, with dark background and a light foreground + /// + Dark + } +} diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index be61bb18a1..329a0fa6ab 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -156,16 +156,13 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property /// - public static readonly DirectProperty RecognizesAccessKeyProperty = - AvaloniaProperty.RegisterDirect( - nameof(RecognizesAccessKey), - cp => cp.RecognizesAccessKey, (cp, value) => cp.RecognizesAccessKey = value); + public static readonly StyledProperty RecognizesAccessKeyProperty = + AvaloniaProperty.Register(nameof(RecognizesAccessKey)); private Control? _child; private bool _createdChild; private IRecyclingDataTemplate? _recyclingDataTemplate; private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper(); - private bool _recognizesAccessKey; /// /// Initializes static members of the class. @@ -386,8 +383,8 @@ namespace Avalonia.Controls.Presenters /// public bool RecognizesAccessKey { - get => _recognizesAccessKey; - set => SetAndRaise(RecognizesAccessKeyProperty, ref _recognizesAccessKey, value); + get => GetValue(RecognizesAccessKeyProperty); + set => SetValue(RecognizesAccessKeyProperty, value); } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index e8eaac7d17..a0020a0b6e 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -166,7 +166,8 @@ namespace Avalonia.Controls.Presenters } Panel = ItemsPanel.Build(); - Panel.SetValue(TemplatedParentProperty, TemplatedParent); + Panel.TemplatedParent = TemplatedParent; + Panel.IsItemsHost = true; _scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo; LogicalChildren.Add(Panel); VisualChildren.Add(Panel); diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index 71ae7a5bf6..55d2ec7506 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -1,6 +1,8 @@ +using System; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives @@ -10,6 +12,9 @@ namespace Avalonia.Controls.Primitives /// public class HeaderedItemsControl : ItemsControl, IContentPresenterHost { + private IDisposable? _itemsBinding; + private bool _prepareItemContainerOnAttach; + /// /// Defines the property. /// @@ -60,6 +65,17 @@ namespace Avalonia.Controls.Primitives /// IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + + if (_prepareItemContainerOnAttach) + { + PrepareItemContainer(); + _prepareItemContainerOnAttach = false; + } + } + /// bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { @@ -81,6 +97,37 @@ namespace Avalonia.Controls.Primitives return false; } + internal void PrepareItemContainer() + { + _itemsBinding?.Dispose(); + _itemsBinding = null; + + var item = Header; + + if (item is null) + { + _prepareItemContainerOnAttach = false; + return; + } + + var headerTemplate = HeaderTemplate; + + if (headerTemplate is null) + { + if (((ILogical)this).IsAttachedToLogicalTree) + headerTemplate = this.FindDataTemplate(item); + else + _prepareItemContainerOnAttach = true; + } + + if (headerTemplate is ITreeDataTemplate treeTemplate && + treeTemplate.Match(item) && + treeTemplate.ItemsSelector(item) is { } itemsBinding) + { + _itemsBinding = BindingOperations.Apply(this, ItemsProperty, itemsBinding, null); + } + } + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) { if (e.OldValue is ILogical oldChild) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 9d443d9289..0c6c434713 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -2,7 +2,6 @@ using System; using System.ComponentModel; using Avalonia.Reactive; using Avalonia.Automation.Peers; -using Avalonia.Controls.Mixins; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; @@ -41,11 +40,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsOpen), - o => o.IsOpen, - (o, v) => o.IsOpen = v); + public static readonly StyledProperty IsOpenProperty = + AvaloniaProperty.Register(nameof(IsOpen)); /// /// Defines the property. @@ -90,11 +86,8 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty OverlayDismissEventPassThroughProperty = AvaloniaProperty.Register(nameof(OverlayDismissEventPassThrough)); - public static readonly DirectProperty OverlayInputPassThroughElementProperty = - AvaloniaProperty.RegisterDirect( - nameof(OverlayInputPassThroughElement), - o => o.OverlayInputPassThroughElement, - (o, v) => o.OverlayInputPassThroughElement = v); + public static readonly StyledProperty OverlayInputPassThroughElementProperty = + AvaloniaProperty.Register(nameof(OverlayInputPassThroughElement)); /// /// Defines the property. @@ -121,10 +114,8 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(Topmost)); private bool _isOpenRequested; - private bool _isOpen; private bool _ignoreIsOpenChanged; private PopupOpenState? _openState; - private IInputElement? _overlayInputPassThroughElement; private Action? _popupHostChangedHandler; /// @@ -209,8 +200,8 @@ namespace Avalonia.Controls.Primitives /// public bool IsOpen { - get { return _isOpen; } - set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } + get => GetValue(IsOpenProperty); + set => SetValue(IsOpenProperty, value); } /// @@ -301,8 +292,8 @@ namespace Avalonia.Controls.Primitives /// public IInputElement? OverlayInputPassThroughElement { - get => _overlayInputPassThroughElement; - set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + get => GetValue(OverlayInputPassThroughElementProperty); + set => SetValue(OverlayInputPassThroughElementProperty, value); } /// @@ -486,7 +477,7 @@ namespace Avalonia.Controls.Primitives using (BeginIgnoringIsOpen()) { - IsOpen = true; + SetCurrentValue(IsOpenProperty, true); } Opened?.Invoke(this, EventArgs.Empty); @@ -704,7 +695,7 @@ namespace Avalonia.Controls.Primitives { using (BeginIgnoringIsOpen()) { - IsOpen = false; + SetCurrentValue(IsOpenProperty, false); } return; @@ -717,7 +708,7 @@ namespace Avalonia.Controls.Primitives using (BeginIgnoringIsOpen()) { - IsOpen = false; + SetCurrentValue(IsOpenProperty, false); } Closed?.Invoke(this, EventArgs.Empty); diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 8253342782..e1cf25d89f 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -275,7 +275,7 @@ namespace Avalonia.Controls.Primitives { foreach (var child in this.GetTemplateChildren()) { - child.SetValue(TemplatedParentProperty, null); + child.TemplatedParent = null; ((ISetLogicalParent)child).SetParent(null); } @@ -377,7 +377,7 @@ namespace Avalonia.Controls.Primitives /// The templated parent to apply. internal static void ApplyTemplatedParent(StyledElement control, AvaloniaObject? templatedParent) { - control.SetValue(TemplatedParentProperty, templatedParent); + control.TemplatedParent = templatedParent; var children = control.LogicalChildren; var count = children.Count; diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 158c5d875b..dfaf7bbc45 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -15,12 +15,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly DirectProperty IsCheckedProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsChecked), - o => o.IsChecked, - (o, v) => o.IsChecked = v, - unsetValue: false, + public static readonly StyledProperty IsCheckedProperty = + AvaloniaProperty.Register(nameof(IsChecked), false, defaultBindingMode: BindingMode.TwoWay); /// @@ -64,8 +60,6 @@ namespace Avalonia.Controls.Primitives nameof(IsCheckedChanged), RoutingStrategies.Bubble); - private bool? _isChecked = false; - static ToggleButton() { } @@ -119,12 +113,8 @@ namespace Avalonia.Controls.Primitives /// public bool? IsChecked { - get => _isChecked; - set - { - SetAndRaise(IsCheckedProperty, ref _isChecked, value); - UpdatePseudoClasses(IsChecked); - } + get => GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); } /// @@ -147,28 +137,31 @@ namespace Avalonia.Controls.Primitives /// protected virtual void Toggle() { + bool? newValue; if (IsChecked.HasValue) { if (IsChecked.Value) { if (IsThreeState) { - IsChecked = null; + newValue = null; } else { - IsChecked = false; + newValue = false; } } else { - IsChecked = true; + newValue = true; } } else { - IsChecked = false; + newValue = false; } + + SetCurrentValue(IsCheckedProperty, newValue); } /// @@ -224,6 +217,8 @@ namespace Avalonia.Controls.Primitives { var newValue = change.GetNewValue(); + UpdatePseudoClasses(newValue); + #pragma warning disable CS0618 // Type or member is obsolete switch (newValue) { diff --git a/src/Avalonia.Controls/RadioButton.cs b/src/Avalonia.Controls/RadioButton.cs index 87772aced7..d4528fdb1c 100644 --- a/src/Avalonia.Controls/RadioButton.cs +++ b/src/Avalonia.Controls/RadioButton.cs @@ -98,31 +98,22 @@ namespace Avalonia.Controls } } - public static readonly DirectProperty GroupNameProperty = - AvaloniaProperty.RegisterDirect( - nameof(GroupName), - o => o.GroupName, - (o, v) => o.GroupName = v); + public static readonly StyledProperty GroupNameProperty = + AvaloniaProperty.Register(nameof(GroupName)); - private string? _groupName; private RadioButtonGroupManager? _groupManager; - public RadioButton() - { - this.GetObservable(IsCheckedProperty).Subscribe(IsCheckedChanged); - } - public string? GroupName { - get { return _groupName; } - set { SetGroupName(value); } + get => GetValue(GroupNameProperty); + set => SetValue(GroupNameProperty, value); } protected override void Toggle() { if (!IsChecked.GetValueOrDefault()) { - IsChecked = true; + SetCurrentValue(IsCheckedProperty, true); } } @@ -154,28 +145,38 @@ namespace Avalonia.Controls return new RadioButtonAutomationPeer(this); } - private void SetGroupName(string? newGroupName) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - var oldGroupName = GroupName; - if (newGroupName != oldGroupName) + base.OnPropertyChanged(change); + + if (change.Property == IsCheckedProperty) { - if (!string.IsNullOrEmpty(oldGroupName)) - { - _groupManager?.Remove(this, oldGroupName); - } - _groupName = newGroupName; - if (!string.IsNullOrEmpty(newGroupName)) + IsCheckedChanged(change.GetNewValue()); + } + else if (change.Property == GroupNameProperty) + { + var (oldValue, newValue) = change.GetOldAndNewValue(); + OnGroupNameChanged(oldValue, newValue); + } + } + + private void OnGroupNameChanged(string? oldGroupName, string? newGroupName) + { + if (!string.IsNullOrEmpty(oldGroupName)) + { + _groupManager?.Remove(this, oldGroupName); + } + if (!string.IsNullOrEmpty(newGroupName)) + { + if (_groupManager == null) { - if (_groupManager == null) - { - _groupManager = RadioButtonGroupManager.GetOrCreateForRoot(this.GetVisualRoot()); - } - _groupManager.Add(this); + _groupManager = RadioButtonGroupManager.GetOrCreateForRoot(this.GetVisualRoot()); } + _groupManager.Add(this); } } - private void IsCheckedChanged(bool? value) + private new void IsCheckedChanged(bool? value) { var groupName = GroupName; if (string.IsNullOrEmpty(groupName)) diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 31a06d875a..e82fb39a66 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -42,10 +42,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = - Button.CommandProperty.AddOwner( - splitButton => splitButton.Command, - (splitButton, command) => splitButton.Command = command); + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(); /// /// Defines the property. @@ -59,8 +57,6 @@ namespace Avalonia.Controls public static readonly StyledProperty FlyoutProperty = Button.FlyoutProperty.AddOwner(); - private ICommand? _Command; - private Button? _primaryButton = null; private Button? _secondaryButton = null; @@ -83,8 +79,8 @@ namespace Avalonia.Controls /// public ICommand? Command { - get => _Command; - set => SetAndRaise(CommandProperty, ref _Command, value); + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index fdcb8cc537..5c2a8c8a13 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -15,6 +15,7 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Platform.Storage; +using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; @@ -391,7 +392,9 @@ namespace Avalonia.Controls ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) ?? PlatformImpl?.TryGetFeature() ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); - + + public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature(); + /// Point IRenderRoot.PointToClient(PixelPoint p) { diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 5713846b35..73bcb84c69 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -13,13 +13,10 @@ namespace Avalonia.Controls public sealed class TrayIcons : AvaloniaList { } - - public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable { private readonly ITrayIconImpl? _impl; - private ICommand? _command; private TrayIcon(ITrayIconImpl? impl) { @@ -85,11 +82,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = - Button.CommandProperty.AddOwner( - trayIcon => trayIcon.Command, - (trayIcon, command) => trayIcon.Command = command, - enableDataValidation: true); + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(new(enableDataValidation: true)); /// /// Defines the property. @@ -136,8 +130,8 @@ namespace Avalonia.Controls /// public ICommand? Command { - get => _command; - set => SetAndRaise(CommandProperty, ref _command, value); + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index e9abfef673..3ac157f727 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -104,12 +104,12 @@ namespace Avalonia.Controls if (ItemTemplate == null && _treeView?.ItemTemplate != null) { - ItemTemplate = _treeView.ItemTemplate; + SetCurrentValue(ItemTemplateProperty, _treeView.ItemTemplate); } if (ItemContainerTheme == null && _treeView?.ItemContainerTheme != null) { - ItemContainerTheme = _treeView.ItemContainerTheme; + SetCurrentValue(ItemContainerThemeProperty, _treeView.ItemContainerTheme); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ba1b599421..f9593f1c1b 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; -using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Automation.Peers; using Avalonia.Controls.Platform; @@ -11,6 +9,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Reactive; using Avalonia.Styling; namespace Avalonia.Controls @@ -149,11 +148,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty WindowStartupLocationProperty = - AvaloniaProperty.RegisterDirect( - nameof(WindowStartupLocation), - o => o.WindowStartupLocation, - (o, v) => o.WindowStartupLocation = v); + public static readonly StyledProperty WindowStartupLocationProperty = + AvaloniaProperty.Register(nameof(WindowStartupLocation)); public static readonly StyledProperty CanResizeProperty = AvaloniaProperty.Register(nameof(CanResize), true); @@ -171,7 +167,6 @@ namespace Avalonia.Controls RoutedEvent.Register("WindowOpened", RoutingStrategies.Direct); private object? _dialogResult; private readonly Size _maxPlatformClientSize; - private WindowStartupLocation _windowStartupLocation; private bool _shown; private bool _showingAsDialog; @@ -305,7 +300,7 @@ namespace Avalonia.Controls { get => GetValue(ExtendClientAreaTitleBarHeightHintProperty); set => SetValue(ExtendClientAreaTitleBarHeightHintProperty, value); - } + } /// /// Gets if the ClientArea is Extended into the Window Decorations. @@ -314,7 +309,7 @@ namespace Avalonia.Controls { get => _isExtendedIntoWindowDecorations; private set => SetAndRaise(IsExtendedIntoWindowDecorationsProperty, ref _isExtendedIntoWindowDecorations, value); - } + } /// /// Gets the WindowDecorationMargin. @@ -324,7 +319,7 @@ namespace Avalonia.Controls { get => _windowDecorationMargin; private set => SetAndRaise(WindowDecorationMarginProperty, ref _windowDecorationMargin, value); - } + } /// /// Gets the window margin that is hidden off the screen area. @@ -397,8 +392,8 @@ namespace Avalonia.Controls /// public WindowStartupLocation WindowStartupLocation { - get { return _windowStartupLocation; } - set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLocation, value); } + get => GetValue(WindowStartupLocationProperty); + set => SetValue(WindowStartupLocationProperty, value); } /// @@ -488,7 +483,7 @@ namespace Avalonia.Controls CloseInternal(); return false; } - + return true; } @@ -614,7 +609,7 @@ namespace Avalonia.Controls if (_shown != isVisible) { - if(!_shown) + if (!_shown) { Show(); } @@ -657,7 +652,7 @@ namespace Avalonia.Controls throw new InvalidOperationException("Cannot re-show a closed window."); } } - + private void EnsureParentStateBeforeShow(Window owner) { if (owner.PlatformImpl == null) @@ -819,7 +814,7 @@ namespace Avalonia.Controls { bool isEnabled = true; - foreach (var (_, isDialog) in _children) + foreach (var (_, isDialog) in _children) { if (isDialog) { @@ -856,7 +851,7 @@ namespace Avalonia.Controls { Window? firstDialogChild = null; - foreach (var (child, isDialog) in _children) + foreach (var (child, isDialog) in _children) { if (isDialog) { @@ -880,7 +875,7 @@ namespace Avalonia.Controls var startupLocation = WindowStartupLocation; if (startupLocation == WindowStartupLocation.CenterOwner && - (owner is null || + (owner is null || (Owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) ) { @@ -902,7 +897,7 @@ namespace Avalonia.Controls if (owner is not null) { - screen = Screens.ScreenFromWindow(owner) + screen = Screens.ScreenFromWindow(owner) ?? Screens.ScreenFromPoint(owner.Position); } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 26e11f0d4a..814a9b5960 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -27,10 +27,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly DirectProperty OwnerProperty = - AvaloniaProperty.RegisterDirect( - nameof(Owner), - o => o.Owner, - (o, v) => o.Owner = v); + AvaloniaProperty.RegisterDirect(nameof(Owner), o => o.Owner); public static readonly StyledProperty TopmostProperty = AvaloniaProperty.Register(nameof(Topmost)); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs new file mode 100644 index 0000000000..b7579ed31b --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs @@ -0,0 +1,90 @@ +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace Avalonia.Diagnostics.Controls +{ + internal sealed class BrushEditor : Control + { + /// + /// Defines the property. + /// + public static readonly DirectProperty BrushProperty = + AvaloniaProperty.RegisterDirect( + nameof(Brush), o => o.Brush, (o, v) => o.Brush = v); + + private IBrush? _brush; + + public IBrush? Brush + { + get => _brush; + set => SetAndRaise(BrushProperty, ref _brush, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == BrushProperty) + { + switch (Brush) + { + case ISolidColorBrush scb: + { + var colorView = new ColorView { Color = scb.Color }; + + colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor); + + FlyoutBase.SetAttachedFlyout(this, new Flyout { Content = colorView }); + ToolTip.SetTip(this, $"{scb.Color} ({Brush.GetType().Name})"); + + break; + } + + default: + + FlyoutBase.SetAttachedFlyout(this, null); + ToolTip.SetTip(this, Brush?.GetType().Name ?? "(null)"); + + break; + } + + InvalidateVisual(); + } + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + FlyoutBase.ShowAttachedFlyout(this); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + if (Brush != null) + { + context.FillRectangle(Brush, Bounds); + } + else + { + context.FillRectangle(Brushes.Black, Bounds); + + var ft = new FormattedText("(null)", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + Typeface.Default, + 10, + Brushes.White); + + context.DrawText(ft, + new Point(Bounds.Width / 2 - ft.Width / 2, Bounds.Height / 2 - ft.Height / 2)); + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs new file mode 100644 index 0000000000..7870febd0a --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs @@ -0,0 +1,89 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Styling; + +namespace Avalonia.Diagnostics.Controls +{ + //TODO: UpdateSourceTrigger & Binding.ValidationRules could help removing the need for this control. + internal sealed class CommitTextBox : TextBox, IStyleable + { + Type IStyleable.StyleKey => typeof(TextBox); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CommittedTextProperty = + AvaloniaProperty.RegisterDirect( + nameof(CommittedText), o => o.CommittedText, (o, v) => o.CommittedText = v); + + private string? _committedText; + + public string? CommittedText + { + get => _committedText; + set => SetAndRaise(CommittedTextProperty, ref _committedText, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == CommittedTextProperty) + { + Text = CommittedText; + } + } + + protected override void OnKeyUp(KeyEventArgs e) + { + base.OnKeyUp(e); + + switch (e.Key) + { + case Key.Enter: + + TryCommit(); + + e.Handled = true; + + break; + + case Key.Escape: + + Cancel(); + + e.Handled = true; + + break; + } + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + + TryCommit(); + } + + private void Cancel() + { + Text = CommittedText; + DataValidationErrors.ClearErrors(this); + } + + private void TryCommit() + { + if (!DataValidationErrors.GetHasErrors(this)) + { + CommittedText = Text; + } + else + { + Text = CommittedText; + DataValidationErrors.ClearErrors(this); + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs index c9189a886d..f765871ee8 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs @@ -6,84 +6,73 @@ namespace Avalonia.Diagnostics.Controls { internal class ThicknessEditor : ContentControl { - public static readonly DirectProperty ThicknessProperty = - AvaloniaProperty.RegisterDirect(nameof(Thickness), o => o.Thickness, - (o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay); + public static readonly StyledProperty ThicknessProperty = + AvaloniaProperty.Register(nameof(Thickness), + defaultBindingMode: BindingMode.TwoWay); - public static readonly DirectProperty HeaderProperty = - AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, - (o, v) => o.Header = v); + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); - public static readonly DirectProperty IsPresentProperty = - AvaloniaProperty.RegisterDirect(nameof(IsPresent), o => o.IsPresent, - (o, v) => o.IsPresent = v); + public static readonly StyledProperty IsPresentProperty = + AvaloniaProperty.Register(nameof(IsPresent), true); - public static readonly DirectProperty LeftProperty = - AvaloniaProperty.RegisterDirect(nameof(Left), o => o.Left, (o, v) => o.Left = v); + public static readonly StyledProperty LeftProperty = + AvaloniaProperty.Register(nameof(Left)); - public static readonly DirectProperty TopProperty = - AvaloniaProperty.RegisterDirect(nameof(Top), o => o.Top, (o, v) => o.Top = v); + public static readonly StyledProperty TopProperty = + AvaloniaProperty.Register(nameof(Top)); - public static readonly DirectProperty RightProperty = - AvaloniaProperty.RegisterDirect(nameof(Right), o => o.Right, - (o, v) => o.Right = v); + public static readonly StyledProperty RightProperty = + AvaloniaProperty.Register(nameof(Right)); - public static readonly DirectProperty BottomProperty = - AvaloniaProperty.RegisterDirect(nameof(Bottom), o => o.Bottom, - (o, v) => o.Bottom = v); + public static readonly StyledProperty BottomProperty = + AvaloniaProperty.Register(nameof(Bottom)); public static readonly StyledProperty HighlightProperty = AvaloniaProperty.Register(nameof(Highlight)); - private Thickness _thickness; - private string? _header; - private bool _isPresent = true; - private double _left; - private double _top; - private double _right; - private double _bottom; private bool _isUpdatingThickness; public Thickness Thickness { - get => _thickness; - set => SetAndRaise(ThicknessProperty, ref _thickness, value); + get => GetValue(ThicknessProperty); + set => SetValue(ThicknessProperty, value); } public string? Header { - get => _header; - set => SetAndRaise(HeaderProperty, ref _header, value); + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); } public bool IsPresent { - get => _isPresent; - set => SetAndRaise(IsPresentProperty, ref _isPresent, value); + get => GetValue(IsPresentProperty); + set => SetValue(IsPresentProperty, value); } public double Left { - get => _left; - set => SetAndRaise(LeftProperty, ref _left, value); + get => GetValue(LeftProperty); + set => SetValue(LeftProperty, value); } public double Top { - get => _top; - set => SetAndRaise(TopProperty, ref _top, value); + get => GetValue(TopProperty); + set => SetValue(TopProperty, value); } public double Right { - get => _right; - set => SetAndRaise(RightProperty, ref _right, value); + get => GetValue(RightProperty); + set => SetValue(RightProperty, value); } public double Bottom { - get => _bottom; - set => SetAndRaise(BottomProperty, ref _bottom, value); + get => GetValue(BottomProperty); + set => SetValue(BottomProperty, value); } public IBrush Highlight @@ -104,10 +93,10 @@ namespace Avalonia.Diagnostics.Controls var value = change.GetNewValue(); - Left = value.Left; - Top = value.Top; - Right = value.Right; - Bottom = value.Bottom; + SetCurrentValue(LeftProperty, value.Left); + SetCurrentValue(TopProperty, value.Top); + SetCurrentValue(RightProperty, value.Right); + SetCurrentValue(BottomProperty, value.Bottom); } finally { @@ -118,7 +107,7 @@ namespace Avalonia.Diagnostics.Controls (change.Property == LeftProperty || change.Property == TopProperty || change.Property == RightProperty || change.Property == BottomProperty)) { - Thickness = new Thickness(Left, Top, Right, Bottom); + SetCurrentValue(ThicknessProperty, new(Left, Top, Right, Bottom)); } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 0e412a2fa5..2412ea5325 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -35,15 +35,14 @@ namespace Avalonia.Diagnostics.ViewModels public override string Priority => _priority; public override Type AssignedType => _assignedType; - public override string? Value + public override object? Value { - get => ConvertToString(_value); + get => _value; set { try { - var convertedValue = ConvertFromString(value, Property.PropertyType); - _target.SetValue(Property, convertedValue); + _target.SetValue(Property, value); Update(); } catch { } @@ -54,6 +53,7 @@ namespace Avalonia.Diagnostics.ViewModels public override Type? DeclaringType { get; } public override Type PropertyType => _propertyType; + public override bool IsReadonly => Property.IsReadOnly; // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] public override void Update() diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index 895ff41f7b..b7ee1459f7 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -40,16 +40,16 @@ namespace Avalonia.Diagnostics.ViewModels public override Type AssignedType => _assignedType; public override Type PropertyType => _propertyType; + public override bool IsReadonly => !Property.CanWrite; - public override string? Value + public override object? Value { - get => ConvertToString(_value); + get => _value; set { try { - var convertedValue = ConvertFromString(value, Property.PropertyType); - Property.SetValue(_target, convertedValue); + Property.SetValue(_target, value); Update(); } catch { } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index a7faf35769..aa2682e376 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -7,78 +7,21 @@ namespace Avalonia.Diagnostics.ViewModels { internal abstract class PropertyViewModel : ViewModelBase { - private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; - private static readonly Type[] StringParameter = { typeof(string) }; - private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; - public abstract object Key { get; } public abstract string Name { get; } public abstract string Group { get; } public abstract Type AssignedType { get; } public abstract Type? DeclaringType { get; } - public abstract string? Value { get; set; } + public abstract object? Value { get; set; } public abstract string Priority { get; } public abstract bool? IsAttached { get; } public abstract void Update(); public abstract Type PropertyType { get; } - public string Type => PropertyType == AssignedType - ? PropertyType.GetTypeName() - : $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}"; - - - protected static string? ConvertToString(object? value) - { - if (value is null) - { - return "(null)"; - } - - var converter = TypeDescriptor.GetConverter(value); - - //CollectionConverter does not deliver any important information. It just displays "(Collection)". - if (!converter.CanConvertTo(typeof(string)) || - converter.GetType() == typeof(CollectionConverter)) - { - return value.ToString() ?? "(null)"; - } - - return converter.ConvertToString(value); - } - - private static object? InvokeParse(string s, Type targetType) - { - var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); - - if (method != null) - { - return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture }); - } - - method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null); - - if (method != null) - { - return method.Invoke(null, new object[] { s }); - } - - throw new InvalidCastException("Unable to convert value."); - } - - protected static object? ConvertFromString(string? s, Type targetType) - { - if (s is null) - { - return null; - } - - var converter = TypeDescriptor.GetConverter(targetType); - if (converter.CanConvertFrom(typeof(string))) - { - return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); - } + public string Type => PropertyType == AssignedType ? + PropertyType.GetTypeName() : + $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}"; - return InvokeParse(s, targetType); - } + public abstract bool IsReadonly { get; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs new file mode 100644 index 0000000000..9425989096 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs @@ -0,0 +1,57 @@ +using System; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using Avalonia.Reactive; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal static class ReactiveExtensions + { + public static IObservable GetObservable( + this TOwner vm, + Expression> property, + bool fireImmediately = true) + where TOwner : INotifyPropertyChanged + { + return Observable.Create(o => + { + var propertyInfo = GetPropertyInfo(property); + + void Fire() + { + o.OnNext((TValue) propertyInfo.GetValue(vm)!); + } + + void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == propertyInfo.Name) + { + Fire(); + } + } + + if (fireImmediately) + { + Fire(); + } + + vm.PropertyChanged += OnPropertyChanged; + + return Disposable.Create(() => vm.PropertyChanged -= OnPropertyChanged); + }); + } + + private static PropertyInfo GetPropertyInfo(this Expression> property) + { + if (property.Body is UnaryExpression unaryExpression) + { + return (PropertyInfo)((MemberExpression)unaryExpression.Operand).Member; + } + + var memExpr = (MemberExpression)property.Body; + + return (PropertyInfo)memExpr.Member; + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 2a69798c6c..97f195c91b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -60,7 +60,11 @@ DoubleTapped="PropertiesGrid_OnDoubleTapped"> - + + + + + ("console"); _consoleSplitter = this.GetControl("consoleSplitter"); _rootGrid = this.GetControl("rootGrid"); @@ -58,7 +57,7 @@ namespace Avalonia.Diagnostics.Views AvaloniaXamlLoader.Load(this); } - private void PreviewKeyDown(object? sender, KeyEventArgs e) + private void PreviewKeyUp(object? sender, KeyEventArgs e) { if (e.Key == Key.Escape) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 748c2cc313..486df860c3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -15,6 +15,7 @@ + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs new file mode 100644 index 0000000000..6e7729a350 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs @@ -0,0 +1,404 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Diagnostics.Controls; +using Avalonia.Diagnostics.ViewModels; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Markup.Xaml.Converters; +using Avalonia.Media; +using Avalonia.Reactive; + +namespace Avalonia.Diagnostics.Views +{ + internal class PropertyValueEditorView : UserControl + { + private static readonly Geometry ImageIcon = Geometry.Parse( + "M12.25 6C8.79822 6 6 8.79822 6 12.25V35.75C6 37.1059 6.43174 38.3609 7.16525 39.3851L21.5252 25.0251C22.8921 23.6583 25.1081 23.6583 26.475 25.0251L40.8348 39.385C41.5683 38.3608 42 37.1058 42 35.75V12.25C42 8.79822 39.2018 6 35.75 6H12.25ZM34.5 17.5C34.5 19.7091 32.7091 21.5 30.5 21.5C28.2909 21.5 26.5 19.7091 26.5 17.5C26.5 15.2909 28.2909 13.5 30.5 13.5C32.7091 13.5 34.5 15.2909 34.5 17.5ZM39.0024 41.0881L24.7072 26.7929C24.3167 26.4024 23.6835 26.4024 23.293 26.7929L8.99769 41.0882C9.94516 41.6667 11.0587 42 12.25 42H35.75C36.9414 42 38.0549 41.6666 39.0024 41.0881Z"); + + private static readonly Geometry GeometryIcon = Geometry.Parse( + "M23.25 15.5H30.8529C29.8865 8.99258 24.2763 4 17.5 4C10.0442 4 4 10.0442 4 17.5C4 24.2763 8.99258 29.8865 15.5 30.8529V23.25C15.5 18.9698 18.9698 15.5 23.25 15.5ZM23.25 18C20.3505 18 18 20.3505 18 23.25V38.75C18 41.6495 20.3505 44 23.25 44H38.75C41.6495 44 44 41.6495 44 38.75V23.25C44 20.3505 41.6495 18 38.75 18H23.25Z"); + + private static readonly ColorToBrushConverter Color2Brush = new(); + + private readonly CompositeDisposable _cleanup = new(); + private PropertyViewModel? Property => (PropertyViewModel?)DataContext; + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + Content = UpdateControl(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _cleanup.Clear(); + } + + private static bool ImplementsInterface(Type type) + { + var interfaceType = typeof(TInterface); + return type == interfaceType || interfaceType.IsAssignableFrom(type); + } + + private Control? UpdateControl() + { + _cleanup.Clear(); + + if (Property?.PropertyType is not { } propertyType) + return null; + + TControl CreateControl(AvaloniaProperty valueProperty, + IValueConverter? converter = null, + Action? init = null, + AvaloniaProperty? readonlyProperty = null) + where TControl : Control, new() + { + var control = new TControl(); + + init?.Invoke(control); + + control.Bind(valueProperty, + new Binding(nameof(Property.Value), BindingMode.TwoWay) + { + Source = Property, + Converter = converter ?? new ValueConverter(), + ConverterParameter = propertyType + }).DisposeWith(_cleanup); + + if (readonlyProperty != null) + { + control[readonlyProperty] = Property.IsReadonly; + } + else + { + control.IsEnabled = !Property.IsReadonly; + } + + return control; + } + + if (propertyType == typeof(bool)) + return CreateControl(ToggleButton.IsCheckedProperty); + + //TODO: Infinity, NaN not working with NumericUpDown + if (propertyType.IsPrimitive && propertyType != typeof(float) && propertyType != typeof(double)) + return CreateControl( + NumericUpDown.ValueProperty, + new ValueToDecimalConverter(), + init: n => + { + n.Increment = 1; + n.NumberFormat = new NumberFormatInfo { NumberDecimalDigits = 0 }; + n.ParsingNumberStyle = NumberStyles.Integer; + }, + readonlyProperty: NumericUpDown.IsReadOnlyProperty); + + if (propertyType == typeof(Color)) + { + var el = new Ellipse { Width = 12, Height = 12, VerticalAlignment = VerticalAlignment.Center }; + + el.Bind( + Shape.FillProperty, + new Binding(nameof(Property.Value)) { Source = Property, Converter = Color2Brush }) + .DisposeWith(_cleanup); + + var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; + + tbl.Bind( + TextBlock.TextProperty, + new Binding(nameof(Property.Value)) { Source = Property }) + .DisposeWith(_cleanup); + + var sp = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 2, + Children = { el, tbl }, + Background = Brushes.Transparent, + Cursor = new Cursor(StandardCursorType.Hand), + IsEnabled = !Property.IsReadonly + }; + + var cv = new ColorView(); + + cv.Bind( + ColorView.ColorProperty, + new Binding(nameof(Property.Value), BindingMode.TwoWay) + { + Source = Property, Converter = Color2Brush + }) + .DisposeWith(_cleanup); + + FlyoutBase.SetAttachedFlyout(sp, new Flyout { Content = cv }); + + sp.PointerPressed += (_, _) => FlyoutBase.ShowAttachedFlyout(sp); + + return sp; + } + + if (ImplementsInterface(propertyType)) + return CreateControl(BrushEditor.BrushProperty); + + var isImage = ImplementsInterface(propertyType); + var isGeometry = propertyType == typeof(Geometry); + + if (isImage || isGeometry) + { + var valueObservable = Property.GetObservable(x => x.Value); + var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; + + tbl.Bind(TextBlock.TextProperty, + valueObservable.Select( + value => value switch + { + IImage img => $"{img.Size.Width} x {img.Size.Height}", + Geometry geom => $"{geom.Bounds.Width} x {geom.Bounds.Height}", + _ => "(null)" + })) + .DisposeWith(_cleanup); + + var sp = new StackPanel + { + Background = Brushes.Transparent, + Orientation = Orientation.Horizontal, + Spacing = 2, + Children = + { + new Path + { + Data = isImage ? ImageIcon : GeometryIcon, + Fill = Brushes.Gray, + Width = 12, + Height = 12, + Stretch = Stretch.Uniform, + VerticalAlignment = VerticalAlignment.Center + }, + tbl + } + }; + + if (isImage) + { + var previewImage = new Image { Stretch = Stretch.Uniform, Width = 300, Height = 300 }; + + previewImage + .Bind(Image.SourceProperty, valueObservable) + .DisposeWith(_cleanup); + + ToolTip.SetTip(sp, previewImage); + } + else + { + var previewShape = new Path + { + Stretch = Stretch.Uniform, + Fill = Brushes.White, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center + }; + + previewShape + .Bind(Path.DataProperty, valueObservable) + .DisposeWith(_cleanup); + + ToolTip.SetTip(sp, new Border { Child = previewShape, Width = 300, Height = 300 }); + } + + return sp; + } + + if (propertyType.IsEnum) + return CreateControl( + SelectingItemsControl.SelectedItemProperty, init: c => + { + c.Items = Enum.GetValues(propertyType); + }); + + var tb = CreateControl( + CommitTextBox.CommittedTextProperty, + new TextToValueConverter(), + t => + { + t.Watermark = "(null)"; + }, + readonlyProperty: TextBox.IsReadOnlyProperty); + + tb.IsReadOnly |= propertyType == typeof(object) || + !StringConversionHelper.CanConvertFromString(propertyType); + + if (!tb.IsReadOnly) + { + tb.GetObservable(TextBox.TextProperty).Subscribe(t => + { + try + { + if (t != null) + { + StringConversionHelper.FromString(t, propertyType); + } + + DataValidationErrors.ClearErrors(tb); + } + catch (Exception ex) + { + DataValidationErrors.SetError(tb, ex.GetBaseException()); + } + }).DisposeWith(_cleanup); + } + + return tb; + } + + //HACK: ValueConverter that skips first target update + //TODO: Would be nice to have some kind of "InitialBindingValue" option on TwoWay bindings to control + //if the first value comes from the source or target + private class ValueConverter : IValueConverter + { + private bool _firstUpdate = true; + + object? IValueConverter.Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Convert(value, targetType, parameter, culture); + } + + object? IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (_firstUpdate) + { + _firstUpdate = false; + + return BindingOperations.DoNothing; + } + + //Note: targetType provided by Converter is simply "object" + return ConvertBack(value, (Type)parameter!, parameter, culture); + } + + protected virtual object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value; + } + + protected virtual object? ConvertBack(object? value, Type targetType, object? parameter, + CultureInfo culture) + { + return value; + } + } + + private static class StringConversionHelper + { + private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; + private static readonly Type[] StringParameter = { typeof(string) }; + private static readonly Type[] StringFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; + + public static bool CanConvertFromString(Type type) + { + var converter = TypeDescriptor.GetConverter(type); + + if (converter.CanConvertFrom(typeof(string))) + return true; + + return GetParseMethod(type, out _) != null; + } + + public static string? ToString(object o) + { + var converter = TypeDescriptor.GetConverter(o); + + //CollectionConverter does not deliver any important information. It just displays "(Collection)". + if (!converter.CanConvertTo(typeof(string)) || + converter.GetType() == typeof(CollectionConverter)) + return o.ToString(); + + return converter.ConvertToInvariantString(o); + } + + public static object? FromString(string str, Type type) + { + var converter = TypeDescriptor.GetConverter(type); + + return converter.CanConvertFrom(typeof(string)) ? + converter.ConvertFrom(null, CultureInfo.InvariantCulture, str) : + InvokeParse(str, type); + } + + private static object? InvokeParse(string s, Type targetType) + { + var m = GetParseMethod(targetType, out var hasFormat); + + if (m == null) + throw new InvalidOperationException(); + + return m.Invoke(null, + hasFormat ? + new object[] { s, CultureInfo.InvariantCulture } : + new object[] { s }); + } + + private static MethodInfo? GetParseMethod(Type type, out bool hasFormat) + { + var m = type.GetMethod("Parse", PublicStatic, null, StringFormatProviderParameters, null); + + if (m != null) + { + hasFormat = true; + + return m; + } + + hasFormat = false; + + return type.GetMethod("Parse", PublicStatic, null, StringParameter, null); + } + } + + private sealed class ValueToDecimalConverter : ValueConverter + { + protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return System.Convert.ToDecimal(value); + } + + protected override object? ConvertBack(object? value, Type targetType, object? parameter, + CultureInfo culture) + { + return System.Convert.ChangeType(value, targetType); + } + } + + private sealed class TextToValueConverter : ValueConverter + { + protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is null ? null : StringConversionHelper.ToString(value); + } + + protected override object? ConvertBack(object? value, Type targetType, object? parameter, + CultureInfo culture) + { + if (value is not string s) + return null; + + try + { + return StringConversionHelper.FromString(s, targetType); + } + catch + { + return BindingOperations.DoNothing; + } + } + } + } +} diff --git a/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs b/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs new file mode 100644 index 0000000000..842629c923 --- /dev/null +++ b/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs @@ -0,0 +1,13 @@ +namespace Avalonia.Fonts.Inter +{ + public static class AppBuilderExtension + { + public static AppBuilder WithInterFont(this AppBuilder appBuilder) + { + return appBuilder.ConfigureFonts(fontManager => + { + fontManager.AddFontCollection(new InterFontCollection()); + }); + } + } +} diff --git a/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj b/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj index c81a13558c..c18c07d347 100644 --- a/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj +++ b/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Avalonia.Fonts.Inter/InterFontCollection.cs b/src/Avalonia.Fonts.Inter/InterFontCollection.cs new file mode 100644 index 0000000000..0ed1779a03 --- /dev/null +++ b/src/Avalonia.Fonts.Inter/InterFontCollection.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Media.Fonts; + +namespace Avalonia.Fonts.Inter +{ + public sealed class InterFontCollection : EmbeddedFontCollection + { + public InterFontCollection() : base( + new Uri("fonts:Inter", UriKind.Absolute), + new Uri("avares://Avalonia.Fonts.Inter/Assets", UriKind.Absolute)) + { + } + } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 31aaebcdc7..f8100d3832 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -118,7 +118,7 @@ namespace Avalonia.Headless public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { - return new HeadlessGeometryStub(new Rect(glyphRun.Size)); + return new HeadlessGeometryStub(glyphRun.Bounds); } public IGlyphRunImpl CreateGlyphRun( @@ -132,7 +132,7 @@ namespace Avalonia.Headless class HeadlessGlyphRunStub : IGlyphRunImpl { - public Size Size => new Size(8, 12); + public Rect Bounds => new Rect(new Size(8, 12)); public Point BaselineOrigin => new Point(0, 8); diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 46e3515d11..ee4cd5af98 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -84,6 +84,14 @@ namespace Avalonia.Headless public FontSimulations FontSimulations { get; } + public string FamilyName => "Arial"; + + public FontWeight Weight => FontWeight.Normal; + + public FontStyle Style => FontStyle.Normal; + + public FontStretch Stretch => FontStretch.Normal; + public void Dispose() { } @@ -147,19 +155,28 @@ namespace Avalonia.Headless class HeadlessFontManagerStub : IFontManagerImpl { - public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) + public string GetDefaultFontFamilyName() { - return new HeadlessGlyphTypefaceImpl(); + return "Arial"; } - public string GetDefaultFontFamilyName() + public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) { - return "Arial"; + return new string[] { "Arial" }; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface) { - return new List { "Arial" }; + glyphTypeface= new HeadlessGlyphTypefaceImpl(); + + return true; + } + + public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + { + glyphTypeface = new HeadlessGlyphTypefaceImpl(); + + return true; } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 82e48851b5..c19a4f5c09 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -3,7 +3,7 @@ xmlns:sys="using:System" xmlns:converters="using:Avalonia.Controls.Converters"> - avares://Avalonia.Fonts.Inter/Assets#Inter, $Default + fonts:Inter#Inter, $Default 14 diff --git a/src/Avalonia.Themes.Simple/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml index 0a06927034..38b122d8b2 100644 --- a/src/Avalonia.Themes.Simple/Accents/Base.xaml +++ b/src/Avalonia.Themes.Simple/Accents/Base.xaml @@ -76,7 +76,7 @@ - avares://Avalonia.Fonts.Inter/Assets#Inter, $Default + fonts://Inter#Inter, $Default #CC119EDA #99119EDA #66119EDA diff --git a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml index 8428e3aae7..0c7095f2f5 100644 --- a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml +++ b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml @@ -92,6 +92,7 @@ + diff --git a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs new file mode 100644 index 0000000000..30f80ba27c --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Browser.Interop; +using Avalonia.Controls.Platform; +using static Avalonia.Controls.Platform.IInsetsManager; + +namespace Avalonia.Browser +{ + internal class BrowserInsetsManager : IInsetsManager + { + public bool? IsSystemBarVisible + { + get + { + return DomHelper.IsFullscreen(); + } + set + { + DomHelper.SetFullscreen(!value ?? false); + } + } + + public bool DisplayEdgeToEdge { get; set; } + + public event EventHandler? SafeAreaChanged; + + public Thickness SafeAreaPadding + { + get + { + var padding = DomHelper.GetSafeAreaPadding(); + + return new Thickness(padding[0], padding[1], padding[2], padding[3]); + } + } + + public void NotifySafeAreaPaddingChanged() + { + SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); + } + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index 1bf4636f61..7c5418dbeb 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -31,6 +31,7 @@ namespace Avalonia.Browser private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; private readonly ISystemNavigationManagerImpl _systemNavigationManager; + private readonly IInsetsManager? _insetsManager; public BrowserTopLevelImpl(AvaloniaView avaloniaView) { @@ -40,9 +41,12 @@ namespace Avalonia.Browser AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); _touchDevice = new TouchDevice(); _penDevice = new PenDevice(); + + _insetsManager = new BrowserInsetsManager(); _nativeControlHost = _avaloniaView.GetNativeControlHostImpl(); _storageProvider = new BrowserStorageProvider(); _systemNavigationManager = new BrowserSystemNavigationManagerImpl(); + } public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; @@ -69,6 +73,8 @@ namespace Avalonia.Browser } Resized?.Invoke(newSize, PlatformResizeReason.User); + + (_insetsManager as BrowserInsetsManager)?.NotifySafeAreaPaddingChanged(); } } @@ -271,6 +277,11 @@ namespace Avalonia.Browser return _nativeControlHost; } + if (featureType == typeof(IInsetsManager)) + { + return _insetsManager; + } + return null; } } diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs index d1133b8916..c1811bb117 100644 --- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs @@ -11,6 +11,15 @@ internal static partial class DomHelper [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)] public static partial JSObject CreateAvaloniaHost(JSObject element); + [JSImport("AvaloniaDOM.isFullscreen", AvaloniaModule.MainModuleName)] + public static partial bool IsFullscreen(); + + [JSImport("AvaloniaDOM.setFullscreen", AvaloniaModule.MainModuleName)] + public static partial JSObject SetFullscreen(bool isFullscreen); + + [JSImport("AvaloniaDOM.getSafeAreaPadding", AvaloniaModule.MainModuleName)] + public static partial byte[] GetSafeAreaPadding(); + [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)] public static partial void AddCssClass(JSObject element, string className); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts index b99f8e7907..d9790f69e9 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts @@ -84,4 +84,26 @@ export class AvaloniaDOM { inputElement }; } + + public static isFullscreen(): boolean { + return document.fullscreenElement != null; + } + + public static async setFullscreen(isFullscreen: boolean) { + if (isFullscreen) { + const doc = document.documentElement; + await doc.requestFullscreen(); + } else { + await document.exitFullscreen(); + } + } + + public static getSafeAreaPadding(): number[] { + const top = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sat")); + const bottom = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sab")); + const left = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sal")); + const right = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sar")); + + return [left, top, bottom, right]; + } } diff --git a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs index ba321db144..cfb1b508d9 100644 --- a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs @@ -104,9 +104,7 @@ namespace Avalonia.Data CultureInfo.CurrentCulture); } - // Use LocalValue priority here, as TemplatedParent doesn't make sense on controls - // that aren't template children. - templatedParent.SetValue(Property, value, BindingPriority.LocalValue); + templatedParent.SetCurrentValue(Property, value); } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index af0231579c..e3e2f664c3 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -516,7 +516,7 @@ namespace Avalonia.Skia return; } - using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Bounds.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 90a2f9169b..29e5687423 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -1,6 +1,7 @@ using System; -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; @@ -16,14 +17,14 @@ namespace Avalonia.Skia return SKTypeface.Default.FamilyName; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) { if (checkForUpdates) { _skFontManager = SKFontManager.CreateDefault(); } - return _skFontManager.FontFamilies; + return _skFontManager.GetFontFamilies(); } [ThreadStatic] private static string[]? t_languageTagBuffer; @@ -95,72 +96,58 @@ namespace Avalonia.Skia return false; } - public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - SKTypeface? skTypeface = null; + glyphTypeface = null; - if(typeface.FontFamily.Key is not null) - { - var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); - - skTypeface = fontCollection.Get(typeface); + var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch, + (SKFontStyleSlant)style); - if (skTypeface is null && !typeface.FontFamily.FamilyNames.HasFallbacks) - { - throw new InvalidOperationException( - $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); - } - } + var skTypeface = _skFontManager.MatchFamily(familyName, fontStyle); if (skTypeface is null) { - var defaultName = SKTypeface.Default.FamilyName; - - var fontStyle = new SKFontStyle((SKFontStyleWeight)typeface.Weight, (SKFontStyleWidth)typeface.Stretch, - (SKFontStyleSlant)typeface.Style); - - foreach (var familyName in typeface.FontFamily.FamilyNames) - { - if(familyName == FontFamily.DefaultFontFamilyName) - { - continue; - } - - skTypeface = _skFontManager.MatchFamily(familyName, fontStyle); - - if (skTypeface is null || defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal)) - { - continue; - } - - break; - } - - // MatchTypeface can return "null" if matched typeface wasn't found for the style - // Fallback to the default typeface and styles instead. - skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle) - ?? SKTypeface.Default; + return false; } - - if (skTypeface == null) + + //MatchFamily can return a font other than we requested so we have to verify we got the expected. + if (!skTypeface.FamilyName.ToLower(CultureInfo.InvariantCulture).Equals(familyName.ToLower(CultureInfo.InvariantCulture), StringComparison.Ordinal)) { - throw new InvalidOperationException( - $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); + return false; } var fontSimulations = FontSimulations.None; - if((int)typeface.Weight >= 600 && !skTypeface.IsBold) + if ((int)weight >= 600 && !skTypeface.IsBold) { fontSimulations |= FontSimulations.Bold; } - if(typeface.Style == FontStyle.Italic && !skTypeface.IsItalic) + if (style == FontStyle.Italic && !skTypeface.IsItalic) { fontSimulations |= FontSimulations.Oblique; } - return new GlyphTypefaceImpl(skTypeface, fontSimulations); + glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations); + + return true; + } + + public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + var skTypeface = SKTypeface.FromStream(stream); + + if (skTypeface != null) + { + glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); + + return true; + } + + glyphTypeface = null; + + return false; } } } diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 079eea7bef..0521e238f3 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.Skia { TextBlob = textBlob ?? throw new ArgumentNullException(nameof(textBlob)); - Size = size; + Bounds = new Rect(new Point(baselineOrigin.X, 0), size); BaselineOrigin = baselineOrigin; } @@ -21,7 +21,7 @@ namespace Avalonia.Skia /// public SKTextBlob TextBlob { get; } - public Size Size { get; } + public Rect Bounds { get; } public Point BaselineOrigin { get; } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index 3093455bec..43e10e3e96 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -51,6 +51,12 @@ namespace Avalonia.Skia GlyphCount = Typeface.GlyphCount; FontSimulations = fontSimulations; + + Weight = (FontWeight)Typeface.FontWeight; + + Style = Typeface.FontSlant.ToAvalonia(); + + Stretch = (FontStretch)Typeface.FontStyle.Width; } public Face Face { get; } @@ -67,6 +73,14 @@ namespace Avalonia.Skia public int GlyphCount { get; } + public string FamilyName => Typeface.FamilyName; + + public FontWeight Weight { get; } + + public FontStyle Style { get; } + + public FontStretch Stretch { get; } + public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) { metrics = default; diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs deleted file mode 100644 index 9ee17a09d6..0000000000 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Media; -using SkiaSharp; - -namespace Avalonia.Skia -{ - internal class SKTypefaceCollection - { - private readonly ConcurrentDictionary _typefaces = new(); - - public void AddTypeface(Typeface key, SKTypeface typeface) - { - _typefaces.TryAdd(key, typeface); - } - - public SKTypeface? Get(Typeface typeface) - { - return GetNearestMatch(typeface); - } - - private SKTypeface? GetNearestMatch(Typeface key) - { - if (_typefaces.Count == 0) - { - return null; - } - - if (_typefaces.TryGetValue(key, out var typeface)) - { - return typeface; - } - - if(key.Style != FontStyle.Normal) - { - key = new Typeface(key.FontFamily, FontStyle.Normal, key.Weight, key.Stretch); - } - - if(key.Stretch != FontStretch.Normal) - { - if(TryFindStretchFallback(key, out typeface)) - { - return typeface; - } - - if(key.Weight != FontWeight.Normal) - { - if (TryFindStretchFallback(new Typeface(key.FontFamily, key.Style, FontWeight.Normal, key.Stretch), out typeface)) - { - return typeface; - } - } - - key = new Typeface(key.FontFamily, key.Style, key.Weight, FontStretch.Normal); - } - - if(TryFindWeightFallback(key, out typeface)) - { - return typeface; - } - - if (TryFindStretchFallback(key, out typeface)) - { - return typeface; - } - - //Nothing was found so we try some regular typeface. - if (_typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface)) - { - return typeface; - } - - SKTypeface? skTypeface = null; - - foreach(var pair in _typefaces) - { - skTypeface = pair.Value; - - if (skTypeface.FamilyName.Contains(key.FontFamily.Name)) - { - return skTypeface; - } - } - - return skTypeface; - } - - private bool TryFindStretchFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface) - { - typeface = null; - var stretch = (int)key.Stretch; - - if (stretch < 5) - { - for (var i = 0; stretch + i < 9; i++) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch + i)), out typeface)) - { - return true; - } - } - } - else - { - for (var i = 0; stretch - i > 1; i++) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch - i)), out typeface)) - { - return true; - } - } - } - - return false; - } - - private bool TryFindWeightFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface) - { - typeface = null; - var weight = (int)key.Weight; - - //If the target weight given is between 400 and 500 inclusive - if (weight >= 400 && weight <= 500) - { - //Look for available weights between the target and 500, in ascending order. - for (var i = 0; weight + i <= 500; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight - i >= 100; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights greater than 500, in ascending order. - for (var i = 0; weight + i <= 900; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) - { - return true; - } - } - } - - //If a weight less than 400 is given, look for available weights less than the target, in descending order. - if (weight < 400) - { - for (var i = 0; weight - i >= 100; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight + i <= 900; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) - { - return true; - } - } - } - - //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order. - if (weight > 500) - { - for (var i = 0; weight + i <= 900; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight - i >= 100; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) - { - return true; - } - } - } - - return false; - } - } -} diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs deleted file mode 100644 index d064f49ae4..0000000000 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Avalonia.Media; -using Avalonia.Media.Fonts; -using Avalonia.Platform; -using SkiaSharp; - -namespace Avalonia.Skia -{ - internal static class SKTypefaceCollectionCache - { - private static readonly ConcurrentDictionary s_cachedCollections; - - static SKTypefaceCollectionCache() - { - s_cachedCollections = new ConcurrentDictionary(); - } - - /// - /// Gets the or add typeface collection. - /// - /// The font family. - /// - public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily) - { - return s_cachedCollections.GetOrAdd(fontFamily, CreateCustomFontCollection); - } - - /// - /// Creates the custom font collection. - /// - /// The font family. - /// - private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily) - { - var typeFaceCollection = new SKTypefaceCollection(); - - if (fontFamily.Key is not { } fontFamilyKey) - { - return typeFaceCollection; - } - - var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamilyKey); - - var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - - foreach (var asset in fontAssets) - { - var assetStream = assetLoader.Open(asset); - - if (assetStream == null) - throw new InvalidOperationException("Asset could not be loaded."); - - var typeface = SKTypeface.FromStream(assetStream); - - if (typeface == null) - throw new InvalidOperationException("Typeface could not be loaded."); - - if (!typeface.FamilyName.Contains(fontFamily.Name)) - { - continue; - } - - var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(), - (FontWeight)typeface.FontWeight, (FontStretch)typeface.FontWidth); - - typeFaceCollection.AddTypeface(key, typeface); - } - - return typeFaceCollection; - } - } -} diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs index 4663a6561f..b60962a091 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs @@ -1,11 +1,10 @@ using System.Collections.Generic; -using Avalonia.Platform; using SharpDX; using SharpDX.DirectWrite; namespace Avalonia.Direct2D1.Media { - using System; + using System.IO; internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader { @@ -18,19 +17,15 @@ namespace Avalonia.Direct2D1.Media /// /// The factory. /// - public DWriteResourceFontLoader(Factory factory, IEnumerable fontAssets) + public DWriteResourceFontLoader(Factory factory, Stream[] fontAssets) { var factory1 = factory; - var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - foreach (var asset in fontAssets) { - var assetStream = assetLoader.Open(asset); - - var dataStream = new DataStream((int)assetStream.Length, true, true); + var dataStream = new DataStream((int)asset.Length, true, true); - assetStream.CopyTo(dataStream); + asset.CopyTo(dataStream); dataStream.Position = 0; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index 792bf2d0be..ad2ede3a91 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -6,6 +6,9 @@ using FontFamily = Avalonia.Media.FontFamily; using FontStyle = SharpDX.DirectWrite.FontStyle; using FontWeight = SharpDX.DirectWrite.FontWeight; using FontStretch = SharpDX.DirectWrite.FontStretch; +using Avalonia.Platform; +using System.Linq; +using System; namespace Avalonia.Direct2D1.Media { @@ -53,9 +56,15 @@ namespace Avalonia.Direct2D1.Media private static FontCollection CreateFontCollection(FontFamilyKey key) { - var assets = FontFamilyLoader.LoadFontAssets(key); + var source = key.BaseUri != null ? new Uri(key.BaseUri, key.Source) : key.Source; - var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets); + var assets = FontFamilyLoader.LoadFontAssets(source); + + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + var fontAssets = assets.Select(x => assetLoader.Open(x)).ToArray(); + + var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, fontAssets); return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index f9b5953e3f..87fa963871 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -387,7 +387,7 @@ namespace Avalonia.Direct2D1.Media /// The glyph run. public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { - using (var brush = CreateBrush(foreground, glyphRun.Item.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Item.Bounds.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index b98ed3ffe6..ec2f6385da 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using Avalonia.Media; using Avalonia.Platform; -using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; using FontStretch = Avalonia.Media.FontStretch; using FontStyle = Avalonia.Media.FontStyle; @@ -18,7 +18,7 @@ namespace Avalonia.Direct2D1.Media return "Segoe UI"; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -62,9 +62,56 @@ namespace Avalonia.Direct2D1.Media return false; } - public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) { - return new GlyphTypefaceImpl(typeface); + var systemFonts = Direct2D1FontCollectionCache.InstalledFontCollection; + + if (familyName == FontFamily.DefaultFontFamilyName) + { + familyName = "Segoe UI"; + } + + if (systemFonts.FindFamilyName(familyName, out var index)) + { + var font = systemFonts.GetFontFamily(index).GetFirstMatchingFont( + (SharpDX.DirectWrite.FontWeight)weight, + (SharpDX.DirectWrite.FontStretch)stretch, + (SharpDX.DirectWrite.FontStyle)style); + + glyphTypeface = new GlyphTypefaceImpl(font); + + return true; + } + + glyphTypeface = null; + + return false; + } + + public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + { + var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, new[] { stream }); + + var fontCollection = new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); + + if (fontCollection.FontFamilyCount > 0) + { + var fontFamily = fontCollection.GetFontFamily(0); + + if (fontFamily.FontCount > 0) + { + var font = fontFamily.GetFont(0); + + glyphTypeface = new GlyphTypefaceImpl(font); + + return true; + } + } + + glyphTypeface = null; + + return false; } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 446db47d92..2e7a4b67f6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -9,12 +9,12 @@ namespace Avalonia.Direct2D1.Media { public GlyphRunImpl(GlyphRun glyphRun, Size size, Point baselineOrigin) { - Size = size; + Bounds = new Rect(new Point(baselineOrigin.X, 0), size); BaselineOrigin = baselineOrigin; GlyphRun = glyphRun; } - public Size Size { get; } + public Rect Bounds{ get; } public Point BaselineOrigin { get; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs index e4988322e7..01add0f0cb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs @@ -12,9 +12,9 @@ namespace Avalonia.Direct2D1.Media { private bool _isDisposed; - public GlyphTypefaceImpl(Typeface typeface) + public GlyphTypefaceImpl(SharpDX.DirectWrite.Font font) { - DWFont = Direct2D1FontCollectionCache.GetFont(typeface); + DWFont = font; FontFace = new FontFace(DWFont).QueryInterface(); @@ -48,6 +48,14 @@ namespace Avalonia.Direct2D1.Media StrikethroughThickness = strikethroughThickness, IsFixedPitch = FontFace.IsMonospacedFont }; + + FamilyName = DWFont.FontFamily.FamilyNames.GetString(0); + + Weight = (Avalonia.Media.FontWeight)DWFont.Weight; + + Style = (Avalonia.Media.FontStyle)DWFont.Style; + + Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch; } private Blob GetTable(Face face, Tag tag) @@ -83,6 +91,14 @@ namespace Avalonia.Direct2D1.Media public FontSimulations FontSimulations => FontSimulations.None; + public string FamilyName { get; } + + public Avalonia.Media.FontWeight Weight { get; } + + public Avalonia.Media.FontStyle Style { get; } + + public Avalonia.Media.FontStretch Stretch { get; } + /// public ushort GetGlyph(uint codepoint) { diff --git a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs index f5b2d462ab..1ab4c0d2dc 100644 --- a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs +++ b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs @@ -4,7 +4,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input { - static class KeyInterop + public static class KeyInterop { private static readonly Dictionary s_virtualKeyFromKey = new Dictionary { diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs index b605d82541..18ccbad692 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs @@ -40,10 +40,12 @@ namespace Avalonia.iOS var view = new AvaloniaView(); lifetime.View = view; - Window.RootViewController = new UIViewController + var controller = new DefaultAvaloniaViewController { View = view }; + Window.RootViewController = controller; + view.InitWithController(controller); }); builder.SetupWithLifetime(lifetime); diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 2d6b93f818..09721ad181 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -16,6 +16,7 @@ using Foundation; using ObjCRuntime; using OpenGLES; using UIKit; +using IInsetsManager = Avalonia.Controls.Platform.IInsetsManager; namespace Avalonia.iOS { @@ -26,6 +27,7 @@ namespace Avalonia.iOS private EmbeddableControlRoot _topLevel; private TouchHandler _touches; private ITextInputMethodClient _client; + private IAvaloniaViewController _controller; public AvaloniaView() { @@ -48,10 +50,13 @@ namespace Avalonia.iOS MultipleTouchEnabled = true; } + /// public override bool CanBecomeFirstResponder => true; + /// public override bool CanResignFirstResponder => true; + /// public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection) { base.TraitCollectionDidChange(previousTraitCollection); @@ -60,6 +65,7 @@ namespace Avalonia.iOS settings?.TraitCollectionDidChange(); } + /// public override void TintColorDidChange() { base.TintColorDidChange(); @@ -68,18 +74,31 @@ namespace Avalonia.iOS settings?.TraitCollectionDidChange(); } + public void InitWithController(TController controller) + where TController : UIViewController, IAvaloniaViewController + { + _controller = controller; + _topLevelImpl._insetsManager.InitWithController(controller); + } + internal class TopLevelImpl : ITopLevelImpl { private readonly AvaloniaView _view; private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; + internal readonly InsetsManager _insetsManager; public AvaloniaView View => _view; public TopLevelImpl(AvaloniaView view) { _view = view; - _nativeControlHost = new NativeControlHostImpl(_view); + _nativeControlHost = new NativeControlHostImpl(view); _storageProvider = new IOSStorageProvider(view); + _insetsManager = new InsetsManager(view); + _insetsManager.DisplayEdgeToEdgeChanged += (sender, b) => + { + view._topLevel.Padding = b ? default : _insetsManager.SafeAreaPadding; + }; } public void Dispose() @@ -141,17 +160,14 @@ namespace Avalonia.iOS public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { // TODO adjust status bar depending on full screen mode. - if (OperatingSystem.IsIOSVersionAtLeast(13)) + if (OperatingSystem.IsIOSVersionAtLeast(13) && _view._controller is not null) { - var uiStatusBarStyle = themeVariant switch + _view._controller.PreferredStatusBarStyle = themeVariant switch { PlatformThemeVariant.Light => UIStatusBarStyle.DarkContent, PlatformThemeVariant.Dark => UIStatusBarStyle.LightContent, - _ => throw new ArgumentOutOfRangeException(nameof(themeVariant), themeVariant, null) + _ => UIStatusBarStyle.Default }; - - // Consider using UIViewController.PreferredStatusBarStyle in the future. - UIApplication.SharedApplication.SetStatusBarStyle(uiStatusBarStyle, true); } } @@ -175,6 +191,11 @@ namespace Avalonia.iOS return _nativeControlHost; } + if (featureType == typeof(IInsetsManager)) + { + return _insetsManager; + } + return null; } } diff --git a/src/iOS/Avalonia.iOS/InsetsManager.cs b/src/iOS/Avalonia.iOS/InsetsManager.cs new file mode 100644 index 0000000000..62e560ddf9 --- /dev/null +++ b/src/iOS/Avalonia.iOS/InsetsManager.cs @@ -0,0 +1,83 @@ +using System; +using Avalonia.Controls.Platform; +using UIKit; + +namespace Avalonia.iOS; +#nullable enable + +internal class InsetsManager : IInsetsManager +{ + private readonly AvaloniaView _view; + private IAvaloniaViewController? _controller; + private bool _displayEdgeToEdge; + + public InsetsManager(AvaloniaView view) + { + _view = view; + } + + internal void InitWithController(IAvaloniaViewController controller) + { + _controller = controller; + if (_controller is not null) + { + _controller.SafeAreaPaddingChanged += (_, _) => + { + SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); + DisplayEdgeToEdgeChanged?.Invoke(this, _displayEdgeToEdge); + }; + } + } + + public SystemBarTheme? SystemBarTheme + { + get => _controller?.PreferredStatusBarStyle switch + { + UIStatusBarStyle.LightContent => Controls.Platform.SystemBarTheme.Dark, + UIStatusBarStyle.DarkContent => Controls.Platform.SystemBarTheme.Light, + _ => null + }; + set + { + if (_controller != null) + { + _controller.PreferredStatusBarStyle = value switch + { + Controls.Platform.SystemBarTheme.Light => UIStatusBarStyle.DarkContent, + Controls.Platform.SystemBarTheme.Dark => UIStatusBarStyle.LightContent, + null => UIStatusBarStyle.Default, + _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) + }; + } + } + } + + public bool? IsSystemBarVisible + { + get => _controller?.PrefersStatusBarHidden == false; + set + { + if (_controller is not null) + { + _controller.PrefersStatusBarHidden = value == false; + } + } + } + public event EventHandler? SafeAreaChanged; + public event EventHandler? DisplayEdgeToEdgeChanged; + + public bool DisplayEdgeToEdge + { + get => _displayEdgeToEdge; + set + { + if (_displayEdgeToEdge != value) + { + _displayEdgeToEdge = value; + DisplayEdgeToEdgeChanged?.Invoke(this, value); + } + } + } + + public Thickness SafeAreaPadding => _controller?.SafeAreaPadding ?? default; +} diff --git a/src/iOS/Avalonia.iOS/ViewController.cs b/src/iOS/Avalonia.iOS/ViewController.cs new file mode 100644 index 0000000000..42a0949a9c --- /dev/null +++ b/src/iOS/Avalonia.iOS/ViewController.cs @@ -0,0 +1,74 @@ +using System; +using Avalonia.Metadata; +using UIKit; + +namespace Avalonia.iOS; + +[Unstable] +public interface IAvaloniaViewController +{ + UIStatusBarStyle PreferredStatusBarStyle { get; set; } + bool PrefersStatusBarHidden { get; set; } + Thickness SafeAreaPadding { get; } + event EventHandler SafeAreaPaddingChanged; +} + +/// +public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewController +{ + private UIStatusBarStyle? _preferredStatusBarStyle; + private bool? _prefersStatusBarHidden; + + /// + public override void ViewDidLayoutSubviews() + { + base.ViewDidLayoutSubviews(); + var size = View?.Frame.Size ?? default; + var frame = View?.SafeAreaLayoutGuide.LayoutFrame ?? default; + var safeArea = new Thickness(frame.Left, frame.Top, size.Width - frame.Right, size.Height - frame.Bottom); + if (SafeAreaPadding != safeArea) + { + SafeAreaPadding = safeArea; + SafeAreaPaddingChanged?.Invoke(this, EventArgs.Empty); + } + } + + /// + public override bool PrefersStatusBarHidden() + { + return _prefersStatusBarHidden ??= base.PrefersStatusBarHidden(); + } + + /// + public override UIStatusBarStyle PreferredStatusBarStyle() + { + // don't set _preferredStatusBarStyle value if it's null, so we can keep "default" there instead of actual app style. + return _preferredStatusBarStyle ?? base.PreferredStatusBarStyle(); + } + + UIStatusBarStyle IAvaloniaViewController.PreferredStatusBarStyle + { + get => _preferredStatusBarStyle ?? UIStatusBarStyle.Default; + set + { + _preferredStatusBarStyle = value; + SetNeedsStatusBarAppearanceUpdate(); + } + } + + bool IAvaloniaViewController.PrefersStatusBarHidden + { + get => _prefersStatusBarHidden ?? false; // false is default on ios/ipados + set + { + _prefersStatusBarHidden = value; + SetNeedsStatusBarAppearanceUpdate(); + } + } + + /// + public Thickness SafeAreaPadding { get; private set; } + + /// + public event EventHandler SafeAreaPaddingChanged; +} diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index 0a27602604..0c0b487dba 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -396,7 +396,7 @@ public partial class AvaloniaPropertyAnalyzer hostTypeRef = new(_avaloniaObjectType, Location.None); // assume that an attached property applies everywhere until we find its registration } - var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; + var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; // assume that the property is owned by its containing type at the point of assignment, until we find its registration result.SetAssignment(s, new(s.ContainingType, Location.None)); @@ -570,7 +570,7 @@ public partial class AvaloniaPropertyAnalyzer if (_allGetSetMethods.Contains(originalMethod)) { - if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && + if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && GetReferencedProperty(invocation.Arguments[0]) is { } refProp && refProp.description.AssignedTo.TryGetValue(refProp.storageSymbol, out var ownerType) && !DerivesFrom(context.ContainingSymbol.ContainingType, ownerType.Type) && @@ -694,9 +694,14 @@ public partial class AvaloniaPropertyAnalyzer void VerifyAccessor(IMethodSymbol? method, string verb, string methodName) { - if (method == null) + if (method is null) { - context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); + if (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Public || + (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Protected + && avaloniaPropertyStorage.ContainingSymbol.DeclaredAccessibility == Accessibility.Public)) + { + context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); + } } else if (method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility) { diff --git a/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs b/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs index 28fb19e119..894b6578e3 100644 --- a/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs +++ b/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs @@ -9,7 +9,7 @@ namespace Avalonia.Base.UnitTests; public class AssetLoaderTests : IDisposable { - public class MockAssembly : Assembly {} + public class MockAssembly : Assembly { } private const string AssemblyNameWithWhitespace = "Awesome Library"; @@ -50,6 +50,17 @@ public class AssetLoaderTests : IDisposable Assert.Equal(AssemblyNameWithNonAscii, assemblyActual?.FullName); } + [Fact] + public void Invalid_AssemblyName_Should_Yield_Empty_Enumerable() + { + var uri = new Uri($"avares://InvalidAssembly"); + var loader = new AssetLoader(); + + var assemblyActual = loader.GetAssets(uri, null); + + Assert.Empty(assemblyActual); + } + private static IAssemblyDescriptor CreateAssemblyDescriptor(string assemblyName) { var assembly = Mock.Of(); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index 8ad36a583e..c850fbdb08 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -1,6 +1,9 @@ using System; +using Avalonia.Controls; using Avalonia.Data; using Avalonia.Diagnostics; +using Avalonia.Styling; +using Avalonia.UnitTests; using Xunit; using Observable = Avalonia.Reactive.Observable; @@ -275,6 +278,79 @@ namespace Avalonia.Base.UnitTests Assert.Equal("style", target.Foo); } + [Fact] + public void SetCurrent_Value_Persists_When_Toggling_Style_1() + { + var target = new Class1(); + var root = new TestRoot(target) + { + Styles = + { + new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(Class1.BarProperty, "bar") }, + } + } + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + + target.Classes.Add("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bar", target.Bar); + + target.Classes.Remove("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + } + + [Fact] + public void SetCurrent_Value_Persists_When_Toggling_Style_2() + { + var target = new Class1(); + var root = new TestRoot(target) + { + Styles = + { + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Class1.BarProperty, "bar"), + new Setter(Class1.InheritedProperty, "inherited"), + }, + } + } + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + Assert.Equal("inheriteddefault", target.Inherited); + + target.Classes.Add("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bar", target.Bar); + Assert.Equal("inherited", target.Inherited); + + target.Classes.Remove("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + Assert.Equal("inheriteddefault", target.Inherited); + } + private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property) { return target.GetDiagnostic(property).Priority; @@ -285,16 +361,19 @@ namespace Avalonia.Base.UnitTests return target.GetDiagnostic(property).IsOverriddenCurrentValue; } - private class Class1 : AvaloniaObject + private class Class1 : Control { public static readonly StyledProperty FooProperty = AvaloniaProperty.Register(nameof(Foo), "foodefault"); + public static readonly StyledProperty BarProperty = + AvaloniaProperty.Register(nameof(Bar), "bardefault"); public static readonly StyledProperty InheritedProperty = AvaloniaProperty.Register(nameof(Inherited), "inheriteddefault", inherits: true); public static readonly StyledProperty CoercedProperty = AvaloniaProperty.Register(nameof(Coerced), coerce: Coerce); public string Foo => GetValue(FooProperty); + public string Bar => GetValue(BarProperty); public string Inherited => GetValue(InheritedProperty); public double Coerced => GetValue(CoercedProperty); public double CoerceMax { get; set; } = 100; diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs index 11ecac0039..89e609eb10 100644 --- a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs @@ -16,9 +16,11 @@ namespace Avalonia.Base.UnitTests.Media var typeface = new Typeface(fontFamily); - var glyphTypeface = FontManager.Current.GetOrAddGlyphTypeface(typeface); + Assert.True(FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface)); - Assert.Same(glyphTypeface, FontManager.Current.GetOrAddGlyphTypeface(typeface)); + FontManager.Current.TryGetGlyphTypeface(typeface, out var other); + + Assert.Same(glyphTypeface, other); } } diff --git a/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs b/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs index afc25ab88e..82dcd8f4fc 100644 --- a/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs @@ -46,9 +46,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts public void Should_Load_Single_FontAsset() { var source = new Uri(AssetMyFontRegular, UriKind.RelativeOrAbsolute); - var key = new FontFamilyKey(source); - var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var fontAssets = FontFamilyLoader.LoadFontAssets(source); Assert.Single(fontAssets); } @@ -57,9 +56,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts public void Should_Load_Single_FontAsset_Avares_Without_BaseUri() { var source = new Uri(AssetYourFontAvares); - var key = new FontFamilyKey(source); - var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var fontAssets = FontFamilyLoader.LoadFontAssets(source); Assert.Single(fontAssets); } @@ -69,9 +67,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts { var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute); var baseUri = new Uri(AssetLocationAvares); - var key = new FontFamilyKey(source, baseUri); - var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var fontAssets = FontFamilyLoader.LoadFontAssets(new Uri(baseUri, source)); Assert.Single(fontAssets); } @@ -80,9 +77,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts public void Should_Load_Matching_Assets() { var source = new Uri(AssetLocation + ".MyFont*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute); - var key = new FontFamilyKey(source); - var fontAssets = FontFamilyLoader.LoadFontAssets(key).ToArray(); + var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray(); foreach (var fontAsset in fontAssets) { @@ -99,9 +95,9 @@ namespace Avalonia.Base.UnitTests.Media.Fonts { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - var fontFamily = new FontFamily("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono"); + var source = new Uri("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono", UriKind.RelativeOrAbsolute); - var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key).ToArray(); + var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray(); Assert.NotEmpty(fontAssets); diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs index 738469bc6f..7528815510 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs @@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Styling .Template() .OfType(); - border.SetValue(StyledElement.TemplatedParentProperty, null); + border.TemplatedParent = null; Assert.Equal(SelectorMatchResult.NeverThisInstance, selector.Match(border).Result); } @@ -124,10 +124,10 @@ namespace Avalonia.Base.UnitTests.Styling { VisualChildren.Add(new Border { - [TemplatedParentProperty] = this, + TemplatedParent = this, Child = new TextBlock { - [TemplatedParentProperty] = this, + TemplatedParent = this, }, }); } diff --git a/tests/Avalonia.Benchmarks/NullGlyphRun.cs b/tests/Avalonia.Benchmarks/NullGlyphRun.cs index c4707c78c8..5b584f302d 100644 --- a/tests/Avalonia.Benchmarks/NullGlyphRun.cs +++ b/tests/Avalonia.Benchmarks/NullGlyphRun.cs @@ -5,7 +5,7 @@ namespace Avalonia.Benchmarks { internal class NullGlyphRun : IGlyphRunImpl { - public Size Size => default; + public Rect Bounds => default; public Point BaselineOrigin => default; diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index f074972cef..7b5aa83b46 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -73,6 +73,19 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(target, target.Presenter.Panel.TemplatedParent); } + [Fact] + public void Panel_Should_Have_ItemsHost_Set_To_True() + { + var target = new ItemsControl(); + + target.Template = GetTemplate(); + target.Items = new[] { "Foo" }; + target.ApplyTemplate(); + target.Presenter!.ApplyTemplate(); + + Assert.True(target.Presenter.Panel!.IsItemsHost); + } + [Fact] public void Container_Should_Have_TemplatedParent_Set_To_Null() { @@ -118,7 +131,7 @@ namespace Avalonia.Controls.UnitTests root.Content = target; var templatedParent = new Button(); - target.SetValue(StyledElement.TemplatedParentProperty, templatedParent); + target.TemplatedParent = templatedParent; target.Template = GetTemplate(); target.Items = new[] { "Foo" }; @@ -634,7 +647,7 @@ namespace Avalonia.Controls.UnitTests target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - var item = target.Presenter.Panel.LogicalChildren[0]; + var item = target.LogicalChildren[0]; Assert.Null(NameScope.GetNameScope((TextBlock)item)); } diff --git a/tests/Avalonia.Controls.UnitTests/PanelTests.cs b/tests/Avalonia.Controls.UnitTests/PanelTests.cs index a31f0dd4c2..82f133d533 100644 --- a/tests/Avalonia.Controls.UnitTests/PanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/PanelTests.cs @@ -141,5 +141,20 @@ namespace Avalonia.Controls.UnitTests var panel = new Panel(); Assert.Throws(() => panel.Children.Add(null!)); } + + [Fact] + public void Adding_Control_To_Items_Host_Panel_Should_Not_Affect_Logical_Children() + { + var child = new Control(); + var realParent = new ContentControl { Content = child }; + var panel = new Panel { IsItemsHost = true }; + + panel.Children.Add(child); + + Assert.Empty(panel.LogicalChildren); + Assert.Same(child.Parent, realParent); + Assert.Same(child.GetLogicalParent(), realParent); + Assert.Same(child.GetVisualParent(), panel); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index d3737de45b..9042e84fa1 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -26,7 +26,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Null(host.Presenter); - target.SetValue(Control.TemplatedParentProperty, host); + target.TemplatedParent = host; Assert.Same(target, host.Presenter); } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 71f803fab7..573ce5834d 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Null(host.Presenter); - target.SetValue(Control.TemplatedParentProperty, host); + target.TemplatedParent = host; Assert.Same(target, host.Presenter); } diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 4c5397db60..0f72b2101a 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -327,7 +327,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); - var logicalChildren = target.ItemsPresenterPart.Panel.GetLogicalChildren(); + var logicalChildren = target.GetLogicalChildren(); var result = logicalChildren .OfType() diff --git a/tests/Avalonia.Controls.UnitTests/Templates/TemplateExtensionsTests.cs b/tests/Avalonia.Controls.UnitTests/Templates/TemplateExtensionsTests.cs index 8789d79742..33593a0631 100644 --- a/tests/Avalonia.Controls.UnitTests/Templates/TemplateExtensionsTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Templates/TemplateExtensionsTests.cs @@ -18,17 +18,17 @@ namespace Avalonia.Controls.Templates.UnitTests var border1 = new Border { Name = "border1", - [StyledElement.TemplatedParentProperty] = target, + TemplatedParent = target, }; var inner = new TestTemplatedControl { Name = "inner", - [StyledElement.TemplatedParentProperty] = target, + TemplatedParent = target, }; - var border2 = new Border { Name = "border2", [StyledElement.TemplatedParentProperty] = inner }; - var border3 = new Border { Name = "border3", [StyledElement.TemplatedParentProperty] = inner }; - var border4 = new Border { Name = "border4", [StyledElement.TemplatedParentProperty] = target }; - var border5 = new Border { Name = "border5", [StyledElement.TemplatedParentProperty] = null }; + var border2 = new Border { Name = "border2", TemplatedParent = inner }; + var border3 = new Border { Name = "border3", TemplatedParent = inner }; + var border4 = new Border { Name = "border4", TemplatedParent = target }; + var border5 = new Border { Name = "border5", TemplatedParent = null }; target.AddVisualChild(border1); border1.Child = inner; @@ -42,4 +42,4 @@ namespace Avalonia.Controls.Templates.UnitTests Assert.Equal(new[] { "border1", "inner", "border4" }, result); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 1a9fb33b7e..eee6e49617 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -100,6 +100,31 @@ namespace Avalonia.Controls.UnitTests Assert.All(items, x => Assert.Same(theme, x.ItemContainerTheme)); } + [Fact] + public void Finds_Correct_DataTemplate_When_Application_DataTemplate_Is_Present() + { + // #10398 + using var app = UnitTestApplication.Start(); + + Avalonia.Application.Current.DataTemplates.Add(new FuncDataTemplate((x, _) => new Canvas())); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Avalonia.Application.Current); + + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = CreateTestTreeData(), + }; + + var root = new TestRoot(target); + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); + Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1)); + Assert.Equal(new[] { "Grandchild2a" }, ExtractItemHeader(target, 2)); + } + [Fact] public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers() { @@ -1178,7 +1203,7 @@ namespace Avalonia.Controls.UnitTests target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - var item = target.Presenter.Panel.LogicalChildren[0]; + var item = target.LogicalChildren[0]; Assert.Null(NameScope.GetNameScope((TreeViewItem)item)); } diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index c6ecc0a7e5..14e48b3b6c 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -1,4 +1,5 @@ -using Avalonia.Direct2D1.Media; +using System; +using Avalonia.Direct2D1.Media; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -16,18 +17,10 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("A, B, Arial"))); - - var font = glyphTypeface.DWFont; - - Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); - - Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); + var glyphTypeface = + new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface; - Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + Assert.Equal("Arial", glyphTypeface.FamilyName); } } @@ -38,18 +31,13 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold)); + var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface; - var font = glyphTypeface.DWFont; + Assert.Equal("Arial", glyphTypeface.FamilyName); - Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); + Assert.Equal(FontWeight.Bold, glyphTypeface.Weight); - Assert.Equal(SharpDX.DirectWrite.FontWeight.Bold, font.Weight); - - Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + Assert.Equal(FontStyle.Normal, glyphTypeface.Style); } } @@ -60,20 +48,11 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); + var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface; - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("Unknown"))); + var defaultName = FontManager.Current.DefaultFontFamilyName; - var font = glyphTypeface.DWFont; - - var defaultName = fontManager.GetDefaultFontFamilyName(); - - Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0)); - - Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); - - Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + Assert.Equal(defaultName, glyphTypeface.FamilyName); } } @@ -86,12 +65,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media var fontManager = new FontManagerImpl(); - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri)); + var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface; - var font = glyphTypeface.DWFont; - - Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); } } @@ -102,14 +78,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black)); - - var font = glyphTypeface.DWFont; + var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface; - Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs b/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs index 65864cc649..775ee723cb 100644 --- a/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs @@ -74,7 +74,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("DoubleTapped", lastGesture.Text); } - [Fact] + [PlatformFact(TestPlatforms.Windows | TestPlatforms.Linux)] public void DoubleTapped_Is_Raised_2() { var border = _session.FindElementByAccessibilityId("GestureBorder"); diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index a2bfb618d6..6921a23161 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Avalonia.Controls; +using Avalonia.Utilities; using Avalonia.Media.Imaging; using OpenQA.Selenium; using OpenQA.Selenium.Appium; @@ -92,7 +93,7 @@ namespace Avalonia.IntegrationTests.Appium { try { - _session.FindElementByAccessibilityId("WindowState").SendClick(); + _session.FindElementByAccessibilityId("CurrentWindowState").SendClick(); _session.FindElementByAccessibilityId("WindowStateNormal").SendClick(); // Wait for animations to run. @@ -112,7 +113,7 @@ namespace Avalonia.IntegrationTests.Appium { using (OpenWindow(new Size(400, 400), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) { - var windowState = _session.FindElementByAccessibilityId("WindowState"); + var windowState = _session.FindElementByAccessibilityId("CurrentWindowState"); Assert.Equal("Normal", windowState.GetComboBoxValue()); @@ -143,6 +144,24 @@ namespace Avalonia.IntegrationTests.Appium } } + + [Fact] + public void Showing_Window_With_Size_Larger_Than_Screen_Measures_Content_With_Working_Area() + { + using (OpenWindow(new Size(4000, 2200), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) + { + var screenRectTextBox = _session.FindElementByAccessibilityId("CurrentClientSize"); + var measuredWithTextBlock = _session.FindElementByAccessibilityId("CurrentMeasuredWithText"); + + var measuredWithString = measuredWithTextBlock.Text; + var workingAreaString = screenRectTextBox.Text; + + var workingArea = Size.Parse(workingAreaString); + var measuredWith = Size.Parse(measuredWithString); + + Assert.Equal(workingArea, measuredWith); + } + } [Theory] [InlineData(ShowWindowMode.NonOwned)] @@ -151,7 +170,7 @@ namespace Avalonia.IntegrationTests.Appium public void ShowMode(ShowWindowMode mode) { using var window = OpenWindow(null, mode, WindowStartupLocation.Manual); - var windowState = _session.FindElementByAccessibilityId("WindowState"); + var windowState = _session.FindElementByAccessibilityId("CurrentWindowState"); var original = GetWindowInfo(); Assert.Equal("Normal", windowState.GetComboBoxValue()); @@ -354,7 +373,7 @@ namespace Avalonia.IntegrationTests.Appium { PixelRect? ReadOwnerRect() { - var text = _session.FindElementByAccessibilityId("OwnerRect").Text; + var text = _session.FindElementByAccessibilityId("CurrentOwnerRect").Text; return !string.IsNullOrWhiteSpace(text) ? PixelRect.Parse(text) : null; } @@ -365,13 +384,13 @@ namespace Avalonia.IntegrationTests.Appium try { return new( - Size.Parse(_session.FindElementByAccessibilityId("ClientSize").Text), - Size.Parse(_session.FindElementByAccessibilityId("FrameSize").Text), - PixelPoint.Parse(_session.FindElementByAccessibilityId("Position").Text), + Size.Parse(_session.FindElementByAccessibilityId("CurrentClientSize").Text), + Size.Parse(_session.FindElementByAccessibilityId("CurrentFrameSize").Text), + PixelPoint.Parse(_session.FindElementByAccessibilityId("CurrentPosition").Text), ReadOwnerRect(), - PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text), - double.Parse(_session.FindElementByAccessibilityId("Scaling").Text), - Enum.Parse(_session.FindElementByAccessibilityId("WindowState").Text)); + PixelRect.Parse(_session.FindElementByAccessibilityId("CurrentScreenRect").Text), + double.Parse(_session.FindElementByAccessibilityId("CurrentScaling").Text), + Enum.Parse(_session.FindElementByAccessibilityId("CurrentWindowState").Text)); } catch (OpenQA.Selenium.NoSuchElementException) when (retry++ < 3) { diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 2eaaf2e0a8..039d30bbc1 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -163,7 +163,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(TestPlatforms.MacOS)] + [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test")] public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked() { // Issue #9565 @@ -393,7 +393,7 @@ namespace Avalonia.IntegrationTests.Appium private int GetWindowOrder(string identifier) { var window = GetWindow(identifier); - var order = window.FindElementByXPath("//*[@identifier='Order']"); + var order = window.FindElementByXPath("//*[@identifier='CurrentOrder']"); return int.Parse(order.Text); } diff --git a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh index dc4619f35c..0d4a1fa57c 100755 --- a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh +++ b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh @@ -1,5 +1,11 @@ # Cleans, builds, and runs integration tests on macOS. # Can be used by `git bisect run` to automatically find the commit which introduced a problem. +arch="x64" + +if [[ $(uname -m) == 'arm64' ]]; then +arch="arm64" +fi + SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) cd "$SCRIPT_DIR"/../.. || exit git clean -xdf @@ -10,7 +16,7 @@ pkill IntegrationTestApp rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")") pkill IntegrationTestApp ./samples/IntegrationTestApp/bundle.sh -open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-arm64/publish/IntegrationTestApp.app +open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-$arch/publish/IntegrationTestApp.app pkill IntegrationTestApp open -b net.avaloniaui.avalonia.integrationtestapp dotnet test tests/Avalonia.IntegrationTests.Appium/ -l "console;verbosity=detailed" diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 8a1ee72c54..933958b3a0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -14,6 +15,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml { public class StyleTests : XamlTestBase { + static StyleTests() + { + GC.KeepAlive(typeof(ItemsRepeater)); + } + [Fact] public void Color_Can_Be_Added_To_Style_Resources() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index be2cae8ec4..09ed78accb 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -21,17 +21,6 @@ namespace Avalonia.Markup.Xaml.UnitTests { public class XamlIlTests : XamlTestBase { - [Fact] - public void Binding_Button_IsPressed_ShouldWork() - { - var parsed = (Button)AvaloniaRuntimeXamlLoader.Parse(@" -