diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings index 1361172fff..7060f4a62a 100644 --- a/Avalonia.sln.DotSettings +++ b/Avalonia.sln.DotSettings @@ -3,6 +3,7 @@ ExplicitlyExcluded ExplicitlyExcluded ExplicitlyExcluded + DO_NOT_SHOW HINT <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> diff --git a/build/Base.props b/build/Base.props index a60373ebb3..100c9088cd 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,5 +1,6 @@  + diff --git a/build/BuildTargets.targets b/build/BuildTargets.targets index 08ec039ec7..a5543cd050 100644 --- a/build/BuildTargets.targets +++ b/build/BuildTargets.targets @@ -2,6 +2,7 @@ $(MSBuildThisFileDirectory)\..\src\Avalonia.Build.Tasks\bin\$(Configuration)\netstandard2.0\Avalonia.Build.Tasks.dll true + true diff --git a/global.json b/global.json index 6d12c28846..1e599211d4 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", - "MSBuild.Sdk.Extras": "1.6.65", + "MSBuild.Sdk.Extras": "2.0.46", "AggregatePackage.NuGet.Sdk" : "0.1.12" } } diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index e54f3fa6a7..f1c7664c3e 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -22,6 +22,8 @@ struct IAvnGlContext; struct IAvnGlDisplay; struct IAvnGlSurfaceRenderTarget; struct IAvnGlSurfaceRenderingSession; +struct IAvnAppMenu; +struct IAvnAppMenuItem; struct AvnSize { @@ -173,6 +175,11 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) = 0; + virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) = 0; + virtual HRESULT SetAppMenu(IAvnAppMenu* menu) = 0; + virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0; + virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0; + virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) = 0; }; AVNCOM(IAvnString, 17) : IUnknown @@ -203,6 +210,8 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT SetCursor(IAvnCursor* cursor) = 0; virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0; virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0; + virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0; + virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0; virtual bool TryLock() = 0; virtual void Unlock() = 0; }; @@ -258,6 +267,7 @@ AVNCOM(IAvnWindowEvents, 06) : IAvnWindowBaseEvents AVNCOM(IAvnMacOptions, 07) : IUnknown { virtual HRESULT SetShowInDock(int show) = 0; + virtual HRESULT SetApplicationTitle (void* utf8string) = 0; }; AVNCOM(IAvnActionCallback, 08) : IUnknown @@ -367,4 +377,25 @@ AVNCOM(IAvnGlSurfaceRenderingSession, 16) : IUnknown virtual HRESULT GetScaling(double* ret) = 0; }; +AVNCOM(IAvnAppMenu, 17) : IUnknown +{ + virtual HRESULT AddItem (IAvnAppMenuItem* item) = 0; + virtual HRESULT RemoveItem (IAvnAppMenuItem* item) = 0; + virtual HRESULT SetTitle (void* utf8String) = 0; + virtual HRESULT Clear () = 0; +}; + +AVNCOM(IAvnPredicateCallback, 18) : IUnknown +{ + virtual bool Evaluate() = 0; +}; + +AVNCOM(IAvnAppMenuItem, 19) : IUnknown +{ + virtual HRESULT SetSubMenu (IAvnAppMenu* menu) = 0; + virtual HRESULT SetTitle (void* utf8String) = 0; + virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0; + virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0; +}; + extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative(); diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 1870ef7ab3..c0a49382a7 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -8,10 +8,12 @@ /* Begin PBXBuildFile section */ 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; + 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; }; 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; }; + 520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; }; 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; @@ -24,6 +26,7 @@ /* Begin PBXFileReference section */ 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; + 37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = ""; }; 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; 37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = ""; }; @@ -32,6 +35,7 @@ 37DDA9AF219330F8002E132B /* AvnString.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnString.mm; sourceTree = ""; }; 37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = ""; }; 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; }; + 520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = ""; }; 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; @@ -85,6 +89,8 @@ AB661C1F2148286E00291242 /* window.mm */, 37C09D8A21581EF2006A6758 /* window.h */, AB00E4F62147CA920032A60A /* main.mm */, + 37155CE3233C00EB0034DCE9 /* menu.h */, + 520624B222973F4100C4DCEF /* menu.mm */, 37A517B22159597E00FBA241 /* Screens.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, AB7A61F02147C815003C5833 /* Products */, @@ -107,6 +113,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 37155CE4233C00EB0034DCE9 /* menu.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -150,6 +157,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = AB7A61E62147C814003C5833; @@ -173,6 +181,7 @@ 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, + 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 45ec40c361..10534dea26 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -19,6 +19,13 @@ extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlFeature* GetGlFeature(); extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); +extern IAvnAppMenu* CreateAppMenu(); +extern IAvnAppMenuItem* CreateAppMenuItem(); +extern IAvnAppMenuItem* CreateAppMenuItemSeperator(); +extern void SetAppMenu (NSString* appName, IAvnAppMenu* appMenu); +extern IAvnAppMenu* GetAppMenu (); +extern NSMenuItem* GetAppMenuItem (); + extern void InitializeAvnApp(); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); @@ -40,4 +47,9 @@ template inline T* objc_cast(id from) { return nil; } +@interface ActionCallback : NSObject +- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback; +- (void) action; +@end + #endif diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 70bd1e67f6..9418782fd1 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -5,10 +5,121 @@ #define COM_GUIDS_MATERIALIZE #include "common.h" +static NSString* s_appTitle = @"Avalonia"; + +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +void SetProcessName(NSString* appTitle) { + s_appTitle = appTitle; + + CFStringRef process_name = (__bridge CFStringRef)appTitle; + + if (!process_name || CFStringGetLength(process_name) == 0) { + //NOTREACHED() << "SetProcessName given bad name."; + return; + } + + if (![NSThread isMainThread]) { + //NOTREACHED() << "Should only set process name from main thread."; + return; + } + + // Warning: here be dragons! This is SPI reverse-engineered from WebKit's + // plugin host, and could break at any time (although realistically it's only + // likely to break in a new major release). + // When 10.7 is available, check that this still works, and update this + // comment for 10.8. + + // Private CFType used in these LaunchServices calls. + typedef CFTypeRef PrivateLSASN; + typedef PrivateLSASN (*LSGetCurrentApplicationASNType)(); + typedef OSStatus (*LSSetApplicationInformationItemType)(int, PrivateLSASN, + CFStringRef, + CFStringRef, + CFDictionaryRef*); + + static LSGetCurrentApplicationASNType ls_get_current_application_asn_func = + NULL; + static LSSetApplicationInformationItemType + ls_set_application_information_item_func = NULL; + static CFStringRef ls_display_name_key = NULL; + + static bool did_symbol_lookup = false; + if (!did_symbol_lookup) { + did_symbol_lookup = true; + CFBundleRef launch_services_bundle = + CFBundleGetBundleWithIdentifier(CFSTR("com.apple.LaunchServices")); + if (!launch_services_bundle) { + //LOG(ERROR) << "Failed to look up LaunchServices bundle"; + return; + } + + ls_get_current_application_asn_func = + reinterpret_cast( + CFBundleGetFunctionPointerForName( + launch_services_bundle, CFSTR("_LSGetCurrentApplicationASN"))); + if (!ls_get_current_application_asn_func){} + //LOG(ERROR) << "Could not find _LSGetCurrentApplicationASN"; + + ls_set_application_information_item_func = + reinterpret_cast( + CFBundleGetFunctionPointerForName( + launch_services_bundle, + CFSTR("_LSSetApplicationInformationItem"))); + if (!ls_set_application_information_item_func){} + //LOG(ERROR) << "Could not find _LSSetApplicationInformationItem"; + + CFStringRef* key_pointer = reinterpret_cast( + CFBundleGetDataPointerForName(launch_services_bundle, + CFSTR("_kLSDisplayNameKey"))); + ls_display_name_key = key_pointer ? *key_pointer : NULL; + if (!ls_display_name_key){} + //LOG(ERROR) << "Could not find _kLSDisplayNameKey"; + + // Internally, this call relies on the Mach ports that are started up by the + // Carbon Process Manager. In debug builds this usually happens due to how + // the logging layers are started up; but in release, it isn't started in as + // much of a defined order. So if the symbols had to be loaded, go ahead + // and force a call to make sure the manager has been initialized and hence + // the ports are opened. + ProcessSerialNumber psn; + GetCurrentProcess(&psn); + } + if (!ls_get_current_application_asn_func || + !ls_set_application_information_item_func || + !ls_display_name_key) { + return; + } + + PrivateLSASN asn = ls_get_current_application_asn_func(); + // Constant used by WebKit; what exactly it means is unknown. + const int magic_session_constant = -2; + OSErr err = + ls_set_application_information_item_func(magic_session_constant, asn, + ls_display_name_key, + process_name, + NULL /* optional out param */); + //LOG_IF(ERROR, err) << "Call to set process name failed, err " << err; +} + class MacOptions : public ComSingleObject { public: FORWARD_IUNKNOWN() + + virtual HRESULT SetApplicationTitle(void* utf8String) override + { + auto appTitle = [NSString stringWithUTF8String:(const char*)utf8String]; + + [[NSProcessInfo processInfo] setProcessName:appTitle]; + + + SetProcessName(appTitle); + + return S_OK; + } + virtual HRESULT SetShowInDock(int show) override { AvnDesiredActivationPolicy = show @@ -17,8 +128,6 @@ public: } }; - - /// See "Using POSIX Threads in a Cocoa Application" section here: /// https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html#//apple_ref/doc/uid/20000738-125024 @interface ThreadingInitializer : NSObject @@ -43,8 +152,6 @@ public: close(_fds[0]); close(_fds[1]); } - - @end @@ -123,6 +230,42 @@ public: *ppv = rv; return S_OK; } + + virtual HRESULT CreateMenu (IAvnAppMenu** ppv) override + { + *ppv = ::CreateAppMenu(); + return S_OK; + } + + virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) override + { + *ppv = ::CreateAppMenuItem(); + return S_OK; + } + + virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) override + { + *ppv = ::CreateAppMenuItemSeperator(); + return S_OK; + } + + virtual HRESULT SetAppMenu (IAvnAppMenu* appMenu) override + { + ::SetAppMenu(s_appTitle, appMenu); + return S_OK; + } + + virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) override + { + if(retOut == nullptr) + { + return E_POINTER; + } + + *retOut = ::GetAppMenu(); + + return S_OK; + } }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h new file mode 100644 index 0000000000..befbe6a7e0 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -0,0 +1,80 @@ +// +// menu.h +// Avalonia.Native.OSX +// +// Created by Dan Walmsley on 01/08/2019. +// Copyright © 2019 Avalonia. All rights reserved. +// + +#ifndef menu_h +#define menu_h + +#include "common.h" + +class AvnAppMenuItem; +class AvnAppMenu; + +@interface AvnMenu : NSMenu // for some reason it doesnt detect nsmenu here but compiler doesnt complain +- (void)setMenu:(NSMenu*) menu; +@end + +@interface AvnMenuItem : NSMenuItem +- (id) initWithAvnAppMenuItem: (AvnAppMenuItem*)menuItem; +- (void)didSelectItem:(id)sender; +@end + +class AvnAppMenuItem : public ComSingleObject +{ +private: + NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem + IAvnActionCallback* _callback; + IAvnPredicateCallback* _predicate; + bool _isSeperator; + +public: + FORWARD_IUNKNOWN() + + AvnAppMenuItem(bool isSeperator); + + NSMenuItem* GetNative(); + + virtual HRESULT SetSubMenu (IAvnAppMenu* menu) override; + + virtual HRESULT SetTitle (void* utf8String) override; + + virtual HRESULT SetGesture (void* key, AvnInputModifiers modifiers) override; + + virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) override; + + bool EvaluateItemEnabled(); + + void RaiseOnClicked(); +}; + + +class AvnAppMenu : public ComSingleObject +{ +private: + AvnMenu* _native; + +public: + FORWARD_IUNKNOWN() + + AvnAppMenu(); + + AvnAppMenu(AvnMenu* native); + + AvnMenu* GetNative(); + + virtual HRESULT AddItem (IAvnAppMenuItem* item) override; + + virtual HRESULT RemoveItem (IAvnAppMenuItem* item) override; + + virtual HRESULT SetTitle (void* utf8String) override; + + virtual HRESULT Clear () override; +}; + + +#endif + diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm new file mode 100644 index 0000000000..1d2f075ccb --- /dev/null +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -0,0 +1,307 @@ + +#include "common.h" +#include "menu.h" +#include "window.h" + +@implementation AvnMenu +@end + +@implementation AvnMenuItem +{ + AvnAppMenuItem* _item; +} + +- (id) initWithAvnAppMenuItem: (AvnAppMenuItem*)menuItem +{ + if(self != nil) + { + _item = menuItem; + self = [super initWithTitle:@"" + action:@selector(didSelectItem:) + keyEquivalent:@""]; + + [self setEnabled:YES]; + + [self setTarget:self]; + } + + return self; +} + +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem +{ + if([self submenu] != nil) + { + return YES; + } + + return _item->EvaluateItemEnabled(); +} + +- (void)didSelectItem:(nullable id)sender +{ + _item->RaiseOnClicked(); +} +@end + +AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) +{ + _isSeperator = isSeperator; + + if(isSeperator) + { + _native = [NSMenuItem separatorItem]; + } + else + { + _native = [[AvnMenuItem alloc] initWithAvnAppMenuItem: this]; + } + + _callback = nullptr; +} + +NSMenuItem* AvnAppMenuItem::GetNative() +{ + return _native; +} + +HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu) +{ + auto nsMenu = dynamic_cast(menu)->GetNative(); + + [_native setSubmenu: nsMenu]; + + return S_OK; +} + +HRESULT AvnAppMenuItem::SetTitle (void* utf8String) +{ + if (utf8String != nullptr) + { + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + } + + return S_OK; +} + +HRESULT AvnAppMenuItem::SetGesture (void* key, AvnInputModifiers modifiers) +{ + NSEventModifierFlags flags = 0; + + if (modifiers & Control) + flags |= NSEventModifierFlagControl; + if (modifiers & Shift) + flags |= NSEventModifierFlagShift; + if (modifiers & Alt) + flags |= NSEventModifierFlagOption; + if (modifiers & Windows) + flags |= NSEventModifierFlagCommand; + + [_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]]; + [_native setKeyEquivalentModifierMask:flags]; + + return S_OK; +} + +HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) +{ + _predicate = predicate; + _callback = callback; + return S_OK; +} + +bool AvnAppMenuItem::EvaluateItemEnabled() +{ + if(_predicate != nullptr) + { + auto result = _predicate->Evaluate (); + + return result; + } + + return false; +} + +void AvnAppMenuItem::RaiseOnClicked() +{ + if(_callback != nullptr) + { + _callback->Run(); + } +} + +AvnAppMenu::AvnAppMenu() +{ + _native = [AvnMenu new]; +} + +AvnAppMenu::AvnAppMenu(AvnMenu* native) +{ + _native = native; +} + +AvnMenu* AvnAppMenu::GetNative() +{ + return _native; +} + +HRESULT AvnAppMenu::AddItem (IAvnAppMenuItem* item) +{ + auto avnMenuItem = dynamic_cast(item); + + if(avnMenuItem != nullptr) + { + [_native addItem: avnMenuItem->GetNative()]; + } + + return S_OK; +} + +HRESULT AvnAppMenu::RemoveItem (IAvnAppMenuItem* item) +{ + auto avnMenuItem = dynamic_cast(item); + + if(avnMenuItem != nullptr) + { + [_native removeItem:avnMenuItem->GetNative()]; + } + + return S_OK; +} + +HRESULT AvnAppMenu::SetTitle (void* utf8String) +{ + if (utf8String != nullptr) + { + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + } + + return S_OK; +} + +HRESULT AvnAppMenu::Clear() +{ + [_native removeAllItems]; + return S_OK; +} + +extern IAvnAppMenu* CreateAppMenu() +{ + @autoreleasepool + { + id menuBar = [NSMenu new]; + return new AvnAppMenu(menuBar); + } +} + +extern IAvnAppMenuItem* CreateAppMenuItem() +{ + @autoreleasepool + { + return new AvnAppMenuItem(false); + } +} + +extern IAvnAppMenuItem* CreateAppMenuItemSeperator() +{ + @autoreleasepool + { + return new AvnAppMenuItem(true); + } +} + +static IAvnAppMenu* s_appMenu = nullptr; +static NSMenuItem* s_appMenuItem = nullptr; + +extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) +{ + s_appMenu = menu; + + if(s_appMenu != nullptr) + { + auto nativeMenu = dynamic_cast(s_appMenu); + + auto currentMenu = [s_appMenuItem menu]; + + if (currentMenu != nullptr) + { + [currentMenu removeItem:s_appMenuItem]; + } + + s_appMenuItem = [nativeMenu->GetNative() itemAtIndex:0]; + + if (currentMenu == nullptr) + { + currentMenu = [s_appMenuItem menu]; + } + + [[s_appMenuItem menu] removeItem:s_appMenuItem]; + + [currentMenu insertItem:s_appMenuItem atIndex:0]; + + if([s_appMenuItem submenu] == nullptr) + { + [s_appMenuItem setSubmenu:[NSMenu new]]; + } + + auto appMenu = [s_appMenuItem submenu]; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // Services item and menu + auto servicesItem = [[NSMenuItem alloc] init]; + servicesItem.title = @"Services"; + NSMenu *servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"]; + servicesItem.submenu = servicesMenu; + [NSApplication sharedApplication].servicesMenu = servicesMenu; + [appMenu addItem:servicesItem]; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // Hide Application + auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"]; + + [appMenu addItem:hideItem]; + + // Hide Others + auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others" + action:@selector(hideOtherApplications:) + keyEquivalent:@"h"]; + + hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption; + [appMenu addItem:hideAllOthersItem]; + + // Show All + auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All" + action:@selector(unhideAllApplications:) + keyEquivalent:@""]; + + [appMenu addItem:showAllItem]; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // Quit Application + auto quitItem = [[NSMenuItem alloc] init]; + quitItem.title = [@"Quit " stringByAppendingString:appName]; + quitItem.keyEquivalent = @"q"; + quitItem.target = [AvnWindow class]; + quitItem.action = @selector(closeAll); + [appMenu addItem:quitItem]; + } + else + { + s_appMenuItem = nullptr; + } +} + +extern IAvnAppMenu* GetAppMenu () +{ + return s_appMenu; +} + +extern NSMenuItem* GetAppMenuItem () +{ + return s_appMenuItem; +} + + diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index 297097584a..e7abedae51 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -10,12 +10,6 @@ class PlatformThreadingInterface; -(Signaler*) init; @end - -@interface ActionCallback : NSObject -- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback; -- (void) action; -@end - @implementation ActionCallback { ComPtr _callback; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index e2221217f3..932bc56a2e 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -15,11 +15,13 @@ class WindowBaseImpl; @end @interface AvnWindow : NSWindow ++(void) closeAll; -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(void) setCanBecomeKeyAndMain; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; +-(void) applyMenu:(NSMenu *)menu; @end struct INSWindowHolder diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 3347d58004..2a5acaea91 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -5,6 +5,7 @@ #include "window.h" #include "KeyTransform.h" #include "cursor.h" +#include "menu.h" #include class SoftwareDrawingOperation @@ -63,9 +64,11 @@ public: SoftwareDrawingOperation CurrentSwDrawingOperation; AvnPoint lastPositionSet; NSString* _lastTitle; + IAvnAppMenu* _mainMenu; WindowBaseImpl(IAvnWindowBaseEvents* events) { + _mainMenu = nullptr; BaseEvents = events; View = [[AvnView alloc] initWithParent:this]; @@ -93,6 +96,7 @@ public: UpdateStyle(); [Window makeKeyAndOrderFront:Window]; + [NSApp activateIgnoringOtherApps:YES]; [Window setTitle:_lastTitle]; [Window setTitleVisibility:NSWindowTitleVisible]; @@ -122,6 +126,7 @@ public: if(Window != nullptr) { [Window makeKeyWindow]; + [NSApp activateIgnoringOtherApps:YES]; } } @@ -209,6 +214,31 @@ public: } } + virtual HRESULT SetMainMenu(IAvnAppMenu* menu) override + { + _mainMenu = menu; + + auto nativeMenu = dynamic_cast(menu); + + auto nsmenu = nativeMenu->GetNative(); + + [Window applyMenu:nsmenu]; + + return S_OK; + } + + virtual HRESULT ObtainMainMenu(IAvnAppMenu** ret) override + { + if(ret == nullptr) + { + return E_POINTER; + } + + *ret = _mainMenu; + + return S_OK; + } + virtual bool TryLock() override { @autoreleasepool @@ -825,8 +855,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent if(type == Wheel) { - delta.X = [event scrollingDeltaX] / 5; - delta.Y = [event scrollingDeltaY] / 5; + delta.X = [event scrollingDeltaX] / 50; + delta.Y = [event scrollingDeltaY] / 50; if(delta.X == 0 && delta.Y == 0) { @@ -1042,6 +1072,19 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent ComPtr _parent; bool _canBecomeKeyAndMain; bool _closed; + NSMenu* _menu; + bool _isAppMenuApplied; +} + ++(void)closeAll +{ + NSArray* windows = [NSArray arrayWithArray:[NSApp windows]]; + auto numWindows = [windows count]; + + for(int i = 0; i < numWindows; i++) + { + [[windows objectAtIndex:i] performClose:nil]; + } } - (void)dealloc @@ -1065,6 +1108,32 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } +-(void) applyMenu:(NSMenu *)menu +{ + if(menu == nullptr) + { + menu = [NSMenu new]; + } + + _menu = menu; + + if ([self isKeyWindow]) + { + auto appMenu = ::GetAppMenuItem(); + + if(appMenu != nullptr) + { + [[appMenu menu] removeItem:appMenu]; + + [_menu insertItem:appMenu atIndex:0]; + + _isAppMenuApplied = true; + } + + [NSApp setMenu:menu]; + } +} + -(void) setCanBecomeKeyAndMain { _canBecomeKeyAndMain = true; @@ -1157,6 +1226,24 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if([self activateAppropriateChild: true]) { + if(_menu == nullptr) + { + _menu = [NSMenu new]; + } + + auto appMenu = ::GetAppMenuItem(); + + if(appMenu != nullptr) + { + [[appMenu menu] removeItem:appMenu]; + + [_menu insertItem:appMenu atIndex:0]; + + _isAppMenuApplied = true; + } + + [NSApp setMenu:_menu]; + _parent->BaseEvents->Activated(); [super becomeKeyWindow]; } @@ -1201,6 +1288,28 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if(_parent) _parent->BaseEvents->Deactivated(); + + auto appMenuItem = ::GetAppMenuItem(); + + if(appMenuItem != nullptr) + { + auto appMenu = ::GetAppMenu(); + + auto nativeAppMenu = dynamic_cast(appMenu); + + [[appMenuItem menu] removeItem:appMenuItem]; + + [nativeAppMenu->GetNative() addItem:appMenuItem]; + + [NSApp setMenu:nativeAppMenu->GetNative()]; + } + else + { + [NSApp setMenu:nullptr]; + } + + // remove window menu items from appmenu? + [super resignKeyWindow]; } diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index a455e2b3fc..552713f94b 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -53,6 +53,7 @@ $(IntermediateOutputPath)/Avalonia/references $(IntermediateOutputPath)/Avalonia/original.dll + false screenshot). Now you can write code and markup that will work on multiple platforms! -For those without Visual Studio, starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). +For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). Avalonia is delivered via NuGet package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed)) -Use these commands in Package Manager console to install Avalonia manually: +Use these commands in the Package Manager console to install Avalonia manually: ``` Install-Package Avalonia Install-Package Avalonia.Desktop diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index bc76a39f08..5b82e2caee 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v8.0 + v9.0 Properties\AndroidManifest.xml diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 157609088f..40d001a195 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -20,7 +20,7 @@ namespace ControlCatalog.Android { if (Avalonia.Application.Current == null) { - AppBuilder.Configure(new App()) + AppBuilder.Configure() .UseAndroid() .SetupWithoutStarting(); Content = new MainView(); diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index d683092edf..854cae484c 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -55,7 +55,11 @@ namespace ControlCatalog.NetCore public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() - .With(new X11PlatformOptions { EnableMultiTouch = true }) + .With(new X11PlatformOptions + { + EnableMultiTouch = true, + UseDBusMenu = true + }) .With(new Win32PlatformOptions { EnableMultitouch = true, diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 2f6d25c089..335c460b40 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -2,9 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.App"> - - - + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 07c42c60c4..4fc63ea054 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,4 +1,6 @@ +using System; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; @@ -6,9 +8,20 @@ namespace ControlCatalog { public class App : Application { + private NativeMenu _recentMenu; + public override void Initialize() { AvaloniaXamlLoader.Load(this); + + Name = "Avalonia"; + + _recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu; + } + + public void OnOpenClicked(object sender, EventArgs args) + { + _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); } public override void OnFrameworkInitializationCompleted() diff --git a/samples/ControlCatalog/DecoratedWindow.xaml b/samples/ControlCatalog/DecoratedWindow.xaml index cb6016b324..8e4c97b7f0 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml +++ b/samples/ControlCatalog/DecoratedWindow.xaml @@ -3,6 +3,31 @@ x:Class="ControlCatalog.DecoratedWindow" Title="Avalonia Control Gallery" xmlns:local="clr-namespace:ControlCatalog" HasSystemDecorations="False" Name="Window"> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/DecoratedWindow.xaml.cs b/samples/ControlCatalog/DecoratedWindow.xaml.cs index 2e7218b956..bdf5b8fbee 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml.cs +++ b/samples/ControlCatalog/DecoratedWindow.xaml.cs @@ -18,18 +18,18 @@ namespace ControlCatalog { var ctl = this.FindControl(name); ctl.Cursor = new Cursor(cursor); - ctl.PointerPressed += delegate + ctl.PointerPressed += (i, e) => { - PlatformImpl?.BeginResizeDrag(edge); + PlatformImpl?.BeginResizeDrag(edge, e); }; } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - this.FindControl("TitleBar").PointerPressed += delegate + this.FindControl("TitleBar").PointerPressed += (i, e) => { - PlatformImpl?.BeginMoveDrag(); + PlatformImpl?.BeginMoveDrag(e); }; SetupSide("Left", StandardCursorType.LeftSide, WindowEdge.West); SetupSide("Right", StandardCursorType.RightSide, WindowEdge.East); diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 9527ac3b4e..6088f2ec57 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -8,7 +8,36 @@ xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" x:Class="ControlCatalog.MainWindow"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 95c65ed92f..7b0ee897c4 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -14,6 +14,7 @@ namespace ControlCatalog public class MainWindow : Window { private WindowNotificationManager _notificationArea; + private NativeMenu _recentMenu; public MainWindow() { @@ -29,8 +30,21 @@ namespace ControlCatalog }; DataContext = new MainWindowViewModel(_notificationArea); + _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; } + public void OnOpenClicked(object sender, EventArgs args) + { + _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); + } + + public void OnCloseClicked(object sender, EventArgs args) + { + Close(); + } + + + private void InitializeComponent() { // TODO: iOS does not support dynamically loading assemblies diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index e1a5cf2c5a..868f0df6ad 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -3,8 +3,11 @@ x:Class="ControlCatalog.Pages.MenuPage"> Menu + Exported menu fallback + (Should be only visible on platforms without desktop-global menu bar) + A window menu - + (this T builder) where T : AppBuilderBase, new() { - builder.UseWindowingSubsystem(() => Android.AndroidPlatform.Initialize(builder.Instance), "Android"); + builder.UseWindowingSubsystem(() => Android.AndroidPlatform.Initialize(builder.ApplicationType), "Android"); builder.UseSkia(); return builder; } @@ -41,7 +41,7 @@ namespace Avalonia.Android _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity; } - public static void Initialize(Avalonia.Application app) + public static void Initialize(Type appType) { AvaloniaLocator.CurrentMutable .Bind().ToTransient() @@ -55,7 +55,7 @@ namespace Avalonia.Android .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new RenderLoop()) .Bind().ToSingleton() - .Bind().ToConstant(new AssetLoader(app.GetType().Assembly)); + .Bind().ToConstant(new AssetLoader(appType.Assembly)); SkiaPlatform.Initialize(); ((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext) diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 0089ea3b8d..c170e8449c 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,6 +1,6 @@  - monoandroid80 + monoandroid90 true diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index 1b2b205d45..2f95a6e4bd 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v8.0 + v9.0 Properties\AndroidManifest.xml diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 8cc512d132..2450f1a3a1 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -326,8 +326,6 @@ namespace Avalonia VerifyAccess(); - var description = GetDescription(source); - if (property.IsDirect) { if (property.IsReadOnly) @@ -335,12 +333,12 @@ namespace Avalonia throw new ArgumentException($"The property {property.Name} is readonly."); } - Logger.Verbose( - LogArea.Property, + Logger.TryGet(LogEventLevel.Verbose)?.Log( + LogArea.Property, this, - "Bound {Property} to {Binding} with priority LocalValue", - property, - description); + "Bound {Property} to {Binding} with priority LocalValue", + property, + GetDescription(source)); if (_directBindings == null) { @@ -351,12 +349,12 @@ namespace Avalonia } else { - Logger.Verbose( + Logger.TryGet(LogEventLevel.Verbose)?.Log( LogArea.Property, this, "Bound {Property} to {Binding} with priority {Priority}", property, - description, + GetDescription(source), priority); return Values.AddBinding(property, source, priority); @@ -406,7 +404,7 @@ namespace Avalonia { RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority); - Logger.Verbose( + Logger.TryGet(LogEventLevel.Verbose)?.Log( LogArea.Property, this, "{Property} changed from {$Old} to {$Value} with priority {Priority}", @@ -458,8 +456,7 @@ namespace Avalonia /// The binding error. protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e) { - Logger.Log( - LogEventLevel.Warning, + Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Binding, this, "Error in binding to {Target}.{Property}: {Message}", @@ -812,7 +809,7 @@ namespace Avalonia /// The priority. private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority priority) { - Logger.Verbose( + Logger.TryGet(LogEventLevel.Verbose)?.Log( LogArea.Property, this, "Set {Property} to {$Value} with priority {Priority}", diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 393482cccf..ad1cefd4ea 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -192,9 +192,9 @@ namespace Avalonia { return observable.Subscribe(e => { - if (e.Sender is TTarget) + if (e.Sender is TTarget target) { - action((TTarget)e.Sender, e); + action(target, e); } }); } @@ -207,6 +207,7 @@ namespace Avalonia /// The property changed observable. /// Given a TTarget, returns the handler. /// A disposable that can be used to terminate the subscription. + [Obsolete("Use overload taking Action.")] public static IDisposable AddClassHandler( this IObservable observable, Func> handler) @@ -238,9 +239,7 @@ namespace Avalonia Func> handler) where TTarget : class { - var target = e.Sender as TTarget; - - if (target != null) + if (e.Sender is TTarget target) { handler(target)(e); } diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 7f8396cdfa..986e2cf012 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -165,7 +165,7 @@ namespace Avalonia.Data.Core } else { - Logger.Error( + Logger.TryGet(LogEventLevel.Error)?.Log( LogArea.Binding, this, "Could not convert FallbackValue {FallbackValue} to {Type}", diff --git a/src/Avalonia.Base/Logging/ILogSink.cs b/src/Avalonia.Base/Logging/ILogSink.cs index 0ed4eede8f..8b5751b0af 100644 --- a/src/Avalonia.Base/Logging/ILogSink.cs +++ b/src/Avalonia.Base/Logging/ILogSink.cs @@ -8,6 +8,77 @@ namespace Avalonia.Logging /// public interface ILogSink { + /// + /// Checks if given log level is enabled. + /// + /// The log event level. + /// if given log level is enabled. + bool IsEnabled(LogEventLevel level); + + /// + /// Logs an event. + /// + /// The log event level. + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate); + + /// + /// Logs an event. + /// + /// The log event level. + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0); + + /// + /// Logs an event. + /// + /// The log event level. + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1); + + /// + /// Logs an event. + /// + /// The log event level. + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + /// Message property value. + void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2); + /// /// Logs a new event. /// diff --git a/src/Avalonia.Base/Logging/Logger.cs b/src/Avalonia.Base/Logging/Logger.cs index b1132ff4a9..c895c70094 100644 --- a/src/Avalonia.Base/Logging/Logger.cs +++ b/src/Avalonia.Base/Logging/Logger.cs @@ -1,8 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Runtime.CompilerServices; - namespace Avalonia.Logging { /// @@ -16,124 +14,43 @@ namespace Avalonia.Logging public static ILogSink Sink { get; set; } /// - /// Logs an event. + /// Checks if given log level is enabled. /// /// The log event level. - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Log( - LogEventLevel level, - string area, - object source, - string messageTemplate, - params object[] propertyValues) - { - Sink?.Log(level, area, source, messageTemplate, propertyValues); - } - - /// - /// Logs an event with the level. - /// - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Verbose( - string area, - object source, - string messageTemplate, - params object[] propertyValues) + /// if given log level is enabled. + public static bool IsEnabled(LogEventLevel level) { - Log(LogEventLevel.Verbose, area, source, messageTemplate, propertyValues); + return Sink?.IsEnabled(level) == true; } /// - /// Logs an event with the level. + /// Returns parametrized logging sink if given log level is enabled. /// - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Debug( - string area, - object source, - string messageTemplate, - params object[] propertyValues) + /// The log event level. + /// Log sink or if log level is not enabled. + public static ParametrizedLogger? TryGet(LogEventLevel level) { - Log(LogEventLevel.Debug, area, source, messageTemplate, propertyValues); - } + if (!IsEnabled(level)) + { + return null; + } - /// - /// Logs an event with the level. - /// - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Information( - string area, - object source, - string messageTemplate, - params object[] propertyValues) - { - Log(LogEventLevel.Information, area, source, messageTemplate, propertyValues); + return new ParametrizedLogger(Sink, level); } /// - /// Logs an event with the level. + /// Returns parametrized logging sink if given log level is enabled. /// - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Warning( - string area, - object source, - string messageTemplate, - params object[] propertyValues) + /// The log event level. + /// Log sink that is valid only if method returns . + /// if logger was obtained successfully. + public static bool TryGet(LogEventLevel level, out ParametrizedLogger outLogger) { - Log(LogEventLevel.Warning, area, source, messageTemplate, propertyValues); - } + ParametrizedLogger? logger = TryGet(level); - /// - /// Logs an event with the level. - /// - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Error( - string area, - object source, - string messageTemplate, - params object[] propertyValues) - { - Log(LogEventLevel.Error, area, source, messageTemplate, propertyValues); - } + outLogger = logger.GetValueOrDefault(); - /// - /// Logs an event with the level. - /// - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// The message property values. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Fatal( - string area, - object source, - string messageTemplate, - params object[] propertyValues) - { - Log(LogEventLevel.Fatal, area, source, messageTemplate, propertyValues); + return logger.HasValue; } } } diff --git a/src/Avalonia.Base/Logging/ParametrizedLogger.cs b/src/Avalonia.Base/Logging/ParametrizedLogger.cs new file mode 100644 index 0000000000..1550cc1b40 --- /dev/null +++ b/src/Avalonia.Base/Logging/ParametrizedLogger.cs @@ -0,0 +1,174 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Runtime.CompilerServices; + +namespace Avalonia.Logging +{ + /// + /// Logger sink parametrized for given logging level. + /// + public readonly struct ParametrizedLogger + { + private readonly ILogSink _sink; + private readonly LogEventLevel _level; + + public ParametrizedLogger(ILogSink sink, LogEventLevel level) + { + _sink = sink; + _level = level; + } + + /// + /// Checks if this logger can be used. + /// + public bool IsValid => _sink != null; + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate) + { + _sink.Log(_level, area, source, messageTemplate); + } + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate, + T0 propertyValue0) + { + _sink.Log(_level, area, source, messageTemplate, propertyValue0); + } + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1) + { + _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1); + } + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2) + { + _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + /// Message property value. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2, + T3 propertyValue3) + { + _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3); + } + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + /// Message property value. + /// Message property value. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2, + T3 propertyValue3, + T4 propertyValue4) + { + _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3, propertyValue4); + } + + /// + /// Logs an event. + /// + /// The area that the event originates. + /// The object from which the event originates. + /// The message template. + /// Message property value. + /// Message property value. + /// Message property value. + /// Message property value. + /// Message property value. + /// Message property value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log( + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2, + T3 propertyValue3, + T4 propertyValue4, + T5 propertyValue5) + { + _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3, propertyValue4, propertyValue5); + } + } +} diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 2871271062..61184ef7b1 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -301,7 +301,7 @@ namespace Avalonia } else { - Logger.Error( + Logger.TryGet(LogEventLevel.Error)?.Log( LogArea.Binding, Owner, "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs index 61303dbbfe..39ee3f6bca 100644 --- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -34,7 +34,7 @@ namespace Avalonia.Build.Tasks var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input, File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), - ProjectDirectory, OutputPath); + ProjectDirectory, OutputPath, VerifyIl); if (!res.Success) return false; if (!res.WrittenFile) @@ -66,6 +66,8 @@ namespace Avalonia.Build.Tasks public string ProjectDirectory { get; set; } public string OutputPath { get; set; } + + public bool VerifyIl { get; set; } public IBuildEngine BuildEngine { get; set; } public ITaskHost HostObject { get; set; } diff --git a/src/Avalonia.Build.Tasks/Program.cs b/src/Avalonia.Build.Tasks/Program.cs index d356b15408..1909c4c6ec 100644 --- a/src/Avalonia.Build.Tasks/Program.cs +++ b/src/Avalonia.Build.Tasks/Program.cs @@ -28,7 +28,8 @@ namespace Avalonia.Build.Tasks ReferencesFilePath = args[1], OutputPath = args[2], BuildEngine = new ConsoleBuildEngine(), - ProjectDirectory = Directory.GetCurrentDirectory() + ProjectDirectory = Directory.GetCurrentDirectory(), + VerifyIl = true }.Execute() ? 0 : 2; diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 0dd52c9b48..e348eb0fbc 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -40,7 +40,7 @@ namespace Avalonia.Build.Tasks } public static CompileResult Compile(IBuildEngine engine, string input, string[] references, string projectDirectory, - string output) + string output, bool verifyIl) { var typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input); var asm = typeSystem.TargetAssemblyDefinition; @@ -65,7 +65,7 @@ namespace Avalonia.Build.Tasks var contextClass = XamlIlContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem, xamlLanguage); - var compiler = new AvaloniaXamlIlCompiler(compilerConfig, contextClass); + var compiler = new AvaloniaXamlIlCompiler(compilerConfig, contextClass) { EnableIlVerification = verifyIl }; var editorBrowsableAttribute = typeSystem .GetTypeReference(typeSystem.FindType("System.ComponentModel.EditorBrowsableAttribute")) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 490a724eda..b65fd2a8b7 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -723,29 +723,29 @@ namespace Avalonia.Controls PseudoClass(IsValidProperty, x => !x, ":invalid"); - ItemsProperty.Changed.AddClassHandler(x => x.OnItemsPropertyChanged); - CanUserResizeColumnsProperty.Changed.AddClassHandler(x => x.OnCanUserResizeColumnsChanged); - ColumnWidthProperty.Changed.AddClassHandler(x => x.OnColumnWidthChanged); - RowBackgroundProperty.Changed.AddClassHandler(x => x.OnRowBackgroundChanged); - AlternatingRowBackgroundProperty.Changed.AddClassHandler(x => x.OnRowBackgroundChanged); - FrozenColumnCountProperty.Changed.AddClassHandler(x => x.OnFrozenColumnCountChanged); - GridLinesVisibilityProperty.Changed.AddClassHandler(x => x.OnGridLinesVisibilityChanged); - HeadersVisibilityProperty.Changed.AddClassHandler(x => x.OnHeadersVisibilityChanged); - HorizontalGridLinesBrushProperty.Changed.AddClassHandler(x => x.OnHorizontalGridLinesBrushChanged); - IsReadOnlyProperty.Changed.AddClassHandler(x => x.OnIsReadOnlyChanged); - MaxColumnWidthProperty.Changed.AddClassHandler(x => x.OnMaxColumnWidthChanged); - MinColumnWidthProperty.Changed.AddClassHandler(x => x.OnMinColumnWidthChanged); - RowHeightProperty.Changed.AddClassHandler(x => x.OnRowHeightChanged); - RowHeaderWidthProperty.Changed.AddClassHandler(x => x.OnRowHeaderWidthChanged); - SelectionModeProperty.Changed.AddClassHandler(x => x.OnSelectionModeChanged); - VerticalGridLinesBrushProperty.Changed.AddClassHandler(x => x.OnVerticalGridLinesBrushChanged); - SelectedIndexProperty.Changed.AddClassHandler(x => x.OnSelectedIndexChanged); - SelectedItemProperty.Changed.AddClassHandler(x => x.OnSelectedItemChanged); - IsEnabledProperty.Changed.AddClassHandler(x => x.DataGrid_IsEnabledChanged); - AreRowGroupHeadersFrozenProperty.Changed.AddClassHandler(x => x.OnAreRowGroupHeadersFrozenChanged); - RowDetailsTemplateProperty.Changed.AddClassHandler(x => x.OnRowDetailsTemplateChanged); - RowDetailsVisibilityModeProperty.Changed.AddClassHandler(x => x.OnRowDetailsVisibilityModeChanged); - AutoGenerateColumnsProperty.Changed.AddClassHandler(x => x.OnAutoGenerateColumnsChanged); + ItemsProperty.Changed.AddClassHandler((x, e) => x.OnItemsPropertyChanged(e)); + CanUserResizeColumnsProperty.Changed.AddClassHandler((x, e) => x.OnCanUserResizeColumnsChanged(e)); + ColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnColumnWidthChanged(e)); + RowBackgroundProperty.Changed.AddClassHandler((x, e) => x.OnRowBackgroundChanged(e)); + AlternatingRowBackgroundProperty.Changed.AddClassHandler((x, e) => x.OnRowBackgroundChanged(e)); + FrozenColumnCountProperty.Changed.AddClassHandler((x, e) => x.OnFrozenColumnCountChanged(e)); + GridLinesVisibilityProperty.Changed.AddClassHandler((x, e) => x.OnGridLinesVisibilityChanged(e)); + HeadersVisibilityProperty.Changed.AddClassHandler((x, e) => x.OnHeadersVisibilityChanged(e)); + HorizontalGridLinesBrushProperty.Changed.AddClassHandler((x, e) => x.OnHorizontalGridLinesBrushChanged(e)); + IsReadOnlyProperty.Changed.AddClassHandler((x, e) => x.OnIsReadOnlyChanged(e)); + MaxColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnMaxColumnWidthChanged(e)); + MinColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnMinColumnWidthChanged(e)); + RowHeightProperty.Changed.AddClassHandler((x, e) => x.OnRowHeightChanged(e)); + RowHeaderWidthProperty.Changed.AddClassHandler((x, e) => x.OnRowHeaderWidthChanged(e)); + SelectionModeProperty.Changed.AddClassHandler((x, e) => x.OnSelectionModeChanged(e)); + VerticalGridLinesBrushProperty.Changed.AddClassHandler((x, e) => x.OnVerticalGridLinesBrushChanged(e)); + SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.OnSelectedIndexChanged(e)); + SelectedItemProperty.Changed.AddClassHandler((x, e) => x.OnSelectedItemChanged(e)); + IsEnabledProperty.Changed.AddClassHandler((x, e) => x.DataGrid_IsEnabledChanged(e)); + AreRowGroupHeadersFrozenProperty.Changed.AddClassHandler((x, e) => x.OnAreRowGroupHeadersFrozenChanged(e)); + RowDetailsTemplateProperty.Changed.AddClassHandler((x, e) => x.OnRowDetailsTemplateChanged(e)); + RowDetailsVisibilityModeProperty.Changed.AddClassHandler((x, e) => x.OnRowDetailsVisibilityModeChanged(e)); + AutoGenerateColumnsProperty.Changed.AddClassHandler((x, e) => x.OnAutoGenerateColumnsChanged(e)); } /// @@ -3533,7 +3533,7 @@ namespace Avalonia.Controls if (AreColumnHeadersVisible && _vScrollBar != null && _vScrollBar.IsVisible) { - _topRightCornerHeader.IsVisible = true; ; + _topRightCornerHeader.IsVisible = true; } else { @@ -5594,7 +5594,7 @@ namespace Avalonia.Controls { // This will trigger a call to this method via Cells_SizeChanged for // which no processing is needed. - _vScrollBar.IsVisible = true; ; + _vScrollBar.IsVisible = true; if (_vScrollBar.DesiredSize.Width == 0) { // We need to know the width for the rest of layout to work correctly so measure it now diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index a21583b38e..e56c534f50 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls static DataGridCell() { PointerPressedEvent.AddClassHandler( - x => x.DataGridCell_PointerPressed, handledEventsToo: true); + (x,e) => x.DataGridCell_PointerPressed(e), handledEventsToo: true); } public DataGridCell() { } @@ -219,4 +219,4 @@ namespace Avalonia.Controls } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 6cb0807e29..d1651b2d09 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls static DataGridColumnHeader() { - AreSeparatorsVisibleProperty.Changed.AddClassHandler(x => x.OnAreSeparatorsVisibleChanged); + AreSeparatorsVisibleProperty.Changed.AddClassHandler((x,e) => x.OnAreSeparatorsVisibleChanged(e)); } /// @@ -468,7 +468,7 @@ namespace Avalonia.Controls private void DataGridColumnHeader_PointerReleased(object sender, PointerReleasedEventArgs e) { - if (OwningColumn == null || e.Handled || !IsEnabled || e.MouseButton != MouseButton.Left) + if (OwningColumn == null || e.Handled || !IsEnabled || e.InitialPressMouseButton != MouseButton.Left) { return; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index 04a1575486..c9924660be 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -116,10 +116,10 @@ namespace Avalonia.Controls static DataGridRow() { - HeaderProperty.Changed.AddClassHandler(x => x.OnHeaderChanged); - DetailsTemplateProperty.Changed.AddClassHandler(x => x.OnDetailsTemplateChanged); - AreDetailsVisibleProperty.Changed.AddClassHandler(x => x.OnAreDetailsVisibleChanged); - PointerPressedEvent.AddClassHandler(x => x.DataGridRow_PointerPressed, handledEventsToo: true); + HeaderProperty.Changed.AddClassHandler((x, e) => x.OnHeaderChanged(e)); + DetailsTemplateProperty.Changed.AddClassHandler((x, e) => x.OnDetailsTemplateChanged(e)); + AreDetailsVisibleProperty.Changed.AddClassHandler((x, e) => x.OnAreDetailsVisibleChanged(e)); + PointerPressedEvent.AddClassHandler((x, e) => x.DataGridRow_PointerPressed(e), handledEventsToo: true); } /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 716997f62c..7dafef9d8b 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -109,7 +109,7 @@ namespace Avalonia.Controls static DataGridRowGroupHeader() { - SublevelIndentProperty.Changed.AddClassHandler(x => x.OnSublevelIndentChanged); + SublevelIndentProperty.Changed.AddClassHandler((x,e) => x.OnSublevelIndentChanged(e)); } /// @@ -446,4 +446,4 @@ namespace Avalonia.Controls } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 307ddd284c..d9be9171ed 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -18,7 +18,9 @@ namespace Avalonia.Controls { private static bool s_setupWasAlreadyCalled; private Action _optionsInitializers; - + private Func _appFactory; + private IApplicationLifetime _lifetime; + /// /// Gets or sets the instance. /// @@ -30,10 +32,15 @@ namespace Avalonia.Controls public Action RuntimePlatformServicesInitializer { get; private set; } /// - /// Gets or sets the instance being initialized. + /// Gets the instance being initialized. /// - public Application Instance { get; protected set; } - + public Application Instance { get; private set; } + + /// + /// Gets the type of the Instance (even if it's not created yet) + /// + public Type ApplicationType { get; private set; } + /// /// Gets or sets a method to call the initialize the windowing subsystem. /// @@ -76,20 +83,11 @@ namespace Avalonia.Controls public static TAppBuilder Configure() where TApp : Application, new() { - return Configure(new TApp()); - } - - /// - /// Begin configuring an . - /// - /// An instance. - public static TAppBuilder Configure(Application app) - { - AvaloniaLocator.CurrentMutable.BindToSelf(app); - return new TAppBuilder() { - Instance = app, + ApplicationType = typeof(TApp), + // Needed for CoreRT compatibility + _appFactory = () => new TApp() }; } @@ -157,6 +155,18 @@ namespace Avalonia.Controls return Self; } + /// + /// Sets up the platform-specific services for the application and initialized it with a particular lifetime, but does not run it. + /// + /// + /// + public TAppBuilder SetupWithLifetime(IApplicationLifetime lifetime) + { + _lifetime = lifetime; + Setup(); + return Self; + } + /// /// Specifies a windowing subsystem to use. /// @@ -254,11 +264,6 @@ namespace Avalonia.Controls /// private void Setup() { - if (Instance == null) - { - throw new InvalidOperationException("No App instance configured."); - } - if (RuntimePlatformServicesInitializer == null) { throw new InvalidOperationException("No runtime platform services configured."); @@ -285,6 +290,9 @@ namespace Avalonia.Controls WindowingSubsystemInitializer(); RenderingSubsystemInitializer(); AfterPlatformServicesSetupCallback(Self); + Instance = _appFactory(); + Instance.ApplicationLifetime = _lifetime; + AvaloniaLocator.CurrentMutable.BindToSelf(Instance); Instance.RegisterServices(); Instance.Initialize(); AfterSetupCallback(Self); diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 382106de65..ce60a0f0b9 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -32,7 +32,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode + public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode { /// /// The application-global data templates. @@ -210,5 +210,22 @@ namespace Avalonia { ResourcesChanged?.Invoke(this, e); } + + private string _name; + /// + /// Defines Name property + /// + public static readonly DirectProperty NameProperty = + AvaloniaProperty.RegisterDirect("Name", o => o.Name, (o, v) => o.Name = v); + + /// + /// Application name to be used for various platform-specific purposes + /// + public string Name + { + get => _name; + set => SetAndRaise(NameProperty, ref _name, value); + } + } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index abca7a64ee..2533191ae4 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -125,8 +125,7 @@ namespace Avalonia where T : AppBuilderBase, new() { var lifetime = new ClassicDesktopStyleApplicationLifetime(builder.Instance) {ShutdownMode = shutdownMode}; - builder.Instance.ApplicationLifetime = lifetime; - builder.SetupWithoutStarting(); + builder.SetupWithLifetime(lifetime); return lifetime.Start(args); } } diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 1e2fc9f9d0..ce4358648b 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -805,15 +805,15 @@ namespace Avalonia.Controls { FocusableProperty.OverrideDefaultValue(true); - MinimumPopulateDelayProperty.Changed.AddClassHandler(x => x.OnMinimumPopulateDelayChanged); - IsDropDownOpenProperty.Changed.AddClassHandler(x => x.OnIsDropDownOpenChanged); - SelectedItemProperty.Changed.AddClassHandler(x => x.OnSelectedItemPropertyChanged); - TextProperty.Changed.AddClassHandler(x => x.OnTextPropertyChanged); - SearchTextProperty.Changed.AddClassHandler(x => x.OnSearchTextPropertyChanged); - FilterModeProperty.Changed.AddClassHandler(x => x.OnFilterModePropertyChanged); - ItemFilterProperty.Changed.AddClassHandler(x => x.OnItemFilterPropertyChanged); - ItemsProperty.Changed.AddClassHandler(x => x.OnItemsPropertyChanged); - IsEnabledProperty.Changed.AddClassHandler(x => x.OnControlIsEnabledChanged); + MinimumPopulateDelayProperty.Changed.AddClassHandler((x,e) => x.OnMinimumPopulateDelayChanged(e)); + IsDropDownOpenProperty.Changed.AddClassHandler((x,e) => x.OnIsDropDownOpenChanged(e)); + SelectedItemProperty.Changed.AddClassHandler((x,e) => x.OnSelectedItemPropertyChanged(e)); + TextProperty.Changed.AddClassHandler((x,e) => x.OnTextPropertyChanged(e)); + SearchTextProperty.Changed.AddClassHandler((x,e) => x.OnSearchTextPropertyChanged(e)); + FilterModeProperty.Changed.AddClassHandler((x,e) => x.OnFilterModePropertyChanged(e)); + ItemFilterProperty.Changed.AddClassHandler((x,e) => x.OnItemFilterPropertyChanged(e)); + ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); + IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnControlIsEnabledChanged(e)); } /// diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index d39ea73828..78d02e200f 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -294,7 +294,7 @@ namespace Avalonia.Controls { base.OnPointerReleased(e); - if (IsPressed && e.MouseButton == MouseButton.Left) + if (IsPressed && e.InitialPressMouseButton == MouseButton.Left) { IsPressed = false; e.Handled = true; diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 56805e1d6a..beafab3edf 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -1565,7 +1565,7 @@ namespace Avalonia.Controls protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); - if (!HasFocusInternal && e.MouseButton == MouseButton.Left) + if (!HasFocusInternal && e.InitialPressMouseButton == MouseButton.Left) { FocusManager.Instance.Focus(this); } @@ -2057,18 +2057,17 @@ namespace Avalonia.Controls static Calendar() { - IsEnabledProperty.Changed.AddClassHandler(x => x.OnIsEnabledChanged); - FirstDayOfWeekProperty.Changed.AddClassHandler(x => x.OnFirstDayOfWeekChanged); - IsTodayHighlightedProperty.Changed.AddClassHandler(x => x.OnIsTodayHighlightedChanged); - DisplayModeProperty.Changed.AddClassHandler(x => x.OnDisplayModePropertyChanged); - SelectionModeProperty.Changed.AddClassHandler(x => x.OnSelectionModeChanged); - SelectedDateProperty.Changed.AddClassHandler(x => x.OnSelectedDateChanged); - DisplayDateProperty.Changed.AddClassHandler(x => x.OnDisplayDateChanged); - DisplayDateStartProperty.Changed.AddClassHandler(x => x.OnDisplayDateStartChanged); - DisplayDateEndProperty.Changed.AddClassHandler(x => x.OnDisplayDateEndChanged); - KeyDownEvent.AddClassHandler(x => x.Calendar_KeyDown); - KeyUpEvent.AddClassHandler(x => x.Calendar_KeyUp); - + IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnIsEnabledChanged(e)); + FirstDayOfWeekProperty.Changed.AddClassHandler((x,e) => x.OnFirstDayOfWeekChanged(e)); + IsTodayHighlightedProperty.Changed.AddClassHandler((x,e) => x.OnIsTodayHighlightedChanged(e)); + DisplayModeProperty.Changed.AddClassHandler((x,e) => x.OnDisplayModePropertyChanged(e)); + SelectionModeProperty.Changed.AddClassHandler((x,e) => x.OnSelectionModeChanged(e)); + SelectedDateProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateChanged(e)); + DisplayDateProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateChanged(e)); + DisplayDateStartProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateStartChanged(e)); + DisplayDateEndProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateEndChanged(e)); + KeyDownEvent.AddClassHandler((x,e) => x.Calendar_KeyDown(e)); + KeyUpEvent.AddClassHandler((x,e) => x.Calendar_KeyUp(e)); } /// diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index 53852defb3..a273e68d56 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -173,7 +173,7 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); - if (e.MouseButton == MouseButton.Left) + if (e.InitialPressMouseButton == MouseButton.Left) CalendarLeftMouseButtonUp?.Invoke(this, e); } } diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index cb2a98e5ca..e62a1ce1f4 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -231,7 +231,7 @@ namespace Avalonia.Controls.Primitives { base.OnPointerReleased(e); - if (e.MouseButton == MouseButton.Left) + if (e.InitialPressMouseButton == MouseButton.Left) CalendarDayButtonMouseUp?.Invoke(this, e); } } diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index 55797ae1e3..841b73cd92 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -393,14 +393,14 @@ namespace Avalonia.Controls { FocusableProperty.OverrideDefaultValue(true); - DisplayDateProperty.Changed.AddClassHandler(x => x.OnDisplayDateChanged); - DisplayDateStartProperty.Changed.AddClassHandler(x => x.OnDisplayDateStartChanged); - DisplayDateEndProperty.Changed.AddClassHandler(x => x.OnDisplayDateEndChanged); - IsDropDownOpenProperty.Changed.AddClassHandler(x => x.OnIsDropDownOpenChanged); - SelectedDateProperty.Changed.AddClassHandler(x => x.OnSelectedDateChanged); - SelectedDateFormatProperty.Changed.AddClassHandler(x => x.OnSelectedDateFormatChanged); - CustomDateFormatStringProperty.Changed.AddClassHandler(x => x.OnCustomDateFormatStringChanged); - TextProperty.Changed.AddClassHandler(x => x.OnTextChanged); + DisplayDateProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateChanged(e)); + DisplayDateStartProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateStartChanged(e)); + DisplayDateEndProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateEndChanged(e)); + IsDropDownOpenProperty.Changed.AddClassHandler((x,e) => x.OnIsDropDownOpenChanged(e)); + SelectedDateProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateChanged(e)); + SelectedDateFormatProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateFormatChanged(e)); + CustomDateFormatStringProperty.Changed.AddClassHandler((x,e) => x.OnCustomDateFormatStringChanged(e)); + TextProperty.Changed.AddClassHandler((x,e) => x.OnTextChanged(e)); } /// /// Initializes a new instance of the diff --git a/src/Avalonia.Controls/Carousel.cs b/src/Avalonia.Controls/Carousel.cs index 069bf40820..ebc8890721 100644 --- a/src/Avalonia.Controls/Carousel.cs +++ b/src/Avalonia.Controls/Carousel.cs @@ -84,17 +84,5 @@ namespace Avalonia.Controls --SelectedIndex; } } - - /// - protected override void OnKeyDown(KeyEventArgs e) - { - // Ignore key presses. - } - - /// - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - // Ignore pointer presses. - } } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index a70d26624c..c2cf20b32d 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -65,8 +65,8 @@ namespace Avalonia.Controls { ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); FocusableProperty.OverrideDefaultValue(true); - SelectedItemProperty.Changed.AddClassHandler(x => x.SelectedItemChanged); - KeyDownEvent.AddClassHandler(x => x.OnKeyDown, Interactivity.RoutingStrategies.Tunnel); + SelectedItemProperty.Changed.AddClassHandler((x,e) => x.SelectedItemChanged(e)); + KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); } /// diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 02d7890404..bb3cc4585b 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -43,7 +43,7 @@ namespace Avalonia.Controls static ContentControl() { - ContentProperty.Changed.AddClassHandler(x => x.ContentChanged); + ContentProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e)); } /// diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index a5025df82d..5dfa5863f5 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -192,7 +192,7 @@ namespace Avalonia.Controls e.Handled = true; } - if (e.MouseButton == MouseButton.Right) + if (e.InitialPressMouseButton == MouseButton.Right) { if (contextMenu.CancelOpening()) return; diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index 50b387e636..2588b7cc11 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -56,7 +56,7 @@ namespace Avalonia.Controls { ErrorsProperty.Changed.Subscribe(ErrorsChanged); HasErrorsProperty.Changed.Subscribe(HasErrorsChanged); - TemplatedParentProperty.Changed.AddClassHandler(x => x.OnTemplatedParentChange); + TemplatedParentProperty.Changed.AddClassHandler((x, e) => x.OnTemplatedParentChange(e)); } private void OnTemplatedParentChange(AvaloniaPropertyChangedEventArgs e) diff --git a/src/Avalonia.Controls/Decorator.cs b/src/Avalonia.Controls/Decorator.cs index 15651b918e..6f16870b9f 100644 --- a/src/Avalonia.Controls/Decorator.cs +++ b/src/Avalonia.Controls/Decorator.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls static Decorator() { AffectsMeasure(ChildProperty, PaddingProperty); - ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); + ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); } /// diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index b67d9ef89a..9da803d16d 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -9,7 +9,7 @@ namespace Avalonia.Controls { public DropDown() { - Logger.Warning(LogArea.Control, this, "DropDown is deprecated: Use ComboBox"); + Logger.TryGet(LogEventLevel.Warning)?.Log(LogArea.Control, this, "DropDown is deprecated: Use ComboBox"); } Type IStyleable.StyleKey => typeof(ComboBox); @@ -20,7 +20,7 @@ namespace Avalonia.Controls { public DropDownItem() { - Logger.Warning(LogArea.Control, this, "DropDownItem is deprecated: Use ComboBoxItem"); + Logger.TryGet(LogEventLevel.Warning)?.Log(LogArea.Control, this, "DropDownItem is deprecated: Use ComboBoxItem"); } Type IStyleable.StyleKey => typeof(ComboBoxItem); diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 1fa9798784..b2a442b6cc 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls PseudoClass(IsExpandedProperty, ":expanded"); - IsExpandedProperty.Changed.AddClassHandler(x => x.OnIsExpandedChanged); + IsExpandedProperty.Changed.AddClassHandler((x, e) => x.OnIsExpandedChanged(e)); } public IPageTransition ContentTransition diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 4f41b0bf1e..8ecfe349f8 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -24,13 +24,14 @@ namespace Avalonia.Controls { static Grid() { - IsSharedSizeScopeProperty.Changed.AddClassHandler(DefinitionBase.OnIsSharedSizeScopePropertyChanged); ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); - ColumnProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); - ColumnSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); - RowProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); - RowSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + IsSharedSizeScopeProperty.Changed.AddClassHandler(DefinitionBase.OnIsSharedSizeScopePropertyChanged); + ColumnProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + ColumnSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + RowProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + RowSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 0fe7291835..b027da6d0c 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -64,8 +64,8 @@ namespace Avalonia.Controls /// static ItemsControl() { - ItemsProperty.Changed.AddClassHandler(x => x.ItemsChanged); - ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); + ItemsProperty.Changed.AddClassHandler((x, e) => x.ItemsChanged(e)); + ItemTemplateProperty.Changed.AddClassHandler((x, e) => x.ItemTemplateChanged(e)); } /// @@ -489,18 +489,20 @@ namespace Avalonia.Controls bool wrap) { IInputElement result; + var c = from; do { - result = container.GetControl(direction, from, wrap); + result = container.GetControl(direction, c, wrap); + from = from ?? result; if (result?.Focusable == true) { return result; } - from = result; - } while (from != null); + c = result; + } while (c != null && c != from); return null; } diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 1430c39c76..db67a24159 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -28,11 +28,13 @@ namespace Avalonia.Controls ClipToBoundsProperty.OverrideDefaultValue(true); LayoutTransformProperty.Changed - .AddClassHandler(x => x.OnLayoutTransformChanged); + .AddClassHandler((x, e) => x.OnLayoutTransformChanged(e)); ChildProperty.Changed - .AddClassHandler(x => x.OnChildChanged); - UseRenderTransformProperty.Changed.AddClassHandler(x => x.OnUseRenderTransformPropertyChanged); + .AddClassHandler((x, e) => x.OnChildChanged(e)); + + UseRenderTransformProperty.Changed + .AddClassHandler((x, e) => x.OnUseRenderTransformPropertyChanged(e)); } /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index 8eed58bb4d..be677b5479 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls /// static MenuBase() { - MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); + MenuItem.SubmenuOpenedEvent.AddClassHandler((x, e) => x.OnSubmenuOpened(e)); } /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 33a708b6a5..3ba0007f6b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -102,13 +102,13 @@ namespace Avalonia.Controls SelectableMixin.Attach(IsSelectedProperty); CommandProperty.Changed.Subscribe(CommandChanged); FocusableProperty.OverrideDefaultValue(true); - HeaderProperty.Changed.AddClassHandler(x => x.HeaderChanged); - IconProperty.Changed.AddClassHandler(x => x.IconChanged); - IsSelectedProperty.Changed.AddClassHandler(x => x.IsSelectedChanged); + HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); + IconProperty.Changed.AddClassHandler((x, e) => x.IconChanged(e)); + IsSelectedProperty.Changed.AddClassHandler((x, e) => x.IsSelectedChanged(e)); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); - ClickEvent.AddClassHandler(x => x.OnClick); - SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); - IsSubMenuOpenProperty.Changed.AddClassHandler(x => x.SubMenuOpenChanged); + ClickEvent.AddClassHandler((x, e) => x.OnClick(e)); + SubmenuOpenedEvent.AddClassHandler((x, e) => x.OnSubmenuOpened(e)); + IsSubMenuOpenProperty.Changed.AddClassHandler((x, e) => x.SubMenuOpenChanged(e)); } public MenuItem() @@ -337,7 +337,7 @@ namespace Avalonia.Controls { base.OnPointerEnter(e); - var point = e.GetPointerPoint(null); + var point = e.GetCurrentPoint(null); RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position, e.Timestamp, point.Properties, e.KeyModifiers)); } @@ -347,7 +347,7 @@ namespace Avalonia.Controls { base.OnPointerLeave(e); - var point = e.GetPointerPoint(null); + var point = e.GetCurrentPoint(null); RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position, e.Timestamp, point.Properties, e.KeyModifiers)); } diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs new file mode 100644 index 0000000000..5d3a4526cc --- /dev/null +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls.Platform; +using Avalonia.Data; + +namespace Avalonia.Controls +{ + public partial class NativeMenu + { + public static readonly AttachedProperty IsNativeMenuExportedProperty = + AvaloniaProperty.RegisterAttached("IsNativeMenuExported"); + + public static bool GetIsNativeMenuExported(TopLevel tl) => tl.GetValue(IsNativeMenuExportedProperty); + + private static readonly AttachedProperty s_nativeMenuInfoProperty = + AvaloniaProperty.RegisterAttached("___NativeMenuInfo"); + + class NativeMenuInfo + { + public bool ChangingIsExported { get; set; } + public ITopLevelNativeMenuExporter Exporter { get; } + + public NativeMenuInfo(TopLevel target) + { + Exporter = (target.PlatformImpl as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter; + if (Exporter != null) + { + Exporter.OnIsNativeMenuExportedChanged += delegate + { + SetIsNativeMenuExported(target, Exporter.IsNativeMenuExported); + }; + } + } + } + + static NativeMenuInfo GetInfo(TopLevel target) + { + var rv = target.GetValue(s_nativeMenuInfoProperty); + if (rv == null) + { + target.SetValue(s_nativeMenuInfoProperty, rv = new NativeMenuInfo(target)); + SetIsNativeMenuExported(target, rv.Exporter?.IsNativeMenuExported ?? false); + } + + return rv; + } + + static void SetIsNativeMenuExported(TopLevel tl, bool value) + { + GetInfo(tl).ChangingIsExported = true; + tl.SetValue(IsNativeMenuExportedProperty, value); + } + + public static readonly AttachedProperty MenuProperty + = AvaloniaProperty.RegisterAttached("Menu", validate: + (o, v) => + { + if(!(o is Application || o is TopLevel)) + throw new InvalidOperationException("NativeMenu.Menu property isn't valid on "+o.GetType()); + return v; + }); + + public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu); + public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); + + static NativeMenu() + { + // This is needed because of the lack of attached direct properties + IsNativeMenuExportedProperty.Changed.Subscribe(args => + { + var info = GetInfo((TopLevel)args.Sender); + if (!info.ChangingIsExported) + throw new InvalidOperationException("IsNativeMenuExported property is read-only"); + info.ChangingIsExported = false; + }); + MenuProperty.Changed.Subscribe(args => + { + if (args.Sender is TopLevel tl) + { + GetInfo(tl).Exporter?.SetNativeMenu((NativeMenu)args.NewValue); + } + }); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs new file mode 100644 index 0000000000..54aa2b5e3d --- /dev/null +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Collections; +using Avalonia.Data; +using Avalonia.LogicalTree; +using Avalonia.Metadata; + +namespace Avalonia.Controls +{ + public partial class NativeMenu : AvaloniaObject, IEnumerable + { + private readonly AvaloniaList _items = + new AvaloniaList { ResetBehavior = ResetBehavior.Remove }; + private NativeMenuItem _parent; + [Content] + public IList Items => _items; + + public NativeMenu() + { + _items.Validate = Validator; + _items.CollectionChanged += ItemsChanged; + } + + private void Validator(NativeMenuItemBase obj) + { + if (obj.Parent != null) + throw new InvalidOperationException("NativeMenuItem already has a parent"); + } + + private void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if(e.OldItems!=null) + foreach (NativeMenuItemBase i in e.OldItems) + i.Parent = null; + if(e.NewItems!=null) + foreach (NativeMenuItemBase i in e.NewItems) + i.Parent = this; + } + + public static readonly DirectProperty ParentProperty = + AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); + + public NativeMenuItem Parent + { + get => _parent; + set => SetAndRaise(ParentProperty, ref _parent, value); + } + + public void Add(NativeMenuItemBase item) => _items.Add(item); + + public IEnumerator GetEnumerator() => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenuBar.cs b/src/Avalonia.Controls/NativeMenuBar.cs new file mode 100644 index 0000000000..9b96ab9c8c --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuBar.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Styling; + +namespace Avalonia.Controls +{ + public class NativeMenuBar : TemplatedControl + { + public static readonly AttachedProperty EnableMenuItemClickForwardingProperty = + AvaloniaProperty.RegisterAttached( + "EnableMenuItemClickForwarding"); + + static NativeMenuBar() + { + EnableMenuItemClickForwardingProperty.Changed.Subscribe(args => + { + var item = (MenuItem)args.Sender; + if (args.NewValue.Equals(true)) + item.Click += OnMenuItemClick; + else + item.Click -= OnMenuItemClick; + }); + } + + public static void SetEnableMenuItemClickForwarding(MenuItem menuItem, bool enable) + { + menuItem.SetValue(EnableMenuItemClickForwardingProperty, enable); + } + + private static void OnMenuItemClick(object sender, RoutedEventArgs e) + { + (((MenuItem)sender).DataContext as NativeMenuItem)?.RaiseClick(); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs new file mode 100644 index 0000000000..c1144d45b2 --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -0,0 +1,162 @@ +using System; +using System.Windows.Input; +using Avalonia.Input; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + public class NativeMenuItem : NativeMenuItemBase + { + private string _header; + private KeyGesture _gesture; + private bool _enabled = true; + + private NativeMenu _menu; + + static NativeMenuItem() + { + MenuProperty.Changed.Subscribe(args => + { + var item = (NativeMenuItem)args.Sender; + var value = (NativeMenu)args.NewValue; + if (value.Parent != null && value.Parent != item) + throw new InvalidOperationException("NativeMenu already has a parent"); + value.Parent = item; + }); + } + + + class CanExecuteChangedSubscriber : IWeakSubscriber + { + private readonly NativeMenuItem _parent; + + public CanExecuteChangedSubscriber(NativeMenuItem parent) + { + _parent = parent; + } + + public void OnEvent(object sender, EventArgs e) + { + _parent.CanExecuteChanged(); + } + } + + private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber; + + + public NativeMenuItem() + { + _canExecuteChangedSubscriber = new CanExecuteChangedSubscriber(this); + } + + public NativeMenuItem(string header) : this() + { + Header = header; + } + + public static readonly DirectProperty MenuProperty = + AvaloniaProperty.RegisterDirect(nameof(Menu), o => o._menu, + (o, v) => + { + if (v.Parent != null && v.Parent != o) + throw new InvalidOperationException("NativeMenu already has a parent"); + o._menu = v; + }); + + public NativeMenu Menu + { + get => _menu; + set + { + if (value.Parent != null && value.Parent != this) + throw new InvalidOperationException("NativeMenu already has a parent"); + SetAndRaise(MenuProperty, ref _menu, value); + } + } + + public static readonly DirectProperty HeaderProperty = + AvaloniaProperty.RegisterDirect(nameof(Header), o => o._header, (o, v) => o._header = v); + + public string Header + { + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); + } + + public static readonly DirectProperty GestureProperty = + AvaloniaProperty.RegisterDirect(nameof(Gesture), o => o._gesture, (o, v) => o._gesture = v); + + public KeyGesture Gesture + { + get => GetValue(GestureProperty); + set => SetValue(GestureProperty, value); + } + + private ICommand _command; + + public static readonly DirectProperty CommandProperty = + AvaloniaProperty.RegisterDirect(nameof(Command), + o => o._command, (o, v) => + { + if (o._command != null) + WeakSubscriptionManager.Unsubscribe(o._command, + nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber); + o._command = v; + if (o._command != null) + WeakSubscriptionManager.Subscribe(o._command, + nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber); + o.CanExecuteChanged(); + }); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandParameterProperty = + Button.CommandParameterProperty.AddOwner(); + + public static readonly DirectProperty EnabledProperty = + AvaloniaProperty.RegisterDirect(nameof(Enabled), o => o._enabled, + (o, v) => o._enabled = v, true); + + public bool Enabled + { + get => GetValue(EnabledProperty); + set => SetValue(EnabledProperty, value); + } + + void CanExecuteChanged() + { + Enabled = _command?.CanExecute(null) ?? true; + } + + public bool HasClickHandlers => Clicked != null; + + public ICommand Command + { + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + /// + /// Gets or sets the parameter to pass to the property of a + /// . + /// + public object CommandParameter + { + get { return GetValue(CommandParameterProperty); } + set { SetValue(CommandParameterProperty, value); } + } + + public event EventHandler Clicked; + + public void RaiseClick() + { + Clicked?.Invoke(this, new EventArgs()); + + if (Command?.CanExecute(CommandParameter) == true) + { + Command.Execute(CommandParameter); + } + } + } +} diff --git a/src/Avalonia.Controls/NativeMenuItemBase.cs b/src/Avalonia.Controls/NativeMenuItemBase.cs new file mode 100644 index 0000000000..47eb86cdc3 --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuItemBase.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Controls +{ + public class NativeMenuItemBase : AvaloniaObject + { + private NativeMenu _parent; + + internal NativeMenuItemBase() + { + + } + + public static readonly DirectProperty ParentProperty = + AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); + + public NativeMenu Parent + { + get => _parent; + set => SetAndRaise(ParentProperty, ref _parent, value); + } + } +} diff --git a/src/Avalonia.Controls/NativeMenuItemSeperator.cs b/src/Avalonia.Controls/NativeMenuItemSeperator.cs new file mode 100644 index 0000000000..e743483dab --- /dev/null +++ b/src/Avalonia.Controls/NativeMenuItemSeperator.cs @@ -0,0 +1,10 @@ +using System; + +namespace Avalonia.Controls +{ + public class NativeMenuItemSeperator : NativeMenuItemBase + { + [Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)] + public string Header => "-"; + } +} diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 98f925cd0c..97aeead608 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -273,7 +273,8 @@ namespace Avalonia.Controls.Platform if (item.IsTopLevel) { - if (item.Parent.SelectedItem?.IsSubMenuOpen == true) + if (item != item.Parent.SelectedItem && + item.Parent.SelectedItem?.IsSubMenuOpen == true) { item.Parent.SelectedItem.Close(); SelectItemAndAncestors(item); @@ -356,7 +357,7 @@ namespace Avalonia.Controls.Platform { var item = GetMenuItem(e.Source as IControl); - if (e.MouseButton == MouseButton.Left && item?.HasSubMenu == false) + if (e.InitialPressMouseButton == MouseButton.Left && item?.HasSubMenu == false) { Click(item); e.Handled = true; diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs new file mode 100644 index 0000000000..3ac5f28956 --- /dev/null +++ b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; + +namespace Avalonia.Controls.Platform +{ + public interface ITopLevelNativeMenuExporter + { + bool IsNativeMenuExported { get; } + event EventHandler OnIsNativeMenuExportedChanged; + void SetNativeMenu(NativeMenu menu); + } + + public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl + { + ITopLevelNativeMenuExporter NativeMenuExporter { get; } + } +} diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index bc5d38c845..91b895f38a 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Controls; +using Avalonia.Input; namespace Avalonia.Platform { @@ -57,28 +58,28 @@ namespace Avalonia.Platform /// Return true to prevent the underlying implementation from closing. /// Func Closing { get; set; } - + /// /// Starts moving a window with left button being held. Should be called from left mouse button press event handler. /// - void BeginMoveDrag(); + void BeginMoveDrag(PointerPressedEventArgs e); /// /// Starts resizing a window. This function is used if an application has window resizing controls. /// Should be called from left mouse button press event handler /// - void BeginResizeDrag(WindowEdge edge); - + void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e); + /// /// Sets the client size of the top level. /// void Resize(Size clientSize); - + /// /// Sets the client size of the top level. /// void Move(PixelPoint point); - + /// /// Minimum width of the window. /// diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs index 6eb056bb32..9d644aaa00 100644 --- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs +++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs @@ -21,7 +21,7 @@ namespace Avalonia.Platform private DragDropEffects _allowedEffects; private IDataObject _draggedData; - private IInputElement _lastRoot; + private IInputRoot _lastRoot; private Point _lastPosition; private StandardCursorType _lastCursorType; private object _originalCursor; @@ -56,7 +56,7 @@ namespace Avalonia.Platform return DragDropEffects.None; } - private DragDropEffects RaiseEventAndUpdateCursor(RawDragEventType type, IInputElement root, Point pt, RawInputModifiers modifiers) + private DragDropEffects RaiseEventAndUpdateCursor(RawDragEventType type, IInputRoot root, Point pt, RawInputModifiers modifiers) { _lastPosition = pt; @@ -91,7 +91,7 @@ namespace Avalonia.Platform return StandardCursorType.No; } - private void UpdateCursor(IInputElement root, DragDropEffects effect) + private void UpdateCursor(IInputRoot root, DragDropEffects effect) { if (_lastRoot != root) { diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index dedab3e43e..89de24a81a 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -46,8 +46,8 @@ namespace Avalonia.Controls.Presenters /// static CarouselPresenter() { - IsVirtualizedProperty.Changed.AddClassHandler(x => x.IsVirtualizedChanged); - SelectedIndexProperty.Changed.AddClassHandler(x => x.SelectedIndexChanged); + IsVirtualizedProperty.Changed.AddClassHandler((x, e) => x.IsVirtualizedChanged(e)); + SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.SelectedIndexChanged(e)); } /// diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index a5374e7c5a..5e1a844720 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -94,9 +94,9 @@ namespace Avalonia.Controls.Presenters { AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty); AffectsMeasure(BorderThicknessProperty, PaddingProperty); - ContentProperty.Changed.AddClassHandler(x => x.ContentChanged); - ContentTemplateProperty.Changed.AddClassHandler(x => x.ContentChanged); - TemplatedParentProperty.Changed.AddClassHandler(x => x.TemplatedParentChanged); + ContentProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e)); + ContentTemplateProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e)); + TemplatedParentProperty.Changed.AddClassHandler((x, e) => x.TemplatedParentChanged(e)); } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 500c7aa187..ab40fbd53b 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls.Presenters KeyboardNavigationMode.Once); VirtualizationModeProperty.Changed - .AddClassHandler(x => x.VirtualizationModeChanged); + .AddClassHandler((x, e) => x.VirtualizationModeChanged(e)); } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index ea56a0c6fc..0f0cdc37cf 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -45,7 +45,7 @@ namespace Avalonia.Controls.Presenters /// static ItemsPresenterBase() { - TemplatedParentProperty.Changed.AddClassHandler(x => x.TemplatedParentChanged); + TemplatedParentProperty.Changed.AddClassHandler((x,e) => x.TemplatedParentChanged(e)); } /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index ec6a228421..48d0aff551 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -73,7 +73,7 @@ namespace Avalonia.Controls.Presenters static ScrollContentPresenter() { ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); - ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); + ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); AffectsArrange(OffsetProperty); } @@ -246,7 +246,7 @@ namespace Avalonia.Controls.Presenters if (isLogical) _activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta); delta += e.Delta; - + if (Extent.Height > Viewport.Height) { double dy; @@ -293,7 +293,7 @@ namespace Avalonia.Controls.Presenters } } - private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e) + private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e) => _activeLogicalGestureScrolls?.Remove(e.Id); /// diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index debbb81264..5931fec350 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -244,7 +244,7 @@ namespace Avalonia.Controls.Presenters var rect = FormattedText.HitTestTextPosition(caretIndex); this.BringIntoView(rect); }, - DispatcherPriority.Normal); + DispatcherPriority.Render); } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs index 3cf50a7b80..d431420a8f 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs @@ -30,7 +30,7 @@ namespace Avalonia.Controls.Primitives /// static HeaderedContentControl() { - ContentProperty.Changed.AddClassHandler(x => x.HeaderChanged); + ContentProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); } /// diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index e0eb0b005f..f4af694f28 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls.Primitives /// static HeaderedItemsControl() { - HeaderProperty.Changed.AddClassHandler(x => x.HeaderChanged); + HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); } /// diff --git a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs index 533b643ea6..5e053ed9b4 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls.Primitives /// static HeaderedSelectingItemsControl() { - HeaderProperty.Changed.AddClassHandler(x => x.HeaderChanged); + HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); } /// diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index b3f86e8a76..77febf9384 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -93,8 +93,8 @@ namespace Avalonia.Controls.Primitives static Popup() { IsHitTestVisibleProperty.OverrideDefaultValue(false); - ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); - IsOpenProperty.Changed.AddClassHandler(x => x.IsOpenChanged); + ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); + IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged(e)); } public Popup() diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index b7f0c8f47d..74a6d288f4 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls.Primitives /// /// The root window of a . /// - public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost + public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { private readonly TopLevel _parent; private PopupPositionerParameters _positionerParameters; diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index c6119e89dc..9251ca273f 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -58,8 +58,8 @@ namespace Avalonia.Controls.Primitives PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); - Thumb.DragDeltaEvent.AddClassHandler(o => o.OnThumbDragDelta, RoutingStrategies.Bubble); - Thumb.DragCompletedEvent.AddClassHandler(o => o.OnThumbDragComplete, RoutingStrategies.Bubble); + Thumb.DragDeltaEvent.AddClassHandler((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble); + Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragComplete(e), RoutingStrategies.Bubble); } /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 6869ea0822..7fddee1012 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -119,7 +119,7 @@ namespace Avalonia.Controls.Primitives /// static SelectingItemsControl() { - IsSelectedChangedEvent.AddClassHandler(x => x.ContainerSelectionChanged); + IsSelectedChangedEvent.AddClassHandler((x, e) => x.ContainerSelectionChanged(e)); } /// @@ -1062,7 +1062,7 @@ namespace Avalonia.Controls.Primitives } catch (Exception ex) { - Logger.Error( + Logger.TryGet(LogEventLevel.Error)?.Log( LogArea.Property, this, "Error thrown updating SelectedItems: {Error}", @@ -1088,8 +1088,8 @@ namespace Avalonia.Controls.Primitives } else { - SelectedIndex = _updateSelectedIndex != int.MinValue ? - _updateSelectedIndex : + SelectedIndex = _updateSelectedIndex != int.MinValue ? + _updateSelectedIndex : AlwaysSelected ? 0 : -1; } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 47c3240374..0ace387185 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -99,7 +99,7 @@ namespace Avalonia.Controls.Primitives static TemplatedControl() { ClipToBoundsProperty.OverrideDefaultValue(true); - TemplateProperty.Changed.AddClassHandler(x => x.OnTemplateChanged); + TemplateProperty.Changed.AddClassHandler((x, e) => x.OnTemplateChanged(e)); } /// @@ -255,7 +255,7 @@ namespace Avalonia.Controls.Primitives if (template != null) { - Logger.Verbose(LogArea.Control, this, "Creating control template"); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Control, this, "Creating control template"); var (child, nameScope) = template.Build(this); ApplyTemplatedParent(child); diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index b01ddd5dba..9f5ddb666c 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -22,9 +22,9 @@ namespace Avalonia.Controls.Primitives static Thumb() { - DragStartedEvent.AddClassHandler(x => x.OnDragStarted, RoutingStrategies.Bubble); - DragDeltaEvent.AddClassHandler(x => x.OnDragDelta, RoutingStrategies.Bubble); - DragCompletedEvent.AddClassHandler(x => x.OnDragCompleted, RoutingStrategies.Bubble); + DragStartedEvent.AddClassHandler((x,e) => x.OnDragStarted(e), RoutingStrategies.Bubble); + DragDeltaEvent.AddClassHandler((x, e) => x.OnDragDelta(e), RoutingStrategies.Bubble); + DragCompletedEvent.AddClassHandler((x, e) => x.OnDragCompleted(e), RoutingStrategies.Bubble); } public event EventHandler DragStarted @@ -83,6 +83,8 @@ namespace Avalonia.Controls.Primitives Vector = (Vector)_lastPoint, }; + PseudoClasses.Add(":pressed"); + RaiseEvent(ev); } @@ -102,6 +104,8 @@ namespace Avalonia.Controls.Primitives RaiseEvent(ev); } + + PseudoClasses.Remove(":pressed"); } } } diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index a569808b35..292c65aa06 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -48,9 +48,9 @@ namespace Avalonia.Controls.Primitives { PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); - ThumbProperty.Changed.AddClassHandler(x => x.ThumbChanged); - IncreaseButtonProperty.Changed.AddClassHandler(x => x.ButtonChanged); - DecreaseButtonProperty.Changed.AddClassHandler(x => x.ButtonChanged); + ThumbProperty.Changed.AddClassHandler((x,e) => x.ThumbChanged(e)); + IncreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); + DecreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); } diff --git a/src/Avalonia.Controls/Primitives/UniformGrid.cs b/src/Avalonia.Controls/Primitives/UniformGrid.cs index f3580eee10..09554412db 100644 --- a/src/Avalonia.Controls/Primitives/UniformGrid.cs +++ b/src/Avalonia.Controls/Primitives/UniformGrid.cs @@ -28,6 +28,11 @@ namespace Avalonia.Controls.Primitives private int _rows; private int _columns; + static UniformGrid() + { + AffectsMeasure(RowsProperty, ColumnsProperty, FirstColumnProperty); + } + /// /// Specifies the row count. If set to 0, row count will be calculated automatically. /// diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 29e3a17f74..94898951a9 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using Avalonia.Controls.Primitives; using Avalonia.Layout; @@ -38,8 +39,8 @@ namespace Avalonia.Controls PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); PseudoClass(IsIndeterminateProperty, ":indeterminate"); - ValueProperty.Changed.AddClassHandler(x => x.UpdateIndicatorWhenPropChanged); - IsIndeterminateProperty.Changed.AddClassHandler(x => x.UpdateIndicatorWhenPropChanged); + ValueProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); + IsIndeterminateProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); } public bool IsIndeterminate diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index be24f22e20..9d70fbcf31 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -227,6 +227,7 @@ namespace Avalonia.Controls.Remote.Server Input?.Invoke(new RawKeyEventArgs( KeyboardDevice, 0, + InputRoot, key.IsDown ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, (Key)key.Key, GetAvaloniaRawInputModifiers(key.Modifiers))); @@ -241,6 +242,7 @@ namespace Avalonia.Controls.Remote.Server Input?.Invoke(new RawTextInputEventArgs( KeyboardDevice, 0, + InputRoot, text.Text)); }, DispatcherPriority.Input); } diff --git a/src/Avalonia.Controls/RepeatButton.cs b/src/Avalonia.Controls/RepeatButton.cs index 07a1e82638..a982a0970c 100644 --- a/src/Avalonia.Controls/RepeatButton.cs +++ b/src/Avalonia.Controls/RepeatButton.cs @@ -98,10 +98,10 @@ namespace Avalonia.Controls { base.OnPointerReleased(e); - if (e.MouseButton == MouseButton.Left) + if (e.InitialPressMouseButton == MouseButton.Left) { StopTimer(); } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index c9b5cbb75b..cdf5010920 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -163,8 +163,8 @@ namespace Avalonia.Controls { AffectsValidation(ExtentProperty, OffsetProperty); AffectsValidation(ViewportProperty, OffsetProperty); - HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler(x => x.ScrollBarVisibilityChanged); - VerticalScrollBarVisibilityProperty.Changed.AddClassHandler(x => x.ScrollBarVisibilityChanged); + HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); + VerticalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); } /// diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 9eaa246434..f71be8d836 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -45,9 +45,9 @@ namespace Avalonia.Controls OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal); PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); - Thumb.DragStartedEvent.AddClassHandler(x => x.OnThumbDragStarted, RoutingStrategies.Bubble); - Thumb.DragDeltaEvent.AddClassHandler(x => x.OnThumbDragDelta, RoutingStrategies.Bubble); - Thumb.DragCompletedEvent.AddClassHandler(x => x.OnThumbDragCompleted, RoutingStrategies.Bubble); + Thumb.DragStartedEvent.AddClassHandler((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble); + Thumb.DragDeltaEvent.AddClassHandler((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble); + Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble); } /// diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 50bcb034ac..61ac0822b0 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -196,7 +196,7 @@ namespace Avalonia.Controls protected override void OnPointerReleased(PointerReleasedEventArgs e) { - if (e.MouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) + if (e.InitialPressMouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) { var container = GetContainerFromEventSource(e.Source); if (container != null diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 47a2348d59..fca1e022aa 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -30,8 +30,8 @@ namespace Avalonia.Controls { SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); - IsSelectedProperty.Changed.AddClassHandler(x => x.UpdateSelectedContent); - DataContextProperty.Changed.AddClassHandler(x => x.UpdateHeader); + IsSelectedProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); + DataContextProperty.Changed.AddClassHandler((x, e) => x.UpdateHeader(e)); } /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index e0acab1133..293809bf51 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -271,7 +271,7 @@ namespace Avalonia.Controls { (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; - Closed?.Invoke(this, EventArgs.Empty); + OnClosed(EventArgs.Empty); Renderer?.Dispose(); Renderer = null; } @@ -317,6 +317,12 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnOpened(EventArgs e) => Opened?.Invoke(this, e); + /// + /// Raises the event. + /// + /// The event args. + protected virtual void OnClosed(EventArgs e) => Closed?.Invoke(this, e); + /// /// Tries to get a service from an , logging a /// warning if not found. @@ -330,7 +336,7 @@ namespace Avalonia.Controls if (result == null) { - Logger.Warning( + Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Control, this, "Could not create {Service} : maybe Application.RegisterServices() wasn't called?", diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index cc47365214..8907137ecb 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -1,3 +1,4 @@ + // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. @@ -470,7 +471,7 @@ namespace Avalonia.Controls if (index > 0) { var previous = (TreeViewItem)parentGenerator.ContainerFromIndex(index - 1); - result = previous.IsExpanded ? + result = previous.IsExpanded && previous.ItemCount > 0 ? (TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1) : previous; } @@ -482,7 +483,7 @@ namespace Avalonia.Controls break; case NavigationDirection.Down: - if (from.IsExpanded && intoChildren) + if (from.IsExpanded && intoChildren && from.ItemCount > 0) { result = (TreeViewItem)from.ItemContainerGenerator.ContainerFromIndex(0); } diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index c7fd96b68a..07d5497c14 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -54,7 +54,7 @@ namespace Avalonia.Controls SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(true); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); - RequestBringIntoViewEvent.AddClassHandler(x => x.OnRequestBringIntoView); + RequestBringIntoViewEvent.AddClassHandler((x, e) => x.OnRequestBringIntoView(e)); } /// diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs index 4d814170c6..78361fcc8f 100644 --- a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs +++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs @@ -178,7 +178,7 @@ namespace Avalonia.Controls.Utils /// The event data. private void OnSelectorPointerReleased(object sender, PointerReleasedEventArgs e) { - if (e.MouseButton == MouseButton.Left) + if (e.InitialPressMouseButton == MouseButton.Left) { OnCommit(); } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ef43746665..1816a6c81d 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -261,13 +261,13 @@ namespace Avalonia.Controls /// /// Starts moving a window with left button being held. Should be called from left mouse button press event handler /// - public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag(); + public void BeginMoveDrag(PointerPressedEventArgs e) => PlatformImpl?.BeginMoveDrag(e); /// /// Starts resizing a window. This function is used if an application has window resizing controls. /// Should be called from left mouse button press event handler /// - public void BeginResizeDrag(WindowEdge edge) => PlatformImpl?.BeginResizeDrag(edge); + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e); /// /// Carries out the arrange pass of the window. @@ -557,7 +557,7 @@ namespace Avalonia.Controls return result; } - protected override void HandleClosed() + protected sealed override void HandleClosed() { RaiseEvent(new RoutedEventArgs(WindowClosedEvent)); @@ -565,7 +565,7 @@ namespace Avalonia.Controls } /// - protected override void HandleResized(Size clientSize) + protected sealed override void HandleResized(Size clientSize) { if (!AutoSizing) { diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index a47c55f87c..196110edf7 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls static WindowBase() { IsVisibleProperty.OverrideDefaultValue(false); - IsVisibleProperty.Changed.AddClassHandler(x => x.IsVisibleChanged); + IsVisibleProperty.Changed.AddClassHandler((x,e) => x.IsVisibleChanged(e)); TopmostProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue)); diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index a7d4b96974..f3bb0edce5 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -69,7 +69,17 @@ namespace Avalonia.DesignerSupport } if (!window.IsSet(Window.SizeToContentProperty)) - window.SizeToContent = SizeToContent.WidthAndHeight; + { + if (double.IsNaN(window.Width)) + { + window.SizeToContent |= SizeToContent.Width; + } + + if (double.IsNaN(window.Height)) + { + window.SizeToContent |= SizeToContent.Height; + } + } } window.Show(); Design.ApplyDesignModeProperties(window, control); diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 40524ad4b7..86e34ca6d4 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -2,6 +2,7 @@ using System.Reactive.Disposables; using Avalonia.Controls; using Avalonia.Controls.Remote.Server; +using Avalonia.Input; using Avalonia.Platform; using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Viewport; @@ -27,11 +28,11 @@ namespace Avalonia.DesignerSupport.Remote { } - public void BeginMoveDrag() + public void BeginMoveDrag(PointerPressedEventArgs e) { } - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 16d434b614..4bba5ef41b 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -46,7 +46,7 @@ namespace Avalonia.DesignerSupport.Remote Resize(size); })); } - + public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root); public void Dispose() { @@ -75,11 +75,11 @@ namespace Avalonia.DesignerSupport.Remote { } - public void BeginMoveDrag() + public void BeginMoveDrag(PointerPressedEventArgs e) { } - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { } @@ -93,7 +93,7 @@ namespace Avalonia.DesignerSupport.Remote public void Move(PixelPoint point) { - + } public IScreenImpl Screen { get; } = new ScreenStub(); @@ -153,7 +153,7 @@ namespace Avalonia.DesignerSupport.Remote { public void Save(Stream outputStream) { - + } } @@ -167,10 +167,10 @@ namespace Avalonia.DesignerSupport.Remote class SystemDialogsStub : ISystemDialogImpl { public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) => - Task.FromResult((string[]) null); + Task.FromResult((string[])null); public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) => - Task.FromResult((string) null); + Task.FromResult((string)null); } class ScreenStub : IScreenImpl diff --git a/src/Avalonia.DesktopRuntime/AppBuilder.cs b/src/Avalonia.DesktopRuntime/AppBuilder.cs index dbe3767df6..ff0d84a6e9 100644 --- a/src/Avalonia.DesktopRuntime/AppBuilder.cs +++ b/src/Avalonia.DesktopRuntime/AppBuilder.cs @@ -18,19 +18,10 @@ namespace Avalonia /// public AppBuilder() : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType()?.Assembly)) + builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly)) { } - /// - /// Initializes a new instance of the class. - /// - /// The instance. - public AppBuilder(Application app) : this() - { - Instance = app; - } - bool CheckEnvironment(Type checkerType) { if (checkerType == null) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index d7e1d8cdb3..d9fd3b78a2 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -8,4 +8,8 @@ + + + + diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs new file mode 100644 index 0000000000..b445f86613 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -0,0 +1,82 @@ +using System; +using System.Threading; +using Avalonia.Threading; +using Tmds.DBus; + +namespace Avalonia.FreeDesktop +{ + public class DBusHelper + { + /// + /// This class uses synchronous execution at DBus connection establishment stage + /// then switches to using AvaloniaSynchronizationContext + /// + class DBusSyncContext : SynchronizationContext + { + private SynchronizationContext _ctx; + private object _lock = new object(); + + public override void Post(SendOrPostCallback d, object state) + { + lock (_lock) + { + if (_ctx != null) + _ctx?.Post(d, state); + else + lock (_lock) + d(state); + } + } + + public override void Send(SendOrPostCallback d, object state) + { + lock (_lock) + { + if (_ctx != null) + _ctx?.Send(d, state); + else + + d(state); + } + } + + public void Initialized() + { + lock (_lock) + _ctx = new AvaloniaSynchronizationContext(); + } + } + public static Connection Connection { get; private set; } + + public static Exception TryInitialize(string dbusAddress = null) + { + var oldContext = SynchronizationContext.Current; + try + { + + var dbusContext = new DBusSyncContext(); + SynchronizationContext.SetSynchronizationContext(dbusContext); + var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session) + { + AutoConnect = false, + SynchronizationContext = dbusContext + }); + // Connect synchronously + conn.ConnectAsync().Wait(); + + // Initialize a brand new sync-context + dbusContext.Initialized(); + Connection = conn; + } + catch (Exception e) + { + return e; + } + finally + { + SynchronizationContext.SetSynchronizationContext(oldContext); + } + return null; + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs new file mode 100644 index 0000000000..7180345386 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusMenu.cs @@ -0,0 +1,56 @@ + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] +namespace Avalonia.FreeDesktop.DBusMenu +{ + + [DBusInterface("org.freedesktop.DBus.Properties")] + interface IFreeDesktopDBusProperties : IDBusObject + { + Task GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action handler); + } + + [DBusInterface("com.canonical.dbusmenu")] + interface IDBusMenu : IFreeDesktopDBusProperties + { + Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync(int ParentId, int RecursionDepth, string[] PropertyNames); + Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames); + Task GetPropertyAsync(int Id, string Name); + Task EventAsync(int Id, string EventId, object Data, uint Timestamp); + Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); + Task AboutToShowAsync(int Id); + Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); + Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError = null); + Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action onError = null); + Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action onError = null); + } + + [Dictionary] + class DBusMenuProperties + { + public uint Version { get; set; } = default (uint); + public string TextDirection { get; set; } = default (string); + public string Status { get; set; } = default (string); + public string[] IconThemePath { get; set; } = default (string[]); + } + + + [DBusInterface("com.canonical.AppMenu.Registrar")] + interface IRegistrar : IDBusObject + { + Task RegisterWindowAsync(uint WindowId, ObjectPath MenuObjectPath); + Task UnregisterWindowAsync(uint WindowId); + Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId); + Task<(uint, string, ObjectPath)[]> GetMenusAsync(); + Task WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action onError = null); + Task WatchWindowUnregisteredAsync(Action handler, Action onError = null); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs new file mode 100644 index 0000000000..90239b5a49 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.FreeDesktop.DBusMenu; +using Avalonia.Input; +using Avalonia.Threading; +using Tmds.DBus; +#pragma warning disable 1998 + +namespace Avalonia.FreeDesktop +{ + public class DBusMenuExporter + { + public static ITopLevelNativeMenuExporter TryCreate(IntPtr xid) + { + if (DBusHelper.Connection == null) + return null; + + return new DBusMenuExporterImpl(DBusHelper.Connection, xid); + } + + class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable + { + private readonly Connection _dbus; + private readonly uint _xid; + private IRegistrar _registar; + private bool _disposed; + private uint _revision = 1; + private NativeMenu _menu; + private Dictionary _idsToItems = new Dictionary(); + private Dictionary _itemsToIds = new Dictionary(); + private readonly HashSet _menus = new HashSet(); + private bool _resetQueued; + private int _nextId = 1; + public DBusMenuExporterImpl(Connection dbus, IntPtr xid) + { + _dbus = dbus; + _xid = (uint)xid.ToInt32(); + ObjectPath = new ObjectPath("/net/avaloniaui/dbusmenu/" + + Guid.NewGuid().ToString().Replace("-", "")); + SetNativeMenu(new NativeMenu()); + Init(); + } + + async void Init() + { + try + { + await _dbus.RegisterObjectAsync(this); + _registar = DBusHelper.Connection.CreateProxy( + "com.canonical.AppMenu.Registrar", + "/com/canonical/AppMenu/Registrar"); + if (!_disposed) + await _registar.RegisterWindowAsync(_xid, ObjectPath); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + // It's not really important if this code succeeds, + // and it's not important to know if it succeeds + // since even if we register the window it's not guaranteed that + // menu will be actually exported + } + } + + public void Dispose() + { + if (_disposed) + return; + _disposed = true; + _dbus.UnregisterObject(this); + // Fire and forget + _registar?.UnregisterWindowAsync(_xid); + } + + + + public bool IsNativeMenuExported { get; private set; } + public event EventHandler OnIsNativeMenuExportedChanged; + + public void SetNativeMenu(NativeMenu menu) + { + if (menu == null) + menu = new NativeMenu(); + + if (_menu != null) + ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; + _menu = menu; + ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged; + + DoLayoutReset(); + } + + /* + This is basic initial implementation, so we don't actually track anything and + just reset the whole layout on *ANY* change + + This is not how it should work and will prevent us from implementing various features, + but that's the fastest way to get things working, so... + */ + void DoLayoutReset() + { + _resetQueued = false; + foreach (var i in _idsToItems.Values) + i.PropertyChanged -= OnItemPropertyChanged; + foreach(var menu in _menus) + ((INotifyCollectionChanged)menu.Items).CollectionChanged -= OnMenuItemsChanged; + _menus.Clear(); + _idsToItems.Clear(); + _itemsToIds.Clear(); + _revision++; + LayoutUpdated?.Invoke((_revision, 0)); + } + + void QueueReset() + { + if(_resetQueued) + return; + _resetQueued = true; + Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); + } + + private (NativeMenuItemBase item, NativeMenu menu) GetMenu(int id) + { + if (id == 0) + return (null, _menu); + _idsToItems.TryGetValue(id, out var item); + return (item, (item as NativeMenuItem)?.Menu); + } + + private void EnsureSubscribed(NativeMenu menu) + { + if(menu!=null && _menus.Add(menu)) + ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged; + } + + private int GetId(NativeMenuItemBase item) + { + if (_itemsToIds.TryGetValue(item, out var id)) + return id; + id = _nextId++; + _idsToItems[id] = item; + _itemsToIds[item] = id; + item.PropertyChanged += OnItemPropertyChanged; + if (item is NativeMenuItem nmi) + EnsureSubscribed(nmi.Menu); + return id; + } + + private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + QueueReset(); + } + + private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + QueueReset(); + } + + public ObjectPath ObjectPath { get; } + + + async Task IFreeDesktopDBusProperties.GetAsync(string prop) + { + if (prop == "Version") + return 2; + if (prop == "Status") + return "normal"; + return 0; + } + + async Task IFreeDesktopDBusProperties.GetAllAsync() + { + return new DBusMenuProperties + { + Version = 2, + Status = "normal", + }; + } + + private static string[] AllProperties = new[] + { + "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display" + }; + + object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name) + { + var (it, menu) = i; + + if (it is NativeMenuItemSeperator) + { + if (name == "type") + return "separator"; + } + else if (it is NativeMenuItem item) + { + if (name == "type") + { + return null; + } + if (name == "label") + return item?.Header ?? ""; + if (name == "enabled") + { + if (item == null) + return null; + if (item.Menu != null && item.Menu.Items.Count == 0) + return false; + if (item.Enabled == false) + return false; + return null; + } + if (name == "shortcut") + { + if (item?.Gesture == null) + return null; + if (item.Gesture.KeyModifiers == 0) + return null; + var lst = new List(); + var mod = item.Gesture; + if ((mod.KeyModifiers & KeyModifiers.Control) != 0) + lst.Add("Control"); + if ((mod.KeyModifiers & KeyModifiers.Alt) != 0) + lst.Add("Alt"); + if ((mod.KeyModifiers & KeyModifiers.Shift) != 0) + lst.Add("Shift"); + if ((mod.KeyModifiers & KeyModifiers.Meta) != 0) + lst.Add("Super"); + lst.Add(item.Gesture.Key.ToString()); + return new[] { lst.ToArray() }; + } + + if (name == "children-display") + return menu != null ? "submenu" : null; + } + + return null; + } + + private List> _reusablePropertyList = new List>(); + KeyValuePair[] GetProperties((NativeMenuItemBase item, NativeMenu menu) i, string[] names) + { + if (names?.Length > 0 != true) + names = AllProperties; + _reusablePropertyList.Clear(); + foreach (var n in names) + { + var v = GetProperty(i, n); + if (v != null) + _reusablePropertyList.Add(new KeyValuePair(n, v)); + } + + return _reusablePropertyList.ToArray(); + } + + + public Task SetAsync(string prop, object val) => Task.CompletedTask; + + public Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync( + int ParentId, int RecursionDepth, string[] PropertyNames) + { + var menu = GetMenu(ParentId); + var rv = (_revision, GetLayout(menu.item, menu.menu, RecursionDepth, PropertyNames)); + if (!IsNativeMenuExported) + { + IsNativeMenuExported = true; + Dispatcher.UIThread.Post(() => + { + OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty); + }); + } + return Task.FromResult(rv); + } + + (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase item, NativeMenu menu, int depth, string[] propertyNames) + { + var id = item == null ? 0 : GetId(item); + var props = GetProperties((item, menu), propertyNames); + var children = (depth == 0 || menu == null) ? new object[0] : new object[menu.Items.Count]; + if(menu != null) + for (var c = 0; c < children.Length; c++) + { + var ch = menu.Items[c]; + + children[c] = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); + } + + return (id, props, children); + } + + public Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames) + { + var arr = new (int, KeyValuePair[])[Ids.Length]; + for (var c = 0; c < Ids.Length; c++) + { + var id = Ids[c]; + var item = GetMenu(id); + var props = GetProperties(item, PropertyNames); + arr[c] = (id, props); + } + + return Task.FromResult(arr); + } + + public async Task GetPropertyAsync(int Id, string Name) + { + return GetProperty(GetMenu(Id), Name) ?? 0; + } + + + + public void HandleEvent(int id, string eventId, object data, uint timestamp) + { + if (eventId == "clicked") + { + var item = GetMenu(id).item; + + if (item is NativeMenuItem menuItem) + { + if (menuItem?.Enabled == true) + menuItem.RaiseClick(); + } + } + } + + public Task EventAsync(int Id, string EventId, object Data, uint Timestamp) + { + HandleEvent(Id, EventId, Data, Timestamp); + return Task.CompletedTask; + } + + public Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] Events) + { + foreach (var e in Events) + HandleEvent(e.id, e.eventId, e.data, e.timestamp); + return Task.FromResult(new int[0]); + } + + public async Task AboutToShowAsync(int Id) + { + return false; + } + + public async Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids) + { + return (new int[0], new int[0]); + } + + #region Events + + private event Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> + ItemsPropertiesUpdated; + private event Action<(uint revision, int parent)> LayoutUpdated; + private event Action<(int id, uint timestamp)> ItemActivationRequested; + private event Action PropertiesChanged; + + async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError) + { + ItemsPropertiesUpdated += handler; + return Disposable.Create(() => ItemsPropertiesUpdated -= handler); + } + async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action onError) + { + LayoutUpdated += handler; + return Disposable.Create(() => LayoutUpdated -= handler); + } + + async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action onError) + { + ItemActivationRequested+= handler; + return Disposable.Create(() => ItemActivationRequested -= handler); + } + + async Task IFreeDesktopDBusProperties.WatchPropertiesAsync(Action handler) + { + PropertiesChanged += handler; + return Disposable.Create(() => PropertiesChanged -= handler); + } + + #endregion + } + } +} diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs index 0b9f09b224..25d3b6887f 100644 --- a/src/Avalonia.Input/DragDropDevice.cs +++ b/src/Avalonia.Input/DragDropDevice.cs @@ -11,7 +11,7 @@ namespace Avalonia.Input private Interactive _lastTarget = null; - private Interactive GetTarget(IInputElement root, Point local) + private Interactive GetTarget(IInputRoot root, Point local) { var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType()?.FirstOrDefault(); if (target != null && DragDrop.GetAllowDrop(target)) @@ -19,7 +19,7 @@ namespace Avalonia.Input return null; } - private DragDropEffects RaiseDragEvent(Interactive target, IInputElement inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, InputModifiers modifiers) + private DragDropEffects RaiseDragEvent(Interactive target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, InputModifiers modifiers) { if (target == null) return DragDropEffects.None; @@ -38,13 +38,13 @@ namespace Avalonia.Input return args.DragEffects; } - private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects DragEnter(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) { _lastTarget = GetTarget(inputRoot, point); return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers); } - private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects DragOver(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) { var target = GetTarget(inputRoot, point); @@ -77,7 +77,7 @@ namespace Avalonia.Input } } - private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects Drop(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) { try { @@ -100,16 +100,16 @@ namespace Avalonia.Input switch (e.Type) { case RawDragEventType.DragEnter: - e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = DragEnter(e.Root, e.Location, e.Data, e.Effects, e.Modifiers); break; case RawDragEventType.DragOver: - e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = DragOver(e.Root, e.Location, e.Data, e.Effects, e.Modifiers); break; case RawDragEventType.DragLeave: - DragLeave(e.InputRoot); + DragLeave(e.Root); break; case RawDragEventType.Drop: - e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = Drop(e.Root, e.Location, e.Data, e.Effects, e.Modifiers); break; } } diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 6b06151773..a5bd4feb64 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -97,7 +97,7 @@ namespace Avalonia.Input if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { - var et = e.MouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent; + var et = e.InitialPressMouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent; e.Source.RaiseEvent(new RoutedEventArgs(et)); } } diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 47e85416cf..535b930f8b 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -169,18 +169,18 @@ namespace Avalonia.Input { IsEnabledProperty.Changed.Subscribe(IsEnabledChanged); - GotFocusEvent.AddClassHandler(x => x.OnGotFocus); - LostFocusEvent.AddClassHandler(x => x.OnLostFocus); - KeyDownEvent.AddClassHandler(x => x.OnKeyDown); - KeyUpEvent.AddClassHandler(x => x.OnKeyUp); - TextInputEvent.AddClassHandler(x => x.OnTextInput); - PointerEnterEvent.AddClassHandler(x => x.OnPointerEnterCore); - PointerLeaveEvent.AddClassHandler(x => x.OnPointerLeaveCore); - PointerMovedEvent.AddClassHandler(x => x.OnPointerMoved); - PointerPressedEvent.AddClassHandler(x => x.OnPointerPressed); - PointerReleasedEvent.AddClassHandler(x => x.OnPointerReleased); - PointerCaptureLostEvent.AddClassHandler(x => x.OnPointerCaptureLost); - PointerWheelChangedEvent.AddClassHandler(x => x.OnPointerWheelChanged); + GotFocusEvent.AddClassHandler((x, e) => x.OnGotFocus(e)); + LostFocusEvent.AddClassHandler((x, e) => x.OnLostFocus(e)); + KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e)); + KeyUpEvent.AddClassHandler((x, e) => x.OnKeyUp(e)); + TextInputEvent.AddClassHandler((x, e) => x.OnTextInput(e)); + PointerEnterEvent.AddClassHandler((x, e) => x.OnPointerEnterCore(e)); + PointerLeaveEvent.AddClassHandler((x, e) => x.OnPointerLeaveCore(e)); + PointerMovedEvent.AddClassHandler((x, e) => x.OnPointerMoved(e)); + PointerPressedEvent.AddClassHandler((x, e) => x.OnPointerPressed(e)); + PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); + PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); + PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); PseudoClass(IsEffectivelyEnabledProperty, x => !x, ":disabled"); PseudoClass(IsFocusedProperty, ":focus"); diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index 5eaee4833c..490c31bef9 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -14,7 +14,7 @@ namespace Avalonia.Input { private static readonly Dictionary s_keySynonyms = new Dictionary { - { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod } + { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod }, { ",", Key.OemComma } }; [Obsolete("Use constructor taking KeyModifiers")] @@ -141,6 +141,11 @@ namespace Avalonia.Input return KeyModifiers.Control; } + if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return KeyModifiers.Meta; + } + return (KeyModifiers)Enum.Parse(typeof(KeyModifiers), modifier.ToString(), true); } diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index a02c580ad2..9db8525591 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -70,66 +70,60 @@ namespace Avalonia.Input { if(e.Handled) return; - IInputElement element = FocusedElement; - if (element != null) - { - var keyInput = e as RawKeyEventArgs; + var element = FocusedElement ?? e.Root; - if (keyInput != null) + if (e is RawKeyEventArgs keyInput) + { + switch (keyInput.Type) { - switch (keyInput.Type) - { - case RawKeyEventType.KeyDown: - case RawKeyEventType.KeyUp: - var routedEvent = keyInput.Type == RawKeyEventType.KeyDown - ? InputElement.KeyDownEvent - : InputElement.KeyUpEvent; - - KeyEventArgs ev = new KeyEventArgs - { - RoutedEvent = routedEvent, - Device = this, - Key = keyInput.Key, - KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), - Source = element, - }; - - IVisual currentHandler = element; - while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown) - { - var bindings = (currentHandler as IInputElement)?.KeyBindings; - if(bindings!=null) - foreach (var binding in bindings) - { - if(ev.Handled) - break; - binding.TryHandle(ev); - } - currentHandler = currentHandler.VisualParent; - } - - element.RaiseEvent(ev); - e.Handled = ev.Handled; - break; - } + case RawKeyEventType.KeyDown: + case RawKeyEventType.KeyUp: + var routedEvent = keyInput.Type == RawKeyEventType.KeyDown + ? InputElement.KeyDownEvent + : InputElement.KeyUpEvent; + + KeyEventArgs ev = new KeyEventArgs + { + RoutedEvent = routedEvent, + Device = this, + Key = keyInput.Key, + KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), + Source = element, + }; + + IVisual currentHandler = element; + while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown) + { + var bindings = (currentHandler as IInputElement)?.KeyBindings; + if (bindings != null) + foreach (var binding in bindings) + { + if (ev.Handled) + break; + binding.TryHandle(ev); + } + currentHandler = currentHandler.VisualParent; + } + + element.RaiseEvent(ev); + e.Handled = ev.Handled; + break; } + } - var text = e as RawTextInputEventArgs; - - if (text != null) + if (e is RawTextInputEventArgs text) + { + var ev = new TextInputEventArgs() { - var ev = new TextInputEventArgs() - { - Device = this, - Text = text.Text, - Source = element, - RoutedEvent = InputElement.TextInputEvent - }; - - element.RaiseEvent(ev); - e.Handled = ev.Handled; - } + Device = this, + Text = text.Text, + Source = element, + RoutedEvent = InputElement.TextInputEvent + }; + + element.RaiseEvent(ev); + e.Handled = ev.Handled; } } } diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 0d5471f790..c84596b913 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -221,7 +221,7 @@ namespace Avalonia.Input _lastClickTime = timestamp; _lastClickRect = new Rect(p, new Size()) .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2)); - _lastMouseDownButton = properties.GetObsoleteMouseButton(); + _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); source.RaiseEvent(e); return e.Handled; @@ -267,7 +267,8 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers); + var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, + _lastMouseDownButton); source?.RaiseEvent(e); _pointer.Capture(null); diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 5b3c43e4df..e12a20f7a2 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -88,9 +88,20 @@ namespace Avalonia.Input return _rootVisualPosition * _rootVisual.TransformToVisual(relativeTo) ?? default; } - public PointerPoint GetPointerPoint(IVisual relativeTo) + [Obsolete("Use GetCurrentPoint")] + public PointerPoint GetPointerPoint(IVisual relativeTo) => GetCurrentPoint(relativeTo); + + /// + /// Returns the PointerPoint associated with the current event + /// + /// The visual which coordinate system to use. Pass null for toplevel coordinate system + /// + public PointerPoint GetCurrentPoint(IVisual relativeTo) => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); + /// + /// Returns the current pointer point properties + /// protected PointerPointProperties Properties => _properties; } @@ -124,7 +135,7 @@ namespace Avalonia.Input public int ClickCount => _obsoleteClickCount; [Obsolete("Use PointerUpdateKind")] - public MouseButton MouseButton => Properties.GetObsoleteMouseButton(); + public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton(); } public class PointerReleasedEventArgs : PointerEventArgs @@ -132,15 +143,21 @@ namespace Avalonia.Input public PointerReleasedEventArgs( IInteractive source, IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp, - PointerPointProperties properties, KeyModifiers modifiers) + PointerPointProperties properties, KeyModifiers modifiers, + MouseButton initialPressMouseButton) : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { - + InitialPressMouseButton = initialPressMouseButton; } - [Obsolete("Use PointerUpdateKind")] - public MouseButton MouseButton => Properties.GetObsoleteMouseButton(); + /// + /// Gets the mouse button that triggered the corresponding PointerPressed event + /// + public MouseButton InitialPressMouseButton { get; } + + [Obsolete("Either use GetCurrentPoint(this).Properties.PointerUpdateKind or InitialPressMouseButton, see https://github.com/AvaloniaUI/Avalonia/wiki/Pointer-events-in-0.9 for more details", true)] + public MouseButton MouseButton => InitialPressMouseButton; } public class PointerCaptureLostEventArgs : RoutedEventArgs diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 55c1889e58..1068a0d4d4 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -52,17 +52,6 @@ namespace Avalonia.Input } public static PointerPointProperties None { get; } = new PointerPointProperties(); - - public MouseButton GetObsoleteMouseButton() - { - if (PointerUpdateKind == PointerUpdateKind.LeftButtonPressed || PointerUpdateKind == PointerUpdateKind.LeftButtonReleased) - return MouseButton.Left; - if (PointerUpdateKind == PointerUpdateKind.MiddleButtonPressed || PointerUpdateKind == PointerUpdateKind.MiddleButtonReleased) - return MouseButton.Middle; - if (PointerUpdateKind == PointerUpdateKind.RightButtonPressed || PointerUpdateKind == PointerUpdateKind.RightButtonReleased) - return MouseButton.Right; - return MouseButton.None; - } } public enum PointerUpdateKind @@ -75,4 +64,18 @@ namespace Avalonia.Input RightButtonReleased, Other } + + public static class PointerUpdateKindExtensions + { + public static MouseButton GetMouseButton(this PointerUpdateKind kind) + { + if (kind == PointerUpdateKind.LeftButtonPressed || kind == PointerUpdateKind.LeftButtonReleased) + return MouseButton.Left; + if (kind == PointerUpdateKind.MiddleButtonPressed || kind == PointerUpdateKind.MiddleButtonReleased) + return MouseButton.Middle; + if (kind == PointerUpdateKind.RightButtonPressed || kind == PointerUpdateKind.RightButtonReleased) + return MouseButton.Right; + return MouseButton.None; + } + } } diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs index 1480f92f03..5722e17593 100644 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ b/src/Avalonia.Input/Raw/RawDragEvent.cs @@ -2,7 +2,6 @@ { public class RawDragEvent : RawInputEventArgs { - public IInputElement InputRoot { get; } public Point Location { get; set; } public IDataObject Data { get; } public DragDropEffects Effects { get; set; } @@ -10,11 +9,10 @@ public InputModifiers Modifiers { get; } public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, - IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) - :base(inputDevice, 0) + IInputRoot root, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) + :base(inputDevice, 0, root) { Type = type; - InputRoot = inputRoot; Location = location; Data = data; Effects = effects; diff --git a/src/Avalonia.Input/Raw/RawInputEventArgs.cs b/src/Avalonia.Input/Raw/RawInputEventArgs.cs index 78c1b58624..6a488c4cc7 100644 --- a/src/Avalonia.Input/Raw/RawInputEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawInputEventArgs.cs @@ -21,12 +21,14 @@ namespace Avalonia.Input.Raw /// /// The associated device. /// The event timestamp. - public RawInputEventArgs(IInputDevice device, ulong timestamp) + /// The root from which the event originates. + public RawInputEventArgs(IInputDevice device, ulong timestamp, IInputRoot root) { Contract.Requires(device != null); Device = device; Timestamp = timestamp; + Root = root; } /// @@ -34,6 +36,11 @@ namespace Avalonia.Input.Raw /// public IInputDevice Device { get; } + /// + /// Gets the root from which the event originates. + /// + public IInputRoot Root { get; } + /// /// Gets or sets a value indicating whether the event was handled. /// diff --git a/src/Avalonia.Input/Raw/RawKeyEventArgs.cs b/src/Avalonia.Input/Raw/RawKeyEventArgs.cs index cd8b2eacf7..c720cf11f8 100644 --- a/src/Avalonia.Input/Raw/RawKeyEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawKeyEventArgs.cs @@ -14,9 +14,10 @@ namespace Avalonia.Input.Raw public RawKeyEventArgs( IKeyboardDevice device, ulong timestamp, + IInputRoot root, RawKeyEventType type, Key key, RawInputModifiers modifiers) - : base(device, timestamp) + : base(device, timestamp, root) { Key = key; Type = type; diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 88f6daf11f..56854c7d29 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -44,22 +44,16 @@ namespace Avalonia.Input.Raw RawPointerEventType type, Point position, RawInputModifiers inputModifiers) - : base(device, timestamp) + : base(device, timestamp, root) { Contract.Requires(device != null); Contract.Requires(root != null); - Root = root; Position = position; Type = type; InputModifiers = inputModifiers; } - /// - /// Gets the root from which the event originates. - /// - public IInputRoot Root { get; } - /// /// Gets the mouse position, in client DIPs. /// diff --git a/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs b/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs index 0d1e5d2422..010342da15 100644 --- a/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs @@ -5,11 +5,16 @@ namespace Avalonia.Input.Raw { public class RawTextInputEventArgs : RawInputEventArgs { - public string Text { get; set; } - - public RawTextInputEventArgs(IKeyboardDevice device, ulong timestamp, string text) : base(device, timestamp) + public RawTextInputEventArgs( + IKeyboardDevice device, + ulong timestamp, + IInputRoot root, + string text) + : base(device, timestamp, root) { Text = text; } + + public string Text { get; set; } } } diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 765e02848f..b231c9fff4 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -60,7 +60,7 @@ namespace Avalonia.Input args.Root, args.Position, ev.Timestamp, new PointerPointProperties(GetModifiers(args.InputModifiers, false), PointerUpdateKind.LeftButtonReleased), - GetKeyModifiers(args.InputModifiers))); + GetKeyModifiers(args.InputModifiers), MouseButton.Left)); } } diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index cfbaddb327..bc5dec9a90 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -113,24 +113,38 @@ namespace Avalonia.Interactivity Contract.Requires(ownerType != null); } + [Obsolete("Use overload taking Action.")] public IDisposable AddClassHandler( Func> handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) - where TTarget : class, IInteractive + where TTarget : class, IInteractive { - EventHandler adapter = (sender, e) => + void Adapter(object sender, RoutedEventArgs e) { - var target = sender as TTarget; - var args = e as TEventArgs; - - if (target != null && args != null) + if (sender is TTarget target && e is TEventArgs args) { handler(target)(args); } - }; + } + + return AddClassHandler(typeof(TTarget), Adapter, routes, handledEventsToo); + } + + public IDisposable AddClassHandler( + Action handler, + RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, + bool handledEventsToo = false) where TTarget : class, IInteractive + { + void Adapter(object sender, RoutedEventArgs e) + { + if (sender is TTarget target && e is TEventArgs args) + { + handler(target, args); + } + } - return AddClassHandler(typeof(TTarget), adapter, routes, handledEventsToo); + return AddClassHandler(typeof(TTarget), Adapter, routes, handledEventsToo); } } } diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 45efccc1fa..855f123748 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Diagnostics; using Avalonia.Logging; using Avalonia.Threading; @@ -69,15 +70,23 @@ namespace Avalonia.Layout { _running = true; - Logger.Information( - LogArea.Layout, - this, - "Started layout pass. To measure: {Measure} To arrange: {Arrange}", - _toMeasure.Count, - _toArrange.Count); + Stopwatch stopwatch = null; - var stopwatch = new System.Diagnostics.Stopwatch(); - stopwatch.Start(); + const LogEventLevel timingLogLevel = LogEventLevel.Information; + bool captureTiming = Logger.IsEnabled(timingLogLevel); + + if (captureTiming) + { + Logger.TryGet(timingLogLevel)?.Log( + LogArea.Layout, + this, + "Started layout pass. To measure: {Measure} To arrange: {Arrange}", + _toMeasure.Count, + _toArrange.Count); + + stopwatch = new Stopwatch(); + stopwatch.Start(); + } _toMeasure.BeginLoop(MaxPasses); _toArrange.BeginLoop(MaxPasses); @@ -103,8 +112,12 @@ namespace Avalonia.Layout _toMeasure.EndLoop(); _toArrange.EndLoop(); - stopwatch.Stop(); - Logger.Information(LogArea.Layout, this, "Layout pass finished in {Time}", stopwatch.Elapsed); + if (captureTiming) + { + stopwatch.Stop(); + + Logger.TryGet(timingLogLevel)?.Log(LogArea.Layout, this, "Layout pass finished in {Time}", stopwatch.Elapsed); + } } _queued = false; diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index bd248d6d44..b0757a823d 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -329,7 +329,7 @@ namespace Avalonia.Layout DesiredSize = desiredSize; _previousMeasure = availableSize; - Logger.Verbose(LogArea.Layout, this, "Measure requested {DesiredSize}", DesiredSize); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Measure requested {DesiredSize}", DesiredSize); if (DesiredSize != previousDesiredSize) { @@ -356,7 +356,7 @@ namespace Avalonia.Layout if (!IsArrangeValid || _previousArrange != rect) { - Logger.Verbose(LogArea.Layout, this, "Arrange to {Rect} ", rect); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Arrange to {Rect} ", rect); IsArrangeValid = true; ArrangeCore(rect); @@ -381,7 +381,7 @@ namespace Avalonia.Layout { if (IsMeasureValid) { - Logger.Verbose(LogArea.Layout, this, "Invalidated measure"); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Invalidated measure"); IsMeasureValid = false; IsArrangeValid = false; @@ -402,7 +402,7 @@ namespace Avalonia.Layout { if (IsArrangeValid) { - Logger.Verbose(LogArea.Layout, this, "Invalidated arrange"); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Invalidated arrange"); IsArrangeValid = false; (VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this); diff --git a/src/Avalonia.Logging.Serilog/SerilogLogger.cs b/src/Avalonia.Logging.Serilog/SerilogLogger.cs index 0534fe3012..895ee268d2 100644 --- a/src/Avalonia.Logging.Serilog/SerilogLogger.cs +++ b/src/Avalonia.Logging.Serilog/SerilogLogger.cs @@ -34,6 +34,76 @@ namespace Avalonia.Logging.Serilog Logger.Sink = new SerilogLogger(output); } + public bool IsEnabled(LogEventLevel level) + { + return _output.IsEnabled((SerilogLogEventLevel)level); + } + + public void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate) + { + Contract.Requires(area != null); + Contract.Requires(messageTemplate != null); + + using (PushLogContextProperties(area, source)) + { + _output.Write((SerilogLogEventLevel)level, messageTemplate); + } + } + + public void Log( + LogEventLevel level, + string area, object source, + string messageTemplate, + T0 propertyValue0) + { + Contract.Requires(area != null); + Contract.Requires(messageTemplate != null); + + using (PushLogContextProperties(area, source)) + { + _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0); + } + } + + public void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1) + { + Contract.Requires(area != null); + Contract.Requires(messageTemplate != null); + + using (PushLogContextProperties(area, source)) + { + _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0, propertyValue1); + } + } + + public void Log( + LogEventLevel level, + string area, + object source, + string messageTemplate, + T0 propertyValue0, + T1 propertyValue1, + T2 propertyValue2) + { + Contract.Requires(area != null); + Contract.Requires(messageTemplate != null); + + using (PushLogContextProperties(area, source)) + { + _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + /// public void Log( AvaloniaLogEventLevel level, @@ -45,12 +115,40 @@ namespace Avalonia.Logging.Serilog Contract.Requires(area != null); Contract.Requires(messageTemplate != null); - using (LogContext.PushProperty("Area", area)) - using (LogContext.PushProperty("SourceType", source?.GetType())) - using (LogContext.PushProperty("SourceHash", source?.GetHashCode())) + using (PushLogContextProperties(area, source)) { _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValues); } } + + private static LogContextDisposable PushLogContextProperties(string area, object source) + { + return new LogContextDisposable( + LogContext.PushProperty("Area", area), + LogContext.PushProperty("SourceType", source?.GetType()), + LogContext.PushProperty("SourceHash", source?.GetHashCode()) + ); + } + + private readonly struct LogContextDisposable : IDisposable + { + private readonly IDisposable _areaDisposable; + private readonly IDisposable _sourceTypeDisposable; + private readonly IDisposable _sourceHashDisposable; + + public LogContextDisposable(IDisposable areaDisposable, IDisposable sourceTypeDisposable, IDisposable sourceHashDisposable) + { + _areaDisposable = areaDisposable; + _sourceTypeDisposable = sourceTypeDisposable; + _sourceHashDisposable = sourceHashDisposable; + } + + public void Dispose() + { + _areaDisposable.Dispose(); + _sourceTypeDisposable.Dispose(); + _sourceHashDisposable.Dispose(); + } + } } } diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs new file mode 100644 index 0000000000..950943d54a --- /dev/null +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -0,0 +1,445 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Native.Interop; +using Avalonia.Platform.Interop; +using Avalonia.Threading; + +namespace Avalonia.Native +{ + enum OsxUnicodeSpecialKey + { + NSUpArrowFunctionKey = 0xF700, + NSDownArrowFunctionKey = 0xF701, + NSLeftArrowFunctionKey = 0xF702, + NSRightArrowFunctionKey = 0xF703, + NSF1FunctionKey = 0xF704, + NSF2FunctionKey = 0xF705, + NSF3FunctionKey = 0xF706, + NSF4FunctionKey = 0xF707, + NSF5FunctionKey = 0xF708, + NSF6FunctionKey = 0xF709, + NSF7FunctionKey = 0xF70A, + NSF8FunctionKey = 0xF70B, + NSF9FunctionKey = 0xF70C, + NSF10FunctionKey = 0xF70D, + NSF11FunctionKey = 0xF70E, + NSF12FunctionKey = 0xF70F, + NSF13FunctionKey = 0xF710, + NSF14FunctionKey = 0xF711, + NSF15FunctionKey = 0xF712, + NSF16FunctionKey = 0xF713, + NSF17FunctionKey = 0xF714, + NSF18FunctionKey = 0xF715, + NSF19FunctionKey = 0xF716, + NSF20FunctionKey = 0xF717, + NSF21FunctionKey = 0xF718, + NSF22FunctionKey = 0xF719, + NSF23FunctionKey = 0xF71A, + NSF24FunctionKey = 0xF71B, + NSF25FunctionKey = 0xF71C, + NSF26FunctionKey = 0xF71D, + NSF27FunctionKey = 0xF71E, + NSF28FunctionKey = 0xF71F, + NSF29FunctionKey = 0xF720, + NSF30FunctionKey = 0xF721, + NSF31FunctionKey = 0xF722, + NSF32FunctionKey = 0xF723, + NSF33FunctionKey = 0xF724, + NSF34FunctionKey = 0xF725, + NSF35FunctionKey = 0xF726, + NSInsertFunctionKey = 0xF727, + NSDeleteFunctionKey = 0xF728, + NSHomeFunctionKey = 0xF729, + NSBeginFunctionKey = 0xF72A, + NSEndFunctionKey = 0xF72B, + NSPageUpFunctionKey = 0xF72C, + NSPageDownFunctionKey = 0xF72D, + NSPrintScreenFunctionKey = 0xF72E, + NSScrollLockFunctionKey = 0xF72F, + NSPauseFunctionKey = 0xF730, + NSSysReqFunctionKey = 0xF731, + NSBreakFunctionKey = 0xF732, + NSResetFunctionKey = 0xF733, + NSStopFunctionKey = 0xF734, + NSMenuFunctionKey = 0xF735, + NSUserFunctionKey = 0xF736, + NSSystemFunctionKey = 0xF737, + NSPrintFunctionKey = 0xF738, + NSClearLineFunctionKey = 0xF739, + NSClearDisplayFunctionKey = 0xF73A, + NSInsertLineFunctionKey = 0xF73B, + NSDeleteLineFunctionKey = 0xF73C, + NSInsertCharFunctionKey = 0xF73D, + NSDeleteCharFunctionKey = 0xF73E, + NSPrevFunctionKey = 0xF73F, + NSNextFunctionKey = 0xF740, + NSSelectFunctionKey = 0xF741, + NSExecuteFunctionKey = 0xF742, + NSUndoFunctionKey = 0xF743, + NSRedoFunctionKey = 0xF744, + NSFindFunctionKey = 0xF745, + NSHelpFunctionKey = 0xF746, + NSModeSwitchFunctionKey = 0xF747 + } + + public class MenuActionCallback : CallbackBase, IAvnActionCallback + { + private Action _action; + + public MenuActionCallback(Action action) + { + _action = action; + } + + void IAvnActionCallback.Run() + { + _action?.Invoke(); + } + } + + public class PredicateCallback : CallbackBase, IAvnPredicateCallback + { + private Func _predicate; + + public PredicateCallback(Func predicate) + { + _predicate = predicate; + } + + bool IAvnPredicateCallback.Evaluate() + { + return _predicate(); + } + } + + class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter + { + private IAvaloniaNativeFactory _factory; + private NativeMenu _menu; + private bool _resetQueued; + private bool _exported = false; + private IAvnWindow _nativeWindow; + private List _menuItems = new List(); + + private static Dictionary osxKeys = new Dictionary + { + {Key.Up, OsxUnicodeSpecialKey.NSUpArrowFunctionKey }, + {Key.Down, OsxUnicodeSpecialKey.NSDownArrowFunctionKey }, + {Key.Left, OsxUnicodeSpecialKey.NSLeftArrowFunctionKey }, + {Key.Right, OsxUnicodeSpecialKey.NSRightArrowFunctionKey }, + { Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey }, + { Key.F2, OsxUnicodeSpecialKey.NSF2FunctionKey }, + { Key.F3, OsxUnicodeSpecialKey.NSF3FunctionKey }, + { Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey }, + { Key.F5, OsxUnicodeSpecialKey.NSF5FunctionKey }, + { Key.F6, OsxUnicodeSpecialKey.NSF6FunctionKey }, + { Key.F7, OsxUnicodeSpecialKey.NSF7FunctionKey }, + { Key.F8, OsxUnicodeSpecialKey.NSF8FunctionKey }, + { Key.F9, OsxUnicodeSpecialKey.NSF9FunctionKey }, + { Key.F10, OsxUnicodeSpecialKey.NSF10FunctionKey }, + { Key.F11, OsxUnicodeSpecialKey.NSF11FunctionKey }, + { Key.F12, OsxUnicodeSpecialKey.NSF12FunctionKey }, + { Key.F13, OsxUnicodeSpecialKey.NSF13FunctionKey }, + { Key.F14, OsxUnicodeSpecialKey.NSF14FunctionKey }, + { Key.F15, OsxUnicodeSpecialKey.NSF15FunctionKey }, + { Key.F16, OsxUnicodeSpecialKey.NSF16FunctionKey }, + { Key.F17, OsxUnicodeSpecialKey.NSF17FunctionKey }, + { Key.F18, OsxUnicodeSpecialKey.NSF18FunctionKey }, + { Key.F19, OsxUnicodeSpecialKey.NSF19FunctionKey }, + { Key.F20, OsxUnicodeSpecialKey.NSF20FunctionKey }, + { Key.F21, OsxUnicodeSpecialKey.NSF21FunctionKey }, + { Key.F22, OsxUnicodeSpecialKey.NSF22FunctionKey }, + { Key.F23, OsxUnicodeSpecialKey.NSF23FunctionKey }, + { Key.F24, OsxUnicodeSpecialKey.NSF24FunctionKey }, + { Key.Insert, OsxUnicodeSpecialKey.NSInsertFunctionKey }, + { Key.Delete, OsxUnicodeSpecialKey.NSDeleteFunctionKey }, + { Key.Home, OsxUnicodeSpecialKey.NSHomeFunctionKey }, + //{ Key.Begin, OsxUnicodeSpecialKey.NSBeginFunctionKey }, + { Key.End, OsxUnicodeSpecialKey.NSEndFunctionKey }, + { Key.PageUp, OsxUnicodeSpecialKey.NSPageUpFunctionKey }, + { Key.PageDown, OsxUnicodeSpecialKey.NSPageDownFunctionKey }, + { Key.PrintScreen, OsxUnicodeSpecialKey.NSPrintScreenFunctionKey }, + { Key.Scroll, OsxUnicodeSpecialKey.NSScrollLockFunctionKey }, + //{ Key.SysReq, OsxUnicodeSpecialKey.NSSysReqFunctionKey }, + //{ Key.Break, OsxUnicodeSpecialKey.NSBreakFunctionKey }, + //{ Key.Reset, OsxUnicodeSpecialKey.NSResetFunctionKey }, + //{ Key.Stop, OsxUnicodeSpecialKey.NSStopFunctionKey }, + //{ Key.Menu, OsxUnicodeSpecialKey.NSMenuFunctionKey }, + //{ Key.UserFunction, OsxUnicodeSpecialKey.NSUserFunctionKey }, + //{ Key.SystemFunction, OsxUnicodeSpecialKey.NSSystemFunctionKey }, + { Key.Print, OsxUnicodeSpecialKey.NSPrintFunctionKey }, + //{ Key.ClearLine, OsxUnicodeSpecialKey.NSClearLineFunctionKey }, + //{ Key.ClearDisplay, OsxUnicodeSpecialKey.NSClearDisplayFunctionKey }, + }; + + public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) + { + _factory = factory; + _nativeWindow = nativeWindow; + + DoLayoutReset(); + } + + public AvaloniaNativeMenuExporter(IAvaloniaNativeFactory factory) + { + _factory = factory; + + _menu = NativeMenu.GetMenu(Application.Current); + DoLayoutReset(); + } + + public bool IsNativeMenuExported => _exported; + + public event EventHandler OnIsNativeMenuExportedChanged; + + public void SetNativeMenu(NativeMenu menu) + { + if (menu == null) + menu = new NativeMenu(); + + if (_menu != null) + ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; + _menu = menu; + ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged; + + DoLayoutReset(); + } + + private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + QueueReset(); + } + + private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + QueueReset(); + } + + void DoLayoutReset() + { + _resetQueued = false; + foreach (var i in _menuItems) + { + i.PropertyChanged -= OnItemPropertyChanged; + if (i.Menu != null) + ((INotifyCollectionChanged)i.Menu.Items).CollectionChanged -= OnMenuItemsChanged; + } + + _menuItems.Clear(); + + if(_nativeWindow is null) + { + _menu = NativeMenu.GetMenu(Application.Current); + + if(_menu != null) + { + SetMenu(_menu); + } + } + else + { + SetMenu(_nativeWindow, _menu?.Items); + } + + _exported = true; + } + + private void QueueReset() + { + if (_resetQueued) + return; + _resetQueued = true; + Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); + } + + private IAvnAppMenu CreateSubmenu(ICollection children) + { + var menu = _factory.CreateMenu(); + + SetChildren(menu, children); + + return menu; + } + + private void AddMenuItem(NativeMenuItem item) + { + if (item.Menu?.Items != null) + { + ((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged; + } + } + + private static string ConvertOSXSpecialKeyCodes(Key key) + { + if (osxKeys.ContainsKey(key)) + { + return ((char)osxKeys[key]).ToString(); + } + else + { + return key.ToString().ToLower(); + } + } + + private void SetChildren(IAvnAppMenu menu, ICollection children) + { + foreach (var i in children) + { + if (i is NativeMenuItem item) + { + AddMenuItem(item); + + var menuItem = _factory.CreateMenuItem(); + + using (var buffer = new Utf8Buffer(item.Header)) + { + menuItem.Title = buffer.DangerousGetHandle(); + } + + if (item.Gesture != null) + { + using (var buffer = new Utf8Buffer(ConvertOSXSpecialKeyCodes(item.Gesture.Key))) + { + menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); + } + } + + menuItem.SetAction(new PredicateCallback(() => + { + if (item.Command != null || item.HasClickHandlers) + { + return item.Enabled; + } + + return false; + }), new MenuActionCallback(() => { item.RaiseClick(); })); + menu.AddItem(menuItem); + + if (item.Menu?.Items?.Count > 0) + { + var submenu = _factory.CreateMenu(); + + using (var buffer = new Utf8Buffer(item.Header)) + { + submenu.Title = buffer.DangerousGetHandle(); + } + + menuItem.SetSubMenu(submenu); + + AddItemsToMenu(submenu, item.Menu?.Items); + } + } + else if (i is NativeMenuItemSeperator seperator) + { + menu.AddItem(_factory.CreateMenuItemSeperator()); + } + } + } + + private void AddItemsToMenu(IAvnAppMenu menu, ICollection items, bool isMainMenu = false) + { + foreach (var i in items) + { + if (i is NativeMenuItem item) + { + var menuItem = _factory.CreateMenuItem(); + + AddMenuItem(item); + + menuItem.SetAction(new PredicateCallback(() => + { + if (item.Command != null || item.HasClickHandlers) + { + return item.Enabled; + } + + return false; + }), new MenuActionCallback(() => { item.RaiseClick(); })); + + if (item.Menu?.Items.Count > 0 || isMainMenu) + { + var subMenu = CreateSubmenu(item.Menu?.Items); + + menuItem.SetSubMenu(subMenu); + + using (var buffer = new Utf8Buffer(item.Header)) + { + subMenu.Title = buffer.DangerousGetHandle(); + } + } + else + { + using (var buffer = new Utf8Buffer(item.Header)) + { + menuItem.Title = buffer.DangerousGetHandle(); + } + + if (item.Gesture != null) + { + using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) + { + menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); + } + } + } + + menu.AddItem(menuItem); + } + else if(i is NativeMenuItemSeperator seperator) + { + menu.AddItem(_factory.CreateMenuItemSeperator()); + } + } + } + + private void SetMenu(NativeMenu menu) + { + var appMenu = _factory.ObtainAppMenu(); + + if (appMenu is null) + { + appMenu = _factory.CreateMenu(); + } + + var menuItem = menu.Parent; + + if(menu.Parent is null) + { + menuItem = new NativeMenuItem(); + } + + menuItem.Menu = menu; + + appMenu.Clear(); + AddItemsToMenu(appMenu, new List { menuItem }); + + _factory.SetAppMenu(appMenu); + } + + private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) + { + if (menuItems is null) + { + menuItems = new List(); + } + + var appMenu = avnWindow.ObtainMainMenu(); + + if (appMenu is null) + { + appMenu = _factory.CreateMenu(); + } + + appMenu.Clear(); + AddItemsToMenu(appMenu, menuItems); + + avnWindow.SetMainMenu(appMenu); + } + } +} diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 6d48ab3829..ddb71b61bb 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -9,6 +9,7 @@ using Avalonia.Native.Interop; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Platform.Interop; namespace Avalonia.Native { @@ -27,15 +28,17 @@ namespace Avalonia.Native public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); //TODO - public static void Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) + public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) { - new AvaloniaNativePlatform(new IAvaloniaNativeFactory(factory)) - .DoInitialize(options); + var result = new AvaloniaNativePlatform(new IAvaloniaNativeFactory(factory)); + result.DoInitialize(options); + + return result; } delegate IntPtr CreateAvaloniaNativeDelegate(); - public static void Initialize(AvaloniaNativePlatformOptions options) + public static AvaloniaNativePlatform Initialize(AvaloniaNativePlatformOptions options) { if (options.AvaloniaNativeLibraryPath != null) { @@ -48,10 +51,26 @@ namespace Avalonia.Native var d = Marshal.GetDelegateForFunctionPointer(proc); - Initialize(d(), options); + return Initialize(d(), options); } else - Initialize(CreateAvaloniaNative(), options); + return Initialize(CreateAvaloniaNative(), options); + } + + public void SetupApplicationMenuExporter () + { + var exporter = new AvaloniaNativeMenuExporter(_factory); + } + + public void SetupApplicationName () + { + if(!string.IsNullOrWhiteSpace(Application.Current.Name)) + { + using (var buffer = new Utf8Buffer(Application.Current.Name)) + { + _factory.MacOptions.SetApplicationTitle(buffer.DangerousGetHandle()); + } + } } private AvaloniaNativePlatform(IAvaloniaNativeFactory factory) @@ -66,6 +85,7 @@ namespace Avalonia.Native if (_factory.MacOptions != null) { var macOpts = AvaloniaLocator.Current.GetService(); + _factory.MacOptions.ShowInDock = macOpts?.ShowInDock != false ? 1 : 0; } diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 02810ed155..091056142f 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -13,9 +13,18 @@ namespace Avalonia where T : AppBuilderBase, new() { builder.UseWindowingSubsystem(() => - AvaloniaNativePlatform.Initialize( + { + var platform = AvaloniaNativePlatform.Initialize( AvaloniaLocator.Current.GetService() ?? - new AvaloniaNativePlatformOptions())); + new AvaloniaNativePlatformOptions()); + + builder.AfterSetup (x=> + { + platform.SetupApplicationName(); + platform.SetupApplicationMenuExporter(); + }); + }); + return builder; } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 490d5688a8..a7828bedaf 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -3,13 +3,14 @@ using System; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Native.Interop; using Avalonia.Platform; using Avalonia.Platform.Interop; namespace Avalonia.Native { - public class WindowImpl : WindowBaseImpl, IWindowImpl + public class WindowImpl : WindowBaseImpl, IWindowImpl, ITopLevelImplWithNativeMenuExporter { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; @@ -22,6 +23,8 @@ namespace Avalonia.Native { Init(_native = factory.CreateWindow(e), factory.CreateScreens()); } + + NativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory); } class WindowEvents : WindowBaseEvents, IAvnWindowEvents @@ -104,6 +107,9 @@ namespace Avalonia.Native } public Func Closing { get; set; } + + public ITopLevelNativeMenuExporter NativeMenuExporter { get; } + public void Move(PixelPoint point) => Position = point; public override IPopupImpl CreatePopup() => diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index d8ff370c45..fe7458d583 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -204,7 +204,7 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - var args = new RawTextInputEventArgs(_keyboard, timeStamp, text); + var args = new RawTextInputEventArgs(_keyboard, timeStamp, _inputRoot, text); Input?.Invoke(args); @@ -215,7 +215,7 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - var args = new RawKeyEventArgs(_keyboard, timeStamp, (RawKeyEventType)type, (Key)key, (RawInputModifiers)modifiers); + var args = new RawKeyEventArgs(_keyboard, timeStamp, _inputRoot, (RawKeyEventType)type, (Key)key, (RawInputModifiers)modifiers); Input?.Invoke(args); @@ -301,7 +301,7 @@ namespace Avalonia.Native _native.Hide(); } - public void BeginMoveDrag() + public void BeginMoveDrag(PointerPressedEventArgs e) { _native.BeginMoveDrag(); } @@ -343,7 +343,7 @@ namespace Avalonia.Native _native.SetMinMaxSize(minSize.ToAvnSize(), maxSize.ToAvnSize()); } - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { } diff --git a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs index 86411b89da..5f5064fba5 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs @@ -31,7 +31,7 @@ namespace Avalonia.OpenGL } catch(Exception e) { - Logger.Error("OpenGL", null, "Unable to initialize EGL-based rendering: {0}", e); + Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", null, "Unable to initialize EGL-based rendering: {0}", e); return null; } } diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 38c29289b6..de8093c048 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -73,7 +73,7 @@ namespace Avalonia /// static StyledElement() { - DataContextProperty.Changed.AddClassHandler(x => x.OnDataContextChangedCore); + DataContextProperty.Changed.AddClassHandler((x,e) => x.OnDataContextChangedCore(e)); } /// @@ -743,11 +743,11 @@ namespace Avalonia #if DEBUG if (((INotifyCollectionChangedDebug)_classes).GetCollectionChangedSubscribers()?.Length > 0) { - Logger.Warning( + Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Control, this, "{Type} detached from logical tree but still has class listeners", - this.GetType()); + GetType()); } #endif } diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index 362ac86e50..f1fd2f6c7f 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs @@ -18,8 +18,9 @@ namespace Avalonia.Styling internal class TypeNameAndClassSelector : Selector { private readonly Selector _previous; + private readonly Lazy> _classes = new Lazy>(() => new List()); private Type _targetType; - private Lazy> _classes = new Lazy>(() => new List()); + private string _selectorString; public static TypeNameAndClassSelector OfType(Selector previous, Type targetType) @@ -27,6 +28,7 @@ namespace Avalonia.Styling var result = new TypeNameAndClassSelector(previous); result._targetType = targetType; result.IsConcreteType = true; + return result; } @@ -35,6 +37,7 @@ namespace Avalonia.Styling var result = new TypeNameAndClassSelector(previous); result._targetType = targetType; result.IsConcreteType = false; + return result; } @@ -42,6 +45,7 @@ namespace Avalonia.Styling { var result = new TypeNameAndClassSelector(previous); result.Name = name; + return result; } @@ -49,6 +53,7 @@ namespace Avalonia.Styling { var result = new TypeNameAndClassSelector(previous); result.Classes.Add(className); + return result; } @@ -126,9 +131,11 @@ namespace Avalonia.Styling if (subscribe) { var observable = new ClassObserver(control.Classes, _classes.Value); + return new SelectorMatch(observable); } - else if (!Matches(control.Classes)) + + if (!AreClassesMatching(control.Classes, Classes)) { return SelectorMatch.NeverThisInstance; } @@ -139,21 +146,6 @@ namespace Avalonia.Styling protected override Selector MovePrevious() => _previous; - private bool Matches(IEnumerable classes) - { - int remaining = Classes.Count; - - foreach (var c in classes) - { - if (Classes.Contains(c)) - { - --remaining; - } - } - - return remaining == 0; - } - private string BuildSelectorString() { var builder = new StringBuilder(); @@ -199,11 +191,41 @@ namespace Avalonia.Styling return builder.ToString(); } - private class ClassObserver : LightweightObservableBase + private static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) { - readonly IList _match; - IAvaloniaReadOnlyList _classes; - bool _value; + int remainingMatches = toMatch.Count; + int classesCount = classes.Count; + + // Early bail out - we can't match if control does not have enough classes. + if (classesCount < remainingMatches) + { + return false; + } + + for (var i = 0; i < classesCount; i++) + { + var c = classes[i]; + + if (toMatch.Contains(c)) + { + --remainingMatches; + + // Already matched so we can skip checking other classes. + if (remainingMatches == 0) + { + break; + } + } + } + + return remainingMatches == 0; + } + + private sealed class ClassObserver : LightweightObservableBase + { + private readonly IList _match; + private readonly IAvaloniaReadOnlyList _classes; + private bool _hasMatch; public ClassObserver(IAvaloniaReadOnlyList classes, IList match) { @@ -215,42 +237,32 @@ namespace Avalonia.Styling protected override void Initialize() { - _value = GetResult(); + _hasMatch = IsMatching(); _classes.CollectionChanged += ClassesChanged; } protected override void Subscribed(IObserver observer, bool first) { - observer.OnNext(_value); + observer.OnNext(_hasMatch); } private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action != NotifyCollectionChangedAction.Move) { - var value = GetResult(); + var hasMatch = IsMatching(); - if (value != _value) + if (hasMatch != _hasMatch) { - PublishNext(GetResult()); - _value = value; + PublishNext(hasMatch); + _hasMatch = hasMatch; } } } - private bool GetResult() + private bool IsMatching() { - int remaining = _match.Count; - - foreach (var c in _classes) - { - if (_match.Contains(c)) - { - --remaining; - } - } - - return remaining == 0; + return AreClassesMatching(_classes, _match); } } } diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index 0ed17fae76..ffe3e92202 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -14,7 +14,9 @@ #FFA0A0A0 #FF282828 #FF505050 + #FF686868 #FF808080 + #FFEFEBEF #FFA8A8A8 #FF828282 #FF505050 @@ -32,7 +34,9 @@ + + @@ -61,6 +65,7 @@ 12 16 - 10 + 18 + 8 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 3a8a8ec446..c0e5f47eed 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -12,9 +12,14 @@ #FFAAAAAA #FF888888 #FF333333 - #FFFFFFFF - #FFAAAAAA - #FF888888 + + + #FF868999 + #FFF5F5F5 + #FFC2C3C9 + #FF686868 + #FF5B5B5B + #FFF0F0F0 #FFD0D0D0 #FF808080 @@ -32,7 +37,9 @@ + + @@ -61,6 +68,7 @@ 12 16 - 10 + 18 + 8 diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 114979fba2..67279fca99 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -51,4 +51,5 @@ + diff --git a/src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs b/src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs new file mode 100644 index 0000000000..7befc81b8e --- /dev/null +++ b/src/Avalonia.Themes.Default/InverseBooleanValueConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Themes.Default +{ + class InverseBooleanValueConverter : IValueConverter + { + public bool Default { get; set; } + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool b ? !b : Default; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool b ? !b : !Default; + } + } +} diff --git a/src/Avalonia.Themes.Default/NativeMenuBar.xaml b/src/Avalonia.Themes.Default/NativeMenuBar.xaml new file mode 100644 index 0000000000..2832bab226 --- /dev/null +++ b/src/Avalonia.Themes.Default/NativeMenuBar.xaml @@ -0,0 +1,25 @@ + + + + + + diff --git a/src/Avalonia.Themes.Default/RepeatButton.xaml b/src/Avalonia.Themes.Default/RepeatButton.xaml index f555209471..702e4e6ebd 100644 --- a/src/Avalonia.Themes.Default/RepeatButton.xaml +++ b/src/Avalonia.Themes.Default/RepeatButton.xaml @@ -33,12 +33,8 @@ - - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ScrollBar.xaml b/src/Avalonia.Themes.Default/ScrollBar.xaml index c9552a607c..64a4399d16 100644 --- a/src/Avalonia.Themes.Default/ScrollBar.xaml +++ b/src/Avalonia.Themes.Default/ScrollBar.xaml @@ -1,128 +1,140 @@ - - - - - - - - + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/ScrollViewer.xaml b/src/Avalonia.Themes.Default/ScrollViewer.xaml index 3e130cad67..38f4eef964 100644 --- a/src/Avalonia.Themes.Default/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Default/ScrollViewer.xaml @@ -36,6 +36,7 @@ Visibility="{TemplateBinding VerticalScrollBarVisibility}" Grid.Column="1" Focusable="False"/> + diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs index e7be272f13..1f1590bdcd 100644 --- a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs @@ -65,14 +65,14 @@ namespace Avalonia.Animation.Animators } } - Logger.Warning( + Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Animations, control, $"Cannot find the appropriate transform: \"{Property.OwnerType}\" in {control}."); } else { - Logger.Error( + Logger.TryGet(LogEventLevel.Error)?.Log( LogArea.Animations, control, $"Cannot apply animation: Target property owner {Property.OwnerType} is not a Transform object."); diff --git a/src/Avalonia.Visuals/CornerRadius.cs b/src/Avalonia.Visuals/CornerRadius.cs index 09163a2dac..ecb9e75d82 100644 --- a/src/Avalonia.Visuals/CornerRadius.cs +++ b/src/Avalonia.Visuals/CornerRadius.cs @@ -8,7 +8,10 @@ using Avalonia.Utilities; namespace Avalonia { - public struct CornerRadius + /// + /// Represents the radii of a rectangle's corners. + /// + public readonly struct CornerRadius : IEquatable { static CornerRadius() { @@ -33,22 +36,59 @@ namespace Avalonia BottomLeft = bottomLeft; } + /// + /// Radius of the top left corner. + /// public double TopLeft { get; } + + /// + /// Radius of the top right corner. + /// public double TopRight { get; } + + /// + /// Radius of the bottom right corner. + /// public double BottomRight { get; } + + /// + /// Radius of the bottom left corner. + /// public double BottomLeft { get; } + + /// + /// Gets a value indicating whether all corner radii are set to 0. + /// public bool IsEmpty => TopLeft.Equals(0) && IsUniform; + + /// + /// Gets a value indicating whether all corner radii are equal. + /// public bool IsUniform => TopLeft.Equals(TopRight) && BottomLeft.Equals(BottomRight) && TopRight.Equals(BottomRight); - public override bool Equals(object obj) + /// + /// Returns a boolean indicating whether the corner radius is equal to the other given corner radius. + /// + /// The other corner radius to test equality against. + /// True if this corner radius is equal to other; False otherwise. + public bool Equals(CornerRadius other) { - if (obj is CornerRadius) - { - return this == (CornerRadius)obj; - } - return false; + // ReSharper disable CompareOfFloatsByEqualityOperator + return TopLeft == other.TopLeft && + + TopRight == other.TopRight && + BottomRight == other.BottomRight && + BottomLeft == other.BottomLeft; + // ReSharper restore CompareOfFloatsByEqualityOperator } + /// + /// Returns a boolean indicating whether the given Object is equal to this corner radius instance. + /// + /// The Object to compare against. + /// True if the Object is equal to this corner radius; False otherwise. + public override bool Equals(object obj) => obj is CornerRadius other && Equals(other); + public override int GetHashCode() { return TopLeft.GetHashCode() ^ TopRight.GetHashCode() ^ BottomLeft.GetHashCode() ^ BottomRight.GetHashCode(); @@ -61,7 +101,9 @@ namespace Avalonia public static CornerRadius Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Thickness")) + const string exceptionMessage = "Invalid CornerRadius."; + + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage)) { if (tokenizer.TryReadDouble(out var a)) { @@ -78,21 +120,18 @@ namespace Avalonia return new CornerRadius(a); } - throw new FormatException("Invalid CornerRadius."); + throw new FormatException(exceptionMessage); } } - public static bool operator ==(CornerRadius cr1, CornerRadius cr2) + public static bool operator ==(CornerRadius left, CornerRadius right) { - return cr1.TopLeft.Equals(cr2.TopLeft) - && cr1.TopRight.Equals(cr2.TopRight) - && cr1.BottomRight.Equals(cr2.BottomRight) - && cr1.BottomLeft.Equals(cr2.BottomLeft); + return left.Equals(right); } - public static bool operator !=(CornerRadius cr1, CornerRadius cr2) + public static bool operator !=(CornerRadius left, CornerRadius right) { - return !(cr1 == cr2); + return !(left == right); } } } diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index d083a2aaf8..92b7dae904 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -10,7 +10,7 @@ namespace Avalonia /// /// A 2x3 matrix. /// - public readonly struct Matrix + public readonly struct Matrix : IEquatable { private readonly double _m11; private readonly double _m12; @@ -235,12 +235,14 @@ namespace Avalonia /// True if this matrix is equal to other; False otherwise. public bool Equals(Matrix other) { + // ReSharper disable CompareOfFloatsByEqualityOperator return _m11 == other.M11 && _m12 == other.M12 && _m21 == other.M21 && _m22 == other.M22 && _m31 == other.M31 && _m32 == other.M32; + // ReSharper restore CompareOfFloatsByEqualityOperator } /// @@ -248,15 +250,7 @@ namespace Avalonia /// /// The Object to compare against. /// True if the Object is equal to this matrix; False otherwise. - public override bool Equals(object obj) - { - if (!(obj is Matrix)) - { - return false; - } - - return Equals((Matrix)obj); - } + public override bool Equals(object obj) => obj is Matrix other && Equals(other); /// /// Returns the hash code for this instance. @@ -316,7 +310,7 @@ namespace Avalonia /// The . public static Matrix Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix.")) { return new Matrix( tokenizer.ReadDouble(), diff --git a/src/Avalonia.Visuals/Media/Geometry.cs b/src/Avalonia.Visuals/Media/Geometry.cs index 748d2526af..f9bcea85af 100644 --- a/src/Avalonia.Visuals/Media/Geometry.cs +++ b/src/Avalonia.Visuals/Media/Geometry.cs @@ -22,7 +22,7 @@ namespace Avalonia.Media static Geometry() { - TransformProperty.Changed.AddClassHandler(x => x.TransformChanged); + TransformProperty.Changed.AddClassHandler((x,e) => x.TransformChanged(e)); } /// diff --git a/src/Avalonia.Visuals/Media/PixelPoint.cs b/src/Avalonia.Visuals/Media/PixelPoint.cs index d62c2a2e55..5384a40be9 100644 --- a/src/Avalonia.Visuals/Media/PixelPoint.cs +++ b/src/Avalonia.Visuals/Media/PixelPoint.cs @@ -10,7 +10,7 @@ namespace Avalonia /// /// Represents a point in device pixels. /// - public readonly struct PixelPoint + public readonly struct PixelPoint : IEquatable { /// /// A point representing 0,0. @@ -46,7 +46,7 @@ namespace Avalonia /// True if the points are equal; otherwise false. public static bool operator ==(PixelPoint left, PixelPoint right) { - return left.X == right.X && left.Y == right.Y; + return left.Equals(right); } /// @@ -120,7 +120,7 @@ namespace Avalonia /// The . public static PixelPoint Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelPoint")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelPoint.")) { return new PixelPoint( tokenizer.ReadInt32(), @@ -128,6 +128,18 @@ namespace Avalonia } } + /// + /// Returns a boolean indicating whether the point is equal to the other given point. + /// + /// The other point to test equality against. + /// True if this point is equal to other; False otherwise. + public bool Equals(PixelPoint other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return X == other.X && Y == other.Y; + // ReSharper restore CompareOfFloatsByEqualityOperator + } + /// /// Checks for equality between a point and an object. /// @@ -135,15 +147,7 @@ namespace Avalonia /// /// True if is a point that equals the current point. /// - public override bool Equals(object obj) - { - if (obj is PixelPoint other) - { - return this == other; - } - - return false; - } + public override bool Equals(object obj) => obj is PixelPoint other && Equals(other); /// /// Returns a hash code for a . diff --git a/src/Avalonia.Visuals/Media/PixelRect.cs b/src/Avalonia.Visuals/Media/PixelRect.cs index 0e2094da07..a024e1af11 100644 --- a/src/Avalonia.Visuals/Media/PixelRect.cs +++ b/src/Avalonia.Visuals/Media/PixelRect.cs @@ -10,7 +10,7 @@ namespace Avalonia /// /// Represents a rectangle in device pixels. /// - public readonly struct PixelRect + public readonly struct PixelRect : IEquatable { /// /// An empty rectangle. @@ -148,7 +148,7 @@ namespace Avalonia /// True if the rects are equal; otherwise false. public static bool operator ==(PixelRect left, PixelRect right) { - return left.Position == right.Position && left.Size == right.Size; + return left.Equals(right); } /// @@ -196,20 +196,22 @@ namespace Avalonia rect.Height); } + /// + /// Returns a boolean indicating whether the rect is equal to the other given rect. + /// + /// The other rect to test equality against. + /// True if this rect is equal to other; False otherwise. + public bool Equals(PixelRect other) + { + return Position == other.Position && Size == other.Size; + } + /// /// Returns a boolean indicating whether the given object is equal to this rectangle. /// /// The object to compare against. /// True if the object is equal to this rectangle; false otherwise. - public override bool Equals(object obj) - { - if (obj is PixelRect other) - { - return this == other; - } - - return false; - } + public override bool Equals(object obj) => obj is PixelRect other && Equals(other); /// /// Returns the hash code for this instance. @@ -432,7 +434,7 @@ namespace Avalonia /// The parsed . public static PixelRect Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelRect")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelRect.")) { return new PixelRect( tokenizer.ReadInt32(), diff --git a/src/Avalonia.Visuals/Media/PixelSize.cs b/src/Avalonia.Visuals/Media/PixelSize.cs index b903b804f9..a49aa82c0d 100644 --- a/src/Avalonia.Visuals/Media/PixelSize.cs +++ b/src/Avalonia.Visuals/Media/PixelSize.cs @@ -10,7 +10,7 @@ namespace Avalonia /// /// Represents a size in device pixels. /// - public readonly struct PixelSize + public readonly struct PixelSize : IEquatable { /// /// A size representing zero @@ -51,7 +51,7 @@ namespace Avalonia /// True if the sizes are equal; otherwise false. public static bool operator ==(PixelSize left, PixelSize right) { - return left.Width == right.Width && left.Height == right.Height; + return left.Equals(right); } /// @@ -72,7 +72,7 @@ namespace Avalonia /// The . public static PixelSize Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelSize")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelSize.")) { return new PixelSize( tokenizer.ReadInt32(), @@ -80,6 +80,16 @@ namespace Avalonia } } + /// + /// Returns a boolean indicating whether the size is equal to the other given size. + /// + /// The other size to test equality against. + /// True if this size is equal to other; False otherwise. + public bool Equals(PixelSize other) + { + return Width == other.Width && Height == other.Height; + } + /// /// Checks for equality between a size and an object. /// @@ -87,15 +97,7 @@ namespace Avalonia /// /// True if is a size that equals the current size. /// - public override bool Equals(object obj) - { - if (obj is PixelSize other) - { - return this == other; - } - - return false; - } + public override bool Equals(object obj) => obj is PixelSize other && Equals(other); /// /// Returns a hash code for a . diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index 0d3e354615..d92f8b0fc4 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Globalization; using Avalonia.Animation.Animators; using Avalonia.Utilities; @@ -10,7 +11,7 @@ namespace Avalonia /// /// Defines a point. /// - public readonly struct Point + public readonly struct Point : IEquatable { static Point() { @@ -75,7 +76,7 @@ namespace Avalonia /// True if the points are equal; otherwise false. public static bool operator ==(Point left, Point right) { - return left.X == right.X && left.Y == right.Y; + return left.Equals(right); } /// @@ -177,7 +178,7 @@ namespace Avalonia /// The . public static Point Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point.")) { return new Point( tokenizer.ReadDouble(), @@ -186,6 +187,19 @@ namespace Avalonia } } + /// + /// Returns a boolean indicating whether the point is equal to the other given point. + /// + /// The other point to test equality against. + /// True if this point is equal to other; False otherwise. + public bool Equals(Point other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _x == other._x && + _y == other._y; + // ReSharper enable CompareOfFloatsByEqualityOperator + } + /// /// Checks for equality between a point and an object. /// @@ -193,16 +207,7 @@ namespace Avalonia /// /// True if is a point that equals the current point. /// - public override bool Equals(object obj) - { - if (obj is Point) - { - var other = (Point)obj; - return X == other.X && Y == other.Y; - } - - return false; - } + public override bool Equals(object obj) => obj is Point other && Equals(other); /// /// Returns a hash code for a . diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 8f08f7f51f..4dfd641525 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -11,7 +11,7 @@ namespace Avalonia /// /// Defines a rectangle. /// - public readonly struct Rect + public readonly struct Rect : IEquatable { static Rect() { @@ -164,7 +164,9 @@ namespace Avalonia /// /// Gets a value that indicates whether the rectangle is empty. /// + // ReSharper disable CompareOfFloatsByEqualityOperator public bool IsEmpty => _width == 0 && _height == 0; + // ReSharper restore CompareOfFloatsByEqualityOperator /// /// Checks for equality between two s. @@ -174,7 +176,7 @@ namespace Avalonia /// True if the rects are equal; otherwise false. public static bool operator ==(Rect left, Rect right) { - return left.Position == right.Position && left.Size == right.Size; + return left.Equals(right); } /// @@ -297,21 +299,27 @@ namespace Avalonia Size.Deflate(thickness)); } + /// + /// Returns a boolean indicating whether the rect is equal to the other given rect. + /// + /// The other rect to test equality against. + /// True if this rect is equal to other; False otherwise. + public bool Equals(Rect other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _x == other._x && + _y == other._y && + _width == other._width && + _height == other._height; + // ReSharper enable CompareOfFloatsByEqualityOperator + } + /// /// Returns a boolean indicating whether the given object is equal to this rectangle. /// /// The object to compare against. /// True if the object is equal to this rectangle; false otherwise. - public override bool Equals(object obj) - { - if (obj is Rect) - { - var other = (Rect)obj; - return Position == other.Position && Size == other.Size; - } - - return false; - } + public override bool Equals(object obj) => obj is Rect other && Equals(other); /// /// Returns the hash code for this instance. @@ -422,10 +430,10 @@ namespace Avalonia } else { - var x1 = Math.Min(this.X, rect.X); - var x2 = Math.Max(this.Right, rect.Right); - var y1 = Math.Min(this.Y, rect.Y); - var y2 = Math.Max(this.Bottom, rect.Bottom); + var x1 = Math.Min(X, rect.X); + var x2 = Math.Max(Right, rect.Right); + var y1 = Math.Min(Y, rect.Y); + var y2 = Math.Max(Bottom, rect.Bottom); return new Rect(new Point(x1, y1), new Point(x2, y2)); } @@ -493,7 +501,7 @@ namespace Avalonia /// The parsed . public static Rect Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect.")) { return new Rect( tokenizer.ReadDouble(), diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs index d38bb1d496..2e8fb16bc1 100644 --- a/src/Avalonia.Visuals/RelativePoint.cs +++ b/src/Avalonia.Visuals/RelativePoint.cs @@ -107,10 +107,7 @@ namespace Avalonia /// /// The other object. /// True if the objects are equal, otherwise false. - public override bool Equals(object obj) - { - return (obj is RelativePoint) && Equals((RelativePoint)obj); - } + public override bool Equals(object obj) => obj is RelativePoint other && Equals(other); /// /// Checks if the equals another point. @@ -130,10 +127,7 @@ namespace Avalonia { unchecked { - int hash = 17; - hash = (hash * 23) + Unit.GetHashCode(); - hash = (hash * 23) + Point.GetHashCode(); - return hash; + return (_point.GetHashCode() * 397) ^ (int)_unit; } } @@ -156,7 +150,7 @@ namespace Avalonia /// The parsed . public static RelativePoint Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint.")) { var x = tokenizer.ReadString(); var y = tokenizer.ReadString(); diff --git a/src/Avalonia.Visuals/RelativeRect.cs b/src/Avalonia.Visuals/RelativeRect.cs index 927ec3ef75..d2e4b2dc26 100644 --- a/src/Avalonia.Visuals/RelativeRect.cs +++ b/src/Avalonia.Visuals/RelativeRect.cs @@ -116,10 +116,7 @@ namespace Avalonia /// /// The other object. /// True if the objects are equal, otherwise false. - public override bool Equals(object obj) - { - return (obj is RelativeRect) && Equals((RelativeRect)obj); - } + public override bool Equals(object obj) => obj is RelativeRect other && Equals(other); /// /// Checks if the equals another rectangle. @@ -139,10 +136,7 @@ namespace Avalonia { unchecked { - int hash = 17; - hash = (hash * 23) + Unit.GetHashCode(); - hash = (hash * 23) + Rect.GetHashCode(); - return hash; + return ((int)Unit * 397) ^ Rect.GetHashCode(); } } @@ -161,7 +155,7 @@ namespace Avalonia Rect.Width * size.Width, Rect.Height * size.Height); } - + /// /// Parses a string. /// @@ -169,7 +163,7 @@ namespace Avalonia /// The parsed . public static RelativeRect Parse(string s) { - using (var tokenizer = new StringTokenizer(s, exceptionMessage: "Invalid RelativeRect")) + using (var tokenizer = new StringTokenizer(s, exceptionMessage: "Invalid RelativeRect.")) { var x = tokenizer.ReadString(); var y = tokenizer.ReadString(); diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index efcc555159..d9a68b236a 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Avalonia.Logging; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -269,7 +270,7 @@ namespace Avalonia.Rendering } catch (RenderTargetCorruptedException ex) { - Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); + Logger.TryGet(LogEventLevel.Information)?.Log("Renderer", this, "Render target was corrupted. Exception: {0}", ex); RenderTarget?.Dispose(); RenderTarget = null; } diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index b2d242d4af..68d56eeedd 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Logging; using Avalonia.Media; using Avalonia.Platform; using Avalonia.VisualTree; @@ -80,7 +81,7 @@ namespace Avalonia.Rendering } catch (RenderTargetCorruptedException ex) { - Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); + Logger.TryGet(LogEventLevel.Information)?.Log("Renderer", this, "Render target was corrupted. Exception: {0}", ex); _renderTarget.Dispose(); _renderTarget = null; } diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index 140688f8bc..c2594658b9 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -120,7 +120,7 @@ namespace Avalonia.Rendering } catch (Exception ex) { - Logger.Error(LogArea.Visual, this, "Exception in render update: {Error}", ex); + Logger.TryGet(LogEventLevel.Error)?.Log(LogArea.Visual, this, "Exception in render update: {Error}", ex); } } } @@ -136,7 +136,7 @@ namespace Avalonia.Rendering } catch (Exception ex) { - Logger.Error(LogArea.Visual, this, "Exception in render loop: {Error}", ex); + Logger.TryGet(LogEventLevel.Error)?.Log(LogArea.Visual, this, "Exception in render loop: {Error}", ex); } finally { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 161cbc099e..558e96e132 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -148,6 +148,19 @@ namespace Avalonia.Rendering.SceneGraph return (VisualNode)node; } + private static object GetOrCreateChildNode(Scene scene, IVisual child, VisualNode parent) + { + var result = (VisualNode)scene.FindNode(child); + + if (result != null && result.Parent != parent) + { + Deindex(scene, result); + result = null; + } + + return result ?? CreateNode(scene, child, parent); + } + private static void Update(DrawingContext context, Scene scene, VisualNode node, Rect clip, bool forceRecurse) { var visual = node.Visual; @@ -231,7 +244,7 @@ namespace Avalonia.Rendering.SceneGraph { foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)) { - var childNode = scene.FindNode(child) ?? CreateNode(scene, child, node); + var childNode = GetOrCreateChildNode(scene, child, node); Update(context, scene, (VisualNode)childNode, clip, forceRecurse); } @@ -240,6 +253,10 @@ namespace Avalonia.Rendering.SceneGraph } } } + else + { + contextImpl.BeginUpdate(node).Dispose(); + } } private void UpdateSize(Scene scene) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index f579bf0a62..d2a9e0a673 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -119,6 +119,11 @@ namespace Avalonia.Rendering.SceneGraph throw new ObjectDisposedException("Visual node for {node.Visual}"); } + if (child.Parent != this) + { + throw new AvaloniaInternalException("VisualNode added to wrong parent."); + } + EnsureChildrenCreated(); _children.Add(child); } @@ -155,6 +160,11 @@ namespace Avalonia.Rendering.SceneGraph throw new ObjectDisposedException("Visual node for {node.Visual}"); } + if (node.Parent != this) + { + throw new AvaloniaInternalException("VisualNode added to wrong parent."); + } + EnsureChildrenCreated(); _children[index] = node; } @@ -218,7 +228,7 @@ namespace Avalonia.Rendering.SceneGraph if (first < _children?.Count) { EnsureChildrenCreated(); - for (int i = first; i < _children.Count - first; i++) + for (int i = first; i < _children.Count; i++) { _children[i].Dispose(); } diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index 782c5ea67b..aba2ed8d62 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.cs @@ -11,7 +11,7 @@ namespace Avalonia /// /// Defines a size. /// - public readonly struct Size + public readonly struct Size : IEquatable { static Size() { @@ -72,7 +72,7 @@ namespace Avalonia /// True if the sizes are equal; otherwise false. public static bool operator ==(Size left, Size right) { - return left._width == right._width && left._height == right._height; + return left.Equals(right); } /// @@ -158,7 +158,7 @@ namespace Avalonia /// The . public static Size Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size")) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size.")) { return new Size( tokenizer.ReadDouble(), @@ -191,6 +191,19 @@ namespace Avalonia Math.Max(0, _height - thickness.Top - thickness.Bottom)); } + /// + /// Returns a boolean indicating whether the size is equal to the other given size. + /// + /// The other size to test equality against. + /// True if this size is equal to other; False otherwise. + public bool Equals(Size other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _width == other._width && + _height == other._height; + // ReSharper enable CompareOfFloatsByEqualityOperator + } + /// /// Checks for equality between a size and an object. /// @@ -198,16 +211,7 @@ namespace Avalonia /// /// True if is a size that equals the current size. /// - public override bool Equals(object obj) - { - if (obj is Size) - { - var other = (Size)obj; - return Width == other.Width && Height == other.Height; - } - - return false; - } + public override bool Equals(object obj) => obj is Size other && Equals(other); /// /// Returns a hash code for a . diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index 830ee4666e..44ff66069f 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.cs @@ -3,7 +3,6 @@ using System; using System.Globalization; -using Avalonia.Animation; using Avalonia.Animation.Animators; using Avalonia.Utilities; @@ -12,7 +11,7 @@ namespace Avalonia /// /// Describes the thickness of a frame around a rectangle. /// - public readonly struct Thickness + public readonly struct Thickness : IEquatable { static Thickness() { @@ -204,7 +203,9 @@ namespace Avalonia /// The . public static Thickness Parse(string s) { - using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Thickness")) + const string exceptionMessage = "Invalid Thickness."; + + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage)) { if (tokenizer.TryReadDouble(out var a)) { @@ -221,10 +222,25 @@ namespace Avalonia return new Thickness(a); } - throw new FormatException("Invalid Thickness."); + throw new FormatException(exceptionMessage); } } + /// + /// Returns a boolean indicating whether the thickness is equal to the other given point. + /// + /// The other thickness to test equality against. + /// True if this thickness is equal to other; False otherwise. + public bool Equals(Thickness other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _left == other._left && + _top == other._top && + _right == other._right && + _bottom == other._bottom; + // ReSharper restore CompareOfFloatsByEqualityOperator + } + /// /// Checks for equality between a thickness and an object. /// @@ -232,19 +248,7 @@ namespace Avalonia /// /// True if is a size that equals the current size. /// - public override bool Equals(object obj) - { - if (obj is Thickness) - { - Thickness other = (Thickness)obj; - return Left == other.Left && - Top == other.Top && - Right == other.Right && - Bottom == other.Bottom; - } - - return false; - } + public override bool Equals(object obj) => obj is Thickness other && Equals(other); /// /// Returns a hash code for a . diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 11bda8b00e..576d2daaaa 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -11,7 +11,7 @@ namespace Avalonia /// /// Defines a vector. /// - public readonly struct Vector + public readonly struct Vector : IEquatable { static Vector() { @@ -138,7 +138,6 @@ namespace Avalonia /// /// The other vector. /// True if vectors are nearly equal. - [Pure] public bool NearlyEquals(Vector other) { const float tolerance = float.Epsilon; @@ -146,13 +145,7 @@ namespace Avalonia return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance; } - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - return false; - - return obj is Vector vector && Equals(vector); - } + public override bool Equals(object obj) => obj is Vector other && Equals(other); public override int GetHashCode() { diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 1f2d67b69e..f4306d3929 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -359,7 +359,7 @@ namespace Avalonia /// The event args. protected virtual void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { - Logger.Verbose(LogArea.Visual, this, "Attached to visual tree"); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Visual, this, "Attached to visual tree"); _visualRoot = e.Root; @@ -388,7 +388,7 @@ namespace Avalonia /// The event args. protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { - Logger.Verbose(LogArea.Visual, this, "Detached from visual tree"); + Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Visual, this, "Detached from visual tree"); _visualRoot = null; @@ -453,8 +453,7 @@ namespace Avalonia return; } - Logger.Log( - LogEventLevel.Warning, + Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Binding, this, "Error in binding to {Target}.{Property}: {Message}", diff --git a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs index 39b328adc2..b2121aa8da 100644 --- a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs @@ -1,13 +1,14 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; namespace Avalonia.VisualTree { /// /// Holds information about the bounds of a control, together with a transform and a clip. /// - public readonly struct TransformedBounds + public readonly struct TransformedBounds : IEquatable { /// /// Initializes a new instance of the struct. @@ -56,15 +57,7 @@ namespace Avalonia.VisualTree return Bounds == other.Bounds && Clip == other.Clip && Transform == other.Transform; } - public override bool Equals(object obj) - { - if (obj is null) - { - return false; - } - - return obj is TransformedBounds other && Equals(other); - } + public override bool Equals(object obj) => obj is TransformedBounds other && Equals(other); public override int GetHashCode() { diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index d15b5fe4b8..3dc2e8e41f 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -36,7 +36,7 @@ namespace Avalonia.X11.Glx } catch(Exception e) { - Logger.Error("OpenGL", null, "Unable to initialize GLX-based rendering: {0}", e); + Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", null, "Unable to initialize GLX-based rendering: {0}", e); return null; } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 1d2290236c..d7a7bb97fd 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -38,7 +38,9 @@ namespace Avalonia.X11 throw new Exception("XOpenDisplay failed"); XError.Init(); Info = new X11Info(Display, DeferredDisplay); - + //TODO: log + if (options.UseDBusMenu) + DBusHelper.TryInitialize(); AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(this) .Bind().ToConstant(new X11PlatformThreading(this)) @@ -95,6 +97,7 @@ namespace Avalonia public bool UseEGL { get; set; } public bool UseGpu { get; set; } = true; public bool OverlayPopups { get; set; } + public bool UseDBusMenu { get; set; } public List GlxRendererBlacklist { get; set; } = new List { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 5b9eebb9cb..17471fad10 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -6,7 +6,9 @@ using System.Linq; using System.Reactive.Disposables; using System.Text; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; @@ -19,7 +21,7 @@ using static Avalonia.X11.XLib; // ReSharper disable StringLiteralTypo namespace Avalonia.X11 { - unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client + unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client, ITopLevelImplWithNativeMenuExporter { private readonly AvaloniaX11Platform _platform; private readonly IWindowImpl _popupParent; @@ -170,6 +172,8 @@ namespace Avalonia.X11 XFlush(_x11.Display); if(_popup) PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); + if (platform.Options.UseDBusMenu) + NativeMenuExporter = DBusMenuExporter.TryCreate(_handle); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -451,7 +455,7 @@ namespace Avalonia.X11 key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32(); - ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), + ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev); @@ -466,7 +470,7 @@ namespace Avalonia.X11 if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL return; } - ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), text), + ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text), ref ev); } } @@ -874,21 +878,23 @@ namespace Avalonia.X11 } - void BeginMoveResize(NetWmMoveResize side) + void BeginMoveResize(NetWmMoveResize side, PointerPressedEventArgs e) { var pos = GetCursorPos(_x11); XUngrabPointer(_x11.Display, new IntPtr(0)); SendNetWMMessage (_x11.Atoms._NET_WM_MOVERESIZE, (IntPtr) pos.x, (IntPtr) pos.y, (IntPtr) side, (IntPtr) 1, (IntPtr)1); // left button + + e.Pointer.Capture(null); } - public void BeginMoveDrag() + public void BeginMoveDrag(PointerPressedEventArgs e) { - BeginMoveResize(NetWmMoveResize._NET_WM_MOVERESIZE_MOVE); + BeginMoveResize(NetWmMoveResize._NET_WM_MOVERESIZE_MOVE, e); } - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { var side = NetWmMoveResize._NET_WM_MOVERESIZE_CANCEL; if (edge == WindowEdge.East) @@ -907,7 +913,7 @@ namespace Avalonia.X11 side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT; if (edge == WindowEdge.SouthWest) side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT; - BeginMoveResize(side); + BeginMoveResize(side, e); } public void SetTitle(string title) @@ -975,5 +981,6 @@ namespace Avalonia.X11 } public IPopupPositioner PopupPositioner { get; } + public ITopLevelNativeMenuExporter NativeMenuExporter { get; } } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 8fc555aac2..db37e4af0b 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -118,8 +118,7 @@ public static class LinuxFramebufferPlatformExtensions where T : AppBuilderBase, new() { var lifetime = LinuxFramebufferPlatform.Initialize(builder, backend); - builder.Instance.ApplicationLifetime = lifetime; - builder.SetupWithoutStarting(); + builder.SetupWithLifetime(lifetime); lifetime.Start(args); builder.Instance.Run(lifetime.Token); return lifetime.ExitCode; diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index b42bd53619..6000b71f9d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -42,7 +42,7 @@ namespace Avalonia.Markup.Xaml.Converters !property.IsAttached && !registry.IsRegistered(targetType, property)) { - Logger.Warning( + Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Property, this, "Property '{Owner}.{Name}' is not registered on '{Type}'.", diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs index b91d679fba..5a5da518d0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs @@ -114,10 +114,10 @@ namespace Avalonia.Markup.Xaml.XamlIl InitializeSre(); var asm = localAssembly == null ? null : _sreTypeSystem.GetAssembly(localAssembly); - + var compiler = new AvaloniaXamlIlCompiler(new XamlIlTransformerConfiguration(_sreTypeSystem, asm, - _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter), - _sreContextType); + _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter), + _sreContextType) { EnableIlVerification = true }; var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); IXamlIlType overrideType = null; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index c7155c5f6c..ad9915e193 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit c7155c5f6c1a5153ee2d8cd78e5d1524dd6744cf +Subproject commit ad9915e19398a49c5a11b66000c361659ca692b3 diff --git a/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs b/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs index e03427c161..f7d228609d 100644 --- a/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs +++ b/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs @@ -150,7 +150,7 @@ namespace Avalonia.Markup.Data } catch (Exception e) { - Logger.Error( + Logger.TryGet(LogEventLevel.Error)?.Log( LogArea.Property, control, "Error setting {Property} on {Target}: {Exception}", diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs index 10b89d79b8..fd2f14fc58 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs @@ -85,7 +85,7 @@ namespace Avalonia.Direct2D1.Media } catch (Exception ex) { - Logger.Error( + Logger.TryGet(LogEventLevel.Error)?.Log( LogArea.Visual, this, "GeometrySink.Close exception: {Exception}", diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 71c398481b..2887468046 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -212,17 +212,17 @@ namespace Avalonia.Win32.Interop.Wpf protected override void OnMouseLeave(MouseEventArgs e) => MouseEvent(RawPointerEventType.LeaveWindow, e); protected override void OnKeyDown(KeyEventArgs e) - => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint) e.Timestamp, RawKeyEventType.KeyDown, + => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint) e.Timestamp, _inputRoot, RawKeyEventType.KeyDown, (Key) e.Key, GetModifiers(null))); protected override void OnKeyUp(KeyEventArgs e) - => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint)e.Timestamp, RawKeyEventType.KeyUp, + => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint)e.Timestamp, _inputRoot, RawKeyEventType.KeyUp, (Key)e.Key, GetModifiers(null))); protected override void OnTextInput(TextCompositionEventArgs e) - => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, e.Text)); + => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, _inputRoot, e.Text)); void ITopLevelImpl.SetCursor(IPlatformHandle cursor) { diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index d37bec3334..2c6425e26c 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1031,6 +1031,9 @@ namespace Avalonia.Win32.Interop [DllImport("shcore.dll")] public static extern long GetDpiForMonitor(IntPtr hmonitor, MONITOR_DPI_TYPE dpiType, out uint dpiX, out uint dpiY); + [DllImport("gdi32.dll")] + public static extern int GetDeviceCaps(IntPtr hdc, DEVICECAP nIndex); + [DllImport("shcore.dll")] public static extern void GetScaleFactorForMonitor(IntPtr hMon, out uint pScale); @@ -1147,6 +1150,12 @@ namespace Avalonia.Win32.Interop } } + public enum DEVICECAP + { + HORZRES = 8, + DESKTOPHORZRES = 118 + } + public enum PROCESS_DPI_AWARENESS { PROCESS_DPI_UNAWARE = 0, diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index 8ac0f25598..b17e0d6c09 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -8,13 +8,13 @@ namespace Avalonia.Win32 { class OleDropTarget : IDropTarget { - private readonly IInputElement _target; + private readonly IInputRoot _target; private readonly ITopLevelImpl _tl; private readonly IDragDropDevice _dragDevice; private IDataObject _currentDrag = null; - public OleDropTarget(ITopLevelImpl tl, IInputElement target) + public OleDropTarget(ITopLevelImpl tl, IInputRoot target) { _dragDevice = AvaloniaLocator.Current.GetService(); _tl = tl; diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs index e77aa07bcd..df4cf7fa19 100644 --- a/src/Windows/Avalonia.Win32/ScreenImpl.cs +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Avalonia.Platform; +using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 @@ -28,9 +29,28 @@ namespace Avalonia.Win32 (IntPtr monitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr data) => { MONITORINFO monitorInfo = MONITORINFO.Create(); - if (GetMonitorInfo(monitor,ref monitorInfo)) + if (GetMonitorInfo(monitor, ref monitorInfo)) { - GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _); + var dpi = 1.0; + + var shcore = LoadLibrary("shcore.dll"); + var method = GetProcAddress(shcore, nameof(GetDpiForMonitor)); + if (method != IntPtr.Zero) + { + GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _); + dpi = (double)x; + } + else + { + var hdc = GetDC(IntPtr.Zero); + + double virtW = GetDeviceCaps(hdc, DEVICECAP.HORZRES); + double physW = GetDeviceCaps(hdc, DEVICECAP.DESKTOPHORZRES); + + dpi = (96d * physW / virtW); + + ReleaseDC(IntPtr.Zero, hdc); + } RECT bounds = monitorInfo.rcMonitor; RECT workingArea = monitorInfo.rcWork; @@ -40,7 +60,7 @@ namespace Avalonia.Win32 new PixelRect(workingArea.left, workingArea.top, workingArea.right - workingArea.left, workingArea.bottom - workingArea.top); screens[index] = - new WinScreen((double)x / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, + new WinScreen(dpi / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, monitor); index++; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 6a733356b8..cafb361239 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -331,11 +331,12 @@ namespace Avalonia.Win32 ShowWindow(_showWindowState); } - public void BeginMoveDrag() + public void BeginMoveDrag(PointerPressedEventArgs e) { WindowsMouseDevice.Instance.Capture(null); UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN, new IntPtr((int)UnmanagedMethods.HitTestValues.HTCAPTION), IntPtr.Zero); + e.Pointer.Capture(null); } static readonly Dictionary EdgeDic = new Dictionary @@ -350,7 +351,7 @@ namespace Avalonia.Win32 {WindowEdge.West, UnmanagedMethods.HitTestValues.HTLEFT} }; - public void BeginResizeDrag(WindowEdge edge) + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { #if USE_MANAGED_DRAG _managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position)); @@ -507,6 +508,7 @@ namespace Avalonia.Win32 e = new RawKeyEventArgs( WindowsKeyboardDevice.Instance, timestamp, + _owner, RawKeyEventType.KeyDown, KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers); break; @@ -520,6 +522,7 @@ namespace Avalonia.Win32 e = new RawKeyEventArgs( WindowsKeyboardDevice.Instance, timestamp, + _owner, RawKeyEventType.KeyUp, KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers); break; @@ -527,7 +530,7 @@ namespace Avalonia.Win32 // Ignore control chars if (ToInt32(wParam) >= 32) { - e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, + e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner, new string((char)ToInt32(wParam), 1)); } diff --git a/src/iOS/Avalonia.iOS/AppBuilder.cs b/src/iOS/Avalonia.iOS/AppBuilder.cs index a68dd6387a..cb8e0a7954 100644 --- a/src/iOS/Avalonia.iOS/AppBuilder.cs +++ b/src/iOS/Avalonia.iOS/AppBuilder.cs @@ -6,7 +6,7 @@ namespace Avalonia public class AppBuilder : AppBuilderBase { public AppBuilder() : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType().Assembly)) + builder => StandardRuntimePlatformServices.Register(builder.ApplicationType.Assembly)) { } diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index ff11bc513d..989bd744a6 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -18,7 +18,9 @@ namespace Avalonia.Controls.UnitTests.Platform default); static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source, - new FakePointer(), (IVisual)source, default,0, new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonReleased), default); + new FakePointer(), (IVisual)source, default,0, + new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonReleased), + default, MouseButton.Left); public class TopLevel { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index d5237e2aca..17f0e609a5 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1000,6 +1000,26 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "Bar" }, selectedItems); } + [Fact] + public void MoveSelection_Wrap_Does_Not_Hang_With_No_Focusable_Controls() + { + // Issue #3094. + var target = new TestSelector + { + Template = Template(), + Items = new[] + { + new ListBoxItem { Focusable = false }, + new ListBoxItem { Focusable = false }, + }, + SelectedIndex = 0, + }; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + target.MoveSelection(NavigationDirection.Next, true); + } + private FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => @@ -1044,5 +1064,13 @@ namespace Avalonia.Controls.UnitTests.Primitives public List Items { get; set; } = new List() { "a", "b", "c", "d", "e" }; public string Selected { get; set; } = "b"; } + + private class TestSelector : SelectingItemsControl + { + public new bool MoveSelection(NavigationDirection direction, bool wrap) + { + return base.MoveSelection(direction, wrap); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 645f87163a..72c81659c3 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -182,6 +182,7 @@ namespace Avalonia.Controls.UnitTests var input = new RawKeyEventArgs( new Mock().Object, 0, + target, RawKeyEventType.KeyDown, Key.A, RawInputModifiers.None); impl.Object.Input(input); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 6c9f0f4f57..a91b7a0701 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -14,6 +14,7 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.Styling; using Avalonia.UnitTests; using Xunit; @@ -892,6 +893,46 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, GetItem(target, 0, 1, 0).Level); } + [Fact] + public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() + { + /// Issue #2980. + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = new DerivedTreeView + { + Template = CreateTreeViewTemplate(), + SelectionMode = SelectionMode.Multiple, + Items = new List + { + new Node { Value = "Root1", }, + new Node { Value = "Root2", }, + }, + }; + + var visualRoot = new TestRoot + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(TreeViewItem.IsExpandedProperty, true), + }, + }, + }, + Child = target, + }; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + _mouse.Click(GetItem(target, 0)); + _mouse.Click(GetItem(target, 1), modifiers: InputModifiers.Shift); + } + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); diff --git a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs new file mode 100644 index 0000000000..3c8e800fca --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs @@ -0,0 +1,90 @@ +using Avalonia.Input.Raw; +using Avalonia.Interactivity; +using Moq; +using Xunit; + +namespace Avalonia.Input.UnitTests +{ + public class KeyboardDeviceTests + { + [Fact] + public void Keypresses_Should_Be_Sent_To_Root_If_No_Focused_Element() + { + var target = new KeyboardDevice(); + var root = new Mock(); + + target.ProcessRawEvent( + new RawKeyEventArgs( + target, + 0, + root.Object, + RawKeyEventType.KeyDown, + Key.A, + RawInputModifiers.None)); + + root.Verify(x => x.RaiseEvent(It.IsAny())); + } + + [Fact] + public void Keypresses_Should_Be_Sent_To_Focused_Element() + { + var target = new KeyboardDevice(); + var focused = new Mock(); + var root = Mock.Of(); + + target.SetFocusedElement( + focused.Object, + NavigationMethod.Unspecified, + InputModifiers.None); + + target.ProcessRawEvent( + new RawKeyEventArgs( + target, + 0, + root, + RawKeyEventType.KeyDown, + Key.A, + RawInputModifiers.None)); + + focused.Verify(x => x.RaiseEvent(It.IsAny())); + } + + [Fact] + public void TextInput_Should_Be_Sent_To_Root_If_No_Focused_Element() + { + var target = new KeyboardDevice(); + var root = new Mock(); + + target.ProcessRawEvent( + new RawTextInputEventArgs( + target, + 0, + root.Object, + "Foo")); + + root.Verify(x => x.RaiseEvent(It.IsAny())); + } + + [Fact] + public void TextInput_Should_Be_Sent_To_Focused_Element() + { + var target = new KeyboardDevice(); + var focused = new Mock(); + var root = Mock.Of(); + + target.SetFocusedElement( + focused.Object, + NavigationMethod.Unspecified, + InputModifiers.None); + + target.ProcessRawEvent( + new RawTextInputEventArgs( + target, + 0, + root, + "Foo")); + + focused.Verify(x => x.RaiseEvent(It.IsAny())); + } + } +} diff --git a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs index 58ee63cea4..414e67bb94 100644 --- a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs @@ -331,7 +331,7 @@ namespace Avalonia.Interactivity.UnitTests var target = CreateTree(ev, null, 0); - ev.AddClassHandler(x => x.ClassHandler, RoutingStrategies.Bubble); + ev.AddClassHandler((x, e) => x.ClassHandler(e), RoutingStrategies.Bubble); var args = new RoutedEventArgs(ev, target); target.RaiseEvent(args); diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index d6e64936c7..48c4d73471 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -86,8 +86,7 @@ namespace Avalonia.UnitTests { _pointer.Capture(null); target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, - Timestamp(), props, - GetModifiers(modifiers))); + Timestamp(), props, GetModifiers(modifiers), _pressedButton)); } else Move(target, source, position); diff --git a/tests/Avalonia.UnitTests/TestLogSink.cs b/tests/Avalonia.UnitTests/TestLogSink.cs index 8e4dd7164f..a2b188c273 100644 --- a/tests/Avalonia.UnitTests/TestLogSink.cs +++ b/tests/Avalonia.UnitTests/TestLogSink.cs @@ -16,7 +16,7 @@ namespace Avalonia.UnitTests public class TestLogSink : ILogSink { - private LogCallback _callback; + private readonly LogCallback _callback; public TestLogSink(LogCallback callback) { @@ -30,7 +30,35 @@ namespace Avalonia.UnitTests return Disposable.Create(() => Logger.Sink = null); } - public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues) + public bool IsEnabled(LogEventLevel level) + { + return true; + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate) + { + _callback(level, area, source, messageTemplate); + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0) + { + _callback(level, area, source, messageTemplate, propertyValue0); + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, + T0 propertyValue0, T1 propertyValue1) + { + _callback(level, area, source, messageTemplate, propertyValue0, propertyValue1); + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, + T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + _callback(level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, + params object[] propertyValues) { _callback(level, area, source, messageTemplate, propertyValues); } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs index 4d23c57eed..f57c73c45c 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs @@ -17,7 +17,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void Should_Add_VisualNode() { var parent = new VisualNode(new TestRoot(), null); - var child = new VisualNode(Mock.Of(), null); + var child = new VisualNode(Mock.Of(), parent); var layers = new SceneLayers(parent.Visual); var target = new DeferredDrawingContextImpl(null, layers); @@ -32,7 +32,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void Should_Not_Replace_Identical_VisualNode() { var parent = new VisualNode(new TestRoot(), null); - var child = new VisualNode(Mock.Of(), null); + var child = new VisualNode(Mock.Of(), parent); var layers = new SceneLayers(parent.Visual); parent.AddChild(child); @@ -50,8 +50,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void Should_Replace_Different_VisualNode() { var parent = new VisualNode(new TestRoot(), null); - var child1 = new VisualNode(Mock.Of(), null); - var child2 = new VisualNode(Mock.Of(), null); + var child1 = new VisualNode(Mock.Of(), parent); + var child2 = new VisualNode(Mock.Of(), parent); var layers = new SceneLayers(parent.Visual); parent.AddChild(child1); @@ -78,8 +78,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var layers = new SceneLayers(root); var target = new DeferredDrawingContextImpl(null, layers); - var child1 = new VisualNode(Mock.Of(), null) { LayerRoot = root }; - var child2 = new VisualNode(Mock.Of(), null) { LayerRoot = root }; + var child1 = new VisualNode(Mock.Of(), node) { LayerRoot = root }; + var child2 = new VisualNode(Mock.Of(), node) { LayerRoot = root }; target.BeginUpdate(node); using (target.BeginUpdate(child1)) { } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index 13bcd27240..327ef98c4d 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -577,6 +577,58 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } + [Fact] + public void Should_Not_Dispose_Active_VisualNode_When_Control_Reparented_And_Child_Made_Invisible() + { + // Issue #3115 + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + StackPanel panel; + Border border1; + Border border2; + var tree = new TestRoot + { + Width = 100, + Height = 100, + Child = panel = new StackPanel + { + Children = + { + (border1 = new Border + { + Background = Brushes.Red, + }), + (border2 = new Border + { + Background = Brushes.Green, + }), + } + } + }; + + tree.Measure(Size.Infinity); + tree.Arrange(new Rect(tree.DesiredSize)); + + var scene = new Scene(tree); + var sceneBuilder = new SceneBuilder(); + sceneBuilder.UpdateAll(scene); + + var decorator = new Decorator(); + tree.Child = null; + decorator.Child = panel; + tree.Child = decorator; + border1.IsVisible = false; + + scene = scene.CloneScene(); + sceneBuilder.Update(scene, decorator); + + var panelNode = (VisualNode)scene.FindNode(panel); + Assert.Equal(2, panelNode.Children.Count); + Assert.False(panelNode.Children[0].Disposed); + Assert.False(panelNode.Children[1].Disposed); + } + } + [Fact] public void Should_Update_ClipBounds_For_Negative_Margin() { diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs index 24ba2d1c48..4ec3630053 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs @@ -24,7 +24,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var node = new VisualNode(Mock.Of(), null); var collection = node.Children; - node.AddChild(Mock.Of()); + node.AddChild(Mock.Of(x => x.Parent == node)); Assert.NotSame(collection, node.Children); } @@ -101,5 +101,24 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph node.SortChildren(scene); } + + [Fact] + public void TrimChildren_Should_Work_Correctly() + { + var parent = new VisualNode(Mock.Of(), null); + var child1 = new VisualNode(Mock.Of(), parent); + var child2 = new VisualNode(Mock.Of(), parent); + var child3 = new VisualNode(Mock.Of(), parent); + + parent.AddChild(child1); + parent.AddChild(child2); + parent.AddChild(child3); + parent.TrimChildren(2); + + Assert.Equal(2, parent.Children.Count); + Assert.False(child1.Disposed); + Assert.False(child2.Disposed); + Assert.True(child3.Disposed); + } } }