Browse Source

Merge branch 'master' into fixes/HotKey_Command_Issues

pull/4737/head
Giuseppe Lippolis 6 years ago
parent
commit
2e76c89c34
  1. 4
      .ncrunch/Avalonia.Build.Tasks.v3.ncrunchproject
  2. 5
      .ncrunch/Avalonia.MicroCom.v3.ncrunchproject
  3. 2
      Avalonia.v3.ncrunchsolution
  4. 2
      Directory.Build.props
  5. 3
      Documentation/build.md
  6. 33
      build/MicroCom.targets
  7. 2
      build/ReactiveUI.props
  8. 1
      nukebuild/Build.cs
  9. 2
      src/Avalonia.Controls/ContextMenu.cs
  10. 1
      src/Avalonia.Controls/Selection/SelectionModel.cs
  11. 5
      src/Avalonia.Controls/TreeView.cs
  12. 6
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  13. 190
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
  14. 179
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  15. 109
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
  16. 130
      src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs
  17. 2
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
  18. 4
      src/Avalonia.Input/ApiCompatBaseline.txt
  19. 5
      src/Avalonia.Input/IInputElement.cs
  20. 22
      src/Avalonia.Input/InputElement.cs
  21. 117
      src/Avalonia.Input/KeyboardDevice.cs
  22. 1
      src/Avalonia.MicroCom/Avalonia.MicroCom.csproj
  23. 33
      src/Avalonia.Native/Avalonia.Native.csproj
  24. 5
      src/Avalonia.Native/WindowImplBase.cs
  25. 12
      src/Avalonia.Visuals/Media/FontFamily.cs
  26. 4
      src/Avalonia.Visuals/Media/TextDecoration.cs
  27. 17
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  28. 6
      src/Windows/Avalonia.Win32/Composition/CompositionConnector.cs
  29. 2
      src/Windows/Avalonia.Win32/Composition/SaturationEffect.cs
  30. 7
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs
  31. 7
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs
  32. 44
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  33. 197
      tests/Avalonia.Input.UnitTests/InputElement_Focus.cs
  34. 23
      tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs
  35. 21
      tests/Avalonia.Markup.UnitTests/Extensions/IEnummerableExtension.cs

4
.ncrunch/Avalonia.Build.Tasks.v3.ncrunchproject

@ -1,5 +1,3 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
<Settings />
</ProjectConfiguration>

5
.ncrunch/Avalonia.MicroCom.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

2
Avalonia.v3.ncrunchsolution

@ -4,6 +4,8 @@
<Value>tests\TestFiles\**.*</Value>
<Value>src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Avalonia.Build.Tasks.dll</Value>
<Value>src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll</Value>
<Value>src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.Rocks.dll</Value>
<Value>src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.Pdb.dll</Value>
</AdditionalFilesToIncludeForSolution>
<AllowParallelTestExecution>True</AllowParallelTestExecution>
<CustomBuildProperties>

2
Directory.Build.props

@ -2,5 +2,7 @@
<PropertyGroup>
<PackageOutputPath Condition="'$(PackageOutputPath)' == ''">$(MSBuildThisFileDirectory)build-intermediate/nuget</PackageOutputPath>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netcoreapp2.0\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<!-- https://github.com/dotnet/msbuild/issues/2661 -->
<AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
</PropertyGroup>
</Project>

3
Documentation/build.md

@ -30,7 +30,7 @@ The build process needs [Xcode](https://developer.apple.com/xcode/) to build the
Linux operating systems ship with their own respective package managers however we will use [Homebrew](https://brew.sh/) to manage packages on macOS. To install follow the instructions [here](https://docs.brew.sh/Installation).
### Install CastXML
### Install CastXML (pre Nov 2020)
Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processing during the build process. The easiest way to install this is via the operating system's package managers, such as below.
@ -61,6 +61,7 @@ git submodule update --init --recursive
### Build native libraries (macOS only)
On macOS it is necessary to build and manually install the respective native libraries using [Xcode](https://developer.apple.com/xcode/). The steps to get this working correctly are:
- (for revisions after 2 Nov 2020) Run `./build.sh GenerateCppHeaders` to generate `avalonia-native.h` from `avn.idl`
- Navigate to the Avalonia/native/Avalonia.Native/src/OSX folder and open the `Avalonia.Native.OSX.xcodeproj` project
- Build the library via the Product->Build menu. This will generate binaries in your local path under ~/Library/Developer/Xcode/DerivedData/Avalonia.Native.OSX-*guid* where "guid" is uniquely generated every time you build.
- Manually install the native library by copying it from the build artifacts folder into the shared dynamic library path:

33
build/MicroCom.targets

@ -0,0 +1,33 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Ensure that code generator is actually built -->
<ItemGroup>
<ProjectReference Include="..\tools\MicroComGenerator\MicroComGenerator.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<ExcludeAssets>all</ExcludeAssets>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
</ProjectReference>
</ItemGroup>
<Target Name="GenerateAvaloniaNativeComInterop"
BeforeTargets="CoreCompile"
DependsOnTargets="ResolveReferences"
Inputs="@(AvnComIdl);$(MSBuildThisFileDirectory)/../tools/MicroComGenerator/**/*.cs"
Outputs="%(AvnComIdl.OutputFile)">
<Message Importance="high" Text="Generating file %(AvnComIdl.OutputFile) from @(AvnComIdl)" />
<Exec Command="dotnet ../tools/MicroComGenerator/bin/$(Configuration)/netcoreapp3.1/MicroComGenerator.dll -i @(AvnComIdl) --cs %(AvnComIdl.OutputFile)" LogStandardErrorAsError="true" />
<ItemGroup>
<!-- Remove and re-add generated file, this is needed for the clean build -->
<Compile Remove="%(AvnComIdl.OutputFile)"/>
<Compile Include="%(AvnComIdl.OutputFile)"/>
</ItemGroup>
</Target>
<ItemGroup>
<UpToDateCheckInput Include="@(AvnComIdl)"/>
<UpToDateCheckInput Include="$(MSBuildThisFileDirectory)/../tools/MicroComGenerator/**/*.cs"/>
</ItemGroup>
<PropertyGroup>
<_AvaloniaPatchComInterop>true</_AvaloniaPatchComInterop>
</PropertyGroup>
<Import Project="$(MSBuildThisFileDirectory)/BuildTargets.targets" />
</Project>

2
build/ReactiveUI.props

@ -1,5 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="ReactiveUI" Version="11.5.17" />
<PackageReference Include="ReactiveUI" Version="12.1.1" />
</ItemGroup>
</Project>

1
nukebuild/Build.cs

@ -141,6 +141,7 @@ partial class Build : NukeBuild
Target CompileNative => _ => _
.DependsOn(Clean)
.DependsOn(GenerateCppHeaders)
.OnlyWhenStatic(() => EnvironmentInfo.IsOsx)
.Executes(() =>
{

2
src/Avalonia.Controls/ContextMenu.cs

@ -236,7 +236,7 @@ namespace Avalonia.Controls
/// <summary>
/// Opens the menu.
/// </summary>
public override void Open() => Open(null);
public override void Open() => throw new NotSupportedException();
/// <summary>
/// Opens a context menu on the specified control.

1
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -443,6 +443,7 @@ namespace Avalonia.Controls.Selection
}
if ((e.Action == NotifyCollectionChangedAction.Remove && e.OldStartingIndex <= oldSelectedIndex) ||
(e.Action == NotifyCollectionChangedAction.Replace && e.OldStartingIndex == oldSelectedIndex) ||
e.Action == NotifyCollectionChangedAction.Reset)
{
RaisePropertyChanged(nameof(SelectedItem));

5
src/Avalonia.Controls/TreeView.cs

@ -378,10 +378,11 @@ namespace Avalonia.Controls
{
if (!this.IsVisualAncestorOf(element))
{
IControl result = _selectedItem != null ?
var result = _selectedItem != null ?
ItemContainerGenerator.Index.ContainerFromItem(_selectedItem) :
ItemContainerGenerator.ContainerFromIndex(0);
return (true, result);
return (result != null, result); // SelectedItem may not be in the treeview.
}
return (true, null);

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

@ -32,6 +32,8 @@ namespace Avalonia.Diagnostics.ViewModels
view.Filter = FilterProperty;
PropertiesView = view;
Layout = new ControlLayoutViewModel(control);
if (control is INotifyPropertyChanged inpc)
{
inpc.PropertyChanged += ControlPropertyChanged;
@ -52,6 +54,8 @@ namespace Avalonia.Diagnostics.ViewModels
get => _selectedProperty;
set => RaiseAndSetIfChanged(ref _selectedProperty, value);
}
public ControlLayoutViewModel Layout { get; }
public void Dispose()
{
@ -112,6 +116,8 @@ namespace Avalonia.Diagnostics.ViewModels
property.Update();
}
}
Layout.ControlPropertyChanged(sender, e);
}
private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e)

190
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs

@ -0,0 +1,190 @@
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ControlLayoutViewModel : ViewModelBase
{
private readonly IVisual _control;
private Thickness _marginThickness;
private Thickness _borderThickness;
private Thickness _paddingThickness;
private double _width;
private double _height;
private string _widthConstraint;
private string _heightConstraint;
private bool _updatingFromControl;
public Thickness MarginThickness
{
get => _marginThickness;
set => RaiseAndSetIfChanged(ref _marginThickness, value);
}
public Thickness BorderThickness
{
get => _borderThickness;
set => RaiseAndSetIfChanged(ref _borderThickness, value);
}
public Thickness PaddingThickness
{
get => _paddingThickness;
set => RaiseAndSetIfChanged(ref _paddingThickness, value);
}
public double Width
{
get => _width;
private set => RaiseAndSetIfChanged(ref _width, value);
}
public double Height
{
get => _height;
private set => RaiseAndSetIfChanged(ref _height, value);
}
public string WidthConstraint
{
get => _widthConstraint;
private set => RaiseAndSetIfChanged(ref _widthConstraint, value);
}
public string HeightConstraint
{
get => _heightConstraint;
private set => RaiseAndSetIfChanged(ref _heightConstraint, value);
}
public bool HasPadding { get; }
public bool HasBorder { get; }
public ControlLayoutViewModel(IVisual control)
{
_control = control;
HasPadding = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Decorator.PaddingProperty);
HasBorder = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Border.BorderThicknessProperty);
if (control is AvaloniaObject ao)
{
MarginThickness = ao.GetValue(Layoutable.MarginProperty);
if (HasPadding)
{
PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
}
if (HasBorder)
{
BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
}
}
UpdateSize();
UpdateSizeConstraints();
}
private void UpdateSizeConstraints()
{
if (_control is IAvaloniaObject ao)
{
string CreateConstraintInfo(StyledProperty<double> minProperty, StyledProperty<double> maxProperty)
{
if (ao.IsSet(minProperty) || ao.IsSet(maxProperty))
{
var minValue = ao.GetValue(minProperty);
var maxValue = ao.GetValue(maxProperty);
return $"{minValue} < size < {maxValue}";
}
return null;
}
WidthConstraint = CreateConstraintInfo(Layoutable.MinWidthProperty, Layoutable.MaxWidthProperty);
HeightConstraint = CreateConstraintInfo(Layoutable.MinHeightProperty, Layoutable.MaxHeightProperty);
}
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (_updatingFromControl)
{
return;
}
if (_control is AvaloniaObject ao)
{
if (e.PropertyName == nameof(MarginThickness))
{
ao.SetValue(Layoutable.MarginProperty, MarginThickness);
}
else if (HasPadding && e.PropertyName == nameof(PaddingThickness))
{
ao.SetValue(Decorator.PaddingProperty, PaddingThickness);
}
else if (HasBorder && e.PropertyName == nameof(BorderThickness))
{
ao.SetValue(Border.BorderThicknessProperty, BorderThickness);
}
}
}
public void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
try
{
_updatingFromControl = true;
if (e.Property == Visual.BoundsProperty)
{
UpdateSize();
}
else
{
if (_control is IAvaloniaObject ao)
{
if (e.Property == Layoutable.MarginProperty)
{
MarginThickness = ao.GetValue(Layoutable.MarginProperty);
}
else if (e.Property == Decorator.PaddingProperty)
{
PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
}
else if (e.Property == Border.BorderThicknessProperty)
{
BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
}
else if (e.Property == Layoutable.MinWidthProperty ||
e.Property == Layoutable.MaxWidthProperty ||
e.Property == Layoutable.MinHeightProperty ||
e.Property == Layoutable.MaxHeightProperty)
{
UpdateSizeConstraints();
}
}
}
}
finally
{
_updatingFromControl = false;
}
}
private void UpdateSize()
{
var size = _control.Bounds;
Width = size.Width;
Height = size.Height;
}
}
}

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

@ -1,38 +1,155 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
xmlns:local="clr-namespace:Avalonia.Diagnostics.Views"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*">
<TextBox Grid.Row="0" Grid.Column="0"
BorderThickness="0"
Text="{Binding TreePage.PropertyFilter}"
Watermark="Filter properties" />
<CheckBox Grid.Row="0"
Grid.Column="1"
Content="Regex"
IsChecked="{Binding TreePage.UseRegexFilter}"/>
<DataGrid Items="{Binding PropertiesView}"
Grid.Row="1" Grid.ColumnSpan="2"
BorderThickness="0"
RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
CanUserResizeColumns="true">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" />
<DataGridTextColumn Header="Type" Binding="{Binding Type}" />
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True" />
</DataGrid.Columns>
<DataGrid.Styles>
<Style Selector="DataGridRow TextBox">
<Setter Property="SelectionBrush" Value="LightBlue"/>
</Style>
</DataGrid.Styles>
</DataGrid>
<UserControl.Resources>
<SolidColorBrush x:Key="ThicknessBorderBrush" Color="#666666" />
<SolidColorBrush x:Key="SizeGuidelineBrush" Color="#333333" />
<SolidColorBrush x:Key="MarginBackgroundBrush" Color="#D78965" />
<SolidColorBrush x:Key="BorderBackgroundBrush" Color="#E3C381" />
<SolidColorBrush x:Key="PaddingBackgroundBrush" Color="#B8C47F" />
<SolidColorBrush x:Key="SizeBackgroundBrush" Color="#88B2BD" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="local|ThicknessEditor">
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{StaticResource ThicknessBorderBrush}" />
<Setter Property="Template">
<ControlTemplate>
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="Auto,*,Auto">
<Grid.Styles>
<Style Selector="TextBox.thickness-edit">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Margin" Value="2" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="(ScrollViewer.HorizontalScrollBarVisibility)" Value="Disabled" />
<Setter Property="(ScrollViewer.VerticalScrollBarVisibility)" Value="Disabled" />
<Setter Property="IsVisible" Value="{Binding $parent[local:ThicknessEditor].IsPresent}" />
</Style>
</Grid.Styles>
<TextBlock IsVisible="{TemplateBinding IsPresent}" Margin="4,0,0,0" Text="{TemplateBinding Header}" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" />
<TextBox Grid.Row="1" Grid.Column="0" Text="{Binding Left, RelativeSource={RelativeSource TemplatedParent}}" Classes="thickness-edit" />
<TextBox x:Name="Right" Grid.Row="0" Grid.Column="1" Text="{Binding Top, RelativeSource={RelativeSource TemplatedParent}}" Classes="thickness-edit" />
<TextBox Grid.Row="1" Grid.Column="2" Text="{Binding Right, RelativeSource={RelativeSource TemplatedParent}}" Classes="thickness-edit" />
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Bottom, RelativeSource={RelativeSource TemplatedParent}}" Classes="thickness-edit" />
<ContentPresenter Grid.Row="1" Grid.Column="1"
Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="local|ThicknessEditor[IsPresent=False]">
<Setter Property="BorderThickness" Value="0" />
</Style>
</UserControl.Styles>
<Grid ColumnDefinitions="*,Auto,280">
<Grid Grid.Column="0" ColumnDefinitions="*,Auto,Auto,Auto" RowDefinitions="Auto,*">
<TextBox Grid.Row="0"
Grid.Column="0"
BorderThickness="0"
Text="{Binding TreePage.PropertyFilter}"
Watermark="Filter properties" />
<CheckBox Grid.Row="0"
Grid.Column="1"
Margin="0,0,4,0"
Content="Regex"
IsChecked="{Binding TreePage.UseRegexFilter}" />
<DataGrid Items="{Binding PropertiesView}"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
BorderThickness="0"
RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
CanUserResizeColumns="true">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" />
<DataGridTextColumn Header="Type" Binding="{Binding Type}" />
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True" />
</DataGrid.Columns>
<DataGrid.Styles>
<Style Selector="DataGridRow TextBox">
<Setter Property="SelectionBrush" Value="LightBlue" />
</Style>
</DataGrid.Styles>
</DataGrid>
</Grid>
<GridSplitter Grid.Column="1" />
<Grid Grid.Column="2" RowDefinitions="Auto,*" >
<TextBlock Grid.Row="0" Text="Layout Visualizer" Margin="4" />
<Grid Grid.Row="1" x:Name="LayoutRoot" Margin="8,0,8,8" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,Auto">
<Border x:Name="VerticalSize" Grid.Row="0" Grid.Column="1" >
<TextBlock VerticalAlignment="Center" FontWeight="Bold"
TextDecorations="{Binding Layout.HeightConstraint, Converter={x:Static local:Converters.HasConstraintConverter}}"
Text="{Binding Layout.Height}"
ToolTip.Tip="{Binding Layout.HeightConstraint}" />
</Border>
<Border x:Name="HorizontalSize" Grid.Row="1" Grid.Column="0" >
<TextBlock HorizontalAlignment="Center" FontWeight="Bold"
TextDecorations="{Binding Layout.WidthConstraint, Converter={x:Static local:Converters.HasConstraintConverter}}"
Text="{Binding Layout.Width}"
ToolTip.Tip="{Binding Layout.WidthConstraint}" />
</Border>
<local:ThicknessEditor Grid.Row="0" Grid.Column="0" Header="margin" VerticalAlignment="Top" HorizontalAlignment="Center" Background="{StaticResource MarginBackgroundBrush}" Thickness="{Binding Layout.MarginThickness}">
<local:ThicknessEditor x:Name="BorderArea" Header="border" Background="{StaticResource BorderBackgroundBrush}" Thickness="{Binding Layout.BorderThickness}" IsPresent="{Binding Layout.HasBorder}">
<local:ThicknessEditor x:Name="PaddingArea" Header="padding" Background="{StaticResource PaddingBackgroundBrush}" Thickness="{Binding Layout.PaddingThickness}" IsPresent="{Binding Layout.HasPadding}">
<Border x:Name="ContentArea" BorderThickness="1" BorderBrush="{StaticResource ThicknessBorderBrush}" MinWidth="100" MinHeight="16" Background="{StaticResource SizeBackgroundBrush}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<TextBlock Margin="2" HorizontalAlignment="Center" VerticalAlignment="Center" Text="content" />
</Border>
</local:ThicknessEditor>
</local:ThicknessEditor>
</local:ThicknessEditor>
<Canvas Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2">
<Canvas.Styles>
<Style Selector="Rectangle">
<Setter Property="StrokeDashArray" Value="1,3" />
<Setter Property="Stroke" Value="{StaticResource SizeGuidelineBrush}" />
<Setter Property="StrokeThickness" Value="1" />
</Style>
</Canvas.Styles>
<Rectangle x:Name="HorizontalSizeBegin" />
<Rectangle x:Name="HorizontalSizeEnd" />
<Rectangle x:Name="VerticalSizeBegin" />
<Rectangle x:Name="VerticalSizeEnd" />
</Canvas>
</Grid>
</Grid>
</Grid>
</UserControl>

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

@ -1,10 +1,24 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.Views
{
internal class ControlDetailsView : UserControl
{
private ThicknessEditor _borderArea;
private ThicknessEditor _paddingArea;
private Rectangle _horizontalSizeBegin;
private Rectangle _horizontalSizeEnd;
private Rectangle _verticalSizeBegin;
private Rectangle _verticalSizeEnd;
private Grid _layoutRoot;
private Border _horizontalSize;
private Border _verticalSize;
private Border _contentArea;
public ControlDetailsView()
{
InitializeComponent();
@ -13,6 +27,101 @@ namespace Avalonia.Diagnostics.Views
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
_borderArea = this.FindControl<ThicknessEditor>("BorderArea");
_paddingArea = this.FindControl<ThicknessEditor>("PaddingArea");
_horizontalSizeBegin = this.FindControl<Rectangle>("HorizontalSizeBegin");
_horizontalSizeEnd = this.FindControl<Rectangle>("HorizontalSizeEnd");
_verticalSizeBegin = this.FindControl<Rectangle>("VerticalSizeBegin");
_verticalSizeEnd = this.FindControl<Rectangle>("VerticalSizeEnd");
_horizontalSize = this.FindControl<Border>("HorizontalSize");
_verticalSize = this.FindControl<Border>("VerticalSize");
_contentArea = this.FindControl<Border>("ContentArea");
_layoutRoot = this.FindControl<Grid>("LayoutRoot");
void SubscribeToBounds(Visual visual)
{
visual.GetPropertyChangedObservable(TransformedBoundsProperty)
.Subscribe(UpdateSizeGuidelines);
}
SubscribeToBounds(_borderArea);
SubscribeToBounds(_paddingArea);
SubscribeToBounds(_contentArea);
}
private void UpdateSizeGuidelines(AvaloniaPropertyChangedEventArgs e)
{
void UpdateGuidelines(Visual area)
{
if (area.TransformedBounds is TransformedBounds bounds)
{
// Horizontal guideline
{
var sizeArea = TranslateToRoot((_horizontalSize.TransformedBounds ?? default).Bounds.BottomLeft,
_horizontalSize);
var start = TranslateToRoot(bounds.Bounds.BottomLeft, area);
SetPosition(_horizontalSizeBegin, start);
var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
SetPosition(_horizontalSizeEnd, end.WithX(end.X - 1));
var height = sizeArea.Y - start.Y + 2;
_horizontalSizeBegin.Height = height;
_horizontalSizeEnd.Height = height;
}
// Vertical guideline
{
var sizeArea = TranslateToRoot((_verticalSize.TransformedBounds ?? default).Bounds.TopRight, _verticalSize);
var start = TranslateToRoot(bounds.Bounds.TopRight, area);
SetPosition(_verticalSizeBegin, start);
var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
SetPosition(_verticalSizeEnd, end.WithY(end.Y - 1));
var width = sizeArea.X - start.X + 2;
_verticalSizeBegin.Width = width;
_verticalSizeEnd.Width = width;
}
}
}
Point TranslateToRoot(Point point, IVisual from)
{
return from.TranslatePoint(point, _layoutRoot) ?? default;
}
static void SetPosition(Rectangle rect, Point start)
{
Canvas.SetLeft(rect, start.X);
Canvas.SetTop(rect, start.Y);
}
if (_borderArea.IsPresent)
{
UpdateGuidelines(_borderArea);
}
else if (_paddingArea.IsPresent)
{
UpdateGuidelines(_paddingArea);
}
else
{
UpdateGuidelines(_contentArea);
}
}
}
}

130
src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs

@ -0,0 +1,130 @@
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace Avalonia.Diagnostics.Views
{
internal static class Converters
{
public static IValueConverter HasConstraintConverter =
new FuncValueConverter<object, TextDecorationCollection>(ConvertToDecoration);
private static TextDecorationCollection ConvertToDecoration(object arg)
{
return arg != null ? TextDecorations.Underline : null;
}
}
internal class ThicknessEditor : ContentControl
{
public static readonly DirectProperty<ThicknessEditor, Thickness> ThicknessProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, Thickness>(nameof(Thickness), o => o.Thickness,
(o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay);
public static readonly DirectProperty<ThicknessEditor, string> HeaderProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, string>(nameof(Header), o => o.Header,
(o, v) => o.Header = v);
public static readonly DirectProperty<ThicknessEditor, bool> IsPresentProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, bool>(nameof(Header), o => o.IsPresent,
(o, v) => o.IsPresent = v);
public static readonly DirectProperty<ThicknessEditor, double> LeftProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, double>(nameof(Left), o => o.Left, (o, v) => o.Left = v);
public static readonly DirectProperty<ThicknessEditor, double> TopProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, double>(nameof(Top), o => o.Top, (o, v) => o.Top = v);
public static readonly DirectProperty<ThicknessEditor, double> RightProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, double>(nameof(Right), o => o.Right,
(o, v) => o.Right = v);
public static readonly DirectProperty<ThicknessEditor, double> BottomProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, double>(nameof(Bottom), o => o.Bottom,
(o, v) => o.Bottom = v);
private Thickness _thickness;
private string _header;
private bool _isPresent = true;
private double _left;
private double _top;
private double _right;
private double _bottom;
private bool _isUpdatingThickness;
public Thickness Thickness
{
get => _thickness;
set => SetAndRaise(ThicknessProperty, ref _thickness, value);
}
public string Header
{
get => _header;
set => SetAndRaise(HeaderProperty, ref _header, value);
}
public bool IsPresent
{
get => _isPresent;
set => SetAndRaise(IsPresentProperty, ref _isPresent, value);
}
public double Left
{
get => _left;
set => SetAndRaise(LeftProperty, ref _left, value);
}
public double Top
{
get => _top;
set => SetAndRaise(TopProperty, ref _top, value);
}
public double Right
{
get => _right;
set => SetAndRaise(RightProperty, ref _right, value);
}
public double Bottom
{
get => _bottom;
set => SetAndRaise(BottomProperty, ref _bottom, value);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == ThicknessProperty)
{
try
{
_isUpdatingThickness = true;
var value = change.NewValue.GetValueOrDefault<Thickness>();
Left = value.Left;
Top = value.Top;
Right = value.Right;
Bottom = value.Bottom;
}
finally
{
_isUpdatingThickness = false;
}
}
else if (!_isUpdatingThickness &&
(change.Property == LeftProperty || change.Property == TopProperty ||
change.Property == RightProperty || change.Property == BottomProperty))
{
Thickness = new Thickness(Left, Top, Right, Bottom);
}
}
}
}

2
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.TreePageView">
<Grid ColumnDefinitions="0.8*,4,*">
<Grid ColumnDefinitions="0.4*,4,0.6*">
<TreeView Name="tree"
BorderThickness="0"
Items="{Binding Nodes}"

4
src/Avalonia.Input/ApiCompatBaseline.txt

@ -0,0 +1,4 @@
Compat issues with assembly Avalonia.Input:
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Input.IInputElement.IsKeyboardFocusWithin' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Input.IInputElement.IsKeyboardFocusWithin.get()' is present in the implementation but not in the contract.
Total Issues: 2

5
src/Avalonia.Input/IInputElement.cs

@ -89,6 +89,11 @@ namespace Avalonia.Input
/// <see cref="IsEnabled"/> value of this control and its parent controls.
/// </remarks>
bool IsEffectivelyEnabled { get; }
/// <summary>
/// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements.
/// </summary>
bool IsKeyboardFocusWithin { get; }
/// <summary>
/// Gets a value indicating whether the control is focused.

22
src/Avalonia.Input/InputElement.cs

@ -42,6 +42,14 @@ namespace Avalonia.Input
public static readonly StyledProperty<Cursor?> CursorProperty =
AvaloniaProperty.Register<InputElement, Cursor?>(nameof(Cursor), null, true);
/// <summary>
/// Defines the <see cref="IsKeyboardFocusWithin"/> property.
/// </summary>
public static readonly DirectProperty<InputElement, bool> IsKeyboardFocusWithinProperty =
AvaloniaProperty.RegisterDirect<InputElement, bool>(
nameof(IsKeyboardFocusWithin),
o => o.IsKeyboardFocusWithin);
/// <summary>
/// Defines the <see cref="IsFocused"/> property.
/// </summary>
@ -160,6 +168,7 @@ namespace Avalonia.Input
private bool _isEffectivelyEnabled = true;
private bool _isFocused;
private bool _isKeyboardFocusWithin;
private bool _isFocusVisible;
private bool _isPointerOver;
private GestureRecognizerCollection? _gestureRecognizers;
@ -343,6 +352,15 @@ namespace Avalonia.Input
get { return GetValue(CursorProperty); }
set { SetValue(CursorProperty, value); }
}
/// <summary>
/// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements.
/// </summary>
public bool IsKeyboardFocusWithin
{
get => _isKeyboardFocusWithin;
internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value);
}
/// <summary>
/// Gets a value indicating whether the control is focused.
@ -544,6 +562,10 @@ namespace Avalonia.Input
{
UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault<bool>());
}
else if (change.Property == IsKeyboardFocusWithinProperty)
{
PseudoClasses.Set(":focus-within", _isKeyboardFocusWithin);
}
}
/// <summary>

117
src/Avalonia.Input/KeyboardDevice.cs

@ -9,6 +9,7 @@ namespace Avalonia.Input
public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged
{
private IInputElement? _focusedElement;
private IInputRoot? _focusedRoot;
public event PropertyChangedEventHandler? PropertyChanged;
@ -28,10 +29,115 @@ namespace Avalonia.Input
private set
{
_focusedElement = value;
if (_focusedElement != null && _focusedElement.IsAttachedToVisualTree)
{
_focusedRoot = _focusedElement.VisualRoot as IInputRoot;
}
else
{
_focusedRoot = null;
}
RaisePropertyChanged();
}
}
private void ClearFocusWithinAncestors(IInputElement? element)
{
var el = element;
while (el != null)
{
if (el is InputElement ie)
{
ie.IsKeyboardFocusWithin = false;
}
el = (IInputElement)el.VisualParent;
}
}
private void ClearFocusWithin(IInputElement element, bool clearRoot)
{
foreach (var visual in element.VisualChildren)
{
if (visual is IInputElement el && el.IsKeyboardFocusWithin)
{
ClearFocusWithin(el, true);
break;
}
}
if (clearRoot)
{
if (element is InputElement ie)
{
ie.IsKeyboardFocusWithin = false;
}
}
}
private void SetIsFocusWithin(IInputElement? oldElement, IInputElement? newElement)
{
if (newElement == null && oldElement != null)
{
ClearFocusWithinAncestors(oldElement);
return;
}
IInputElement? branch = null;
var el = newElement;
while (el != null)
{
if (el.IsKeyboardFocusWithin)
{
branch = el;
break;
}
el = el.VisualParent as IInputElement;
}
el = oldElement;
if (el != null && branch != null)
{
ClearFocusWithin(branch, false);
}
el = newElement;
while (el != null && el != branch)
{
if (el is InputElement ie)
{
ie.IsKeyboardFocusWithin = true;
}
el = el.VisualParent as IInputElement;
}
}
private void ClearChildrenFocusWithin(IInputElement element, bool clearRoot)
{
foreach (var visual in element.VisualChildren)
{
if (visual is IInputElement el && el.IsKeyboardFocusWithin)
{
ClearChildrenFocusWithin(el, true);
break;
}
}
if (clearRoot && element is InputElement ie)
{
ie.IsKeyboardFocusWithin = false;
}
}
public void SetFocusedElement(
IInputElement? element,
NavigationMethod method,
@ -40,6 +146,17 @@ namespace Avalonia.Input
if (element != FocusedElement)
{
var interactive = FocusedElement as IInteractive;
if (FocusedElement != null &&
(!FocusedElement.IsAttachedToVisualTree ||
_focusedRoot != element?.VisualRoot as IInputRoot) &&
_focusedRoot != null)
{
ClearChildrenFocusWithin(_focusedRoot, true);
}
SetIsFocusWithin(FocusedElement, element);
FocusedElement = element;
interactive?.RaiseEvent(new RoutedEventArgs

1
src/Avalonia.MicroCom/Avalonia.MicroCom.csproj

@ -10,7 +10,6 @@
<ProjectReference Include="..\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<ExcludeAssets>all</ExcludeAssets>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
</ProjectReference>
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />

33
src/Avalonia.Native/Avalonia.Native.csproj

@ -20,36 +20,7 @@
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<AvnComIdl Include="avn.idl" OutputFile="Interop.Generated.cs" />
</ItemGroup>
<!-- COM Interop generation -->
<ItemGroup>
<ProjectReference Include="..\tools\MicroComGenerator\MicroComGenerator.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<ExcludeAssets>all</ExcludeAssets>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
</ProjectReference>
</ItemGroup>
<Target Name="GenerateAvaloniaNativeComInterop"
BeforeTargets="CoreCompile"
DependsOnTargets="ResolveReferences"
Inputs="avn.idl;$(MSBuildThisFileDirectory)/../tools/MicroComGenerator/**/*.cs"
Outputs="Interop.Generated.cs">
<Message Importance="high" Text="Generating Interop.Generated.cs" />
<Exec Command="dotnet ../tools/MicroComGenerator/bin/$(Configuration)/netcoreapp3.1/MicroComGenerator.dll -i avn.idl --cs Interop.Generated.cs" LogStandardErrorAsError="true" />
<ItemGroup>
<!-- Remove and re-add generated file, this is needed for the clean build -->
<Compile Remove="Interop.Generated.cs"/>
<Compile Include="Interop.Generated.cs"/>
</ItemGroup>
</Target>
<ItemGroup>
<UpToDateCheckInput Include="avn.idl"/>
<UpToDateCheckInput Include="$(MSBuildThisFileDirectory)/../tools/MicroComGenerator/**/*.cs"/>
</ItemGroup>
<PropertyGroup>
<_AvaloniaPatchComInterop>true</_AvaloniaPatchComInterop>
</PropertyGroup>
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="../../build/MicroCom.targets" />
</Project>

5
src/Avalonia.Native/WindowImplBase.cs

@ -155,10 +155,10 @@ namespace Avalonia.Native
}
finally
{
_parent?.Dispose();
n?.Dispose();
}
_parent._mouse.Dispose();
}
void IAvnWindowBaseEvents.Activated() => _parent.Activated?.Invoke();
@ -336,6 +336,7 @@ namespace Avalonia.Native
_nativeControlHost = null;
(Screen as ScreenImpl)?.Dispose();
_mouse.Dispose();
}

12
src/Avalonia.Visuals/Media/FontFamily.cs

@ -133,6 +133,16 @@ namespace Avalonia.Media
}
}
/// <summary>
/// Parses a <see cref="T:Avalonia.Media.FontFamily"/> string.
/// </summary>
/// <param name="s">The <see cref="T:Avalonia.Media.FontFamily"/> string.</param>
/// <returns></returns>
/// <exception cref="ArgumentException">
/// Specified family is not supported.
/// </exception>
public static FontFamily Parse(string s) => Parse(s, null);
/// <summary>
/// Parses a <see cref="T:Avalonia.Media.FontFamily"/> string.
/// </summary>
@ -142,7 +152,7 @@ namespace Avalonia.Media
/// <exception cref="ArgumentException">
/// Specified family is not supported.
/// </exception>
public static FontFamily Parse(string s, Uri baseUri = null)
public static FontFamily Parse(string s, Uri baseUri)
{
if (string.IsNullOrEmpty(s))
{

4
src/Avalonia.Visuals/Media/TextDecoration.cs

@ -189,11 +189,11 @@ namespace Avalonia.Media
break;
case TextDecorationLocation.Strikethrough:
origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X,
shapedTextCharacters.GlyphRun.BaselineOrigin.Y - fontMetrics.StrikethroughPosition);
shapedTextCharacters.GlyphRun.BaselineOrigin.Y + fontMetrics.StrikethroughPosition);
break;
case TextDecorationLocation.Underline:
origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X,
shapedTextCharacters.GlyphRun.BaselineOrigin.Y - fontMetrics.UnderlinePosition);
shapedTextCharacters.GlyphRun.BaselineOrigin.Y + fontMetrics.UnderlinePosition);
break;
}

17
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@ -48,7 +48,7 @@ namespace Avalonia.Data
/// Gets or sets the binding priority.
/// </summary>
public BindingPriority Priority { get; set; }
/// <summary>
/// Gets or sets the relative source for the binding.
/// </summary>
@ -77,12 +77,12 @@ namespace Avalonia.Data
// We only respect `StringFormat` if the type of the property we're assigning to will
// accept a string. Note that this is slightly different to WPF in that WPF only applies
// `StringFormat` for target type `string` (not `object`).
if (!string.IsNullOrWhiteSpace(StringFormat) &&
if (!string.IsNullOrWhiteSpace(StringFormat) &&
(targetType == typeof(string) || targetType == typeof(object)))
{
converter = new StringFormatMultiValueConverter(StringFormat, converter);
}
var children = Bindings.Select(x => x.Initiate(target, null));
var input = children.Select(x => x.Observable)
@ -116,7 +116,16 @@ namespace Avalonia.Data
}
var culture = CultureInfo.CurrentCulture;
var converted = converter.Convert(values, targetType, ConverterParameter, culture);
values = new System.Collections.ObjectModel.ReadOnlyCollection<object>(values);
object converted;
if (converter != null)
{
converted = converter.Convert(values, targetType, ConverterParameter, culture);
}
else
{
converted = values;
}
if (converted == null)
{

6
src/Windows/Avalonia.Win32/Composition/CompositionConnector.cs

@ -127,17 +127,13 @@ namespace Avalonia.Win32
private SpriteVisual CreateBlur()
{
var blurEffect = new GaussianBlurEffect(new CompositionEffectSourceParameter("backdrop"));
var blurEffectFactory = _compositor.CreateEffectFactory(blurEffect);
var blurBrush = blurEffectFactory.CreateBrush();
var backDropBrush = _compositor.CreateBackdropBrush();
blurBrush.SetSourceParameter("backdrop", backDropBrush);
var saturateEffect = new SaturationEffect(blurEffect);
var satEffectFactory = _compositor.CreateEffectFactory(saturateEffect);
var satBrush = satEffectFactory.CreateBrush();
satBrush.SetSourceParameter("backdrop", backDropBrush);
var visual = _compositor.CreateSpriteVisual();

2
src/Windows/Avalonia.Win32/Composition/SaturationEffect.cs

@ -6,7 +6,7 @@ namespace Avalonia.Win32
{
class SaturationEffect : EffectBase
{
public SaturationEffect(IGraphicsEffect source) : base(source)
public SaturationEffect(IGraphicsEffectSource source) : base(source)
{
}

7
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs

@ -1216,6 +1216,7 @@ namespace Avalonia.Controls.UnitTests.Selection
var data = (AvaloniaList<string>)target.Source!;
var selectionChangedRaised = 0;
var selectedIndexRaised = 0;
var selectedItemRaised = 0;
var indexesChangedRaised = 0;
target.Source = data;
@ -1227,6 +1228,11 @@ namespace Avalonia.Controls.UnitTests.Selection
{
++selectedIndexRaised;
}
if (e.PropertyName == nameof(target.SelectedItem))
{
++selectedItemRaised;
}
};
target.IndexesChanged += (s, e) => ++indexesChangedRaised;
@ -1249,6 +1255,7 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal(2, target.AnchorIndex);
Assert.Equal(1, selectionChangedRaised);
Assert.Equal(1, selectedIndexRaised);
Assert.Equal(1, selectedItemRaised);
Assert.Equal(0, indexesChangedRaised);
}

7
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs

@ -1040,6 +1040,7 @@ namespace Avalonia.Controls.UnitTests.Selection
var data = (AvaloniaList<string>)target.Source!;
var selectionChangedRaised = 0;
var selectedIndexRaised = 0;
var selectedItemRaised = 0;
target.Source = data;
target.Select(1);
@ -1050,6 +1051,11 @@ namespace Avalonia.Controls.UnitTests.Selection
{
++selectedIndexRaised;
}
if (e.PropertyName == nameof(target.SelectedItem))
{
++selectedItemRaised;
}
};
target.SelectionChanged += (s, e) =>
@ -1070,6 +1076,7 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal(-1, target.AnchorIndex);
Assert.Equal(1, selectionChangedRaised);
Assert.Equal(1, selectedIndexRaised);
Assert.Equal(1, selectedItemRaised);
}
[Fact]

44
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -675,6 +675,50 @@ namespace Avalonia.Controls.UnitTests
Assert.Same(node, focus.Current);
}
}
[Fact]
public void Keyboard_Navigation_Should_Not_Crash_If_Selected_Item_Is_not_In_Tree()
{
using (Application())
{
var focus = FocusManager.Instance;
var navigation = AvaloniaLocator.Current.GetService<IKeyboardNavigationHandler>();
var data = CreateTestTreeData();
var selectedNode = new Node { Value = "Out of Tree Selected Item" };
var target = new TreeView
{
Template = CreateTreeViewTemplate(),
Items = data,
SelectedItem = selectedNode
};
var button = new Button();
var root = new TestRoot
{
Child = new StackPanel
{
Children = { target, button },
}
};
CreateNodeDataTemplate(target);
ApplyTemplates(target);
ExpandAll(target);
var item = data[0].Children[0];
var node = target.ItemContainerGenerator.Index.ContainerFromItem(item);
Assert.NotNull(node);
target.SelectedItem = selectedNode;
node.Focus();
Assert.Same(node, focus.Current);
var next = KeyboardNavigationHandler.GetNext(node, NavigationDirection.Previous);
}
}
[Fact]
public void Pressing_SelectAll_Gesture_Should_Select_All_Nodes()

197
tests/Avalonia.Input.UnitTests/InputElement_Focus.cs

@ -121,5 +121,202 @@ namespace Avalonia.Input.UnitTests
Assert.False(target2.Classes.Contains(":focus-visible"));
}
}
[Fact]
public void Control_FocusWithin_PseudoClass_Should_Be_Applied()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
target1,
target2
}
}
};
target1.ApplyTemplate();
target2.ApplyTemplate();
FocusManager.Instance?.Focus(target1);
Assert.True(target1.IsFocused);
Assert.True(target1.Classes.Contains(":focus-within"));
Assert.True(target1.IsKeyboardFocusWithin);
Assert.True(root.Child.Classes.Contains(":focus-within"));
Assert.True(root.Child.IsKeyboardFocusWithin);
Assert.True(root.Classes.Contains(":focus-within"));
Assert.True(root.IsKeyboardFocusWithin);
}
}
[Fact]
public void Control_FocusWithin_PseudoClass_Should_Be_Applied_and_Removed()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var panel1 = new Panel { Children = { target1 } };
var panel2 = new Panel { Children = { target2 } };
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
panel1,
panel2
}
}
};
target1.ApplyTemplate();
target2.ApplyTemplate();
FocusManager.Instance?.Focus(target1);
Assert.True(target1.IsFocused);
Assert.True(target1.Classes.Contains(":focus-within"));
Assert.True(target1.IsKeyboardFocusWithin);
Assert.True(panel1.Classes.Contains(":focus-within"));
Assert.True(panel1.IsKeyboardFocusWithin);
Assert.True(root.Child.Classes.Contains(":focus-within"));
Assert.True(root.Child.IsKeyboardFocusWithin);
Assert.True(root.Classes.Contains(":focus-within"));
Assert.True(root.IsKeyboardFocusWithin);
FocusManager.Instance?.Focus(target2);
Assert.False(target1.IsFocused);
Assert.False(target1.Classes.Contains(":focus-within"));
Assert.False(target1.IsKeyboardFocusWithin);
Assert.False(panel1.Classes.Contains(":focus-within"));
Assert.False(panel1.IsKeyboardFocusWithin);
Assert.True(root.Child.Classes.Contains(":focus-within"));
Assert.True(root.Child.IsKeyboardFocusWithin);
Assert.True(root.Classes.Contains(":focus-within"));
Assert.True(root.IsKeyboardFocusWithin);
Assert.True(target2.IsFocused);
Assert.True(target2.Classes.Contains(":focus-within"));
Assert.True(target2.IsKeyboardFocusWithin);
Assert.True(panel2.Classes.Contains(":focus-within"));
Assert.True(panel2.IsKeyboardFocusWithin);
}
}
[Fact]
public void Control_FocusWithin_Pseudoclass_Should_Be_Removed_When_Removed_From_Tree()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
target1,
target2
}
}
};
target1.ApplyTemplate();
target2.ApplyTemplate();
FocusManager.Instance?.Focus(target1);
Assert.True(target1.IsFocused);
Assert.True(target1.Classes.Contains(":focus-within"));
Assert.True(target1.IsKeyboardFocusWithin);
Assert.True(root.Child.Classes.Contains(":focus-within"));
Assert.True(root.Child.IsKeyboardFocusWithin);
Assert.True(root.Classes.Contains(":focus-within"));
Assert.True(root.IsKeyboardFocusWithin);
Assert.Equal(KeyboardDevice.Instance.FocusedElement, target1);
root.Child = null;
Assert.Null(KeyboardDevice.Instance.FocusedElement);
Assert.False(target1.IsFocused);
Assert.False(target1.Classes.Contains(":focus-within"));
Assert.False(target1.IsKeyboardFocusWithin);
Assert.False(root.Classes.Contains(":focus-within"));
Assert.False(root.IsKeyboardFocusWithin);
}
}
[Fact]
public void Control_FocusWithin_Pseudoclass_Should_Be_Removed_Focus_Moves_To_Different_Root()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var root1 = new TestRoot
{
Child = new StackPanel
{
Children =
{
target1,
}
}
};
var root2 = new TestRoot
{
Child = new StackPanel
{
Children =
{
target2,
}
}
};
target1.ApplyTemplate();
target2.ApplyTemplate();
FocusManager.Instance?.Focus(target1);
Assert.True(target1.IsFocused);
Assert.True(target1.Classes.Contains(":focus-within"));
Assert.True(target1.IsKeyboardFocusWithin);
Assert.True(root1.Child.Classes.Contains(":focus-within"));
Assert.True(root1.Child.IsKeyboardFocusWithin);
Assert.True(root1.Classes.Contains(":focus-within"));
Assert.True(root1.IsKeyboardFocusWithin);
Assert.Equal(KeyboardDevice.Instance.FocusedElement, target1);
FocusManager.Instance?.Focus(target2);
Assert.False(target1.IsFocused);
Assert.False(target1.Classes.Contains(":focus-within"));
Assert.False(target1.IsKeyboardFocusWithin);
Assert.False(root1.Child.Classes.Contains(":focus-within"));
Assert.False(root1.Child.IsKeyboardFocusWithin);
Assert.False(root1.Classes.Contains(":focus-within"));
Assert.False(root1.IsKeyboardFocusWithin);
Assert.True(target2.IsFocused);
Assert.True(target2.Classes.Contains(":focus-within"));
Assert.True(target2.IsKeyboardFocusWithin);
Assert.True(root2.Child.Classes.Contains(":focus-within"));
Assert.True(root2.Child.IsKeyboardFocusWithin);
Assert.True(root2.Classes.Contains(":focus-within"));
Assert.True(root2.IsKeyboardFocusWithin);
}
}
}
}

23
tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs

@ -157,6 +157,29 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal("1,2,Fallback", target.Text);
}
[Fact]
public void MultiBinding_Without_StringFormat_And_Converter()
{
var source = new { A = 1, B = 2, C = 3 };
var target = new ItemsControl { };
var binding = new MultiBinding
{
Bindings = new[]
{
new Binding { Path = "A", Source = source },
new Binding { Path = "B", Source = source },
new Binding { Path = "C", Source = source },
},
};
target.Bind(ItemsControl.ItemsProperty, binding);
Assert.Equal(target.ItemCount, 3);
Assert.Equal(target.Items.ElementAt(0), source.A);
Assert.Equal(target.Items.ElementAt(1), source.B);
Assert.Equal(target.Items.ElementAt(2), source.C);
}
private class ConcatConverter : IMultiValueConverter
{
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)

21
tests/Avalonia.Markup.UnitTests/Extensions/IEnummerableExtension.cs

@ -0,0 +1,21 @@
using System;
using System.Collections;
namespace Avalonia.Markup.UnitTests
{
internal static class IEnumerableExtensions
{
public static object ElementAt(this IEnumerable source, int index)
{
var i = -1;
var enumerator = source.GetEnumerator();
while (enumerator.MoveNext() && ++i < index);
if (i == index)
{
return enumerator.Current;
}
throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
Loading…
Cancel
Save