Browse Source

Merge branch 'master' into move-name-generator

pull/10407/head
Max Katz 3 years ago
committed by GitHub
parent
commit
fa6fb50620
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      azure-pipelines-integrationtests.yml
  2. 7
      native/Avalonia.Native/src/OSX/AvnView.mm
  3. 2
      native/Avalonia.Native/src/OSX/Screens.mm
  4. 24
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  5. 5
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  6. 4
      nukebuild/Build.cs
  7. 5
      packages/Avalonia/Avalonia.props
  8. 1
      samples/BindingDemo/BindingDemo.csproj
  9. 2
      samples/ControlCatalog.Android/Resources/values/styles.xml
  10. 9
      samples/ControlCatalog.Browser/app.css
  11. 2
      samples/ControlCatalog.NetCore/Program.cs
  12. 4
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  13. 6
      samples/ControlCatalog.iOS/Info.plist
  14. 4
      samples/ControlCatalog/App.xaml.cs
  15. 38
      samples/ControlCatalog/MainView.xaml.cs
  16. 1
      samples/ControlCatalog/MainWindow.xaml.cs
  17. 2
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  18. 22
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  19. 30
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  20. 1
      samples/GpuInterop/GpuInterop.csproj
  21. 75
      samples/IntegrationTestApp/ShowWindowTest.axaml
  22. 31
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  23. 11
      samples/IntegrationTestApp/bundle.sh
  24. 1
      samples/MobileSandbox/MobileSandbox.csproj
  25. 1
      samples/PlatformSanityChecks/PlatformSanityChecks.csproj
  26. 1
      samples/Previewer/Previewer.csproj
  27. 1
      samples/ReactiveUIDemo/ReactiveUIDemo.csproj
  28. 1
      samples/RenderDemo/RenderDemo.csproj
  29. 1
      samples/Sandbox/Sandbox.csproj
  30. 1
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  31. 15
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  32. 6
      src/Android/Avalonia.Android/AvaloniaView.cs
  33. 235
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  34. 43
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  35. 7
      src/Avalonia.Base/AttachedProperty.cs
  36. 15
      src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs
  37. 12
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  38. 52
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  39. 130
      src/Avalonia.Base/Media/FontManager.cs
  40. 290
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  41. 4
      src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs
  42. 66
      src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
  43. 33
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  44. 107
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  45. 14
      src/Avalonia.Base/Media/GlyphRun.cs
  46. 2
      src/Avalonia.Base/Media/GlyphRunDrawing.cs
  47. 20
      src/Avalonia.Base/Media/IGlyphTypeface.cs
  48. 2
      src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs
  49. 2
      src/Avalonia.Base/Media/TextDecoration.cs
  50. 4
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  51. 15
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  52. 2
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  53. 2
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  54. 6
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  55. 13
      src/Avalonia.Base/Media/Typeface.cs
  56. 155
      src/Avalonia.Base/Platform/AssetLoader.cs
  57. 30
      src/Avalonia.Base/Platform/IFontManagerImpl.cs
  58. 3
      src/Avalonia.Base/Platform/IGlyphRunImpl.cs
  59. 2
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  60. 8
      src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs
  61. 5
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
  62. 3
      src/Avalonia.Base/StyledElement.cs
  63. 7
      src/Avalonia.Base/StyledProperty.cs
  64. 5
      src/Avalonia.Base/Utilities/UriExtensions.cs
  65. 15
      src/Avalonia.Controls/AppBuilder.cs
  66. 34
      src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs
  67. 45
      src/Avalonia.Controls/Button.cs
  68. 90
      src/Avalonia.Controls/Calendar/Calendar.cs
  69. 14
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  70. 68
      src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs
  71. 52
      src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs
  72. 28
      src/Avalonia.Controls/ComboBox.cs
  73. 183
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  74. 181
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
  75. 93
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  76. 68
      src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
  77. 8
      src/Avalonia.Controls/Documents/InlineCollection.cs
  78. 23
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  79. 9
      src/Avalonia.Controls/ItemsControl.cs
  80. 27
      src/Avalonia.Controls/Label.cs
  81. 22
      src/Avalonia.Controls/MenuItem.cs
  82. 4
      src/Avalonia.Controls/NativeMenu.cs
  83. 142
      src/Avalonia.Controls/NativeMenuItem.cs
  84. 4
      src/Avalonia.Controls/NativeMenuItemBase.cs
  85. 9
      src/Avalonia.Controls/Notifications/NotificationCard.cs
  86. 90
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  87. 35
      src/Avalonia.Controls/Panel.cs
  88. 55
      src/Avalonia.Controls/Platform/IInsetsManager.cs
  89. 11
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  90. 3
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  91. 47
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  92. 31
      src/Avalonia.Controls/Primitives/Popup.cs
  93. 4
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  94. 31
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  95. 59
      src/Avalonia.Controls/RadioButton.cs
  96. 12
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  97. 5
      src/Avalonia.Controls/TopLevel.cs
  98. 14
      src/Avalonia.Controls/TrayIcon.cs
  99. 4
      src/Avalonia.Controls/TreeViewItem.cs
  100. 35
      src/Avalonia.Controls/Window.cs

18
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'

7
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);
}
}

2
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;

24
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -4,6 +4,7 @@
//
#import <AppKit/AppKit.h>
#import <Cocoa/Cocoa.h>
#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 {

5
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);
}
}

4
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;
}

5
packages/Avalonia/Avalonia.props

@ -6,4 +6,9 @@
<AvaloniaUseExternalMSBuild>false</AvaloniaUseExternalMSBuild>
</PropertyGroup>
<Import Project="$(MSBuildThisFileDirectory)\AvaloniaBuildTasks.props"/>
<!-- Allow loading the AvaloniaVS extension when referencing the Avalonia nuget package -->
<ItemGroup>
<ProjectCapability Include="Avalonia"/>
</ItemGroup>
</Project>

1
samples/BindingDemo/BindingDemo.csproj

@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />

2
samples/ControlCatalog.Android/Resources/values/styles.xml

@ -4,7 +4,7 @@
<style name="MyTheme">
</style>
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>

9
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
}

2
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()

4
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

@ -3,7 +3,7 @@
<OutputType>Exe</OutputType>
<ProvisioningType>manual</ProvisioningType>
<TargetFramework>net6.0-ios</TargetFramework>
<SupportedOSPlatformVersion>10.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
<!-- temporal workaround for our GL interface backend -->
<UseInterpreter>True</UseInterpreter>
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
@ -16,4 +16,4 @@
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup>
</Project>
</Project>

6
samples/ControlCatalog.iOS/Info.plist

@ -13,7 +13,7 @@
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>10.0</string>
<string>13.0</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
@ -39,9 +39,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

4
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();

38
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<ComboBox>("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());
}

1
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;
}

2
samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Get<ComboBox>("fontComboBox");
fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
fontComboBox.Items = FontManager.Current.SystemFonts;
fontComboBox.SelectedIndex = 0;
}
}

22
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">
<StackPanel Spacing="10" Margin="25">
<CheckBox Content="Extend Client Area to Decorations" IsChecked="{Binding ExtendClientAreaEnabled}" />
<CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" />
<CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" />
<Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" />
x:DataType="viewModels:MainWindowViewModel"
x:CompileBindings="True">
<StackPanel>
<StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Desktop=true}">
<TextBlock Classes="h2" Text="Desktop properties" Margin="4" />
<CheckBox Content="Extend Client Area to Decorations" IsChecked="{Binding ExtendClientAreaEnabled}" />
<CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" />
<CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" />
<Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" />
</StackPanel>
<StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Mobile=true}">
<TextBlock Classes="h2" Text="Mobile properties" Margin="4" />
<CheckBox Content="Is System Bar Visible" IsChecked="{Binding IsSystemBarVisible}" />
<CheckBox Content="Display Edge To Edge" IsChecked="{Binding DisplayEdgeToEdge}" />
<TextBlock Text="{Binding SafeAreaPadding, StringFormat='Safe Area Padding: {0}'}" />
</StackPanel>
</StackPanel>
</UserControl>

30
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; }

1
samples/GpuInterop/GpuInterop.csproj

@ -15,6 +15,7 @@
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
</ItemGroup>

75
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -1,41 +1,48 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:integrationTestApp="clr-namespace:IntegrationTestApp"
x:Class="IntegrationTestApp.ShowWindowTest"
Name="SecondaryWindow"
x:DataType="Window"
Title="Show Window Test">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="ClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}"/>
<Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
<TextBox Name="FrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"
Text="{Binding FrameSize, Mode=OneWay}"/>
<Label Grid.Column="0" Grid.Row="3">Position</Label>
<TextBox Name="Position" Grid.Column="1" Grid.Row="3" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="4">Owner Rect</Label>
<TextBox Name="OwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="5">Screen Rect</Label>
<TextBox Name="ScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="6">Scaling</Label>
<TextBox Name="Scaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="7">WindowState</Label>
<ComboBox Name="WindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
<ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
<TextBox Name="Order" Grid.Column="1" Grid.Row="8" IsReadOnly="True"/>
<Button Name="HideButton" Grid.Row="9" Command="{Binding $parent[Window].Hide}">Hide</Button>
</Grid>
<integrationTestApp:MeasureBorder Name="MyBorder">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}" />
<Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
<TextBox Name="CurrentFrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"
Text="{Binding FrameSize, Mode=OneWay}" />
<Label Grid.Column="0" Grid.Row="3">Position</Label>
<TextBox Name="CurrentPosition" Grid.Column="1" Grid.Row="3" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="4">Owner Rect</Label>
<TextBox Name="CurrentOwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="5">Screen Rect</Label>
<TextBox Name="CurrentScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="6">Scaling</Label>
<TextBox Name="CurrentScaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="7">WindowState</Label>
<ComboBox Name="CurrentWindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
<ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True" />
<Label Grid.Row="9" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="9" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
<Button Name="HideButton" Grid.Row="10" Command="{Binding $parent[Window].Hide}">Hide</Button>
</Grid>
</integrationTestApp:MeasureBorder>
</Window>

31
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<Size> MeasuredWithProperty = AvaloniaProperty.Register<MeasureBorder, Size>(
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<TextBox>("Position").Text = $"{Position}";
PositionChanged += (s, e) => this.GetControl<TextBox>("CurrentPosition").Text = $"{Position}";
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
_orderTextBox = this.GetControl<TextBox>("Order");
_orderTextBox = this.GetControl<TextBox>("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<TextBox>("Position").Text = $"{Position}";
this.GetControl<TextBox>("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}";
this.GetControl<TextBox>("Scaling").Text = $"{scaling}";
this.GetControl<TextBox>("CurrentPosition").Text = $"{Position}";
this.GetControl<TextBox>("CurrentScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}";
this.GetControl<TextBox>("CurrentScaling").Text = $"{scaling}";
if (Owner is not null)
{
var ownerRect = this.GetControl<TextBox>("OwnerRect");
var ownerRect = this.GetControl<TextBox>("CurrentOwnerRect");
var owner = (Window)Owner;
ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}";
}

11
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
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

1
samples/MobileSandbox/MobileSandbox.csproj

@ -28,6 +28,7 @@
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
<ProjectReference Include="..\SampleControls\ControlSamples.csproj" />

1
samples/PlatformSanityChecks/PlatformSanityChecks.csproj

@ -7,6 +7,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
</ItemGroup>

1
samples/Previewer/Previewer.csproj

@ -10,6 +10,7 @@
<EmbeddedResource Include="**\*.xaml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
</ItemGroup>

1
samples/ReactiveUIDemo/ReactiveUIDemo.csproj

@ -7,6 +7,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
</ItemGroup>

1
samples/RenderDemo/RenderDemo.csproj

@ -12,6 +12,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />

1
samples/Sandbox/Sandbox.csproj

@ -10,6 +10,7 @@
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
</ItemGroup>

1
samples/VirtualizationDemo/VirtualizationDemo.csproj

@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />

15
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<AndroidBackRequestedEventArgs> BackRequested;
public override void OnBackPressed()

6
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<IInsetsManager>(out var insetsManager) == true)
{
(insetsManager as AndroidInsetsManager)?.ApplyStatusBarState();
}
}
else
{

235
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<SafeAreaChangedArgs> 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<WindowInsetsAnimationCompat> 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;
}
}
}
}

43
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;
}
}

7
src/Avalonia.Base/AttachedProperty.cs

@ -32,9 +32,14 @@ namespace Avalonia
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <returns>The property.</returns>
public new AttachedProperty<TValue> AddOwner<TOwner>() where TOwner : AvaloniaObject
public new AttachedProperty<TValue> AddOwner<TOwner>(StyledPropertyMetadata<TValue>? metadata = null) where TOwner : AvaloniaObject
{
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this);
if (metadata != null)
{
OverrideMetadata<TOwner>(metadata);
}
return this;
}
}

15
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<IGestureRecognizer>? _recognizers;
private Dictionary<IPointer, IGestureRecognizer>? _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);
}

12
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;
/// <summary>
/// Defines the <see cref="PullDirection"/> property.
/// </summary>
public static readonly DirectProperty<PullGestureRecognizer, PullDirection> PullDirectionProperty =
AvaloniaProperty.RegisterDirect<PullGestureRecognizer, PullDirection>(
nameof(PullDirection),
o => o.PullDirection,
(o, v) => o.PullDirection = v);
public static readonly StyledProperty<PullDirection> PullDirectionProperty =
AvaloniaProperty.Register<PullGestureRecognizer, PullDirection>(nameof(PullDirection));
public PullDirection PullDirection
{
get => _pullDirection;
set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value);
get => GetValue(PullDirectionProperty);
set => SetValue(PullDirectionProperty, value);
}
public PullGestureRecognizer(PullDirection pullDirection)

52
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;
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
nameof(CanHorizontallyScroll),
o => o.CanHorizontallyScroll,
(o, v) => o.CanHorizontallyScroll = v);
public static readonly StyledProperty<bool> CanHorizontallyScrollProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll));
/// <summary>
/// Defines the <see cref="CanVerticallyScroll"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanVerticallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
nameof(CanVerticallyScroll),
o => o.CanVerticallyScroll,
(o, v) => o.CanVerticallyScroll = v);
public static readonly StyledProperty<bool> CanVerticallyScrollProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(CanVerticallyScroll));
/// <summary>
/// Defines the <see cref="IsScrollInertiaEnabled"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> IsScrollInertiaEnabledProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
nameof(IsScrollInertiaEnabled),
o => o.IsScrollInertiaEnabled,
(o, v) => o.IsScrollInertiaEnabled = v);
public static readonly StyledProperty<bool> IsScrollInertiaEnabledProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(IsScrollInertiaEnabled));
/// <summary>
/// Defines the <see cref="ScrollStartDistance"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, int> ScrollStartDistanceProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, int>(
nameof(ScrollStartDistance),
o => o.ScrollStartDistance,
(o, v) => o.ScrollStartDistance = v);
public static readonly StyledProperty<int> ScrollStartDistanceProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, int>(nameof(ScrollStartDistance), 30);
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
public bool CanHorizontallyScroll
{
get => _canHorizontallyScroll;
set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value);
get => GetValue(CanHorizontallyScrollProperty);
set => SetValue(CanHorizontallyScrollProperty, value);
}
/// <summary>
@ -79,8 +63,8 @@ namespace Avalonia.Input.GestureRecognizers
/// </summary>
public bool CanVerticallyScroll
{
get => _canVerticallyScroll;
set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
get => GetValue(CanVerticallyScrollProperty);
set => SetValue(CanVerticallyScrollProperty, value);
}
/// <summary>
@ -88,8 +72,8 @@ namespace Avalonia.Input.GestureRecognizers
/// </summary>
public bool IsScrollInertiaEnabled
{
get => _isScrollInertiaEnabled;
set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value);
get => GetValue(IsScrollInertiaEnabledProperty);
set => SetValue(IsScrollInertiaEnabledProperty, value);
}
/// <summary>
@ -97,8 +81,8 @@ namespace Avalonia.Input.GestureRecognizers
/// </summary>
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);
}

130
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
/// </summary>
public sealed class FontManager
{
private readonly ConcurrentDictionary<Typeface, IGlyphTypeface> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, IGlyphTypeface>();
private readonly FontFamily _defaultFontFamily;
internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts");
public const string FontCollectionScheme = "fonts";
private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
private readonly IReadOnlyList<FontFallback>? _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));
}
/// <summary>
/// Get the current font manager instance.
/// </summary>
public static FontManager Current
{
get
@ -57,11 +64,6 @@ namespace Avalonia.Media
}
}
/// <summary>
///
/// </summary>
public IFontManagerImpl PlatformImpl { get; }
/// <summary>
/// Gets the system's default font family's name.
/// </summary>
@ -71,41 +73,109 @@ namespace Avalonia.Media
}
/// <summary>
/// Get all installed font family names.
/// Get all system fonts.
/// </summary>
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
public IFontCollection SystemFonts => _fontCollections[SystemFontsKey];
internal IFontManagerImpl PlatformImpl { get; }
/// <summary>
/// Returns a new <see cref="IGlyphTypeface"/>, or an existing one if a matching <see cref="IGlyphTypeface"/> exists.
/// Tries to get a glyph typeface for specified typeface.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns>
/// The <see cref="IGlyphTypeface"/>.
/// <c>True</c>, if the <see cref="FontManager"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns>
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);
}
/// <summary>
/// Add a font collection to the manager.
/// </summary>
/// <param name="fontCollection">The font collection.</param>
/// <exception cref="ArgumentException"></exception>
/// <remarks>If a font collection's key is already present the collection is replaced.</remarks>
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);
}
/// <summary>
/// Removes the font collection that corresponds to specified key.
/// </summary>
/// <param name="key">The font collection's key.</param>
public void RemoveFontCollection(Uri key)
{
if (_fontCollections.TryRemove(key, out var fontCollection))
{
fontCollection.Dispose();
}
}
@ -123,18 +193,16 @@ namespace Avalonia.Media
/// <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
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;
}
}

290
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<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
private readonly List<FontFamily> _fontFamilies = new List<FontFamily>(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<IAssetLoader>();
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<FontCollectionKey, IGlyphTypeface>();
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<FontFamily> 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<FontCollectionKey, IGlyphTypeface> 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<FontCollectionKey, IGlyphTypeface> 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<FontCollectionKey, IGlyphTypeface> 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;
}
}
}

4
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);
}

66
src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs

@ -11,22 +11,30 @@ namespace Avalonia.Media.Fonts
/// <summary>
/// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <param name="source"></param>
/// <returns></returns>
public static IEnumerable<Uri> LoadFontAssets(FontFamilyKey fontFamilyKey) =>
IsFontTtfOrOtf(fontFamilyKey.Source) ?
GetFontAssetsByExpression(fontFamilyKey) :
GetFontAssetsBySource(fontFamilyKey);
public static IEnumerable<Uri> LoadFontAssets(Uri source)
{
if (source.IsAvares() || source.IsAbsoluteResm())
{
return IsFontTtfOrOtf(source) ?
GetFontAssetsByExpression(source) :
GetFontAssetsBySource(source);
}
return Enumerable.Empty<Uri>();
}
/// <summary>
/// Searches for font assets at a given location and returns a quantity of found assets
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <param name="source"></param>
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsBySource(FontFamilyKey fontFamilyKey)
private static IEnumerable<Uri> GetFontAssetsBySource(Uri source)
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
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.
/// <para>File names can target multiple files with * wildcard. For example "FontFile*.ttf"</para>
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <param name="source"></param>
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
private static IEnumerable<Uri> 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<IAssetLoader>();
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;
}

33
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<FontFamily>, IDisposable
{
/// <summary>
/// Get the font collection's key.
/// </summary>
Uri Key { get; }
/// <summary>
/// Initializes the font collection.
/// </summary>
/// <param name="fontManager">The font manager the collection is registered with.</param>
void Initialize(IFontManagerImpl fontManager);
/// <summary>
/// Try to get a glyph typeface for given parameters.
/// </summary>
/// <param name="familyName">The family name.</param>
/// <param name="style">The font style.</param>
/// <param name="weight">The font weight.</param>
/// <param name="stretch">The font stretch.</param>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <returns>Returns <c>true</c> if a glyph typface can be found; otherwise, <c>false</c></returns>
bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
}
}

107
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<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _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<FontCollectionKey, IGlyphTypeface>();
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<FontFamily> 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);
}
}
}

14
src/Avalonia.Base/Media/GlyphRun.cs

@ -151,9 +151,9 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
/// Gets the conservative bounding box of the <see cref="GlyphRun"/>.
/// </summary>
public Size Size => PlatformImpl.Item.Size;
public Rect Bounds => PlatformImpl.Item.Bounds;
/// <summary>
///
@ -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--)
{

2
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;
}
}
}

20
src/Avalonia.Base/Media/IGlyphTypeface.cs

@ -6,6 +6,26 @@ namespace Avalonia.Media
[Unstable]
public interface IGlyphTypeface : IDisposable
{
/// <summary>
/// Gets the family name for the <see cref="IGlyphTypeface"/> object.
/// </summary>
string FamilyName { get; }
/// <summary>
/// Gets the designed weight of the font represented by the <see cref="IGlyphTypeface"/> object.
/// </summary>
FontWeight Weight { get; }
/// <summary>
/// Gets the style for the <see cref="IGlyphTypeface"/> object.
/// </summary>
FontStyle Style { get; }
/// <summary>
/// Gets the <see cref="FontStretch"/> value for the <see cref="IGlyphTypeface"/> object.
/// </summary>
FontStretch Stretch { get; }
/// <summary>
/// Gets the number of glyphs held by this glyph typeface.
/// </summary>

2
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)

2
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<double>();

4
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();
/// <inheritdoc/>
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)
{

15
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

2
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<TextRun>();

2
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<TextRun>();
}

6
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;
}

13
src/Avalonia.Base/Media/Typeface.cs

@ -80,7 +80,18 @@ namespace Avalonia.Media
/// <value>
/// The glyph typeface.
/// </value>
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)
{

155
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
/// <returns>True if the asset could be found; otherwise false.</returns>
public bool Exists(Uri uri, Uri? baseUri = null)
{
return GetAsset(uri, baseUri) != null;
return TryGetAsset(uri, baseUri, out _);
}
/// <summary>
@ -94,21 +95,27 @@ namespace Avalonia.Platform
/// </exception>
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;
}
/// <summary>
@ -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<Uri>();
.Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath()))
.Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ??
Enumerable.Empty<Uri>();
}
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<Uri>();
}
if (asm.AvaloniaResources == null)
if (assembly?.AvaloniaResources == null)
{
return Enumerable.Empty<Uri>();
}
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<Uri>();
}
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"))

30
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.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// </summary>
IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
string[] GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <summary>
/// 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);
/// <summary>
/// Creates a glyph typeface.
/// Tries to get a glyph typeface for specified parameters.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <returns>0
/// The created glyph typeface. Can be <c>Null</c> if it was not possible to create a glyph typeface.
/// <param name="familyName">The family name.</param>
/// <param name="style">The font style.</param>
/// <param name="weight">The font weiht.</param>
/// <param name="stretch">The font stretch.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns>
bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
/// <summary>
/// Tries to create a glyph typeface from specified stream.
/// </summary>
/// <param name="stream">A stream that holds the font's data.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns>
IGlyphTypeface CreateGlyphTypeface(Typeface typeface);
bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
}
}

3
src/Avalonia.Base/Platform/IGlyphRunImpl.cs

@ -10,11 +10,10 @@ namespace Avalonia.Platform
[Unstable]
public interface IGlyphRunImpl : IDisposable
{
/// <summary>
/// Gets the conservative bounding box of the glyph run./>.
/// </summary>
Size Size { get; }
Rect Bounds { get; }
/// <summary>
/// Gets the baseline origin of the glyph run./>.

2
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);

8
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;

5
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

@ -16,12 +16,11 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="transform">The transform.</param>
/// <param name="foreground">The foreground brush.</param>
/// <param name="glyphRun">The glyph run to draw.</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public GlyphRunNode(
Matrix transform,
IImmutableBrush foreground,
IRef<IGlyphRunImpl> 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
}
/// <inheritdoc/>
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()
{

3
src/Avalonia.Base/StyledElement.cs

@ -67,8 +67,7 @@ namespace Avalonia
public static readonly DirectProperty<StyledElement, AvaloniaObject?> TemplatedParentProperty =
AvaloniaProperty.RegisterDirect<StyledElement, AvaloniaObject?>(
nameof(TemplatedParent),
o => o.TemplatedParent,
(o ,v) => o.TemplatedParent = v);
o => o.TemplatedParent);
/// <summary>
/// Defines the <see cref="Theme"/> property.

7
src/Avalonia.Base/StyledProperty.cs

@ -56,9 +56,14 @@ namespace Avalonia
/// </summary>
/// <typeparam name="TOwner">The type of the additional owner.</typeparam>
/// <returns>The property.</returns>
public StyledProperty<TValue> AddOwner<TOwner>() where TOwner : AvaloniaObject
public StyledProperty<TValue> AddOwner<TOwner>(StyledPropertyMetadata<TValue>? metadata = null) where TOwner : AvaloniaObject
{
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this);
if (metadata != null)
{
OverrideMetadata<TOwner>(metadata);
}
return this;
}

5
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)

15
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;
}
/// <summary>
/// Registers an action that is executed with the current font manager.
/// </summary>
/// <param name="action">The action.</param>
/// <returns>An <see cref="AppBuilder"/> instance.</returns>
public AppBuilder ConfigureFonts(Action<FontManager> action)
{
return AfterSetup(appBuilder =>
{
action?.Invoke(FontManager.Current);
});
}
/// <summary>
/// Sets up the platform-specific services for the <see cref="Application"/>.
/// </summary>

34
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;
}
}
}

45
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
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
public static readonly DirectProperty<Button, ICommand?> CommandProperty =
AvaloniaProperty.RegisterDirect<Button, ICommand?>(nameof(Command),
button => button.Command, (button, command) => button.Command = command, enableDataValidation: true);
public static readonly StyledProperty<ICommand?> CommandProperty =
AvaloniaProperty.Register<Button, ICommand?>(nameof(Command), enableDataValidation: true);
/// <summary>
/// Defines the <see cref="HotKey"/> property.
@ -85,8 +82,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="IsPressed"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsPressedProperty =
AvaloniaProperty.Register<Button, bool>(nameof(IsPressed));
public static readonly DirectProperty<Button, bool> IsPressedProperty =
AvaloniaProperty.RegisterDirect<Button, bool>(nameof(IsPressed), b => b.IsPressed);
/// <summary>
/// Defines the <see cref="Flyout"/> property
@ -94,10 +91,10 @@ namespace Avalonia.Controls
public static readonly StyledProperty<FlyoutBase?> FlyoutProperty =
AvaloniaProperty.Register<Button, FlyoutBase?>(nameof(Flyout));
private ICommand? _command;
private bool _commandCanExecute = true;
private KeyGesture? _hotkey;
private bool _isFlyoutOpen = false;
private bool _isPressed = false;
/// <summary>
/// Initializes static members of the <see cref="Button"/> class.
@ -138,8 +135,8 @@ namespace Avalonia.Controls
/// </summary>
public ICommand? Command
{
get => _command;
set => SetAndRaise(CommandProperty, ref _command, value);
get => GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
/// <summary>
@ -185,8 +182,8 @@ namespace Avalonia.Controls
/// </summary>
public bool IsPressed
{
get => GetValue(IsPressedProperty);
private set => SetValue(IsPressedProperty, value);
get => _isPressed;
private set => SetAndRaise(IsPressedProperty, ref _isPressed, value);
}
/// <summary>
@ -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;
}
}
}
/// <summary>
@ -671,7 +668,7 @@ namespace Avalonia.Controls
void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e);
void IClickableControl.RaiseClick() => OnClick();
/// <summary>
/// Event handler for when the button's flyout is opened.
/// </summary>

90
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<Calendar, DateTime?> SelectedDateProperty =
AvaloniaProperty.RegisterDirect<Calendar, DateTime?>(
nameof(SelectedDate),
o => o.SelectedDate,
(o, v) => o.SelectedDate = v,
public static readonly StyledProperty<DateTime?> SelectedDateProperty =
AvaloniaProperty.Register<Calendar, DateTime?>(nameof(SelectedDate),
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
@ -529,8 +521,8 @@ namespace Avalonia.Controls
/// </remarks>
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<Calendar, DateTime> DisplayDateProperty =
AvaloniaProperty.RegisterDirect<Calendar, DateTime>(
nameof(DisplayDate),
o => o.DisplayDate,
(o, v) => o.DisplayDate = v,
public static readonly StyledProperty<DateTime> DisplayDateProperty =
AvaloniaProperty.Register<Calendar, DateTime>(nameof(DisplayDate),
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
@ -760,8 +749,8 @@ namespace Avalonia.Controls
/// </remarks>
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<Calendar, DateTime?> DisplayDateStartProperty =
AvaloniaProperty.RegisterDirect<Calendar, DateTime?>(
nameof(DisplayDateStart),
o => o.DisplayDateStart,
(o, v) => o.DisplayDateStart = v,
public static readonly StyledProperty<DateTime?> DisplayDateStartProperty =
AvaloniaProperty.Register<Calendar, DateTime?>(nameof(DisplayDateStart),
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets the first date to be displayed.
@ -814,8 +800,8 @@ namespace Avalonia.Controls
/// </remarks>
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<Calendar, DateTime?> DisplayDateEndProperty =
AvaloniaProperty.RegisterDirect<Calendar, DateTime?>(
nameof(DisplayDateEnd),
o => o.DisplayDateEnd,
(o, v) => o.DisplayDateEnd = v,
public static readonly StyledProperty<DateTime?> DisplayDateEndProperty =
AvaloniaProperty.Register<Calendar, DateTime?>(nameof(DisplayDateEnd),
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
@ -924,8 +907,8 @@ namespace Avalonia.Controls
/// </remarks>
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
/// </summary>
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<DateTime>();

14
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<Control>? _dayTitleTemplate;
private DateTime _currentMonth;
private bool _isMouseLeftButtonDown;
@ -61,17 +60,15 @@ namespace Avalonia.Controls.Primitives
set { SetValue(HeaderBackgroundProperty, value); }
}
public static readonly DirectProperty<CalendarItem, ITemplate<Control>?> DayTitleTemplateProperty =
AvaloniaProperty.RegisterDirect<CalendarItem, ITemplate<Control>?>(
public static readonly StyledProperty<ITemplate<Control>?> DayTitleTemplateProperty =
AvaloniaProperty.Register<CalendarItem, ITemplate<Control>?>(
nameof(DayTitleTemplate),
o => o.DayTitleTemplate,
(o,v) => o.DayTitleTemplate = v,
defaultBindingMode: BindingMode.OneTime);
public ITemplate<Control>? DayTitleTemplate
{
get { return _dayTitleTemplate; }
set { SetAndRaise(DayTitleTemplateProperty, ref _dayTitleTemplate, value); }
get => GetValue(DayTitleTemplateProperty);
set => SetValue(DayTitleTemplateProperty, value);
}
/// <summary>
@ -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);

68
src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs

@ -11,29 +11,22 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="DisplayDate"/> property.
/// </summary>
public static readonly DirectProperty<CalendarDatePicker, DateTime> DisplayDateProperty =
AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime>(
nameof(DisplayDate),
o => o.DisplayDate,
(o, v) => o.DisplayDate = v);
public static readonly StyledProperty<DateTime> DisplayDateProperty =
AvaloniaProperty.Register<CalendarDatePicker, DateTime>(nameof(DisplayDate));
/// <summary>
/// Defines the <see cref="DisplayDateStart"/> property.
/// </summary>
public static readonly DirectProperty<CalendarDatePicker, DateTime?> DisplayDateStartProperty =
AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime?>(
nameof(DisplayDateStart),
o => o.DisplayDateStart,
(o, v) => o.DisplayDateStart = v);
public static readonly StyledProperty<DateTime?> DisplayDateStartProperty =
AvaloniaProperty.Register<CalendarDatePicker, DateTime?>(
nameof(DisplayDateStart));
/// <summary>
/// Defines the <see cref="DisplayDateEnd"/> property.
/// </summary>
public static readonly DirectProperty<CalendarDatePicker, DateTime?> DisplayDateEndProperty =
AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime?>(
nameof(DisplayDateEnd),
o => o.DisplayDateEnd,
(o, v) => o.DisplayDateEnd = v);
public static readonly StyledProperty<DateTime?> DisplayDateEndProperty =
AvaloniaProperty.Register<CalendarDatePicker, DateTime?>(
nameof(DisplayDateEnd));
/// <summary>
/// Defines the <see cref="FirstDayOfWeek"/> property.
@ -44,11 +37,9 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="IsDropDownOpen"/> property.
/// </summary>
public static readonly DirectProperty<CalendarDatePicker, bool> IsDropDownOpenProperty =
AvaloniaProperty.RegisterDirect<CalendarDatePicker, bool>(
nameof(IsDropDownOpen),
o => o.IsDropDownOpen,
(o, v) => o.IsDropDownOpen = v);
public static readonly StyledProperty<bool> IsDropDownOpenProperty =
AvaloniaProperty.Register<CalendarDatePicker, bool>(
nameof(IsDropDownOpen));
/// <summary>
/// Defines the <see cref="IsTodayHighlighted"/> property.
@ -59,11 +50,9 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="SelectedDate"/> property.
/// </summary>
public static readonly DirectProperty<CalendarDatePicker, DateTime?> SelectedDateProperty =
AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime?>(
public static readonly StyledProperty<DateTime?> SelectedDateProperty =
AvaloniaProperty.Register<CalendarDatePicker, DateTime?>(
nameof(SelectedDate),
o => o.SelectedDate,
(o, v) => o.SelectedDate = v,
enableDataValidation: true,
defaultBindingMode:BindingMode.TwoWay);
@ -88,11 +77,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="Text"/> property.
/// </summary>
public static readonly DirectProperty<CalendarDatePicker, string?> TextProperty =
AvaloniaProperty.RegisterDirect<CalendarDatePicker, string?>(
nameof(Text),
o => o.Text,
(o, v) => o.Text = v);
public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.Register<CalendarDatePicker, string?>(nameof(Text));
/// <summary>
/// Defines the <see cref="Watermark"/> property.
@ -141,8 +127,8 @@ namespace Avalonia.Controls
/// </exception>
public DateTime DisplayDate
{
get => _displayDate;
set => SetAndRaise(DisplayDateProperty, ref _displayDate, value);
get => GetValue(DisplayDateProperty);
set => SetValue(DisplayDateProperty, value);
}
/// <summary>
@ -151,8 +137,8 @@ namespace Avalonia.Controls
/// <value>The first date to display.</value>
public DateTime? DisplayDateStart
{
get => _displayDateStart;
set => SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value);
get => GetValue(DisplayDateStartProperty);
set => SetValue(DisplayDateStartProperty, value);
}
/// <summary>
@ -161,8 +147,8 @@ namespace Avalonia.Controls
/// <value>The last date to display.</value>
public DateTime? DisplayDateEnd
{
get => _displayDateEnd;
set => SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value);
get => GetValue(DisplayDateEndProperty);
set => SetValue(DisplayDateEndProperty, value);
}
/// <summary>
@ -188,8 +174,8 @@ namespace Avalonia.Controls
/// </value>
public bool IsDropDownOpen
{
get => _isDropDownOpen;
set => SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value);
get => GetValue(IsDropDownOpenProperty);
set => SetValue(IsDropDownOpenProperty, value);
}
/// <summary>
@ -223,8 +209,8 @@ namespace Avalonia.Controls
/// </exception>
public DateTime? SelectedDate
{
get => _selectedDate;
set => SetAndRaise(SelectedDateProperty, ref _selectedDate, value);
get => GetValue(SelectedDateProperty);
set => SetValue(SelectedDateProperty, value);
}
/// <summary>
@ -264,8 +250,8 @@ namespace Avalonia.Controls
/// </exception>
public string? Text
{
get => _text;
set => SetAndRaise(TextProperty, ref _text, value);
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
/// <inheritdoc cref="TextBox.Watermark"/>

52
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
/// </summary>
public CalendarDatePicker()
{
FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek;
SetCurrentValue(FirstDayOfWeekProperty, DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek);
_defaultText = string.Empty;
DisplayDate = DateTime.Today;
SetCurrentValue(DisplayDateProperty, DateTime.Today);
}
/// <summary>
@ -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;

28
src/Avalonia.Controls/ComboBox.cs

@ -35,11 +35,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="IsDropDownOpen"/> property.
/// </summary>
public static readonly DirectProperty<ComboBox, bool> IsDropDownOpenProperty =
AvaloniaProperty.RegisterDirect<ComboBox, bool>(
nameof(IsDropDownOpen),
o => o.IsDropDownOpen,
(o, v) => o.IsDropDownOpen = v);
public static readonly StyledProperty<bool> IsDropDownOpenProperty =
AvaloniaProperty.Register<ComboBox, bool>(nameof(IsDropDownOpen));
/// <summary>
/// Defines the <see cref="MaxDropDownHeight"/> property.
@ -77,7 +74,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>();
private bool _isDropDownOpen;
private Popup? _popup;
private object? _selectionBoxItem;
private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
@ -107,8 +103,8 @@ namespace Avalonia.Controls
/// </summary>
public bool IsDropDownOpen
{
get => _isDropDownOpen;
set => SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value);
get => GetValue(IsDropDownOpenProperty);
set => SetValue(IsDropDownOpenProperty, value);
}
/// <summary>
@ -123,10 +119,10 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the item to display as the control's content.
/// </summary>
protected object? SelectionBoxItem
public object? SelectionBoxItem
{
get => _selectionBoxItem;
set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value);
protected set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value);
}
/// <summary>
@ -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);
}
}

183
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
/// <summary>
/// Define the <see cref="DayFormat"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, string> DayFormatProperty =
AvaloniaProperty.RegisterDirect<DatePicker, string>(nameof(DayFormat),
x => x.DayFormat, (x, v) => x.DayFormat = v);
public static readonly StyledProperty<string> DayFormatProperty =
AvaloniaProperty.Register<DatePicker, string>(nameof(DayFormat), "%d");
/// <summary>
/// Defines the <see cref="DayVisible"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, bool> DayVisibleProperty =
AvaloniaProperty.RegisterDirect<DatePicker, bool>(nameof(DayVisible),
x => x.DayVisible, (x, v) => x.DayVisible = v);
public static readonly StyledProperty<bool> DayVisibleProperty =
AvaloniaProperty.Register<DatePicker, bool>(nameof(DayVisible), true);
/// <summary>
/// Defines the <see cref="MaxYear"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, DateTimeOffset> MaxYearProperty =
AvaloniaProperty.RegisterDirect<DatePicker, DateTimeOffset>(nameof(MaxYear),
x => x.MaxYear, (x, v) => x.MaxYear = v);
public static readonly StyledProperty<DateTimeOffset> MaxYearProperty =
AvaloniaProperty.Register<DatePicker, DateTimeOffset>(nameof(MaxYear), DateTimeOffset.MaxValue, coerce: CoerceMaxYear);
/// <summary>
/// Defines the <see cref="MinYear"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, DateTimeOffset> MinYearProperty =
AvaloniaProperty.RegisterDirect<DatePicker, DateTimeOffset>(nameof(MinYear),
x => x.MinYear, (x, v) => x.MinYear = v);
public static readonly StyledProperty<DateTimeOffset> MinYearProperty =
AvaloniaProperty.Register<DatePicker, DateTimeOffset>(nameof(MinYear), DateTimeOffset.MinValue, coerce: CoerceMinYear);
/// <summary>
/// Defines the <see cref="MonthFormat"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, string> MonthFormatProperty =
AvaloniaProperty.RegisterDirect<DatePicker, string>(nameof(MonthFormat),
x => x.MonthFormat, (x, v) => x.MonthFormat = v);
public static readonly StyledProperty<string> MonthFormatProperty =
AvaloniaProperty.Register<DatePicker, string>(nameof(MonthFormat), "MMMM");
/// <summary>
/// Defines the <see cref="MonthVisible"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, bool> MonthVisibleProperty =
AvaloniaProperty.RegisterDirect<DatePicker, bool>(nameof(MonthVisible),
x => x.MonthVisible, (x, v) => x.MonthVisible = v);
public static readonly StyledProperty<bool> MonthVisibleProperty =
AvaloniaProperty.Register<DatePicker, bool>(nameof(MonthVisible), true);
/// <summary>
/// Defines the <see cref="YearFormat"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, string> YearFormatProperty =
AvaloniaProperty.RegisterDirect<DatePicker, string>(nameof(YearFormat),
x => x.YearFormat, (x, v) => x.YearFormat = v);
public static readonly StyledProperty<string> YearFormatProperty =
AvaloniaProperty.Register<DatePicker, string>(nameof(YearFormat), "yyyy");
/// <summary>
/// Defines the <see cref="YearVisible"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, bool> YearVisibleProperty =
AvaloniaProperty.RegisterDirect<DatePicker, bool>(nameof(YearVisible),
x => x.YearVisible, (x, v) => x.YearVisible = v);
public static readonly StyledProperty<bool> YearVisibleProperty =
AvaloniaProperty.Register<DatePicker, bool>(nameof(YearVisible), true);
/// <summary>
/// Defines the <see cref="SelectedDate"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, DateTimeOffset?> SelectedDateProperty =
AvaloniaProperty.RegisterDirect<DatePicker, DateTimeOffset?>(nameof(SelectedDate),
x => x.SelectedDate, (x, v) => x.SelectedDate = v,
public static readonly StyledProperty<DateTimeOffset?> SelectedDateProperty =
AvaloniaProperty.Register<DatePicker, DateTimeOffset?>(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);
}
/// <summary>
@ -132,12 +114,8 @@ namespace Avalonia.Controls
/// </summary>
public bool DayVisible
{
get => _dayVisible;
set
{
SetAndRaise(DayVisibleProperty, ref _dayVisible, value);
SetGrid();
}
get => GetValue(DayVisibleProperty);
set => SetValue(DayVisibleProperty, value);
}
/// <summary>
@ -145,16 +123,24 @@ namespace Avalonia.Controls
/// </summary>
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);
}
/// <summary>
@ -162,16 +148,24 @@ namespace Avalonia.Controls
/// </summary>
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);
}
/// <summary>
@ -179,8 +173,8 @@ namespace Avalonia.Controls
/// </summary>
public string MonthFormat
{
get => _monthFormat;
set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value);
get => GetValue(MonthFormatProperty);
set => SetValue(MonthFormatProperty, value);
}
/// <summary>
@ -188,12 +182,8 @@ namespace Avalonia.Controls
/// </summary>
public bool MonthVisible
{
get => _monthVisible;
set
{
SetAndRaise(MonthVisibleProperty, ref _monthVisible, value);
SetGrid();
}
get => GetValue(MonthVisibleProperty);
set => SetValue(MonthVisibleProperty, value);
}
/// <summary>
@ -201,8 +191,8 @@ namespace Avalonia.Controls
/// </summary>
public string YearFormat
{
get => _yearFormat;
set => SetAndRaise(YearFormatProperty, ref _yearFormat, value);
get => GetValue(YearFormatProperty);
set => SetValue(YearFormatProperty, value);
}
/// <summary>
@ -210,12 +200,8 @@ namespace Avalonia.Controls
/// </summary>
public bool YearVisible
{
get => _yearVisible;
set
{
SetAndRaise(YearVisibleProperty, ref _yearVisible, value);
SetGrid();
}
get => GetValue(YearVisibleProperty);
set => SetValue(YearVisibleProperty, value);
}
/// <summary>
@ -223,14 +209,8 @@ namespace Avalonia.Controls
/// </summary>
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);
}
/// <summary>
@ -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<DateTimeOffset>());
}
else if (change.Property == MinYearProperty)
{
OnMinYearChanged(change.GetNewValue<DateTimeOffset>());
}
else if (change.Property == SelectedDateProperty)
{
SetSelectedDateText();
var (oldValue, newValue) = change.GetOldAndNewValue<DateTimeOffset?>();
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()

181
src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs

@ -35,65 +35,72 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="Date"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, DateTimeOffset> DateProperty =
AvaloniaProperty.RegisterDirect<DatePickerPresenter, DateTimeOffset>(nameof(Date),
x => x.Date, (x, v) => x.Date = v);
public static readonly StyledProperty<DateTimeOffset> DateProperty =
AvaloniaProperty.Register<DatePickerPresenter, DateTimeOffset>(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;
}
/// <summary>
/// Defines the <see cref="DayFormat"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, string> DayFormatProperty =
DatePicker.DayFormatProperty.AddOwner<DatePickerPresenter>(x =>
x.DayFormat, (x, v) => x.DayFormat = v);
public static readonly StyledProperty<string> DayFormatProperty =
DatePicker.DayFormatProperty.AddOwner<DatePickerPresenter>();
/// <summary>
/// Defines the <see cref="DayVisible"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, bool> DayVisibleProperty =
DatePicker.DayVisibleProperty.AddOwner<DatePickerPresenter>(x =>
x.DayVisible, (x, v) => x.DayVisible = v);
public static readonly StyledProperty<bool> DayVisibleProperty =
DatePicker.DayVisibleProperty.AddOwner<DatePickerPresenter>();
/// <summary>
/// Defines the <see cref="MaxYear"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, DateTimeOffset> MaxYearProperty =
DatePicker.MaxYearProperty.AddOwner<DatePickerPresenter>(x =>
x.MaxYear, (x, v) => x.MaxYear = v);
public static readonly StyledProperty<DateTimeOffset> MaxYearProperty =
DatePicker.MaxYearProperty.AddOwner<DatePickerPresenter>();
/// <summary>
/// Defines the <see cref="MinYear"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, DateTimeOffset> MinYearProperty =
DatePicker.MinYearProperty.AddOwner<DatePickerPresenter>(x =>
x.MinYear, (x, v) => x.MinYear = v);
public static readonly StyledProperty<DateTimeOffset> MinYearProperty =
DatePicker.MinYearProperty.AddOwner<DatePickerPresenter>();
/// <summary>
/// Defines the <see cref="MonthFormat"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, string> MonthFormatProperty =
DatePicker.MonthFormatProperty.AddOwner<DatePickerPresenter>(x =>
x.MonthFormat, (x, v) => x.MonthFormat = v);
public static readonly StyledProperty<string> MonthFormatProperty =
DatePicker.MonthFormatProperty.AddOwner<DatePickerPresenter>();
/// <summary>
/// Defines the <see cref="MonthVisible"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, bool> MonthVisibleProperty =
DatePicker.MonthVisibleProperty.AddOwner<DatePickerPresenter>(x =>
x.MonthVisible, (x, v) => x.MonthVisible = v);
public static readonly StyledProperty<bool> MonthVisibleProperty =
DatePicker.MonthVisibleProperty.AddOwner<DatePickerPresenter>();
/// <summary>
/// Defines the <see cref="YearFormat"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, string> YearFormatProperty =
DatePicker.YearFormatProperty.AddOwner<DatePickerPresenter>(x =>
x.YearFormat, (x, v) => x.YearFormat = v);
public static readonly StyledProperty<string> YearFormatProperty =
DatePicker.YearFormatProperty.AddOwner<DatePickerPresenter>();
/// <summary>
/// Defines the <see cref="YearVisible"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, bool> YearVisibleProperty =
DatePicker.YearVisibleProperty.AddOwner<DatePickerPresenter>(x =>
x.YearVisible, (x, v) => x.YearVisible = v);
public static readonly StyledProperty<bool> YearVisibleProperty =
DatePicker.YearVisibleProperty.AddOwner<DatePickerPresenter>();
// 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<DatePickerPresenter>(KeyboardNavigationMode.Cycle);
}
private static void OnDateRangeChanged(DatePickerPresenter sender, AvaloniaPropertyChangedEventArgs e)
{
sender.CoerceValue(DateProperty);
}
/// <summary>
@ -143,13 +150,14 @@ namespace Avalonia.Controls
/// </summary>
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();
}
/// <summary>
@ -157,8 +165,8 @@ namespace Avalonia.Controls
/// </summary>
public string DayFormat
{
get => _dayFormat;
set => SetAndRaise(DayFormatProperty, ref _dayFormat, value);
get => GetValue(DayFormatProperty);
set => SetValue(DayFormatProperty, value);
}
/// <summary>
@ -166,11 +174,8 @@ namespace Avalonia.Controls
/// </summary>
public bool DayVisible
{
get => _dayVisible;
set
{
SetAndRaise(DayVisibleProperty, ref _dayVisible, value);
}
get => GetValue(DayVisibleProperty);
set => SetValue(DayVisibleProperty, value);
}
/// <summary>
@ -178,16 +183,8 @@ namespace Avalonia.Controls
/// </summary>
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);
}
/// <summary>
@ -195,16 +192,8 @@ namespace Avalonia.Controls
/// </summary>
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);
}
/// <summary>
@ -212,8 +201,8 @@ namespace Avalonia.Controls
/// </summary>
public string MonthFormat
{
get => _monthFormat;
set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value);
get => GetValue(MonthFormatProperty);
set => SetValue(MonthFormatProperty, value);
}
/// <summary>
@ -221,11 +210,8 @@ namespace Avalonia.Controls
/// </summary>
public bool MonthVisible
{
get => _monthVisible;
set
{
SetAndRaise(MonthVisibleProperty, ref _monthVisible, value);
}
get => GetValue(MonthVisibleProperty);
set => SetValue(MonthVisibleProperty, value);
}
/// <summary>
@ -233,8 +219,8 @@ namespace Avalonia.Controls
/// </summary>
public string YearFormat
{
get => _yearFormat;
set => SetAndRaise(YearFormatProperty, ref _yearFormat, value);
get => GetValue(YearFormatProperty);
set => SetValue(YearFormatProperty, value);
}
/// <summary>
@ -242,11 +228,8 @@ namespace Avalonia.Controls
/// </summary>
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<DateTimeOffset>());
}
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();
}

93
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
/// <summary>
/// Defines the <see cref="MinuteIncrement"/> property
/// </summary>
public static readonly DirectProperty<TimePicker, int> MinuteIncrementProperty =
AvaloniaProperty.RegisterDirect<TimePicker, int>(nameof(MinuteIncrement),
x => x.MinuteIncrement, (x, v) => x.MinuteIncrement = v);
public static readonly StyledProperty<int> MinuteIncrementProperty =
AvaloniaProperty.Register<TimePicker, int>(nameof(MinuteIncrement), 1, coerce: CoerceMinuteIncrement);
/// <summary>
/// Defines the <see cref="ClockIdentifier"/> property
/// </summary>
public static readonly DirectProperty<TimePicker, string> ClockIdentifierProperty =
AvaloniaProperty.RegisterDirect<TimePicker, string>(nameof(ClockIdentifier),
x => x.ClockIdentifier, (x, v) => x.ClockIdentifier = v);
public static readonly StyledProperty<string> ClockIdentifierProperty =
AvaloniaProperty.Register<TimePicker, string>(nameof(ClockIdentifier), "12HourClock", coerce: CoerceClockIdentifier);
/// <summary>
/// Defines the <see cref="SelectedTime"/> property
/// </summary>
public static readonly DirectProperty<TimePicker, TimeSpan?> SelectedTimeProperty =
AvaloniaProperty.RegisterDirect<TimePicker, TimeSpan?>(nameof(SelectedTime),
x => x.SelectedTime, (x, v) => x.SelectedTime = v,
public static readonly StyledProperty<TimeSpan?> SelectedTimeProperty =
AvaloniaProperty.Register<TimePicker, TimeSpan?>(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");
}
/// <summary>
@ -81,14 +73,16 @@ namespace Avalonia.Controls
/// </summary>
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;
}
/// <summary>
@ -96,15 +90,17 @@ namespace Avalonia.Controls
/// </summary>
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;
}
/// <summary>
@ -112,14 +108,8 @@ namespace Avalonia.Controls
/// </summary>
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);
}
/// <summary>
@ -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<TimeSpan?>();
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);
}
}
}

68
src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs

@ -30,28 +30,29 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="MinuteIncrement"/> property
/// </summary>
public static readonly DirectProperty<TimePickerPresenter, int> MinuteIncrementProperty =
TimePicker.MinuteIncrementProperty.AddOwner<TimePickerPresenter>(x => x.MinuteIncrement,
(x, v) => x.MinuteIncrement = v);
public static readonly StyledProperty<int> MinuteIncrementProperty =
TimePicker.MinuteIncrementProperty.AddOwner<TimePickerPresenter>();
/// <summary>
/// Defines the <see cref="ClockIdentifier"/> property
/// </summary>
public static readonly DirectProperty<TimePickerPresenter, string> ClockIdentifierProperty =
TimePicker.ClockIdentifierProperty.AddOwner<TimePickerPresenter>(x => x.ClockIdentifier,
(x, v) => x.ClockIdentifier = v);
public static readonly StyledProperty<string> ClockIdentifierProperty =
TimePicker.ClockIdentifierProperty.AddOwner<TimePickerPresenter>();
/// <summary>
/// Defines the <see cref="Time"/> property
/// </summary>
public static readonly DirectProperty<TimePickerPresenter, TimeSpan> TimeProperty =
AvaloniaProperty.RegisterDirect<TimePickerPresenter, TimeSpan>(nameof(Time),
x => x.Time, (x, v) => x.Time = v);
public static readonly StyledProperty<TimeSpan> TimeProperty =
AvaloniaProperty.Register<TimePickerPresenter, TimeSpan>(nameof(Time));
public TimePickerPresenter()
{
Time = DateTime.Now.TimeOfDay;
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
SetCurrentValue(TimeProperty, DateTime.Now.TimeOfDay);
}
static TimePickerPresenter()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue<TimePickerPresenter>(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";
/// <summary>
/// Gets or sets the minute increment in the selector
/// </summary>
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);
}
/// <summary>
@ -95,14 +85,8 @@ namespace Avalonia.Controls
/// </summary>
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);
}
/// <summary>
@ -110,12 +94,8 @@ namespace Avalonia.Controls
/// </summary>
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();
}

8
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);
}

23
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@ -35,17 +35,14 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="ShowMode"/> property
/// </summary>
public static readonly DirectProperty<FlyoutBase, FlyoutShowMode> ShowModeProperty =
AvaloniaProperty.RegisterDirect<FlyoutBase, FlyoutShowMode>(nameof(ShowMode),
x => x.ShowMode, (x, v) => x.ShowMode = v);
public static readonly StyledProperty<FlyoutShowMode> ShowModeProperty =
AvaloniaProperty.Register<FlyoutBase, FlyoutShowMode>(nameof(ShowMode));
/// <summary>
/// Defines the <see cref="OverlayInputPassThroughElement"/> property
/// </summary>
public static readonly DirectProperty<FlyoutBase, IInputElement?> OverlayInputPassThroughElementProperty =
Popup.OverlayInputPassThroughElementProperty.AddOwner<FlyoutBase>(
o => o._overlayInputPassThroughElement,
(o, v) => o._overlayInputPassThroughElement = v);
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
Popup.OverlayInputPassThroughElementProperty.AddOwner<FlyoutBase>();
/// <summary>
/// Defines the AttachedFlyout property
@ -56,12 +53,10 @@ namespace Avalonia.Controls.Primitives
private readonly Lazy<Popup> _popupLazy;
private bool _isOpen;
private Control? _target;
private FlyoutShowMode _showMode = FlyoutShowMode.Standard;
private Rect? _enlargedPopupRect;
private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable;
private Action<IPopupHost?>? _popupHostChangedHandler;
private IInputElement? _overlayInputPassThroughElement;
static FlyoutBase()
{
@ -98,8 +93,8 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public FlyoutShowMode ShowMode
{
get => _showMode;
set => SetAndRaise(ShowModeProperty, ref _showMode, value);
get => GetValue(ShowModeProperty);
set => SetValue(ShowModeProperty, value);
}
/// <summary>
@ -117,8 +112,8 @@ namespace Avalonia.Controls.Primitives
/// </summary>
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)

9
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();
}
}

27
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
/// <summary>
/// Defines the <see cref="Target"/> Direct property
/// </summary>
public static readonly DirectProperty<Label, IInputElement?> TargetProperty =
AvaloniaProperty.RegisterDirect<Label, IInputElement?>(nameof(Target), lbl => lbl.Target, (lbl, inp) => lbl.Target = inp);
/// <summary>
/// Label focus target storage field
/// </summary>
private IInputElement? _target;
public static readonly StyledProperty<IInputElement?> TargetProperty =
AvaloniaProperty.Register<Label, IInputElement?>(nameof(Target));
/// <summary>
/// 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);
}
}
}

22
src/Avalonia.Controls/MenuItem.cs

@ -27,11 +27,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
public static readonly DirectProperty<MenuItem, ICommand?> CommandProperty =
Button.CommandProperty.AddOwner<MenuItem>(
menuItem => menuItem.Command,
(menuItem, command) => menuItem.Command = command,
enableDataValidation: true);
public static readonly StyledProperty<ICommand?> CommandProperty =
Button.CommandProperty.AddOwner<MenuItem>(new(enableDataValidation: true));
/// <summary>
/// Defines the <see cref="HotKey"/> property.
@ -113,7 +110,6 @@ namespace Avalonia.Controls
private static readonly ITemplate<Panel> DefaultPanel =
new FuncTemplate<Panel>(() => new StackPanel());
private ICommand? _command;
private bool _commandCanExecute = true;
private bool _commandBindingError;
private Popup? _popup;
@ -217,8 +213,8 @@ namespace Avalonia.Controls
/// </summary>
public ICommand? Command
{
get { return _command; }
set { SetAndRaise(CommandProperty, ref _command, value); }
get => GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
/// <summary>
@ -337,7 +333,7 @@ namespace Avalonia.Controls
/// <remarks>
/// This has the same effect as setting <see cref="IsSubMenuOpen"/> to true.
/// </remarks>
public void Open() => IsSubMenuOpen = true;
public void Open() => SetCurrentValue(IsSubMenuOpenProperty, true);
/// <summary>
/// Closes the submenu.
@ -345,7 +341,7 @@ namespace Avalonia.Controls
/// <remarks>
/// This has the same effect as setting <see cref="IsSubMenuOpen"/> to false.
/// </remarks>
public void Close() => IsSubMenuOpen = false;
public void Close() => SetCurrentValue(IsSubMenuOpenProperty, false);
/// <inheritdoc/>
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

4
src/Avalonia.Controls/NativeMenu.cs

@ -79,12 +79,12 @@ namespace Avalonia.Controls
}
public static readonly DirectProperty<NativeMenu, NativeMenuItem?> ParentProperty =
AvaloniaProperty.RegisterDirect<NativeMenu, NativeMenuItem?>("Parent", o => o.Parent, (o, v) => o.Parent = v);
AvaloniaProperty.RegisterDirect<NativeMenu, NativeMenuItem?>(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);

142
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<EventArgs>
{
private readonly NativeMenuItem _parent;
@ -60,78 +37,70 @@ namespace Avalonia.Controls
Header = header;
}
public static readonly DirectProperty<NativeMenuItem, NativeMenu?> MenuProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu?>(nameof(Menu), o => o.Menu, (o, v) => o.Menu = v);
public static readonly StyledProperty<NativeMenu?> MenuProperty =
AvaloniaProperty.Register<NativeMenuItem, NativeMenu?>(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<NativeMenuItem, IBitmap?> IconProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, IBitmap?>(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<IBitmap?> IconProperty =
AvaloniaProperty.Register<NativeMenuItem, IBitmap?>(nameof(Icon));
public IBitmap? Icon
{
get => _icon;
set => SetAndRaise(IconProperty, ref _icon, value);
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public static readonly DirectProperty<NativeMenuItem, string?> HeaderProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, string?>(nameof(Header), o => o.Header, (o, v) => o.Header = v);
public static readonly StyledProperty<string?> HeaderProperty =
AvaloniaProperty.Register<NativeMenuItem, string?>(nameof(Header));
public string? Header
{
get => _header;
set => SetAndRaise(HeaderProperty, ref _header, value);
get => GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
public static readonly DirectProperty<NativeMenuItem, KeyGesture?> GestureProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, KeyGesture?>(nameof(Gesture), o => o.Gesture, (o, v) => o.Gesture = v);
public static readonly StyledProperty<KeyGesture?> GestureProperty =
AvaloniaProperty.Register<NativeMenuItem, KeyGesture?>(nameof(Gesture));
public KeyGesture? Gesture
{
get => _gesture;
set => SetAndRaise(GestureProperty, ref _gesture, value);
get => GetValue(GestureProperty);
set => SetValue(GestureProperty, value);
}
public static readonly DirectProperty<NativeMenuItem, bool> IsCheckedProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, bool>(
nameof(IsChecked),
o => o.IsChecked,
(o, v) => o.IsChecked = v);
public static readonly StyledProperty<bool> IsCheckedProperty =
AvaloniaProperty.Register<NativeMenuItem, bool>(nameof(IsChecked));
public bool IsChecked
{
get => _isChecked;
set => SetAndRaise(IsCheckedProperty, ref _isChecked, value);
get => GetValue(IsCheckedProperty);
set => SetValue(IsCheckedProperty, value);
}
public static readonly DirectProperty<NativeMenuItem, NativeMenuItemToggleType> ToggleTypeProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenuItemToggleType>(
nameof(ToggleType),
o => o.ToggleType,
(o, v) => o.ToggleType = v);
public static readonly StyledProperty<NativeMenuItemToggleType> ToggleTypeProperty =
AvaloniaProperty.Register<NativeMenuItem, NativeMenuItemToggleType>(nameof(ToggleType));
public NativeMenuItemToggleType ToggleType
{
get => _toggleType;
set => SetAndRaise(ToggleTypeProperty, ref _toggleType, value);
get => GetValue(ToggleTypeProperty);
set => SetValue(ToggleTypeProperty, value);
}
public static readonly DirectProperty<NativeMenuItem, ICommand?> CommandProperty =
Button.CommandProperty.AddOwner<NativeMenuItem>(
menuItem => menuItem.Command,
(menuItem, command) => menuItem.Command = command,
enableDataValidation: true);
public static readonly StyledProperty<ICommand?> CommandProperty =
Button.CommandProperty.AddOwner<NativeMenuItem>(new(enableDataValidation: true));
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
@ -139,37 +108,26 @@ namespace Avalonia.Controls
public static readonly StyledProperty<object?> CommandParameterProperty =
Button.CommandParameterProperty.AddOwner<NativeMenuItem>();
public static readonly DirectProperty<NativeMenuItem, bool> IsEnabledProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, bool>(nameof(IsEnabled), o => o.IsEnabled, (o, v) => o.IsEnabled = v, true);
public static readonly StyledProperty<bool> IsEnabledProperty =
AvaloniaProperty.Register<NativeMenuItem, bool>(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);
}
/// <summary>
@ -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,

4
src/Avalonia.Controls/NativeMenuItemBase.cs

@ -12,12 +12,12 @@ namespace Avalonia.Controls
}
public static readonly DirectProperty<NativeMenuItemBase, NativeMenu?> ParentProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItemBase, NativeMenu?>("Parent", o => o.Parent, (o, v) => o.Parent = v);
AvaloniaProperty.RegisterDirect<NativeMenuItemBase, NativeMenu?>(nameof(Parent), o => o.Parent);
public NativeMenu? Parent
{
get => _parent;
set => SetAndRaise(ParentProperty, ref _parent, value);
internal set => SetAndRaise(ParentProperty, ref _parent, value);
}
}
}

9
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
/// </summary>
public bool IsClosed
{
get { return _isClosed; }
set { SetAndRaise(IsClosedProperty, ref _isClosed, value); }
get => GetValue(IsClosedProperty);
set => SetValue(IsClosedProperty, value);
}
/// <summary>
/// Defines the <see cref="IsClosed"/> property.
/// </summary>
public static readonly DirectProperty<NotificationCard, bool> IsClosedProperty =
AvaloniaProperty.RegisterDirect<NotificationCard, bool>(nameof(IsClosed), o => o.IsClosed, (o, v) => o.IsClosed = v);
public static readonly StyledProperty<bool> IsClosedProperty =
AvaloniaProperty.Register<NotificationCard, bool>(nameof(IsClosed));
/// <summary>
/// Defines the <see cref="NotificationClosed"/> event.

90
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -43,16 +43,14 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="ClipValueToMinMax"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, bool> ClipValueToMinMaxProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, bool>(nameof(ClipValueToMinMax),
updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b);
public static readonly StyledProperty<bool> ClipValueToMinMaxProperty =
AvaloniaProperty.Register<NumericUpDown, bool>(nameof(ClipValueToMinMax));
/// <summary>
/// Defines the <see cref="NumberFormat"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, NumberFormatInfo?> NumberFormatProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, NumberFormatInfo?>(nameof(NumberFormat), o => o.NumberFormat,
(o, v) => o.NumberFormat = v, NumberFormatInfo.CurrentInfo);
public static readonly StyledProperty<NumberFormatInfo?> NumberFormatProperty =
AvaloniaProperty.Register<NumericUpDown, NumberFormatInfo?>(nameof(NumberFormat), NumberFormatInfo.CurrentInfo);
/// <summary>
/// Defines the <see cref="FormatString"/> property.
@ -87,30 +85,28 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="ParsingNumberStyle"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, NumberStyles> ParsingNumberStyleProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, NumberStyles>(nameof(ParsingNumberStyle),
updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style);
public static readonly StyledProperty<NumberStyles> ParsingNumberStyleProperty =
AvaloniaProperty.Register<NumericUpDown, NumberStyles>(nameof(ParsingNumberStyle), NumberStyles.Any);
/// <summary>
/// Defines the <see cref="Text"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, string?> TextProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, string?>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.Register<NumericUpDown, string?>(nameof(Text),
defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary>
/// Defines the <see cref="TextConverter"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, IValueConverter?> TextConverterProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, IValueConverter?>(nameof(TextConverter),
updown => updown.TextConverter, (o, v) => o.TextConverter = v, null, BindingMode.OneWay, false);
public static readonly StyledProperty<IValueConverter?> TextConverterProperty =
AvaloniaProperty.Register<NumericUpDown, IValueConverter?>(nameof(TextConverter), defaultBindingMode: BindingMode.OneWay);
/// <summary>
/// Defines the <see cref="Value"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, decimal?> ValueProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, decimal?>(nameof(Value), updown => updown.Value,
(updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
public static readonly StyledProperty<decimal?> ValueProperty =
AvaloniaProperty.Register<NumericUpDown, decimal?>(nameof(Value), coerce: (s,v) => ((NumericUpDown)s).OnCoerceValue(v),
defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary>
/// Defines the <see cref="Watermark"/> 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;
/// <summary>
/// Gets the Spinner template part.
@ -184,8 +174,8 @@ namespace Avalonia.Controls
/// </summary>
public bool ClipValueToMinMax
{
get { return _clipValueToMinMax; }
set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); }
get => GetValue(ClipValueToMinMaxProperty);
set => SetValue(ClipValueToMinMaxProperty, value);
}
/// <summary>
@ -193,8 +183,8 @@ namespace Avalonia.Controls
/// </summary>
public NumberFormatInfo? NumberFormat
{
get { return _numberFormat; }
set { SetAndRaise(NumberFormatProperty, ref _numberFormat, value); }
get => GetValue(NumberFormatProperty);
set => SetValue(NumberFormatProperty, value);
}
/// <summary>
@ -249,8 +239,8 @@ namespace Avalonia.Controls
/// </summary>
public NumberStyles ParsingNumberStyle
{
get { return _parsingNumberStyle; }
set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); }
get => GetValue(ParsingNumberStyleProperty);
set => SetValue(ParsingNumberStyleProperty, value);
}
/// <summary>
@ -258,8 +248,8 @@ namespace Avalonia.Controls
/// </summary>
public string? Text
{
get { return _text; }
set { SetAndRaise(TextProperty, ref _text, value); }
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
/// <summary>
@ -269,8 +259,8 @@ namespace Avalonia.Controls
/// </summary>
public IValueConverter? TextConverter
{
get { return _textConverter; }
set { SetAndRaise(TextConverterProperty, ref _textConverter, value); }
get => GetValue(TextConverterProperty);
set => SetValue(TextConverterProperty, value);
}
/// <summary>
@ -278,12 +268,8 @@ namespace Avalonia.Controls
/// </summary>
public decimal? Value
{
get { return _value; }
set
{
value = OnCoerceValue(value);
SetAndRaise(ValueProperty, ref _value, value);
}
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
/// <summary>
@ -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);
}
}
/// <summary>
/// Called when the <see cref="Text"/> property value changed.
/// </summary>
@ -675,8 +661,8 @@ namespace Avalonia.Controls
{
result = Minimum;
}
Value = MathUtilities.Clamp(result, Minimum, Maximum);
SetCurrentValue(ValueProperty, MathUtilities.Clamp(result, Minimum, Maximum));
}
/// <summary>
@ -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));
}
/// <summary>
@ -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);

35
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); }
}
/// <summary>
/// Gets whether the <see cref="Panel"/> hosts the items created by an <see cref="ItemsPresenter"/>.
/// </summary>
public bool IsItemsHost { get; internal set; }
event EventHandler<ChildIndexChangedEventArgs>? IChildIndexProvider.ChildIndexChanged
{
add
@ -129,24 +133,29 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
protected virtual void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
List<Control> controls;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
controls = e.NewItems!.OfType<Control>().ToList();
LogicalChildren.InsertRange(e.NewStartingIndex, controls);
if (!IsItemsHost)
{
LogicalChildren.InsertRange(e.NewStartingIndex, e.NewItems!.OfType<Control>().ToList());
}
VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems!.OfType<Visual>());
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<Control>().ToList();
LogicalChildren.RemoveAll(controls);
if (!IsItemsHost)
{
LogicalChildren.RemoveAll(e.OldItems!.OfType<Control>().ToList());
}
VisualChildren.RemoveAll(e.OldItems!.OfType<Visual>());
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;
}
/// <inheritdoc />
public bool TryGetTotalCount(out int count)
{
count = Children.Count;

55
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
{
/// <summary>
/// Gets or sets whether the system bars are visible.
/// </summary>
bool? IsSystemBarVisible { get; set; }
/// <summary>
/// Gets or sets whether the window draws edge to edge. behind any visibile system bars.
/// </summary>
bool DisplayEdgeToEdge { get; set; }
/// <summary>
/// Gets the current safe area padding.
/// </summary>
Thickness SafeAreaPadding { get; }
/// <summary>
/// Occurs when safe area for the current window changes.
/// </summary>
event EventHandler<SafeAreaChangedArgs>? SafeAreaChanged;
}
public class SafeAreaChangedArgs : EventArgs
{
public SafeAreaChangedArgs(Thickness safeArePadding)
{
SafeAreaPadding = safeArePadding;
}
/// <inheritdoc cref="IInsetsManager.GetSafeAreaPadding"/>
public Thickness SafeAreaPadding { get; }
}
public enum SystemBarTheme
{
/// <summary>
/// Light system bar theme, with light background and a dark foreground
/// </summary>
Light,
/// <summary>
/// Bark system bar theme, with dark background and a light foreground
/// </summary>
Dark
}
}

11
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -156,16 +156,13 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Defines the <see cref="RecognizesAccessKey"/> property
/// </summary>
public static readonly DirectProperty<ContentPresenter, bool> RecognizesAccessKeyProperty =
AvaloniaProperty.RegisterDirect<ContentPresenter, bool>(
nameof(RecognizesAccessKey),
cp => cp.RecognizesAccessKey, (cp, value) => cp.RecognizesAccessKey = value);
public static readonly StyledProperty<bool> RecognizesAccessKeyProperty =
AvaloniaProperty.Register<ContentPresenter, bool>(nameof(RecognizesAccessKey));
private Control? _child;
private bool _createdChild;
private IRecyclingDataTemplate? _recyclingDataTemplate;
private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
private bool _recognizesAccessKey;
/// <summary>
/// Initializes static members of the <see cref="ContentPresenter"/> class.
@ -386,8 +383,8 @@ namespace Avalonia.Controls.Presenters
/// </summary>
public bool RecognizesAccessKey
{
get => _recognizesAccessKey;
set => SetAndRaise(RecognizesAccessKeyProperty, ref _recognizesAccessKey, value);
get => GetValue(RecognizesAccessKeyProperty);
set => SetValue(RecognizesAccessKeyProperty, value);
}
/// <summary>

3
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);

47
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
/// </summary>
public class HeaderedItemsControl : ItemsControl, IContentPresenterHost
{
private IDisposable? _itemsBinding;
private bool _prepareItemContainerOnAttach;
/// <summary>
/// Defines the <see cref="Header"/> property.
/// </summary>
@ -60,6 +65,17 @@ namespace Avalonia.Controls.Primitives
/// <inheritdoc/>
IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
if (_prepareItemContainerOnAttach)
{
PrepareItemContainer();
_prepareItemContainerOnAttach = false;
}
}
/// <inheritdoc/>
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)

31
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
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
public static readonly DirectProperty<Popup, bool> IsOpenProperty =
AvaloniaProperty.RegisterDirect<Popup, bool>(
nameof(IsOpen),
o => o.IsOpen,
(o, v) => o.IsOpen = v);
public static readonly StyledProperty<bool> IsOpenProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(IsOpen));
/// <summary>
/// Defines the <see cref="PlacementAnchor"/> property.
@ -90,11 +86,8 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<bool> OverlayDismissEventPassThroughProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(OverlayDismissEventPassThrough));
public static readonly DirectProperty<Popup, IInputElement?> OverlayInputPassThroughElementProperty =
AvaloniaProperty.RegisterDirect<Popup, IInputElement?>(
nameof(OverlayInputPassThroughElement),
o => o.OverlayInputPassThroughElement,
(o, v) => o.OverlayInputPassThroughElement = v);
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
AvaloniaProperty.Register<Popup, IInputElement?>(nameof(OverlayInputPassThroughElement));
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
@ -121,10 +114,8 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<Popup, bool>(nameof(Topmost));
private bool _isOpenRequested;
private bool _isOpen;
private bool _ignoreIsOpenChanged;
private PopupOpenState? _openState;
private IInputElement? _overlayInputPassThroughElement;
private Action<IPopupHost?>? _popupHostChangedHandler;
/// <summary>
@ -209,8 +200,8 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public bool IsOpen
{
get { return _isOpen; }
set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
get => GetValue(IsOpenProperty);
set => SetValue(IsOpenProperty, value);
}
/// <summary>
@ -301,8 +292,8 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public IInputElement? OverlayInputPassThroughElement
{
get => _overlayInputPassThroughElement;
set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value);
get => GetValue(OverlayInputPassThroughElementProperty);
set => SetValue(OverlayInputPassThroughElementProperty, value);
}
/// <summary>
@ -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);

4
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
/// <param name="templatedParent">The templated parent to apply.</param>
internal static void ApplyTemplatedParent(StyledElement control, AvaloniaObject? templatedParent)
{
control.SetValue(TemplatedParentProperty, templatedParent);
control.TemplatedParent = templatedParent;
var children = control.LogicalChildren;
var count = children.Count;

31
src/Avalonia.Controls/Primitives/ToggleButton.cs

@ -15,12 +15,8 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="IsChecked"/> property.
/// </summary>
public static readonly DirectProperty<ToggleButton, bool?> IsCheckedProperty =
AvaloniaProperty.RegisterDirect<ToggleButton, bool?>(
nameof(IsChecked),
o => o.IsChecked,
(o, v) => o.IsChecked = v,
unsetValue: false,
public static readonly StyledProperty<bool?> IsCheckedProperty =
AvaloniaProperty.Register<ToggleButton, bool?>(nameof(IsChecked), false,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
@ -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
/// </summary>
public bool? IsChecked
{
get => _isChecked;
set
{
SetAndRaise(IsCheckedProperty, ref _isChecked, value);
UpdatePseudoClasses(IsChecked);
}
get => GetValue(IsCheckedProperty);
set => SetValue(IsCheckedProperty, value);
}
/// <summary>
@ -147,28 +137,31 @@ namespace Avalonia.Controls.Primitives
/// </summary>
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);
}
/// <summary>
@ -224,6 +217,8 @@ namespace Avalonia.Controls.Primitives
{
var newValue = change.GetNewValue<bool?>();
UpdatePseudoClasses(newValue);
#pragma warning disable CS0618 // Type or member is obsolete
switch (newValue)
{

59
src/Avalonia.Controls/RadioButton.cs

@ -98,31 +98,22 @@ namespace Avalonia.Controls
}
}
public static readonly DirectProperty<RadioButton, string?> GroupNameProperty =
AvaloniaProperty.RegisterDirect<RadioButton, string?>(
nameof(GroupName),
o => o.GroupName,
(o, v) => o.GroupName = v);
public static readonly StyledProperty<string?> GroupNameProperty =
AvaloniaProperty.Register<RadioButton, string?>(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<bool?>());
}
else if (change.Property == GroupNameProperty)
{
var (oldValue, newValue) = change.GetOldAndNewValue<string?>();
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))

12
src/Avalonia.Controls/SplitButton/SplitButton.cs

@ -42,10 +42,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
public static readonly DirectProperty<SplitButton, ICommand?> CommandProperty =
Button.CommandProperty.AddOwner<SplitButton>(
splitButton => splitButton.Command,
(splitButton, command) => splitButton.Command = command);
public static readonly StyledProperty<ICommand?> CommandProperty =
Button.CommandProperty.AddOwner<SplitButton>();
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
@ -59,8 +57,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<FlyoutBase?> FlyoutProperty =
Button.FlyoutProperty.AddOwner<SplitButton>();
private ICommand? _Command;
private Button? _primaryButton = null;
private Button? _secondaryButton = null;
@ -83,8 +79,8 @@ namespace Avalonia.Controls
/// </summary>
public ICommand? Command
{
get => _Command;
set => SetAndRaise(CommandProperty, ref _Command, value);
get => GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
/// <summary>

5
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<IStorageProviderFactory>()?.CreateProvider(this)
?? PlatformImpl?.TryGetFeature<IStorageProvider>()
?? throw new InvalidOperationException("StorageProvider platform implementation is not available.");
public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature<IInsetsManager>();
/// <inheritdoc/>
Point IRenderRoot.PointToClient(PixelPoint p)
{

14
src/Avalonia.Controls/TrayIcon.cs

@ -13,13 +13,10 @@ namespace Avalonia.Controls
public sealed class TrayIcons : AvaloniaList<TrayIcon>
{
}
public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable
{
private readonly ITrayIconImpl? _impl;
private ICommand? _command;
private TrayIcon(ITrayIconImpl? impl)
{
@ -85,11 +82,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
public static readonly DirectProperty<TrayIcon, ICommand?> CommandProperty =
Button.CommandProperty.AddOwner<TrayIcon>(
trayIcon => trayIcon.Command,
(trayIcon, command) => trayIcon.Command = command,
enableDataValidation: true);
public static readonly StyledProperty<ICommand?> CommandProperty =
Button.CommandProperty.AddOwner<TrayIcon>(new(enableDataValidation: true));
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
@ -136,8 +130,8 @@ namespace Avalonia.Controls
/// </summary>
public ICommand? Command
{
get => _command;
set => SetAndRaise(CommandProperty, ref _command, value);
get => GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
/// <summary>

4
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);
}
}

35
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
/// <summary>
/// Defines the <see cref="WindowStartupLocation"/> property.
/// </summary>
public static readonly DirectProperty<Window, WindowStartupLocation> WindowStartupLocationProperty =
AvaloniaProperty.RegisterDirect<Window, WindowStartupLocation>(
nameof(WindowStartupLocation),
o => o.WindowStartupLocation,
(o, v) => o.WindowStartupLocation = v);
public static readonly StyledProperty<WindowStartupLocation> WindowStartupLocationProperty =
AvaloniaProperty.Register<Window, WindowStartupLocation>(nameof(WindowStartupLocation));
public static readonly StyledProperty<bool> CanResizeProperty =
AvaloniaProperty.Register<Window, bool>(nameof(CanResize), true);
@ -171,7 +167,6 @@ namespace Avalonia.Controls
RoutedEvent.Register<Window, RoutedEventArgs>("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);
}
}
/// <summary>
/// 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);
}
}
/// <summary>
/// Gets the WindowDecorationMargin.
@ -324,7 +319,7 @@ namespace Avalonia.Controls
{
get => _windowDecorationMargin;
private set => SetAndRaise(WindowDecorationMarginProperty, ref _windowDecorationMargin, value);
}
}
/// <summary>
/// Gets the window margin that is hidden off the screen area.
@ -397,8 +392,8 @@ namespace Avalonia.Controls
/// </summary>
public WindowStartupLocation WindowStartupLocation
{
get { return _windowStartupLocation; }
set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLocation, value); }
get => GetValue(WindowStartupLocationProperty);
set => SetValue(WindowStartupLocationProperty, value);
}
/// <summary>
@ -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);
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save