Browse Source

Merge branch 'master' into feature/2736-applicationShouldTerminate

pull/6174/head
Dan Walmsley 5 years ago
committed by GitHub
parent
commit
20098b6824
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      native/Avalonia.Native/src/OSX/window.mm
  2. 102
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
  3. 45
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs
  4. 157
      samples/ControlCatalog/Pages/ContextFlyoutPage.xaml
  5. 91
      samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs
  6. 140
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  7. 42
      samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
  8. 25
      samples/ControlCatalog/Pages/DataGridPage.xaml
  9. 78
      samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs
  10. 4
      samples/ControlCatalog/ViewModels/ContextPageViewModel.cs
  11. 42
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  12. 4
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  13. 24
      src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs
  14. 1
      src/Avalonia.Controls/ComboBox.cs
  15. 65
      src/Avalonia.Controls/ContextMenu.cs
  16. 58
      src/Avalonia.Controls/ContextRequestedEventArgs.cs
  17. 62
      src/Avalonia.Controls/Control.cs
  18. 167
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  19. 2
      src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs
  20. 10
      src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs
  21. 67
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  22. 10
      src/Avalonia.Controls/Primitives/Popup.cs
  23. 2
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  24. 20
      src/Avalonia.Controls/Primitives/Thumb.cs
  25. 21
      src/Avalonia.Controls/TextBox.cs
  26. 15
      src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs
  27. 2
      src/Avalonia.Native/WindowImpl.cs
  28. 2
      src/Avalonia.Themes.Default/TextBox.xaml
  29. 2
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  30. 2
      src/Avalonia.Visuals/Media/GlyphRun.cs
  31. 87
      src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
  32. 9
      src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
  33. 86
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  34. 19
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  35. 28
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs
  36. 9
      src/Windows/Avalonia.Win32/Win32Platform.cs
  37. 247
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  38. 234
      tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
  39. 29
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  40. 104
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
  41. 35
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  42. 28
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

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

@ -29,10 +29,12 @@ public:
IAvnMenu* _mainMenu;
bool _shown;
bool _inResize;
WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl)
{
_shown = false;
_inResize = false;
_mainMenu = nullptr;
BaseEvents = events;
_glContext = gl;
@ -277,6 +279,13 @@ public:
virtual HRESULT Resize(double x, double y) override
{
if(_inResize)
{
return S_OK;
}
_inResize = true;
START_COM_CALL;
@autoreleasepool
@ -304,13 +313,19 @@ public:
y = maxSize.height;
}
if(!_shown)
@try
{
BaseEvents->Resized(AvnSize{x,y});
if(!_shown)
{
BaseEvents->Resized(AvnSize{x,y});
}
[Window setContentSize:NSSize{x, y}];
}
@finally
{
_inResize = false;
}
[StandardContainer setFrameSize:NSSize{x,y}];
[Window setContentSize:NSSize{x, y}];
return S_OK;
}
@ -757,6 +772,7 @@ private:
}
_lastWindowState = state;
_actualWindowState = state;
WindowEvents->WindowStateChanged(state);
}
}
@ -1276,6 +1292,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[_blurBehind setWantsLayer:true];
_blurBehind.hidden = true;
[_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
[_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
[self addSubview:_blurBehind];
[self addSubview:_content];
@ -1311,9 +1330,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_settingSize = true;
[super setFrameSize:newSize];
[_blurBehind setFrameSize:newSize];
[_content setFrameSize:newSize];
auto window = objc_cast<AvnWindow>([self window]);
// TODO get actual titlebar size
@ -1329,6 +1345,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[_titleBarMaterial setFrame:tbar];
tbar.size.height = height < 1 ? 0 : 1;
[_titleBarUnderline setFrame:tbar];
_settingSize = false;
}
@ -2363,11 +2380,12 @@ protected:
virtual HRESULT Resize(double x, double y) override
{
START_COM_CALL;
@autoreleasepool
{
if (Window != nullptr)
{
[StandardContainer setFrameSize:NSSize{x,y}];
[Window setContentSize:NSSize{x, y}];
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];

102
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml

@ -1,102 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ControlCatalog.Pages.ContextFlyoutPage">
<UserControl.Styles>
<Style Selector="FlyoutPresenter.NoPadding">
<Setter Property="Padding" Value="0" />
</Style>
</UserControl.Styles>
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Context Flyout</TextBlock>
<TextBlock Classes="h2">A right click Flyout that can be applied to any control.</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<Border Background="{DynamicResource SystemAccentColor}"
Margin="16"
Padding="48,48,48,48">
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
<MenuItem Header="_Disabled Menu Item" IsEnabled="False" InputGesture="Ctrl+D" />
<Separator/>
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1"/>
<MenuItem Header="Submenu _2"/>
</MenuItem>
<MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
<MenuItem.Icon>
<Image Source="/Assets/github_icon.png"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Menu Item with _Checkbox">
<MenuItem.Icon>
<CheckBox BorderThickness="0" IsHitTestVisible="False" IsChecked="True"/>
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Border.ContextFlyout>
<TextBlock Text="Defined in XAML"/>
</Border>
<Border Background="{DynamicResource SystemAccentColor}"
Margin="16"
Padding="48,48,48,48">
<Border.ContextMenu>
<ContextMenu Items="{Binding MenuItems}">
<ContextMenu.Styles>
<Style Selector="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style>
</ContextMenu.Styles>
</ContextMenu>
</Border.ContextMenu>
<TextBlock Text="Dynamically Generated"/>
</Border>
</StackPanel>
<TextBlock Text="Custom ContextFlyout for TextBox" />
<TextBox Name="TextBox" Width="150" HorizontalAlignment="Center" ContextMenu="{x:Null}">
<TextBox.ContextFlyout>
<Flyout FlyoutPresenterClasses="NoPadding">
<StackPanel Orientation="Horizontal">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="40" />
<Setter Property="Width" Value="40" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Opacity" Value="0.5" />
</Style>
</StackPanel.Styles>
<Button Name="CutButton" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}">
<PathIcon Width="14" Height="14" Data="M5.22774,2.08072 C5.43359778,1.94704 5.7011484,1.98419259 5.86368634,2.15675215 L5.91939,2.22774 L12.5191,12.3904 C12.956,12.1419 13.4614,12.0000019 14,12.0000019 C15.6569,12.0000019 17,13.3431 17,15.0000019 C17,16.6569 15.6569,18.0000019 14,18.0000019 C12.3431,18.0000019 11,16.6569 11,15.0000019 C11,14.3201402 11.226152,13.693011 11.6073785,13.1899092 L11.7401,13.0269 L10,10.3474 L8.25991,13.0269 C8.72078,13.5543 9,14.2446 9,15.0000019 C9,16.6569 7.65685,18.0000019 6,18.0000019 C4.34315,18.0000019 3,16.6569 3,15.0000019 C3,13.3431 4.34315,12.0000019 6,12.0000019 C6.46163143,12.0000019 6.89890041,12.1042536 7.28955831,12.2905296 L7.4809,12.3904 L9.40382,9.42936 L5.08072,2.77238 C4.93033,2.54079 4.99615,2.23112 5.22774,2.08072 Z M14,13 C12.8954,13 12,13.8954 12,15 C12,16.1046 12.8954,17 14,17 C15.1046,17 16,16.1046 16,15 C16,13.8954 15.1046,13 14,13 Z M6,13 C4.89543,13 4,13.8954 4,15 C4,16.1046 4.89543,17 6,17 C7.10457,17 8,16.1046 8,15 C8,13.8954 7.10457,13 6,13 Z M14.7723,2.08072 C15.0039,2.23112 15.0697,2.54079 14.9193,2.77238 L11.1924,8.51133 L10.5962,7.59329 L14.0806,2.22774 C14.231,1.99615 14.5407,1.93033 14.7723,2.08072 Z" />
</Button>
<Button Name="CopyButton" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}">
<PathIcon Width="14" Height="14" Data="M5.50280381,4.62704038 L5.5,6.75 L5.5,17.2542087 C5.5,19.0491342 6.95507456,20.5042087 8.75,20.5042087 L17.3662868,20.5044622 C17.057338,21.3782241 16.2239751,22.0042087 15.2444057,22.0042087 L8.75,22.0042087 C6.12664744,22.0042087 4,19.8775613 4,17.2542087 L4,6.75 C4,5.76928848 4.62744523,4.93512464 5.50280381,4.62704038 Z M17.75,2 C18.9926407,2 20,3.00735931 20,4.25 L20,17.25 C20,18.4926407 18.9926407,19.5 17.75,19.5 L8.75,19.5 C7.50735931,19.5 6.5,18.4926407 6.5,17.25 L6.5,4.25 C6.5,3.00735931 7.50735931,2 8.75,2 L17.75,2 Z M17.75,3.5 L8.75,3.5 C8.33578644,3.5 8,3.83578644 8,4.25 L8,17.25 C8,17.6642136 8.33578644,18 8.75,18 L17.75,18 C18.1642136,18 18.5,17.6642136 18.5,17.25 L18.5,4.25 C18.5,3.83578644 18.1642136,3.5 17.75,3.5 Z" />
</Button>
<Button Name="PasteButton" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}">
<PathIcon Width="14" Height="14" Data="M13.75,2 C14.940864,2 15.9156449,2.92516159 15.9948092,4.09595119 L16,4.25 L16,4.25 C16,4.16530567 15.9953205,4.0817043 15.9862059,3.99944035 L17.75,4 C18.9926407,4 20,5.00735931 20,6.25 L20,19.75 C20,20.9926407 18.9926407,22 17.75,22 L6.25,22 C5.00735931,22 4,20.9926407 4,19.75 L4,6.25 C4,5.00735931 5.00735931,4 6.25,4 L8.01379413,3.99944035 C8.00733496,4.05773764 8.00310309,4.11670658 8.00118552,4.17626017 L8,4.25 C8,3.00735931 9.00735931,2 10.25,2 L13.75,2 Z M13.75,6.5 L10.25,6.5 C9.45594921,6.5 8.75796956,6.08867052 8.357512,5.4674625 L8.37902077,5.50019943 L8.37902077,5.50019943 L6.25,5.5 C5.83578644,5.5 5.5,5.83578644 5.5,6.25 L5.5,19.75 C5.5,20.1642136 5.83578644,20.5 6.25,20.5 L17.75,20.5 C18.1642136,20.5 18.5,20.1642136 18.5,19.75 L18.5,6.25 C18.5,5.83578644 18.1642136,5.5 17.75,5.5 L15.6209792,5.50019943 L15.642488,5.4674625 C15.2420304,6.08867052 14.5440508,6.5 13.75,6.5 Z M13.75,3.5 L10.25,3.5 C9.83578644,3.5 9.5,3.83578644 9.5,4.25 C9.5,4.66421356 9.83578644,5 10.25,5 L13.75,5 C14.1642136,5 14.5,4.66421356 14.5,4.25 C14.5,3.83578644 14.1642136,3.5 13.75,3.5 Z" />
</Button>
<Button Name="ClearButton" Command="{Binding $parent[TextBox].Clear}">
<PathIcon Width="14" Height="14" Data="M3.52499419,3.71761187 L3.61611652,3.61611652 C4.0717282,3.16050485 4.79154862,3.13013074 5.28238813,3.52499419 L5.38388348,3.61611652 L14,12.233 L22.6161165,3.61611652 C23.1042719,3.12796116 23.8957281,3.12796116 24.3838835,3.61611652 C24.8720388,4.10427189 24.8720388,4.89572811 24.3838835,5.38388348 L15.767,14 L24.3838835,22.6161165 C24.8394952,23.0717282 24.8698693,23.7915486 24.4750058,24.2823881 L24.3838835,24.3838835 C23.9282718,24.8394952 23.2084514,24.8698693 22.7176119,24.4750058 L22.6161165,24.3838835 L14,15.767 L5.38388348,24.3838835 C4.89572811,24.8720388 4.10427189,24.8720388 3.61611652,24.3838835 C3.12796116,23.8957281 3.12796116,23.1042719 3.61611652,22.6161165 L12.233,14 L3.61611652,5.38388348 C3.16050485,4.9282718 3.13013074,4.20845138 3.52499419,3.71761187 L3.61611652,3.61611652 L3.52499419,3.71761187 Z" />
</Button>
</StackPanel>
</Flyout>
</TextBox.ContextFlyout>
</TextBox>
</StackPanel>
</UserControl>

45
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs

@ -1,45 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public class ContextFlyoutPage : UserControl
{
private TextBox _textBox;
public ContextFlyoutPage()
{
InitializeComponent();
var vm = new ContextFlyoutPageViewModel();
vm.View = this;
DataContext = vm;
_textBox = this.FindControl<TextBox>("TextBox");
var cutButton = this.FindControl<Button>("CutButton");
cutButton.Click += CloseFlyout;
var copyButton = this.FindControl<Button>("CopyButton");
copyButton.Click += CloseFlyout;
var pasteButton = this.FindControl<Button>("PasteButton");
pasteButton.Click += CloseFlyout;
var clearButton = this.FindControl<Button>("ClearButton");
clearButton.Click += CloseFlyout;
}
private void CloseFlyout(object sender, RoutedEventArgs e)
{
_textBox.ContextFlyout.Hide();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

157
samples/ControlCatalog/Pages/ContextFlyoutPage.xaml

@ -0,0 +1,157 @@
<UserControl x:Class="ControlCatalog.Pages.ContextFlyoutPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.Styles>
<Style Selector="FlyoutPresenter.NoPadding">
<Setter Property="Padding" Value="0" />
</Style>
</UserControl.Styles>
<StackPanel Orientation="Vertical" Spacing="4">
<StackPanel.Styles>
<Style Selector="Border.context-target">
<Setter Property="Padding" Value="48,20" />
<Setter Property="Margin" Value="8" />
<Setter Property="Focusable" Value="True" />
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" />
</Style>
<Style Selector="Border.context-target > :is(Control)">
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</StackPanel.Styles>
<TextBlock Classes="h1">Context Flyout</TextBlock>
<TextBlock Classes="h2">A right click Flyout that can be applied to any control.</TextBlock>
<UniformGrid HorizontalAlignment="Center" Rows="2">
<Border Classes="context-target">
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
<MenuItem Header="_Disabled Menu Item"
InputGesture="Ctrl+D"
IsEnabled="False" />
<Separator />
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1" />
<MenuItem Header="Submenu _2" />
</MenuItem>
<MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
<MenuItem.Icon>
<Image Source="/Assets/github_icon.png" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Menu Item with _Checkbox">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="True"
IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Border.ContextFlyout>
<TextBlock Text="Defined in XAML" />
</Border>
<Border Classes="context-target">
<Border.Styles>
<Style Selector="MenuFlyoutPresenter MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style>
</Border.Styles>
<Border.ContextFlyout>
<MenuFlyout Items="{Binding MenuItems}" />
</Border.ContextFlyout>
<TextBlock Text="Dynamically Generated"/>
</Border>
<Border x:Name="CustomContextRequestedBorder"
Classes="context-target">
<Border.ContextFlyout>
<Flyout Content="Should never be visible" />
</Border.ContextFlyout>
<TextBlock Text="Custom ContextRequested handler" TextWrapping="Wrap" />
</Border>
<Border x:Name="CancellableContextBorder"
Classes="context-target">
<Border.ContextFlyout>
<Flyout>
<CheckBox x:Name="CancelCloseCheckBox" Content="Cancel close" />
</Flyout>
</Border.ContextFlyout>
<StackPanel>
<TextBlock Text="Cancellable" />
<CheckBox x:Name="CancelOpenCheckBox" Content="Cancel open" />
</StackPanel>
</Border>
</UniformGrid>
<TextBlock Text="Custom ContextFlyout for TextBox" />
<TextBox Name="TextBox"
Width="150"
HorizontalAlignment="Center"
ContextMenu="{x:Null}">
<TextBox.ContextFlyout>
<Flyout FlyoutPresenterClasses="NoPadding">
<StackPanel>
<StackPanel Orientation="Horizontal">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="40" />
<Setter Property="Width" Value="40" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Opacity" Value="0.5" />
</Style>
</StackPanel.Styles>
<Button Name="CutButton"
Command="{Binding $parent[TextBox].Cut}"
IsEnabled="{Binding $parent[TextBox].CanCut}">
<PathIcon Width="14"
Height="14"
Data="M5.22774,2.08072 C5.43359778,1.94704 5.7011484,1.98419259 5.86368634,2.15675215 L5.91939,2.22774 L12.5191,12.3904 C12.956,12.1419 13.4614,12.0000019 14,12.0000019 C15.6569,12.0000019 17,13.3431 17,15.0000019 C17,16.6569 15.6569,18.0000019 14,18.0000019 C12.3431,18.0000019 11,16.6569 11,15.0000019 C11,14.3201402 11.226152,13.693011 11.6073785,13.1899092 L11.7401,13.0269 L10,10.3474 L8.25991,13.0269 C8.72078,13.5543 9,14.2446 9,15.0000019 C9,16.6569 7.65685,18.0000019 6,18.0000019 C4.34315,18.0000019 3,16.6569 3,15.0000019 C3,13.3431 4.34315,12.0000019 6,12.0000019 C6.46163143,12.0000019 6.89890041,12.1042536 7.28955831,12.2905296 L7.4809,12.3904 L9.40382,9.42936 L5.08072,2.77238 C4.93033,2.54079 4.99615,2.23112 5.22774,2.08072 Z M14,13 C12.8954,13 12,13.8954 12,15 C12,16.1046 12.8954,17 14,17 C15.1046,17 16,16.1046 16,15 C16,13.8954 15.1046,13 14,13 Z M6,13 C4.89543,13 4,13.8954 4,15 C4,16.1046 4.89543,17 6,17 C7.10457,17 8,16.1046 8,15 C8,13.8954 7.10457,13 6,13 Z M14.7723,2.08072 C15.0039,2.23112 15.0697,2.54079 14.9193,2.77238 L11.1924,8.51133 L10.5962,7.59329 L14.0806,2.22774 C14.231,1.99615 14.5407,1.93033 14.7723,2.08072 Z" />
</Button>
<Button Name="CopyButton"
Command="{Binding $parent[TextBox].Copy}"
IsEnabled="{Binding $parent[TextBox].CanCopy}">
<PathIcon Width="14"
Height="14"
Data="M5.50280381,4.62704038 L5.5,6.75 L5.5,17.2542087 C5.5,19.0491342 6.95507456,20.5042087 8.75,20.5042087 L17.3662868,20.5044622 C17.057338,21.3782241 16.2239751,22.0042087 15.2444057,22.0042087 L8.75,22.0042087 C6.12664744,22.0042087 4,19.8775613 4,17.2542087 L4,6.75 C4,5.76928848 4.62744523,4.93512464 5.50280381,4.62704038 Z M17.75,2 C18.9926407,2 20,3.00735931 20,4.25 L20,17.25 C20,18.4926407 18.9926407,19.5 17.75,19.5 L8.75,19.5 C7.50735931,19.5 6.5,18.4926407 6.5,17.25 L6.5,4.25 C6.5,3.00735931 7.50735931,2 8.75,2 L17.75,2 Z M17.75,3.5 L8.75,3.5 C8.33578644,3.5 8,3.83578644 8,4.25 L8,17.25 C8,17.6642136 8.33578644,18 8.75,18 L17.75,18 C18.1642136,18 18.5,17.6642136 18.5,17.25 L18.5,4.25 C18.5,3.83578644 18.1642136,3.5 17.75,3.5 Z" />
</Button>
<Button Name="PasteButton"
Command="{Binding $parent[TextBox].Paste}"
IsEnabled="{Binding $parent[TextBox].CanPaste}">
<PathIcon Width="14"
Height="14"
Data="M13.75,2 C14.940864,2 15.9156449,2.92516159 15.9948092,4.09595119 L16,4.25 L16,4.25 C16,4.16530567 15.9953205,4.0817043 15.9862059,3.99944035 L17.75,4 C18.9926407,4 20,5.00735931 20,6.25 L20,19.75 C20,20.9926407 18.9926407,22 17.75,22 L6.25,22 C5.00735931,22 4,20.9926407 4,19.75 L4,6.25 C4,5.00735931 5.00735931,4 6.25,4 L8.01379413,3.99944035 C8.00733496,4.05773764 8.00310309,4.11670658 8.00118552,4.17626017 L8,4.25 C8,3.00735931 9.00735931,2 10.25,2 L13.75,2 Z M13.75,6.5 L10.25,6.5 C9.45594921,6.5 8.75796956,6.08867052 8.357512,5.4674625 L8.37902077,5.50019943 L8.37902077,5.50019943 L6.25,5.5 C5.83578644,5.5 5.5,5.83578644 5.5,6.25 L5.5,19.75 C5.5,20.1642136 5.83578644,20.5 6.25,20.5 L17.75,20.5 C18.1642136,20.5 18.5,20.1642136 18.5,19.75 L18.5,6.25 C18.5,5.83578644 18.1642136,5.5 17.75,5.5 L15.6209792,5.50019943 L15.642488,5.4674625 C15.2420304,6.08867052 14.5440508,6.5 13.75,6.5 Z M13.75,3.5 L10.25,3.5 C9.83578644,3.5 9.5,3.83578644 9.5,4.25 C9.5,4.66421356 9.83578644,5 10.25,5 L13.75,5 C14.1642136,5 14.5,4.66421356 14.5,4.25 C14.5,3.83578644 14.1642136,3.5 13.75,3.5 Z" />
</Button>
<Button Name="ClearButton" Command="{Binding $parent[TextBox].Clear}">
<PathIcon Width="14"
Height="14"
Data="M3.52499419,3.71761187 L3.61611652,3.61611652 C4.0717282,3.16050485 4.79154862,3.13013074 5.28238813,3.52499419 L5.38388348,3.61611652 L14,12.233 L22.6161165,3.61611652 C23.1042719,3.12796116 23.8957281,3.12796116 24.3838835,3.61611652 C24.8720388,4.10427189 24.8720388,4.89572811 24.3838835,5.38388348 L15.767,14 L24.3838835,22.6161165 C24.8394952,23.0717282 24.8698693,23.7915486 24.4750058,24.2823881 L24.3838835,24.3838835 C23.9282718,24.8394952 23.2084514,24.8698693 22.7176119,24.4750058 L22.6161165,24.3838835 L14,15.767 L5.38388348,24.3838835 C4.89572811,24.8720388 4.10427189,24.8720388 3.61611652,24.3838835 C3.12796116,23.8957281 3.12796116,23.1042719 3.61611652,22.6161165 L12.233,14 L3.61611652,5.38388348 C3.16050485,4.9282718 3.13013074,4.20845138 3.52499419,3.71761187 L3.61611652,3.61611652 L3.52499419,3.71761187 Z" />
</Button>
</StackPanel>
<Border Classes="context-target"
Padding="4, 20">
<Border.ContextFlyout>
<Flyout>
<TextBlock>Hello world</TextBlock>
</Flyout>
</Border.ContextFlyout>
<TextBlock>Inner context flyout</TextBlock>
</Border>
</StackPanel>
</Flyout>
</TextBox.ContextFlyout>
</TextBox>
</StackPanel>
</UserControl>

91
samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs

@ -0,0 +1,91 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
using Avalonia.Interactivity;
using System;
using System.ComponentModel;
namespace ControlCatalog.Pages
{
public class ContextFlyoutPage : UserControl
{
private TextBox _textBox;
public ContextFlyoutPage()
{
InitializeComponent();
DataContext = new ContextPageViewModel();
_textBox = this.FindControl<TextBox>("TextBox");
var cutButton = this.FindControl<Button>("CutButton");
cutButton.Click += CloseFlyout;
var copyButton = this.FindControl<Button>("CopyButton");
copyButton.Click += CloseFlyout;
var pasteButton = this.FindControl<Button>("PasteButton");
pasteButton.Click += CloseFlyout;
var clearButton = this.FindControl<Button>("ClearButton");
clearButton.Click += CloseFlyout;
var customContextRequestedBorder = this.FindControl<Border>("CustomContextRequestedBorder");
customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel);
var cancellableContextBorder = this.FindControl<Border>("CancellableContextBorder");
cancellableContextBorder.ContextFlyout!.Closing += ContextFlyoutPage_Closing;
cancellableContextBorder.ContextFlyout!.Opening += ContextFlyoutPage_Opening;
}
private ContextPageViewModel _model;
protected override void OnDataContextChanged(EventArgs e)
{
if (_model != null)
_model.View = null;
_model = DataContext as ContextPageViewModel;
if (_model != null)
_model.View = this;
base.OnDataContextChanged(e);
}
private void ContextFlyoutPage_Closing(object sender, CancelEventArgs e)
{
var cancelCloseCheckBox = this.FindControl<CheckBox>("CancelCloseCheckBox");
e.Cancel = cancelCloseCheckBox.IsChecked ?? false;
}
private void ContextFlyoutPage_Opening(object sender, EventArgs e)
{
if (e is CancelEventArgs cancelArgs)
{
var cancelCloseCheckBox = this.FindControl<CheckBox>("CancelOpenCheckBox");
cancelArgs.Cancel = cancelCloseCheckBox.IsChecked ?? false;
}
}
private void CloseFlyout(object sender, RoutedEventArgs e)
{
_textBox.ContextFlyout.Hide();
}
public void CustomContextRequested(object sender, ContextRequestedEventArgs e)
{
var border = (Border)sender;
var textBlock = (TextBlock)border.Child;
textBlock.Text = e.TryGetPosition(border, out var point)
? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}"
: "Context was requested without pointer";
e.Handled = true;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

140
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@ -1,58 +1,88 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ContextMenuPage">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Context Menu</TextBlock>
<TextBlock Classes="h2">A right click menu that can be applied to any control.</TextBlock>
<UserControl x:Class="ControlCatalog.Pages.ContextMenuPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Context Menu</TextBlock>
<TextBlock Classes="h2">A right click menu that can be applied to any control.</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<Border Background="{DynamicResource SystemAccentColor}"
Margin="16"
Padding="48,48,48,48">
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
<MenuItem Header="_Disabled Menu Item" IsEnabled="False" InputGesture="Ctrl+D" />
<Separator/>
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1"/>
<MenuItem Header="Submenu _2"/>
</MenuItem>
<MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
<MenuItem.Icon>
<Image Source="/Assets/github_icon.png"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Menu Item with _Checkbox">
<MenuItem.Icon>
<CheckBox BorderThickness="0" IsHitTestVisible="False" IsChecked="True"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Menu Item that won't close on click" StaysOpenOnClick="True" />
</ContextMenu>
</Border.ContextMenu>
<TextBlock Text="Defined in XAML"/>
</Border>
<Border Background="{DynamicResource SystemAccentColor}"
Margin="16"
Padding="48,48,48,48">
<Border.ContextMenu>
<ContextMenu Items="{Binding MenuItems}">
<ContextMenu.Styles>
<Style Selector="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style>
</ContextMenu.Styles>
</ContextMenu>
</Border.ContextMenu>
<TextBlock Text="Dynamically Generated"/>
</Border>
<UniformGrid HorizontalAlignment="Center" Rows="2">
<UniformGrid.Styles>
<Style Selector="UniformGrid > Border">
<Setter Property="Padding" Value="48,20" />
<Setter Property="Margin" Value="8" />
<Setter Property="Focusable" Value="True" />
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" />
</Style>
<Style Selector="UniformGrid > Border > :is(Control)">
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</UniformGrid.Styles>
<Border>
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
<MenuItem Header="_Disabled Menu Item"
InputGesture="Ctrl+D"
IsEnabled="False" />
<Separator />
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1" />
<MenuItem Header="Submenu _2" />
</MenuItem>
<MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
<MenuItem.Icon>
<Image Source="/Assets/github_icon.png" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Menu Item with _Checkbox">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="True"
IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Menu Item that won't close on click" StaysOpenOnClick="True" />
</ContextMenu>
</Border.ContextMenu>
<TextBlock Text="Defined in XAML" />
</Border>
<Border>
<Border.Styles>
<Style Selector="ContextMenu MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style>
</Border.Styles>
<Border.ContextMenu>
<ContextMenu Items="{Binding MenuItems}" />
</Border.ContextMenu>
<TextBlock Text="Dynamically Generated"/>
</Border>
<Border x:Name="CustomContextRequestedBorder">
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="Should never be visible" />
</ContextMenu>
</Border.ContextMenu>
<TextBlock Text="Custom ContextRequested handler" TextWrapping="Wrap" />
</Border>
<Border x:Name="CancellableContextBorder">
<Border.ContextMenu>
<ContextMenu>
<MenuItem>
<MenuItem.Header>
<CheckBox x:Name="CancelCloseCheckBox" Content="Cancel close" />
</MenuItem.Header>
</MenuItem>
</ContextMenu>
</Border.ContextMenu>
<StackPanel>
<TextBlock Text="Cancellable" />
<CheckBox x:Name="CancelOpenCheckBox" Content="Cancel open" />
</StackPanel>
</StackPanel>
</Border>
</UniformGrid>
</StackPanel>
</UserControl>

42
samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs

@ -1,5 +1,8 @@
using System;
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
@ -10,21 +13,54 @@ namespace ControlCatalog.Pages
public ContextMenuPage()
{
this.InitializeComponent();
DataContext = new ContextMenuPageViewModel();
DataContext = new ContextPageViewModel();
var customContextRequestedBorder = this.FindControl<Border>("CustomContextRequestedBorder");
customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel);
var cancellableContextBorder = this.FindControl<Border>("CancellableContextBorder");
cancellableContextBorder.ContextMenu!.ContextMenuClosing += ContextFlyoutPage_Closing;
cancellableContextBorder.ContextMenu!.ContextMenuOpening += ContextFlyoutPage_Opening;
}
private ContextMenuPageViewModel _model;
private ContextPageViewModel _model;
protected override void OnDataContextChanged(EventArgs e)
{
if (_model != null)
_model.View = null;
_model = DataContext as ContextMenuPageViewModel;
_model = DataContext as ContextPageViewModel;
if (_model != null)
_model.View = this;
base.OnDataContextChanged(e);
}
private void ContextFlyoutPage_Closing(object sender, CancelEventArgs e)
{
var cancelCloseCheckBox = this.FindControl<CheckBox>("CancelCloseCheckBox");
e.Cancel = cancelCloseCheckBox.IsChecked ?? false;
}
private void ContextFlyoutPage_Opening(object sender, EventArgs e)
{
if (e is CancelEventArgs cancelArgs)
{
var cancelCloseCheckBox = this.FindControl<CheckBox>("CancelOpenCheckBox");
cancelArgs.Cancel = cancelCloseCheckBox.IsChecked ?? false;
}
}
public void CustomContextRequested(object sender, ContextRequestedEventArgs e)
{
var border = (Border)sender;
var textBlock = (TextBlock)border.Child;
textBlock.Text = e.TryGetPosition(border, out var point)
? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}"
: "Context was requested without pointer";
e.Handled = true;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);

25
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -23,16 +23,21 @@
</StackPanel>
<TabControl Grid.Row="2">
<TabItem Header="DataGrid">
<DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All">
<DataGrid.Columns>
<DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" />
<!-- CompiledBinding example of usage. -->
<DataGridTextColumn Header="Region" Binding="{CompiledBinding Region}" Width="4*" x:DataType="local:Country" />
<DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" />
<DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" />
<DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*" CellStyleClasses="gdp" />
</DataGrid.Columns>
</DataGrid>
<DockPanel>
<CheckBox x:Name="ShowGDP" IsChecked="True" Content="Toggle GDP Column Visibility"
DockPanel.Dock="Top"/>
<DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All">
<DataGrid.Columns>
<DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" />
<!-- CompiledBinding example of usage. -->
<DataGridTextColumn Header="Region" Binding="{CompiledBinding Region}" Width="4*" x:DataType="local:Country" />
<DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" />
<DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" />
<DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*" CellStyleClasses="gdp"
IsVisible="{Binding #ShowGDP.IsChecked}"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
<TabItem Header="Grouping">
<DataGrid Name="dataGridGrouping" Margin="12">

78
samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs

@ -1,78 +0,0 @@
using System.Collections.Generic;
using System.Reactive;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.VisualTree;
using MiniMvvm;
namespace ControlCatalog.ViewModels
{
public class ContextFlyoutPageViewModel
{
public Control View { get; set; }
public ContextFlyoutPageViewModel()
{
OpenCommand = MiniCommand.CreateFromTask(Open);
SaveCommand = MiniCommand.Create(Save);
OpenRecentCommand = MiniCommand.Create<string>(OpenRecent);
MenuItems = new[]
{
new MenuItemViewModel { Header = "_Open...", Command = OpenCommand },
new MenuItemViewModel { Header = "Save", Command = SaveCommand },
new MenuItemViewModel { Header = "-" },
new MenuItemViewModel
{
Header = "Recent",
Items = new[]
{
new MenuItemViewModel
{
Header = "File1.txt",
Command = OpenRecentCommand,
CommandParameter = @"c:\foo\File1.txt"
},
new MenuItemViewModel
{
Header = "File2.txt",
Command = OpenRecentCommand,
CommandParameter = @"c:\foo\File2.txt"
},
}
},
};
}
public IReadOnlyList<MenuItemViewModel> MenuItems { get; set; }
public MiniCommand OpenCommand { get; }
public MiniCommand SaveCommand { get; }
public MiniCommand OpenRecentCommand { get; }
public async Task Open()
{
var window = View?.GetVisualRoot() as Window;
if (window == null)
return;
var dialog = new OpenFileDialog();
var result = await dialog.ShowAsync(window);
if (result != null)
{
foreach (var path in result)
{
System.Diagnostics.Debug.WriteLine($"Opened: {path}");
}
}
}
public void Save()
{
System.Diagnostics.Debug.WriteLine("Save");
}
public void OpenRecent(string path)
{
System.Diagnostics.Debug.WriteLine($"Open recent: {path}");
}
}
}

4
samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs → samples/ControlCatalog/ViewModels/ContextPageViewModel.cs

@ -7,10 +7,10 @@ using MiniMvvm;
namespace ControlCatalog.ViewModels
{
public class ContextMenuPageViewModel
public class ContextPageViewModel
{
public Control View { get; set; }
public ContextMenuPageViewModel()
public ContextPageViewModel()
{
OpenCommand = MiniCommand.CreateFromTask(Open);
SaveCommand = MiniCommand.Create(Save);

42
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -27,7 +27,6 @@ namespace Avalonia.Controls
private double? _minWidth;
private bool _settingWidthInternally;
private int _displayIndexWithFiller;
private bool _isVisible;
private object _header;
private DataGridColumnHeader _headerCell;
private IControl _editingElement;
@ -40,7 +39,6 @@ namespace Avalonia.Controls
/// </summary>
protected internal DataGridColumn()
{
_isVisible = true;
_displayIndexWithFiller = -1;
IsInitialDesiredWidthDetermined = false;
InheritsWidth = true;
@ -174,32 +172,42 @@ namespace Avalonia.Controls
get => _editBinding;
}
/// <summary>
/// Defines the <see cref="IsVisible"/> property.
/// </summary>
public static StyledProperty<bool> IsVisibleProperty =
Control.IsVisibleProperty.AddOwner<DataGridColumn>();
/// <summary>
/// Determines whether or not this column is visible.
/// </summary>
public bool IsVisible
{
get
{
return _isVisible;
}
set
{
if (value != IsVisible)
{
OwningGrid?.OnColumnVisibleStateChanging(this);
_isVisible = value;
get => GetValue(IsVisibleProperty);
set => SetValue(IsVisibleProperty, value);
}
if (_headerCell != null)
{
_headerCell.IsVisible = value;
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
OwningGrid?.OnColumnVisibleStateChanged(this);
if (change.Property == IsVisibleProperty)
{
OwningGrid?.OnColumnVisibleStateChanging(this);
var isVisible = (change as AvaloniaPropertyChangedEventArgs<bool>).NewValue.Value;
if (_headerCell != null)
{
_headerCell.IsVisible = isVisible;
}
OwningGrid?.OnColumnVisibleStateChanged(this);
NotifyPropertyChanged(change.Property.Name);
}
}
/// <summary>
/// Actual visible width after Width, MinWidth, and MaxWidth setting at the Column level and DataGrid level
/// have been taken into account

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

@ -6,8 +6,8 @@
<x:Double x:Key="ListAccentLowOpacity">0.6</x:Double>
<x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double>
<StreamGeometry x:Key="DataGridSortIconAscendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry>
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
<StreamGeometry x:Key="DataGridSortIconAscendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M1939 1581l90 -90l-1005 -1005l-1005 1005l90 90l915 -915z</StreamGeometry>

24
src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs

@ -340,10 +340,30 @@ namespace Avalonia.Controls.Utils
internal static PropertyInfo GetPropertyOrIndexer(this Type type, string propertyPath, out object[] index)
{
index = null;
// Return the default value of GetProperty if the first character is not an indexer token.
if (string.IsNullOrEmpty(propertyPath) || propertyPath[0] != LeftIndexerToken)
{
// Return the default value of GetProperty if the first character is not an indexer token.
return type.GetProperty(propertyPath);
var property = type.GetProperty(propertyPath);
if (property != null)
{
return property;
}
// GetProperty does not return inherited interface properties,
// so we need to enumerate them manually.
if (type.IsInterface)
{
foreach (var typeInterface in type.GetInterfaces())
{
property = type.GetProperty(propertyPath);
if (property != null)
{
return property;
}
}
}
return null;
}
if (propertyPath.Length < 2 || propertyPath[propertyPath.Length - 1] != RightIndexerToken)

1
src/Avalonia.Controls/ComboBox.cs

@ -91,6 +91,7 @@ namespace Avalonia.Controls
FocusableProperty.OverrideDefaultValue<ComboBox>(true);
SelectedItemProperty.Changed.AddClassHandler<ComboBox>((x,e) => x.SelectedItemChanged(e));
KeyDownEvent.AddClassHandler<ComboBox>((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel);
IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(true);
}
/// <summary>

65
src/Avalonia.Controls/ContextMenu.cs

@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Styling;
@ -220,7 +223,8 @@ namespace Avalonia.Controls
if (e.OldValue is ContextMenu oldMenu)
{
control.PointerReleased -= ControlPointerReleased;
control.ContextRequested -= ControlContextRequested;
control.DetachedFromVisualTree -= ControlDetachedFromVisualTree;
oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent?)oldMenu._popup)?.SetParent(null);
}
@ -229,7 +233,8 @@ namespace Avalonia.Controls
{
newMenu._attachedControls ??= new List<Control>();
newMenu._attachedControls.Add(control);
control.PointerReleased += ControlPointerReleased;
control.ContextRequested += ControlContextRequested;
control.DetachedFromVisualTree += ControlDetachedFromVisualTree;
}
}
@ -269,7 +274,7 @@ namespace Avalonia.Controls
}
control ??= _attachedControls![0];
Open(control, PlacementTarget ?? control);
Open(control, PlacementTarget ?? control, false);
}
/// <summary>
@ -304,7 +309,7 @@ namespace Avalonia.Controls
return new MenuItemContainerGenerator(this);
}
private void Open(Control control, Control placementTarget)
private void Open(Control control, Control placementTarget, bool requestedByPointer)
{
if (IsOpen)
{
@ -329,6 +334,8 @@ namespace Avalonia.Controls
_popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
_popup.Closing += PopupClosing;
_popup.KeyUp += PopupKeyUp;
}
if (_popup.Parent != control)
@ -337,6 +344,10 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.PlacementMode = !requestedByPointer && PlacementMode == PlacementMode.Pointer
? PlacementMode.Bottom
: PlacementMode;
_popup.PlacementTarget = placementTarget;
_popup.Child = this;
IsOpen = true;
@ -355,6 +366,11 @@ namespace Avalonia.Controls
Focus();
}
private void PopupClosing(object sender, CancelEventArgs e)
{
e.Cancel = CancelClosing();
}
private void PopupClosed(object sender, EventArgs e)
{
foreach (var i in LogicalChildren)
@ -383,30 +399,43 @@ namespace Avalonia.Controls
});
}
private static void ControlPointerReleased(object sender, PointerReleasedEventArgs e)
private void PopupKeyUp(object sender, KeyEventArgs e)
{
var control = (Control)sender;
var contextMenu = control.ContextMenu;
if (control.ContextMenu.IsOpen)
if (IsOpen)
{
if (contextMenu.CancelClosing())
return;
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
control.ContextMenu.Close();
e.Handled = true;
if (keymap.OpenContextMenu.Any(k => k.Matches(e))
&& !CancelClosing())
{
Close();
e.Handled = true;
}
}
}
if (e.InitialPressMouseButton == MouseButton.Right)
private static void ControlContextRequested(object sender, ContextRequestedEventArgs e)
{
if (sender is Control control
&& control.ContextMenu is ContextMenu contextMenu
&& !e.Handled
&& !contextMenu.CancelOpening())
{
if (contextMenu.CancelOpening())
return;
contextMenu.Open(control, e.Source as Control ?? control);
var requestedByPointer = e.TryGetPosition(null, out _);
contextMenu.Open(control, e.Source as Control ?? control, requestedByPointer);
e.Handled = true;
}
}
private static void ControlDetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
if (sender is Control control
&& control.ContextMenu is ContextMenu contextMenu)
{
contextMenu.Close();
}
}
private bool CancelClosing()
{
var eventArgs = new CancelEventArgs();

58
src/Avalonia.Controls/ContextRequestedEventArgs.cs

@ -0,0 +1,58 @@
using Avalonia.Input;
using Avalonia.Interactivity;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Provides event data for the ContextRequested event.
/// </summary>
public class ContextRequestedEventArgs : RoutedEventArgs
{
private readonly PointerEventArgs? _pointerEventArgs;
/// <summary>
/// Initializes a new instance of the ContextRequestedEventArgs class.
/// </summary>
public ContextRequestedEventArgs()
: base(Control.ContextRequestedEvent)
{
}
/// <inheritdoc cref="ContextRequestedEventArgs()" />
public ContextRequestedEventArgs(PointerEventArgs pointerEventArgs)
: this()
{
_pointerEventArgs = pointerEventArgs;
}
/// <summary>
/// Gets the x- and y-coordinates of the pointer position, optionally evaluated against a coordinate origin of a supplied <see cref="Control"/>.
/// </summary>
/// <param name="relativeTo">
/// Any <see cref="Control"/>-derived object that is connected to the same object tree.
/// To specify the object relative to the overall coordinate system, use a relativeTo value of null.
/// </param>
/// <param name="point">
/// A <see cref="Point"/> that represents the current x- and y-coordinates of the mouse pointer position.
/// If null was passed as relativeTo, this coordinate is for the overall window.
/// If a relativeTo value other than null was passed, this coordinate is relative to the object referenced by relativeTo.
/// </param>
/// <returns>
/// true if the context request was initiated by a pointer device; otherwise, false.
/// </returns>
public bool TryGetPosition(Control? relativeTo, out Point point)
{
if (_pointerEventArgs is null)
{
point = default;
return false;
}
point = _pointerEventArgs.GetPosition(relativeTo);
return true;
}
}
}

62
src/Avalonia.Controls/Control.cs

@ -3,6 +3,7 @@ using System.ComponentModel;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Rendering;
using Avalonia.Styling;
@ -19,6 +20,7 @@ namespace Avalonia.Controls
/// The control class extends <see cref="InputElement"/> and adds the following features:
///
/// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control.
/// - <see cref="ContextRequestedEvent"/> and other context menu related members.
/// </remarks>
public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, ISetterValue
{
@ -52,6 +54,13 @@ namespace Avalonia.Controls
public static readonly RoutedEvent<RequestBringIntoViewEventArgs> RequestBringIntoViewEvent =
RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>("RequestBringIntoView", RoutingStrategies.Bubble);
/// <summary>
/// Provides event data for the <see cref="ContextRequested"/> event.
/// </summary>
public static readonly RoutedEvent<ContextRequestedEventArgs> ContextRequestedEvent =
RoutedEvent.Register<Control, ContextRequestedEventArgs>(nameof(ContextRequested),
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
private DataTemplates? _dataTemplates;
private IControl? _focusAdorner;
@ -100,6 +109,15 @@ namespace Avalonia.Controls
set => SetValue(TagProperty, value);
}
/// <summary>
/// Occurs when the user has completed a context input gesture, such as a right-click.
/// </summary>
public event EventHandler<ContextRequestedEventArgs> ContextRequested
{
add => AddHandler(ContextRequestedEvent, value);
remove => RemoveHandler(ContextRequestedEvent, value);
}
public new IControl? Parent => (IControl?)base.Parent;
/// <inheritdoc/>
@ -208,5 +226,49 @@ namespace Avalonia.Controls
_focusAdorner = null;
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (e.Source == this
&& !e.Handled
&& e.InitialPressMouseButton == MouseButton.Right)
{
var args = new ContextRequestedEventArgs(e);
RaiseEvent(args);
e.Handled = args.Handled;
}
}
protected override void OnKeyUp(KeyEventArgs e)
{
base.OnKeyUp(e);
if (e.Source == this
&& !e.Handled)
{
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>().OpenContextMenu;
var matches = false;
for (var index = 0; index < keymap.Count; index++)
{
var key = keymap[index];
matches |= key.Matches(e);
if (matches)
{
break;
}
}
if (matches)
{
var args = new ContextRequestedEventArgs();
RaiseEvent(args);
e.Handled = args.Handled;
}
}
}
}
}

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

@ -1,6 +1,9 @@
using System;
using System.ComponentModel;
using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Logging;
@ -49,6 +52,7 @@ namespace Avalonia.Controls.Primitives
public static readonly AttachedProperty<FlyoutBase?> AttachedFlyoutProperty =
AvaloniaProperty.RegisterAttached<FlyoutBase, Control, FlyoutBase?>("AttachedFlyout", null);
private readonly Lazy<Popup> _popupLazy;
private bool _isOpen;
private Control? _target;
private FlyoutShowMode _showMode = FlyoutShowMode.Standard;
@ -56,7 +60,12 @@ namespace Avalonia.Controls.Primitives
private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable;
protected Popup? Popup { get; private set; }
public FlyoutBase()
{
_popupLazy = new Lazy<Popup>(() => CreatePopup());
}
protected Popup Popup => _popupLazy.Value;
/// <summary>
/// Gets whether this Flyout is currently Open
@ -142,22 +151,19 @@ namespace Avalonia.Controls.Primitives
HideCore();
}
protected virtual void HideCore(bool canCancel = true)
/// <returns>True, if action was handled</returns>
protected virtual bool HideCore(bool canCancel = true)
{
if (!IsOpen)
{
return;
return false;
}
if (canCancel)
{
bool cancel = false;
var closing = new CancelEventArgs();
Closing?.Invoke(this, closing);
if (cancel || closing.Cancel)
if (CancelClosing())
{
return;
return false;
}
}
@ -170,31 +176,42 @@ namespace Avalonia.Controls.Primitives
_enlargedPopupRect = null;
_enlargePopupRectScreenPixelRect = null;
if (Target != null)
{
Target.DetachedFromVisualTree -= PlacementTarget_DetachedFromVisualTree;
Target.KeyUp -= OnPlacementTargetOrPopupKeyUp;
}
OnClosed();
return true;
}
protected virtual void ShowAtCore(Control placementTarget, bool showAtPointer = false)
/// <returns>True, if action was handled</returns>
protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = false)
{
if (placementTarget == null)
throw new ArgumentNullException("placementTarget cannot be null");
if (Popup == null)
{
InitPopup();
throw new ArgumentNullException(nameof(placementTarget));
}
if (IsOpen)
{
if (placementTarget == Target)
{
return;
return false;
}
else // Close before opening a new one
{
HideCore(false);
_ = HideCore(false);
}
}
if (CancelOpening())
{
return false;
}
if (Popup.Parent != null && Popup.Parent != placementTarget)
{
((ISetLogicalParent)Popup).SetParent(null);
@ -211,11 +228,13 @@ namespace Avalonia.Controls.Primitives
Popup.Child = CreatePresenter();
}
OnOpening();
PositionPopup(showAtPointer);
IsOpen = Popup.IsOpen = true;
IsOpen = Popup.IsOpen = true;
OnOpened();
placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree;
placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp;
if (ShowMode == FlyoutShowMode.Standard)
{
// Try and focus content inside Flyout
@ -236,6 +255,13 @@ namespace Avalonia.Controls.Primitives
{
_transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss);
}
return true;
}
private void PlacementTarget_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
_ = HideCore(false);
}
private void HandleTransientDismiss(RawInputEventArgs args)
@ -254,7 +280,7 @@ namespace Avalonia.Controls.Primitives
{
// Only do this once when the Flyout opens & cache the result
if (Popup?.Host is PopupRoot root)
{
{
// Get the popup root bounds and convert to screen coordinates
var tmp = root.Bounds.Inflate(100);
@ -294,9 +320,9 @@ namespace Avalonia.Controls.Primitives
}
}
protected virtual void OnOpening()
protected virtual void OnOpening(CancelEventArgs args)
{
Opening?.Invoke(this, null);
Opening?.Invoke(this, args);
}
protected virtual void OnOpened()
@ -320,14 +346,18 @@ namespace Avalonia.Controls.Primitives
/// <returns></returns>
protected abstract Control CreatePresenter();
private void InitPopup()
private Popup CreatePopup()
{
Popup = new Popup();
Popup.WindowManagerAddShadowHint = false;
Popup.IsLightDismissEnabled = true;
Popup.Opened += OnPopupOpened;
Popup.Closed += OnPopupClosed;
var popup = new Popup();
popup.WindowManagerAddShadowHint = false;
popup.IsLightDismissEnabled = true;
popup.OverlayDismissEventPassThrough = true;
popup.Opened += OnPopupOpened;
popup.Closed += OnPopupClosed;
popup.Closing += OnPopupClosing;
popup.KeyUp += OnPlacementTargetOrPopupKeyUp;
return popup;
}
private void OnPopupOpened(object sender, EventArgs e)
@ -335,15 +365,40 @@ namespace Avalonia.Controls.Primitives
IsOpen = true;
}
private void OnPopupClosing(object sender, CancelEventArgs e)
{
if (IsOpen)
{
e.Cancel = CancelClosing();
}
}
private void OnPopupClosed(object sender, EventArgs e)
{
HideCore();
HideCore(false);
}
// This method is handling both popup logical tree and target logical tree.
private void OnPlacementTargetOrPopupKeyUp(object sender, KeyEventArgs e)
{
if (!e.Handled
&& IsOpen
&& Target?.ContextFlyout == this)
{
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
if (keymap.OpenContextMenu.Any(k => k.Matches(e)))
{
e.Handled = HideCore();
}
}
}
private void PositionPopup(bool showAtPointer)
{
Size sz;
if(Popup.Child.DesiredSize == Size.Empty)
// Popup.Child can't be null here, it was set in ShowAtCore.
if (Popup.Child!.DesiredSize == Size.Empty)
{
// Popup may not have been shown yet. Measure content
sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
@ -370,19 +425,19 @@ namespace Avalonia.Controls.Primitives
switch (Placement)
{
case FlyoutPlacementMode.Top: //Above & centered
Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width-1, 1);
Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width - 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Top;
break;
case FlyoutPlacementMode.TopEdgeAlignedLeft:
Popup.PlacementRect = new Rect(0, 0, 0, 0);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
break;
case FlyoutPlacementMode.TopEdgeAlignedRight:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 10, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
break;
case FlyoutPlacementMode.RightEdgeAlignedTop:
@ -454,33 +509,45 @@ namespace Avalonia.Controls.Primitives
{
if (args.OldValue is FlyoutBase)
{
c.PointerReleased -= OnControlWithContextFlyoutPointerReleased;
c.ContextRequested -= OnControlContextRequested;
}
if (args.NewValue is FlyoutBase)
{
c.PointerReleased += OnControlWithContextFlyoutPointerReleased;
c.ContextRequested += OnControlContextRequested;
}
}
}
private static void OnControlWithContextFlyoutPointerReleased(object sender, PointerReleasedEventArgs e)
private static void OnControlContextRequested(object sender, ContextRequestedEventArgs e)
{
if (sender is Control c)
var control = (Control)sender;
if (!e.Handled
&& control.ContextFlyout is FlyoutBase flyout)
{
if (e.InitialPressMouseButton == MouseButton.Right &&
e.GetCurrentPoint(c).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonReleased)
if (control.ContextMenu != null)
{
if (c.ContextFlyout != null)
{
if (c.ContextMenu != null)
{
Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(c, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
return;
}
c.ContextFlyout.ShowAt(c, true);
}
Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
return;
}
}
// We do not support absolute popup positioning yet, so we ignore "point" at this moment.
var triggeredByPointerInput = e.TryGetPosition(null, out _);
e.Handled = flyout.ShowAtCore(control, triggeredByPointerInput);
}
}
private bool CancelClosing()
{
var eventArgs = new CancelEventArgs();
OnClosing(eventArgs);
return eventArgs.Cancel;
}
private bool CancelOpening()
{
var eventArgs = new CancelEventArgs();
OnOpening(eventArgs);
return eventArgs.Cancel;
}
internal static void SetPresenterClasses(IControl presenter, Classes classes)

2
src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs

@ -12,7 +12,7 @@
Standard,
/// <summary>
/// Behavior is typical of a flyout shown proactively. The open flyout does not take focus. For a CommandBarFlyout, it opens in it's collapsed state.
/// Behavior is typical of a flyout shown proactively. The open flyout does not take focus.
/// </summary>
Transient,

10
src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs

@ -29,16 +29,8 @@ namespace Avalonia.Controls
var host = this.FindLogicalAncestorOfType<Popup>();
if (host != null)
{
for (int i = 0; i < LogicalChildren.Count; i++)
{
if (LogicalChildren[i] is MenuItem item)
{
item.IsSubMenuOpen = false;
}
}
SelectedIndex = -1;
host.IsOpen = false;
host.IsOpen = false;
}
}

67
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -1,19 +1,33 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Resources;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.Utilities;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls.Primitives
{
// TODO: Need to track position of adorned elements and move the adorner if they move.
/// <summary>
/// Represents a surface for showing adorners.
/// Adorners are always on top of the adorned element and are positioned to stay relative to the adorned element.
/// </summary>
/// <remarks>
/// TODO: Need to track position of adorned elements and move the adorner if they move.
/// </remarks>
public class AdornerLayer : Canvas, ICustomSimpleHitTest
{
public static readonly AttachedProperty<Visual> AdornedElementProperty =
AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, Visual>("AdornedElement");
/// <summary>
/// Allows for getting and setting of the adorned element.
/// </summary>
public static readonly AttachedProperty<Visual?> AdornedElementProperty =
AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, Visual?>("AdornedElement");
/// <summary>
/// Allows for controlling clipping of the adorner.
/// </summary>
public static readonly AttachedProperty<bool> IsClipEnabledProperty =
AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, bool>("IsClipEnabled", true);
private static readonly AttachedProperty<AdornedElementInfo> s_adornedElementInfoProperty =
AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, AdornedElementInfo>("AdornedElementInfo");
@ -28,7 +42,7 @@ namespace Avalonia.Controls.Primitives
Children.CollectionChanged += ChildrenCollectionChanged;
}
public static Visual GetAdornedElement(Visual adorner)
public static Visual? GetAdornedElement(Visual adorner)
{
return adorner.GetValue(AdornedElementProperty);
}
@ -38,12 +52,19 @@ namespace Avalonia.Controls.Primitives
adorner.SetValue(AdornedElementProperty, adorned);
}
public static AdornerLayer GetAdornerLayer(IVisual visual)
public static AdornerLayer? GetAdornerLayer(IVisual visual)
{
return visual.FindAncestorOfType<VisualLayerManager>()?.AdornerLayer;
}
public static bool GetIsClipEnabled(Visual adorner)
{
return visual.GetVisualAncestors()
.OfType<VisualLayerManager>()
.FirstOrDefault()
?.AdornerLayer;
return adorner.GetValue(IsClipEnabledProperty);
}
public static void SetIsClipEnabled(Visual adorner, bool isClipEnabled)
{
adorner.SetValue(IsClipEnabledProperty, isClipEnabled);
}
protected override Size MeasureOverride(Size availableSize)
@ -70,12 +91,13 @@ namespace Avalonia.Controls.Primitives
foreach (var child in Children)
{
var info = child.GetValue(s_adornedElementInfoProperty);
var isClipEnabled = child.GetValue(IsClipEnabledProperty);
if (info != null && info.Bounds.HasValue)
{
child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform);
child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute);
UpdateClip(child, info.Bounds.Value);
UpdateClip(child, info.Bounds.Value, isClipEnabled);
child.Arrange(info.Bounds.Value.Bounds);
}
else
@ -87,16 +109,23 @@ namespace Avalonia.Controls.Primitives
return finalSize;
}
private static void AdornedElementChanged(AvaloniaPropertyChangedEventArgs e)
private static void AdornedElementChanged(AvaloniaPropertyChangedEventArgs<Visual?> e)
{
var adorner = (Visual)e.Sender;
var adorned = (Visual)e.NewValue;
var adorned = e.NewValue.GetValueOrDefault();
var layer = adorner.GetVisualParent<AdornerLayer>();
layer?.UpdateAdornedElement(adorner, adorned);
}
private void UpdateClip(IControl control, TransformedBounds bounds)
private void UpdateClip(IControl control, TransformedBounds bounds, bool isEnabled)
{
if (!isEnabled)
{
control.Clip = null;
return;
}
if (!(control.Clip is RectangleGeometry clip))
{
clip = new RectangleGeometry();
@ -129,13 +158,13 @@ namespace Avalonia.Controls.Primitives
InvalidateArrange();
}
private void UpdateAdornedElement(Visual adorner, Visual adorned)
private void UpdateAdornedElement(Visual adorner, Visual? adorned)
{
var info = adorner.GetValue(s_adornedElementInfoProperty);
if (info != null)
{
info.Subscription.Dispose();
info.Subscription!.Dispose();
if (adorned == null)
{
@ -163,7 +192,7 @@ namespace Avalonia.Controls.Primitives
private class AdornedElementInfo
{
public IDisposable Subscription { get; set; }
public IDisposable? Subscription { get; set; }
public TransformedBounds? Bounds { get; set; }
}

10
src/Avalonia.Controls/Primitives/Popup.cs

@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Presenters;
@ -154,6 +155,8 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public event EventHandler? Opened;
internal event EventHandler<CancelEventArgs>? Closing;
public IPopupHost? Host => _openState?.PopupHost;
public bool WindowManagerAddShadowHint
@ -567,6 +570,13 @@ namespace Avalonia.Controls.Primitives
private void CloseCore()
{
var closingArgs = new CancelEventArgs();
Closing?.Invoke(this, closingArgs);
if (closingArgs.Cancel)
{
return;
}
_isOpenRequested = false;
if (_openState is null)
{

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

@ -96,7 +96,7 @@ namespace Avalonia.Controls.Primitives
/// Defines the <see cref="IsTextSearchEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsTextSearchEnabledProperty =
AvaloniaProperty.Register<ItemsControl, bool>(nameof(IsTextSearchEnabled), true);
AvaloniaProperty.Register<ItemsControl, bool>(nameof(IsTextSearchEnabled), false);
/// <summary>
/// Event that should be raised by items that implement <see cref="ISelectable"/> to

20
src/Avalonia.Controls/Primitives/Thumb.cs

@ -56,6 +56,26 @@ namespace Avalonia.Controls.Primitives
{
}
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
if (_lastPoint.HasValue)
{
var ev = new VectorEventArgs
{
RoutedEvent = DragCompletedEvent,
Vector = _lastPoint.Value,
};
_lastPoint = null;
RaiseEvent(ev);
}
PseudoClasses.Remove(":pressed");
base.OnPointerCaptureLost(e);
}
protected override void OnPointerMoved(PointerEventArgs e)
{
if (_lastPoint.HasValue)

21
src/Avalonia.Controls/TextBox.cs

@ -1098,6 +1098,11 @@ namespace Avalonia.Controls
private bool MoveVertical(int count)
{
if (_presenter is null)
{
return false;
}
var formattedText = _presenter.FormattedText;
var lines = formattedText.GetLines().ToList();
var caretIndex = CaretIndex;
@ -1113,14 +1118,17 @@ namespace Avalonia.Controls
CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0);
return true;
}
else
{
return false;
}
return false;
}
private void MoveHome(bool document)
{
if (_presenter is null)
{
return;
}
var text = Text ?? string.Empty;
var caretIndex = CaretIndex;
@ -1151,6 +1159,11 @@ namespace Avalonia.Controls
private void MoveEnd(bool document)
{
if (_presenter is null)
{
return;
}
var text = Text ?? string.Empty;
var caretIndex = CaretIndex;

15
src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs

@ -1,14 +1,16 @@
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Input.Platform
{
public class PlatformHotkeyConfiguration
{
public PlatformHotkeyConfiguration() : this(KeyModifiers.Control)
{
}
public PlatformHotkeyConfiguration(KeyModifiers commandModifiers,
KeyModifiers selectionModifiers = KeyModifiers.Shift,
KeyModifiers wholeWordTextActionModifiers = KeyModifiers.Control)
@ -73,8 +75,12 @@ namespace Avalonia.Input.Platform
{
new KeyGesture(Key.End, commandModifiers | selectionModifiers)
};
OpenContextMenu = new List<KeyGesture>
{
new KeyGesture(Key.Apps)
};
}
public KeyModifiers CommandModifiers { get; set; }
public KeyModifiers WholeWordTextActionModifiers { get; set; }
public KeyModifiers SelectionModifiers { get; set; }
@ -92,7 +98,6 @@ namespace Avalonia.Input.Platform
public List<KeyGesture> MoveCursorToTheEndOfLineWithSelection { get; set; }
public List<KeyGesture> MoveCursorToTheStartOfDocumentWithSelection { get; set; }
public List<KeyGesture> MoveCursorToTheEndOfDocumentWithSelection { get; set; }
public List<KeyGesture> OpenContextMenu { get; set; }
}
}

2
src/Avalonia.Native/WindowImpl.cs

@ -114,7 +114,7 @@ namespace Avalonia.Native
{
var visual = (_inputRoot as Window).Renderer.HitTestFirst(e.Position, _inputRoot as Window, x =>
{
if (x is IInputElement ie && !ie.IsHitTestVisible)
if (x is IInputElement ie && (!ie.IsHitTestVisible || !ie.IsVisible))
{
return false;
}

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

@ -4,7 +4,7 @@
<StreamGeometry x:Key="PasswordBoxRevealButtonData">m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z</StreamGeometry>
<StreamGeometry x:Key="PasswordBoxHideButtonData">m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z</StreamGeometry>
<MenuFlyout x:Key="DefaultTextBoxContextFlyout">
<MenuFlyout x:Key="DefaultTextBoxContextFlyout" Placement="Bottom">
<MenuItem x:Name="TextBoxContextFlyoutCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
<MenuItem x:Name="TextBoxContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
<MenuItem x:Name="TextBoxContextFlyoutPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}"/>

2
src/Avalonia.Themes.Fluent/Controls/TextBox.xaml

@ -15,7 +15,7 @@
<StreamGeometry x:Key="PasswordBoxRevealButtonData">m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z</StreamGeometry>
<StreamGeometry x:Key="PasswordBoxHideButtonData">m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z</StreamGeometry>
<MenuFlyout x:Key="DefaultTextBoxContextFlyout">
<MenuFlyout x:Key="DefaultTextBoxContextFlyout" Placement="Bottom">
<MenuItem x:Name="TextBoxContextFlyoutCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
<MenuItem x:Name="TextBoxContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
<MenuItem x:Name="TextBoxContextFlyoutPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}"/>

2
src/Avalonia.Visuals/Media/GlyphRun.cs

@ -582,7 +582,7 @@ namespace Avalonia.Media
{
var cluster = _glyphClusters[i];
var codepointIndex = cluster - _characters.Start;
var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster;
var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);

87
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs

@ -90,7 +90,9 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The split result.</returns>
public SplitTextCharactersResult Split(int length)
{
var glyphCount = GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length);
var glyphCount = GlyphRun.IsLeftToRight ?
GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length) :
GlyphRun.FindGlyphIndex(GlyphRun.Characters.End - length);
if (GlyphRun.Characters.Length == length)
{
@ -102,31 +104,64 @@ namespace Avalonia.Media.TextFormatting
return new SplitTextCharactersResult(this, null);
}
var firstGlyphRun = new GlyphRun(
Properties.Typeface.GlyphTypeface,
Properties.FontRenderingEmSize,
GlyphRun.GlyphIndices.Take(glyphCount),
GlyphRun.GlyphAdvances.Take(glyphCount),
GlyphRun.GlyphOffsets.Take(glyphCount),
GlyphRun.Characters.Take(length),
GlyphRun.GlyphClusters.Take(glyphCount),
GlyphRun.BiDiLevel);
var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties);
var secondGlyphRun = new GlyphRun(
Properties.Typeface.GlyphTypeface,
Properties.FontRenderingEmSize,
GlyphRun.GlyphIndices.Skip(glyphCount),
GlyphRun.GlyphAdvances.Skip(glyphCount),
GlyphRun.GlyphOffsets.Skip(glyphCount),
GlyphRun.Characters.Skip(length),
GlyphRun.GlyphClusters.Skip(glyphCount),
GlyphRun.BiDiLevel);
var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties);
return new SplitTextCharactersResult(firstTextRun, secondTextRun);
if (GlyphRun.IsLeftToRight)
{
var firstGlyphRun = new GlyphRun(
Properties.Typeface.GlyphTypeface,
Properties.FontRenderingEmSize,
GlyphRun.GlyphIndices.Take(glyphCount),
GlyphRun.GlyphAdvances.Take(glyphCount),
GlyphRun.GlyphOffsets.Take(glyphCount),
GlyphRun.Characters.Take(length),
GlyphRun.GlyphClusters.Take(glyphCount),
GlyphRun.BiDiLevel);
var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties);
var secondGlyphRun = new GlyphRun(
Properties.Typeface.GlyphTypeface,
Properties.FontRenderingEmSize,
GlyphRun.GlyphIndices.Skip(glyphCount),
GlyphRun.GlyphAdvances.Skip(glyphCount),
GlyphRun.GlyphOffsets.Skip(glyphCount),
GlyphRun.Characters.Skip(length),
GlyphRun.GlyphClusters.Skip(glyphCount),
GlyphRun.BiDiLevel);
var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties);
return new SplitTextCharactersResult(firstTextRun, secondTextRun);
}
else
{
var take = GlyphRun.GlyphIndices.Length - glyphCount;
var firstGlyphRun = new GlyphRun(
Properties.Typeface.GlyphTypeface,
Properties.FontRenderingEmSize,
GlyphRun.GlyphIndices.Take(take),
GlyphRun.GlyphAdvances.Take(take),
GlyphRun.GlyphOffsets.Take(take),
GlyphRun.Characters.Skip(length),
GlyphRun.GlyphClusters.Take(take),
GlyphRun.BiDiLevel);
var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties);
var secondGlyphRun = new GlyphRun(
Properties.Typeface.GlyphTypeface,
Properties.FontRenderingEmSize,
GlyphRun.GlyphIndices.Skip(take),
GlyphRun.GlyphAdvances.Skip(take),
GlyphRun.GlyphOffsets.Skip(take),
GlyphRun.Characters.Take(length),
GlyphRun.GlyphClusters.Skip(take),
GlyphRun.BiDiLevel);
var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties);
return new SplitTextCharactersResult(secondTextRun,firstTextRun);
}
}
public readonly struct SplitTextCharactersResult

9
src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs

@ -134,7 +134,7 @@ namespace Avalonia.Media.TextFormatting
var isFallback = typeface != defaultTypeface;
count = 0;
var script = Script.Common;
var script = Script.Unknown;
var direction = BiDiClass.LeftToRight;
var font = typeface.GlyphTypeface;
@ -161,7 +161,7 @@ namespace Avalonia.Media.TextFormatting
if (currentScript != script)
{
if (script == Script.Inherited || script == Script.Common)
if (script is Script.Unknown)
{
script = currentScript;
}
@ -174,13 +174,16 @@ namespace Avalonia.Media.TextFormatting
}
}
if (currentScript != Script.Common && currentScript != Script.Inherited)
//Only handle non whitespace here
if (!currentGrapheme.FirstCodepoint.IsWhiteSpace)
{
//Stop at the first glyph that is present in the default typeface.
if (isFallback && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
break;
}
//Stop at the first missing glyph
if (!font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
break;

86
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@ -70,34 +70,74 @@ namespace Avalonia.Media.TextFormatting
{
var glyphTypeface = glyphRun.GlyphTypeface;
for (var i = 0; i < glyphRun.GlyphClusters.Length; i++)
if (glyphRun.IsLeftToRight)
{
var glyph = glyphRun.GlyphIndices[i];
foreach (var glyph in glyphRun.GlyphIndices)
{
var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
if (currentWidth + advance > availableWidth)
{
break;
}
if (currentWidth + advance > availableWidth)
{
break;
currentWidth += advance;
glyphCount++;
}
}
else
{
for (var index = glyphRun.GlyphClusters.Length - 1; index > 0; index--)
{
var glyph = glyphRun.GlyphIndices[index];
var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
currentWidth += advance;
if (currentWidth + advance > availableWidth)
{
break;
}
glyphCount++;
currentWidth += advance;
glyphCount++;
}
}
}
else
{
foreach (var advance in glyphRun.GlyphAdvances)
if (glyphRun.IsLeftToRight)
{
if (currentWidth + advance > availableWidth)
for (var index = 0; index < glyphRun.GlyphAdvances.Length; index++)
{
break;
var advance = glyphRun.GlyphAdvances[index];
if (currentWidth + advance > availableWidth)
{
break;
}
currentWidth += advance;
glyphCount++;
}
}
else
{
for (var index = glyphRun.GlyphAdvances.Length - 1; index > 0; index--)
{
var advance = glyphRun.GlyphAdvances[index];
if (currentWidth + advance > availableWidth)
{
break;
}
currentWidth += advance;
currentWidth += advance;
glyphCount++;
glyphCount++;
}
}
}
@ -475,24 +515,14 @@ namespace Avalonia.Media.TextFormatting
var remainingCharacters = splitResult.Second;
if (currentLineBreak?.RemainingCharacters != null)
var lineBreak = remainingCharacters?.Count > 0 ? new TextLineBreak(remainingCharacters) : null;
if (lineBreak is null && currentLineBreak.TextEndOfLine != null)
{
if (remainingCharacters != null)
{
remainingCharacters.AddRange(currentLineBreak.RemainingCharacters);
}
else
{
remainingCharacters = new List<ShapedTextCharacters>(currentLineBreak.RemainingCharacters);
}
lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine);
}
var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ?
new TextLineBreak(remainingCharacters) :
null;
return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties,
lineBreak);
return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, lineBreak);
}
/// <summary>

19
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@ -258,14 +258,25 @@ namespace Avalonia.Rendering.SceneGraph
}
else if (visualChildren.Count > 1)
{
var sortedChildren = new IVisual[visualChildren.Count];
visualChildren.CopyTo(sortedChildren, 0);
var count = visualChildren.Count;
var sortedChildren = new (IVisual visual, int index)[count];
Array.Sort(sortedChildren, ZIndexComparer.ComparisonInstance);
for (var i = 0; i < count; i++)
{
sortedChildren[i] = (visualChildren[i], i);
}
// Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements.
Array.Sort(sortedChildren, (lhs, rhs) =>
{
var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual);
return result == 0 ? lhs.index.CompareTo(rhs.index) : result;
});
foreach (var child in sortedChildren)
{
var childNode = GetOrCreateChildNode(scene, child, node);
var childNode = GetOrCreateChildNode(scene, child.Item1, node);
Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
}
}

28
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs

@ -9,7 +9,6 @@ using XamlX.Ast;
using XamlX.Transform;
using XamlX.Transform.Transformers;
using XamlX.TypeSystem;
using XamlParseException = XamlX.XamlParseException;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
@ -21,6 +20,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
if (node is XamlAstObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension))
{
var convertedNode = ConvertLongFormPropertiesToBindingExpressionNode(context, binding);
var foundPath = false;
if (binding.Arguments.Count > 0 && binding.Arguments[0] is XamlAstTextNode bindingPathText)
{
@ -32,9 +32,18 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode);
}
binding.Arguments[0] = new ParsedBindingPathNode(bindingPathText, context.GetAvaloniaTypes().CompiledBindingPath, nodes);
if (nodes.Count == 1 && nodes[0] is BindingExpressionGrammar.EmptyExpressionNode)
{
binding.Arguments.RemoveAt(0);
}
else
{
binding.Arguments[0] = new ParsedBindingPathNode(bindingPathText, context.GetAvaloniaTypes().CompiledBindingPath, nodes);
foundPath = true;
}
}
else
if (!foundPath)
{
var bindingPathAssignment = binding.Children.OfType<XamlAstXamlPropertyValueNode>()
.FirstOrDefault(v => v.Property.GetClrProperty().Name == "Path");
@ -44,12 +53,19 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
var reader = new CharacterReader(pathValue.Text.AsSpan());
var (nodes, _) = BindingExpressionGrammar.Parse(ref reader);
if (convertedNode != null)
if (nodes.Count == 1 && nodes[0] is BindingExpressionGrammar.EmptyExpressionNode)
{
nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode);
bindingPathAssignment.Values.RemoveAt(0);
}
else
{
if (convertedNode != null)
{
nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode);
}
bindingPathAssignment.Values[0] = new ParsedBindingPathNode(pathValue, context.GetAvaloniaTypes().CompiledBindingPath, nodes);
bindingPathAssignment.Values[0] = new ParsedBindingPathNode(pathValue, context.GetAvaloniaTypes().CompiledBindingPath, nodes);
}
}
}
}

9
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -112,7 +112,14 @@ namespace Avalonia.Win32
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogImpl>()
.Bind<IWindowingPlatform>().ToConstant(s_instance)
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)
{
OpenContextMenu =
{
// Add Shift+F10
new KeyGesture(Key.F10, KeyModifiers.Shift)
}
})
.Bind<IPlatformIconLoader>().ToConstant(s_instance)
.Bind<NonPumpingLockHelper.IHelperImpl>().ToConstant(new NonPumpingSyncContext.HelperImpl())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new WindowsMountedVolumeInfoProvider());

247
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -1,11 +1,12 @@
using System;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Castle.DynamicProxy.Generators;
using Moq;
using Xunit;
@ -16,6 +17,107 @@ namespace Avalonia.Controls.UnitTests
private Mock<IPopupImpl> popupImpl;
private MouseTestHelper _mouse = new MouseTestHelper();
[Fact]
public void ContextRequested_Opens_ContextMenu()
{
using (Application())
{
var sut = new ContextMenu();
var target = new Panel
{
ContextMenu = sut
};
var window = new Window { Content = target };
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
int openedCount = 0;
sut.MenuOpened += (sender, args) =>
{
openedCount++;
};
target.RaiseEvent(new ContextRequestedEventArgs());
Assert.True(sut.IsOpen);
Assert.Equal(1, openedCount);
}
}
[Fact]
public void ContextMenu_Is_Opened_When_ContextFlyout_Is_Also_Set()
{
// We have this test for backwards compatability with the code that already sets custom ContextMenu.
using (Application())
{
var sut = new ContextMenu();
var flyout = new Flyout();
var target = new Panel
{
ContextMenu = sut,
ContextFlyout = flyout
};
var window = new Window { Content = target };
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
target.RaiseEvent(new ContextRequestedEventArgs());
Assert.True(sut.IsOpen);
Assert.False(flyout.IsOpen);
}
}
[Fact]
public void KeyUp_Raised_On_Target_Opens_ContextFlyout()
{
using (Application())
{
var sut = new ContextMenu();
var target = new Panel
{
ContextMenu = sut
};
var contextRequestedCount = 0;
target.AddHandler(Control.ContextRequestedEvent, (s, a) => contextRequestedCount++, Interactivity.RoutingStrategies.Tunnel);
var window = PreparedWindow(target);
window.Show();
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window });
Assert.True(sut.IsOpen);
Assert.Equal(1, contextRequestedCount);
}
}
[Fact]
public void KeyUp_Raised_On_Flyout_Closes_Opened_ContextMenu()
{
using (Application())
{
var sut = new ContextMenu();
var target = new Panel
{
ContextMenu = sut
};
var window = PreparedWindow(target);
window.Show();
target.RaiseEvent(new ContextRequestedEventArgs());
Assert.True(sut.IsOpen);
sut.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window });
Assert.False(sut.IsOpen);
}
}
[Fact]
public void Opening_Raises_Single_Opened_Event()
{
@ -123,6 +225,91 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Cancel_Light_Dismiss_Closing_Keeps_Flyout_Open()
{
using (Application())
{
popupImpl.Setup(x => x.Show(true)).Verifiable();
popupImpl.Setup(x => x.Hide()).Verifiable();
var window = PreparedWindow();
window.Width = 100;
window.Height = 100;
var button = new Button
{
Height = 10,
Width = 10,
HorizontalAlignment = Layout.HorizontalAlignment.Left,
VerticalAlignment = Layout.VerticalAlignment.Top
};
window.Content = button;
window.ApplyTemplate();
window.Show();
var tracker = 0;
var c = new ContextMenu();
c.ContextMenuClosing += (s, e) =>
{
tracker++;
e.Cancel = true;
};
button.ContextMenu = c;
c.Open(button);
var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
_mouse.Down(overlay, MouseButton.Left, new Point(90, 90));
_mouse.Up(button, MouseButton.Left, new Point(90, 90));
Assert.Equal(1, tracker);
Assert.True(c.IsOpen);
popupImpl.Verify(x => x.Hide(), Times.Never);
popupImpl.Verify(x => x.Show(true), Times.Exactly(1));
}
}
[Fact]
public void Light_Dismiss_Closes_Flyout()
{
using (Application())
{
popupImpl.Setup(x => x.Show(true)).Verifiable();
popupImpl.Setup(x => x.Hide()).Verifiable();
var window = PreparedWindow();
window.Width = 100;
window.Height = 100;
var button = new Button
{
Height = 10,
Width = 10,
HorizontalAlignment = Layout.HorizontalAlignment.Left,
VerticalAlignment = Layout.VerticalAlignment.Top
};
window.Content = button;
window.ApplyTemplate();
window.Show();
var c = new ContextMenu();
c.PlacementMode = PlacementMode.Bottom;
c.Open(button);
var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
_mouse.Down(overlay, MouseButton.Left, new Point(90, 90));
_mouse.Up(button, MouseButton.Left, new Point(90, 90));
Assert.False(c.IsOpen);
popupImpl.Verify(x => x.Hide(), Times.Exactly(1));
popupImpl.Verify(x => x.Show(true), Times.Exactly(1));
}
}
[Fact]
public void Clicking_On_Control_Toggles_ContextMenu()
{
@ -137,15 +324,16 @@ namespace Avalonia.Controls.UnitTests
ContextMenu = sut
};
var window = new Window {Content = target};
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
var window = PreparedWindow(target);
window.Show();
var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
_mouse.Click(target, MouseButton.Right);
Assert.True(sut.IsOpen);
_mouse.Click(target);
_mouse.Down(overlay);
_mouse.Up(target);
Assert.False(sut.IsOpen);
popupImpl.Verify(x => x.Show(true), Times.Once);
@ -167,15 +355,16 @@ namespace Avalonia.Controls.UnitTests
ContextMenu = sut
};
var window = new Window {Content = target};
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
var window = PreparedWindow(target);
window.Show();
_mouse.Click(target, MouseButton.Right);
var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
_mouse.Click(target, MouseButton.Right);
Assert.True(sut.IsOpen);
_mouse.Click(target, MouseButton.Right);
_mouse.Down(overlay, MouseButton.Right);
_mouse.Up(target, MouseButton.Right);
Assert.True(sut.IsOpen);
popupImpl.Verify(x => x.Hide(), Times.Once);
@ -209,12 +398,10 @@ namespace Avalonia.Controls.UnitTests
Assert.True(sut.IsOpen);
_mouse.Click(target2, MouseButton.Left);
Assert.False(sut.IsOpen);
sp.Children.Remove(target1);
Assert.False(sut.IsOpen);
_mouse.Click(target2, MouseButton.Right);
Assert.True(sut.IsOpen);
@ -341,7 +528,7 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact(Skip = "The only reason this test was 'passing' before was that the author forgot to call Window.ApplyTemplate()")]
[Fact]
public void Cancelling_Closing_Leaves_ContextMenuOpen()
{
using (Application())
@ -355,17 +542,20 @@ namespace Avalonia.Controls.UnitTests
{
ContextMenu = sut
};
var window = new Window {Content = target};
window.ApplyTemplate();
var window = PreparedWindow(target);
var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; };
window.Show();
_mouse.Click(target, MouseButton.Right);
Assert.True(sut.IsOpen);
_mouse.Click(target, MouseButton.Right);
_mouse.Down(overlay, MouseButton.Right);
_mouse.Up(target, MouseButton.Right);
Assert.True(eventCalled);
Assert.True(sut.IsOpen);
@ -375,6 +565,19 @@ namespace Avalonia.Controls.UnitTests
}
}
private Window PreparedWindow(object content = null)
{
var renderer = new Mock<IRenderer>();
var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();
var windowImpl = Mock.Get(platform.CreateWindow());
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object);
var w = new Window(windowImpl.Object) { Content = content };
w.ApplyTemplate();
w.Presenter.ApplyTemplate();
return w;
}
private IDisposable Application()
{
var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100));

234
tests/Avalonia.Controls.UnitTests/FlyoutTests.cs

@ -1,10 +1,18 @@
using System;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
@ -28,6 +36,7 @@ namespace Avalonia.Controls.UnitTests
f.ShowAt(window);
Assert.Equal(1, tracker);
Assert.True(f.IsOpen);
}
}
@ -51,6 +60,31 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Opening_Is_Cancellable()
{
using (CreateServicesWithFocus())
{
var window = PreparedWindow();
window.Show();
int tracker = 0;
Flyout f = new Flyout();
f.Opening += (s, e) =>
{
tracker++;
if (e is CancelEventArgs cancelEventArgs)
{
cancelEventArgs.Cancel = true;
}
};
f.ShowAt(window);
Assert.Equal(1, tracker);
Assert.False(f.IsOpen);
}
}
[Fact]
public void Closing_Raises_Single_Closing_Event()
{
@ -101,16 +135,89 @@ namespace Avalonia.Controls.UnitTests
var window = PreparedWindow();
window.Show();
int tracker = 0;
Flyout f = new Flyout();
var tracker = 0;
var f = new Flyout();
f.Closing += (s, e) =>
{
tracker++;
e.Cancel = true;
};
f.ShowAt(window);
f.Hide();
Assert.True(f.IsOpen);
Assert.Equal(1, tracker);
}
}
[Fact]
public void Cancel_Light_Dismiss_Closing_Keeps_Flyout_Open()
{
using (CreateServicesWithFocus())
{
var window = PreparedWindow();
window.Width = 100;
window.Height = 100;
var button = new Button
{
Height = 10,
Width = 10,
HorizontalAlignment = Layout.HorizontalAlignment.Left,
VerticalAlignment = Layout.VerticalAlignment.Top
};
window.Content = button;
window.Show();
var tracker = 0;
var f = new Flyout();
f.Content = new Border { Width = 10, Height = 10 };
f.Closing += (s, e) =>
{
tracker++;
e.Cancel = true;
};
f.ShowAt(window);
var e = CreatePointerPressedEventArgs(window, new Point(90, 90));
var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
overlay.RaiseEvent(e);
Assert.Equal(1, tracker);
Assert.True(f.IsOpen);
}
}
[Fact]
public void Light_Dismiss_Closes_Flyout()
{
using (CreateServicesWithFocus())
{
var window = PreparedWindow();
window.Width = 100;
window.Height = 100;
var button = new Button
{
Height = 10,
Width = 10,
HorizontalAlignment = Layout.HorizontalAlignment.Left,
VerticalAlignment = Layout.VerticalAlignment.Top
};
window.Content = button;
window.Show();
var f = new Flyout();
f.Content = new Border { Width = 10, Height = 10 };
f.ShowAt(window);
var e = CreatePointerPressedEventArgs(window, new Point(90, 90));
var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
overlay.RaiseEvent(e);
Assert.False(f.IsOpen);
}
}
@ -222,6 +329,109 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void ContextRequested_Opens_ContextFlyout()
{
using (CreateServicesWithFocus())
{
var flyout = new Flyout();
var target = new Panel
{
ContextFlyout = flyout
};
var window = PreparedWindow(target);
window.Show();
int openedCount = 0;
flyout.Opened += (sender, args) =>
{
openedCount++;
};
target.RaiseEvent(new ContextRequestedEventArgs());
Assert.True(flyout.IsOpen);
Assert.Equal(1, openedCount);
}
}
[Fact]
public void KeyUp_Raised_On_Target_Opens_ContextFlyout()
{
using (CreateServicesWithFocus())
{
var flyout = new Flyout();
var target = new Panel
{
ContextFlyout = flyout
};
var contextRequestedCount = 0;
target.AddHandler(Control.ContextRequestedEvent, (s, a) => contextRequestedCount++, Interactivity.RoutingStrategies.Tunnel);
var window = PreparedWindow(target);
window.Show();
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window });
Assert.True(flyout.IsOpen);
Assert.Equal(1, contextRequestedCount);
}
}
[Fact]
public void KeyUp_Raised_On_Target_Closes_Opened_ContextFlyout()
{
using (CreateServicesWithFocus())
{
var flyout = new Flyout();
var target = new Panel
{
ContextFlyout = flyout
};
var window = PreparedWindow(target);
window.Show();
target.RaiseEvent(new ContextRequestedEventArgs());
Assert.True(flyout.IsOpen);
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window });
Assert.False(flyout.IsOpen);
}
}
[Fact]
public void KeyUp_Raised_On_Flyout_Closes_Opened_ContextFlyout()
{
using (CreateServicesWithFocus())
{
var flyoutContent = new Button();
var flyout = new Flyout()
{
Content = flyoutContent
};
var target = new Panel
{
ContextFlyout = flyout
};
var window = PreparedWindow(target);
window.Show();
target.RaiseEvent(new ContextRequestedEventArgs());
Assert.True(flyout.IsOpen);
flyoutContent.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window });
Assert.False(flyout.IsOpen);
}
}
[Fact]
public void ContextFlyout_Can_Be_Set_In_Styles()
{
@ -317,9 +527,27 @@ namespace Avalonia.Controls.UnitTests
private Window PreparedWindow(object content = null)
{
var w = new Window { Content = content };
var renderer = new Mock<IRenderer>();
var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();
var windowImpl = Mock.Get(platform.CreateWindow());
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object);
var w = new Window(windowImpl.Object) { Content = content };
w.ApplyTemplate();
return w;
}
private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source, Point p)
{
var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
return new PointerPressedEventArgs(
source,
pointer,
source,
p,
0,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
KeyModifiers.None);
}
}
}

29
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -8,6 +8,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
@ -556,6 +557,31 @@ namespace Avalonia.Controls.UnitTests
Assert.True(true);
}
}
[Theory]
[InlineData(Key.Up)]
[InlineData(Key.Down)]
[InlineData(Key.Home)]
[InlineData(Key.End)]
public void Textbox_doesnt_crash_when_Receives_input_and_template_not_applied(Key key)
{
using (UnitTestApplication.Start(FocusServices))
{
var target1 = new TextBox
{
Template = CreateTemplate(),
Text = "1234",
IsVisible = false
};
var root = new TestRoot { Child = target1 };
target1.Focus();
Assert.True(target1.IsFocused);
RaiseKeyEvent(target1, key, KeyModifiers.None);
}
}
[Fact]
public void TextBox_GotFocus_And_LostFocus_Work_Properly()
@ -763,6 +789,9 @@ namespace Avalonia.Controls.UnitTests
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: new KeyboardNavigationHandler(),
inputManager: new InputManager(),
renderInterface: new MockPlatformRenderInterface(),
fontManagerImpl: new MockFontManagerImpl(),
textShaperImpl: new MockTextShaperImpl(),
standardCursorFactory: Mock.Of<ICursorFactory>());
private static TestServices Services => TestServices.MockThreadingInterface.With(

104
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@ -938,6 +938,110 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
}
}
[Fact]
public void SupportsEmptyPath()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'>
<TextBlock Text='{CompiledBinding}' Name='textBlock' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
StringProperty = "foobar"
};
window.DataContext = dataContext;
Assert.Equal(typeof(TestDataContext).FullName, textBlock.Text);
}
}
[Fact]
public void SupportsEmptyPathWithStringFormat()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'>
<TextBlock Text='{CompiledBinding StringFormat=bar-\{0\}}' Name='textBlock' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
StringProperty = "foobar"
};
window.DataContext = dataContext;
Assert.Equal("bar-" + typeof(TestDataContext).FullName, textBlock.Text);
}
}
[Fact]
public void SupportsDotPath()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'>
<TextBlock Text='{CompiledBinding .}' Name='textBlock' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
StringProperty = "foobar"
};
window.DataContext = dataContext;
Assert.Equal(typeof(TestDataContext).FullName, textBlock.Text);
}
}
[Fact]
public void SupportsExplicitDotPathWithStringFormat()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'>
<TextBlock Text='{CompiledBinding Path=., StringFormat=bar-\{0\}}' Name='textBlock' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
StringProperty = "foobar"
};
window.DataContext = dataContext;
Assert.Equal("bar-" + typeof(TestDataContext).FullName, textBlock.Text);
}
}
void Throws(string type, Action cb)
{
try

35
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
@ -203,6 +204,40 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(expectedNumberOfLines, numberOfLines);
}
}
[Fact]
public void Should_Wrap_RightToLeft()
{
using (Start())
{
const string text =
"قطاعات الصناعة على الشبكة العالمية انترنيت ويونيكود، حيث ستتم، على الصعيدين الدولي والمحلي على حد سواء";
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
var currentTextSourceIndex = 0;
while (currentTextSourceIndex < text.Length)
{
var textLine =
formatter.FormatLine(textSource, currentTextSourceIndex, 50,
new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
var glyphClusters = textLine.TextRuns.Cast<ShapedTextCharacters>()
.SelectMany(x => x.GlyphRun.GlyphClusters).ToArray();
Assert.True(glyphClusters[0] >= glyphClusters[^1]);
Assert.Equal(currentTextSourceIndex, glyphClusters[^1]);
currentTextSourceIndex += textLine.TextRange.Length;
}
}
}
[InlineData("Whether to turn off HTTPS. This option only applies if Individual, " +
"IndividualB2C, SingleOrg, or MultiOrg aren't used for &#8209;&#8209;auth."

28
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@ -245,6 +245,34 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
}
}
[Fact]
public void Should_Respect_Uniform_ZIndex()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Panel panel;
var tree = new TestRoot
{
Child = panel = new Panel()
};
for (var i = 0; i < 128; i++)
{
panel.Children.Add(new Border());
}
var result = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(result);
var panelNode = result.FindNode(tree.Child);
var expected = panel.Children.ToArray();
var actual = panelNode.Children.OfType<IVisualNode>().Select(x => x.Visual).ToArray();
Assert.Equal(expected, actual);
}
}
[Fact]
public void ClipBounds_Should_Be_In_Global_Coordinates()
{

Loading…
Cancel
Save