Browse Source

Merge remote-tracking branch 'origin/jmacato/update-xamlil' into jmacato/update-xamlil

pull/5668/head
Jumar Macato 6 years ago
parent
commit
b3cadaa1a5
No known key found for this signature in database GPG Key ID: B19884DAC3A5BF3F
  1. 7
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 8
      .github/ISSUE_TEMPLATE/config.yml
  3. 1
      .github/ISSUE_TEMPLATE/feature_request.md
  4. 1
      native/Avalonia.Native/src/OSX/AvnString.h
  5. 15
      native/Avalonia.Native/src/OSX/AvnString.mm
  6. 10
      native/Avalonia.Native/src/OSX/Screens.mm
  7. 28
      native/Avalonia.Native/src/OSX/app.mm
  8. 2
      native/Avalonia.Native/src/OSX/clipboard.mm
  9. 5
      native/Avalonia.Native/src/OSX/common.h
  10. 22
      native/Avalonia.Native/src/OSX/cursor.mm
  11. 31
      native/Avalonia.Native/src/OSX/main.mm
  12. 85
      native/Avalonia.Native/src/OSX/menu.mm
  13. 34
      native/Avalonia.Native/src/OSX/window.mm
  14. BIN
      samples/ControlCatalog/Assets/avalonia-32.png
  15. 4
      samples/ControlCatalog/MainView.xaml
  16. 8
      samples/ControlCatalog/Pages/AcrylicPage.xaml
  17. 29
      samples/ControlCatalog/Pages/CursorPage.xaml
  18. 20
      samples/ControlCatalog/Pages/CursorPage.xaml.cs
  19. 5
      samples/ControlCatalog/Pages/DataGridPage.xaml
  20. 6
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  21. 7
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  22. 16
      samples/ControlCatalog/Pages/ScreenPage.cs
  23. 15
      samples/ControlCatalog/Pages/SliderPage.xaml
  24. 44
      samples/ControlCatalog/ViewModels/CursorPageViewModel.cs
  25. 16
      src/Avalonia.Animation/Animation.cs
  26. 40
      src/Avalonia.Base/AvaloniaObject.cs
  27. 28
      src/Avalonia.Base/Data/BindingOperations.cs
  28. 6
      src/Avalonia.Base/Data/Converters/BoolConverters.cs
  29. 30
      src/Avalonia.Base/EnumExtensions.cs
  30. 57
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  31. 30
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  32. 8
      src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
  33. 9
      src/Avalonia.Base/PropertyStore/IValue.cs
  34. 16
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  35. 121
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  36. 5
      src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
  37. 17
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  38. 4
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  39. 264
      src/Avalonia.Base/ValueStore.cs
  40. 13
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  41. 21
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  42. 56
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  43. 16
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  44. 5
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  45. 2
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  46. 7
      src/Avalonia.Controls/ApiCompatBaseline.txt
  47. 11
      src/Avalonia.Controls/Application.cs
  48. 14
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  49. 13
      src/Avalonia.Controls/Button.cs
  50. 72
      src/Avalonia.Controls/ComboBox.cs
  51. 77
      src/Avalonia.Controls/ContextMenu.cs
  52. 69
      src/Avalonia.Controls/Control.cs
  53. 77
      src/Avalonia.Controls/DefinitionBase.cs
  54. 2
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  55. 55
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  56. 45
      src/Avalonia.Controls/Grid.cs
  57. 8
      src/Avalonia.Controls/ListBox.cs
  58. 54
      src/Avalonia.Controls/MenuItem.cs
  59. 6
      src/Avalonia.Controls/NativeControlHost.cs
  60. 17
      src/Avalonia.Controls/Notifications/NotificationCard.cs
  61. 7
      src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs
  62. 2
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  63. 12
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  64. 4
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  65. 21
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  66. 48
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  67. 8
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  68. 4
      src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs
  69. 47
      src/Avalonia.Controls/Slider.cs
  70. 47
      src/Avalonia.Controls/TextBox.cs
  71. 2
      src/Avalonia.Controls/TopLevel.cs
  72. 12
      src/Avalonia.Controls/TreeView.cs
  73. 14
      src/Avalonia.Controls/UrlOpenedEventArgs.cs
  74. 2
      src/Avalonia.Controls/Utils/AncestorFinder.cs
  75. 45
      src/Avalonia.Controls/Window.cs
  76. 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  77. 12
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  78. 21
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
  79. 226
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  80. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  81. 51
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs
  82. 27
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
  83. 59
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
  84. 43
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs
  85. 7
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  86. 124
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  87. 10
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  88. 2
      src/Avalonia.Dialogs/ManagedFileChooserSources.cs
  89. 8
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  90. 4
      src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs
  91. 2
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  92. 8
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  93. 2
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  94. 2
      src/Avalonia.Input/AccessKeyHandler.cs
  95. 4
      src/Avalonia.Input/ApiCompatBaseline.txt
  96. 39
      src/Avalonia.Input/Cursor.cs
  97. 4
      src/Avalonia.Input/FocusManager.cs
  98. 12
      src/Avalonia.Input/Platform/ICursorFactory.cs
  99. 14
      src/Avalonia.Input/Platform/ICursorImpl.cs
  100. 9
      src/Avalonia.Input/Platform/IStandardCursorFactory.cs

7
.github/ISSUE_TEMPLATE/bug_report.md

@ -4,7 +4,6 @@ about: Create a report to help us improve Avalonia
title: '' title: ''
labels: bug labels: bug
assignees: '' assignees: ''
--- ---
**Describe the bug** **Describe the bug**
@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
@ -24,8 +24,9 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. Windows, Mac, Linux (State distribution)]
- Version [e.g. 0.10.0-rc1 or 0.9.12] - OS: [e.g. Windows, Mac, Linux (State distribution)]
- Version [e.g. 0.10.0-rc1 or 0.9.12]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

8
.github/ISSUE_TEMPLATE/config.yml

@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Questions, Discussions, Ideas
url: https://github.com/AvaloniaUI/Avalonia/discussions/new
about: Please ask and answer questions here.
- name: Avalonia Community Support on Gitter
url: https://gitter.im/AvaloniaUI/Avalonia
about: Please ask and answer questions here.

1
.github/ISSUE_TEMPLATE/feature_request.md

@ -4,7 +4,6 @@ about: Suggest an idea for this project
title: '' title: ''
labels: enhancement labels: enhancement
assignees: '' assignees: ''
--- ---
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**

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

10
native/Avalonia.Native/src/OSX/Screens.mm

@ -5,12 +5,6 @@ class Screens : public ComSingleObject<IAvnScreens, &IID_IAvnScreens>
public: public:
FORWARD_IUNKNOWN() FORWARD_IUNKNOWN()
private:
CGFloat PrimaryDisplayHeight()
{
return NSMaxY([[[NSScreen screens] firstObject] frame]);
}
public: public:
virtual HRESULT GetScreenCount (int* ret) override virtual HRESULT GetScreenCount (int* ret) override
{ {
@ -36,12 +30,12 @@ public:
ret->Bounds.Height = [screen frame].size.height; ret->Bounds.Height = [screen frame].size.height;
ret->Bounds.Width = [screen frame].size.width; ret->Bounds.Width = [screen frame].size.width;
ret->Bounds.X = [screen frame].origin.x; ret->Bounds.X = [screen frame].origin.x;
ret->Bounds.Y = PrimaryDisplayHeight() - [screen frame].origin.y - ret->Bounds.Height; ret->Bounds.Y = ConvertPointY(ToAvnPoint([screen frame].origin)).Y - ret->Bounds.Height;
ret->WorkingArea.Height = [screen visibleFrame].size.height; ret->WorkingArea.Height = [screen visibleFrame].size.height;
ret->WorkingArea.Width = [screen visibleFrame].size.width; ret->WorkingArea.Width = [screen visibleFrame].size.width;
ret->WorkingArea.X = [screen visibleFrame].origin.x; ret->WorkingArea.X = [screen visibleFrame].origin.x;
ret->WorkingArea.Y = ret->Bounds.Height - [screen visibleFrame].origin.y - ret->WorkingArea.Height; ret->WorkingArea.Y = ConvertPointY(ToAvnPoint([screen visibleFrame].origin)).Y - ret->WorkingArea.Height;
ret->PixelDensity = [screen backingScaleFactor]; ret->PixelDensity = [screen backingScaleFactor];

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

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

@ -28,12 +28,15 @@ 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);
extern AvnPoint ConvertPointY (AvnPoint p); extern AvnPoint ConvertPointY (AvnPoint p);
extern CGFloat PrimaryDisplayHeight();
extern NSSize ToNSSize (AvnSize s); extern NSSize ToNSSize (AvnSize s);
#ifdef DEBUG #ifdef DEBUG
#define NSDebugLog(...) NSLog(__VA_ARGS__) #define NSDebugLog(...) NSLog(__VA_ARGS__)

22
native/Avalonia.Native/src/OSX/cursor.mm

@ -62,6 +62,28 @@ public:
return S_OK; return S_OK;
} }
virtual HRESULT CreateCustomCursor (void* bitmapData, size_t length, AvnPixelSize hotPixel, IAvnCursor** retOut) override
{
if(bitmapData == nullptr || retOut == nullptr)
{
return E_POINTER;
}
NSData *imageData = [NSData dataWithBytes:bitmapData length:length];
NSImage *image = [[NSImage alloc] initWithData:imageData];
NSPoint hotSpot;
hotSpot.x = hotPixel.Width;
hotSpot.y = hotPixel.Height;
*retOut = new Cursor([[NSCursor new] initWithImage: image hotSpot: hotSpot]);
(*retOut)->AddRef();
return S_OK;
}
}; };
extern IAvnCursorFactory* CreateCursorFactory() extern IAvnCursorFactory* CreateCursorFactory()

31
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;
}; };
@ -299,10 +306,24 @@ AvnPoint ToAvnPoint (NSPoint p)
AvnPoint ConvertPointY (AvnPoint p) AvnPoint ConvertPointY (AvnPoint p)
{ {
auto sw = [NSScreen.screens objectAtIndex:0].frame; auto primaryDisplayHeight = NSMaxY([[[NSScreen screens] firstObject] frame]);
auto t = MAX(sw.origin.y, sw.origin.y + sw.size.height); p.Y = primaryDisplayHeight - p.Y;
p.Y = t - p.Y;
return p; return p;
} }
CGFloat PrimaryDisplayHeight()
{
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
{ {

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

@ -1391,17 +1391,20 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[super viewDidChangeBackingProperties]; [super viewDidChangeBackingProperties];
} }
- (bool) ignoreUserInput - (bool) ignoreUserInput:(bool)trigerInputWhenDisabled
{ {
auto parentWindow = objc_cast<AvnWindow>([self window]); auto parentWindow = objc_cast<AvnWindow>([self window]);
if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents])
{ {
auto window = dynamic_cast<WindowImpl*>(_parent.getRaw()); if(trigerInputWhenDisabled)
if(window != nullptr)
{ {
window->WindowEvents->GotInputWhenDisabled(); auto window = dynamic_cast<WindowImpl*>(_parent.getRaw());
if(window != nullptr)
{
window->WindowEvents->GotInputWhenDisabled();
}
} }
return TRUE; return TRUE;
@ -1412,7 +1415,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type
{ {
if([self ignoreUserInput]) bool triggerInputWhenDisabled = type != Move;
if([self ignoreUserInput: triggerInputWhenDisabled])
{ {
return; return;
} }
@ -1578,7 +1583,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type
{ {
if([self ignoreUserInput]) if([self ignoreUserInput: false])
{ {
return; return;
} }
@ -1872,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];
}
} }
} }
@ -2063,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;

BIN
samples/ControlCatalog/Assets/avalonia-32.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

4
samples/ControlCatalog/MainView.xaml

@ -22,6 +22,10 @@
<TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem> <TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
<TabItem Header="ComboBox"><pages:ComboBoxPage/></TabItem> <TabItem Header="ComboBox"><pages:ComboBoxPage/></TabItem>
<TabItem Header="ContextMenu"><pages:ContextMenuPage/></TabItem> <TabItem Header="ContextMenu"><pages:ContextMenuPage/></TabItem>
<TabItem Header="Cursor"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<pages:CursorPage/>
</TabItem>
<TabItem Header="DataGrid" <TabItem Header="DataGrid"
ScrollViewer.VerticalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"> ScrollViewer.HorizontalScrollBarVisibility="Disabled">

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>

29
samples/ControlCatalog/Pages/CursorPage.xaml

@ -0,0 +1,29 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.CursorPage">
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,*">
<StackPanel Grid.ColumnSpan="2" Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Cursor</TextBlock>
<TextBlock Classes="h2">Defines a cursor (mouse pointer)</TextBlock>
</StackPanel>
<ListBox Grid.Row="1" Items="{Binding StandardCursors}" Margin="0 8 8 8">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Cursor" Value="{Binding Cursor}"/>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Type}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Column="1" Grid.Row="1" Margin="8 8 0 8">
<Button Cursor="{Binding CustomCursor}" Margin="0 8" Padding="16">
<TextBlock>Custom Cursor</TextBlock>
</Button>
</StackPanel>
</Grid>
</UserControl>

20
samples/ControlCatalog/Pages/CursorPage.xaml.cs

@ -0,0 +1,20 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public class CursorPage : UserControl
{
public CursorPage()
{
this.InitializeComponent();
DataContext = new CursorPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

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>

16
samples/ControlCatalog/Pages/ScreenPage.cs

@ -29,7 +29,7 @@ namespace ControlCatalog.Pages
var screens = w.Screens.All; var screens = w.Screens.All;
var scaling = ((IRenderRoot)w).RenderScaling; var scaling = ((IRenderRoot)w).RenderScaling;
var drawBrush = Brushes.Green; var drawBrush = Brushes.Black;
Pen p = new Pen(drawBrush); Pen p = new Pen(drawBrush);
if (screens != null) if (screens != null)
foreach (Screen screen in screens) foreach (Screen screen in screens)
@ -45,18 +45,16 @@ namespace ControlCatalog.Pages
screen.Bounds.Height / 10f); screen.Bounds.Height / 10f);
Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f, Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f,
screen.WorkingArea.Height / 10f); screen.WorkingArea.Height / 10f);
context.DrawRectangle(p, boundsRect); context.DrawRectangle(p, boundsRect);
context.DrawRectangle(p, workingAreaRect); context.DrawRectangle(p, workingAreaRect);
FormattedText text = new FormattedText()
{
Typeface = Typeface.Default
};
text.Text = $"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}"; var text = new FormattedText() { Typeface = new Typeface("Arial"), FontSize = 18 };
text.Text = $"Bounds: {screen.Bounds.TopLeft} {screen.Bounds.Width}:{screen.Bounds.Height}";
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height), text); context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height), text);
text.Text = $"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}"; text.Text = $"WorkArea: {screen.WorkingArea.TopLeft} {screen.WorkingArea.Width}:{screen.WorkingArea.Height}";
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text); context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text);
text.Text = $"Scaling: {screen.PixelDensity * 100}%"; text.Text = $"Scaling: {screen.PixelDensity * 100}%";
@ -69,7 +67,7 @@ namespace ControlCatalog.Pages
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text); context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text);
} }
context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10, w.Bounds.Width / 10, w.Bounds.Height / 10)); context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10));
} }
} }
} }

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>

44
samples/ControlCatalog/ViewModels/CursorPageViewModel.cs

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Input;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using MiniMvvm;
namespace ControlCatalog.ViewModels
{
public class CursorPageViewModel : ViewModelBase
{
public CursorPageViewModel()
{
StandardCursors = Enum.GetValues(typeof(StandardCursorType))
.Cast<StandardCursorType>()
.Select(x => new StandardCursorModel(x))
.ToList();
var loader = AvaloniaLocator.Current.GetService<IAssetLoader>();
var s = loader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png"));
var bitmap = new Bitmap(s);
CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16));
}
public IEnumerable<StandardCursorModel> StandardCursors { get; }
public Cursor CustomCursor { get; }
public class StandardCursorModel
{
public StandardCursorModel(StandardCursorType type)
{
Type = type;
Cursor = new Cursor(type);
}
public StandardCursorType Type { get; }
public Cursor Cursor { get; }
}
}
}

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

30
src/Avalonia.Base/EnumExtensions.cs

@ -11,10 +11,32 @@ namespace Avalonia
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe bool HasFlagCustom<T>(this T value, T flag) where T : unmanaged, Enum public static unsafe bool HasFlagCustom<T>(this T value, T flag) where T : unmanaged, Enum
{ {
var intValue = *(int*)&value; if (sizeof(T) == 1)
var intFlag = *(int*)&flag; {
var byteValue = Unsafe.As<T, byte>(ref value);
return (intValue & intFlag) == intFlag; var byteFlag = Unsafe.As<T, byte>(ref flag);
return (byteValue & byteFlag) == byteFlag;
}
else if (sizeof(T) == 2)
{
var shortValue = Unsafe.As<T, short>(ref value);
var shortFlag = Unsafe.As<T, short>(ref flag);
return (shortValue & shortFlag) == shortFlag;
}
else if (sizeof(T) == 4)
{
var intValue = Unsafe.As<T, int>(ref value);
var intFlag = Unsafe.As<T, int>(ref flag);
return (intValue & intFlag) == intFlag;
}
else if (sizeof(T) == 8)
{
var longValue = Unsafe.As<T, long>(ref value);
var longFlag = Unsafe.As<T, long>(ref flag);
return (longValue & longFlag) == longFlag;
}
else
throw new NotSupportedException("Enum with size of " + Unsafe.SizeOf<T>() + " are not supported");
} }
} }
} }

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;

4
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -372,8 +372,8 @@ namespace Avalonia.Utilities
const string implicitName = "op_Implicit"; const string implicitName = "op_Implicit";
const string explicitName = "op_Explicit"; const string explicitName = "op_Explicit";
bool allowImplicit = (operatorType & OperatorType.Implicit) != 0; bool allowImplicit = operatorType.HasFlagCustom(OperatorType.Implicit);
bool allowExplicit = (operatorType & OperatorType.Explicit) != 0; bool allowExplicit = operatorType.HasFlagCustom(OperatorType.Explicit);
foreach (MethodInfo method in fromType.GetMethods()) foreach (MethodInfo method in fromType.GetMethods())
{ {

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

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

@ -2595,7 +2595,7 @@ namespace Avalonia.Collections
/// <returns>Whether the specified flag is set</returns> /// <returns>Whether the specified flag is set</returns>
private bool CheckFlag(CollectionViewFlags flags) private bool CheckFlag(CollectionViewFlags flags)
{ {
return (_flags & flags) != 0; return _flags.HasFlagCustom(flags);
} }
/// <summary> /// <summary>
@ -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; }

7
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -0,0 +1,7 @@
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 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.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.
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

13
src/Avalonia.Controls/Button.cs

@ -80,6 +80,7 @@ namespace Avalonia.Controls
private ICommand _command; private ICommand _command;
private bool _commandCanExecute = true; private bool _commandCanExecute = true;
private KeyGesture _hotkey;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="Button"/> class. /// Initializes static members of the <see cref="Button"/> class.
@ -207,6 +208,11 @@ namespace Avalonia.Controls
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{ {
if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control
{
HotKey = _hotkey;
}
base.OnAttachedToLogicalTree(e); base.OnAttachedToLogicalTree(e);
if (Command != null) if (Command != null)
@ -217,6 +223,13 @@ namespace Avalonia.Controls
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{ {
// This will cause the hotkey manager to dispose the observer and the reference to this control
if (HotKey != null)
{
_hotkey = HotKey;
HotKey = null;
}
base.OnDetachedFromLogicalTree(e); base.OnDetachedFromLogicalTree(e);
if (Command != null) if (Command != null)

72
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()
{ {
@ -188,7 +206,7 @@ namespace Avalonia.Controls
return; return;
if (e.Key == Key.F4 || if (e.Key == Key.F4 ||
((e.Key == Key.Down || e.Key == Key.Up) && ((e.KeyModifiers & KeyModifiers.Alt) != 0))) ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasFlagCustom(KeyModifiers.Alt)))
{ {
IsDropDownOpen = !IsDropDownOpen; IsDropDownOpen = !IsDropDownOpen;
e.Handled = true; e.Handled = true;
@ -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;

2
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -61,7 +61,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
public virtual PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, 1); public virtual PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, 1);
public virtual void SetCursor(IPlatformHandle cursor) public virtual void SetCursor(ICursorImpl cursor)
{ {
} }

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

45
src/Avalonia.Controls/Grid.cs

@ -637,7 +637,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
internal bool MeasureOverrideInProgress internal bool MeasureOverrideInProgress
{ {
get { return (CheckFlagsAnd(Flags.MeasureOverrideInProgress)); } get { return CheckFlags(Flags.MeasureOverrideInProgress); }
set { SetFlags(value, Flags.MeasureOverrideInProgress); } set { SetFlags(value, Flags.MeasureOverrideInProgress); }
} }
@ -646,7 +646,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
internal bool ArrangeOverrideInProgress internal bool ArrangeOverrideInProgress
{ {
get { return (CheckFlagsAnd(Flags.ArrangeOverrideInProgress)); } get { return CheckFlags(Flags.ArrangeOverrideInProgress); }
set { SetFlags(value, Flags.ArrangeOverrideInProgress); } set { SetFlags(value, Flags.ArrangeOverrideInProgress); }
} }
@ -2350,25 +2350,12 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// CheckFlagsAnd returns <c>true</c> if all the flags in the /// CheckFlags returns <c>true</c> if all the flags in the
/// given bitmask are set on the object. /// given bitmask are set on the object.
/// </summary> /// </summary>
private bool CheckFlagsAnd(Flags flags) private bool CheckFlags(Flags flags)
{ {
return ((_flags & flags) == flags); return _flags.HasFlagCustom(flags);
}
/// <summary>
/// CheckFlagsOr returns <c>true</c> if at least one flag in the
/// given bitmask is set.
/// </summary>
/// <remarks>
/// If no bits are set in the given bitmask, the method returns
/// <c>true</c>.
/// </remarks>
private bool CheckFlagsOr(Flags flags)
{
return (flags == 0 || (_flags & flags) != 0);
} }
private static void OnShowGridLinesPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) private static void OnShowGridLinesPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
@ -2535,7 +2522,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private bool CellsStructureDirty private bool CellsStructureDirty
{ {
get { return (!CheckFlagsAnd(Flags.ValidCellsStructure)); } get { return !CheckFlags(Flags.ValidCellsStructure); }
set { SetFlags(!value, Flags.ValidCellsStructure); } set { SetFlags(!value, Flags.ValidCellsStructure); }
} }
@ -2544,7 +2531,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private bool ListenToNotifications private bool ListenToNotifications
{ {
get { return (CheckFlagsAnd(Flags.ListenToNotifications)); } get { return CheckFlags(Flags.ListenToNotifications); }
set { SetFlags(value, Flags.ListenToNotifications); } set { SetFlags(value, Flags.ListenToNotifications); }
} }
@ -2553,7 +2540,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private bool SizeToContentU private bool SizeToContentU
{ {
get { return (CheckFlagsAnd(Flags.SizeToContentU)); } get { return CheckFlags(Flags.SizeToContentU); }
set { SetFlags(value, Flags.SizeToContentU); } set { SetFlags(value, Flags.SizeToContentU); }
} }
@ -2562,7 +2549,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private bool SizeToContentV private bool SizeToContentV
{ {
get { return (CheckFlagsAnd(Flags.SizeToContentV)); } get { return CheckFlags(Flags.SizeToContentV); }
set { SetFlags(value, Flags.SizeToContentV); } set { SetFlags(value, Flags.SizeToContentV); }
} }
@ -2571,7 +2558,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private bool HasStarCellsU private bool HasStarCellsU
{ {
get { return (CheckFlagsAnd(Flags.HasStarCellsU)); } get { return CheckFlags(Flags.HasStarCellsU); }
set { SetFlags(value, Flags.HasStarCellsU); } set { SetFlags(value, Flags.HasStarCellsU); }
} }
@ -2580,7 +2567,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private bool HasStarCellsV private bool HasStarCellsV
{ {
get { return (CheckFlagsAnd(Flags.HasStarCellsV)); } get { return CheckFlags(Flags.HasStarCellsV); }
set { SetFlags(value, Flags.HasStarCellsV); } set { SetFlags(value, Flags.HasStarCellsV); }
} }
@ -2589,7 +2576,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private bool HasGroup3CellsInAutoRows private bool HasGroup3CellsInAutoRows
{ {
get { return (CheckFlagsAnd(Flags.HasGroup3CellsInAutoRows)); } get { return CheckFlags(Flags.HasGroup3CellsInAutoRows); }
set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); } set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); }
} }
@ -2803,10 +2790,10 @@ namespace Avalonia.Controls
internal LayoutTimeSizeType SizeTypeU; internal LayoutTimeSizeType SizeTypeU;
internal LayoutTimeSizeType SizeTypeV; internal LayoutTimeSizeType SizeTypeV;
internal int Next; internal int Next;
internal bool IsStarU { get { return ((SizeTypeU & LayoutTimeSizeType.Star) != 0); } } internal bool IsStarU => SizeTypeU.HasFlagCustom(LayoutTimeSizeType.Star);
internal bool IsAutoU { get { return ((SizeTypeU & LayoutTimeSizeType.Auto) != 0); } } internal bool IsAutoU => SizeTypeU.HasFlagCustom(LayoutTimeSizeType.Auto);
internal bool IsStarV { get { return ((SizeTypeV & LayoutTimeSizeType.Star) != 0); } } internal bool IsStarV => SizeTypeV.HasFlagCustom(LayoutTimeSizeType.Star);
internal bool IsAutoV { get { return ((SizeTypeV & LayoutTimeSizeType.Auto) != 0); } } internal bool IsAutoV => SizeTypeV.HasFlagCustom(LayoutTimeSizeType.Auto);
} }
/// <summary> /// <summary>

8
src/Avalonia.Controls/ListBox.cs

@ -135,8 +135,8 @@ namespace Avalonia.Controls
e.Handled = UpdateSelectionFromEventSource( e.Handled = UpdateSelectionFromEventSource(
e.Source, e.Source,
true, true,
(e.KeyModifiers & KeyModifiers.Shift) != 0, e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift),
(e.KeyModifiers & KeyModifiers.Control) != 0); e.KeyModifiers.HasFlagCustom(KeyModifiers.Control));
} }
} }
@ -154,8 +154,8 @@ namespace Avalonia.Controls
e.Handled = UpdateSelectionFromEventSource( e.Handled = UpdateSelectionFromEventSource(
e.Source, e.Source,
true, true,
(e.KeyModifiers & KeyModifiers.Shift) != 0, e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift),
(e.KeyModifiers & KeyModifiers.Control) != 0, e.KeyModifiers.HasFlagCustom(KeyModifiers.Control),
point.Properties.IsRightButtonPressed); point.Properties.IsRightButtonPressed);
} }
} }

54
src/Avalonia.Controls/MenuItem.cs

@ -102,6 +102,8 @@ namespace Avalonia.Controls
private ICommand? _command; private ICommand? _command;
private bool _commandCanExecute = true; private bool _commandCanExecute = true;
private Popup? _popup; private Popup? _popup;
private KeyGesture _hotkey;
private bool _isEmbeddedInMenu;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="MenuItem"/> class. /// Initializes static members of the <see cref="MenuItem"/> class.
@ -111,6 +113,7 @@ namespace Avalonia.Controls
SelectableMixin.Attach<MenuItem>(IsSelectedProperty); SelectableMixin.Attach<MenuItem>(IsSelectedProperty);
PressedMixin.Attach<MenuItem>(); PressedMixin.Attach<MenuItem>();
CommandProperty.Changed.Subscribe(CommandChanged); CommandProperty.Changed.Subscribe(CommandChanged);
CommandParameterProperty.Changed.Subscribe(CommandParameterChanged);
FocusableProperty.OverrideDefaultValue<MenuItem>(true); FocusableProperty.OverrideDefaultValue<MenuItem>(true);
HeaderProperty.Changed.AddClassHandler<MenuItem>((x, e) => x.HeaderChanged(e)); HeaderProperty.Changed.AddClassHandler<MenuItem>((x, e) => x.HeaderChanged(e));
IconProperty.Changed.AddClassHandler<MenuItem>((x, e) => x.IconChanged(e)); IconProperty.Changed.AddClassHandler<MenuItem>((x, e) => x.IconChanged(e));
@ -145,7 +148,7 @@ namespace Avalonia.Controls
{ {
var parent = x as Control; var parent = x as Control;
return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ?? return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ??
Observable.Return<DefinitionBase.SharedSizeScope?>(null); Observable.Return<DefinitionBase.SharedSizeScope?>(null);
}); });
this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope); this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope);
@ -273,7 +276,7 @@ namespace Avalonia.Controls
public bool IsTopLevel => Parent is Menu; public bool IsTopLevel => Parent is Menu;
/// <inheritdoc/> /// <inheritdoc/>
bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false; bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false;
/// <inheritdoc/> /// <inheritdoc/>
IMenuElement? IMenuItem.Parent => Parent as IMenuElement; IMenuElement? IMenuItem.Parent => Parent as IMenuElement;
@ -308,7 +311,7 @@ namespace Avalonia.Controls
.Select(x => x.ContainerControl) .Select(x => x.ContainerControl)
.OfType<IMenuItem>(); .OfType<IMenuItem>();
} }
} }
/// <summary> /// <summary>
/// Opens the submenu. /// Opens the submenu.
@ -335,18 +338,51 @@ namespace Avalonia.Controls
return new MenuItemContainerGenerator(this); return new MenuItemContainerGenerator(this);
} }
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (!_isEmbeddedInMenu)
{
//Normally the Menu's IMenuInteractionHandler is sending the click events for us
//However when the item is not embedded into a menu we need to send them ourselves.
RaiseEvent(new RoutedEventArgs(ClickEvent));
}
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{ {
if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control
{
HotKey = _hotkey;
}
base.OnAttachedToLogicalTree(e); base.OnAttachedToLogicalTree(e);
if (Command != null) if (Command != null)
{ {
Command.CanExecuteChanged += CanExecuteChanged; Command.CanExecuteChanged += CanExecuteChanged;
} }
var parent = Parent;
while (parent is MenuItem)
{
parent = parent.Parent;
}
_isEmbeddedInMenu = parent is IMenu;
} }
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{ {
// This will cause the hotkey manager to dispose the observer and the reference to this control
if (HotKey != null)
{
_hotkey = HotKey;
HotKey = null;
}
base.OnDetachedFromLogicalTree(e); base.OnDetachedFromLogicalTree(e);
if (Command != null) if (Command != null)
@ -493,6 +529,18 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Called when the <see cref="CommandParameter"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void CommandParameterChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is MenuItem menuItem)
{
menuItem.CanExecuteChanged(menuItem, EventArgs.Empty);
}
}
/// <summary> /// <summary>
/// Called when the <see cref="ICommand.CanExecuteChanged"/> event fires. /// Called when the <see cref="ICommand.CanExecuteChanged"/> event fires.
/// </summary> /// </summary>

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>

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

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

2
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@ -98,7 +98,7 @@ namespace Avalonia.Platform
/// Sets the cursor associated with the toplevel. /// Sets the cursor associated with the toplevel.
/// </summary> /// </summary>
/// <param name="cursor">The cursor. Use null for default cursor</param> /// <param name="cursor">The cursor. Use null for default cursor</param>
void SetCursor(IPlatformHandle cursor); void SetCursor(ICursorImpl cursor);
/// <summary> /// <summary>
/// Gets or sets a method called when the underlying implementation is destroyed. /// Gets or sets a method called when the underlying implementation is destroyed.

12
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@ -73,20 +73,20 @@ namespace Avalonia.Platform
{ {
if (effect == DragDropEffects.Copy || effect == DragDropEffects.Move || effect == DragDropEffects.Link || effect == DragDropEffects.None) if (effect == DragDropEffects.Copy || effect == DragDropEffects.Move || effect == DragDropEffects.Link || effect == DragDropEffects.None)
return effect; // No need to check for the modifiers. return effect; // No need to check for the modifiers.
if (effect.HasFlag(DragDropEffects.Link) && modifiers.HasFlag(RawInputModifiers.Alt)) if (effect.HasFlagCustom(DragDropEffects.Link) && modifiers.HasFlagCustom(RawInputModifiers.Alt))
return DragDropEffects.Link; return DragDropEffects.Link;
if (effect.HasFlag(DragDropEffects.Copy) && modifiers.HasFlag(RawInputModifiers.Control)) if (effect.HasFlagCustom(DragDropEffects.Copy) && modifiers.HasFlagCustom(RawInputModifiers.Control))
return DragDropEffects.Copy; return DragDropEffects.Copy;
return DragDropEffects.Move; return DragDropEffects.Move;
} }
private StandardCursorType GetCursorForDropEffect(DragDropEffects effects) private StandardCursorType GetCursorForDropEffect(DragDropEffects effects)
{ {
if (effects.HasFlag(DragDropEffects.Copy)) if (effects.HasFlagCustom(DragDropEffects.Copy))
return StandardCursorType.DragCopy; return StandardCursorType.DragCopy;
if (effects.HasFlag(DragDropEffects.Move)) if (effects.HasFlagCustom(DragDropEffects.Move))
return StandardCursorType.DragMove; return StandardCursorType.DragMove;
if (effects.HasFlag(DragDropEffects.Link)) if (effects.HasFlagCustom(DragDropEffects.Link))
return StandardCursorType.DragLink; return StandardCursorType.DragLink;
return StandardCursorType.No; return StandardCursorType.No;
} }
@ -161,7 +161,7 @@ namespace Avalonia.Platform
void CheckDraggingAccepted(RawInputModifiers changedMouseButton) void CheckDraggingAccepted(RawInputModifiers changedMouseButton)
{ {
if (_initialInputModifiers.Value.HasFlag(changedMouseButton)) if (_initialInputModifiers.Value.HasFlagCustom(changedMouseButton))
{ {
var result = RaiseEventAndUpdateCursor(RawDragEventType.Drop, e.Root, e.Position, e.InputModifiers); var result = RaiseEventAndUpdateCursor(RawDragEventType.Drop, e.Root, e.Position, e.InputModifiers);
UpdateCursor(null, DragDropEffects.None); UpdateCursor(null, DragDropEffects.None);

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

21
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@ -253,9 +253,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
{ {
public static void ValidateEdge(this PopupAnchor edge) public static void ValidateEdge(this PopupAnchor edge)
{ {
if (((edge & PopupAnchor.Left) != 0 && (edge & PopupAnchor.Right) != 0) if (edge.HasFlagCustom(PopupAnchor.Left) && edge.HasFlagCustom(PopupAnchor.Right) ||
|| edge.HasFlagCustom(PopupAnchor.Top) && edge.HasFlagCustom(PopupAnchor.Bottom))
((edge & PopupAnchor.Top) != 0 && (edge & PopupAnchor.Bottom) != 0))
throw new ArgumentException("Opposite edges specified"); throw new ArgumentException("Opposite edges specified");
} }
@ -266,25 +265,25 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
public static PopupAnchor Flip(this PopupAnchor edge) public static PopupAnchor Flip(this PopupAnchor edge)
{ {
var hmask = PopupAnchor.Left | PopupAnchor.Right; if (edge.HasFlagCustom(PopupAnchor.HorizontalMask))
var vmask = PopupAnchor.Top | PopupAnchor.Bottom; edge ^= PopupAnchor.HorizontalMask;
if ((edge & hmask) != 0)
edge ^= hmask; if (edge.HasFlagCustom(PopupAnchor.VerticalMask))
if ((edge & vmask) != 0) edge ^= PopupAnchor.VerticalMask;
edge ^= vmask;
return edge; return edge;
} }
public static PopupAnchor FlipX(this PopupAnchor edge) public static PopupAnchor FlipX(this PopupAnchor edge)
{ {
if ((edge & PopupAnchor.HorizontalMask) != 0) if (edge.HasFlagCustom(PopupAnchor.HorizontalMask))
edge ^= PopupAnchor.HorizontalMask; edge ^= PopupAnchor.HorizontalMask;
return edge; return edge;
} }
public static PopupAnchor FlipY(this PopupAnchor edge) public static PopupAnchor FlipY(this PopupAnchor edge)
{ {
if ((edge & PopupAnchor.VerticalMask) != 0) if (edge.HasFlagCustom(PopupAnchor.VerticalMask))
edge ^= PopupAnchor.VerticalMask; edge ^= PopupAnchor.VerticalMask;
return edge; return edge;
} }

48
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@ -42,16 +42,16 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
private static Point GetAnchorPoint(Rect anchorRect, PopupAnchor edge) private static Point GetAnchorPoint(Rect anchorRect, PopupAnchor edge)
{ {
double x, y; double x, y;
if ((edge & PopupAnchor.Left) != 0) if (edge.HasFlagCustom(PopupAnchor.Left))
x = anchorRect.X; x = anchorRect.X;
else if ((edge & PopupAnchor.Right) != 0) else if (edge.HasFlagCustom(PopupAnchor.Right))
x = anchorRect.Right; x = anchorRect.Right;
else else
x = anchorRect.X + anchorRect.Width / 2; x = anchorRect.X + anchorRect.Width / 2;
if ((edge & PopupAnchor.Top) != 0) if (edge.HasFlagCustom(PopupAnchor.Top))
y = anchorRect.Y; y = anchorRect.Y;
else if ((edge & PopupAnchor.Bottom) != 0) else if (edge.HasFlagCustom(PopupAnchor.Bottom))
y = anchorRect.Bottom; y = anchorRect.Bottom;
else else
y = anchorRect.Y + anchorRect.Height / 2; y = anchorRect.Y + anchorRect.Height / 2;
@ -61,16 +61,16 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
private static Point Gravitate(Point anchorPoint, Size size, PopupGravity gravity) private static Point Gravitate(Point anchorPoint, Size size, PopupGravity gravity)
{ {
double x, y; double x, y;
if ((gravity & PopupGravity.Left) != 0) if (gravity.HasFlagCustom(PopupGravity.Left))
x = -size.Width; x = -size.Width;
else if ((gravity & PopupGravity.Right) != 0) else if (gravity.HasFlagCustom(PopupGravity.Right))
x = 0; x = 0;
else else
x = -size.Width / 2; x = -size.Width / 2;
if ((gravity & PopupGravity.Top) != 0) if (gravity.HasFlagCustom(PopupGravity.Top))
y = -size.Height; y = -size.Height;
else if ((gravity & PopupGravity.Bottom) != 0) else if (gravity.HasFlagCustom(PopupGravity.Bottom))
y = 0; y = 0;
else else
y = -size.Height / 2; y = -size.Height / 2;
@ -125,21 +125,13 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
bool FitsInBounds(Rect rc, PopupAnchor edge = PopupAnchor.AllMask) bool FitsInBounds(Rect rc, PopupAnchor edge = PopupAnchor.AllMask)
{ {
if ((edge & PopupAnchor.Left) != 0 if (edge.HasFlagCustom(PopupAnchor.Left) && rc.X < bounds.X ||
&& rc.X < bounds.X) edge.HasFlagCustom(PopupAnchor.Top) && rc.Y < bounds.Y ||
return false; edge.HasFlagCustom(PopupAnchor.Right) && rc.Right > bounds.Right ||
edge.HasFlagCustom(PopupAnchor.Bottom) && rc.Bottom > bounds.Bottom)
if ((edge & PopupAnchor.Top) != 0 {
&& rc.Y < bounds.Y)
return false;
if ((edge & PopupAnchor.Right) != 0
&& rc.Right > bounds.Right)
return false;
if ((edge & PopupAnchor.Bottom) != 0
&& rc.Bottom > bounds.Bottom)
return false; return false;
}
return true; return true;
} }
@ -155,7 +147,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
// If flipping geometry and anchor is allowed and helps, use the flipped one, // If flipping geometry and anchor is allowed and helps, use the flipped one,
// otherwise leave it as is // otherwise leave it as is
if (!FitsInBounds(geo, PopupAnchor.HorizontalMask) if (!FitsInBounds(geo, PopupAnchor.HorizontalMask)
&& (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0) && constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.FlipX))
{ {
var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX()); var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX());
if (FitsInBounds(flipped, PopupAnchor.HorizontalMask)) if (FitsInBounds(flipped, PopupAnchor.HorizontalMask))
@ -163,7 +155,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
} }
// If sliding is allowed, try moving the rect into the bounds // If sliding is allowed, try moving the rect into the bounds
if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideX) != 0) if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.SlideX))
{ {
geo = geo.WithX(Math.Max(geo.X, bounds.X)); geo = geo.WithX(Math.Max(geo.X, bounds.X));
if (geo.Right > bounds.Right) if (geo.Right > bounds.Right)
@ -171,7 +163,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
} }
// Resize the rect horizontally if allowed. // Resize the rect horizontally if allowed.
if ((constraintAdjustment & PopupPositionerConstraintAdjustment.ResizeX) != 0) if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.ResizeX))
{ {
var unconstrainedRect = geo; var unconstrainedRect = geo;
@ -194,7 +186,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
// If flipping geometry and anchor is allowed and helps, use the flipped one, // If flipping geometry and anchor is allowed and helps, use the flipped one,
// otherwise leave it as is // otherwise leave it as is
if (!FitsInBounds(geo, PopupAnchor.VerticalMask) if (!FitsInBounds(geo, PopupAnchor.VerticalMask)
&& (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0) && constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.FlipY))
{ {
var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY()); var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY());
if (FitsInBounds(flipped, PopupAnchor.VerticalMask)) if (FitsInBounds(flipped, PopupAnchor.VerticalMask))
@ -202,7 +194,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
} }
// If sliding is allowed, try moving the rect into the bounds // If sliding is allowed, try moving the rect into the bounds
if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideY) != 0) if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.SlideY))
{ {
geo = geo.WithY(Math.Max(geo.Y, bounds.Y)); geo = geo.WithY(Math.Max(geo.Y, bounds.Y));
if (geo.Bottom > bounds.Bottom) if (geo.Bottom > bounds.Bottom)
@ -210,7 +202,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
} }
// Resize the rect vertically if allowed. // Resize the rect vertically if allowed.
if ((constraintAdjustment & PopupPositionerConstraintAdjustment.ResizeY) != 0) if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.ResizeY))
{ {
var unconstrainedRect = geo; var unconstrainedRect = geo;

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

@ -321,7 +321,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Gets a value indicating whether <see cref="SelectionMode.AlwaysSelected"/> is set. /// Gets a value indicating whether <see cref="SelectionMode.AlwaysSelected"/> is set.
/// </summary> /// </summary>
protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0; protected bool AlwaysSelected => SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected);
/// <inheritdoc/> /// <inheritdoc/>
public override void BeginInit() public override void BeginInit()
@ -487,7 +487,7 @@ namespace Avalonia.Controls.Primitives
if (ItemCount > 0 && if (ItemCount > 0 &&
Match(keymap.SelectAll) && Match(keymap.SelectAll) &&
SelectionMode.HasFlag(SelectionMode.Multiple)) SelectionMode.HasFlagCustom(SelectionMode.Multiple))
{ {
Selection.SelectAll(); Selection.SelectAll();
e.Handled = true; e.Handled = true;
@ -577,8 +577,8 @@ namespace Avalonia.Controls.Primitives
} }
var mode = SelectionMode; var mode = SelectionMode;
var multi = (mode & SelectionMode.Multiple) != 0; var multi = mode.HasFlagCustom(SelectionMode.Multiple);
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); var toggle = toggleModifier || mode.HasFlagCustom(SelectionMode.Toggle);
var range = multi && rangeModifier; var range = multi && rangeModifier;
if (!select) if (!select)

4
src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs

@ -53,8 +53,8 @@ namespace Avalonia.Controls
{ {
return _owner.GetElementImpl( return _owner.GetElementImpl(
index, index,
(options & ElementRealizationOptions.ForceCreate) != 0, options.HasFlagCustom(ElementRealizationOptions.ForceCreate),
(options & ElementRealizationOptions.SuppressAutoRecycle) != 0); options.HasFlagCustom(ElementRealizationOptions.SuppressAutoRecycle));
} }
protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index); protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index);

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)

47
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();
} }
} }
@ -585,7 +600,7 @@ namespace Avalonia.Controls
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>(); var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e)); bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
bool DetectSelection() => e.KeyModifiers.HasFlag(keymap.SelectionModifiers); bool DetectSelection() => e.KeyModifiers.HasFlagCustom(keymap.SelectionModifiers);
if (Match(keymap.SelectAll)) if (Match(keymap.SelectAll))
{ {
@ -703,7 +718,7 @@ namespace Avalonia.Controls
} }
else else
{ {
bool hasWholeWordModifiers = modifiers.HasFlag(keymap.WholeWordTextActionModifiers); bool hasWholeWordModifiers = modifiers.HasFlagCustom(keymap.WholeWordTextActionModifiers);
switch (e.Key) switch (e.Key)
{ {
case Key.Left: case Key.Left:

2
src/Avalonia.Controls/TopLevel.cs

@ -165,7 +165,7 @@ namespace Avalonia.Controls
this.GetObservable(PointerOverElementProperty) this.GetObservable(PointerOverElementProperty)
.Select( .Select(
x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty<Cursor>()) x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty<Cursor>())
.Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformCursor)); .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformImpl));
if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) if (((IStyleHost)this).StylingParent is IResourceHost applicationResources)
{ {

12
src/Avalonia.Controls/TreeView.cs

@ -412,7 +412,7 @@ namespace Avalonia.Controls
e.Handled = UpdateSelectionFromEventSource( e.Handled = UpdateSelectionFromEventSource(
e.Source, e.Source,
true, true,
(e.KeyModifiers & KeyModifiers.Shift) != 0); e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift));
} }
} }
@ -521,8 +521,8 @@ namespace Avalonia.Controls
e.Handled = UpdateSelectionFromEventSource( e.Handled = UpdateSelectionFromEventSource(
e.Source, e.Source,
true, true,
(e.KeyModifiers & KeyModifiers.Shift) != 0, e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift),
(e.KeyModifiers & KeyModifiers.Control) != 0, e.KeyModifiers.HasFlagCustom(KeyModifiers.Control),
point.Properties.IsRightButtonPressed); point.Properties.IsRightButtonPressed);
} }
} }
@ -558,9 +558,9 @@ namespace Avalonia.Controls
} }
var mode = SelectionMode; var mode = SelectionMode;
var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0; var toggle = toggleModifier || mode.HasFlagCustom(SelectionMode.Toggle);
var multi = (mode & SelectionMode.Multiple) != 0; var multi = mode.HasFlagCustom(SelectionMode.Multiple);
var range = multi && selectedContainer != null && rangeModifier; var range = multi && rangeModifier && selectedContainer != null;
if (rightButton) if (rightButton)
{ {

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

2
src/Avalonia.Controls/Utils/AncestorFinder.cs

@ -47,6 +47,8 @@ namespace Avalonia.Controls.Utils
public void Dispose() public void Dispose()
{ {
_child?.Dispose();
_subject.Dispose();
_disposable.Dispose(); _disposable.Dispose();
} }
} }

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)

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

@ -47,7 +47,7 @@ namespace Avalonia.DesignerSupport.Remote
var threading = new InternalPlatformThreadingInterface(); var threading = new InternalPlatformThreadingInterface();
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToSingleton<ClipboardStub>() .Bind<IClipboard>().ToSingleton<ClipboardStub>()
.Bind<IStandardCursorFactory>().ToSingleton<CursorFactoryStub>() .Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(Keyboard) .Bind<IKeyboardDevice>().ToConstant(Keyboard)
.Bind<IPlatformSettings>().ToConstant(instance) .Bind<IPlatformSettings>().ToConstant(instance)
.Bind<IPlatformThreadingInterface>().ToConstant(threading) .Bind<IPlatformThreadingInterface>().ToConstant(threading)

12
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -73,7 +73,7 @@ namespace Avalonia.DesignerSupport.Remote
public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1);
public void SetCursor(IPlatformHandle cursor) public void SetCursor(ICursorImpl cursor)
{ {
} }
@ -192,9 +192,15 @@ namespace Avalonia.DesignerSupport.Remote
public Task<object> GetDataAsync(string format) => Task.FromResult((object)null); public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
} }
class CursorFactoryStub : IStandardCursorFactory class CursorFactoryStub : ICursorFactory
{ {
public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "STUB"); public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub();
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub();
private class CursorStub : ICursorImpl
{
public void Dispose() { }
}
} }
class IconLoaderStub : IPlatformIconLoader class IconLoaderStub : IPlatformIconLoader

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

8
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -223,13 +223,13 @@ namespace Avalonia.FreeDesktop
return null; return null;
var lst = new List<string>(); var lst = new List<string>();
var mod = item.Gesture; var mod = item.Gesture;
if ((mod.KeyModifiers & KeyModifiers.Control) != 0) if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
lst.Add("Control"); lst.Add("Control");
if ((mod.KeyModifiers & KeyModifiers.Alt) != 0) if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
lst.Add("Alt"); lst.Add("Alt");
if ((mod.KeyModifiers & KeyModifiers.Shift) != 0) if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
lst.Add("Shift"); lst.Add("Shift");
if ((mod.KeyModifiers & KeyModifiers.Meta) != 0) if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
lst.Add("Super"); lst.Add("Super");
lst.Add(item.Gesture.Key.ToString()); lst.Add(item.Gesture.Key.ToString());
return new[] { lst.ToArray() }; return new[] { lst.ToArray() };

4
src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs

@ -33,11 +33,11 @@ namespace Avalonia.Headless.Vnc
{ {
Window?.MouseMove(pt); Window?.MouseMove(pt);
foreach (var btn in CheckedButtons) foreach (var btn in CheckedButtons)
if (_previousButtons.HasFlag(btn) && !buttons.HasFlag(btn)) if (_previousButtons.HasFlagCustom(btn) && !buttons.HasFlagCustom(btn))
Window?.MouseUp(pt, TranslateButton(btn), modifiers); Window?.MouseUp(pt, TranslateButton(btn), modifiers);
foreach (var btn in CheckedButtons) foreach (var btn in CheckedButtons)
if (!_previousButtons.HasFlag(btn) && buttons.HasFlag(btn)) if (!_previousButtons.HasFlagCustom(btn) && buttons.HasFlagCustom(btn))
Window?.MouseDown(pt, TranslateButton(btn), modifiers); Window?.MouseDown(pt, TranslateButton(btn), modifiers);
_previousButtons = buttons; _previousButtons = buttons;
}, DispatcherPriority.Input); }, DispatcherPriority.Input);

2
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -58,7 +58,7 @@ namespace Avalonia.Headless
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<IPlatformThreadingInterface>().ToConstant(new HeadlessPlatformThreadingInterface()) .Bind<IPlatformThreadingInterface>().ToConstant(new HeadlessPlatformThreadingInterface())
.Bind<IClipboard>().ToSingleton<HeadlessClipboardStub>() .Bind<IClipboard>().ToSingleton<HeadlessClipboardStub>()
.Bind<IStandardCursorFactory>().ToSingleton<HeadlessCursorFactoryStub>() .Bind<ICursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()
.Bind<IPlatformSettings>().ToConstant(new HeadlessPlatformSettingsStub()) .Bind<IPlatformSettings>().ToConstant(new HeadlessPlatformSettingsStub())
.Bind<ISystemDialogImpl>().ToSingleton<HeadlessSystemDialogsStub>() .Bind<ISystemDialogImpl>().ToSingleton<HeadlessSystemDialogsStub>()
.Bind<IPlatformIconLoader>().ToSingleton<HeadlessIconLoaderStub>() .Bind<IPlatformIconLoader>().ToSingleton<HeadlessIconLoaderStub>()

8
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -52,12 +52,14 @@ namespace Avalonia.Headless
} }
} }
class HeadlessCursorFactoryStub : IStandardCursorFactory class HeadlessCursorFactoryStub : ICursorFactory
{ {
public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub();
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub();
public IPlatformHandle GetCursor(StandardCursorType cursorType) private class CursorStub : ICursorImpl
{ {
return new PlatformHandle(new IntPtr((int)cursorType), "STUB"); public void Dispose() { }
} }
} }

2
src/Avalonia.Headless/HeadlessWindowImpl.cs

@ -67,7 +67,7 @@ namespace Avalonia.Headless
public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling);
public void SetCursor(IPlatformHandle cursor) public void SetCursor(ICursorImpl cursor)
{ {
} }

2
src/Avalonia.Input/AccessKeyHandler.cs

@ -177,7 +177,7 @@ namespace Avalonia.Input
{ {
bool menuIsOpen = MainMenu?.IsOpen == true; bool menuIsOpen = MainMenu?.IsOpen == true;
if ((e.KeyModifiers & KeyModifiers.Alt) != 0 || menuIsOpen) if (e.KeyModifiers.HasFlagCustom(KeyModifiers.Alt) || menuIsOpen)
{ {
// If any other key is pressed with the Alt key held down, or the main menu is open, // If any other key is pressed with the Alt key held down, or the main menu is open,
// find all controls who have registered that access key. // find all controls who have registered that access key.

4
src/Avalonia.Input/ApiCompatBaseline.txt

@ -0,0 +1,4 @@
Compat issues with assembly Avalonia.Input:
MembersMustExist : Member 'public Avalonia.Platform.IPlatformHandle Avalonia.Input.Cursor.PlatformCursor.get()' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Platform.IStandardCursorFactory' does not exist in the implementation but it does exist in the contract.
Total Issues: 2

39
src/Avalonia.Input/Cursors.cs → src/Avalonia.Input/Cursor.cs

@ -1,15 +1,11 @@
using System; using System;
using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
#nullable enable
namespace Avalonia.Input namespace Avalonia.Input
{ {
/*
=========================================================================================
NOTE: Cursors are NOT disposable and are cached in platform implementation.
To support loading custom cursors some measures about that should be taken beforehand
=========================================================================================
*/
public enum StandardCursorType public enum StandardCursorType
{ {
Arrow, Arrow,
@ -46,21 +42,28 @@ namespace Avalonia.Input
// SizeNorthEastSouthWest, // SizeNorthEastSouthWest,
} }
public class Cursor public class Cursor : IDisposable
{ {
public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow); public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow);
internal Cursor(IPlatformHandle platformCursor) internal Cursor(ICursorImpl platformImpl)
{ {
PlatformCursor = platformCursor; PlatformImpl = platformImpl;
} }
public Cursor(StandardCursorType cursorType) public Cursor(StandardCursorType cursorType)
: this(GetCursor(cursorType)) : this(GetCursorFactory().GetCursor(cursorType))
{
}
public Cursor(IBitmap cursor, PixelPoint hotSpot)
: this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot))
{ {
} }
public IPlatformHandle PlatformCursor { get; } public ICursorImpl PlatformImpl { get; }
public void Dispose() => PlatformImpl.Dispose();
public static Cursor Parse(string s) public static Cursor Parse(string s)
{ {
@ -69,16 +72,10 @@ namespace Avalonia.Input
throw new ArgumentException($"Unrecognized cursor type '{s}'."); throw new ArgumentException($"Unrecognized cursor type '{s}'.");
} }
private static IPlatformHandle GetCursor(StandardCursorType type) private static ICursorFactory GetCursorFactory()
{ {
var platform = AvaloniaLocator.Current.GetService<IStandardCursorFactory>(); return AvaloniaLocator.Current.GetService<ICursorFactory>() ??
throw new Exception("Could not create Cursor: ICursorFactory not registered.");
if (platform == null)
{
throw new Exception("Could not create Cursor: IStandardCursorFactory not registered.");
}
return platform.GetCursor(type);
} }
} }
} }

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;

12
src/Avalonia.Input/Platform/ICursorFactory.cs

@ -0,0 +1,12 @@
using Avalonia.Input;
#nullable enable
namespace Avalonia.Platform
{
public interface ICursorFactory
{
ICursorImpl GetCursor(StandardCursorType cursorType);
ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot);
}
}

14
src/Avalonia.Input/Platform/ICursorImpl.cs

@ -0,0 +1,14 @@
using System;
using Avalonia.Input;
#nullable enable
namespace Avalonia.Platform
{
/// <summary>
/// Represents a platform implementation of a <see cref="Cursor"/>.
/// </summary>
public interface ICursorImpl : IDisposable
{
}
}

9
src/Avalonia.Input/Platform/IStandardCursorFactory.cs

@ -1,9 +0,0 @@
using Avalonia.Input;
namespace Avalonia.Platform
{
public interface IStandardCursorFactory
{
IPlatformHandle GetCursor(StandardCursorType cursorType);
}
}

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

Loading…
Cancel
Save