Browse Source

Merge remote-tracking branch 'origin/master' into xunit-ext

# Conflicts:
#	src/Avalonia.Controls/TopLevel.cs
pull/10473/head
Max Katz 4 years ago
parent
commit
93c0274730
  1. 18
      azure-pipelines-integrationtests.yml
  2. 7
      native/Avalonia.Native/src/OSX/AvnView.mm
  3. 24
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  4. 5
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  5. 4
      nukebuild/Build.cs
  6. 1
      samples/BindingDemo/BindingDemo.csproj
  7. 2
      samples/ControlCatalog.Android/Resources/values/styles.xml
  8. 9
      samples/ControlCatalog.Browser/app.css
  9. 2
      samples/ControlCatalog.NetCore/Program.cs
  10. 4
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  11. 6
      samples/ControlCatalog.iOS/Info.plist
  12. 4
      samples/ControlCatalog/App.xaml.cs
  13. 38
      samples/ControlCatalog/MainView.xaml.cs
  14. 1
      samples/ControlCatalog/MainWindow.xaml.cs
  15. 2
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  16. 22
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  17. 30
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  18. 1
      samples/GpuInterop/GpuInterop.csproj
  19. 75
      samples/IntegrationTestApp/ShowWindowTest.axaml
  20. 31
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  21. 11
      samples/IntegrationTestApp/bundle.sh
  22. 1
      samples/MobileSandbox/MobileSandbox.csproj
  23. 1
      samples/PlatformSanityChecks/PlatformSanityChecks.csproj
  24. 1
      samples/Previewer/Previewer.csproj
  25. 1
      samples/ReactiveUIDemo/ReactiveUIDemo.csproj
  26. 1
      samples/RenderDemo/RenderDemo.csproj
  27. 1
      samples/Sandbox/Sandbox.csproj
  28. 1
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  29. 15
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  30. 6
      src/Android/Avalonia.Android/AvaloniaView.cs
  31. 235
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  32. 43
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  33. 7
      src/Avalonia.Base/AttachedProperty.cs
  34. 15
      src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs
  35. 12
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  36. 52
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  37. 130
      src/Avalonia.Base/Media/FontManager.cs
  38. 290
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  39. 4
      src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs
  40. 66
      src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
  41. 33
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  42. 107
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  43. 20
      src/Avalonia.Base/Media/IGlyphTypeface.cs
  44. 2
      src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs
  45. 15
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  46. 13
      src/Avalonia.Base/Media/Typeface.cs
  47. 30
      src/Avalonia.Base/Platform/IFontManagerImpl.cs
  48. 3
      src/Avalonia.Base/StyledElement.cs
  49. 7
      src/Avalonia.Base/StyledProperty.cs
  50. 5
      src/Avalonia.Base/Utilities/UriExtensions.cs
  51. 15
      src/Avalonia.Controls/AppBuilder.cs
  52. 34
      src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs
  53. 45
      src/Avalonia.Controls/Button.cs
  54. 90
      src/Avalonia.Controls/Calendar/Calendar.cs
  55. 14
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  56. 68
      src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs
  57. 52
      src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs
  58. 28
      src/Avalonia.Controls/ComboBox.cs
  59. 183
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  60. 181
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
  61. 93
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  62. 68
      src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
  63. 8
      src/Avalonia.Controls/Documents/InlineCollection.cs
  64. 23
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  65. 9
      src/Avalonia.Controls/ItemsControl.cs
  66. 27
      src/Avalonia.Controls/Label.cs
  67. 22
      src/Avalonia.Controls/MenuItem.cs
  68. 4
      src/Avalonia.Controls/NativeMenu.cs
  69. 142
      src/Avalonia.Controls/NativeMenuItem.cs
  70. 4
      src/Avalonia.Controls/NativeMenuItemBase.cs
  71. 9
      src/Avalonia.Controls/Notifications/NotificationCard.cs
  72. 90
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  73. 55
      src/Avalonia.Controls/Platform/IInsetsManager.cs
  74. 11
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  75. 2
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  76. 47
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  77. 31
      src/Avalonia.Controls/Primitives/Popup.cs
  78. 4
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  79. 31
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  80. 59
      src/Avalonia.Controls/RadioButton.cs
  81. 12
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  82. 5
      src/Avalonia.Controls/TopLevel.cs
  83. 14
      src/Avalonia.Controls/TrayIcon.cs
  84. 4
      src/Avalonia.Controls/TreeViewItem.cs
  85. 35
      src/Avalonia.Controls/Window.cs
  86. 5
      src/Avalonia.Controls/WindowBase.cs
  87. 90
      src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs
  88. 89
      src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs
  89. 79
      src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
  90. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  91. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  92. 67
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  93. 57
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs
  94. 6
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  95. 5
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
  96. 1
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
  97. 404
      src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs
  98. 13
      src/Avalonia.Fonts.Inter/AppBuilderExtension.cs
  99. 1
      src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj
  100. 14
      src/Avalonia.Fonts.Inter/InterFontCollection.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);
}
}

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

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

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)

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

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

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/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
{
@ -242,6 +244,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);

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>

2
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -166,7 +166,7 @@ namespace Avalonia.Controls.Presenters
}
Panel = ItemsPanel.Build();
Panel.SetValue(TemplatedParentProperty, TemplatedParent);
Panel.TemplatedParent = TemplatedParent;
Panel.IsItemsHost = true;
_scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo;
LogicalChildren.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;
@ -394,7 +395,9 @@ namespace Avalonia.Controls
??= AvaloniaLocator.Current.GetService<IStorageProviderFactory>()?.CreateProvider(this)
?? PlatformImpl?.TryGetFeature<IStorageProvider>()
?? new NoopStorageProvider();
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);
}

5
src/Avalonia.Controls/WindowBase.cs

@ -27,10 +27,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="Owner"/> property.
/// </summary>
public static readonly DirectProperty<WindowBase, WindowBase?> OwnerProperty =
AvaloniaProperty.RegisterDirect<WindowBase, WindowBase?>(
nameof(Owner),
o => o.Owner,
(o, v) => o.Owner = v);
AvaloniaProperty.RegisterDirect<WindowBase, WindowBase?>(nameof(Owner), o => o.Owner);
public static readonly StyledProperty<bool> TopmostProperty =
AvaloniaProperty.Register<WindowBase, bool>(nameof(Topmost));

90
src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs

@ -0,0 +1,90 @@
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Immutable;
namespace Avalonia.Diagnostics.Controls
{
internal sealed class BrushEditor : Control
{
/// <summary>
/// Defines the <see cref="Brush" /> property.
/// </summary>
public static readonly DirectProperty<BrushEditor, IBrush?> BrushProperty =
AvaloniaProperty.RegisterDirect<BrushEditor, IBrush?>(
nameof(Brush), o => o.Brush, (o, v) => o.Brush = v);
private IBrush? _brush;
public IBrush? Brush
{
get => _brush;
set => SetAndRaise(BrushProperty, ref _brush, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == BrushProperty)
{
switch (Brush)
{
case ISolidColorBrush scb:
{
var colorView = new ColorView { Color = scb.Color };
colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor);
FlyoutBase.SetAttachedFlyout(this, new Flyout { Content = colorView });
ToolTip.SetTip(this, $"{scb.Color} ({Brush.GetType().Name})");
break;
}
default:
FlyoutBase.SetAttachedFlyout(this, null);
ToolTip.SetTip(this, Brush?.GetType().Name ?? "(null)");
break;
}
InvalidateVisual();
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
FlyoutBase.ShowAttachedFlyout(this);
}
public override void Render(DrawingContext context)
{
base.Render(context);
if (Brush != null)
{
context.FillRectangle(Brush, Bounds);
}
else
{
context.FillRectangle(Brushes.Black, Bounds);
var ft = new FormattedText("(null)",
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
Typeface.Default,
10,
Brushes.White);
context.DrawText(ft,
new Point(Bounds.Width / 2 - ft.Width / 2, Bounds.Height / 2 - ft.Height / 2));
}
}
}
}

89
src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs

@ -0,0 +1,89 @@
using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Styling;
namespace Avalonia.Diagnostics.Controls
{
//TODO: UpdateSourceTrigger & Binding.ValidationRules could help removing the need for this control.
internal sealed class CommitTextBox : TextBox, IStyleable
{
Type IStyleable.StyleKey => typeof(TextBox);
/// <summary>
/// Defines the <see cref="CommittedText" /> property.
/// </summary>
public static readonly DirectProperty<CommitTextBox, string?> CommittedTextProperty =
AvaloniaProperty.RegisterDirect<CommitTextBox, string?>(
nameof(CommittedText), o => o.CommittedText, (o, v) => o.CommittedText = v);
private string? _committedText;
public string? CommittedText
{
get => _committedText;
set => SetAndRaise(CommittedTextProperty, ref _committedText, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == CommittedTextProperty)
{
Text = CommittedText;
}
}
protected override void OnKeyUp(KeyEventArgs e)
{
base.OnKeyUp(e);
switch (e.Key)
{
case Key.Enter:
TryCommit();
e.Handled = true;
break;
case Key.Escape:
Cancel();
e.Handled = true;
break;
}
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
TryCommit();
}
private void Cancel()
{
Text = CommittedText;
DataValidationErrors.ClearErrors(this);
}
private void TryCommit()
{
if (!DataValidationErrors.GetHasErrors(this))
{
CommittedText = Text;
}
else
{
Text = CommittedText;
DataValidationErrors.ClearErrors(this);
}
}
}
}

79
src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs

@ -6,84 +6,73 @@ namespace Avalonia.Diagnostics.Controls
{
internal class ThicknessEditor : ContentControl
{
public static readonly DirectProperty<ThicknessEditor, Thickness> ThicknessProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, Thickness>(nameof(Thickness), o => o.Thickness,
(o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<Thickness> ThicknessProperty =
AvaloniaProperty.Register<ThicknessEditor, Thickness>(nameof(Thickness),
defaultBindingMode: BindingMode.TwoWay);
public static readonly DirectProperty<ThicknessEditor, string?> HeaderProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, string?>(nameof(Header), o => o.Header,
(o, v) => o.Header = v);
public static readonly StyledProperty<string?> HeaderProperty =
AvaloniaProperty.Register<ThicknessEditor, string?>(nameof(Header));
public static readonly DirectProperty<ThicknessEditor, bool> IsPresentProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, bool>(nameof(IsPresent), o => o.IsPresent,
(o, v) => o.IsPresent = v);
public static readonly StyledProperty<bool> IsPresentProperty =
AvaloniaProperty.Register<ThicknessEditor, bool>(nameof(IsPresent), true);
public static readonly DirectProperty<ThicknessEditor, double> LeftProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, double>(nameof(Left), o => o.Left, (o, v) => o.Left = v);
public static readonly StyledProperty<double> LeftProperty =
AvaloniaProperty.Register<ThicknessEditor, double>(nameof(Left));
public static readonly DirectProperty<ThicknessEditor, double> TopProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, double>(nameof(Top), o => o.Top, (o, v) => o.Top = v);
public static readonly StyledProperty<double> TopProperty =
AvaloniaProperty.Register<ThicknessEditor, double>(nameof(Top));
public static readonly DirectProperty<ThicknessEditor, double> RightProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, double>(nameof(Right), o => o.Right,
(o, v) => o.Right = v);
public static readonly StyledProperty<double> RightProperty =
AvaloniaProperty.Register<ThicknessEditor, double>(nameof(Right));
public static readonly DirectProperty<ThicknessEditor, double> BottomProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, double>(nameof(Bottom), o => o.Bottom,
(o, v) => o.Bottom = v);
public static readonly StyledProperty<double> BottomProperty =
AvaloniaProperty.Register<ThicknessEditor, double>(nameof(Bottom));
public static readonly StyledProperty<IBrush> HighlightProperty =
AvaloniaProperty.Register<ThicknessEditor, IBrush>(nameof(Highlight));
private Thickness _thickness;
private string? _header;
private bool _isPresent = true;
private double _left;
private double _top;
private double _right;
private double _bottom;
private bool _isUpdatingThickness;
public Thickness Thickness
{
get => _thickness;
set => SetAndRaise(ThicknessProperty, ref _thickness, value);
get => GetValue(ThicknessProperty);
set => SetValue(ThicknessProperty, value);
}
public string? Header
{
get => _header;
set => SetAndRaise(HeaderProperty, ref _header, value);
get => GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
public bool IsPresent
{
get => _isPresent;
set => SetAndRaise(IsPresentProperty, ref _isPresent, value);
get => GetValue(IsPresentProperty);
set => SetValue(IsPresentProperty, value);
}
public double Left
{
get => _left;
set => SetAndRaise(LeftProperty, ref _left, value);
get => GetValue(LeftProperty);
set => SetValue(LeftProperty, value);
}
public double Top
{
get => _top;
set => SetAndRaise(TopProperty, ref _top, value);
get => GetValue(TopProperty);
set => SetValue(TopProperty, value);
}
public double Right
{
get => _right;
set => SetAndRaise(RightProperty, ref _right, value);
get => GetValue(RightProperty);
set => SetValue(RightProperty, value);
}
public double Bottom
{
get => _bottom;
set => SetAndRaise(BottomProperty, ref _bottom, value);
get => GetValue(BottomProperty);
set => SetValue(BottomProperty, value);
}
public IBrush Highlight
@ -104,10 +93,10 @@ namespace Avalonia.Diagnostics.Controls
var value = change.GetNewValue<Thickness>();
Left = value.Left;
Top = value.Top;
Right = value.Right;
Bottom = value.Bottom;
SetCurrentValue(LeftProperty, value.Left);
SetCurrentValue(TopProperty, value.Top);
SetCurrentValue(RightProperty, value.Right);
SetCurrentValue(BottomProperty, value.Bottom);
}
finally
{
@ -118,7 +107,7 @@ namespace Avalonia.Diagnostics.Controls
(change.Property == LeftProperty || change.Property == TopProperty ||
change.Property == RightProperty || change.Property == BottomProperty))
{
Thickness = new Thickness(Left, Top, Right, Bottom);
SetCurrentValue(ThicknessProperty, new(Left, Top, Right, Bottom));
}
}
}

8
src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs

@ -35,15 +35,14 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Priority => _priority;
public override Type AssignedType => _assignedType;
public override string? Value
public override object? Value
{
get => ConvertToString(_value);
get => _value;
set
{
try
{
var convertedValue = ConvertFromString(value, Property.PropertyType);
_target.SetValue(Property, convertedValue);
_target.SetValue(Property, value);
Update();
}
catch { }
@ -54,6 +53,7 @@ namespace Avalonia.Diagnostics.ViewModels
public override Type? DeclaringType { get; }
public override Type PropertyType => _propertyType;
public override bool IsReadonly => Property.IsReadOnly;
// [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
public override void Update()

8
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs

@ -40,16 +40,16 @@ namespace Avalonia.Diagnostics.ViewModels
public override Type AssignedType => _assignedType;
public override Type PropertyType => _propertyType;
public override bool IsReadonly => !Property.CanWrite;
public override string? Value
public override object? Value
{
get => ConvertToString(_value);
get => _value;
set
{
try
{
var convertedValue = ConvertFromString(value, Property.PropertyType);
Property.SetValue(_target, convertedValue);
Property.SetValue(_target, value);
Update();
}
catch { }

67
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

@ -7,78 +7,21 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class PropertyViewModel : ViewModelBase
{
private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
private static readonly Type[] StringParameter = { typeof(string) };
private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) };
public abstract object Key { get; }
public abstract string Name { get; }
public abstract string Group { get; }
public abstract Type AssignedType { get; }
public abstract Type? DeclaringType { get; }
public abstract string? Value { get; set; }
public abstract object? Value { get; set; }
public abstract string Priority { get; }
public abstract bool? IsAttached { get; }
public abstract void Update();
public abstract Type PropertyType { get; }
public string Type => PropertyType == AssignedType
? PropertyType.GetTypeName()
: $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}";
protected static string? ConvertToString(object? value)
{
if (value is null)
{
return "(null)";
}
var converter = TypeDescriptor.GetConverter(value);
//CollectionConverter does not deliver any important information. It just displays "(Collection)".
if (!converter.CanConvertTo(typeof(string)) ||
converter.GetType() == typeof(CollectionConverter))
{
return value.ToString() ?? "(null)";
}
return converter.ConvertToString(value);
}
private static object? InvokeParse(string s, Type targetType)
{
var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
if (method != null)
{
return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture });
}
method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null);
if (method != null)
{
return method.Invoke(null, new object[] { s });
}
throw new InvalidCastException("Unable to convert value.");
}
protected static object? ConvertFromString(string? s, Type targetType)
{
if (s is null)
{
return null;
}
var converter = TypeDescriptor.GetConverter(targetType);
if (converter.CanConvertFrom(typeof(string)))
{
return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
}
public string Type => PropertyType == AssignedType ?
PropertyType.GetTypeName() :
$"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}";
return InvokeParse(s, targetType);
}
public abstract bool IsReadonly { get; }
}
}

57
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs

@ -0,0 +1,57 @@
using System;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using Avalonia.Reactive;
namespace Avalonia.Diagnostics.ViewModels
{
internal static class ReactiveExtensions
{
public static IObservable<TValue> GetObservable<TOwner, TValue>(
this TOwner vm,
Expression<Func<TOwner, TValue>> property,
bool fireImmediately = true)
where TOwner : INotifyPropertyChanged
{
return Observable.Create<TValue>(o =>
{
var propertyInfo = GetPropertyInfo(property);
void Fire()
{
o.OnNext((TValue) propertyInfo.GetValue(vm)!);
}
void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == propertyInfo.Name)
{
Fire();
}
}
if (fireImmediately)
{
Fire();
}
vm.PropertyChanged += OnPropertyChanged;
return Disposable.Create(() => vm.PropertyChanged -= OnPropertyChanged);
});
}
private static PropertyInfo GetPropertyInfo<TOwner, TValue>(this Expression<Func<TOwner, TValue>> property)
{
if (property.Body is UnaryExpression unaryExpression)
{
return (PropertyInfo)((MemberExpression)unaryExpression.Operand).Member;
}
var memExpr = (MemberExpression)property.Body;
return (PropertyInfo)memExpr.Member;
}
}
}

6
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -60,7 +60,11 @@
DoubleTapped="PropertiesGrid_OnDoubleTapped">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" x:DataType="vm:PropertyViewModel" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" x:DataType="vm:PropertyViewModel" />
<DataGridTemplateColumn Header="Value" Width="100">
<DataTemplate>
<local:PropertyValueEditorView />
</DataTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Type" Binding="{Binding Type}"
IsReadOnly="True"
IsVisible="{Binding !$parent[UserControl;2].((vm:MainViewModel)DataContext).ShowDetailsPropertyType}"

5
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs

@ -1,7 +1,6 @@
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
@ -18,7 +17,7 @@ namespace Avalonia.Diagnostics.Views
public MainView()
{
InitializeComponent();
AddHandler(KeyDownEvent, PreviewKeyDown, RoutingStrategies.Tunnel);
AddHandler(KeyUpEvent, PreviewKeyUp);
_console = this.GetControl<ConsoleView>("console");
_consoleSplitter = this.GetControl<GridSplitter>("consoleSplitter");
_rootGrid = this.GetControl<Grid>("rootGrid");
@ -58,7 +57,7 @@ namespace Avalonia.Diagnostics.Views
AvaloniaXamlLoader.Load(this);
}
private void PreviewKeyDown(object? sender, KeyEventArgs e)
private void PreviewKeyUp(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{

1
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml

@ -15,6 +15,7 @@
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml"/>
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml" />
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml" />
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml" />
</Window.Styles>
<Window.KeyBindings>
<KeyBinding Gesture="F8" Command="{Binding Shot}"/>

404
src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs

@ -0,0 +1,404 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Diagnostics.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Markup.Xaml.Converters;
using Avalonia.Media;
using Avalonia.Reactive;
namespace Avalonia.Diagnostics.Views
{
internal class PropertyValueEditorView : UserControl
{
private static readonly Geometry ImageIcon = Geometry.Parse(
"M12.25 6C8.79822 6 6 8.79822 6 12.25V35.75C6 37.1059 6.43174 38.3609 7.16525 39.3851L21.5252 25.0251C22.8921 23.6583 25.1081 23.6583 26.475 25.0251L40.8348 39.385C41.5683 38.3608 42 37.1058 42 35.75V12.25C42 8.79822 39.2018 6 35.75 6H12.25ZM34.5 17.5C34.5 19.7091 32.7091 21.5 30.5 21.5C28.2909 21.5 26.5 19.7091 26.5 17.5C26.5 15.2909 28.2909 13.5 30.5 13.5C32.7091 13.5 34.5 15.2909 34.5 17.5ZM39.0024 41.0881L24.7072 26.7929C24.3167 26.4024 23.6835 26.4024 23.293 26.7929L8.99769 41.0882C9.94516 41.6667 11.0587 42 12.25 42H35.75C36.9414 42 38.0549 41.6666 39.0024 41.0881Z");
private static readonly Geometry GeometryIcon = Geometry.Parse(
"M23.25 15.5H30.8529C29.8865 8.99258 24.2763 4 17.5 4C10.0442 4 4 10.0442 4 17.5C4 24.2763 8.99258 29.8865 15.5 30.8529V23.25C15.5 18.9698 18.9698 15.5 23.25 15.5ZM23.25 18C20.3505 18 18 20.3505 18 23.25V38.75C18 41.6495 20.3505 44 23.25 44H38.75C41.6495 44 44 41.6495 44 38.75V23.25C44 20.3505 41.6495 18 38.75 18H23.25Z");
private static readonly ColorToBrushConverter Color2Brush = new();
private readonly CompositeDisposable _cleanup = new();
private PropertyViewModel? Property => (PropertyViewModel?)DataContext;
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
Content = UpdateControl();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_cleanup.Clear();
}
private static bool ImplementsInterface<TInterface>(Type type)
{
var interfaceType = typeof(TInterface);
return type == interfaceType || interfaceType.IsAssignableFrom(type);
}
private Control? UpdateControl()
{
_cleanup.Clear();
if (Property?.PropertyType is not { } propertyType)
return null;
TControl CreateControl<TControl>(AvaloniaProperty valueProperty,
IValueConverter? converter = null,
Action<TControl>? init = null,
AvaloniaProperty? readonlyProperty = null)
where TControl : Control, new()
{
var control = new TControl();
init?.Invoke(control);
control.Bind(valueProperty,
new Binding(nameof(Property.Value), BindingMode.TwoWay)
{
Source = Property,
Converter = converter ?? new ValueConverter(),
ConverterParameter = propertyType
}).DisposeWith(_cleanup);
if (readonlyProperty != null)
{
control[readonlyProperty] = Property.IsReadonly;
}
else
{
control.IsEnabled = !Property.IsReadonly;
}
return control;
}
if (propertyType == typeof(bool))
return CreateControl<CheckBox>(ToggleButton.IsCheckedProperty);
//TODO: Infinity, NaN not working with NumericUpDown
if (propertyType.IsPrimitive && propertyType != typeof(float) && propertyType != typeof(double))
return CreateControl<NumericUpDown>(
NumericUpDown.ValueProperty,
new ValueToDecimalConverter(),
init: n =>
{
n.Increment = 1;
n.NumberFormat = new NumberFormatInfo { NumberDecimalDigits = 0 };
n.ParsingNumberStyle = NumberStyles.Integer;
},
readonlyProperty: NumericUpDown.IsReadOnlyProperty);
if (propertyType == typeof(Color))
{
var el = new Ellipse { Width = 12, Height = 12, VerticalAlignment = VerticalAlignment.Center };
el.Bind(
Shape.FillProperty,
new Binding(nameof(Property.Value)) { Source = Property, Converter = Color2Brush })
.DisposeWith(_cleanup);
var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
tbl.Bind(
TextBlock.TextProperty,
new Binding(nameof(Property.Value)) { Source = Property })
.DisposeWith(_cleanup);
var sp = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 2,
Children = { el, tbl },
Background = Brushes.Transparent,
Cursor = new Cursor(StandardCursorType.Hand),
IsEnabled = !Property.IsReadonly
};
var cv = new ColorView();
cv.Bind(
ColorView.ColorProperty,
new Binding(nameof(Property.Value), BindingMode.TwoWay)
{
Source = Property, Converter = Color2Brush
})
.DisposeWith(_cleanup);
FlyoutBase.SetAttachedFlyout(sp, new Flyout { Content = cv });
sp.PointerPressed += (_, _) => FlyoutBase.ShowAttachedFlyout(sp);
return sp;
}
if (ImplementsInterface<IBrush>(propertyType))
return CreateControl<BrushEditor>(BrushEditor.BrushProperty);
var isImage = ImplementsInterface<IImage>(propertyType);
var isGeometry = propertyType == typeof(Geometry);
if (isImage || isGeometry)
{
var valueObservable = Property.GetObservable(x => x.Value);
var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
tbl.Bind(TextBlock.TextProperty,
valueObservable.Select(
value => value switch
{
IImage img => $"{img.Size.Width} x {img.Size.Height}",
Geometry geom => $"{geom.Bounds.Width} x {geom.Bounds.Height}",
_ => "(null)"
}))
.DisposeWith(_cleanup);
var sp = new StackPanel
{
Background = Brushes.Transparent,
Orientation = Orientation.Horizontal,
Spacing = 2,
Children =
{
new Path
{
Data = isImage ? ImageIcon : GeometryIcon,
Fill = Brushes.Gray,
Width = 12,
Height = 12,
Stretch = Stretch.Uniform,
VerticalAlignment = VerticalAlignment.Center
},
tbl
}
};
if (isImage)
{
var previewImage = new Image { Stretch = Stretch.Uniform, Width = 300, Height = 300 };
previewImage
.Bind(Image.SourceProperty, valueObservable)
.DisposeWith(_cleanup);
ToolTip.SetTip(sp, previewImage);
}
else
{
var previewShape = new Path
{
Stretch = Stretch.Uniform,
Fill = Brushes.White,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
previewShape
.Bind(Path.DataProperty, valueObservable)
.DisposeWith(_cleanup);
ToolTip.SetTip(sp, new Border { Child = previewShape, Width = 300, Height = 300 });
}
return sp;
}
if (propertyType.IsEnum)
return CreateControl<ComboBox>(
SelectingItemsControl.SelectedItemProperty, init: c =>
{
c.Items = Enum.GetValues(propertyType);
});
var tb = CreateControl<CommitTextBox>(
CommitTextBox.CommittedTextProperty,
new TextToValueConverter(),
t =>
{
t.Watermark = "(null)";
},
readonlyProperty: TextBox.IsReadOnlyProperty);
tb.IsReadOnly |= propertyType == typeof(object) ||
!StringConversionHelper.CanConvertFromString(propertyType);
if (!tb.IsReadOnly)
{
tb.GetObservable(TextBox.TextProperty).Subscribe(t =>
{
try
{
if (t != null)
{
StringConversionHelper.FromString(t, propertyType);
}
DataValidationErrors.ClearErrors(tb);
}
catch (Exception ex)
{
DataValidationErrors.SetError(tb, ex.GetBaseException());
}
}).DisposeWith(_cleanup);
}
return tb;
}
//HACK: ValueConverter that skips first target update
//TODO: Would be nice to have some kind of "InitialBindingValue" option on TwoWay bindings to control
//if the first value comes from the source or target
private class ValueConverter : IValueConverter
{
private bool _firstUpdate = true;
object? IValueConverter.Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return Convert(value, targetType, parameter, culture);
}
object? IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (_firstUpdate)
{
_firstUpdate = false;
return BindingOperations.DoNothing;
}
//Note: targetType provided by Converter is simply "object"
return ConvertBack(value, (Type)parameter!, parameter, culture);
}
protected virtual object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value;
}
protected virtual object? ConvertBack(object? value, Type targetType, object? parameter,
CultureInfo culture)
{
return value;
}
}
private static class StringConversionHelper
{
private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
private static readonly Type[] StringParameter = { typeof(string) };
private static readonly Type[] StringFormatProviderParameters = { typeof(string), typeof(IFormatProvider) };
public static bool CanConvertFromString(Type type)
{
var converter = TypeDescriptor.GetConverter(type);
if (converter.CanConvertFrom(typeof(string)))
return true;
return GetParseMethod(type, out _) != null;
}
public static string? ToString(object o)
{
var converter = TypeDescriptor.GetConverter(o);
//CollectionConverter does not deliver any important information. It just displays "(Collection)".
if (!converter.CanConvertTo(typeof(string)) ||
converter.GetType() == typeof(CollectionConverter))
return o.ToString();
return converter.ConvertToInvariantString(o);
}
public static object? FromString(string str, Type type)
{
var converter = TypeDescriptor.GetConverter(type);
return converter.CanConvertFrom(typeof(string)) ?
converter.ConvertFrom(null, CultureInfo.InvariantCulture, str) :
InvokeParse(str, type);
}
private static object? InvokeParse(string s, Type targetType)
{
var m = GetParseMethod(targetType, out var hasFormat);
if (m == null)
throw new InvalidOperationException();
return m.Invoke(null,
hasFormat ?
new object[] { s, CultureInfo.InvariantCulture } :
new object[] { s });
}
private static MethodInfo? GetParseMethod(Type type, out bool hasFormat)
{
var m = type.GetMethod("Parse", PublicStatic, null, StringFormatProviderParameters, null);
if (m != null)
{
hasFormat = true;
return m;
}
hasFormat = false;
return type.GetMethod("Parse", PublicStatic, null, StringParameter, null);
}
}
private sealed class ValueToDecimalConverter : ValueConverter
{
protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return System.Convert.ToDecimal(value);
}
protected override object? ConvertBack(object? value, Type targetType, object? parameter,
CultureInfo culture)
{
return System.Convert.ChangeType(value, targetType);
}
}
private sealed class TextToValueConverter : ValueConverter
{
protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value is null ? null : StringConversionHelper.ToString(value);
}
protected override object? ConvertBack(object? value, Type targetType, object? parameter,
CultureInfo culture)
{
if (value is not string s)
return null;
try
{
return StringConversionHelper.FromString(s, targetType);
}
catch
{
return BindingOperations.DoNothing;
}
}
}
}
}

13
src/Avalonia.Fonts.Inter/AppBuilderExtension.cs

@ -0,0 +1,13 @@
namespace Avalonia.Fonts.Inter
{
public static class AppBuilderExtension
{
public static AppBuilder WithInterFont(this AppBuilder appBuilder)
{
return appBuilder.ConfigureFonts(fontManager =>
{
fontManager.AddFontCollection(new InterFontCollection());
});
}
}
}

1
src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj

@ -8,6 +8,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

14
src/Avalonia.Fonts.Inter/InterFontCollection.cs

@ -0,0 +1,14 @@
using System;
using Avalonia.Media.Fonts;
namespace Avalonia.Fonts.Inter
{
public sealed class InterFontCollection : EmbeddedFontCollection
{
public InterFontCollection() : base(
new Uri("fonts:Inter", UriKind.Absolute),
new Uri("avares://Avalonia.Fonts.Inter/Assets", UriKind.Absolute))
{
}
}
}

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

Loading…
Cancel
Save