Browse Source

Merge branch 'master' into feature/ui-automation

ui-automation-test
Steven Kirk 5 years ago
parent
commit
651fffdfa6
  1. 50
      azure-pipelines.yml
  2. 48
      build.sh
  3. 1
      build/SharedVersion.props
  4. 2
      global.json
  5. 11
      native/Avalonia.Native/src/OSX/window.mm
  6. 1
      packages/Avalonia/AvaloniaBuildTasks.targets
  7. 11
      readme.md
  8. 8
      samples/ControlCatalog/MainWindow.xaml
  9. 2
      samples/ControlCatalog/Pages/ButtonPage.xaml
  10. 6
      samples/ControlCatalog/Pages/CheckBoxPage.xaml
  11. 20
      samples/ControlCatalog/Pages/DialogsPage.xaml
  12. 24
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  13. 11
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  14. 6
      samples/ControlCatalog/Pages/RadioButtonPage.xaml
  15. 6
      samples/ControlCatalog/Pages/ToggleSwitchPage.xaml
  16. 6
      src/Avalonia.Animation/Properties/AssemblyInfo.cs
  17. 10
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  18. 30
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  19. 8
      src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs
  20. 3
      src/Avalonia.Controls/Button.cs
  21. 15
      src/Avalonia.Controls/ContextMenu.cs
  22. 43
      src/Avalonia.Controls/ItemsControl.cs
  23. 3
      src/Avalonia.Controls/NativeMenuItem.cs
  24. 24
      src/Avalonia.Controls/Panel.cs
  25. 6
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  26. 41
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  27. 2
      src/Avalonia.Controls/Primitives/AccessText.cs
  28. 8
      src/Avalonia.Controls/Properties/AssemblyInfo.cs
  29. 10
      src/Avalonia.Controls/RepeatButton.cs
  30. 31
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  31. 49
      src/Avalonia.Controls/TrayIcon.cs
  32. 27
      src/Avalonia.Controls/Utils/IEnumerableUtils.cs
  33. 2
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs
  34. 2
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  35. 4
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  36. 4
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  37. 141
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  38. 31
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  39. 3
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
  40. 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  41. 17
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  42. 11
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
  43. 6
      src/Avalonia.FreeDesktop/DBusHelper.cs
  44. 178
      src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
  45. 2
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  46. 2
      src/Avalonia.Input/InputElement.cs
  47. 9
      src/Avalonia.Layout/ElementManager.cs
  48. 6
      src/Avalonia.Layout/Properties/AssemblyInfo.cs
  49. 26
      src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs
  50. 32
      src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs
  51. 7
      src/Avalonia.Styling/Properties/AssemblyInfo.cs
  52. 56
      src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs
  53. 145
      src/Avalonia.Styling/Styling/NthChildSelector.cs
  54. 23
      src/Avalonia.Styling/Styling/NthLastChildSelector.cs
  55. 16
      src/Avalonia.Styling/Styling/Selectors.cs
  56. 3
      src/Avalonia.Themes.Default/AutoCompleteBox.xaml
  57. 1
      src/Avalonia.Themes.Default/Button.xaml
  58. 1
      src/Avalonia.Themes.Default/CheckBox.xaml
  59. 3
      src/Avalonia.Themes.Default/ComboBox.xaml
  60. 1
      src/Avalonia.Themes.Default/ContextMenu.xaml
  61. 2
      src/Avalonia.Themes.Default/FlyoutPresenter.xaml
  62. 2
      src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml
  63. 4
      src/Avalonia.Themes.Default/MenuItem.xaml
  64. 22
      src/Avalonia.Themes.Default/OverlayPopupHost.xaml
  65. 2
      src/Avalonia.Themes.Default/PopupRoot.xaml
  66. 1
      src/Avalonia.Themes.Default/RadioButton.xaml
  67. 1
      src/Avalonia.Themes.Default/ToggleButton.xaml
  68. 1
      src/Avalonia.Themes.Default/ToggleSwitch.xaml
  69. 1
      src/Avalonia.Themes.Fluent/Controls/Button.xaml
  70. 1
      src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
  71. 4
      src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml
  72. 17
      src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml
  73. 1
      src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml
  74. 1
      src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml
  75. 1
      src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml
  76. 1
      src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml
  77. 9
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  78. 19
      src/Avalonia.X11/X11Platform.cs
  79. 2
      src/Avalonia.X11/X11Window.Ime.cs
  80. 47
      src/Avalonia.X11/XEmbedTrayIconImpl.cs
  81. 35
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
  82. 7
      src/Markup/Avalonia.Markup.Xaml/Properties/AssemblyInfo.cs
  83. 149
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  84. 6
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  85. 7
      src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs
  86. 2
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs
  87. 7
      src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs
  88. 7
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  89. 6
      src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs
  90. 8
      src/tools/MicroComGenerator/Program.cs
  91. 14
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  92. 17
      tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
  93. 75
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  94. 159
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  95. 197
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  96. 22
      tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs
  97. 291
      tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs
  98. 220
      tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs
  99. 7
      tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs

50
azure-pipelines.yml

@ -3,19 +3,23 @@ jobs:
pool:
vmImage: 'ubuntu-20.04'
steps:
- task: CmdLine@2
displayName: 'Install Nuke'
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.414'
inputs:
script: |
dotnet tool install --global Nuke.GlobalTool --version 0.24.0
version: 3.1.414
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 5.0.402'
inputs:
version: 5.0.402
- task: CmdLine@2
displayName: 'Run Nuke'
displayName: 'Run Build'
inputs:
script: |
export PATH="$PATH:$HOME/.dotnet/tools"
dotnet --info
printenv
nuke --target CiAzureLinux --configuration=Release
./build.sh --target CiAzureLinux --configuration=Release
- task: PublishTestResults@2
inputs:
@ -23,6 +27,7 @@ jobs:
testResultsFiles: '$(Build.SourcesDirectory)/artifacts/test-results/*.trx'
condition: not(canceled())
- job: macOS
variables:
SolutionDir: '$(Build.SourcesDirectory)'
@ -30,10 +35,15 @@ jobs:
vmImage: 'macOS-10.15'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.401'
displayName: 'Use .NET Core SDK 3.1.414'
inputs:
version: 3.1.401
version: 3.1.414
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 5.0.402'
inputs:
version: 5.0.402
- task: CmdLine@2
displayName: 'Install Mono 5.18'
inputs:
@ -45,6 +55,7 @@ jobs:
displayName: 'Generate avalonia-native'
inputs:
script: |
export PATH="`pwd`/sdk:$PATH"
cd src/tools/MicroComGenerator; dotnet run -i ../../Avalonia.Native/avn.idl --cpp ../../../native/Avalonia.Native/inc/avalonia-native.h
- task: Xcode@5
@ -58,13 +69,7 @@ jobs:
args: '-derivedDataPath ./'
- task: CmdLine@2
displayName: 'Install Nuke'
inputs:
script: |
dotnet tool install --global Nuke.GlobalTool --version 0.24.0
- task: CmdLine@2
displayName: 'Run Nuke'
displayName: 'Run Build'
inputs:
script: |
export COREHOST_TRACE=0
@ -72,10 +77,8 @@ jobs:
export DOTNET_CLI_TELEMETRY_OPTOUT=1
which dotnet
dotnet --info
export PATH="$PATH:$HOME/.dotnet/tools"
dotnet --info
printenv
nuke --target CiAzureOSX --configuration Release --skip-previewer
./build.sh --target CiAzureOSX --configuration Release --skip-previewer
- task: PublishTestResults@2
inputs:
@ -102,9 +105,14 @@ jobs:
SolutionDir: '$(Build.SourcesDirectory)'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.401'
displayName: 'Use .NET Core SDK 3.1.414'
inputs:
version: 3.1.414
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 5.0.402'
inputs:
version: 3.1.401
version: 5.0.402
- task: CmdLine@2
displayName: 'Install Nuke'

48
build.sh

@ -20,55 +20,11 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
###########################################################################
BUILD_PROJECT_FILE="$SCRIPT_DIR/nukebuild/_build.csproj"
TEMP_DIRECTORY="$SCRIPT_DIR//.tmp"
DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
DOTNET_INSTALL_URL="https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh"
DOTNET_CHANNEL="Current"
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
export NUGET_XMLDOC_MODE="skip"
###########################################################################
# EXECUTION
###########################################################################
function FirstJsonValue {
perl -nle 'print $1 if m{"'$1'": "([^"\-]+)",?}' <<< ${@:2}
}
# If global.json exists, load expected version
if [ -f "$DOTNET_GLOBAL_FILE" ]; then
DOTNET_VERSION=$(FirstJsonValue "version" $(cat "$DOTNET_GLOBAL_FILE"))
if [ "$DOTNET_VERSION" == "" ]; then
unset DOTNET_VERSION
fi
fi
# If dotnet is installed locally, and expected version is not set or installation matches the expected version
if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") || "$SKIP_DOTNET_DOWNLOAD" == "1" ]]; then
export DOTNET_EXE="$(command -v dotnet)"
else
DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
# Download install script
DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
mkdir -p "$TEMP_DIRECTORY"
curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
chmod +x "$DOTNET_INSTALL_FILE"
# Install by channel or version
if [ -z ${DOTNET_VERSION+x} ]; then
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
else
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
fi
fi
export PATH=$DOTNET_DIRECTORY:$PATH
echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)"
dotnet --info
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]}
dotnet run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]}

1
build/SharedVersion.props

@ -17,7 +17,6 @@
<RepositoryType>git</RepositoryType>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\avalonia.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
<DefineConstants Condition="$(SignAssembly) == true">$(DefineConstants);SIGNED_BUILD</DefineConstants>
</PropertyGroup>
<ItemGroup Label="PackageIcon">

2
global.json

@ -1,6 +1,6 @@
{
"sdk": {
"version": "3.1.401"
"version": "5.0.402"
},
"msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43",

11
native/Avalonia.Native/src/OSX/window.mm

@ -63,7 +63,6 @@ public:
[Window setBackingType:NSBackingStoreBuffered];
[Window setOpaque:false];
[Window setContentView: StandardContainer];
}
virtual HRESULT ObtainNSWindowHandle(void** ret) override
@ -146,6 +145,8 @@ public:
SetPosition(lastPositionSet);
UpdateStyle();
[Window setContentView: StandardContainer];
[Window setTitle:_lastTitle];
if(ShouldTakeFocusOnShow() && activate)
@ -344,6 +345,7 @@ public:
BaseEvents->Resized(AvnSize{x,y}, reason);
}
[StandardContainer setFrameSize:NSSize{x,y}];
[Window setContentSize:NSSize{x, y}];
}
@finally
@ -2429,7 +2431,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void)sendEvent:(NSEvent *)event
{
if(_parent != nullptr)
[super sendEvent:event];
/// This is to detect non-client clicks. This can only be done on Windows... not popups, hence the dynamic_cast.
if(_parent != nullptr && dynamic_cast<WindowImpl*>(_parent.getRaw()) != nullptr)
{
switch(event.type)
{
@ -2459,8 +2464,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
break;
}
}
[super sendEvent:event];
}
- (BOOL)isAccessibilityElement

1
packages/Avalonia/AvaloniaBuildTasks.targets

@ -54,6 +54,7 @@
<Output TaskParameter="HashResult" PropertyName="AvaloniaResourcesDependencyHash" />
</Hash>
<MakeDir Directories="$(IntermediateOutputPath)/Avalonia" />
<WriteLinesToFile Overwrite="true" File="$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache" Lines="$(AvaloniaResourcesDependencyHash)" WriteOnlyWhenDifferent="True" />
</Target>

11
readme.md

@ -60,6 +60,9 @@ See the [build instructions here](Documentation/build.md).
## Contributing
This project exists thanks to all the people who contribute.
<a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
Please read the [contribution guidelines](CONTRIBUTING.md) before submitting a pull request.
## Code of Conduct
@ -71,11 +74,6 @@ For more information see the [.NET Foundation Code of Conduct](https://dotnetfou
Avalonia is licenced under the [MIT licence](licence.md).
## Contributors
This project exists thanks to all the people who contribute. [[Contribute](https://avaloniaui.net/contributing)].
<a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
### Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)]
@ -95,7 +93,8 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
<a href="https://opencollective.com/Avalonia/sponsor/6/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/Avalonia/sponsor/7/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/Avalonia/sponsor/8/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/Avalonia/sponsor/9/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/9/avatar.svg"></a>
<a href="https://opencollective.com/Avalonia/sponsor/9/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/9/avatar.svg"></a>
<a href="https://baseheadinc.com/" target="_blank"><img height="50" src="https://baseheadinc.com/wp-content/uploads/2020/09/BH-Logo-for-Site-Header-New.png"></a>
## .NET Foundation

8
samples/ControlCatalog/MainWindow.xaml

@ -63,11 +63,11 @@
<Panel Margin="{Binding #MainWindow.OffScreenMargin}">
<DockPanel LastChildFill="True" Margin="{Binding #MainWindow.WindowDecorationMargin}">
<Menu Name="MainMenu" DockPanel.Dock="Top">
<MenuItem Header="File">
<MenuItem Header="Exit" Command="{Binding ExitCommand}" />
<MenuItem Header="_File">
<MenuItem Header="E_xit" Command="{Binding ExitCommand}" />
</MenuItem>
<MenuItem Header="Help">
<MenuItem Header="About" Command="{Binding AboutCommand}" />
<MenuItem Header="_Help">
<MenuItem Header="_About" Command="{Binding AboutCommand}" />
</MenuItem>
</Menu>
<local:MainView />

2
samples/ControlCatalog/Pages/ButtonPage.xaml

@ -10,7 +10,7 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8" Width="200">
<Button>Standard XAML Button</Button>
<Button>Standard _XAML Button</Button>
<Button Foreground="White">Foreground</Button>
<Button Background="{DynamicResource SystemAccentColor}">Background</Button>
<Button IsEnabled="False">Disabled</Button>

6
samples/ControlCatalog/Pages/CheckBoxPage.xaml

@ -11,9 +11,9 @@
Spacing="16">
<StackPanel Orientation="Vertical"
Spacing="16">
<CheckBox>Unchecked</CheckBox>
<CheckBox IsChecked="True">Checked</CheckBox>
<CheckBox IsChecked="{x:Null}">Indeterminate</CheckBox>
<CheckBox>_Unchecked</CheckBox>
<CheckBox IsChecked="True">_Checked</CheckBox>
<CheckBox IsChecked="{x:Null}">_Indeterminate</CheckBox>
<CheckBox IsChecked="True" IsEnabled="False">Disabled</CheckBox>
</StackPanel>
<StackPanel Orientation="Vertical"

20
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -3,15 +3,15 @@
x:Class="ControlCatalog.Pages.DialogsPage">
<StackPanel Orientation="Vertical" Spacing="4" Margin="4">
<CheckBox Name="UseFilters">Use filters</CheckBox>
<Button Name="OpenFile">Open File</Button>
<Button Name="SaveFile">Save File</Button>
<Button Name="SelectFolder">Select Folder</Button>
<Button Name="OpenBoth">Select Both</Button>
<Button Name="DecoratedWindow">Decorated window</Button>
<Button Name="DecoratedWindowDialog">Decorated window (dialog)</Button>
<Button Name="Dialog">Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (No taskbar icon)</Button>
<Button Name="OwnedWindow">Owned window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No taskbar icon)</Button>
<Button Name="OpenFile">_Open File</Button>
<Button Name="SaveFile">_Save File</Button>
<Button Name="SelectFolder">Select Fo_lder</Button>
<Button Name="OpenBoth">Select _Both</Button>
<Button Name="DecoratedWindow">Decorated _window</Button>
<Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button>
<Button Name="Dialog">_Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button>
<Button Name="OwnedWindow">Own_ed window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button>
</StackPanel>
</UserControl>

24
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -1,17 +1,33 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ItemsRepeaterPage">
<UserControl.Styles>
<Style Selector="ItemsRepeater TextBlock.oddTemplate">
<Setter Property="Background" Value="Yellow" />
<Setter Property="Foreground" Value="Black" />
</Style>
<Style Selector="ItemsRepeater TextBlock.evenTemplate">
<Setter Property="Background" Value="Wheat" />
<Setter Property="Foreground" Value="Black" />
</Style>
<Style Selector="ItemsRepeater TextBlock:nth-child(5n+3)">
<Setter Property="Foreground" Value="Red" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="ItemsRepeater TextBlock:nth-last-child(5n+4)">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</UserControl.Styles>
<UserControl.Resources>
<RecyclePool x:Key="RecyclePool" />
<DataTemplate x:Key="odd">
<TextBlock Background="Yellow"
Foreground="Black"
<TextBlock Classes="oddTemplate"
Height="{Binding Height}"
Text="{Binding Text}"/>
</DataTemplate>
<DataTemplate x:Key="even">
<TextBlock Background="Wheat"
Foreground="Black"
<TextBlock Classes="evenTemplate"
Height="{Binding Height}"
Text="{Binding Text}"/>
</DataTemplate>

11
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -2,9 +2,20 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ListBoxPage">
<DockPanel>
<DockPanel.Styles>
<Style Selector="ListBox ListBoxItem:nth-child(5n+3)">
<Setter Property="Foreground" Value="Red" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="ListBox ListBoxItem:nth-last-child(5n+4)">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</DockPanel.Styles>
<StackPanel DockPanel.Dock="Top" Margin="4">
<TextBlock Classes="h1">ListBox</TextBlock>
<TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock>
<TextBlock Classes="h2">Each 5th item is highlighted with nth-child(5n+3) and nth-last-child(5n+4) rules.</TextBlock>
</StackPanel>
<StackPanel DockPanel.Dock="Right" Margin="4">
<CheckBox IsChecked="{Binding Multiple}">Multiple</CheckBox>

6
samples/ControlCatalog/Pages/RadioButtonPage.xaml

@ -11,9 +11,9 @@
Spacing="16">
<StackPanel Orientation="Vertical"
Spacing="16">
<RadioButton IsChecked="True">Option 1</RadioButton>
<RadioButton>Option 2</RadioButton>
<RadioButton IsChecked="{x:Null}">Option 3</RadioButton>
<RadioButton IsChecked="True">_Option 1</RadioButton>
<RadioButton>O_ption 2</RadioButton>
<RadioButton IsChecked="{x:Null}">Op_tion 3</RadioButton>
<RadioButton IsEnabled="False">Disabled</RadioButton>
</StackPanel>
<StackPanel Orientation="Vertical"

6
samples/ControlCatalog/Pages/ToggleSwitchPage.xaml

@ -14,7 +14,7 @@
<Border Classes="Thin">
<StackPanel>
<ToggleSwitch Content="headered" IsChecked="true" Margin="10"/>
<ToggleSwitch Content="h_eadered" IsChecked="true" Margin="10"/>
<TextBox Classes="CodeBox"
Text="&lt;ToggleSwitch&gt;headered&lt;/ToggleSwitch&gt;"/>
</StackPanel>
@ -24,7 +24,7 @@
<Border Classes="Thin">
<StackPanel>
<ToggleSwitch Content="Custom"
<ToggleSwitch Content="_Custom"
OnContent="On"
OffContent="Off"
Margin="10"/>
@ -40,7 +40,7 @@ ContentOff=&quot;Off&quot; /&gt;"
<Border Classes="Thin">
<StackPanel>
<ToggleSwitch Content="Just Click!" Margin="10">
<ToggleSwitch Content="_Just Click!" Margin="10">
<ToggleSwitch.OnContent>
<Image Source="/Assets/hirsch-899118_640.jpg" Height="32"/>
</ToggleSwitch.OnContent>

6
src/Avalonia.Animation/Properties/AssemblyInfo.cs

@ -1,15 +1,9 @@
using Avalonia.Metadata;
using System.Reflection;
using System.Runtime.CompilerServices;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")]
#if SIGNED_BUILD
[assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
#else
[assembly: InternalsVisibleTo("Avalonia.LeakTests")]
[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests")]
#endif

10
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -5,18 +5,10 @@ using System.Runtime.CompilerServices;
using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")]
#if SIGNED_BUILD
[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Visuals, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
#else
[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")]
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.Visuals")]
#endif

30
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -35,6 +35,7 @@ namespace Avalonia.Controls
private const int DATAGRIDCOLUMNHEADER_resizeRegionWidth = 5;
private const double DATAGRIDCOLUMNHEADER_separatorThickness = 1;
private const int DATAGRIDCOLUMNHEADER_columnsDragTreshold = 5;
private bool _areHandlersSuspended;
private static DragMode _dragMode;
@ -448,19 +449,6 @@ namespace Avalonia.Controls
OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders, distanceFromLeft, distanceFromRight);
// if we still haven't done anything about moving the mouse while
// the button is down, we remember that we're dragging, but we don't
// claim to have actually handled the event
if (_dragMode == DragMode.MouseDown)
{
_dragMode = DragMode.Drag;
}
_lastMousePositionHeaders = mousePositionHeaders;
if (args.Pointer.Captured != this && _dragMode == DragMode.Drag)
args.Pointer.Capture(this);
SetDragCursor(mousePosition);
}
@ -732,15 +720,19 @@ namespace Avalonia.Controls
{
return;
}
//handle entry into reorder mode
if (_dragMode == DragMode.MouseDown && _dragColumn == null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth))
if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth))
{
handled = CanReorderColumn(OwningColumn);
if (handled)
var distanceFromInitial = (Vector)(mousePositionHeaders - _lastMousePositionHeaders);
if (distanceFromInitial.Length > DATAGRIDCOLUMNHEADER_columnsDragTreshold)
{
OnMouseMove_BeginReorder(mousePosition);
handled = CanReorderColumn(OwningColumn);
if (handled)
{
OnMouseMove_BeginReorder(mousePosition);
}
}
}

8
src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs

@ -1,13 +1,9 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Metadata;
#if SIGNED_BUILD
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
#else
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport")]
#endif
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Collections")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")]

3
src/Avalonia.Controls/Button.cs

@ -100,6 +100,7 @@ namespace Avalonia.Controls
CommandParameterProperty.Changed.Subscribe(CommandParameterChanged);
IsDefaultProperty.Changed.Subscribe(IsDefaultChanged);
IsCancelProperty.Changed.Subscribe(IsCancelChanged);
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Button>((lbl, args) => lbl.OnAccessKey(args));
}
public Button()
@ -257,6 +258,8 @@ namespace Avalonia.Controls
}
}
protected virtual void OnAccessKey(RoutedEventArgs e) => OnClick();
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{

15
src/Avalonia.Controls/ContextMenu.cs

@ -333,16 +333,8 @@ namespace Avalonia.Controls
{
_popup = new Popup
{
HorizontalOffset = HorizontalOffset,
VerticalOffset = VerticalOffset,
PlacementAnchor = PlacementAnchor,
PlacementConstraintAdjustment = PlacementConstraintAdjustment,
PlacementGravity = PlacementGravity,
PlacementMode = PlacementMode,
PlacementRect = PlacementRect,
IsLightDismissEnabled = true,
OverlayDismissEventPassThrough = true,
WindowManagerAddShadowHint = WindowManagerAddShadowHint,
};
_popup.Opened += PopupOpened;
@ -362,6 +354,13 @@ namespace Avalonia.Controls
: PlacementMode;
_popup.PlacementTarget = placementTarget;
_popup.HorizontalOffset = HorizontalOffset;
_popup.VerticalOffset = VerticalOffset;
_popup.PlacementAnchor = PlacementAnchor;
_popup.PlacementConstraintAdjustment = PlacementConstraintAdjustment;
_popup.PlacementGravity = PlacementGravity;
_popup.PlacementRect = PlacementRect;
_popup.WindowManagerAddShadowHint = WindowManagerAddShadowHint;
_popup.Child = this;
IsOpen = true;
_popup.IsOpen = true;

43
src/Avalonia.Controls/ItemsControl.cs

@ -22,7 +22,7 @@ namespace Avalonia.Controls
/// Displays a collection of items.
/// </summary>
[PseudoClasses(":empty", ":singleitem")]
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider
{
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> property.
@ -57,6 +57,7 @@ namespace Avalonia.Controls
private IEnumerable _items = new AvaloniaList<object>();
private int _itemCount;
private IItemContainerGenerator _itemContainerGenerator;
private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
/// <summary>
/// Initializes static members of the <see cref="ItemsControl"/> class.
@ -146,11 +147,28 @@ namespace Avalonia.Controls
protected set;
}
event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
/// <inheritdoc/>
void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter)
{
if (Presenter is IChildIndexProvider oldInnerProvider)
{
oldInnerProvider.ChildIndexChanged -= PresenterChildIndexChanged;
}
Presenter = presenter;
ItemContainerGenerator.Clear();
if (Presenter is IChildIndexProvider innerProvider)
{
innerProvider.ChildIndexChanged += PresenterChildIndexChanged;
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
}
}
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
@ -512,5 +530,28 @@ namespace Avalonia.Controls
return null;
}
private void PresenterChildIndexChanged(object sender, ChildIndexChangedEventArgs e)
{
_childIndexChanged?.Invoke(this, e);
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
return Presenter is IChildIndexProvider innerProvider
? innerProvider.GetChildIndex(child) : -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
if (Presenter is IChildIndexProvider presenter
&& presenter.TryGetTotalCount(out count))
{
return true;
}
count = ItemCount;
return true;
}
}
}

3
src/Avalonia.Controls/NativeMenuItem.cs

@ -16,6 +16,7 @@ namespace Avalonia.Controls
private bool _isChecked = false;
private NativeMenuItemToggleType _toggleType;
private IBitmap _icon;
private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber;
private NativeMenu _menu;
@ -47,8 +48,6 @@ namespace Avalonia.Controls
}
}
private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber;
public NativeMenuItem()
{

24
src/Avalonia.Controls/Panel.cs

@ -2,8 +2,10 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Controls
{
@ -14,7 +16,7 @@ namespace Avalonia.Controls
/// Controls can be added to a <see cref="Panel"/> by adding them to its <see cref="Children"/>
/// collection. All children are layed out to fill the panel.
/// </remarks>
public class Panel : Control, IPanel
public class Panel : Control, IPanel, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -30,6 +32,8 @@ namespace Avalonia.Controls
AffectsRender<Panel>(BackgroundProperty);
}
private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
/// <summary>
/// Initializes a new instance of the <see cref="Panel"/> class.
/// </summary>
@ -53,6 +57,12 @@ namespace Avalonia.Controls
set { SetValue(BackgroundProperty, value); }
}
event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
/// <summary>
/// Renders the visual to a <see cref="DrawingContext"/>.
/// </summary>
@ -137,6 +147,7 @@ namespace Avalonia.Controls
throw new NotSupportedException();
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
InvalidateMeasureOnChildrenChanged();
}
@ -160,5 +171,16 @@ namespace Avalonia.Controls
var panel = control?.VisualParent as TPanel;
panel?.InvalidateMeasure();
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
return child is IControl control ? Children.IndexOf(control) : -1;
}
public bool TryGetTotalCount(out int count)
{
count = Children.Count;
return true;
}
}
}

6
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
@ -376,7 +377,10 @@ namespace Avalonia.Controls.Platform
{
if (item.IsSubMenuOpen)
{
if (item.IsTopLevel)
// PointerPressed events may bubble from disabled items in sub-menus. In this case,
// keep the sub-menu open.
var popup = (e.Source as ILogical)?.FindLogicalAncestorOfType<Popup>();
if (item.IsTopLevel && popup == null)
{
CloseMenu(item);
}

41
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -5,6 +5,7 @@ using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls.Presenters
@ -12,7 +13,7 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Base class for controls that present items inside an <see cref="ItemsControl"/>.
/// </summary>
public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl
public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="Items"/> property.
@ -36,6 +37,7 @@ namespace Avalonia.Controls.Presenters
private IDisposable _itemsSubscription;
private bool _createdPanel;
private IItemContainerGenerator _generator;
private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
/// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class.
@ -129,6 +131,12 @@ namespace Avalonia.Controls.Presenters
protected bool IsHosted => TemplatedParent is IItemsPresenterHost;
event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
/// <inheritdoc/>
public override sealed void ApplyTemplate()
{
@ -149,6 +157,8 @@ namespace Avalonia.Controls.Presenters
if (Panel != null)
{
ItemsChanged(e);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
}
}
@ -169,9 +179,21 @@ namespace Avalonia.Controls.Presenters
result.ItemTemplate = ItemTemplate;
}
result.Materialized += ContainerActionHandler;
result.Dematerialized += ContainerActionHandler;
result.Recycled += ContainerActionHandler;
return result;
}
private void ContainerActionHandler(object sender, ItemContainerEventArgs e)
{
for (var i = 0; i < e.Containers.Count; i++)
{
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl));
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
@ -248,5 +270,22 @@ namespace Avalonia.Controls.Presenters
{
(e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this);
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
if (child is IControl control && ItemContainerGenerator is { } generator)
{
var index = ItemContainerGenerator.IndexFromContainer(control);
return index;
}
return -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
return Items.TryGetCountFast(out count);
}
}
}

2
src/Avalonia.Controls/Primitives/AccessText.cs

@ -69,7 +69,7 @@ namespace Avalonia.Controls.Primitives
if (underscore != -1 && ShowAccessKey)
{
var rect = TextLayout.HitTestTextPosition(underscore);
var offset = new Vector(0, -0.5);
var offset = new Vector(0, -1.5);
context.DrawLine(
new Pen(Foreground, 1),
rect.BottomLeft + offset,

8
src/Avalonia.Controls/Properties/AssemblyInfo.cs

@ -1,13 +1,9 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Metadata;
#if SIGNED_BUILD
[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
#else
[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport")]
#endif
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Automation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]

10
src/Avalonia.Controls/RepeatButton.cs

@ -70,6 +70,16 @@ namespace Avalonia.Controls
_repeatTimer?.Stop();
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == IsPressedProperty && change.NewValue.GetValueOrDefault<bool>() == false)
{
StopTimer();
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);

31
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Utilities;
using Avalonia.VisualTree;
@ -19,7 +20,7 @@ namespace Avalonia.Controls
/// Represents a data-driven collection control that incorporates a flexible layout system,
/// custom views, and virtualization.
/// </summary>
public class ItemsRepeater : Panel
public class ItemsRepeater : Panel, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property.
@ -61,8 +62,9 @@ namespace Avalonia.Controls
private readonly ViewportManager _viewportManager;
private IEnumerable _items;
private VirtualizingLayoutContext _layoutContext;
private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
private bool _isLayoutInProgress;
private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs;
private ItemsRepeaterElementClearingEventArgs _elementClearingArgs;
private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs;
@ -163,6 +165,25 @@ namespace Avalonia.Controls
}
}
event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
return child is IControl control
? GetElementIndex(control)
: -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
count = ItemsSourceView.Count;
return true;
}
/// <summary>
/// Occurs each time an element is cleared and made available to be re-used.
/// </summary>
@ -545,6 +566,8 @@ namespace Avalonia.Controls
ElementPrepared(this, _elementPreparedArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
}
internal void OnElementClearing(IControl element)
@ -562,6 +585,8 @@ namespace Avalonia.Controls
ElementClearing(this, _elementClearingArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
}
internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex)
@ -579,6 +604,8 @@ namespace Avalonia.Controls
ElementIndexChanged(this, _elementIndexChangedArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
}
private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue)

49
src/Avalonia.Controls/TrayIcon.cs

@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using Avalonia.Collections;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Utilities;
#nullable enable
@ -13,10 +15,13 @@ 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)
{
@ -26,7 +31,15 @@ namespace Avalonia.Controls
_impl.SetIsVisible(IsVisible);
_impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty);
_impl.OnClicked = () =>
{
Clicked?.Invoke(this, EventArgs.Empty);
if (Command?.CanExecute(CommandParameter) == true)
{
Command.Execute(CommandParameter);
}
};
}
}
@ -64,6 +77,21 @@ namespace Avalonia.Controls
/// on OSX this event is not raised.
/// </summary>
public event EventHandler? Clicked;
/// <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);
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
/// </summary>
public static readonly StyledProperty<object?> CommandParameterProperty =
Button.CommandParameterProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="TrayIcons"/> attached property.
@ -98,6 +126,25 @@ namespace Avalonia.Controls
public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons);
public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty);
/// <summary>
/// Gets or sets the <see cref="Command"/> property of a TrayIcon.
/// </summary>
public ICommand? Command
{
get => _command;
set => SetAndRaise(CommandProperty, ref _command, value);
}
/// <summary>
/// Gets or sets the parameter to pass to the <see cref="Command"/> property of a
/// <see cref="TrayIcon"/>.
/// </summary>
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
/// <summary>
/// Gets or sets the Menu of the TrayIcon.

27
src/Avalonia.Controls/Utils/IEnumerableUtils.cs

@ -12,23 +12,36 @@ namespace Avalonia.Controls.Utils
return items.IndexOf(item) != -1;
}
public static int Count(this IEnumerable items)
public static bool TryGetCountFast(this IEnumerable items, out int count)
{
if (items != null)
{
if (items is ICollection collection)
{
return collection.Count;
count = collection.Count;
return true;
}
else if (items is IReadOnlyCollection<object> readOnly)
{
return readOnly.Count;
}
else
{
return Enumerable.Count(items.Cast<object>());
count = readOnly.Count;
return true;
}
}
count = 0;
return false;
}
public static int Count(this IEnumerable items)
{
if (TryGetCountFast(items, out var count))
{
return count;
}
else if (items != null)
{
return Enumerable.Count(items.Cast<object>());
}
else
{
return 0;

2
src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs

@ -25,7 +25,6 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport
private AutoResetEvent _wakeup = new AutoResetEvent(false);
private FrameMessage _lastFrameMessage = null;
private FrameMessage _lastSentFrameMessage = null;
private RequestViewportResizeMessage _lastViewportRequest;
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage;
private Action<IAvaloniaRemoteTransportConnection, Exception> _onException;
@ -177,6 +176,7 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport
public void Dispose()
{
_disposed = true;
_pendingSocket?.Dispose();
_simpleServer.Dispose();
}

2
src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs

@ -29,7 +29,7 @@ namespace Avalonia.Diagnostics.Models
}
}
public bool Handled { get; }
public bool Handled { get; set; }
public RoutingStrategies Route { get; }
}

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

@ -19,7 +19,7 @@ namespace Avalonia.Diagnostics.ViewModels
Name = property.IsAttached ?
$"[{property.OwnerType.Name}.{property.Name}]" :
property.Name;
DeclaringType = property.OwnerType;
Update();
}
@ -50,6 +50,8 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Group => _group;
public override System.Type? DeclaringType { get; }
// [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
public override void Update()
{

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

@ -24,7 +24,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
Name = property.DeclaringType.Name + '.' + property.Name;
}
DeclaringType = property.DeclaringType;
Update();
}
@ -55,6 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels
public override bool? IsAttached =>
default;
public override System.Type? DeclaringType { get; }
// [MemberNotNull(nameof(_type))]
public override void Update()
{

141
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -17,42 +17,26 @@ namespace Avalonia.Diagnostics.ViewModels
internal class ControlDetailsViewModel : ViewModelBase, IDisposable
{
private readonly IVisual _control;
private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
private IDictionary<object, List<PropertyViewModel>> _propertyIndex;
private PropertyViewModel? _selectedProperty;
private DataGridCollectionView _propertiesView;
private bool _snapshotStyles;
private bool _showInactiveStyles;
private string? _styleStatus;
private object _selectedEntity;
private readonly Stack<(string Name,object Entry)> _selectedEntitiesStack = new();
private string _selectedEntityName;
private string _selectedEntityType;
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
{
_control = control;
TreePage = treePage;
var properties = GetAvaloniaProperties(control)
.Concat(GetClrProperties(control))
.OrderBy(x => x, PropertyComparer.Instance)
.ThenBy(x => x.Name)
.ToList();
_propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList());
var view = new DataGridCollectionView(properties);
view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)));
view.Filter = FilterProperty;
PropertiesView = view;
Layout = new ControlLayoutViewModel(control);
if (control is INotifyPropertyChanged inpc)
{
inpc.PropertyChanged += ControlPropertyChanged;
}
if (control is AvaloniaObject ao)
{
ao.PropertyChanged += ControlPropertyChanged;
}
NavigateToProperty(control, (control as IControl)?.Name ?? control.ToString());
AppliedStyles = new ObservableCollection<StyleViewModel>();
PseudoClasses = new ObservableCollection<PseudoClassViewModel>();
@ -133,12 +117,46 @@ namespace Avalonia.Diagnostics.ViewModels
public TreePageViewModel TreePage { get; }
public DataGridCollectionView PropertiesView { get; }
public DataGridCollectionView PropertiesView
{
get => _propertiesView;
private set => RaiseAndSetIfChanged(ref _propertiesView, value);
}
public ObservableCollection<StyleViewModel> AppliedStyles { get; }
public ObservableCollection<PseudoClassViewModel> PseudoClasses { get; }
public object SelectedEntity
{
get => _selectedEntity;
set
{
RaiseAndSetIfChanged(ref _selectedEntity, value);
}
}
public string SelectedEntityName
{
get => _selectedEntityName;
set
{
RaiseAndSetIfChanged(ref _selectedEntityName, value);
}
}
public string SelectedEntityType
{
get => _selectedEntityType;
set
{
RaiseAndSetIfChanged(ref _selectedEntityType, value);
}
}
public PropertyViewModel? SelectedProperty
{
get => _selectedProperty;
@ -378,5 +396,78 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
}
public void ApplySelectedProperty()
{
var selectedProperty = SelectedProperty;
var selectedEntity = SelectedEntity;
var selectedEntityName = SelectedEntityName;
if (selectedProperty == null)
return;
object? property;
if (selectedProperty.Key is AvaloniaProperty avaloniaProperty)
{
property = (_selectedEntity as IControl)?.GetValue(avaloniaProperty);
}
else
{
property = selectedEntity.GetType().GetProperties()
.FirstOrDefault(pi => pi.Name == selectedProperty.Name
&& pi.DeclaringType == selectedProperty.DeclaringType
&& pi.PropertyType.Name == selectedProperty.Type)
?.GetValue(selectedEntity);
}
if (property == null) return;
_selectedEntitiesStack.Push((Name:selectedEntityName,Entry:selectedEntity));
NavigateToProperty(property, selectedProperty.Name);
}
public void ApplyParentProperty()
{
if (_selectedEntitiesStack.Any())
{
var property = _selectedEntitiesStack.Pop();
NavigateToProperty(property.Entry, property.Name);
}
}
protected void NavigateToProperty(object o, string entityName)
{
var oldSelectedEntity = SelectedEntity;
if (oldSelectedEntity is IAvaloniaObject ao1)
{
ao1.PropertyChanged -= ControlPropertyChanged;
}
else if (oldSelectedEntity is INotifyPropertyChanged inpc1)
{
inpc1.PropertyChanged -= ControlPropertyChanged;
}
SelectedEntity = o;
SelectedEntityName = entityName;
SelectedEntityType = o.ToString();
var properties = GetAvaloniaProperties(o)
.Concat(GetClrProperties(o))
.OrderBy(x => x, PropertyComparer.Instance)
.ThenBy(x => x.Name)
.ToList();
_propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList());
var view = new DataGridCollectionView(properties);
view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)));
view.Filter = FilterProperty;
PropertiesView = view;
if (o is IAvaloniaObject ao2)
{
ao2.PropertyChanged += ControlPropertyChanged;
}
else if (o is INotifyPropertyChanged inpc2)
{
inpc2.PropertyChanged += ControlPropertyChanged;
}
}
}
}

31
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs

@ -55,6 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels
// FIXME: This leaks event handlers.
Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true);
Event.RouteFinished.Subscribe(HandleRouteFinished);
_isRegistered = true;
}
}
@ -92,6 +94,35 @@ namespace Avalonia.Diagnostics.ViewModels
else
handler();
}
private void HandleRouteFinished(RoutedEventArgs e)
{
if (!_isRegistered || IsEnabled == false)
return;
if (e.Source is IVisual v && BelongsToDevTool(v))
return;
var s = e.Source;
var handled = e.Handled;
var route = e.Route;
void handler()
{
if (_currentEvent != null && handled)
{
var linkIndex = _currentEvent.EventChain.Count - 1;
var link = _currentEvent.EventChain[linkIndex];
link.Handled = true;
_currentEvent.HandledBy = link;
}
}
if (!Dispatcher.UIThread.CheckAccess())
Dispatcher.UIThread.Post(handler);
else
handler();
}
private static bool BelongsToDevTool(IVisual v)
{

3
src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs

@ -63,7 +63,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
if (EventChain.Count > 0)
{
var prevLink = EventChain[EventChain.Count-1];
var prevLink = EventChain[EventChain.Count - 1];
if (prevLink.Route != link.Route)
{
@ -72,6 +72,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
EventChain.Add(link);
if (HandledBy == null && link.Handled)
HandledBy = link;
}

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

@ -15,6 +15,7 @@ namespace Avalonia.Diagnostics.ViewModels
public abstract string Name { get; }
public abstract string Group { get; }
public abstract string Type { get; }
public abstract Type? DeclaringType { get; }
public abstract string Value { get; set; }
public abstract string Priority { get; }
public abstract bool? IsAttached { get; }

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

@ -13,23 +13,30 @@
<Grid ColumnDefinitions="*,Auto,320">
<Grid Grid.Column="0" RowDefinitions="Auto,*">
<Grid Grid.Column="0" RowDefinitions="Auto,Auto,*">
<controls:FilterTextBox Grid.Row="0"
<Grid ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto">
<Button Grid.Column="0" Grid.RowSpan="2" Content="^" Command="{Binding ApplyParentProperty}" />
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding SelectedEntityName}" FontWeight="Bold" />
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding SelectedEntityType}" FontStyle="Italic" />
</Grid>
<controls:FilterTextBox Grid.Row="1"
BorderThickness="0"
DataContext="{Binding TreePage.PropertiesFilter}"
Text="{Binding FilterString}"
Watermark="Filter properties"
UseCaseSensitiveFilter="{Binding UseCaseSensitiveFilter}"
UseWholeWordFilter="{Binding UseWholeWordFilter}"
UseRegexFilter="{Binding UseRegexFilter}" />
UseRegexFilter="{Binding UseRegexFilter}"/>
<DataGrid Items="{Binding PropertiesView}"
Grid.Row="1"
Grid.Row="2"
BorderThickness="0"
RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
CanUserResizeColumns="true">
CanUserResizeColumns="true"
DoubleTapped="PropertiesGrid_OnDoubleTapped">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" />

11
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs

@ -1,4 +1,6 @@
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
namespace Avalonia.Diagnostics.Views
@ -14,5 +16,14 @@ namespace Avalonia.Diagnostics.Views
{
AvaloniaXamlLoader.Load(this);
}
private void PropertiesGrid_OnDoubleTapped(object sender, TappedEventArgs e)
{
if (sender is DataGrid grid && grid.DataContext is ControlDetailsViewModel controlDetails)
{
controlDetails.ApplySelectedProperty();
}
}
}
}

6
src/Avalonia.FreeDesktop/DBusHelper.cs

@ -12,7 +12,7 @@ namespace Avalonia.FreeDesktop
/// This class uses synchronous execution at DBus connection establishment stage
/// then switches to using AvaloniaSynchronizationContext
/// </summary>
class DBusSyncContext : SynchronizationContext
private class DBusSyncContext : SynchronizationContext
{
private SynchronizationContext _ctx;
private object _lock = new object();
@ -51,10 +51,10 @@ namespace Avalonia.FreeDesktop
public static Connection TryInitialize(string dbusAddress = null)
{
return Connection ?? TryGetConnection(dbusAddress);
return Connection ?? TryCreateNewConnection(dbusAddress);
}
public static Connection TryGetConnection(string dbusAddress = null)
public static Connection TryCreateNewConnection(string dbusAddress = null)
{
var oldContext = SynchronizationContext.Current;
try

178
src/Avalonia.X11/X11TrayIconImpl.cs → src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs

@ -6,55 +6,65 @@ using System.Reactive.Disposables;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.FreeDesktop;
using Avalonia.Logging;
using Avalonia.Platform;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
namespace Avalonia.X11
[assembly:
InternalsVisibleTo(
"Avalonia.X11, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
namespace Avalonia.FreeDesktop
{
internal class X11TrayIconImpl : ITrayIconImpl
internal class DBusTrayIconImpl : ITrayIconImpl
{
private static int s_trayIconInstanceId;
private readonly ObjectPath _dbusMenuPath;
private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj;
private readonly Connection? _connection;
private DbusPixmap _icon;
private IDisposable? _serviceWatchDisposable;
private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj;
private IStatusNotifierWatcher? _statusNotifierWatcher;
private DbusPixmap _icon;
private string? _sysTrayServiceName;
private string? _tooltipText;
private bool _isActive;
private bool _isDisposed;
private readonly bool _ctorFinished;
private bool _serviceConnected;
private bool _isVisible = true;
public bool IsActive { get; private set; }
public INativeMenuExporter? MenuExporter { get; }
public Action? OnClicked { get; set; }
public Func<IWindowIconImpl?, uint[]>? IconConverterDelegate { get; set; }
public X11TrayIconImpl()
public DBusTrayIconImpl()
{
_connection = DBusHelper.TryGetConnection();
_connection = DBusHelper.TryCreateNewConnection();
if (_connection is null)
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)
Logger.TryGet(LogEventLevel.Error, "DBUS")
?.Log(this, "Unable to get a dbus connection for system tray icons.");
return;
}
IsActive = true;
_dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath;
MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection);
CreateTrayIcon();
_ctorFinished = true;
WatchAsync();
}
public async void CreateTrayIcon()
private void InitializeSNWService()
{
if (_connection is null)
return;
if (_connection is null || _isDisposed) return;
try
{
@ -64,12 +74,58 @@ namespace Avalonia.X11
}
catch
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)
Logger.TryGet(LogEventLevel.Error, "DBUS")
?.Log(this,
"org.kde.StatusNotifierWatcher service is not available on this system. Tray Icons will not work without it.");
return;
}
_serviceConnected = true;
}
private async void WatchAsync()
{
try
{
_serviceWatchDisposable =
await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!;
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, "DBUS")
?.Log(this,
"DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it.");
$"Unable to hook watcher method on org.kde.StatusNotifierWatcher: {e}");
}
}
if (_statusNotifierWatcher is null)
private void OnNameChange(ServiceOwnerChangedEventArgs obj)
{
if (_isDisposed)
return;
if (!_serviceConnected & obj.NewOwner != null)
{
_serviceConnected = true;
InitializeSNWService();
DestroyTrayIcon();
if (_isVisible)
{
CreateTrayIcon();
}
}
else if (_serviceConnected & obj.NewOwner is null)
{
DestroyTrayIcon();
_serviceConnected = false;
}
}
private void CreateTrayIcon()
{
if (_connection is null || !_serviceConnected || _isDisposed)
return;
var pid = Process.GetCurrentProcess().Id;
@ -78,45 +134,61 @@ namespace Avalonia.X11
_sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}";
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath);
await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj);
await _connection.RegisterServiceAsync(_sysTrayServiceName);
try
{
_connection.RegisterObjectAsync(_statusNotifierItemDbusObj);
_connection.RegisterServiceAsync(_sysTrayServiceName);
_statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, "DBUS")
?.Log(this, $"Error creating a DBus tray icon: {e}.");
await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
_serviceConnected = false;
}
_statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText);
_statusNotifierItemDbusObj.SetIcon(_icon);
_statusNotifierItemDbusObj.ActivationDelegate += OnClicked;
_isActive = true;
}
public async void DestroyTrayIcon()
private void DestroyTrayIcon()
{
if (_connection is null)
if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null)
return;
_connection.UnregisterObject(_statusNotifierItemDbusObj);
await _connection.UnregisterServiceAsync(_sysTrayServiceName);
_isActive = false;
_connection.UnregisterServiceAsync(_sysTrayServiceName);
}
public void Dispose()
{
IsActive = false;
_isDisposed = true;
DestroyTrayIcon();
_connection?.Dispose();
_serviceWatchDisposable?.Dispose();
}
public void SetIcon(IWindowIconImpl? icon)
{
if (_isDisposed)
if (_isDisposed || IconConverterDelegate is null)
return;
if (!(icon is X11IconData x11icon))
if (icon is null)
{
_statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap);
return;
}
var x11iconData = IconConverterDelegate(icon);
if (x11iconData.Length == 0) return;
var w = (int)x11icon.Data[0];
var h = (int)x11icon.Data[1];
var w = (int)x11iconData[0];
var h = (int)x11iconData[1];
var pixLength = w * h;
var pixByteArrayCounter = 0;
@ -124,7 +196,7 @@ namespace Avalonia.X11
for (var i = 0; i < pixLength; i++)
{
var rawPixel = x11icon.Data[i + 2].ToUInt32();
var rawPixel = x11iconData[i + 2];
pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24);
pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16);
pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8);
@ -137,18 +209,21 @@ namespace Avalonia.X11
public void SetIsVisible(bool visible)
{
if (_isDisposed || !_ctorFinished)
if (_isDisposed)
return;
if (visible & !_isActive)
{
DestroyTrayIcon();
CreateTrayIcon();
}
else if (!visible & _isActive)
switch (visible)
{
DestroyTrayIcon();
case true when !_isVisible:
DestroyTrayIcon();
CreateTrayIcon();
break;
case false when _isVisible:
DestroyTrayIcon();
break;
}
_isVisible = visible;
}
public void SetToolTipText(string? text)
@ -248,7 +323,20 @@ namespace Avalonia.X11
return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler));
}
public Task<object> GetAsync(string prop) => Task.FromResult(new object());
public Task<object?> GetAsync(string prop)
{
return Task.FromResult<object?>(prop switch
{
nameof(_backingProperties.Category) => _backingProperties.Category,
nameof(_backingProperties.Id) => _backingProperties.Id,
nameof(_backingProperties.Menu) => _backingProperties.Menu,
nameof(_backingProperties.IconPixmap) => _backingProperties.IconPixmap,
nameof(_backingProperties.Status) => _backingProperties.Status,
nameof(_backingProperties.Title) => _backingProperties.Title,
nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip,
_ => null
});
}
public Task<StatusNotifierItemProperties> GetAllAsync() => Task.FromResult(_backingProperties);
@ -298,17 +386,17 @@ namespace Avalonia.X11
Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError);
Task<object> GetAsync(string prop);
Task<object?> GetAsync(string prop);
Task<StatusNotifierItemProperties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
[Dictionary]
// This class is used by Tmds.Dbus to ferry properties
// from the SNI spec.
// Don't change this to actual C# properties since
// Tmds.Dbus will get confused.
[Dictionary]
internal class StatusNotifierItemProperties
{
public string? Category;
@ -363,5 +451,7 @@ namespace Avalonia.X11
Height = height;
Data = data;
}
public static DbusPixmap EmptyPixmap = new DbusPixmap(1, 1, new byte[] { 255, 0, 0, 0 });
}
}

2
src/Avalonia.Headless/HeadlessWindowImpl.cs

@ -200,7 +200,7 @@ namespace Avalonia.Headless
public ILockedFramebuffer Lock()
{
var bmp = new WriteableBitmap(PixelSize.FromSize(ClientSize, RenderScaling), new Vector(96, 96) * RenderScaling);
var bmp = new WriteableBitmap(PixelSize.FromSize(ClientSize, RenderScaling), new Vector(96, 96) * RenderScaling, PixelFormat.Rgba8888, AlphaFormat.Premul);
var fb = bmp.Lock();
return new FramebufferProxy(fb, () =>
{

2
src/Avalonia.Input/InputElement.cs

@ -16,7 +16,7 @@ namespace Avalonia.Input
/// <summary>
/// Implements input-related functionality for a control.
/// </summary>
[PseudoClasses(":disabled", ":focus", ":focus-visible", ":pointerover")]
[PseudoClasses(":disabled", ":focus", ":focus-visible", ":focus-within", ":pointerover")]
public class InputElement : Interactive, IInputElement
{
/// <summary>

9
src/Avalonia.Layout/ElementManager.cs

@ -314,12 +314,11 @@ namespace Avalonia.Layout
}
break;
// Remove clear all realized elements just to align the begavior
// with ViewManager which resets realized item indices to defaults.
// Freeing only removed items causes wrong indices to be stored
// in virtualized info of items under some circumstances.
case NotifyCollectionChangedAction.Remove:
{
OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
}
break;
case NotifyCollectionChangedAction.Reset:
ClearRealizedRange();
break;

6
src/Avalonia.Layout/Properties/AssemblyInfo.cs

@ -1,9 +1,7 @@
using System.Runtime.CompilerServices;
using Avalonia.Metadata;
#if SIGNED_BUILD
[assembly: InternalsVisibleTo("Avalonia.Layout.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
#else
[assembly: InternalsVisibleTo("Avalonia.Layout.UnitTests")]
#endif
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")]

26
src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs

@ -0,0 +1,26 @@
#nullable enable
using System;
namespace Avalonia.LogicalTree
{
/// <summary>
/// Event args for <see cref="IChildIndexProvider.ChildIndexChanged"/> event.
/// </summary>
public class ChildIndexChangedEventArgs : EventArgs
{
public ChildIndexChangedEventArgs()
{
}
public ChildIndexChangedEventArgs(ILogical child)
{
Child = child;
}
/// <summary>
/// Logical child which index was changed.
/// If null, all children should be reset.
/// </summary>
public ILogical? Child { get; }
}
}

32
src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs

@ -0,0 +1,32 @@
#nullable enable
using System;
namespace Avalonia.LogicalTree
{
/// <summary>
/// Child's index and total count information provider used by list-controls (ListBox, StackPanel, etc.)
/// </summary>
/// <remarks>
/// Used by nth-child and nth-last-child selectors.
/// </remarks>
public interface IChildIndexProvider
{
/// <summary>
/// Gets child's actual index in order of the original source.
/// </summary>
/// <param name="child">Logical child.</param>
/// <returns>Index or -1 if child was not found.</returns>
int GetChildIndex(ILogical child);
/// <summary>
/// Total children count or null if source is infinite.
/// Some Avalonia features might not work if <see cref="TryGetTotalCount"/> returns false, for instance: nth-last-child selector.
/// </summary>
bool TryGetTotalCount(out int count);
/// <summary>
/// Notifies subscriber when child's index or total count was changed.
/// </summary>
event EventHandler<ChildIndexChangedEventArgs>? ChildIndexChanged;
}
}

7
src/Avalonia.Styling/Properties/AssemblyInfo.cs

@ -1,12 +1,9 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.LogicalTree")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Styling")]
#if SIGNED_BUILD
[assembly: InternalsVisibleTo("Avalonia.Styling.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
#else
[assembly: InternalsVisibleTo("Avalonia.Styling.UnitTests")]
#endif

56
src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs

@ -0,0 +1,56 @@
#nullable enable
using Avalonia.LogicalTree;
namespace Avalonia.Styling.Activators
{
/// <summary>
/// An <see cref="IStyleActivator"/> which is active when control's index was changed.
/// </summary>
internal sealed class NthChildActivator : StyleActivatorBase
{
private readonly ILogical _control;
private readonly IChildIndexProvider _provider;
private readonly int _step;
private readonly int _offset;
private readonly bool _reversed;
public NthChildActivator(
ILogical control,
IChildIndexProvider provider,
int step, int offset, bool reversed)
{
_control = control;
_provider = provider;
_step = step;
_offset = offset;
_reversed = reversed;
}
protected override void Initialize()
{
PublishNext(IsMatching());
_provider.ChildIndexChanged += ChildIndexChanged;
}
protected override void Deinitialize()
{
_provider.ChildIndexChanged -= ChildIndexChanged;
}
private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e)
{
// Run matching again if:
// 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index.
// 2. e.Child is null, when all children indeces were changed.
// 3. Subscribed child index was changed.
if (_reversed
|| e.Child is null
|| e.Child == _control)
{
PublishNext(IsMatching());
}
}
private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
}
}

145
src/Avalonia.Styling/Styling/NthChildSelector.cs

@ -0,0 +1,145 @@
#nullable enable
using System;
using System.Text;
using Avalonia.LogicalTree;
using Avalonia.Styling.Activators;
namespace Avalonia.Styling
{
/// <summary>
/// The :nth-child() pseudo-class matches elements based on their position in a group of siblings.
/// </summary>
/// <remarks>
/// Element indices are 1-based.
/// </remarks>
public class NthChildSelector : Selector
{
private const string NthChildSelectorName = "nth-child";
private const string NthLastChildSelectorName = "nth-last-child";
private readonly Selector? _previous;
private readonly bool _reversed;
internal protected NthChildSelector(Selector? previous, int step, int offset, bool reversed)
{
_previous = previous;
Step = step;
Offset = offset;
_reversed = reversed;
}
/// <summary>
/// Creates an instance of <see cref="NthChildSelector"/>
/// </summary>
/// <param name="previous">Previous selector.</param>
/// <param name="step">Position step.</param>
/// <param name="offset">Initial index offset.</param>
public NthChildSelector(Selector? previous, int step, int offset)
: this(previous, step, offset, false)
{
}
public override bool InTemplate => _previous?.InTemplate ?? false;
public override bool IsCombinator => false;
public override Type? TargetType => _previous?.TargetType;
public int Step { get; }
public int Offset { get; }
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
{
if (!(control is ILogical logical))
{
return SelectorMatch.NeverThisType;
}
var controlParent = logical.LogicalParent;
if (controlParent is IChildIndexProvider childIndexProvider)
{
return subscribe
? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed))
: Evaluate(logical, childIndexProvider, Step, Offset, _reversed);
}
else
{
return SelectorMatch.NeverThisInstance;
}
}
internal static SelectorMatch Evaluate(
ILogical logical, IChildIndexProvider childIndexProvider,
int step, int offset, bool reversed)
{
var index = childIndexProvider.GetChildIndex(logical);
if (index < 0)
{
return SelectorMatch.NeverThisInstance;
}
if (reversed)
{
if (childIndexProvider.TryGetTotalCount(out var totalCountValue))
{
index = totalCountValue - index;
}
else
{
return SelectorMatch.NeverThisInstance;
}
}
else
{
// nth child index is 1-based
index += 1;
}
var n = Math.Sign(step);
var diff = index - offset;
var match = diff == 0 || (Math.Sign(diff) == n && diff % step == 0);
return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
}
protected override Selector? MovePrevious() => _previous;
public override string ToString()
{
var expectedCapacity = NthLastChildSelectorName.Length + 8;
var stringBuilder = new StringBuilder(_previous?.ToString(), expectedCapacity);
stringBuilder.Append(':');
stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName);
stringBuilder.Append('(');
var hasStep = false;
if (Step != 0)
{
hasStep = true;
stringBuilder.Append(Step);
stringBuilder.Append('n');
}
if (Offset > 0)
{
if (hasStep)
{
stringBuilder.Append('+');
}
stringBuilder.Append(Offset);
}
else if (Offset < 0)
{
stringBuilder.Append('-');
stringBuilder.Append(-Offset);
}
stringBuilder.Append(')');
return stringBuilder.ToString();
}
}
}

23
src/Avalonia.Styling/Styling/NthLastChildSelector.cs

@ -0,0 +1,23 @@
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
/// The :nth-child() pseudo-class matches elements based on their position among a group of siblings, counting from the end.
/// </summary>
/// <remarks>
/// Element indices are 1-based.
/// </remarks>
public class NthLastChildSelector : NthChildSelector
{
/// <summary>
/// Creates an instance of <see cref="NthLastChildSelector"/>
/// </summary>
/// <param name="previous">Previous selector.</param>
/// <param name="step">Position step.</param>
/// <param name="offset">Initial index offset, counting from the end.</param>
public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true)
{
}
}
}

16
src/Avalonia.Styling/Styling/Selectors.cs

@ -123,6 +123,22 @@ namespace Avalonia.Styling
return new NotSelector(previous, argument);
}
/// <inheritdoc cref="NthChildSelector"/>
/// <inheritdoc cref="NthChildSelector(Selector?, int, int)"/>
/// <returns>The selector.</returns>
public static Selector NthChild(this Selector previous, int step, int offset)
{
return new NthChildSelector(previous, step, offset);
}
/// <inheritdoc cref="NthLastChildSelector"/>
/// <inheritdoc cref="NthLastChildSelector(Selector?, int, int)"/>
/// <returns>The selector.</returns>
public static Selector NthLastChild(this Selector previous, int step, int offset)
{
return new NthLastChildSelector(previous, step, offset);
}
/// <summary>
/// Returns a selector which matches a type.
/// </summary>

3
src/Avalonia.Themes.Default/AutoCompleteBox.xaml

@ -21,7 +21,8 @@
MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="{TemplateBinding}"
IsLightDismissEnabled="True">
<Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
<Border Background="{DynamicResource ThemeBackgroundBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1">
<ListBox Name="PART_SelectingItemsControl"
BorderThickness="0"

1
src/Avalonia.Themes.Default/Button.xaml

@ -17,6 +17,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
RecognizesAccessKey="True"
TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>

1
src/Avalonia.Themes.Default/CheckBox.xaml

@ -41,6 +41,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
IsVisible="{TemplateBinding Content, Converter={x:Static ObjectConverters.IsNotNull}}"

3
src/Avalonia.Themes.Default/ComboBox.xaml

@ -69,7 +69,8 @@
MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="{TemplateBinding}"
IsLightDismissEnabled="True">
<Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
<Border Background="{DynamicResource ThemeBackgroundBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1">
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">

1
src/Avalonia.Themes.Default/ContextMenu.xaml

@ -1,4 +1,5 @@
<Style xmlns="https://github.com/avaloniaui" Selector="ContextMenu">
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="4,2"/>

2
src/Avalonia.Themes.Default/FlyoutPresenter.xaml

@ -2,7 +2,7 @@
<Style Selector="FlyoutPresenter">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="4" />

2
src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml

@ -1,6 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="MenuFlyoutPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />

4
src/Avalonia.Themes.Default/MenuItem.xaml

@ -62,7 +62,7 @@
PlacementMode="Right"
IsLightDismissEnabled="False"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Background="{TemplateBinding Background}"
<Border Background="{DynamicResource ThemeBackgroundBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer Classes="menuscroller">
@ -113,7 +113,7 @@
IsLightDismissEnabled="True"
OverlayInputPassThroughElement="{Binding $parent[Menu]}"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Background="{TemplateBinding Background}"
<Border Background="{DynamicResource ThemeBackgroundBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer Classes="menuscroller">

22
src/Avalonia.Themes.Default/OverlayPopupHost.xaml

@ -1,23 +1,21 @@
<Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Selector="OverlayPopupHost">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/>
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}" />
<Setter Property="FontFamily" Value="{x:Static FontFamily.Default}" />
<Setter Property="FontWeight" Value="400" />
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</Panel>
<!-- Do not forget to update Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent test -->
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</ControlTemplate>
</Setter>
</Style>

2
src/Avalonia.Themes.Default/PopupRoot.xaml

@ -1,6 +1,8 @@
<Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Selector="PopupRoot">
<Setter Property="Background" Value="{x:Null}"/>
<Setter Property="TransparencyLevelHint" Value="Transparent" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/>
<Setter Property="FontFamily" Value="{x:Static FontFamily.Default}" />

1
src/Avalonia.Themes.Default/RadioButton.xaml

@ -31,6 +31,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="4,0,0,0"
RecognizesAccessKey="True"
VerticalAlignment="Center"
Grid.Column="1"/>
</Grid>

1
src/Avalonia.Themes.Default/ToggleButton.xaml

@ -17,6 +17,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
RecognizesAccessKey="True"
TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>

1
src/Avalonia.Themes.Default/ToggleSwitch.xaml

@ -87,6 +87,7 @@
Grid.Row="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
RecognizesAccessKey="True"
VerticalAlignment="Top"/>
<Grid Grid.Row="1"

1
src/Avalonia.Themes.Fluent/Controls/Button.xaml

@ -34,6 +34,7 @@
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Padding="{TemplateBinding Padding}"
RecognizesAccessKey="True"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
</ControlTemplate>

1
src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml

@ -44,6 +44,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Grid.Column="1" />

4
src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml

@ -38,6 +38,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="0"
MinWidth="{TemplateBinding MinWidth}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
AllowSpin="{TemplateBinding AllowSpin}"
@ -49,6 +50,9 @@
BorderBrush="Transparent"
Margin="-1"
Padding="{TemplateBinding Padding}"
MinWidth="{TemplateBinding MinWidth}"
Foreground="{TemplateBinding Foreground}"
FontSize="{TemplateBinding FontSize}"
Watermark="{TemplateBinding Watermark}"
IsReadOnly="{TemplateBinding IsReadOnly}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"

17
src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml

@ -6,16 +6,13 @@
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</Panel>
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</ControlTemplate>
</Setter>
</Style>

1
src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml

@ -1,6 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="PopupRoot">
<Setter Property="Background" Value="{x:Null}"/>
<Setter Property="TransparencyLevelHint" Value="Transparent" />
<Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/>
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}"/>

1
src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml

@ -51,6 +51,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
TextBlock.Foreground="{TemplateBinding Foreground}"
Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Grid.Column="1" />

1
src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml

@ -34,6 +34,7 @@
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Padding="{TemplateBinding Padding}"
RecognizesAccessKey="True"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
</ControlTemplate>

1
src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml

@ -53,6 +53,7 @@
Grid.Row="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
RecognizesAccessKey="True"
VerticalAlignment="Top"/>
<Grid Grid.Row="1"

9
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@ -1,4 +1,3 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Metadata;
@ -8,14 +7,8 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Transformation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
#if SIGNED_BUILD
[assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
#else
[assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]
[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests")]
#endif

19
src/Avalonia.X11/X11Platform.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using Avalonia.Controls;
@ -104,11 +105,25 @@ namespace Avalonia.X11
public IntPtr DeferredDisplay { get; set; }
public IntPtr Display { get; set; }
public ITrayIconImpl CreateTrayIcon ()
private static uint[] X11IconConverter(IWindowIconImpl icon)
{
return new X11TrayIconImpl();
if (!(icon is X11IconData x11icon))
return Array.Empty<uint>();
return x11icon.Data.Select(x => x.ToUInt32()).ToArray();
}
public ITrayIconImpl CreateTrayIcon()
{
var dbusTrayIcon = new DBusTrayIconImpl();
if (!dbusTrayIcon.IsActive) return new XEmbedTrayIconImpl();
dbusTrayIcon.IconConverterDelegate = X11IconConverter;
return dbusTrayIcon;
}
public IWindowImpl CreateWindow()
{
return new X11Window(this, null);

2
src/Avalonia.X11/X11Window.Ime.cs

@ -33,8 +33,6 @@ namespace Avalonia.X11
&& ((int)(style & XIMProperties.XIMStatusNothing) != 0))
{
XPoint spot = default;
XRectangle area = default;
//using var areaS = new Utf8Buffer("area");
using var spotS = new Utf8Buffer("spotLocation");

47
src/Avalonia.X11/XEmbedTrayIconImpl.cs

@ -0,0 +1,47 @@
using System;
using Avalonia.Controls.Platform;
using Avalonia.Logging;
using Avalonia.Platform;
namespace Avalonia.X11
{
internal class XEmbedTrayIconImpl : ITrayIconImpl
{
private bool _isCalled;
private void NotImplemented()
{
if(_isCalled) return;
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)
?.Log(this,
"TODO: XEmbed System Tray Icons is not implemented yet. Tray icons won't be available on this system.");
_isCalled = true;
}
public void Dispose()
{
NotImplemented();
}
public void SetIcon(IWindowIconImpl icon)
{
NotImplemented();
}
public void SetToolTipText(string text)
{
NotImplemented();
}
public void SetIsVisible(bool visible)
{
NotImplemented();
}
public INativeMenuExporter MenuExporter { get; }
public Action OnClicked { get; set; }
}
}

35
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs

@ -97,6 +97,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
case SelectorGrammar.NotSyntax not:
result = new XamlIlNotSelector(result, Create(not.Argument, typeResolver));
break;
case SelectorGrammar.NthChildSyntax nth:
result = new XamlIlNthChildSelector(result, nth.Step, nth.Offset, XamlIlNthChildSelector.SelectorType.NthChild);
break;
case SelectorGrammar.NthLastChildSyntax nth:
result = new XamlIlNthChildSelector(result, nth.Step, nth.Offset, XamlIlNthChildSelector.SelectorType.NthLastChild);
break;
case SelectorGrammar.CommaSyntax comma:
if (results == null)
results = new XamlIlOrSelectorNode(node, selectorType);
@ -273,6 +279,35 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
}
}
class XamlIlNthChildSelector : XamlIlSelectorNode
{
private readonly int _step;
private readonly int _offset;
private readonly SelectorType _type;
public enum SelectorType
{
NthChild,
NthLastChild
}
public XamlIlNthChildSelector(XamlIlSelectorNode previous, int step, int offset, SelectorType type) : base(previous)
{
_step = step;
_offset = offset;
_type = type;
}
public override IXamlType TargetType => Previous?.TargetType;
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
codeGen.Ldc_I4(_step);
codeGen.Ldc_I4(_offset);
EmitCall(context, codeGen,
m => m.Name == _type.ToString() && m.Parameters.Count == 3);
}
}
class XamlIlPropertyEqualsSelector : XamlIlSelectorNode
{
public XamlIlPropertyEqualsSelector(XamlIlSelectorNode previous,

7
src/Markup/Avalonia.Markup.Xaml/Properties/AssemblyInfo.cs

@ -1,12 +1,9 @@
using System.Reflection;
using Avalonia.Metadata;
using System.Runtime.CompilerServices;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Xaml.MarkupExtensions")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Xaml.Styling")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Xaml.Templates")]
#if SIGNED_BUILD
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
#else
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests")]
#endif

149
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs

@ -160,11 +160,13 @@ namespace Avalonia.Markup.Parsers
if (identifier.IsEmpty)
{
throw new ExpressionParseException(r.Position, "Expected class name or is selector after ':'.");
throw new ExpressionParseException(r.Position, "Expected class name, is, nth-child or nth-last-child selector after ':'.");
}
const string IsKeyword = "is";
const string NotKeyword = "not";
const string NthChildKeyword = "nth-child";
const string NthLastChildKeyword = "nth-last-child";
if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('('))
{
@ -181,6 +183,20 @@ namespace Avalonia.Markup.Parsers
var syntax = new NotSyntax { Argument = argument };
return (State.Middle, syntax);
}
if (identifier.SequenceEqual(NthChildKeyword.AsSpan()) && r.TakeIf('('))
{
var (step, offset) = ParseNthChildArguments(ref r);
var syntax = new NthChildSyntax { Step = step, Offset = offset };
return (State.Middle, syntax);
}
if (identifier.SequenceEqual(NthLastChildKeyword.AsSpan()) && r.TakeIf('('))
{
var (step, offset) = ParseNthChildArguments(ref r);
var syntax = new NthLastChildSyntax { Step = step, Offset = offset };
return (State.Middle, syntax);
}
else
{
return (
@ -191,7 +207,6 @@ namespace Avalonia.Markup.Parsers
});
}
}
private static (State, ISyntax?) ParseTraversal(ref CharacterReader r)
{
r.SkipWhitespace();
@ -302,6 +317,114 @@ namespace Avalonia.Markup.Parsers
return syntax;
}
private static (int step, int offset) ParseNthChildArguments(ref CharacterReader r)
{
int step = 0;
int offset = 0;
if (r.Peek == 'o')
{
var constArg = r.TakeUntil(')').ToString().Trim();
if (constArg.Equals("odd", StringComparison.Ordinal))
{
step = 2;
offset = 1;
}
else
{
throw new ExpressionParseException(r.Position, $"Expected nth-child(odd). Actual '{constArg}'.");
}
}
else if (r.Peek == 'e')
{
var constArg = r.TakeUntil(')').ToString().Trim();
if (constArg.Equals("even", StringComparison.Ordinal))
{
step = 2;
offset = 0;
}
else
{
throw new ExpressionParseException(r.Position, $"Expected nth-child(even). Actual '{constArg}'.");
}
}
else
{
r.SkipWhitespace();
var stepOrOffset = 0;
var stepOrOffsetStr = r.TakeWhile(c => char.IsDigit(c) || c == '-' || c == '+').ToString();
if (stepOrOffsetStr.Length == 0
|| (stepOrOffsetStr.Length == 1
&& stepOrOffsetStr[0] == '+'))
{
stepOrOffset = 1;
}
else if (stepOrOffsetStr.Length == 1
&& stepOrOffsetStr[0] == '-')
{
stepOrOffset = -1;
}
else if (!int.TryParse(stepOrOffsetStr.ToString(), out stepOrOffset))
{
throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step or offset value. Integer was expected.");
}
r.SkipWhitespace();
if (r.Peek == ')')
{
step = 0;
offset = stepOrOffset;
}
else
{
step = stepOrOffset;
if (r.Peek != 'n')
{
throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step value, \"xn+y\" pattern was expected.");
}
r.Skip(1); // skip 'n'
r.SkipWhitespace();
if (r.Peek != ')')
{
int sign;
var nextChar = r.Take();
if (nextChar == '+')
{
sign = 1;
}
else if (nextChar == '-')
{
sign = -1;
}
else
{
throw new ExpressionParseException(r.Position, "Couldn't parse nth-child sign. '+' or '-' was expected.");
}
r.SkipWhitespace();
if (sign != 0
&& !int.TryParse(r.TakeUntil(')').ToString(), out offset))
{
throw new ExpressionParseException(r.Position, "Couldn't parse nth-child offset value. Integer was expected.");
}
offset *= sign;
}
}
}
Expect(ref r, ')');
return (step, offset);
}
private static void Expect(ref CharacterReader r, char c)
{
if (r.End)
@ -419,6 +542,28 @@ namespace Avalonia.Markup.Parsers
}
}
public class NthChildSyntax : ISyntax
{
public int Offset { get; set; }
public int Step { get; set; }
public override bool Equals(object? obj)
{
return (obj is NthChildSyntax nth) && nth.Offset == Offset && nth.Step == Step;
}
}
public class NthLastChildSyntax : ISyntax
{
public int Offset { get; set; }
public int Step { get; set; }
public override bool Equals(object? obj)
{
return (obj is NthLastChildSyntax nth) && nth.Offset == Offset && nth.Step == Step;
}
}
public class CommaSyntax : ISyntax
{
public override bool Equals(object? obj)

6
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs

@ -104,6 +104,12 @@ namespace Avalonia.Markup.Parsers
case SelectorGrammar.NotSyntax not:
result = result.Not(x => Create(not.Argument));
break;
case SelectorGrammar.NthChildSyntax nth:
result = result.NthChild(nth.Step, nth.Offset);
break;
case SelectorGrammar.NthLastChildSyntax nth:
result = result.NthLastChild(nth.Step, nth.Offset);
break;
case SelectorGrammar.CommaSyntax comma:
if (results == null)
{

7
src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs

@ -1,11 +1,8 @@
using System.Reflection;
using Avalonia.Metadata;
using System.Runtime.CompilerServices;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Data")]
#if SIGNED_BUILD
[assembly: InternalsVisibleTo("Avalonia.Markup.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
#else
[assembly: InternalsVisibleTo("Avalonia.Markup.UnitTests")]
#endif

2
src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs

@ -66,7 +66,7 @@ namespace Avalonia.Skia
_canCreateSurfaces = true;
return surface;
}
catch (Exception e)
catch (Exception)
{
Logger.TryGet(LogEventLevel.Error, "OpenGL")
?.Log(this, "Unable to create a Skia-compatible FBO manually");

7
src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs

@ -1,8 +1,5 @@
using System.Runtime.CompilerServices;
#if SIGNED_BUILD
[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
#else
[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]
[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests")]
#endif

7
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -27,15 +27,16 @@ namespace Avalonia.Skia
{
return typeface;
}
var initialWeight = (int)key.Weight;
var weight = (int)key.Weight;
weight -= weight % 100; // make sure we start at a full weight
weight -= weight % 50; // make sure we start at a full weight
for (var i = 0; i < 2; i++)
{
// only try 2 font weights in each direction
for (var j = 0; j < 200; j += 100)
for (var j = 0; j < initialWeight; j += 50)
{
if (weight - j >= 100)
{

6
src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs

@ -1,4 +1,3 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Platform;
using Avalonia.Direct2D1;
@ -6,10 +5,5 @@ using Avalonia.Direct2D1;
[assembly: ExportRenderingSubsystem(OperatingSystemType.WinNT, 1, "Direct2D1", typeof(Direct2D1Platform), nameof(Direct2D1Platform.Initialize),
typeof(Direct2DChecker))]
#if SIGNED_BUILD
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
#else
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests")]
#endif

8
src/tools/MicroComGenerator/Program.cs

@ -35,8 +35,16 @@ namespace MicroComGenerator
if (opts.CppOutput != null)
File.WriteAllText(opts.CppOutput, CppGen.GenerateCpp(ast));
if (opts.CSharpOutput != null)
{
File.WriteAllText(opts.CSharpOutput, new CSharpGen(ast).Generate());
// HACK: Can't work out how to get the VS project system's fast up-to-date checks
// to ignore the generated code, so as a workaround set the write time to that of
// the input.
File.SetLastWriteTime(opts.CSharpOutput, File.GetLastWriteTime(opts.Input));
}
return 0;
}

14
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@ -2,6 +2,7 @@
using System.Windows.Input;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
@ -271,6 +272,19 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Button_Invokes_CanExecute_When_CommandParameter_Changed()
{
var target = new Button();
var raised = 0;
target.Click += (s, e) => ++raised;
target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
Assert.Equal(1, raised);
}
[Fact]
public void Raises_Click_When_AccessKey_Raised()
{
var command = new TestCommand(p => p is bool value && value);
var target = new Button { Command = command };

17
tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@ -540,6 +541,22 @@ namespace Avalonia.Controls.UnitTests.Platform
Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true), Times.Never);
Assert.True(e.Handled);
}
[Fact]
public void PointerPressed_On_Disabled_Item_Doesnt_Close_SubMenu()
{
var target = new DefaultMenuInteractionHandler(false);
var menu = Mock.Of<IMenu>();
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.IsSubMenuOpen == true && x.Parent == menu);
var popup = new Popup();
var e = CreatePressed(popup);
((ISetLogicalParent)popup).SetParent(parentItem);
target.PointerPressed(parentItem, e);
Mock.Get(parentItem).Verify(x => x.Close(), Times.Never);
Assert.True(e.Handled);
}
}
public class ContextMenu

75
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -294,6 +294,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
[Fact]
public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent()
{
// Test uses OverlayPopupHost default template
using (CreateServices())
{
PopupContentControl target;
@ -316,33 +317,63 @@ namespace Avalonia.Controls.UnitTests.Primitives
var children = popupRoot.GetVisualDescendants().ToList();
var types = children.Select(x => x.GetType().Name).ToList();
Assert.Equal(
new[]
{
"Panel",
"Border",
"VisualLayerManager",
"ContentPresenter",
"ContentPresenter",
"Border",
},
types);
if (UsePopupHost)
{
Assert.Equal(
new[]
{
"VisualLayerManager",
"ContentPresenter",
"ContentPresenter",
"Border",
},
types);
}
else
{
Assert.Equal(
new[]
{
"Panel",
"Border",
"VisualLayerManager",
"ContentPresenter",
"ContentPresenter",
"Border",
},
types);
}
var templatedParents = children
.OfType<IControl>()
.Select(x => x.TemplatedParent).ToList();
Assert.Equal(
new object[]
{
popupRoot,
popupRoot,
popupRoot,
popupRoot,
target,
null,
},
templatedParents);
if (UsePopupHost)
{
Assert.Equal(
new object[]
{
popupRoot,
popupRoot,
target,
null,
},
templatedParents);
}
else
{
Assert.Equal(
new object[]
{
popupRoot,
popupRoot,
popupRoot,
popupRoot,
target,
null,
},
templatedParents);
}
}
}

159
tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

@ -236,6 +236,165 @@ namespace Avalonia.Markup.UnitTests.Parsers
result);
}
[Theory]
[InlineData(":nth-child(xn+2)")]
[InlineData(":nth-child(2n+b)")]
[InlineData(":nth-child(2n+)")]
[InlineData(":nth-child(2na)")]
[InlineData(":nth-child(2x+1)")]
public void NthChild_Invalid_Inputs(string input)
{
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(input));
}
[Theory]
[InlineData(":nth-child(+1)", 0, 1)]
[InlineData(":nth-child(1)", 0, 1)]
[InlineData(":nth-child(-1)", 0, -1)]
[InlineData(":nth-child(2n+1)", 2, 1)]
[InlineData(":nth-child(n)", 1, 0)]
[InlineData(":nth-child(+n)", 1, 0)]
[InlineData(":nth-child(-n)", -1, 0)]
[InlineData(":nth-child(-2n)", -2, 0)]
[InlineData(":nth-child(n+5)", 1, 5)]
[InlineData(":nth-child(n-5)", 1, -5)]
[InlineData(":nth-child( 2n + 1 )", 2, 1)]
[InlineData(":nth-child( 2n - 1 )", 2, -1)]
public void NthChild_Variations(string input, int step, int offset)
{
var result = SelectorGrammar.Parse(input);
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NthChildSyntax()
{
Step = step,
Offset = offset
}
},
result);
}
[Theory]
[InlineData(":nth-last-child(+1)", 0, 1)]
[InlineData(":nth-last-child(1)", 0, 1)]
[InlineData(":nth-last-child(-1)", 0, -1)]
[InlineData(":nth-last-child(2n+1)", 2, 1)]
[InlineData(":nth-last-child(n)", 1, 0)]
[InlineData(":nth-last-child(+n)", 1, 0)]
[InlineData(":nth-last-child(-n)", -1, 0)]
[InlineData(":nth-last-child(-2n)", -2, 0)]
[InlineData(":nth-last-child(n+5)", 1, 5)]
[InlineData(":nth-last-child(n-5)", 1, -5)]
[InlineData(":nth-last-child( 2n + 1 )", 2, 1)]
[InlineData(":nth-last-child( 2n - 1 )", 2, -1)]
public void NthLastChild_Variations(string input, int step, int offset)
{
var result = SelectorGrammar.Parse(input);
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NthLastChildSyntax()
{
Step = step,
Offset = offset
}
},
result);
}
[Fact]
public void OfType_NthChild()
{
var result = SelectorGrammar.Parse("Button:nth-child(2n+1)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.NthChildSyntax()
{
Step = 2,
Offset = 1
}
},
result);
}
[Fact]
public void OfType_NthChild_Without_Offset()
{
var result = SelectorGrammar.Parse("Button:nth-child(2147483647n)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.NthChildSyntax()
{
Step = int.MaxValue,
Offset = 0
}
},
result);
}
[Fact]
public void OfType_NthLastChild()
{
var result = SelectorGrammar.Parse("Button:nth-last-child(2n+1)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.NthLastChildSyntax()
{
Step = 2,
Offset = 1
}
},
result);
}
[Fact]
public void OfType_NthChild_Odd()
{
var result = SelectorGrammar.Parse("Button:nth-child(odd)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.NthChildSyntax()
{
Step = 2,
Offset = 1
}
},
result);
}
[Fact]
public void OfType_NthChild_Even()
{
var result = SelectorGrammar.Parse("Button:nth-child(even)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.NthChildSyntax()
{
Step = 2,
Offset = 0
}
},
result);
}
[Fact]
public void Is_Descendent_Not_OfType_Class()
{

197
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml;
using Avalonia.Controls;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
@ -267,6 +269,199 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void Style_Can_Use_NthChild_Selector()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border.foo:nth-child(2n+1)'>
<Setter Property='Background' Value='Red'/>
</Style>
</Window.Styles>
<StackPanel>
<Border x:Name='b1' Classes='foo'/>
<Border x:Name='b2' />
</StackPanel>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var b1 = window.FindControl<Border>("b1");
var b2 = window.FindControl<Border>("b2");
Assert.Equal(Brushes.Red, b1.Background);
Assert.Null(b2.Background);
}
}
[Fact]
public void Style_Can_Use_NthChild_Selector_After_Reoder()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border:nth-child(2n)'>
<Setter Property='Background' Value='Red'/>
</Style>
</Window.Styles>
<StackPanel x:Name='parent'>
<Border x:Name='b1' />
<Border x:Name='b2' />
</StackPanel>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var parent = window.FindControl<StackPanel>("parent");
var b1 = window.FindControl<Border>("b1");
var b2 = window.FindControl<Border>("b2");
Assert.Null(b1.Background);
Assert.Equal(Brushes.Red, b2.Background);
parent.Children.Remove(b1);
Assert.Null(b1.Background);
Assert.Null(b2.Background);
parent.Children.Add(b1);
Assert.Equal(Brushes.Red, b1.Background);
Assert.Null(b2.Background);
}
}
[Fact]
public void Style_Can_Use_NthLastChild_Selector_After_Reoder()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border:nth-last-child(2n)'>
<Setter Property='Background' Value='Red'/>
</Style>
</Window.Styles>
<StackPanel x:Name='parent'>
<Border x:Name='b1' />
<Border x:Name='b2' />
</StackPanel>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var parent = window.FindControl<StackPanel>("parent");
var b1 = window.FindControl<Border>("b1");
var b2 = window.FindControl<Border>("b2");
Assert.Equal(Brushes.Red, b1.Background);
Assert.Null(b2.Background);
parent.Children.Remove(b1);
Assert.Null(b1.Background);
Assert.Null(b2.Background);
parent.Children.Add(b1);
Assert.Null(b1.Background);
Assert.Equal(Brushes.Red, b2.Background);
}
}
[Fact]
public void Style_Can_Use_NthChild_Selector_With_ListBox()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='ListBoxItem:nth-child(2n)'>
<Setter Property='Background' Value='{Binding}'/>
</Style>
</Window.Styles>
<ListBox x:Name='list' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var collection = new ObservableCollection<IBrush>()
{
Brushes.Red, Brushes.Green, Brushes.Blue
};
var list = window.FindControl<ListBox>("list");
list.VirtualizationMode = ItemVirtualizationMode.Simple;
list.Items = collection;
window.Show();
IEnumerable<IBrush> GetColors() => list.Presenter.Panel.Children.Cast<ListBoxItem>().Select(t => t.Background);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors());
collection.Remove(Brushes.Green);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors());
collection.Add(Brushes.Violet);
collection.Add(Brushes.Black);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors());
}
}
[Fact]
public void Style_Can_Use_NthChild_Selector_With_ItemsRepeater()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='Foreground' Value='Transparent'/>
</Style>
<Style Selector='TextBlock:nth-child(2n)'>
<Setter Property='Foreground' Value='{Binding}'/>
</Style>
</Window.Styles>
<ItemsRepeater x:Name='list' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var collection = new ObservableCollection<IBrush>()
{
Brushes.Red, Brushes.Green, Brushes.Blue
};
var list = window.FindControl<ItemsRepeater>("list");
list.Items = collection;
window.Show();
IEnumerable<IBrush> GetColors() => Enumerable.Range(0, list.ItemsSourceView.Count)
.Select(t => (list.GetOrCreateElement(t) as TextBlock)!.Foreground);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors());
collection.Remove(Brushes.Green);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors());
collection.Add(Brushes.Violet);
collection.Add(Brushes.Black);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors());
}
}
[Fact]
public void Style_Can_Use_Or_Selector_1()
{

22
tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs

@ -6,18 +6,24 @@ namespace Avalonia.Skia.UnitTests.Media
{
public class SKTypefaceCollectionCacheTests
{
[Fact]
public void Should_Get_Near_Matching_Typeface()
private const string s_notoMono =
"resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono";
[InlineData(s_notoMono, FontWeight.SemiLight, FontStyle.Normal)]
[InlineData(s_notoMono, FontWeight.Bold, FontStyle.Italic)]
[InlineData(s_notoMono, FontWeight.Heavy, FontStyle.Oblique)]
[Theory]
public void Should_Get_Near_Matching_Typeface(string familyName, FontWeight fontWeight, FontStyle fontStyle)
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var notoMono =
new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono);
var fontFamily = new FontFamily(familyName);
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily);
Assert.Equal("Noto Mono",
notoMonoCollection.Get(new Typeface(notoMono, weight: FontWeight.Bold)).FamilyName);
var actual = typefaceCollection.Get(new Typeface(fontFamily, fontStyle, fontWeight))?.FamilyName;
Assert.Equal("Noto Mono", actual);
}
}

291
tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs

@ -0,0 +1,291 @@
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class SelectorTests_NthChild
{
[Theory]
[InlineData(2, 0, ":nth-child(2n)")]
[InlineData(2, 1, ":nth-child(2n+1)")]
[InlineData(1, 0, ":nth-child(1n)")]
[InlineData(4, -1, ":nth-child(4n-1)")]
[InlineData(0, 1, ":nth-child(1)")]
[InlineData(0, -1, ":nth-child(-1)")]
[InlineData(int.MaxValue, int.MinValue + 1, ":nth-child(2147483647n-2147483647)")]
public void Not_Selector_Should_Have_Correct_String_Representation(int step, int offset, string expected)
{
var target = default(Selector).NthChild(step, offset);
Assert.Equal(expected, target.ToString());
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(2, 0);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.False(await target.Match(b3).Activator!.Take(1));
Assert.True(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(2, 1);
Assert.True(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Negative_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(4, -1);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(1, 2);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.True(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(1, -1);
Assert.True(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.True(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(0, 2);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.False(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(0, -2);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.False(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector()
{
Border b1, b2;
Button b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new Control[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Button(),
b4 = new Button()
});
var previous = default(Selector).OfType<Border>();
var target = previous.NthChild(2, 0);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.Null(target.Match(b3).Activator);
Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result);
Assert.Null(target.Match(b4).Activator);
Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result);
}
[Fact]
public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent()
{
Border b1;
var contentControl = new ContentControl();
contentControl.Content = b1 = new Border();
var target = default(Selector).NthChild(1, 0);
Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1));
}
[Theory] // http://nthmaster.com/
[InlineData(+0, 8, false, false, false, false, false, false, false, true , false, false, false)]
[InlineData(+1, 6, false, false, false, false, false, true , true , true , true , true , true )]
[InlineData(-1, 9, true , true , true , true , true , true , true , true , true , false, false)]
public async Task Nth_Child_Master_Com_Test_Sigle_Selector(
int step, int offset, params bool[] items)
{
var panel = new StackPanel();
panel.Children.AddRange(items.Select(_ => new Border()));
var previous = default(Selector).OfType<Border>();
var target = previous.NthChild(step, offset);
var results = new bool[items.Length];
for (int index = 0; index < items.Length; index++)
{
var border = panel.Children[index];
results[index] = await target.Match(border).Activator!.Take(1);
}
Assert.Equal(items, results);
}
[Theory] // http://nthmaster.com/
[InlineData(+1, 4, -1, 8, false, false, false, true , true , true , true , true , false, false, false)]
[InlineData(+3, 1, +2, 0, false, false, false, true , false, false, false, false, false, true , false)]
public async Task Nth_Child_Master_Com_Test_Double_Selector(
int step1, int offset1, int step2, int offset2, params bool[] items)
{
var panel = new StackPanel();
panel.Children.AddRange(items.Select(_ => new Border()));
var previous = default(Selector).OfType<Border>();
var middle = previous.NthChild(step1, offset1);
var target = middle.NthChild(step2, offset2);
var results = new bool[items.Length];
for (int index = 0; index < items.Length; index++)
{
var border = panel.Children[index];
results[index] = await target.Match(border).Activator!.Take(1);
}
Assert.Equal(items, results);
}
[Theory] // http://nthmaster.com/
[InlineData(+1, 2, 2, 1, -1, 9, false, false, true , false, true , false, true , false, true , false, false)]
public async Task Nth_Child_Master_Com_Test_Triple_Selector(
int step1, int offset1, int step2, int offset2, int step3, int offset3, params bool[] items)
{
var panel = new StackPanel();
panel.Children.AddRange(items.Select(_ => new Border()));
var previous = default(Selector).OfType<Border>();
var middle1 = previous.NthChild(step1, offset1);
var middle2 = middle1.NthChild(step2, offset2);
var target = middle2.NthChild(step3, offset3);
var results = new bool[items.Length];
for (int index = 0; index < items.Length; index++)
{
var border = panel.Children[index];
results[index] = await target.Match(border).Activator!.Take(1);
}
Assert.Equal(items, results);
}
[Fact]
public void Returns_Correct_TargetType()
{
var target = new NthChildSelector(default(Selector).OfType<Control1>(), 1, 0);
Assert.Equal(typeof(Control1), target.TargetType);
}
public class Control1 : Control
{
}
}
}

220
tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs

@ -0,0 +1,220 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class SelectorTests_NthLastChild
{
[Theory]
[InlineData(2, 0, ":nth-last-child(2n)")]
[InlineData(2, 1, ":nth-last-child(2n+1)")]
[InlineData(1, 0, ":nth-last-child(1n)")]
[InlineData(4, -1, ":nth-last-child(4n-1)")]
[InlineData(0, 1, ":nth-last-child(1)")]
[InlineData(0, -1, ":nth-last-child(-1)")]
[InlineData(int.MaxValue, int.MinValue + 1, ":nth-last-child(2147483647n-2147483647)")]
public void Not_Selector_Should_Have_Correct_String_Representation(int step, int offset, string expected)
{
var target = default(Selector).NthLastChild(step, offset);
Assert.Equal(expected, target.ToString());
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(2, 0);
Assert.True(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(2, 1);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.False(await target.Match(b3).Activator!.Take(1));
Assert.True(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Negative_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(4, -1);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.False(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(1, 2);
Assert.True(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(1, -2);
Assert.True(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.True(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(0, 2);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(0, -2);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.False(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector()
{
Border b1, b2;
Button b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new Control[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Button(),
b4 = new Button()
});
var previous = default(Selector).OfType<Border>();
var target = previous.NthLastChild(2, 0);
Assert.True(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.Null(target.Match(b3).Activator);
Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result);
Assert.Null(target.Match(b4).Activator);
Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result);
}
[Fact]
public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent()
{
Border b1;
var contentControl = new ContentControl();
contentControl.Content = b1 = new Border();
var target = default(Selector).NthLastChild(1, 0);
Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1));
}
[Fact]
public void Returns_Correct_TargetType()
{
var target = new NthLastChildSelector(default(Selector).OfType<Control1>(), 1, 0);
Assert.Equal(typeof(Control1), target.TargetType);
}
public class Control1 : Control
{
}
}
}

7
tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs

@ -20,13 +20,17 @@ namespace Avalonia.Styling.UnitTests
public static IObservable<bool> ToObservable(this IStyleActivator activator)
{
if (activator == null)
{
throw new ArgumentNullException(nameof(activator));
}
return new ObservableAdapter(activator);
}
private class ObservableAdapter : LightweightObservableBase<bool>, IStyleActivatorSink
{
private readonly IStyleActivator _source;
private bool _value;
public ObservableAdapter(IStyleActivator source) => _source = source;
protected override void Initialize() => _source.Subscribe(this);
@ -34,7 +38,6 @@ namespace Avalonia.Styling.UnitTests
void IStyleActivatorSink.OnNext(bool value, int tag)
{
_value = value;
PublishNext(value);
}
}

Loading…
Cancel
Save