Browse Source

Merge branch 'master' into android

pull/5337/head
Jumar Macato 6 years ago
committed by GitHub
parent
commit
929e09240d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      native/Avalonia.Native/src/OSX/AvnString.h
  2. 15
      native/Avalonia.Native/src/OSX/AvnString.mm
  3. 28
      native/Avalonia.Native/src/OSX/app.mm
  4. 2
      native/Avalonia.Native/src/OSX/clipboard.mm
  5. 4
      native/Avalonia.Native/src/OSX/common.h
  6. 21
      native/Avalonia.Native/src/OSX/main.mm
  7. 85
      native/Avalonia.Native/src/OSX/menu.mm
  8. 15
      native/Avalonia.Native/src/OSX/window.mm
  9. 8
      samples/ControlCatalog/Pages/AcrylicPage.xaml
  10. 5
      samples/ControlCatalog/Pages/DataGridPage.xaml
  11. 6
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  12. 7
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  13. 15
      samples/ControlCatalog/Pages/SliderPage.xaml
  14. 16
      src/Avalonia.Animation/Animation.cs
  15. 40
      src/Avalonia.Base/AvaloniaObject.cs
  16. 28
      src/Avalonia.Base/Data/BindingOperations.cs
  17. 6
      src/Avalonia.Base/Data/Converters/BoolConverters.cs
  18. 57
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  19. 30
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  20. 8
      src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
  21. 9
      src/Avalonia.Base/PropertyStore/IValue.cs
  22. 16
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  23. 121
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  24. 5
      src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
  25. 17
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  26. 264
      src/Avalonia.Base/ValueStore.cs
  27. 11
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  28. 21
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  29. 56
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  30. 16
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  31. 5
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  32. 2
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  33. 3
      src/Avalonia.Controls/ApiCompatBaseline.txt
  34. 11
      src/Avalonia.Controls/Application.cs
  35. 14
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  36. 8
      src/Avalonia.Controls/AutoCompleteBox.cs
  37. 70
      src/Avalonia.Controls/ComboBox.cs
  38. 77
      src/Avalonia.Controls/ContextMenu.cs
  39. 69
      src/Avalonia.Controls/Control.cs
  40. 77
      src/Avalonia.Controls/DefinitionBase.cs
  41. 55
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  42. 6
      src/Avalonia.Controls/NativeControlHost.cs
  43. 17
      src/Avalonia.Controls/Notifications/NotificationCard.cs
  44. 18
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  45. 7
      src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs
  46. 4
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  47. 47
      src/Avalonia.Controls/Slider.cs
  48. 43
      src/Avalonia.Controls/TextBox.cs
  49. 14
      src/Avalonia.Controls/UrlOpenedEventArgs.cs
  50. 45
      src/Avalonia.Controls/Window.cs
  51. 21
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
  52. 226
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  53. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  54. 51
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs
  55. 27
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
  56. 59
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
  57. 43
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs
  58. 7
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  59. 124
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  60. 10
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  61. 2
      src/Avalonia.Dialogs/ManagedFileChooserSources.cs
  62. 4
      src/Avalonia.Input/FocusManager.cs
  63. 2
      src/Avalonia.Layout/ElementManager.cs
  64. 2
      src/Avalonia.Layout/UniformGridLayout.cs
  65. 10
      src/Avalonia.Layout/UniformGridLayoutState.cs
  66. 14
      src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
  67. 9
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  68. 2
      src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
  69. 9
      src/Avalonia.Native/avn.idl
  70. 2
      src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs
  71. 2
      src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
  72. 12
      src/Avalonia.ReactiveUI/ReactiveUserControl.cs
  73. 14
      src/Avalonia.ReactiveUI/ReactiveWindow.cs
  74. 16
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  75. 16
      src/Avalonia.ReactiveUI/TransitioningContentControl.cs
  76. 12
      src/Avalonia.ReactiveUI/ViewModelViewHost.cs
  77. 4
      src/Avalonia.Styling/ApiCompatBaseline.txt
  78. 21
      src/Avalonia.Styling/Diagnostics/StyleDiagnostics.cs
  79. 17
      src/Avalonia.Styling/Diagnostics/StyledElementExtensions.cs
  80. 4
      src/Avalonia.Styling/IStyledElement.cs
  81. 40
      src/Avalonia.Styling/StyledElement.cs
  82. 5
      src/Avalonia.Styling/Styling/IStyleInstance.cs
  83. 18
      src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs
  84. 2
      src/Avalonia.Styling/Styling/PropertySetterInstance.cs
  85. 128
      src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs
  86. 24
      src/Avalonia.Styling/Styling/Setter.cs
  87. 9
      src/Avalonia.Styling/Styling/StyleInstance.cs
  88. 3
      src/Avalonia.Themes.Default/MenuItem.xaml
  89. 5
      src/Avalonia.Themes.Default/ProgressBar.xaml
  90. 5
      src/Avalonia.Themes.Default/Slider.xaml
  91. 3
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  92. 6
      src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml
  93. 12
      src/Avalonia.Themes.Fluent/Controls/Slider.xaml
  94. 4
      src/Avalonia.Visuals/Media/FormattedText.cs
  95. 18
      src/Avalonia.Visuals/Media/PathGeometryCollections.cs
  96. 14
      src/Avalonia.Visuals/Visual.cs
  97. 9
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  98. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  99. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  100. 11
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs

1
native/Avalonia.Native/src/OSX/AvnString.h

@ -11,6 +11,7 @@
extern IAvnString* CreateAvnString(NSString* string); extern IAvnString* CreateAvnString(NSString* string);
extern IAvnStringArray* CreateAvnStringArray(NSArray<NSString*>* array); extern IAvnStringArray* CreateAvnStringArray(NSArray<NSString*>* array);
extern IAvnStringArray* CreateAvnStringArray(NSArray<NSURL*>* array);
extern IAvnStringArray* CreateAvnStringArray(NSString* string); extern IAvnStringArray* CreateAvnStringArray(NSString* string);
extern IAvnString* CreateByteArray(void* data, int len); extern IAvnString* CreateByteArray(void* data, int len);
#endif /* AvnString_h */ #endif /* AvnString_h */

15
native/Avalonia.Native/src/OSX/AvnString.mm

@ -85,6 +85,16 @@ public:
} }
} }
AvnStringArrayImpl(NSArray<NSURL*>* array)
{
for(int c = 0; c < [array count]; c++)
{
ComPtr<IAvnString> s;
*s.getPPV() = new AvnStringImpl([array objectAtIndex:c].absoluteString);
_list.push_back(s);
}
}
AvnStringArrayImpl(NSString* string) AvnStringArrayImpl(NSString* string)
{ {
ComPtr<IAvnString> s; ComPtr<IAvnString> s;
@ -117,6 +127,11 @@ IAvnStringArray* CreateAvnStringArray(NSArray<NSString*> * array)
return new AvnStringArrayImpl(array); return new AvnStringArrayImpl(array);
} }
IAvnStringArray* CreateAvnStringArray(NSArray<NSURL*> * array)
{
return new AvnStringArrayImpl(array);
}
IAvnStringArray* CreateAvnStringArray(NSString* string) IAvnStringArray* CreateAvnStringArray(NSString* string)
{ {
return new AvnStringArrayImpl(string); return new AvnStringArrayImpl(string);

28
native/Avalonia.Native/src/OSX/app.mm

@ -1,10 +1,20 @@
#include "common.h" #include "common.h"
#include "AvnString.h"
@interface AvnAppDelegate : NSObject<NSApplicationDelegate> @interface AvnAppDelegate : NSObject<NSApplicationDelegate>
-(AvnAppDelegate* _Nonnull) initWithEvents: (IAvnApplicationEvents* _Nonnull) events;
@end @end
NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
@implementation AvnAppDelegate @implementation AvnAppDelegate
ComPtr<IAvnApplicationEvents> _events;
- (AvnAppDelegate *)initWithEvents:(IAvnApplicationEvents *)events
{
_events = events;
return self;
}
- (void)applicationWillFinishLaunching:(NSNotification *)notification - (void)applicationWillFinishLaunching:(NSNotification *)notification
{ {
if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy) if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy)
@ -27,11 +37,23 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps]; [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
} }
- (void)application:(NSApplication *)sender openFiles:(NSArray<NSString *> *)filenames
{
auto array = CreateAvnStringArray(filenames);
_events->FilesOpened(array);
}
- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls
{
auto array = CreateAvnStringArray(urls);
_events->FilesOpened(array);
}
@end @end
@interface AvnApplication : NSApplication @interface AvnApplication : NSApplication
@end @end
@implementation AvnApplication @implementation AvnApplication
@ -63,9 +85,9 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
@end @end
extern void InitializeAvnApp() extern void InitializeAvnApp(IAvnApplicationEvents* events)
{ {
NSApplication* app = [AvnApplication sharedApplication]; NSApplication* app = [AvnApplication sharedApplication];
id delegate = [AvnAppDelegate new]; id delegate = [[AvnAppDelegate alloc] initWithEvents:events];
[app setDelegate:delegate]; [app setDelegate:delegate];
} }

2
native/Avalonia.Native/src/OSX/clipboard.mm

@ -56,7 +56,7 @@ public:
return S_OK; return S_OK;
} }
NSArray* arr = (NSArray*)data; NSArray<NSString*>* arr = (NSArray*)data;
for(int c = 0; c < [arr count]; c++) for(int c = 0; c < [arr count]; c++)
if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]]) if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]])

4
native/Avalonia.Native/src/OSX/common.h

@ -28,8 +28,10 @@ extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu);
extern IAvnMenu* GetAppMenu (); extern IAvnMenu* GetAppMenu ();
extern NSMenuItem* GetAppMenuItem (); extern NSMenuItem* GetAppMenuItem ();
extern void SetAutoGenerateDefaultAppMenuItems (bool enabled);
extern bool GetAutoGenerateDefaultAppMenuItems ();
extern void InitializeAvnApp(); extern void InitializeAvnApp(IAvnApplicationEvents* events);
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
extern NSPoint ToNSPoint (AvnPoint p); extern NSPoint ToNSPoint (AvnPoint p);
extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ToAvnPoint (NSPoint p);

21
native/Avalonia.Native/src/OSX/main.mm

@ -2,6 +2,7 @@
#define COM_GUIDS_MATERIALIZE #define COM_GUIDS_MATERIALIZE
#include "common.h" #include "common.h"
static bool s_generateDefaultAppMenuItems = true;
static NSString* s_appTitle = @"Avalonia"; static NSString* s_appTitle = @"Avalonia";
// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Copyright (c) 2011 The Chromium Authors. All rights reserved.
@ -122,6 +123,12 @@ public:
? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory;
return S_OK; return S_OK;
} }
virtual HRESULT SetDisableDefaultApplicationMenuItems (bool enabled) override
{
SetAutoGenerateDefaultAppMenuItems(!enabled);
return S_OK;
}
}; };
/// See "Using POSIX Threads in a Cocoa Application" section here: /// See "Using POSIX Threads in a Cocoa Application" section here:
@ -156,13 +163,13 @@ class AvaloniaNative : public ComSingleObject<IAvaloniaNativeFactory, &IID_IAval
public: public:
FORWARD_IUNKNOWN() FORWARD_IUNKNOWN()
virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) override virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* events) override
{ {
_deallocator = deallocator; _deallocator = deallocator;
@autoreleasepool{ @autoreleasepool{
[[ThreadingInitializer new] do]; [[ThreadingInitializer new] do];
} }
InitializeAvnApp(); InitializeAvnApp(events);
return S_OK; return S_OK;
}; };
@ -310,3 +317,13 @@ CGFloat PrimaryDisplayHeight()
{ {
return NSMaxY([[[NSScreen screens] firstObject] frame]); return NSMaxY([[[NSScreen screens] firstObject] frame]);
} }
void SetAutoGenerateDefaultAppMenuItems (bool enabled)
{
s_generateDefaultAppMenuItems = enabled;
}
bool GetAutoGenerateDefaultAppMenuItems ()
{
return s_generateDefaultAppMenuItems;
}

85
native/Avalonia.Native/src/OSX/menu.mm

@ -445,47 +445,50 @@ extern void SetAppMenu (NSString* appName, IAvnMenu* menu)
auto appMenu = [s_appMenuItem submenu]; auto appMenu = [s_appMenuItem submenu];
[appMenu addItem:[NSMenuItem separatorItem]]; if(GetAutoGenerateDefaultAppMenuItems())
{
// Services item and menu [appMenu addItem:[NSMenuItem separatorItem]];
auto servicesItem = [[NSMenuItem alloc] init];
servicesItem.title = @"Services"; // Services item and menu
NSMenu *servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"]; auto servicesItem = [[NSMenuItem alloc] init];
servicesItem.submenu = servicesMenu; servicesItem.title = @"Services";
[NSApplication sharedApplication].servicesMenu = servicesMenu; NSMenu *servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"];
[appMenu addItem:servicesItem]; servicesItem.submenu = servicesMenu;
[NSApplication sharedApplication].servicesMenu = servicesMenu;
[appMenu addItem:[NSMenuItem separatorItem]]; [appMenu addItem:servicesItem];
// Hide Application [appMenu addItem:[NSMenuItem separatorItem]];
auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"];
// Hide Application
[appMenu addItem:hideItem]; auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"];
// Hide Others [appMenu addItem:hideItem];
auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others"
action:@selector(hideOtherApplications:) // Hide Others
keyEquivalent:@"h"]; auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others"
action:@selector(hideOtherApplications:)
hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption; keyEquivalent:@"h"];
[appMenu addItem:hideAllOthersItem];
hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption;
// Show All [appMenu addItem:hideAllOthersItem];
auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All"
action:@selector(unhideAllApplications:) // Show All
keyEquivalent:@""]; auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All"
action:@selector(unhideAllApplications:)
[appMenu addItem:showAllItem]; keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]]; [appMenu addItem:showAllItem];
// Quit Application [appMenu addItem:[NSMenuItem separatorItem]];
auto quitItem = [[NSMenuItem alloc] init];
quitItem.title = [@"Quit " stringByAppendingString:appName]; // Quit Application
quitItem.keyEquivalent = @"q"; auto quitItem = [[NSMenuItem alloc] init];
quitItem.target = [AvnWindow class]; quitItem.title = [@"Quit " stringByAppendingString:appName];
quitItem.action = @selector(closeAll); quitItem.keyEquivalent = @"q";
[appMenu addItem:quitItem]; quitItem.target = [AvnWindow class];
quitItem.action = @selector(closeAll);
[appMenu addItem:quitItem];
}
} }
else else
{ {

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

@ -1877,7 +1877,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
for(int i = 0; i < numWindows; i++) for(int i = 0; i < numWindows; i++)
{ {
[[windows objectAtIndex:i] performClose:nil]; auto window = (AvnWindow*)[windows objectAtIndex:i];
if([window parentWindow] == nullptr) // Avalonia will handle the child windows.
{
[window performClose:nil];
}
} }
} }
@ -2068,17 +2073,17 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
-(void)becomeKeyWindow -(void)becomeKeyWindow
{ {
[self showWindowMenuWithAppMenu];
if([self activateAppropriateChild: true]) if([self activateAppropriateChild: true])
{ {
[self showWindowMenuWithAppMenu];
if(_parent != nullptr) if(_parent != nullptr)
{ {
_parent->BaseEvents->Activated(); _parent->BaseEvents->Activated();
} }
[super becomeKeyWindow];
} }
[super becomeKeyWindow];
} }
-(void) restoreParentWindow; -(void) restoreParentWindow;

8
samples/ControlCatalog/Pages/AcrylicPage.xaml

@ -16,13 +16,13 @@
<StackPanel Spacing="5" Margin="40 10"> <StackPanel Spacing="5" Margin="40 10">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="TintOpacity" Foreground="Black" /> <TextBlock Text="TintOpacity" Foreground="Black" />
<Slider Name="TintOpacitySlider" Minimum="0" Maximum="1" Value="0.9" Width="400" /> <Slider Name="TintOpacitySlider" Minimum="0" Maximum="1" Value="0.9" SmallChange="0.1" LargeChange="0.2" Width="400" />
<TextBlock Text="{Binding #TintOpacitySlider.Value}" Foreground="Black" /> <TextBlock Text="{Binding #TintOpacitySlider.Value, StringFormat=\{0:0.#\}}" Foreground="Black" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="MaterialOpacity" Foreground="Black" /> <TextBlock Text="MaterialOpacity" Foreground="Black" />
<Slider Name="MaterialOpacitySlider" Minimum="0" Maximum="1" Value="0.8" Width="400" /> <Slider Name="MaterialOpacitySlider" Minimum="0" Maximum="1" Value="0.8" SmallChange="0.1" LargeChange="0.2" Width="400" />
<TextBlock Text="{Binding #MaterialOpacitySlider.Value}" Foreground="Black" /> <TextBlock Text="{Binding #MaterialOpacitySlider.Value, StringFormat=\{0:0.#\}}" Foreground="Black" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</ExperimentalAcrylicBorder> </ExperimentalAcrylicBorder>

5
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -1,5 +1,5 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:local="clr-namespace:ControlCatalog.Models;assembly=ControlCatalog" xmlns:local="using:ControlCatalog.Models"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.DataGridPage"> x:Class="ControlCatalog.Pages.DataGridPage">
<UserControl.Resources> <UserControl.Resources>
@ -26,7 +26,8 @@
<DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All"> <DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" /> <DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" />
<DataGridTextColumn Header="Region" Binding="{Binding Region}" Width="4*" /> <!-- 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="Population" Binding="{Binding Population}" Width="3*" />
<DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" /> <DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" />
<DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*" CellStyleClasses="gdp" /> <DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*" CellStyleClasses="gdp" />

6
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@ -24,8 +24,10 @@ namespace ControlCatalog.Pages
dg1.LoadingRow += Dg1_LoadingRow; dg1.LoadingRow += Dg1_LoadingRow;
dg1.Sorting += (s, a) => dg1.Sorting += (s, a) =>
{ {
var property = ((a.Column as DataGridBoundColumn)?.Binding as Binding).Path; var binding = (a.Column as DataGridBoundColumn)?.Binding as Binding;
if (property == dataGridSortDescription.PropertyPath
if (binding?.Path is string property
&& property == dataGridSortDescription.PropertyPath
&& !collectionView1.SortDescriptions.Contains(dataGridSortDescription)) && !collectionView1.SortDescriptions.Contains(dataGridSortDescription))
{ {
collectionView1.SortDescriptions.Add(dataGridSortDescription); collectionView1.SortDescriptions.Add(dataGridSortDescription);

7
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@ -15,6 +15,13 @@
<Slider Name="hprogress" Maximum="100" Value="40" /> <Slider Name="hprogress" Maximum="100" Value="40" />
<Slider Name="vprogress" Maximum="100" Value="60" /> <Slider Name="vprogress" Maximum="100" Value="60" />
</StackPanel> </StackPanel>
<StackPanel Spacing="10">
<ProgressBar VerticalAlignment="Center" IsIndeterminate="True" />
<ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
<ProgressBar VerticalAlignment="Center" Value="50" />
<ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />
</StackPanel>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

15
samples/ControlCatalog/Pages/SliderPage.xaml

@ -45,6 +45,12 @@
<sys:Exception /> <sys:Exception />
</DataValidationErrors.Error> </DataValidationErrors.Error>
</Slider> </Slider>
<Slider Value="0"
IsDirectionReversed="True"
Minimum="0"
Maximum="100"
TickFrequency="10"
Width="300" />
</StackPanel> </StackPanel>
<Slider Value="0" <Slider Value="0"
Minimum="0" Minimum="0"
@ -54,6 +60,15 @@
TickPlacement="Outside" TickPlacement="Outside"
TickFrequency="10" TickFrequency="10"
Height="300"/> Height="300"/>
<Slider Value="0"
IsDirectionReversed="True"
Minimum="0"
Maximum="100"
Orientation="Vertical"
IsSnapToTickEnabled="True"
TickPlacement="Outside"
TickFrequency="10"
Height="300"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

16
src/Avalonia.Animation/Animation.cs

@ -22,7 +22,7 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public static readonly DirectProperty<Animation, TimeSpan> DurationProperty = public static readonly DirectProperty<Animation, TimeSpan> DurationProperty =
AvaloniaProperty.RegisterDirect<Animation, TimeSpan>( AvaloniaProperty.RegisterDirect<Animation, TimeSpan>(
nameof(_duration), nameof(Duration),
o => o._duration, o => o._duration,
(o, v) => o._duration = v); (o, v) => o._duration = v);
@ -31,7 +31,7 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public static readonly DirectProperty<Animation, IterationCount> IterationCountProperty = public static readonly DirectProperty<Animation, IterationCount> IterationCountProperty =
AvaloniaProperty.RegisterDirect<Animation, IterationCount>( AvaloniaProperty.RegisterDirect<Animation, IterationCount>(
nameof(_iterationCount), nameof(IterationCount),
o => o._iterationCount, o => o._iterationCount,
(o, v) => o._iterationCount = v); (o, v) => o._iterationCount = v);
@ -40,7 +40,7 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public static readonly DirectProperty<Animation, PlaybackDirection> PlaybackDirectionProperty = public static readonly DirectProperty<Animation, PlaybackDirection> PlaybackDirectionProperty =
AvaloniaProperty.RegisterDirect<Animation, PlaybackDirection>( AvaloniaProperty.RegisterDirect<Animation, PlaybackDirection>(
nameof(_playbackDirection), nameof(PlaybackDirection),
o => o._playbackDirection, o => o._playbackDirection,
(o, v) => o._playbackDirection = v); (o, v) => o._playbackDirection = v);
@ -49,7 +49,7 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public static readonly DirectProperty<Animation, FillMode> FillModeProperty = public static readonly DirectProperty<Animation, FillMode> FillModeProperty =
AvaloniaProperty.RegisterDirect<Animation, FillMode>( AvaloniaProperty.RegisterDirect<Animation, FillMode>(
nameof(_fillMode), nameof(FillMode),
o => o._fillMode, o => o._fillMode,
(o, v) => o._fillMode = v); (o, v) => o._fillMode = v);
@ -58,7 +58,7 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public static readonly DirectProperty<Animation, Easing> EasingProperty = public static readonly DirectProperty<Animation, Easing> EasingProperty =
AvaloniaProperty.RegisterDirect<Animation, Easing>( AvaloniaProperty.RegisterDirect<Animation, Easing>(
nameof(_easing), nameof(Easing),
o => o._easing, o => o._easing,
(o, v) => o._easing = v); (o, v) => o._easing = v);
@ -67,7 +67,7 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public static readonly DirectProperty<Animation, TimeSpan> DelayProperty = public static readonly DirectProperty<Animation, TimeSpan> DelayProperty =
AvaloniaProperty.RegisterDirect<Animation, TimeSpan>( AvaloniaProperty.RegisterDirect<Animation, TimeSpan>(
nameof(_delay), nameof(Delay),
o => o._delay, o => o._delay,
(o, v) => o._delay = v); (o, v) => o._delay = v);
@ -76,7 +76,7 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public static readonly DirectProperty<Animation, TimeSpan> DelayBetweenIterationsProperty = public static readonly DirectProperty<Animation, TimeSpan> DelayBetweenIterationsProperty =
AvaloniaProperty.RegisterDirect<Animation, TimeSpan>( AvaloniaProperty.RegisterDirect<Animation, TimeSpan>(
nameof(_delayBetweenIterations), nameof(DelayBetweenIterations),
o => o._delayBetweenIterations, o => o._delayBetweenIterations,
(o, v) => o._delayBetweenIterations = v); (o, v) => o._delayBetweenIterations = v);
@ -85,7 +85,7 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public static readonly DirectProperty<Animation, double> SpeedRatioProperty = public static readonly DirectProperty<Animation, double> SpeedRatioProperty =
AvaloniaProperty.RegisterDirect<Animation, double>( AvaloniaProperty.RegisterDirect<Animation, double>(
nameof(_speedRatio), nameof(SpeedRatio),
o => o._speedRatio, o => o._speedRatio,
(o, v) => o._speedRatio = v, (o, v) => o._speedRatio = v,
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay);

40
src/Avalonia.Base/AvaloniaObject.cs

@ -23,7 +23,7 @@ namespace Avalonia
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged; private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private List<IAvaloniaObject> _inheritanceChildren; private List<IAvaloniaObject> _inheritanceChildren;
private ValueStore _values; private ValueStore _values;
private ValueStore Values => _values ?? (_values = new ValueStore(this)); private bool _batchUpdate;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class. /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@ -117,6 +117,22 @@ namespace Avalonia
set { this.Bind(binding.Property, value); } set { this.Bind(binding.Property, value); }
} }
private ValueStore Values
{
get
{
if (_values is null)
{
_values = new ValueStore(this);
if (_batchUpdate)
_values.BeginBatchUpdate();
}
return _values;
}
}
public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
@ -434,6 +450,28 @@ namespace Avalonia
_values?.CoerceValue(property); _values?.CoerceValue(property);
} }
public void BeginBatchUpdate()
{
if (_batchUpdate)
{
throw new InvalidOperationException("Batch update already in progress.");
}
_batchUpdate = true;
_values?.BeginBatchUpdate();
}
public void EndBatchUpdate()
{
if (!_batchUpdate)
{
throw new InvalidOperationException("No batch update in progress.");
}
_batchUpdate = false;
_values?.EndBatchUpdate();
}
/// <inheritdoc/> /// <inheritdoc/>
void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child) void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
{ {

28
src/Avalonia.Base/Data/BindingOperations.cs

@ -45,7 +45,7 @@ namespace Avalonia.Data
case BindingMode.OneWay: case BindingMode.OneWay:
return target.Bind(property, binding.Observable ?? binding.Subject, binding.Priority); return target.Bind(property, binding.Observable ?? binding.Subject, binding.Priority);
case BindingMode.TwoWay: case BindingMode.TwoWay:
return new CompositeDisposable( return new TwoWayBindingDisposable(
target.Bind(property, binding.Subject, binding.Priority), target.Bind(property, binding.Subject, binding.Priority),
target.GetObservable(property).Subscribe(binding.Subject)); target.GetObservable(property).Subscribe(binding.Subject));
case BindingMode.OneTime: case BindingMode.OneTime:
@ -88,6 +88,32 @@ namespace Avalonia.Data
throw new ArgumentException("Invalid binding mode."); throw new ArgumentException("Invalid binding mode.");
} }
} }
private sealed class TwoWayBindingDisposable : IDisposable
{
private readonly IDisposable _first;
private readonly IDisposable _second;
private bool _isDisposed;
public TwoWayBindingDisposable(IDisposable first, IDisposable second)
{
_first = first;
_second = second;
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
_first.Dispose();
_second.Dispose();
_isDisposed = true;
}
}
} }
public sealed class DoNothingType public sealed class DoNothingType

6
src/Avalonia.Base/Data/Converters/BoolConverters.cs

@ -12,5 +12,11 @@ namespace Avalonia.Data.Converters
/// </summary> /// </summary>
public static readonly IMultiValueConverter And = public static readonly IMultiValueConverter And =
new FuncMultiValueConverter<bool, bool>(x => x.All(y => y)); new FuncMultiValueConverter<bool, bool>(x => x.All(y => y));
/// <summary>
/// A multi-value converter that returns true if any of the inputs is true.
/// </summary>
public static readonly IMultiValueConverter Or =
new FuncMultiValueConverter<bool, bool>(x => x.Any(y => y));
} }
} }

57
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -9,8 +9,9 @@ namespace Avalonia.PropertyStore
/// <summary> /// <summary>
/// Represents an untyped interface to <see cref="BindingEntry{T}"/>. /// Represents an untyped interface to <see cref="BindingEntry{T}"/>.
/// </summary> /// </summary>
internal interface IBindingEntry : IPriorityValueEntry, IDisposable internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable
{ {
void Start(bool ignoreBatchUpdate);
} }
/// <summary> /// <summary>
@ -22,6 +23,8 @@ namespace Avalonia.PropertyStore
private readonly IAvaloniaObject _owner; private readonly IAvaloniaObject _owner;
private IValueSink _sink; private IValueSink _sink;
private IDisposable? _subscription; private IDisposable? _subscription;
private bool _isSubscribed;
private bool _batchUpdate;
private Optional<T> _value; private Optional<T> _value;
public BindingEntry( public BindingEntry(
@ -39,10 +42,20 @@ namespace Avalonia.PropertyStore
} }
public StyledPropertyBase<T> Property { get; } public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; } public BindingPriority Priority { get; private set; }
public IObservable<BindingValue<T>> Source { get; } public IObservable<BindingValue<T>> Source { get; }
Optional<object> IValue.GetValue() => _value.ToObject(); Optional<object> IValue.GetValue() => _value.ToObject();
public void BeginBatchUpdate() => _batchUpdate = true;
public void EndBatchUpdate()
{
_batchUpdate = false;
if (_sink is ValueStore)
Start();
}
public Optional<T> GetValue(BindingPriority maxPriority) public Optional<T> GetValue(BindingPriority maxPriority)
{ {
return Priority >= maxPriority ? _value : Optional<T>.Empty; return Priority >= maxPriority ? _value : Optional<T>.Empty;
@ -52,10 +65,17 @@ namespace Avalonia.PropertyStore
{ {
_subscription?.Dispose(); _subscription?.Dispose();
_subscription = null; _subscription = null;
_sink.Completed(Property, this, _value); _isSubscribed = false;
OnCompleted();
} }
public void OnCompleted() => _sink.Completed(Property, this, _value); public void OnCompleted()
{
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_sink.Completed(Property, this, oldValue);
}
public void OnError(Exception error) public void OnError(Exception error)
{ {
@ -79,13 +99,36 @@ namespace Avalonia.PropertyStore
} }
} }
public void Start() public void Start() => Start(false);
public void Start(bool ignoreBatchUpdate)
{ {
_subscription = Source.Subscribe(this); // We can't use _subscription to check whether we're subscribed because it won't be set
// until Subscribe has finished, which will be too late to prevent reentrancy.
if (!_isSubscribed && (!_batchUpdate || ignoreBatchUpdate))
{
_isSubscribed = true;
_subscription = Source.Subscribe(this);
}
} }
public void Reparent(IValueSink sink) => _sink = sink; public void Reparent(IValueSink sink) => _sink = sink;
public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
Priority));
}
private void UpdateValue(BindingValue<T> value) private void UpdateValue(BindingValue<T> value)
{ {
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false) if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)

30
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data; using Avalonia.Data;
#nullable enable #nullable enable
@ -17,7 +18,7 @@ namespace Avalonia.PropertyStore
public ConstantValueEntry( public ConstantValueEntry(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
T value, [AllowNull] T value,
BindingPriority priority, BindingPriority priority,
IValueSink sink) IValueSink sink)
{ {
@ -28,7 +29,7 @@ namespace Avalonia.PropertyStore
} }
public StyledPropertyBase<T> Property { get; } public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; } public BindingPriority Priority { get; private set; }
Optional<object> IValue.GetValue() => _value.ToObject(); Optional<object> IValue.GetValue() => _value.ToObject();
public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation) public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
@ -36,7 +37,30 @@ namespace Avalonia.PropertyStore
return Priority >= maxPriority ? _value : Optional<T>.Empty; return Priority >= maxPriority ? _value : Optional<T>.Empty;
} }
public void Dispose() => _sink.Completed(Property, this, _value); public void Dispose()
{
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_sink.Completed(Property, this, oldValue);
}
public void Reparent(IValueSink sink) => _sink = sink; public void Reparent(IValueSink sink) => _sink = sink;
public void Start() { }
public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
Priority));
}
} }
} }

8
src/Avalonia.Base/PropertyStore/IBatchUpdate.cs

@ -0,0 +1,8 @@
namespace Avalonia.PropertyStore
{
internal interface IBatchUpdate
{
void BeginBatchUpdate();
void EndBatchUpdate();
}
}

9
src/Avalonia.Base/PropertyStore/IValue.cs

@ -9,8 +9,15 @@ namespace Avalonia.PropertyStore
/// </summary> /// </summary>
internal interface IValue internal interface IValue
{ {
Optional<object> GetValue();
BindingPriority Priority { get; } BindingPriority Priority { get; }
Optional<object> GetValue();
void Start();
void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue);
} }
/// <summary> /// <summary>

16
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@ -24,5 +24,21 @@ namespace Avalonia.PropertyStore
} }
public void SetValue(T value) => _value = value; public void SetValue(T value) => _value = value;
public void Start() { }
public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
BindingPriority.LocalValue));
}
} }
} }

121
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
/// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order /// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order
/// they were added) plus a local value. /// they were added) plus a local value.
/// </remarks> /// </remarks>
internal class PriorityValue<T> : IValue<T>, IValueSink internal class PriorityValue<T> : IValue<T>, IValueSink, IBatchUpdate
{ {
private readonly IAvaloniaObject _owner; private readonly IAvaloniaObject _owner;
private readonly IValueSink _sink; private readonly IValueSink _sink;
@ -26,6 +26,8 @@ namespace Avalonia.PropertyStore
private readonly Func<IAvaloniaObject, T, T>? _coerceValue; private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
private Optional<T> _localValue; private Optional<T> _localValue;
private Optional<T> _value; private Optional<T> _value;
private bool _isCalculatingValue;
private bool _batchUpdate;
public PriorityValue( public PriorityValue(
IAvaloniaObject owner, IAvaloniaObject owner,
@ -53,6 +55,18 @@ namespace Avalonia.PropertyStore
existing.Reparent(this); existing.Reparent(this);
_entries.Add(existing); _entries.Add(existing);
if (existing is IBindingEntry binding &&
existing.Priority == BindingPriority.LocalValue)
{
// Bit of a special case here: if we have a local value binding that is being
// promoted to a priority value we need to make sure the binding is subscribed
// even if we've got a batch operation in progress because otherwise we don't know
// whether the binding or a subsequent SetValue with local priority will win. A
// notification won't be sent during batch update anyway because it will be
// caught and stored for later by the ValueStore.
binding.Start(ignoreBatchUpdate: true);
}
var v = existing.GetValue(); var v = existing.GetValue();
if (v.HasValue) if (v.HasValue)
@ -78,6 +92,28 @@ namespace Avalonia.PropertyStore
public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries; public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
Optional<object> IValue.GetValue() => _value.ToObject(); Optional<object> IValue.GetValue() => _value.ToObject();
public void BeginBatchUpdate()
{
_batchUpdate = true;
foreach (var entry in _entries)
{
(entry as IBatchUpdate)?.BeginBatchUpdate();
}
}
public void EndBatchUpdate()
{
_batchUpdate = false;
foreach (var entry in _entries)
{
(entry as IBatchUpdate)?.EndBatchUpdate();
}
UpdateEffectiveValue(null);
}
public void ClearLocalValue() public void ClearLocalValue()
{ {
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>( UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
@ -134,10 +170,37 @@ namespace Avalonia.PropertyStore
var binding = new BindingEntry<T>(_owner, Property, source, priority, this); var binding = new BindingEntry<T>(_owner, Property, source, priority, this);
var insert = FindInsertPoint(binding.Priority); var insert = FindInsertPoint(binding.Priority);
_entries.Insert(insert, binding); _entries.Insert(insert, binding);
if (_batchUpdate)
{
binding.BeginBatchUpdate();
if (priority == BindingPriority.LocalValue)
{
binding.Start(ignoreBatchUpdate: true);
}
}
return binding; return binding;
} }
public void CoerceValue() => UpdateEffectiveValue(null); public void UpdateEffectiveValue() => UpdateEffectiveValue(null);
public void Start() => UpdateEffectiveValue(null);
public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
Priority));
}
void IValueSink.ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change) void IValueSink.ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
{ {
@ -146,7 +209,7 @@ namespace Avalonia.PropertyStore
_localValue = default; _localValue = default;
} }
if (change is AvaloniaPropertyChangedEventArgs<T> c) if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs<T> c)
{ {
UpdateEffectiveValue(c); UpdateEffectiveValue(c);
} }
@ -188,41 +251,47 @@ namespace Avalonia.PropertyStore
public (Optional<T>, BindingPriority) CalculateValue(BindingPriority maxPriority) public (Optional<T>, BindingPriority) CalculateValue(BindingPriority maxPriority)
{ {
var reachedLocalValues = false; _isCalculatingValue = true;
for (var i = _entries.Count - 1; i >= 0; --i) try
{ {
var entry = _entries[i]; for (var i = _entries.Count - 1; i >= 0; --i)
if (entry.Priority < maxPriority)
{ {
continue; var entry = _entries[i];
if (entry.Priority < maxPriority)
{
continue;
}
entry.Start();
if (entry.Priority >= BindingPriority.LocalValue &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
{
return (_localValue, BindingPriority.LocalValue);
}
var entryValue = entry.GetValue();
if (entryValue.HasValue)
{
return (entryValue, entry.Priority);
}
} }
if (!reachedLocalValues && if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue)
entry.Priority >= BindingPriority.LocalValue &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
{ {
return (_localValue, BindingPriority.LocalValue); return (_localValue, BindingPriority.LocalValue);
} }
var entryValue = entry.GetValue(); return (default, BindingPriority.Unset);
if (entryValue.HasValue)
{
return (entryValue, entry.Priority);
}
} }
finally
if (!reachedLocalValues &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
{ {
return (_localValue, BindingPriority.LocalValue); _isCalculatingValue = false;
} }
return (default, BindingPriority.Unset);
} }
private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs<T>? change) private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs<T>? change)

5
src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace Avalonia.Reactive namespace Avalonia.Reactive
{ {
@ -55,9 +56,9 @@ namespace Avalonia.Reactive
newValue = (T)e.Sender.GetValue(e.Property); newValue = (T)e.Sender.GetValue(e.Property);
} }
if (!Equals(newValue, _value)) if (!EqualityComparer<T>.Default.Equals(newValue, _value))
{ {
_value = (T)newValue; _value = newValue;
PublishNext(_value); PublishNext(_value);
} }
} }

17
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -22,6 +22,9 @@ namespace Avalonia.Utilities
_entries = s_emptyEntries; _entries = s_emptyEntries;
} }
public int Count => _entries.Length - 1;
public TValue this[int index] => _entries[index].Value;
private (int, bool) TryFindEntry(int propertyId) private (int, bool) TryFindEntry(int propertyId)
{ {
if (_entries.Length <= 12) if (_entries.Length <= 12)
@ -163,18 +166,6 @@ namespace Avalonia.Utilities
} }
} }
public Dictionary<AvaloniaProperty, TValue> ToDictionary()
{
var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);
for (int i = 0; i < _entries.Length - 1; ++i)
{
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
}
return dict;
}
private struct Entry private struct Entry
{ {
internal int PropertyId; internal int PropertyId;

264
src/Avalonia.Base/ValueStore.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.PropertyStore; using Avalonia.PropertyStore;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -26,6 +27,7 @@ namespace Avalonia
private readonly AvaloniaObject _owner; private readonly AvaloniaObject _owner;
private readonly IValueSink _sink; private readonly IValueSink _sink;
private readonly AvaloniaPropertyValueStore<IValue> _values; private readonly AvaloniaPropertyValueStore<IValue> _values;
private BatchUpdate? _batchUpdate;
public ValueStore(AvaloniaObject owner) public ValueStore(AvaloniaObject owner)
{ {
@ -33,6 +35,25 @@ namespace Avalonia
_values = new AvaloniaPropertyValueStore<IValue>(); _values = new AvaloniaPropertyValueStore<IValue>();
} }
public void BeginBatchUpdate()
{
_batchUpdate ??= new BatchUpdate(this);
_batchUpdate.Begin();
}
public void EndBatchUpdate()
{
if (_batchUpdate is null)
{
throw new InvalidOperationException("No batch update in progress.");
}
if (_batchUpdate.End())
{
_batchUpdate = null;
}
}
public bool IsAnimating(AvaloniaProperty property) public bool IsAnimating(AvaloniaProperty property)
{ {
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot))
@ -90,23 +111,21 @@ namespace Avalonia
{ {
// If the property has any coercion callbacks then always create a PriorityValue. // If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue<T>(_owner, property, this); var entry = new PriorityValue<T>(_owner, property, this);
_values.AddValue(property, entry); AddValue(property, entry);
result = entry.SetValue(value, priority); result = entry.SetValue(value, priority);
} }
else else
{ {
var change = new AvaloniaPropertyChangedEventArgs<T>(_owner, property, default, value, priority);
if (priority == BindingPriority.LocalValue) if (priority == BindingPriority.LocalValue)
{ {
_values.AddValue(property, new LocalValueEntry<T>(value)); AddValue(property, new LocalValueEntry<T>(value));
_sink.ValueChanged(change); NotifyValueChanged<T>(property, default, value, priority);
} }
else else
{ {
var entry = new ConstantValueEntry<T>(property, value, priority, this); var entry = new ConstantValueEntry<T>(property, value, priority, this);
_values.AddValue(property, entry); AddValue(property, entry);
_sink.ValueChanged(change); NotifyValueChanged<T>(property, default, value, priority);
result = entry; result = entry;
} }
} }
@ -128,15 +147,13 @@ namespace Avalonia
// If the property has any coercion callbacks then always create a PriorityValue. // If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue<T>(_owner, property, this); var entry = new PriorityValue<T>(_owner, property, this);
var binding = entry.AddBinding(source, priority); var binding = entry.AddBinding(source, priority);
_values.AddValue(property, entry); AddValue(property, entry);
binding.Start();
return binding; return binding;
} }
else else
{ {
var entry = new BindingEntry<T>(_owner, property, source, priority, this); var entry = new BindingEntry<T>(_owner, property, source, priority, this);
_values.AddValue(property, entry); AddValue(property, entry);
entry.Start();
return entry; return entry;
} }
} }
@ -149,23 +166,32 @@ namespace Avalonia
{ {
p.ClearLocalValue(); p.ClearLocalValue();
} }
else else if (slot.Priority == BindingPriority.LocalValue)
{ {
var remove = slot is ConstantValueEntry<T> c ? var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
c.Priority == BindingPriority.LocalValue :
!(slot is IPriorityValueEntry<T>);
if (remove) // During batch update values can't be removed immediately because they're needed to raise
// a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal
// by setting their priority to Unset.
if (_batchUpdate is null)
{ {
var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
_values.Remove(property); _values.Remove(property);
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property,
new Optional<T>(old),
default,
BindingPriority.Unset));
} }
else if (slot is IDisposable d)
{
d.Dispose();
}
else
{
// Local value entries are optimized and contain only a single value field to save space,
// so there's no way to mark them for removal at the end of a batch update. Instead convert
// them to a constant value entry with Unset priority in the event of a local value being
// cleared during a batch update.
var sentinel = new ConstantValueEntry<T>(property, default, BindingPriority.Unset, _sink);
_values.SetValue(property, sentinel);
}
NotifyValueChanged<T>(property, old, default, BindingPriority.Unset);
} }
} }
} }
@ -176,7 +202,7 @@ namespace Avalonia
{ {
if (slot is PriorityValue<T> p) if (slot is PriorityValue<T> p)
{ {
p.CoerceValue(); p.UpdateEffectiveValue();
} }
} }
} }
@ -198,7 +224,17 @@ namespace Avalonia
void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{ {
_sink.ValueChanged(change); if (_batchUpdate is object)
{
if (change.IsEffectiveValueChange)
{
NotifyValueChanged<T>(change.Property, change.OldValue, change.NewValue, change.Priority);
}
}
else
{
_sink.ValueChanged(change);
}
} }
void IValueSink.Completed<T>( void IValueSink.Completed<T>(
@ -206,13 +242,17 @@ namespace Avalonia
IPriorityValueEntry entry, IPriorityValueEntry entry,
Optional<T> oldValue) Optional<T> oldValue)
{ {
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot) && slot == entry)
{ {
if (slot == entry) if (_batchUpdate is null)
{ {
_values.Remove(property); _values.Remove(property);
_sink.Completed(property, entry, oldValue); _sink.Completed(property, entry, oldValue);
} }
else
{
_batchUpdate.ValueChanged(property, oldValue.ToObject());
}
} }
} }
@ -240,16 +280,13 @@ namespace Avalonia
{ {
var old = l.GetValue(BindingPriority.LocalValue); var old = l.GetValue(BindingPriority.LocalValue);
l.SetValue(value); l.SetValue(value);
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( NotifyValueChanged<T>(property, old, value, priority);
_owner,
property,
old,
value,
priority));
} }
else else
{ {
var priorityValue = new PriorityValue<T>(_owner, property, this, l); var priorityValue = new PriorityValue<T>(_owner, property, this, l);
if (_batchUpdate is object)
priorityValue.BeginBatchUpdate();
result = priorityValue.SetValue(value, priority); result = priorityValue.SetValue(value, priority);
_values.SetValue(property, priorityValue); _values.SetValue(property, priorityValue);
} }
@ -273,6 +310,11 @@ namespace Avalonia
if (slot is IPriorityValueEntry<T> e) if (slot is IPriorityValueEntry<T> e)
{ {
priorityValue = new PriorityValue<T>(_owner, property, this, e); priorityValue = new PriorityValue<T>(_owner, property, this, e);
if (_batchUpdate is object)
{
priorityValue.BeginBatchUpdate();
}
} }
else if (slot is PriorityValue<T> p) else if (slot is PriorityValue<T> p)
{ {
@ -289,8 +331,162 @@ namespace Avalonia
var binding = priorityValue.AddBinding(source, priority); var binding = priorityValue.AddBinding(source, priority);
_values.SetValue(property, priorityValue); _values.SetValue(property, priorityValue);
binding.Start(); priorityValue.UpdateEffectiveValue();
return binding; return binding;
} }
private void AddValue(AvaloniaProperty property, IValue value)
{
_values.AddValue(property, value);
if (_batchUpdate is object && value is IBatchUpdate batch)
batch.BeginBatchUpdate();
value.Start();
}
private void NotifyValueChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
if (_batchUpdate is null)
{
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property,
oldValue,
newValue,
priority));
}
else
{
_batchUpdate.ValueChanged(property, oldValue.ToObject());
}
}
private class BatchUpdate
{
private ValueStore _owner;
private List<Notification>? _notifications;
private int _batchUpdateCount;
private int _iterator = -1;
public BatchUpdate(ValueStore owner) => _owner = owner;
public void Begin()
{
if (_batchUpdateCount++ == 0)
{
var values = _owner._values;
for (var i = 0; i < values.Count; ++i)
{
(values[i] as IBatchUpdate)?.BeginBatchUpdate();
}
}
}
public bool End()
{
if (--_batchUpdateCount > 0)
return false;
var values = _owner._values;
// First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed
// but notifications will still not be raised because the owner ValueStore will still have a reference
// to this batch update object.
for (var i = 0; i < values.Count; ++i)
{
(values[i] as IBatchUpdate)?.EndBatchUpdate();
// Somehow subscribing to a binding caused a new batch update. This shouldn't happen but in case it
// does, abort and continue batch updating.
if (_batchUpdateCount > 0)
return false;
}
if (_notifications is object)
{
// Raise all batched notifications. Doing this can cause other notifications to be added and even
// cause a new batch update to start, so we need to handle _notifications being modified by storing
// the index in field.
_iterator = 0;
for (; _iterator < _notifications.Count; ++_iterator)
{
var entry = _notifications[_iterator];
if (values.TryGetValue(entry.property, out var slot))
{
var oldValue = entry.oldValue;
var newValue = slot.GetValue();
// Raising this notification can cause a new batch update to be started, which in turn
// results in another change to the property. In this case we need to update the old value
// so that the *next* notification has an oldValue which follows on from the newValue
// raised here.
_notifications[_iterator] = new Notification
{
property = entry.property,
oldValue = newValue,
};
// Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs<T>.
slot.RaiseValueChanged(_owner._sink, _owner._owner, entry.property, oldValue, newValue);
// During batch update values can't be removed immediately because they're needed to raise
// the _sink.ValueChanged notification. They instead mark themselves for removal by setting
// their priority to Unset.
if (slot.Priority == BindingPriority.Unset)
{
values.Remove(entry.property);
}
}
else
{
throw new AvaloniaInternalException("Value could not be found at the end of batch update.");
}
// If a new batch update was started while ending this one, abort.
if (_batchUpdateCount > 0)
return false;
}
}
_iterator = int.MaxValue - 1;
return true;
}
public void ValueChanged(AvaloniaProperty property, Optional<object> oldValue)
{
_notifications ??= new List<Notification>();
for (var i = 0; i < _notifications.Count; ++i)
{
if (_notifications[i].property == property)
{
oldValue = _notifications[i].oldValue;
_notifications.RemoveAt(i);
if (i <= _iterator)
--_iterator;
break;
}
}
_notifications.Add(new Notification
{
property = property,
oldValue = oldValue,
});
}
private struct Notification
{
public AvaloniaProperty property;
public Optional<object> oldValue;
}
}
} }
} }

11
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

@ -3275,7 +3275,7 @@ namespace Avalonia.Collections
addIndex); addIndex);
// next check if we need to add an item into the current group // next check if we need to add an item into the current group
// bool needsGrouping = false; bool needsGrouping = false;
if (Count == 1 && GroupDescriptions.Count > 0) if (Count == 1 && GroupDescriptions.Count > 0)
{ {
// if this is the first item being added // if this is the first item being added
@ -3302,7 +3302,7 @@ namespace Avalonia.Collections
// otherwise, we need to validate that it is within the current page. // otherwise, we need to validate that it is within the current page.
if (PageSize == 0 || (PageIndex + 1) * PageSize > leafIndex) if (PageSize == 0 || (PageIndex + 1) * PageSize > leafIndex)
{ {
//needsGrouping = true; needsGrouping = true;
int pageStartIndex = PageIndex * PageSize; int pageStartIndex = PageIndex * PageSize;
@ -3340,6 +3340,13 @@ namespace Avalonia.Collections
} }
} }
// if we need to add the item into the current group
// that will be displayed
if (needsGrouping)
{
this._group.AddToSubgroups(addedItem, false /*loading*/);
}
int addedIndex = IndexOf(addedItem); int addedIndex = IndexOf(addedItem);
// if the item is within the current page // if the item is within the current page

21
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

@ -10,7 +10,8 @@ using System.Reactive.Disposables;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Avalonia.Reactive; using Avalonia.Reactive;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Markup.Xaml.MarkupExtensions;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -47,14 +48,15 @@ namespace Avalonia.Controls
if (_binding != null) if (_binding != null)
{ {
if(_binding is Avalonia.Data.Binding binding) if(_binding is BindingBase binding)
{ {
if (binding.Mode == BindingMode.OneWayToSource) if (binding.Mode == BindingMode.OneWayToSource)
{ {
throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource. Use BindingMode.TwoWay instead."); throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource. Use BindingMode.TwoWay instead.");
} }
if (!String.IsNullOrEmpty(binding.Path) && binding.Mode == BindingMode.Default) var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
if (!string.IsNullOrEmpty(path) && binding.Mode == BindingMode.Default)
{ {
binding.Mode = BindingMode.TwoWay; binding.Mode = BindingMode.TwoWay;
} }
@ -136,13 +138,16 @@ namespace Avalonia.Controls
internal void SetHeaderFromBinding() internal void SetHeaderFromBinding()
{ {
if (OwningGrid != null && OwningGrid.DataConnection.DataType != null if (OwningGrid != null && OwningGrid.DataConnection.DataType != null
&& Header == null && Binding != null && Binding is Binding binding && Header == null && Binding != null && Binding is BindingBase binding)
&& !String.IsNullOrWhiteSpace(binding.Path))
{ {
string header = OwningGrid.DataConnection.DataType.GetDisplayName(binding.Path); var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
if (header != null) if (!string.IsNullOrWhiteSpace(path))
{ {
Header = header; var header = OwningGrid.DataConnection.DataType.GetDisplayName(path);
if (header != null)
{
Header = header;
}
} }
} }
} }

56
src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs

@ -17,7 +17,6 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public class DataGridCheckBoxColumn : DataGridBoundColumn public class DataGridCheckBoxColumn : DataGridBoundColumn
{ {
private bool _beganEditWithKeyboard;
private CheckBox _currentCheckBox; private CheckBox _currentCheckBox;
private DataGrid _owningGrid; private DataGrid _owningGrid;
@ -153,23 +152,7 @@ namespace Avalonia.Controls
{ {
if (editingElement is CheckBox editingCheckBox) if (editingElement is CheckBox editingCheckBox)
{ {
bool? uneditedValue = editingCheckBox.IsChecked; void EditValue()
bool editValue = false;
if(editingEventArgs is PointerPressedEventArgs args)
{
// Editing was triggered by a mouse click
Point position = args.GetPosition(editingCheckBox);
Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
editValue = rect.Contains(position);
}
else if (_beganEditWithKeyboard)
{
// Editing began by a user pressing spacebar
editValue = true;
_beganEditWithKeyboard = false;
}
if (editValue)
{ {
// User clicked the checkbox itself or pressed space, let's toggle the IsChecked value // User clicked the checkbox itself or pressed space, let's toggle the IsChecked value
if (editingCheckBox.IsThreeState) if (editingCheckBox.IsThreeState)
@ -192,6 +175,40 @@ namespace Avalonia.Controls
editingCheckBox.IsChecked = !editingCheckBox.IsChecked; editingCheckBox.IsChecked = !editingCheckBox.IsChecked;
} }
} }
bool? uneditedValue = editingCheckBox.IsChecked;
if(editingEventArgs is PointerPressedEventArgs args)
{
void ProcessPointerArgs()
{
// Editing was triggered by a mouse click
Point position = args.GetPosition(editingCheckBox);
Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
if(rect.Contains(position))
{
EditValue();
}
}
void OnLayoutUpdated(object sender, EventArgs e)
{
if(!editingCheckBox.Bounds.IsEmpty)
{
editingCheckBox.LayoutUpdated -= OnLayoutUpdated;
ProcessPointerArgs();
}
}
if(editingCheckBox.Bounds.IsEmpty)
{
editingCheckBox.LayoutUpdated += OnLayoutUpdated;
}
else
{
ProcessPointerArgs();
}
}
return uneditedValue; return uneditedValue;
} }
return false; return false;
@ -284,13 +301,10 @@ namespace Avalonia.Controls
CheckBox checkBox = GetCellContent(row) as CheckBox; CheckBox checkBox = GetCellContent(row) as CheckBox;
if (checkBox == _currentCheckBox) if (checkBox == _currentCheckBox)
{ {
_beganEditWithKeyboard = true;
OwningGrid.BeginEdit(); OwningGrid.BeginEdit();
return;
} }
} }
} }
_beganEditWithKeyboard = false;
} }
private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e) private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e)

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

@ -12,6 +12,7 @@ using System;
using System.Linq; using System.Linq;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Markup.Xaml.MarkupExtensions;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -1033,13 +1034,16 @@ namespace Avalonia.Controls
if (String.IsNullOrEmpty(result)) if (String.IsNullOrEmpty(result))
{ {
if (this is DataGridBoundColumn boundColumn)
if(this is DataGridBoundColumn boundColumn &&
boundColumn.Binding != null &&
boundColumn.Binding is Binding binding &&
binding.Path != null)
{ {
result = binding.Path; if (boundColumn.Binding is Binding binding)
{
result = binding.Path;
}
else if (boundColumn.Binding is CompiledBindingExtension compiledBinding)
{
result = compiledBinding.Path.ToString();
}
} }
} }

5
src/Avalonia.Controls.DataGrid/DataGridColumns.cs

@ -5,6 +5,7 @@
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Utilities; using Avalonia.Utilities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -141,9 +142,9 @@ namespace Avalonia.Controls
Debug.Assert(dataGridColumn != null); Debug.Assert(dataGridColumn != null);
if (dataGridColumn is DataGridBoundColumn dataGridBoundColumn && if (dataGridColumn is DataGridBoundColumn dataGridBoundColumn &&
dataGridBoundColumn.Binding is Binding binding) dataGridBoundColumn.Binding is BindingBase binding)
{ {
string path = binding.Path; var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
if (string.IsNullOrWhiteSpace(path)) if (string.IsNullOrWhiteSpace(path))
{ {

2
src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs

@ -8,6 +8,7 @@ using Avalonia.Controls.Utils;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -22,6 +23,7 @@ namespace Avalonia.Controls
o => o.CellTemplate, o => o.CellTemplate,
(o, v) => o.CellTemplate = v); (o, v) => o.CellTemplate = v);
[Content]
public IDataTemplate CellTemplate public IDataTemplate CellTemplate
{ {
get { return _cellTemplate; } get { return _cellTemplate; }

3
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -1,6 +1,7 @@
Compat issues with assembly Avalonia.Controls: Compat issues with assembly Avalonia.Controls:
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
Total Issues: 4 Total Issues: 5

11
src/Avalonia.Controls/Application.cs

@ -30,7 +30,7 @@ namespace Avalonia
/// method. /// method.
/// - Tracks the lifetime of the application. /// - Tracks the lifetime of the application.
/// </remarks> /// </remarks>
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost, IApplicationPlatformEvents
{ {
/// <summary> /// <summary>
/// The application-global data templates. /// The application-global data templates.
@ -55,6 +55,8 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged; public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
public event EventHandler<UrlOpenedEventArgs> UrlsOpened;
/// <summary> /// <summary>
/// Creates an instance of the <see cref="Application"/> class. /// Creates an instance of the <see cref="Application"/> class.
/// </summary> /// </summary>
@ -247,7 +249,11 @@ namespace Avalonia
public virtual void OnFrameworkInitializationCompleted() public virtual void OnFrameworkInitializationCompleted()
{ {
}
void IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls)
{
UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls));
} }
private void NotifyResourcesChanged(ResourcesChangedEventArgs e) private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
@ -288,5 +294,6 @@ namespace Avalonia
get => _name; get => _name;
set => SetAndRaise(NameProperty, ref _name, value); set => SetAndRaise(NameProperty, ref _name, value);
} }
} }
} }

14
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@ -5,6 +5,7 @@ using System.Threading;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia.Controls.ApplicationLifetimes namespace Avalonia.Controls.ApplicationLifetimes
@ -102,6 +103,14 @@ namespace Avalonia.Controls.ApplicationLifetimes
public int Start(string[] args) public int Start(string[] args)
{ {
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
var options = AvaloniaLocator.Current.GetService<ClassicDesktopStyleApplicationLifetimeOptions>();
if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0)
{
((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args);
}
_cts = new CancellationTokenSource(); _cts = new CancellationTokenSource();
MainWindow?.Show(); MainWindow?.Show();
Dispatcher.UIThread.MainLoop(_cts.Token); Dispatcher.UIThread.MainLoop(_cts.Token);
@ -115,6 +124,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
_activeLifetime = null; _activeLifetime = null;
} }
} }
public class ClassicDesktopStyleApplicationLifetimeOptions
{
public bool ProcessUrlActivationCommandLine { get; set; }
}
} }
namespace Avalonia namespace Avalonia

8
src/Avalonia.Controls/AutoCompleteBox.cs

@ -483,7 +483,9 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<AutoCompleteBox, object>( AvaloniaProperty.RegisterDirect<AutoCompleteBox, object>(
nameof(SelectedItem), nameof(SelectedItem),
o => o.SelectedItem, o => o.SelectedItem,
(o, v) => o.SelectedItem = v); (o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);
/// <summary> /// <summary>
/// Identifies the /// Identifies the
@ -1333,7 +1335,7 @@ namespace Avalonia.Controls
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
} }
/// <summary> /// <summary>
/// Called to update the validation state for properties for which data validation is /// Called to update the validation state for properties for which data validation is
/// enabled. /// enabled.
@ -1342,7 +1344,7 @@ namespace Avalonia.Controls
/// <param name="value">The new binding value for the property.</param> /// <param name="value">The new binding value for the property.</param>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value) protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{ {
if (property == TextProperty) if (property == TextProperty || property == SelectedItemProperty)
{ {
DataValidationErrors.SetError(this, value.Error); DataValidationErrors.SetError(this, value.Error);
} }

70
src/Avalonia.Controls/ComboBox.cs

@ -10,6 +10,7 @@ using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -76,6 +77,14 @@ namespace Avalonia.Controls
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty = public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>(); ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>();
/// <summary>
/// Defines the <see cref="IsTextSearchEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsTextSearchEnabledProperty =
AvaloniaProperty.Register<ComboBox, bool>(nameof(IsTextSearchEnabled), true);
private string _textSearchTerm = string.Empty;
private DispatcherTimer _textSearchTimer;
private bool _isDropDownOpen; private bool _isDropDownOpen;
private Popup _popup; private Popup _popup;
private object _selectionBoxItem; private object _selectionBoxItem;
@ -164,6 +173,15 @@ namespace Avalonia.Controls
set { SetValue(VerticalContentAlignmentProperty, value); } set { SetValue(VerticalContentAlignmentProperty, value); }
} }
/// <summary>
/// Gets or sets a value that specifies whether a user can jump to a value by typing.
/// </summary>
public bool IsTextSearchEnabled
{
get { return GetValue(IsTextSearchEnabledProperty); }
set { SetValue(IsTextSearchEnabledProperty, value); }
}
/// <inheritdoc/> /// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator() protected override IItemContainerGenerator CreateItemContainerGenerator()
{ {
@ -229,6 +247,32 @@ namespace Avalonia.Controls
} }
} }
/// <inheritdoc />
protected override void OnTextInput(TextInputEventArgs e)
{
if (!IsTextSearchEnabled || e.Handled)
return;
StopTextSearchTimer();
_textSearchTerm += e.Text;
bool match(ItemContainerInfo info) =>
info.ContainerControl is IContentControl control &&
control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true;
var info = ItemContainerGenerator.Containers.FirstOrDefault(match);
if (info != null)
{
SelectedIndex = info.Index;
}
StartTextSearchTimer();
e.Handled = true;
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e) protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{ {
@ -426,5 +470,31 @@ namespace Avalonia.Controls
SelectedIndex = prev; SelectedIndex = prev;
} }
private void StartTextSearchTimer()
{
_textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_textSearchTimer.Tick += TextSearchTimer_Tick;
_textSearchTimer.Start();
}
private void StopTextSearchTimer()
{
if (_textSearchTimer == null)
{
return;
}
_textSearchTimer.Stop();
_textSearchTimer.Tick -= TextSearchTimer_Tick;
_textSearchTimer = null;
}
private void TextSearchTimer_Tick(object sender, EventArgs e)
{
_textSearchTerm = string.Empty;
StopTextSearchTimer();
}
} }
} }

77
src/Avalonia.Controls/ContextMenu.cs

@ -269,7 +269,43 @@ namespace Avalonia.Controls
} }
control ??= _attachedControls![0]; control ??= _attachedControls![0];
Open(control, PlacementTarget ?? control);
}
/// <summary>
/// Closes the menu.
/// </summary>
public override void Close()
{
if (!IsOpen)
{
return;
}
if (_popup != null && _popup.IsVisible)
{
_popup.IsOpen = false;
}
}
void ISetterValue.Initialize(ISetter setter)
{
// ContextMenu can be assigned to the ContextMenu property in a setter. This overrides
// the behavior defined in Control which requires controls to be wrapped in a <template>.
if (!(setter is Setter s && s.Property == ContextMenuProperty))
{
throw new InvalidOperationException(
"Cannot use a control as a Setter value. Wrap the control in a <Template>.");
}
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
private void Open(Control control, Control placementTarget)
{
if (IsOpen) if (IsOpen)
{ {
return; return;
@ -286,7 +322,6 @@ namespace Avalonia.Controls
PlacementGravity = PlacementGravity, PlacementGravity = PlacementGravity,
PlacementMode = PlacementMode, PlacementMode = PlacementMode,
PlacementRect = PlacementRect, PlacementRect = PlacementRect,
PlacementTarget = PlacementTarget ?? control,
IsLightDismissEnabled = true, IsLightDismissEnabled = true,
OverlayDismissEventPassThrough = true, OverlayDismissEventPassThrough = true,
WindowManagerAddShadowHint = WindowManagerAddShadowHint, WindowManagerAddShadowHint = WindowManagerAddShadowHint,
@ -302,11 +337,7 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control); ((ISetLogicalParent)_popup).SetParent(control);
} }
if (PlacementTarget is null && _popup.PlacementTarget != control) _popup.PlacementTarget = placementTarget;
{
_popup.PlacementTarget = control;
}
_popup.Child = this; _popup.Child = this;
IsOpen = true; IsOpen = true;
_popup.IsOpen = true; _popup.IsOpen = true;
@ -318,38 +349,6 @@ namespace Avalonia.Controls
}); });
} }
/// <summary>
/// Closes the menu.
/// </summary>
public override void Close()
{
if (!IsOpen)
{
return;
}
if (_popup != null && _popup.IsVisible)
{
_popup.IsOpen = false;
}
}
void ISetterValue.Initialize(ISetter setter)
{
// ContextMenu can be assigned to the ContextMenu property in a setter. This overrides
// the behavior defined in Control which requires controls to be wrapped in a <template>.
if (!(setter is Setter s && s.Property == ContextMenuProperty))
{
throw new InvalidOperationException(
"Cannot use a control as a Setter value. Wrap the control in a <Template>.");
}
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
private void PopupOpened(object sender, EventArgs e) private void PopupOpened(object sender, EventArgs e)
{ {
_previousFocus = FocusManager.Instance?.Current; _previousFocus = FocusManager.Instance?.Current;
@ -403,7 +402,7 @@ namespace Avalonia.Controls
if (contextMenu.CancelOpening()) if (contextMenu.CancelOpening())
return; return;
contextMenu.Open(control); contextMenu.Open(control, e.Source as Control ?? control);
e.Handled = true; e.Handled = true;
} }
} }

69
src/Avalonia.Controls/Control.cs

@ -8,6 +8,8 @@ using Avalonia.Rendering;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
@ -18,25 +20,25 @@ namespace Avalonia.Controls
/// ///
/// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control. /// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control.
/// </remarks> /// </remarks>
public class Control : InputElement, IControl, INamed, ISupportInitialize, IVisualBrushInitialize, ISetterValue public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, ISetterValue
{ {
/// <summary> /// <summary>
/// Defines the <see cref="FocusAdorner"/> property. /// Defines the <see cref="FocusAdorner"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<ITemplate<IControl>> FocusAdornerProperty = public static readonly StyledProperty<ITemplate<IControl>?> FocusAdornerProperty =
AvaloniaProperty.Register<Control, ITemplate<IControl>>(nameof(FocusAdorner)); AvaloniaProperty.Register<Control, ITemplate<IControl>?>(nameof(FocusAdorner));
/// <summary> /// <summary>
/// Defines the <see cref="Tag"/> property. /// Defines the <see cref="Tag"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<object> TagProperty = public static readonly StyledProperty<object?> TagProperty =
AvaloniaProperty.Register<Control, object>(nameof(Tag)); AvaloniaProperty.Register<Control, object?>(nameof(Tag));
/// <summary> /// <summary>
/// Defines the <see cref="ContextMenu"/> property. /// Defines the <see cref="ContextMenu"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<ContextMenu> ContextMenuProperty = public static readonly StyledProperty<ContextMenu?> ContextMenuProperty =
AvaloniaProperty.Register<Control, ContextMenu>(nameof(ContextMenu)); AvaloniaProperty.Register<Control, ContextMenu?>(nameof(ContextMenu));
/// <summary> /// <summary>
/// Event raised when an element wishes to be scrolled into view. /// Event raised when an element wishes to be scrolled into view.
@ -44,16 +46,16 @@ namespace Avalonia.Controls
public static readonly RoutedEvent<RequestBringIntoViewEventArgs> RequestBringIntoViewEvent = public static readonly RoutedEvent<RequestBringIntoViewEventArgs> RequestBringIntoViewEvent =
RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>("RequestBringIntoView", RoutingStrategies.Bubble); RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>("RequestBringIntoView", RoutingStrategies.Bubble);
private DataTemplates _dataTemplates; private DataTemplates? _dataTemplates;
private IControl _focusAdorner; private IControl? _focusAdorner;
/// <summary> /// <summary>
/// Gets or sets the control's focus adorner. /// Gets or sets the control's focus adorner.
/// </summary> /// </summary>
public ITemplate<IControl> FocusAdorner public ITemplate<IControl>? FocusAdorner
{ {
get { return GetValue(FocusAdornerProperty); } get => GetValue(FocusAdornerProperty);
set { SetValue(FocusAdornerProperty, value); } set => SetValue(FocusAdornerProperty, value);
} }
/// <summary> /// <summary>
@ -63,27 +65,27 @@ namespace Avalonia.Controls
/// Each control may define data templates which are applied to the control itself and its /// Each control may define data templates which are applied to the control itself and its
/// children. /// children.
/// </remarks> /// </remarks>
public DataTemplates DataTemplates => _dataTemplates ?? (_dataTemplates = new DataTemplates()); public DataTemplates DataTemplates => _dataTemplates ??= new DataTemplates();
/// <summary> /// <summary>
/// Gets or sets a context menu to the control. /// Gets or sets a context menu to the control.
/// </summary> /// </summary>
public ContextMenu ContextMenu public ContextMenu? ContextMenu
{ {
get { return GetValue(ContextMenuProperty); } get => GetValue(ContextMenuProperty);
set { SetValue(ContextMenuProperty, value); } set => SetValue(ContextMenuProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets a user-defined object attached to the control. /// Gets or sets a user-defined object attached to the control.
/// </summary> /// </summary>
public object Tag public object? Tag
{ {
get { return GetValue(TagProperty); } get => GetValue(TagProperty);
set { SetValue(TagProperty, value); } set => SetValue(TagProperty, value);
} }
public new IControl Parent => (IControl)base.Parent; public new IControl? Parent => (IControl?)base.Parent;
/// <inheritdoc/> /// <inheritdoc/>
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
@ -106,15 +108,10 @@ namespace Avalonia.Controls
{ {
var c = i as IControl; var c = i as IControl;
if (c?.IsInitialized == false) if (c?.IsInitialized == false && c is ISupportInitialize init)
{ {
var init = c as ISupportInitialize; init.BeginInit();
init.EndInit();
if (init != null)
{
init.BeginInit();
init.EndInit();
}
} }
} }
} }
@ -131,10 +128,7 @@ namespace Avalonia.Controls
/// Gets the element that receives the focus adorner. /// Gets the element that receives the focus adorner.
/// </summary> /// </summary>
/// <returns>The control that receives the focus adorner.</returns> /// <returns>The control that receives the focus adorner.</returns>
protected virtual IControl GetTemplateFocusTarget() protected virtual IControl? GetTemplateFocusTarget() => this;
{
return this;
}
/// <inheritdoc/> /// <inheritdoc/>
protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
@ -173,15 +167,10 @@ namespace Avalonia.Controls
} }
} }
if (_focusAdorner != null) if (_focusAdorner != null && GetTemplateFocusTarget() is Visual target)
{ {
var target = (Visual)GetTemplateFocusTarget(); AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target);
adornerLayer.Children.Add(_focusAdorner);
if (target != null)
{
AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target);
adornerLayer.Children.Add(_focusAdorner);
}
} }
} }
} }

77
src/Avalonia.Controls/DefinitionBase.cs

@ -662,31 +662,64 @@ namespace Avalonia.Controls
{ {
DefinitionBase definitionBase = _registry[i]; DefinitionBase definitionBase = _registry[i];
if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated) // we'll set d.UseSharedMinimum to maintain the invariant:
// d.UseSharedMinimum iff d._minSize < this.MinSize
// i.e. iff d is not a "long-pole" definition.
//
// Measure/Arrange of d's Grid uses d._minSize for long-pole
// definitions, and max(d._minSize, shared size) for
// short-pole definitions. This distinction allows us to react
// to changes in "long-pole-ness" more efficiently and correctly,
// by avoiding remeasures when a long-pole definition changes.
bool useSharedMinimum = !MathUtilities.AreClose(definitionBase._minSize, sharedMinSize);
// before doing that, determine whether d's Grid needs to be remeasured.
// It's important _not_ to remeasure if the last measure is still
// valid, otherwise infinite loops are possible
bool measureIsValid;
if(!definitionBase.UseSharedMinimum)
{ {
// if definition's min size is different, then need to re-measure // d was a long-pole. measure is valid iff it's still a long-pole,
if (!MathUtilities.AreClose(sharedMinSize, definitionBase.MinSize)) // since previous measure didn't use shared size.
{ measureIsValid = !useSharedMinimum;
Grid parentGrid = (Grid)definitionBase.Parent; }
parentGrid.InvalidateMeasure(); else if(useSharedMinimum)
definitionBase.UseSharedMinimum = true; {
} // d was a short-pole, and still is. measure is valid
else // iff the shared size didn't change
{ measureIsValid = !sharedMinSizeChanged;
definitionBase.UseSharedMinimum = false; }
else
// if measure is valid then also need to check arrange. {
// Note: definitionBase.SizeCache is volatile but at this point // d was a short-pole, but is now a long-pole. This can
// it contains up-to-date final size // happen in several ways:
if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache)) // a. d's minSize increased to or past the old shared size
{ // b. other long-pole definitions decreased, leaving
Grid parentGrid = (Grid)definitionBase.Parent; // d as the new winner
parentGrid.InvalidateArrange(); // In the former case, the measure is valid - it used
} // d's new larger minSize. In the latter case, the
} // measure is invalid - it used the old shared size,
// which is larger than d's (possibly changed) minSize
measureIsValid = (definitionBase.LayoutWasUpdated &&
MathUtilities.GreaterThanOrClose(definitionBase._minSize, this.MinSize));
}
definitionBase.LayoutWasUpdated = false; if(!measureIsValid)
{
definitionBase.Parent.InvalidateMeasure();
} }
else if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache))
{
// if measure is valid then also need to check arrange.
// Note: definitionBase.SizeCache is volatile but at this point
// it contains up-to-date final size
definitionBase.Parent.InvalidateArrange();
}
// now we can restore the invariant, and clear the layout flag
definitionBase.UseSharedMinimum = useSharedMinimum;
definitionBase.LayoutWasUpdated = false;
} }
_minSize = sharedMinSize; _minSize = sharedMinSize;

55
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

@ -1,4 +1,10 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Generators namespace Avalonia.Controls.Generators
{ {
@ -16,11 +22,15 @@ namespace Avalonia.Controls.Generators
{ {
var tabItem = (TabItem)base.CreateContainer(item); var tabItem = (TabItem)base.CreateContainer(item);
tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty]; tabItem.Bind(TabItem.TabStripPlacementProperty, new OwnerBinding<Dock>(
tabItem,
TabControl.TabStripPlacementProperty));
if (tabItem.HeaderTemplate == null) if (tabItem.HeaderTemplate == null)
{ {
tabItem[~HeaderedContentControl.HeaderTemplateProperty] = Owner[~ItemsControl.ItemTemplateProperty]; tabItem.Bind(TabItem.HeaderTemplateProperty, new OwnerBinding<IDataTemplate>(
tabItem,
TabControl.ItemTemplateProperty));
} }
if (tabItem.Header == null) if (tabItem.Header == null)
@ -40,10 +50,49 @@ namespace Avalonia.Controls.Generators
if (!(tabItem.Content is IControl)) if (!(tabItem.Content is IControl))
{ {
tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty]; tabItem.Bind(TabItem.ContentTemplateProperty, new OwnerBinding<IDataTemplate>(
tabItem,
TabControl.ContentTemplateProperty));
} }
return tabItem; return tabItem;
} }
private class OwnerBinding<T> : SingleSubscriberObservableBase<T>
{
private readonly TabItem _item;
private readonly StyledProperty<T> _ownerProperty;
private IDisposable _ownerSubscription;
private IDisposable _propertySubscription;
public OwnerBinding(TabItem item, StyledProperty<T> ownerProperty)
{
_item = item;
_ownerProperty = ownerProperty;
}
protected override void Subscribed()
{
_ownerSubscription = ControlLocator.Track(_item, 0, typeof(TabControl)).Subscribe(OwnerChanged);
}
protected override void Unsubscribed()
{
_ownerSubscription?.Dispose();
_ownerSubscription = null;
}
private void OwnerChanged(ILogical c)
{
_propertySubscription?.Dispose();
_propertySubscription = null;
if (c is TabControl tabControl)
{
_propertySubscription = tabControl.GetObservable(_ownerProperty)
.Subscribe(x => PublishNext(x));
}
}
}
} }
} }

6
src/Avalonia.Controls/NativeControlHost.cs

@ -157,10 +157,14 @@ namespace Avalonia.Controls
var needsShow = IsEffectivelyVisible && bounds.HasValue; var needsShow = IsEffectivelyVisible && bounds.HasValue;
if (needsShow) if (needsShow)
{
if (bounds.Value.IsEmpty)
return false;
_attachment?.ShowInBounds(bounds.Value); _attachment?.ShowInBounds(bounds.Value);
}
else else
_attachment?.HideWithSize(Bounds.Size); _attachment?.HideWithSize(Bounds.Size);
return false; return true;
} }
private void CheckDestruction() private void CheckDestruction()

17
src/Avalonia.Controls/Notifications/NotificationCard.cs

@ -16,6 +16,11 @@ namespace Avalonia.Controls.Notifications
private bool _isClosed; private bool _isClosed;
private bool _isClosing; private bool _isClosing;
static NotificationCard()
{
CloseOnClickProperty.Changed.AddClassHandler<Button>(OnCloseOnClickPropertyChanged);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="NotificationCard"/> class. /// Initializes a new instance of the <see cref="NotificationCard"/> class.
/// </summary> /// </summary>
@ -105,22 +110,26 @@ namespace Avalonia.Controls.Notifications
public static bool GetCloseOnClick(Button obj) public static bool GetCloseOnClick(Button obj)
{ {
Contract.Requires<ArgumentNullException>(obj != null);
return (bool)obj.GetValue(CloseOnClickProperty); return (bool)obj.GetValue(CloseOnClickProperty);
} }
public static void SetCloseOnClick(Button obj, bool value) public static void SetCloseOnClick(Button obj, bool value)
{ {
Contract.Requires<ArgumentNullException>(obj != null);
obj.SetValue(CloseOnClickProperty, value); obj.SetValue(CloseOnClickProperty, value);
} }
/// <summary> /// <summary>
/// Defines the CloseOnClick property. /// Defines the CloseOnClick property.
/// </summary> /// </summary>
public static readonly AvaloniaProperty CloseOnClickProperty = public static readonly AttachedProperty<bool> CloseOnClickProperty =
AvaloniaProperty.RegisterAttached<Button, bool>("CloseOnClick", typeof(NotificationCard)/*, validate: CloseOnClickChanged*/); AvaloniaProperty.RegisterAttached<NotificationCard, Button, bool>("CloseOnClick", defaultValue: false);
private static bool CloseOnClickChanged(Button button, bool value) private static void OnCloseOnClickPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
{ {
var button = (Button)d;
var value = (bool)e.NewValue;
if (value) if (value)
{ {
button.Click += Button_Click; button.Click += Button_Click;
@ -129,8 +138,6 @@ namespace Avalonia.Controls.Notifications
{ {
button.Click -= Button_Click; button.Click -= Button_Click;
} }
return true;
} }
/// <summary> /// <summary>

18
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -91,14 +91,14 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public static readonly DirectProperty<NumericUpDown, string> TextProperty = public static readonly DirectProperty<NumericUpDown, string> TextProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v, AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary> /// <summary>
/// Defines the <see cref="Value"/> property. /// Defines the <see cref="Value"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<NumericUpDown, double> ValueProperty = public static readonly DirectProperty<NumericUpDown, double> ValueProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value, AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value,
(updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay); (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary> /// <summary>
/// Defines the <see cref="Watermark"/> property. /// Defines the <see cref="Watermark"/> property.
@ -370,6 +370,20 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The new binding value for the property.</param>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == TextProperty || property == ValueProperty)
{
DataValidationErrors.SetError(this, value.Error);
}
}
/// <summary> /// <summary>
/// Called when the <see cref="CultureInfo"/> property value changed. /// Called when the <see cref="CultureInfo"/> property value changed.
/// </summary> /// </summary>

7
src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs

@ -0,0 +1,7 @@
namespace Avalonia.Platform
{
public interface IApplicationPlatformEvents
{
void RaiseUrlsOpened(string[] urls);
}
}

4
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -80,7 +80,9 @@ namespace Avalonia.Controls.Presenters
static TextPresenter() static TextPresenter()
{ {
AffectsRender<TextPresenter>(SelectionBrushProperty, TextBlock.ForegroundProperty, AffectsRender<TextPresenter>(SelectionBrushProperty, TextBlock.ForegroundProperty,
SelectionForegroundBrushProperty, CaretBrushProperty); SelectionForegroundBrushProperty, CaretBrushProperty,
SelectionStartProperty, SelectionEndProperty);
AffectsMeasure<TextPresenter>(TextProperty, PasswordCharProperty, RevealPasswordProperty, AffectsMeasure<TextPresenter>(TextProperty, PasswordCharProperty, RevealPasswordProperty,
TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty, TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty); TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);

47
src/Avalonia.Controls/Slider.cs

@ -49,6 +49,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Orientation> OrientationProperty = public static readonly StyledProperty<Orientation> OrientationProperty =
ScrollBar.OrientationProperty.AddOwner<Slider>(); ScrollBar.OrientationProperty.AddOwner<Slider>();
/// <summary>
/// Defines the <see cref="IsDirectionReversed"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsDirectionReversedProperty =
Track.IsDirectionReversedProperty.AddOwner<Slider>();
/// <summary> /// <summary>
/// Defines the <see cref="IsSnapToTickEnabled"/> property. /// Defines the <see cref="IsSnapToTickEnabled"/> property.
/// </summary> /// </summary>
@ -83,7 +89,6 @@ namespace Avalonia.Controls
private IDisposable _increaseButtonSubscription; private IDisposable _increaseButtonSubscription;
private IDisposable _increaseButtonReleaseDispose; private IDisposable _increaseButtonReleaseDispose;
private IDisposable _pointerMovedDispose; private IDisposable _pointerMovedDispose;
private IDisposable _trackOnKeyDownDispose;
private const double Tolerance = 0.0001; private const double Tolerance = 0.0001;
@ -93,6 +98,7 @@ namespace Avalonia.Controls
static Slider() static Slider()
{ {
PressedMixin.Attach<Slider>(); PressedMixin.Attach<Slider>();
FocusableProperty.OverrideDefaultValue<Slider>(true);
OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal); OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal);
Thumb.DragStartedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble); Thumb.DragStartedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble);
Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e), Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e),
@ -127,6 +133,19 @@ namespace Avalonia.Controls
set { SetValue(OrientationProperty, value); } set { SetValue(OrientationProperty, value); }
} }
/// <summary>
/// Gets or sets the direction of increasing value.
/// </summary>
/// <value>
/// true if the direction of increasing value is to the left for a horizontal slider or
/// down for a vertical slider; otherwise, false. The default is false.
/// </value>
public bool IsDirectionReversed
{
get { return GetValue(IsDirectionReversedProperty); }
set { SetValue(IsDirectionReversedProperty, value); }
}
/// <summary> /// <summary>
/// Gets or sets a value that indicates whether the <see cref="Slider"/> automatically moves the <see cref="Thumb"/> to the closest tick mark. /// Gets or sets a value that indicates whether the <see cref="Slider"/> automatically moves the <see cref="Thumb"/> to the closest tick mark.
/// </summary> /// </summary>
@ -165,7 +184,6 @@ namespace Avalonia.Controls
_increaseButtonSubscription?.Dispose(); _increaseButtonSubscription?.Dispose();
_increaseButtonReleaseDispose?.Dispose(); _increaseButtonReleaseDispose?.Dispose();
_pointerMovedDispose?.Dispose(); _pointerMovedDispose?.Dispose();
_trackOnKeyDownDispose?.Dispose();
_decreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton"); _decreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton");
_track = e.NameScope.Find<Track>("PART_Track"); _track = e.NameScope.Find<Track>("PART_Track");
@ -174,7 +192,6 @@ namespace Avalonia.Controls
if (_track != null) if (_track != null)
{ {
_track.IsThumbDragHandled = true; _track.IsThumbDragHandled = true;
_trackOnKeyDownDispose = _track.AddDisposableHandler(KeyDownEvent, TrackOnKeyDown);
} }
if (_decreaseButton != null) if (_decreaseButton != null)
@ -192,26 +209,32 @@ namespace Avalonia.Controls
_pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel); _pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel);
} }
private void TrackOnKeyDown(object sender, KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)
{ {
if (e.KeyModifiers != KeyModifiers.None) return; base.OnKeyDown(e);
if (e.Handled || e.KeyModifiers != KeyModifiers.None) return;
var handled = true;
switch (e.Key) switch (e.Key)
{ {
case Key.Down:
case Key.Left: case Key.Left:
MoveToNextTick(-SmallChange); MoveToNextTick(IsDirectionReversed ? SmallChange : -SmallChange);
break; break;
case Key.Up:
case Key.Right: case Key.Right:
MoveToNextTick(SmallChange); MoveToNextTick(IsDirectionReversed ? -SmallChange : SmallChange);
break; break;
case Key.PageUp: case Key.PageUp:
MoveToNextTick(-LargeChange); MoveToNextTick(IsDirectionReversed ? -LargeChange : LargeChange);
break; break;
case Key.PageDown: case Key.PageDown:
MoveToNextTick(LargeChange); MoveToNextTick(IsDirectionReversed ? LargeChange : -LargeChange);
break; break;
case Key.Home: case Key.Home:
@ -221,7 +244,13 @@ namespace Avalonia.Controls
case Key.End: case Key.End:
Value = Maximum; Value = Maximum;
break; break;
default:
handled = false;
break;
} }
e.Handled = handled;
} }
private void MoveToNextTick(double direction) private void MoveToNextTick(double direction)

43
src/Avalonia.Controls/TextBox.cs

@ -514,21 +514,36 @@ namespace Avalonia.Controls
private void HandleTextInput(string input) private void HandleTextInput(string input)
{ {
if (!IsReadOnly) if (IsReadOnly)
{ {
input = RemoveInvalidCharacters(input); return;
string text = Text ?? string.Empty; }
int caretIndex = CaretIndex;
if (!string.IsNullOrEmpty(input) && (MaxLength == 0 || input.Length + text.Length - (Math.Abs(SelectionStart - SelectionEnd)) <= MaxLength)) input = RemoveInvalidCharacters(input);
{
DeleteSelection(); if (string.IsNullOrEmpty(input))
caretIndex = CaretIndex; {
text = Text ?? string.Empty; return;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); }
CaretIndex += input.Length;
ClearSelection(); string text = Text ?? string.Empty;
_undoRedoHelper.DiscardRedo(); int caretIndex = CaretIndex;
} int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
if (MaxLength > 0 && newLength > MaxLength)
{
input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength)));
}
if (!string.IsNullOrEmpty(input))
{
DeleteSelection();
caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection();
_undoRedoHelper.DiscardRedo();
} }
} }

14
src/Avalonia.Controls/UrlOpenedEventArgs.cs

@ -0,0 +1,14 @@
using System;
namespace Avalonia
{
public class UrlOpenedEventArgs : EventArgs
{
public UrlOpenedEventArgs(string[] urls)
{
Urls = urls;
}
public string[] Urls { get; }
}
}

45
src/Avalonia.Controls/Window.cs

@ -4,10 +4,7 @@ using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls.Chrome;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
@ -482,10 +479,9 @@ namespace Avalonia.Controls
try try
{ {
if (!ignoreCancel && HandleClosing()) if (!ignoreCancel && ShouldCancelClose())
{ {
close = false; close = false;
return;
} }
} }
finally finally
@ -497,11 +493,25 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Handles a closing notification from <see cref="IWindowImpl.Closing"/>.
/// <returns>true if closing is cancelled. Otherwise false.</returns>
/// </summary>
protected virtual bool HandleClosing()
{
if (!ShouldCancelClose())
{
CloseInternal();
return false;
}
return true;
}
private void CloseInternal() private void CloseInternal()
{ {
foreach (var (child, _) in _children.ToList()) foreach (var (child, _) in _children.ToList())
{ {
// if we HandleClosing() before then there will be no children.
child.CloseInternal(); child.CloseInternal();
} }
@ -515,20 +525,18 @@ namespace Avalonia.Controls
PlatformImpl?.Dispose(); PlatformImpl?.Dispose();
} }
/// <summary> private bool ShouldCancelClose(CancelEventArgs args = null)
/// Handles a closing notification from <see cref="IWindowImpl.Closing"/>.
/// </summary>
protected virtual bool HandleClosing()
{ {
if (args is null)
{
args = new CancelEventArgs();
}
bool canClose = true; bool canClose = true;
foreach (var (child, _) in _children.ToList()) foreach (var (child, _) in _children.ToList())
{ {
if (!child.HandleClosing()) if (child.ShouldCancelClose(args))
{
child.CloseInternal();
}
else
{ {
canClose = false; canClose = false;
} }
@ -536,15 +544,12 @@ namespace Avalonia.Controls
if (canClose) if (canClose)
{ {
var args = new CancelEventArgs();
OnClosing(args); OnClosing(args);
return args.Cancel; return args.Cancel;
} }
else
{ return true;
return !canClose;
}
} }
protected virtual void HandleWindowStateChanged(WindowState state) protected virtual void HandleWindowStateChanged(WindowState state)

21
src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs

@ -0,0 +1,21 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace Avalonia.Diagnostics.Converters
{
internal class BoolToOpacityConverter : IValueConverter
{
public double Opacity { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? 1d : Opacity;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

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

@ -1,8 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reflection;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
@ -12,6 +19,10 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly IVisual _control; private readonly IVisual _control;
private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex; private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
private AvaloniaPropertyViewModel _selectedProperty; private AvaloniaPropertyViewModel _selectedProperty;
private string _styleFilter;
private bool _snapshotStyles;
private bool _showInactiveStyles;
private string _styleStatus;
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
{ {
@ -43,20 +54,160 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
ao.PropertyChanged += ControlPropertyChanged; ao.PropertyChanged += ControlPropertyChanged;
} }
AppliedStyles = new ObservableCollection<StyleViewModel>();
PseudoClasses = new ObservableCollection<PseudoClassViewModel>();
if (control is StyledElement styledElement)
{
styledElement.Classes.CollectionChanged += OnClassesChanged;
var pseudoClassAttributes = styledElement.GetType().GetCustomAttributes<PseudoClassesAttribute>(true);
foreach (var classAttribute in pseudoClassAttributes)
{
foreach (var className in classAttribute.PseudoClasses)
{
PseudoClasses.Add(new PseudoClassViewModel(className, styledElement));
}
}
var styleDiagnostics = styledElement.GetStyleDiagnostics();
foreach (var appliedStyle in styleDiagnostics.AppliedStyles)
{
var styleSource = appliedStyle.Source;
var setters = new List<SetterViewModel>();
if (styleSource is Style style)
{
foreach (var setter in style.Setters)
{
if (setter is Setter regularSetter)
{
var setterValue = regularSetter.Value;
var resourceInfo = GetResourceInfo(setterValue);
SetterViewModel setterVm;
if (resourceInfo.HasValue)
{
var resourceKey = resourceInfo.Value.resourceKey;
var resourceValue = styledElement.FindResource(resourceKey);
setterVm = new ResourceSetterViewModel(regularSetter.Property, resourceKey, resourceValue, resourceInfo.Value.isDynamic);
}
else
{
setterVm = new SetterViewModel(regularSetter.Property, setterValue);
}
setters.Add(setterVm);
}
}
AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters));
}
}
UpdateStyles();
}
}
private (object resourceKey, bool isDynamic)? GetResourceInfo(object value)
{
if (value is StaticResourceExtension staticResource)
{
return (staticResource.ResourceKey, false);
}
else if (value is DynamicResourceExtension dynamicResource)
{
return (dynamicResource.ResourceKey, true);
}
return null;
} }
public TreePageViewModel TreePage { get; } public TreePageViewModel TreePage { get; }
public DataGridCollectionView PropertiesView { get; } public DataGridCollectionView PropertiesView { get; }
public ObservableCollection<StyleViewModel> AppliedStyles { get; }
public ObservableCollection<PseudoClassViewModel> PseudoClasses { get; }
public AvaloniaPropertyViewModel SelectedProperty public AvaloniaPropertyViewModel SelectedProperty
{ {
get => _selectedProperty; get => _selectedProperty;
set => RaiseAndSetIfChanged(ref _selectedProperty, value); set => RaiseAndSetIfChanged(ref _selectedProperty, value);
} }
public string StyleFilter
{
get => _styleFilter;
set => RaiseAndSetIfChanged(ref _styleFilter, value);
}
public bool SnapshotStyles
{
get => _snapshotStyles;
set => RaiseAndSetIfChanged(ref _snapshotStyles, value);
}
public bool ShowInactiveStyles
{
get => _showInactiveStyles;
set => RaiseAndSetIfChanged(ref _showInactiveStyles, value);
}
public string StyleStatus
{
get => _styleStatus;
set => RaiseAndSetIfChanged(ref _styleStatus, value);
}
public ControlLayoutViewModel Layout { get; } public ControlLayoutViewModel Layout { get; }
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.PropertyName == nameof(StyleFilter))
{
UpdateStyleFilters();
}
else if (e.PropertyName == nameof(SnapshotStyles))
{
if (!SnapshotStyles)
{
UpdateStyles();
}
}
}
private void UpdateStyleFilters()
{
var filter = StyleFilter;
bool hasFilter = !string.IsNullOrEmpty(filter);
foreach (var style in AppliedStyles)
{
var hasVisibleSetter = false;
foreach (var setter in style.Setters)
{
setter.IsVisible =
!hasFilter || setter.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
hasVisibleSetter |= setter.IsVisible;
}
style.IsVisible = hasVisibleSetter;
}
}
public void Dispose() public void Dispose()
{ {
if (_control is INotifyPropertyChanged inpc) if (_control is INotifyPropertyChanged inpc)
@ -68,6 +219,11 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
ao.PropertyChanged -= ControlPropertyChanged; ao.PropertyChanged -= ControlPropertyChanged;
} }
if (_control is StyledElement se)
{
se.Classes.CollectionChanged -= OnClassesChanged;
}
} }
private IEnumerable<PropertyViewModel> GetAvaloniaProperties(object o) private IEnumerable<PropertyViewModel> GetAvaloniaProperties(object o)
@ -129,6 +285,74 @@ namespace Avalonia.Diagnostics.ViewModels
property.Update(); property.Update();
} }
} }
if (!SnapshotStyles)
{
UpdateStyles();
}
}
private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (!SnapshotStyles)
{
UpdateStyles();
}
}
private void UpdateStyles()
{
int activeCount = 0;
foreach (var style in AppliedStyles)
{
style.Update();
if (style.IsActive)
{
activeCount++;
}
}
var propertyBuckets = new Dictionary<AvaloniaProperty, List<SetterViewModel>>();
foreach (var style in AppliedStyles)
{
if (!style.IsActive)
{
continue;
}
foreach (var setter in style.Setters)
{
if (propertyBuckets.TryGetValue(setter.Property, out var setters))
{
foreach (var otherSetter in setters)
{
otherSetter.IsActive = false;
}
setter.IsActive = true;
setters.Add(setter);
}
else
{
setter.IsActive = true;
setters = new List<SetterViewModel> { setter };
propertyBuckets.Add(setter.Property, setters);
}
}
}
foreach (var pseudoClass in PseudoClasses)
{
pseudoClass.Update();
}
StyleStatus = $"Styles ({activeCount}/{AppliedStyles.Count} active)";
} }
private bool FilterProperty(object arg) private bool FilterProperty(object arg)

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

@ -163,6 +163,14 @@ namespace Avalonia.Diagnostics.ViewModels
tree?.SelectControl(control); tree?.SelectControl(control);
} }
public void EnableSnapshotStyles(bool enable)
{
if (Content is TreePageViewModel treeVm && treeVm.Details != null)
{
treeVm.Details.SnapshotStyles = enable;
}
}
public void Dispose() public void Dispose()
{ {
KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged; KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged;

51
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs

@ -0,0 +1,51 @@
using Avalonia.Controls;
namespace Avalonia.Diagnostics.ViewModels
{
internal class PseudoClassViewModel : ViewModelBase
{
private readonly IPseudoClasses _pseudoClasses;
private readonly StyledElement _source;
private bool _isActive;
private bool _isUpdating;
public PseudoClassViewModel(string name, StyledElement source)
{
Name = name;
_source = source;
_pseudoClasses = _source.Classes;
Update();
}
public string Name { get; }
public bool IsActive
{
get => _isActive;
set
{
RaiseAndSetIfChanged(ref _isActive, value);
if (!_isUpdating)
{
_pseudoClasses.Set(Name, value);
}
}
}
public void Update()
{
try
{
_isUpdating = true;
IsActive = _source.Classes.Contains(Name);
}
finally
{
_isUpdating = false;
}
}
}
}

27
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs

@ -0,0 +1,27 @@
using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ResourceSetterViewModel : SetterViewModel
{
public object Key { get; }
public IBrush Tint { get; }
public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object resourceValue, bool isDynamic) : base(property, resourceValue)
{
Key = resourceKey;
Tint = isDynamic ? Brushes.Orange : Brushes.Brown;
}
public void CopyResourceKey()
{
if (Key is null)
{
return;
}
CopyToClipboard(Key.ToString());
}
}
}

59
src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs

@ -0,0 +1,59 @@
using Avalonia.Input.Platform;
namespace Avalonia.Diagnostics.ViewModels
{
internal class SetterViewModel : ViewModelBase
{
private bool _isActive;
private bool _isVisible;
public AvaloniaProperty Property { get; }
public string Name { get; }
public object Value { get; }
public bool IsActive
{
get => _isActive;
set => RaiseAndSetIfChanged(ref _isActive, value);
}
public bool IsVisible
{
get => _isVisible;
set => RaiseAndSetIfChanged(ref _isVisible, value);
}
public SetterViewModel(AvaloniaProperty property, object value)
{
Property = property;
Name = property.Name;
Value = value;
IsActive = true;
IsVisible = true;
}
public void CopyValue()
{
if (Value is null)
{
return;
}
CopyToClipboard(Value.ToString());
}
public void CopyPropertyName()
{
CopyToClipboard(Property.Name);
}
protected static void CopyToClipboard(string value)
{
var clipboard = AvaloniaLocator.Current.GetService<IClipboard>();
clipboard?.SetTextAsync(value);
}
}
}

43
src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs

@ -0,0 +1,43 @@
using System.Collections.Generic;
using Avalonia.Styling;
namespace Avalonia.Diagnostics.ViewModels
{
internal class StyleViewModel : ViewModelBase
{
private readonly IStyleInstance _styleInstance;
private bool _isActive;
private bool _isVisible;
public StyleViewModel(IStyleInstance styleInstance, string name, List<SetterViewModel> setters)
{
_styleInstance = styleInstance;
IsVisible = true;
Name = name;
Setters = setters;
Update();
}
public bool IsActive
{
get => _isActive;
set => RaiseAndSetIfChanged(ref _isActive, value);
}
public bool IsVisible
{
get => _isVisible;
set => RaiseAndSetIfChanged(ref _isVisible, value);
}
public string Name { get; }
public List<SetterViewModel> Setters { get; }
public void Update()
{
IsActive = _styleInstance.IsActive;
}
}
}

7
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@ -31,11 +31,18 @@ namespace Avalonia.Diagnostics.ViewModels
get => _selectedNode; get => _selectedNode;
private set private set
{ {
var oldDetails = Details;
if (RaiseAndSetIfChanged(ref _selectedNode, value)) if (RaiseAndSetIfChanged(ref _selectedNode, value))
{ {
Details = value != null ? Details = value != null ?
new ControlDetailsViewModel(this, value.Visual) : new ControlDetailsViewModel(this, value.Visual) :
null; null;
if (Details != null && oldDetails != null)
{
Details.StyleFilter = oldDetails.StyleFilter;
}
} }
} }
} }

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

@ -2,7 +2,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters" xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
xmlns:local="clr-namespace:Avalonia.Diagnostics.Views" xmlns:local="clr-namespace:Avalonia.Diagnostics.Views"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"> xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
x:Name="Main">
<UserControl.Resources> <UserControl.Resources>
<SolidColorBrush x:Key="ThicknessBorderBrush" Color="#666666" /> <SolidColorBrush x:Key="ThicknessBorderBrush" Color="#666666" />
@ -11,6 +13,7 @@
<SolidColorBrush x:Key="BorderBackgroundBrush" Color="#E3C381" /> <SolidColorBrush x:Key="BorderBackgroundBrush" Color="#E3C381" />
<SolidColorBrush x:Key="PaddingBackgroundBrush" Color="#B8C47F" /> <SolidColorBrush x:Key="PaddingBackgroundBrush" Color="#B8C47F" />
<SolidColorBrush x:Key="SizeBackgroundBrush" Color="#88B2BD" /> <SolidColorBrush x:Key="SizeBackgroundBrush" Color="#88B2BD" />
<conv:BoolToOpacityConverter x:Key="BoolToOpacity" Opacity="0.6"/>
</UserControl.Resources> </UserControl.Resources>
<UserControl.Styles> <UserControl.Styles>
@ -105,7 +108,7 @@
<GridSplitter Grid.Column="1" /> <GridSplitter Grid.Column="1" />
<Grid Grid.Column="2" RowDefinitions="Auto,*" > <Grid Grid.Column="2" RowDefinitions="Auto,*, Auto,*,Auto" >
<TextBlock Grid.Row="0" Text="Layout Visualizer" Margin="4" /> <TextBlock Grid.Row="0" Text="Layout Visualizer" Margin="4" />
<Grid Grid.Row="1" x:Name="LayoutRoot" Margin="8,0,8,8" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,Auto"> <Grid Grid.Row="1" x:Name="LayoutRoot" Margin="8,0,8,8" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,Auto">
@ -148,7 +151,122 @@
<Rectangle x:Name="VerticalSizeEnd" /> <Rectangle x:Name="VerticalSizeEnd" />
</Canvas> </Canvas>
</Grid> </Grid>
<Grid Grid.Row="2" Margin="4" RowDefinitions="Auto,Auto">
<Grid Grid.Row="0" Margin="2" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock FontWeight="Bold" Grid.Column="0" Text="{Binding StyleStatus}" VerticalAlignment="Center" />
<CheckBox Margin="2,0,0,0" Grid.Column="2" Content="Show inactive" IsChecked="{Binding ShowInactiveStyles}" ToolTip.Tip="Show styles that are currently inactive" />
<ToggleButton Margin="2,0,0,0" Grid.Column="3" ToolTip.Tip="Snapshot current styles (Alt+S/Alt+D to enable/disable within debugged window)" Content="Snapshot" IsChecked="{Binding SnapshotStyles}" />
</Grid>
<TextBox Grid.Row="1" Margin="2" Grid.Column="0" Watermark="Filter" Text="{Binding StyleFilter}" />
</Grid>
<ScrollViewer Grid.Row="3" HorizontalScrollBarVisibility="Disabled">
<ItemsControl Items="{Binding AppliedStyles}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderThickness="0,0,0,1" BorderBrush="#6C6C6C" Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}">
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<MultiBinding Converter="{x:Static BoolConverters.Or}" >
<Binding Path="IsActive" />
<Binding Path="#Main.DataContext.ShowInactiveStyles" />
</MultiBinding>
<Binding Path="IsVisible" />
</MultiBinding>
</Border.IsVisible>
<Expander IsExpanded="True" Margin="0" Padding="8,0" ContentTransition="{x:Null}" >
<Expander.Header>
<TextBlock Grid.Row="0" Text="{Binding Name}" />
</Expander.Header>
<ItemsControl Margin="20,0,0,0" Grid.Row="1" Items="{Binding Setters}">
<ItemsControl.DataTemplates>
<DataTemplate DataType="IBrush">
<StackPanel Orientation="Horizontal" Spacing="2">
<Border BorderThickness="1" BorderBrush="Black" Background="{Binding}" Width="8" Height="8"/>
<TextBlock Text="{Binding}" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="Color">
<StackPanel Orientation="Horizontal" Spacing="2">
<Border BorderThickness="1" BorderBrush="Black" Width="8" Height="8">
<Border.Background>
<SolidColorBrush Color="{Binding}" />
</Border.Background>
</Border>
<TextBlock Text="{Binding}" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="vm:ResourceSetterViewModel">
<Panel Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}" IsVisible="{Binding IsVisible}" HorizontalAlignment="Left">
<Panel.ContextMenu>
<ContextMenu>
<MenuItem Header="Copy property name" Command="{Binding CopyPropertyName} "/>
<MenuItem Header="Copy value" Command="{Binding CopyValue} "/>
<MenuItem Header="Copy resource key" Command="{Binding CopyResourceKey}" />
</ContextMenu>
</Panel.ContextMenu>
<StackPanel Orientation="Horizontal" Spacing="2" HorizontalAlignment="Left">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" />
<TextBlock Text=":" />
<ContentControl Content="{Binding Value}"/>
<TextBlock>(</TextBlock>
<Ellipse Height="8" Width="8" VerticalAlignment="Center" Fill="{Binding Tint}"/>
<TextBlock FontStyle="Italic" Text="{Binding Key}" />
<TextBlock>)</TextBlock>
</StackPanel>
<Rectangle Height="1" Fill="#6C6C6C" IsVisible="{Binding !IsActive}" />
</Panel>
</DataTemplate>
<DataTemplate DataType="vm:SetterViewModel">
<Panel Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}" IsVisible="{Binding IsVisible}" HorizontalAlignment="Left">
<Panel.ContextMenu>
<ContextMenu>
<MenuItem Header="Copy property name" Command="{Binding CopyPropertyName} "/>
<MenuItem Header="Copy value" Command="{Binding CopyValue} "/>
</ContextMenu>
</Panel.ContextMenu>
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" />
<TextBlock Text=":" />
<ContentControl Content="{Binding Value}"/>
</StackPanel>
<Rectangle Height="1" Fill="#6C6C6C" VerticalAlignment="Center" IsVisible="{Binding !IsActive}" />
</Panel>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</Expander>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Expander Header="Pseudo Classes" Grid.Row="4">
<ItemsControl Items="{Binding PseudoClasses}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Margin="2" Content="{Binding Name}" IsChecked="{Binding IsActive, Mode=TwoWay}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Expander>
</Grid> </Grid>
</Grid> </Grid>

10
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@ -90,6 +90,16 @@ namespace Avalonia.Diagnostics.Views
var vm = (MainViewModel)DataContext; var vm = (MainViewModel)DataContext;
vm.SelectControl((IControl)control); vm.SelectControl((IControl)control);
} }
}
else if (e.Modifiers == RawInputModifiers.Alt)
{
if (e.Key == Key.S || e.Key == Key.D)
{
var enable = e.Key == Key.S;
var vm = (MainViewModel)DataContext;
vm.EnableSnapshotStyles(enable);
}
} }
} }

2
src/Avalonia.Dialogs/ManagedFileChooserSources.cs

@ -67,7 +67,7 @@ namespace Avalonia.Dialogs
{ {
Directory.GetFiles(x.VolumePath); Directory.GetFiles(x.VolumePath);
} }
catch (UnauthorizedAccessException _) catch (Exception _)
{ {
return null; return null;
} }

4
src/Avalonia.Input/FocusManager.cs

@ -75,7 +75,9 @@ namespace Avalonia.Input
// If control is null, set focus to the topmost focus scope. // If control is null, set focus to the topmost focus scope.
foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList()) foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList())
{ {
if (_focusScopes.TryGetValue(scope, out var element) && element != null) if (scope != Scope &&
_focusScopes.TryGetValue(scope, out var element) &&
element != null)
{ {
Focus(element, method); Focus(element, method);
return; return;

2
src/Avalonia.Layout/ElementManager.cs

@ -207,7 +207,7 @@ namespace Avalonia.Layout
} }
} }
public bool IsIndexValidInData(int currentIndex) => currentIndex >= 0 && currentIndex < _context.ItemCount; public bool IsIndexValidInData(int currentIndex) => (uint)currentIndex < _context.ItemCount;
public ILayoutable GetRealizedElement(int dataIndex) public ILayoutable GetRealizedElement(int dataIndex)
{ {

2
src/Avalonia.Layout/UniformGridLayout.cs

@ -447,7 +447,7 @@ namespace Avalonia.Layout
// and only use the layout when to clear it when it's done. // and only use the layout when to clear it when it's done.
gridState.EnsureFirstElementOwnership(context); gridState.EnsureFirstElementOwnership(context);
return new Size(desiredSize.Width, desiredSize.Height); return desiredSize;
} }
protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)

10
src/Avalonia.Layout/UniformGridLayoutState.cs

@ -44,7 +44,7 @@ namespace Avalonia.Layout
Size availableSize, Size availableSize,
VirtualizingLayoutContext context, VirtualizingLayoutContext context,
double layoutItemWidth, double layoutItemWidth,
double LayoutItemHeight, double layoutItemHeight,
UniformGridLayoutItemsStretch stretch, UniformGridLayoutItemsStretch stretch,
Orientation orientation, Orientation orientation,
double minRowSpacing, double minRowSpacing,
@ -63,7 +63,7 @@ namespace Avalonia.Layout
if (realizedElement != null) if (realizedElement != null)
{ {
realizedElement.Measure(availableSize); realizedElement.Measure(availableSize);
SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); SetSize(realizedElement, layoutItemWidth, layoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
_cachedFirstElement = null; _cachedFirstElement = null;
} }
else else
@ -78,7 +78,7 @@ namespace Avalonia.Layout
_cachedFirstElement.Measure(availableSize); _cachedFirstElement.Measure(availableSize);
SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); SetSize(_cachedFirstElement, layoutItemWidth, layoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
// See if we can move ownership to the flow algorithm. If we can, we do not need a local cache. // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache.
bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement); bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement);
@ -93,7 +93,7 @@ namespace Avalonia.Layout
private void SetSize( private void SetSize(
ILayoutable element, ILayoutable element,
double layoutItemWidth, double layoutItemWidth,
double LayoutItemHeight, double layoutItemHeight,
Size availableSize, Size availableSize,
UniformGridLayoutItemsStretch stretch, UniformGridLayoutItemsStretch stretch,
Orientation orientation, Orientation orientation,
@ -107,7 +107,7 @@ namespace Avalonia.Layout
} }
EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth); EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth);
EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight); EffectiveItemHeight = (double.IsNaN(layoutItemHeight) ? element.DesiredSize.Height : layoutItemHeight);
var availableSizeMinor = orientation == Orientation.Horizontal ? availableSize.Width : availableSize.Height; var availableSizeMinor = orientation == Orientation.Horizontal ? availableSize.Width : availableSize.Height;
var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing; var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing;

14
src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs

@ -0,0 +1,14 @@
using System;
using Avalonia.Native.Interop;
using Avalonia.Platform;
namespace Avalonia.Native
{
internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents
{
void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls)
{
((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray());
}
}
}

9
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
@ -9,7 +8,6 @@ using Avalonia.Native.Interop;
using Avalonia.OpenGL; using Avalonia.OpenGL;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Platform.Interop;
namespace Avalonia.Native namespace Avalonia.Native
{ {
@ -86,12 +84,17 @@ namespace Avalonia.Native
void DoInitialize(AvaloniaNativePlatformOptions options) void DoInitialize(AvaloniaNativePlatformOptions options)
{ {
_options = options; _options = options;
_factory.Initialize(new GCHandleDeallocator());
var applicationPlatform = new AvaloniaNativeApplicationPlatform();
_factory.Initialize(new GCHandleDeallocator(), applicationPlatform);
if (_factory.MacOptions != null) if (_factory.MacOptions != null)
{ {
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>(); var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();
_factory.MacOptions.SetShowInDock(macOpts?.ShowInDock != false ? 1 : 0); _factory.MacOptions.SetShowInDock(macOpts?.ShowInDock != false ? 1 : 0);
_factory.MacOptions.SetDisableDefaultApplicationMenuItems(
macOpts?.DisableDefaultApplicationMenuItems == true ? 1 : 0);
} }
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable

2
src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs

@ -38,5 +38,7 @@ namespace Avalonia
public class MacOSPlatformOptions public class MacOSPlatformOptions
{ {
public bool ShowInDock { get; set; } = true; public bool ShowInDock { get; set; } = true;
public bool DisableDefaultApplicationMenuItems { get; set; }
} }
} }

9
src/Avalonia.Native/avn.idl

@ -403,7 +403,7 @@ enum AvnExtendClientAreaChromeHints
[uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]
interface IAvaloniaNativeFactory : IUnknown interface IAvaloniaNativeFactory : IUnknown
{ {
HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator); HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* appCb);
IAvnMacOptions* GetMacOptions(); IAvnMacOptions* GetMacOptions();
HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv); HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv);
HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv); HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv);
@ -528,6 +528,7 @@ interface IAvnMacOptions : IUnknown
{ {
HRESULT SetShowInDock(int show); HRESULT SetShowInDock(int show);
HRESULT SetApplicationTitle(char* utf8string); HRESULT SetApplicationTitle(char* utf8string);
HRESULT SetDisableDefaultApplicationMenuItems(bool enabled);
} }
[uuid(04c1b049-1f43-418a-9159-cae627ec1367)] [uuid(04c1b049-1f43-418a-9159-cae627ec1367)]
@ -727,3 +728,9 @@ interface IAvnNativeControlHostTopLevelAttachment : IUnknown
void HideWithSize(float width, float height); void HideWithSize(float width, float height);
void ReleaseChild(); void ReleaseChild();
} }
[uuid(6575b5af-f27a-4609-866c-f1f014c20f79)]
interface IAvnApplicationEvents : IUnknown
{
void FilesOpened (IAvnStringArray* urls);
}

2
src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs

@ -29,7 +29,7 @@ namespace Avalonia.ReactiveUI
/// <inheritdoc/> /// <inheritdoc/>
public bool ExecuteHook( public bool ExecuteHook(
object source, object target, object? source, object target,
Func<IObservedChange<object, object>[]> getCurrentViewModelProperties, Func<IObservedChange<object, object>[]> getCurrentViewModelProperties,
Func<IObservedChange<object, object>[]> getCurrentViewProperties, Func<IObservedChange<object, object>[]> getCurrentViewProperties,
BindingDirection direction) BindingDirection direction)

2
src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj

@ -3,6 +3,8 @@
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Avalonia.ReactiveUI</PackageId> <PackageId>Avalonia.ReactiveUI</PackageId>
<SignAssembly>false</SignAssembly> <SignAssembly>false</SignAssembly>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" /> <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />

12
src/Avalonia.ReactiveUI/ReactiveUserControl.cs

@ -17,8 +17,8 @@ namespace Avalonia.ReactiveUI
/// <typeparam name="TViewModel">ViewModel type.</typeparam> /// <typeparam name="TViewModel">ViewModel type.</typeparam>
public class ReactiveUserControl<TViewModel> : UserControl, IViewFor<TViewModel> where TViewModel : class public class ReactiveUserControl<TViewModel> : UserControl, IViewFor<TViewModel> where TViewModel : class
{ {
public static readonly StyledProperty<TViewModel> ViewModelProperty = AvaloniaProperty public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty
.Register<ReactiveUserControl<TViewModel>, TViewModel>(nameof(ViewModel)); .Register<ReactiveUserControl<TViewModel>, TViewModel?>(nameof(ViewModel));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ReactiveUserControl{TViewModel}"/> class. /// Initializes a new instance of the <see cref="ReactiveUserControl{TViewModel}"/> class.
@ -34,16 +34,16 @@ namespace Avalonia.ReactiveUI
/// <summary> /// <summary>
/// The ViewModel. /// The ViewModel.
/// </summary> /// </summary>
public TViewModel ViewModel public TViewModel? ViewModel
{ {
get => GetValue(ViewModelProperty); get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value); set => SetValue(ViewModelProperty, value);
} }
object IViewFor.ViewModel object? IViewFor.ViewModel
{ {
get => ViewModel; get => ViewModel;
set => ViewModel = (TViewModel)value; set => ViewModel = (TViewModel?)value;
} }
protected override void OnDataContextChanged(EventArgs e) protected override void OnDataContextChanged(EventArgs e)
@ -51,7 +51,7 @@ namespace Avalonia.ReactiveUI
ViewModel = DataContext as TViewModel; ViewModel = DataContext as TViewModel;
} }
private void OnViewModelChanged(object value) private void OnViewModelChanged(object? value)
{ {
if (value == null) if (value == null)
{ {

14
src/Avalonia.ReactiveUI/ReactiveWindow.cs

@ -17,8 +17,8 @@ namespace Avalonia.ReactiveUI
/// <typeparam name="TViewModel">ViewModel type.</typeparam> /// <typeparam name="TViewModel">ViewModel type.</typeparam>
public class ReactiveWindow<TViewModel> : Window, IViewFor<TViewModel> where TViewModel : class public class ReactiveWindow<TViewModel> : Window, IViewFor<TViewModel> where TViewModel : class
{ {
public static readonly StyledProperty<TViewModel> ViewModelProperty = AvaloniaProperty public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty
.Register<ReactiveWindow<TViewModel>, TViewModel>(nameof(ViewModel)); .Register<ReactiveWindow<TViewModel>, TViewModel?>(nameof(ViewModel));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ReactiveWindow{TViewModel}"/> class. /// Initializes a new instance of the <see cref="ReactiveWindow{TViewModel}"/> class.
@ -35,19 +35,19 @@ namespace Avalonia.ReactiveUI
/// <summary> /// <summary>
/// The ViewModel. /// The ViewModel.
/// </summary> /// </summary>
public TViewModel ViewModel public TViewModel? ViewModel
{ {
get => GetValue(ViewModelProperty); get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value); set => SetValue(ViewModelProperty, value);
} }
object IViewFor.ViewModel object? IViewFor.ViewModel
{ {
get => ViewModel; get => ViewModel;
set => ViewModel = (TViewModel)value; set => ViewModel = (TViewModel?)value;
} }
private void OnDataContextChanged(object value) private void OnDataContextChanged(object? value)
{ {
if (value is TViewModel viewModel) if (value is TViewModel viewModel)
{ {
@ -59,7 +59,7 @@ namespace Avalonia.ReactiveUI
} }
} }
private void OnViewModelChanged(object value) private void OnViewModelChanged(object? value)
{ {
if (value == null) if (value == null)
{ {

16
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@ -55,8 +55,8 @@ namespace Avalonia.ReactiveUI
/// <summary> /// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="Router"/> property. /// <see cref="AvaloniaProperty"/> for the <see cref="Router"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<RoutingState> RouterProperty = public static readonly StyledProperty<RoutingState?> RouterProperty =
AvaloniaProperty.Register<RoutedViewHost, RoutingState>(nameof(Router)); AvaloniaProperty.Register<RoutedViewHost, RoutingState?>(nameof(Router));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class. /// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
@ -67,12 +67,12 @@ namespace Avalonia.ReactiveUI
{ {
var routerRemoved = this var routerRemoved = this
.WhenAnyValue(x => x.Router) .WhenAnyValue(x => x.Router)
.Where(router => router == null) .Where(router => router == null)!
.Cast<object>(); .Cast<object?>();
this.WhenAnyValue(x => x.Router) this.WhenAnyValue(x => x.Router)
.Where(router => router != null) .Where(router => router != null)
.SelectMany(router => router.CurrentViewModel) .SelectMany(router => router!.CurrentViewModel)
.Merge(routerRemoved) .Merge(routerRemoved)
.Subscribe(NavigateToViewModel) .Subscribe(NavigateToViewModel)
.DisposeWith(disposables); .DisposeWith(disposables);
@ -82,7 +82,7 @@ namespace Avalonia.ReactiveUI
/// <summary> /// <summary>
/// Gets or sets the <see cref="RoutingState"/> of the view model stack. /// Gets or sets the <see cref="RoutingState"/> of the view model stack.
/// </summary> /// </summary>
public RoutingState Router public RoutingState? Router
{ {
get => GetValue(RouterProperty); get => GetValue(RouterProperty);
set => SetValue(RouterProperty, value); set => SetValue(RouterProperty, value);
@ -91,13 +91,13 @@ namespace Avalonia.ReactiveUI
/// <summary> /// <summary>
/// Gets or sets the ReactiveUI view locator used by this router. /// Gets or sets the ReactiveUI view locator used by this router.
/// </summary> /// </summary>
public IViewLocator ViewLocator { get; set; } public IViewLocator? ViewLocator { get; set; }
/// <summary> /// <summary>
/// Invoked when ReactiveUI router navigates to a view model. /// Invoked when ReactiveUI router navigates to a view model.
/// </summary> /// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param> /// <param name="viewModel">ViewModel to which the user navigates.</param>
private void NavigateToViewModel(object viewModel) private void NavigateToViewModel(object? viewModel)
{ {
if (Router == null) if (Router == null)
{ {

16
src/Avalonia.ReactiveUI/TransitioningContentControl.cs

@ -13,20 +13,20 @@ namespace Avalonia.ReactiveUI
/// <summary> /// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="PageTransition"/> property. /// <see cref="AvaloniaProperty"/> for the <see cref="PageTransition"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IPageTransition> PageTransitionProperty = public static readonly StyledProperty<IPageTransition?> PageTransitionProperty =
AvaloniaProperty.Register<TransitioningContentControl, IPageTransition>(nameof(PageTransition), AvaloniaProperty.Register<TransitioningContentControl, IPageTransition?>(nameof(PageTransition),
new CrossFade(TimeSpan.FromSeconds(0.5))); new CrossFade(TimeSpan.FromSeconds(0.5)));
/// <summary> /// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property. /// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<object> DefaultContentProperty = public static readonly StyledProperty<object?> DefaultContentProperty =
AvaloniaProperty.Register<TransitioningContentControl, object>(nameof(DefaultContent)); AvaloniaProperty.Register<TransitioningContentControl, object?>(nameof(DefaultContent));
/// <summary> /// <summary>
/// Gets or sets the animation played when content appears and disappears. /// Gets or sets the animation played when content appears and disappears.
/// </summary> /// </summary>
public IPageTransition PageTransition public IPageTransition? PageTransition
{ {
get => GetValue(PageTransitionProperty); get => GetValue(PageTransitionProperty);
set => SetValue(PageTransitionProperty, value); set => SetValue(PageTransitionProperty, value);
@ -35,7 +35,7 @@ namespace Avalonia.ReactiveUI
/// <summary> /// <summary>
/// Gets or sets the content displayed whenever there is no page currently routed. /// Gets or sets the content displayed whenever there is no page currently routed.
/// </summary> /// </summary>
public object DefaultContent public object? DefaultContent
{ {
get => GetValue(DefaultContentProperty); get => GetValue(DefaultContentProperty);
set => SetValue(DefaultContentProperty, value); set => SetValue(DefaultContentProperty, value);
@ -44,7 +44,7 @@ namespace Avalonia.ReactiveUI
/// <summary> /// <summary>
/// Gets or sets the content with animation. /// Gets or sets the content with animation.
/// </summary> /// </summary>
public new object Content public new object? Content
{ {
get => base.Content; get => base.Content;
set => UpdateContentWithTransition(value); set => UpdateContentWithTransition(value);
@ -60,7 +60,7 @@ namespace Avalonia.ReactiveUI
/// Updates the content with transitions. /// Updates the content with transitions.
/// </summary> /// </summary>
/// <param name="content">New content to set.</param> /// <param name="content">New content to set.</param>
private async void UpdateContentWithTransition(object content) private async void UpdateContentWithTransition(object? content)
{ {
if (PageTransition != null) if (PageTransition != null)
await PageTransition.Start(this, null, true); await PageTransition.Start(this, null, true);

12
src/Avalonia.ReactiveUI/ViewModelViewHost.cs

@ -15,8 +15,8 @@ namespace Avalonia.ReactiveUI
/// <summary> /// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewModel"/> property. /// <see cref="AvaloniaProperty"/> for the <see cref="ViewModel"/> property.
/// </summary> /// </summary>
public static readonly AvaloniaProperty<object> ViewModelProperty = public static readonly AvaloniaProperty<object?> ViewModelProperty =
AvaloniaProperty.Register<ViewModelViewHost, object>(nameof(ViewModel)); AvaloniaProperty.Register<ViewModelViewHost, object?>(nameof(ViewModel));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ViewModelViewHost"/> class. /// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
@ -34,7 +34,7 @@ namespace Avalonia.ReactiveUI
/// <summary> /// <summary>
/// Gets or sets the ViewModel to display. /// Gets or sets the ViewModel to display.
/// </summary> /// </summary>
public object ViewModel public object? ViewModel
{ {
get => GetValue(ViewModelProperty); get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value); set => SetValue(ViewModelProperty, value);
@ -43,13 +43,13 @@ namespace Avalonia.ReactiveUI
/// <summary> /// <summary>
/// Gets or sets the view locator. /// Gets or sets the view locator.
/// </summary> /// </summary>
public IViewLocator ViewLocator { get; set; } public IViewLocator? ViewLocator { get; set; }
/// <summary> /// <summary>
/// Invoked when ReactiveUI router navigates to a view model. /// Invoked when ReactiveUI router navigates to a view model.
/// </summary> /// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param> /// <param name="viewModel">ViewModel to which the user navigates.</param>
private void NavigateToViewModel(object viewModel) private void NavigateToViewModel(object? viewModel)
{ {
if (viewModel == null) if (viewModel == null)
{ {
@ -74,4 +74,4 @@ namespace Avalonia.ReactiveUI
Content = viewInstance; Content = viewInstance;
} }
} }
} }

4
src/Avalonia.Styling/ApiCompatBaseline.txt

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

21
src/Avalonia.Styling/Diagnostics/StyleDiagnostics.cs

@ -0,0 +1,21 @@
using System.Collections.Generic;
using Avalonia.Styling;
namespace Avalonia.Diagnostics
{
/// <summary>
/// Contains information about style related diagnostics of a control.
/// </summary>
public class StyleDiagnostics
{
/// <summary>
/// Currently applied styles.
/// </summary>
public IReadOnlyList<IStyleInstance> AppliedStyles { get; }
public StyleDiagnostics(IReadOnlyList<IStyleInstance> appliedStyles)
{
AppliedStyles = appliedStyles;
}
}
}

17
src/Avalonia.Styling/Diagnostics/StyledElementExtensions.cs

@ -0,0 +1,17 @@
namespace Avalonia.Diagnostics
{
/// <summary>
/// Defines diagnostic extensions on <see cref="StyledElement"/>s.
/// </summary>
public static class StyledElementExtensions
{
/// <summary>
/// Gets a style diagnostics for a <see cref="StyledElement"/>.
/// </summary>
/// <param name="styledElement">The element.</param>
public static StyleDiagnostics GetStyleDiagnostics(this StyledElement styledElement)
{
return styledElement.GetStyleDiagnosticsInternal();
}
}
}

4
src/Avalonia.Styling/IStyledElement.cs

@ -1,4 +1,5 @@
using System; using System;
using System.ComponentModel;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Styling; using Avalonia.Styling;
@ -10,7 +11,8 @@ namespace Avalonia
IStyleHost, IStyleHost,
ILogical, ILogical,
IResourceHost, IResourceHost,
IDataContextProvider IDataContextProvider,
ISupportInitialize
{ {
/// <summary> /// <summary>
/// Occurs when the control has finished initialization. /// Occurs when the control has finished initialization.

40
src/Avalonia.Styling/StyledElement.cs

@ -334,7 +334,16 @@ namespace Avalonia
{ {
if (_initCount == 0 && !_styled) if (_initCount == 0 && !_styled)
{ {
AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this); try
{
BeginBatchUpdate();
AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
}
finally
{
EndBatchUpdate();
}
_styled = true; _styled = true;
} }
@ -356,6 +365,18 @@ namespace Avalonia
} }
} }
internal StyleDiagnostics GetStyleDiagnosticsInternal()
{
IReadOnlyList<IStyleInstance>? appliedStyles = _appliedStyles;
if (appliedStyles is null)
{
appliedStyles = Array.Empty<IStyleInstance>();
}
return new StyleDiagnostics(appliedStyles);
}
/// <inheritdoc/> /// <inheritdoc/>
void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{ {
@ -736,12 +757,21 @@ namespace Avalonia
{ {
if (_appliedStyles is object) if (_appliedStyles is object)
{ {
foreach (var i in _appliedStyles) BeginBatchUpdate();
try
{ {
i.Dispose(); foreach (var i in _appliedStyles)
} {
i.Dispose();
}
_appliedStyles.Clear(); _appliedStyles.Clear();
}
finally
{
EndBatchUpdate();
}
} }
_styled = false; _styled = false;

5
src/Avalonia.Styling/Styling/IStyleInstance.cs

@ -14,6 +14,11 @@ namespace Avalonia.Styling
/// </summary> /// </summary>
IStyle Source { get; } IStyle Source { get; }
/// <summary>
/// Gets a value indicating whether this style is active.
/// </summary>
bool IsActive { get; }
/// <summary> /// <summary>
/// Instructs the style to start acting upon the control. /// Instructs the style to start acting upon the control.
/// </summary> /// </summary>

18
src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs

@ -92,6 +92,7 @@ namespace Avalonia.Styling
{ {
if (!_isActive) if (!_isActive)
{ {
_innerSubscription ??= _binding.Observable.Subscribe(_inner);
_isActive = true; _isActive = true;
PublishNext(); PublishNext();
} }
@ -102,6 +103,8 @@ namespace Avalonia.Styling
if (_isActive) if (_isActive)
{ {
_isActive = false; _isActive = false;
_innerSubscription?.Dispose();
_innerSubscription = null;
PublishNext(); PublishNext();
} }
} }
@ -122,9 +125,6 @@ namespace Avalonia.Styling
sub.Dispose(); sub.Dispose();
} }
_innerSubscription?.Dispose();
_innerSubscription = null;
base.Dispose(); base.Dispose();
} }
@ -148,7 +148,17 @@ namespace Avalonia.Styling
protected override void Subscribed() protected override void Subscribed()
{ {
_innerSubscription = _binding.Observable.Subscribe(_inner); if (_isActive)
{
if (_innerSubscription is null)
{
_innerSubscription ??= _binding.Observable.Subscribe(_inner);
}
else
{
PublishNext();
}
}
} }
protected override void Unsubscribed() protected override void Unsubscribed()

2
src/Avalonia.Styling/Styling/PropertySetterInstance.cs

@ -7,7 +7,7 @@ using Avalonia.Reactive;
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
/// <summary> /// <summary>
/// A <see cref="Setter"/> which has been instance on a control. /// A <see cref="Setter"/> which has been instanced on a control.
/// </summary> /// </summary>
/// <typeparam name="T">The target property type.</typeparam> /// <typeparam name="T">The target property type.</typeparam>
internal class PropertySetterInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>, internal class PropertySetterInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,

128
src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs

@ -0,0 +1,128 @@
using System;
using Avalonia.Data;
using Avalonia.Reactive;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
/// A <see cref="Setter"/> which has been instanced on a control and whose value is lazily
/// evaluated.
/// </summary>
/// <typeparam name="T">The target property type.</typeparam>
internal class PropertySetterLazyInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,
ISetterInstance
{
private readonly IStyleable _target;
private readonly StyledPropertyBase<T>? _styledProperty;
private readonly DirectPropertyBase<T>? _directProperty;
private readonly Func<T> _valueFactory;
private BindingValue<T> _value;
private IDisposable? _subscription;
private bool _isActive;
public PropertySetterLazyInstance(
IStyleable target,
StyledPropertyBase<T> property,
Func<T> valueFactory)
{
_target = target;
_styledProperty = property;
_valueFactory = valueFactory;
}
public PropertySetterLazyInstance(
IStyleable target,
DirectPropertyBase<T> property,
Func<T> valueFactory)
{
_target = target;
_directProperty = property;
_valueFactory = valueFactory;
}
public void Start(bool hasActivator)
{
_isActive = !hasActivator;
if (_styledProperty is object)
{
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style;
_subscription = _target.Bind(_styledProperty, this, priority);
}
else
{
_subscription = _target.Bind(_directProperty, this);
}
}
public void Activate()
{
if (!_isActive)
{
_isActive = true;
PublishNext();
}
}
public void Deactivate()
{
if (_isActive)
{
_isActive = false;
PublishNext();
}
}
public override void Dispose()
{
if (_subscription is object)
{
var sub = _subscription;
_subscription = null;
sub.Dispose();
}
else if (_isActive)
{
if (_styledProperty is object)
{
_target.ClearValue(_styledProperty);
}
else
{
_target.ClearValue(_directProperty);
}
}
base.Dispose();
}
protected override void Subscribed() => PublishNext();
protected override void Unsubscribed() { }
private T GetValue()
{
if (_value.HasValue)
{
return _value.Value;
}
_value = _valueFactory();
return _value.Value;
}
private void PublishNext()
{
if (_isActive)
{
GetValue();
PublishNext(_value);
}
else
{
PublishNext(default);
}
}
}
}

24
src/Avalonia.Styling/Styling/Setter.cs

@ -68,18 +68,10 @@ namespace Avalonia.Styling
throw new InvalidOperationException("Setter.Property must be set."); throw new InvalidOperationException("Setter.Property must be set.");
} }
var value = Value;
if (value is ITemplate template &&
!typeof(ITemplate).IsAssignableFrom(Property.PropertyType))
{
value = template.Build();
}
var data = new SetterVisitorData var data = new SetterVisitorData
{ {
target = target, target = target,
value = value, value = Value,
}; };
Property.Accept(this, ref data); Property.Accept(this, ref data);
@ -97,6 +89,13 @@ namespace Avalonia.Styling
property, property,
binding); binding);
} }
else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType))
{
data.result = new PropertySetterLazyInstance<T>(
data.target,
property,
() => (T)template.Build());
}
else else
{ {
data.result = new PropertySetterInstance<T>( data.result = new PropertySetterInstance<T>(
@ -117,6 +116,13 @@ namespace Avalonia.Styling
property, property,
binding); binding);
} }
else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType))
{
data.result = new PropertySetterLazyInstance<T>(
data.target,
property,
() => (T)template.Build());
}
else else
{ {
data.result = new PropertySetterInstance<T>( data.result = new PropertySetterInstance<T>(

9
src/Avalonia.Styling/Styling/StyleInstance.cs

@ -17,7 +17,6 @@ namespace Avalonia.Styling
private readonly List<IDisposable>? _animations; private readonly List<IDisposable>? _animations;
private readonly IStyleActivator? _activator; private readonly IStyleActivator? _activator;
private readonly Subject<bool>? _animationTrigger; private readonly Subject<bool>? _animationTrigger;
private bool _active;
public StyleInstance( public StyleInstance(
IStyle source, IStyle source,
@ -29,6 +28,7 @@ namespace Avalonia.Styling
Source = source ?? throw new ArgumentNullException(nameof(source)); Source = source ?? throw new ArgumentNullException(nameof(source));
Target = target ?? throw new ArgumentNullException(nameof(target)); Target = target ?? throw new ArgumentNullException(nameof(target));
_activator = activator; _activator = activator;
IsActive = _activator is null;
if (setters is object) if (setters is object)
{ {
@ -56,6 +56,7 @@ namespace Avalonia.Styling
} }
} }
public bool IsActive { get; private set; }
public IStyle Source { get; } public IStyle Source { get; }
public IStyleable Target { get; } public IStyleable Target { get; }
@ -104,15 +105,15 @@ namespace Avalonia.Styling
private void ActivatorChanged(bool value) private void ActivatorChanged(bool value)
{ {
if (_active != value) if (IsActive != value)
{ {
_active = value; IsActive = value;
_animationTrigger?.OnNext(value); _animationTrigger?.OnNext(value);
if (_setters is object) if (_setters is object)
{ {
if (_active) if (IsActive)
{ {
foreach (var setter in _setters) foreach (var setter in _setters)
{ {

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

@ -59,8 +59,7 @@
Grid.Column="4"/> Grid.Column="4"/>
<Popup Name="PART_Popup" <Popup Name="PART_Popup"
PlacementMode="Right" PlacementMode="Right"
IsLightDismissEnabled="True" IsLightDismissEnabled="False"
OverlayInputPassThroughElement="{Binding $parent[MenuItem]}"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"> IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Background="{TemplateBinding Background}" <Border Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderBrush="{DynamicResource ThemeBorderMidBrush}"

5
src/Avalonia.Themes.Default/ProgressBar.xaml

@ -1,8 +1,11 @@
<Styles xmlns="https://github.com/avaloniaui"> <Styles xmlns="https://github.com/avaloniaui">
<Design.PreviewWith> <Design.PreviewWith>
<Border Padding="20"> <Border Padding="20">
<StackPanel> <StackPanel Spacing="10">
<ProgressBar VerticalAlignment="Center" IsIndeterminate="True" /> <ProgressBar VerticalAlignment="Center" IsIndeterminate="True" />
<ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
<ProgressBar VerticalAlignment="Center" Value="50" />
<ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />
<ProgressBar HorizontalAlignment="Left" IsIndeterminate="True" Orientation="Vertical" /> <ProgressBar HorizontalAlignment="Left" IsIndeterminate="True" Orientation="Vertical" />
</StackPanel> </StackPanel>
</Border> </Border>

5
src/Avalonia.Themes.Default/Slider.xaml

@ -11,7 +11,7 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border Name="TrackBackground" Grid.Row="1" Height="4" Margin="6,0" VerticalAlignment="Center"/> <Border Name="TrackBackground" Grid.Row="1" Height="4" Margin="6,0" VerticalAlignment="Center"/>
<Track Name="PART_Track" Grid.Row="1" Orientation="Horizontal"> <Track Name="PART_Track" Grid.Row="1" IsDirectionReversed="{TemplateBinding IsDirectionReversed}" Orientation="Horizontal">
<Track.DecreaseButton> <Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton" <RepeatButton Name="PART_DecreaseButton"
Classes="repeattrack" /> Classes="repeattrack" />
@ -46,7 +46,7 @@
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Border Name="TrackBackground" Grid.Column="1" Width="4" Margin="0,6" HorizontalAlignment="Center"/> <Border Name="TrackBackground" Grid.Column="1" Width="4" Margin="0,6" HorizontalAlignment="Center"/>
<Track Name="PART_Track" Grid.Column="1" Orientation="Vertical"> <Track Name="PART_Track" Grid.Column="1" IsDirectionReversed="{TemplateBinding IsDirectionReversed}" Orientation="Vertical">
<Track.DecreaseButton> <Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton" <RepeatButton Name="PART_DecreaseButton"
Classes="repeattrack" /> Classes="repeattrack" />
@ -80,6 +80,7 @@
</Style> </Style>
<Style Selector="Slider /template/ RepeatButton.repeattrack"> <Style Selector="Slider /template/ RepeatButton.repeattrack">
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Foreground" Value="{DynamicResource ThemeBorderLowBrush}"/> <Setter Property="Foreground" Value="{DynamicResource ThemeBorderLowBrush}"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>

3
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

@ -112,9 +112,8 @@
<Popup Name="PART_Popup" <Popup Name="PART_Popup"
WindowManagerAddShadowHint="False" WindowManagerAddShadowHint="False"
PlacementMode="Right" PlacementMode="Right"
OverlayInputPassThroughElement="{Binding $parent[MenuItem]}"
HorizontalOffset="{DynamicResource MenuFlyoutSubItemPopupHorizontalOffset}" HorizontalOffset="{DynamicResource MenuFlyoutSubItemPopupHorizontalOffset}"
IsLightDismissEnabled="True" IsLightDismissEnabled="False"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"> IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Background="{DynamicResource MenuFlyoutPresenterBackground}" <Border Background="{DynamicResource MenuFlyoutPresenterBackground}"
BorderBrush="{DynamicResource MenuFlyoutPresenterBorderBrush}" BorderBrush="{DynamicResource MenuFlyoutPresenterBorderBrush}"

6
src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml

@ -1,8 +1,11 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith> <Design.PreviewWith>
<Border Padding="20"> <Border Padding="20">
<StackPanel> <StackPanel Spacing="10">
<ProgressBar VerticalAlignment="Center" IsIndeterminate="True" /> <ProgressBar VerticalAlignment="Center" IsIndeterminate="True" />
<ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
<ProgressBar VerticalAlignment="Center" Value="50" />
<ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />
<ProgressBar HorizontalAlignment="Left" IsIndeterminate="True" Orientation="Vertical" /> <ProgressBar HorizontalAlignment="Left" IsIndeterminate="True" Orientation="Vertical" />
</StackPanel> </StackPanel>
</Border> </Border>
@ -12,7 +15,6 @@
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" /> <Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource ProgressBarBorderThemeThickness}" /> <Setter Property="BorderThickness" Value="{DynamicResource ProgressBarBorderThemeThickness}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlHighlightTransparentBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource SystemControlHighlightTransparentBrush}" />
<Setter Property="Maximum" Value="100" />
<Setter Property="MinHeight" Value="{DynamicResource ProgressBarThemeMinHeight}" /> <Setter Property="MinHeight" Value="{DynamicResource ProgressBarThemeMinHeight}" />
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template"> <Setter Property="Template">

12
src/Avalonia.Themes.Fluent/Controls/Slider.xaml

@ -62,9 +62,9 @@
<TickBar Name="TopTickBar" Placement="Top" Height="{DynamicResource SliderOutsideTickBarThemeHeight}" VerticalAlignment="Bottom" Margin="0,0,0,4" Grid.ColumnSpan="3" /> <TickBar Name="TopTickBar" Placement="Top" Height="{DynamicResource SliderOutsideTickBarThemeHeight}" VerticalAlignment="Bottom" Margin="0,0,0,4" Grid.ColumnSpan="3" />
<!-- <TickBar Name="HorizontalInlineTickBar" Placement="Top" Fill="{DynamicResource SliderInlineTickBarFill}" Height="{DynamicResource SliderTrackThemeHeight}" Grid.Row="1" Grid.ColumnSpan="3" /> --> <!-- <TickBar Name="HorizontalInlineTickBar" Placement="Top" Fill="{DynamicResource SliderInlineTickBarFill}" Height="{DynamicResource SliderTrackThemeHeight}" Grid.Row="1" Grid.ColumnSpan="3" /> -->
<TickBar Name="BottomTickBar" Placement="Bottom" Height="{DynamicResource SliderOutsideTickBarThemeHeight}" VerticalAlignment="Top" Margin="0,4,0,0" Grid.Row="2" Grid.ColumnSpan="3" /> <TickBar Name="BottomTickBar" Placement="Bottom" Height="{DynamicResource SliderOutsideTickBarThemeHeight}" VerticalAlignment="Top" Margin="0,4,0,0" Grid.Row="2" Grid.ColumnSpan="3" />
<Track Name="PART_Track" Grid.Row="1" Grid.ColumnSpan="3" Orientation="Horizontal"> <Track Name="PART_Track" Grid.Row="1" Grid.ColumnSpan="3" IsDirectionReversed="{TemplateBinding IsDirectionReversed}" Orientation="Horizontal">
<Track.DecreaseButton> <Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton" Background="{TemplateBinding Foreground}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <RepeatButton Name="PART_DecreaseButton" Background="{TemplateBinding Foreground}" Focusable="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<RepeatButton.Template> <RepeatButton.Template>
<ControlTemplate> <ControlTemplate>
<Grid> <Grid>
@ -76,7 +76,7 @@
</RepeatButton> </RepeatButton>
</Track.DecreaseButton> </Track.DecreaseButton>
<Track.IncreaseButton> <Track.IncreaseButton>
<RepeatButton Name="PART_IncreaseButton" Background="{TemplateBinding Background}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <RepeatButton Name="PART_IncreaseButton" Background="{TemplateBinding Background}" Focusable="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<RepeatButton.Template> <RepeatButton.Template>
<ControlTemplate> <ControlTemplate>
<Grid> <Grid>
@ -124,9 +124,9 @@
<TickBar Name="LeftTickBar" Placement="Left" Width="{DynamicResource SliderOutsideTickBarThemeHeight}" HorizontalAlignment="Right" Margin="0,0,4,0" Grid.RowSpan="3" /> <TickBar Name="LeftTickBar" Placement="Left" Width="{DynamicResource SliderOutsideTickBarThemeHeight}" HorizontalAlignment="Right" Margin="0,0,4,0" Grid.RowSpan="3" />
<!-- <TickBar Name="VerticalInlineTickBar" Placement="Inline" Fill="{DynamicResource SliderInlineTickBarFill}" Width="{DynamicResource SliderTrackThemeHeight}" Grid.Column="1" Grid.RowSpan="3" /> --> <!-- <TickBar Name="VerticalInlineTickBar" Placement="Inline" Fill="{DynamicResource SliderInlineTickBarFill}" Width="{DynamicResource SliderTrackThemeHeight}" Grid.Column="1" Grid.RowSpan="3" /> -->
<TickBar Name="RightTickBar" Placement="Right" Width="{DynamicResource SliderOutsideTickBarThemeHeight}" HorizontalAlignment="Left" Margin="4,0,0,0" Grid.Column="2" Grid.RowSpan="3" /> <TickBar Name="RightTickBar" Placement="Right" Width="{DynamicResource SliderOutsideTickBarThemeHeight}" HorizontalAlignment="Left" Margin="4,0,0,0" Grid.Column="2" Grid.RowSpan="3" />
<Track Name="PART_Track" Grid.Column="1" Grid.ColumnSpan="1" Grid.RowSpan="3" Orientation="Vertical"> <Track Name="PART_Track" Grid.Column="1" Grid.ColumnSpan="1" Grid.RowSpan="3" IsDirectionReversed="{TemplateBinding IsDirectionReversed}" Orientation="Vertical">
<Track.DecreaseButton> <Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton" Background="{TemplateBinding Foreground}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"> <RepeatButton Name="PART_DecreaseButton" Background="{TemplateBinding Foreground}" Focusable="False" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<RepeatButton.Template> <RepeatButton.Template>
<ControlTemplate> <ControlTemplate>
<Grid> <Grid>
@ -138,7 +138,7 @@
</RepeatButton> </RepeatButton>
</Track.DecreaseButton> </Track.DecreaseButton>
<Track.IncreaseButton> <Track.IncreaseButton>
<RepeatButton Name="PART_IncreaseButton" Background="{TemplateBinding Background}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"> <RepeatButton Name="PART_IncreaseButton" Background="{TemplateBinding Background}" Focusable="False" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<RepeatButton.Template> <RepeatButton.Template>
<ControlTemplate> <ControlTemplate>
<Grid> <Grid>

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

@ -36,7 +36,7 @@ namespace Avalonia.Media
} }
/// <summary> /// <summary>
/// /// Initializes a new instance of the <see cref="FormattedText"/> class.
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text"></param>
/// <param name="typeface"></param> /// <param name="typeface"></param>
@ -45,7 +45,7 @@ namespace Avalonia.Media
/// <param name="textWrapping"></param> /// <param name="textWrapping"></param>
/// <param name="constraint"></param> /// <param name="constraint"></param>
public FormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment, public FormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment,
TextWrapping textWrapping, Size constraint) TextWrapping textWrapping, Size constraint) : this()
{ {
_text = text; _text = text;

18
src/Avalonia.Visuals/Media/PathGeometryCollections.cs

@ -1,9 +1,27 @@
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Visuals.Platform;
namespace Avalonia.Media namespace Avalonia.Media
{ {
public sealed class PathFigures : AvaloniaList<PathFigure> public sealed class PathFigures : AvaloniaList<PathFigure>
{ {
/// <summary>
/// Parses the specified path data to a <see cref="PathFigures"/>.
/// </summary>
/// <param name="pathData">The s.</param>
/// <returns></returns>
public static PathFigures Parse(string pathData)
{
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
parser.Parse(pathData);
}
return pathGeometry.Figures;
}
} }
public sealed class PathSegments : AvaloniaList<PathSegment> public sealed class PathSegments : AvaloniaList<PathSegment>

14
src/Avalonia.Visuals/Visual.cs

@ -150,7 +150,7 @@ namespace Avalonia
public TransformedBounds? TransformedBounds => _transformedBounds; public TransformedBounds? TransformedBounds => _transformedBounds;
/// <summary> /// <summary>
/// Gets a value indicating whether the control should be clipped to its bounds. /// Gets or sets a value indicating whether the control should be clipped to its bounds.
/// </summary> /// </summary>
public bool ClipToBounds public bool ClipToBounds
{ {
@ -191,7 +191,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Gets a value indicating whether this control is visible. /// Gets or sets a value indicating whether this control is visible.
/// </summary> /// </summary>
public bool IsVisible public bool IsVisible
{ {
@ -200,7 +200,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Gets the opacity of the control. /// Gets or sets the opacity of the control.
/// </summary> /// </summary>
public double Opacity public double Opacity
{ {
@ -209,7 +209,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Gets the opacity mask of the control. /// Gets or sets the opacity mask of the control.
/// </summary> /// </summary>
public IBrush OpacityMask public IBrush OpacityMask
{ {
@ -218,7 +218,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Gets the render transform of the control. /// Gets or sets the render transform of the control.
/// </summary> /// </summary>
public ITransform RenderTransform public ITransform RenderTransform
{ {
@ -227,7 +227,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Gets the transform origin of the control. /// Gets or sets the transform origin of the control.
/// </summary> /// </summary>
public RelativePoint RenderTransformOrigin public RelativePoint RenderTransformOrigin
{ {
@ -236,7 +236,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Gets the Z index of the control. /// Gets or sets the Z index of the control.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Controls with a higher <see cref="ZIndex"/> will appear in front of controls with /// Controls with a higher <see cref="ZIndex"/> will appear in front of controls with

9
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs

@ -198,6 +198,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
return ConvertDefinitionList(node, text, types, types.RowDefinitions, types.RowDefinition, "row definitions", out result); return ConvertDefinitionList(node, text, types, types.RowDefinitions, types.RowDefinition, "row definitions", out result);
} }
if (type.Equals(types.Classes))
{
var classes = text.Split(' ');
var classNodes = classes.Select(c => new XamlAstTextNode(node, c, types.XamlIlTypes.String)).ToArray();
result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, types.Classes, types.XamlIlTypes.String, classNodes);
return true;
}
result = null; result = null;
return false; return false;
} }

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -77,6 +77,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType RowDefinitions { get; } public IXamlType RowDefinitions { get; }
public IXamlType ColumnDefinition { get; } public IXamlType ColumnDefinition { get; }
public IXamlType ColumnDefinitions { get; } public IXamlType ColumnDefinitions { get; }
public IXamlType Classes { get; }
public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{ {
@ -166,6 +167,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ColumnDefinitions = cfg.TypeSystem.GetType("Avalonia.Controls.ColumnDefinitions"); ColumnDefinitions = cfg.TypeSystem.GetType("Avalonia.Controls.ColumnDefinitions");
RowDefinition = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinition"); RowDefinition = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinition");
RowDefinitions = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinitions"); RowDefinitions = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinitions");
Classes = cfg.TypeSystem.GetType("Avalonia.Controls.Classes");
} }
} }

2
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@ -1 +1 @@
Subproject commit ea80a607c5e9d8f000160dbbb48c27ed4cfafbc9 Subproject commit 9e90d34e97c766ba8dcb70128147fcded65d195a

11
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs

@ -72,7 +72,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
internal object RawSource { get; } internal object RawSource { get; }
public override string ToString() public override string ToString()
=> string.Concat(_elements.Select(e => e.ToString())); => string.Concat(_elements);
} }
public class CompiledBindingPathBuilder public class CompiledBindingPathBuilder
@ -88,7 +88,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
public CompiledBindingPathBuilder Property(IPropertyInfo info, Func<WeakReference<object>, IPropertyInfo, IPropertyAccessor> accessorFactory) public CompiledBindingPathBuilder Property(IPropertyInfo info, Func<WeakReference<object>, IPropertyInfo, IPropertyAccessor> accessorFactory)
{ {
_elements.Add(new PropertyElement(info, accessorFactory)); _elements.Add(new PropertyElement(info, accessorFactory, _elements.Count == 0));
return this; return this;
} }
@ -161,10 +161,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
internal class PropertyElement : ICompiledBindingPathElement internal class PropertyElement : ICompiledBindingPathElement
{ {
public PropertyElement(IPropertyInfo property, Func<WeakReference<object>, IPropertyInfo, IPropertyAccessor> accessorFactory) private readonly bool _isFirstElement;
public PropertyElement(IPropertyInfo property, Func<WeakReference<object>, IPropertyInfo, IPropertyAccessor> accessorFactory, bool isFirstElement)
{ {
Property = property; Property = property;
AccessorFactory = accessorFactory; AccessorFactory = accessorFactory;
_isFirstElement = isFirstElement;
} }
public IPropertyInfo Property { get; } public IPropertyInfo Property { get; }
@ -172,7 +175,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
public Func<WeakReference<object>, IPropertyInfo, IPropertyAccessor> AccessorFactory { get; } public Func<WeakReference<object>, IPropertyInfo, IPropertyAccessor> AccessorFactory { get; }
public override string ToString() public override string ToString()
=> $".{Property.Name}"; => _isFirstElement ? Property.Name : $".{Property.Name}";
} }
internal interface IStronglyTypedStreamElement : ICompiledBindingPathElement internal interface IStronglyTypedStreamElement : ICompiledBindingPathElement

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

Loading…
Cancel
Save