diff --git a/Avalonia.sln b/Avalonia.sln index 5bff2fa0a0..3a2c619d5b 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -201,7 +201,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Ava EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeEmbedSample", "samples\interop\NativeEmbedSample\NativeEmbedSample.csproj", "{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" EndProject @@ -1896,6 +1898,30 @@ Global {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.Build.0 = Release|Any CPU {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhone.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhone.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|Any CPU.Build.0 = Release|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhone.ActiveCfg = Release|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhone.Build.0 = Release|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU @@ -1977,6 +2003,7 @@ Global {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index f9bfaf0b47..1cf3bc75b0 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -26,7 +26,8 @@ struct IAvnStringArray; struct IAvnDndResultCallback; struct IAvnGCHandleDeallocatorCallback; struct IAvnMenuEvents; - +struct IAvnNativeControlHost; +struct IAvnNativeControlHostTopLevelAttachment; enum SystemDecorations { SystemDecorationsNone = 0, SystemDecorationsBorderOnly = 1, @@ -256,6 +257,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0; virtual HRESULT ObtainNSViewHandle(void** retOut) = 0; virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0; + virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) = 0; virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0; virtual HRESULT SetBlurEnabled (bool enable) = 0; @@ -295,6 +297,7 @@ AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown virtual bool RawTextInputEvent (unsigned int timeStamp, const char* text) = 0; virtual void ScalingChanged(double scaling) = 0; virtual void RunRenderPriorityJobs() = 0; + virtual void LostFocus() = 0; virtual AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, AvnInputModifiers modifiers, AvnDragDropEffects effects, IAvnClipboard* clipboard, void* dataObjectHandle) = 0; @@ -478,4 +481,22 @@ AVNCOM(IAvnGCHandleDeallocatorCallback, 22) : IUnknown virtual void FreeGCHandle(void* handle) = 0; }; +AVNCOM(IAvnNativeControlHost, 20) : IUnknown +{ + virtual HRESULT CreateDefaultChild(void* parent, void** retOut) = 0; + virtual IAvnNativeControlHostTopLevelAttachment* CreateAttachment() = 0; + virtual void DestroyDefaultChild(void* child) = 0; +}; + +AVNCOM(IAvnNativeControlHostTopLevelAttachment, 21) : IUnknown +{ + virtual void* GetParentHandle() = 0; + virtual HRESULT InitializeWithChildHandle(void* child) = 0; + virtual HRESULT AttachTo(IAvnNativeControlHost* host) = 0; + virtual void MoveTo(float x, float y, float width, float height) = 0; + virtual void Hide() = 0; + virtual void ReleaseChild() = 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 ea28780c81..d5cad4d1ca 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 @@ -10,6 +10,8 @@ 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; }; 1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; }; + 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; }; + 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AFD334023E03C4F0042899B /* controlhost.mm */; }; 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; }; 1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; }; 1A465D10246AB61600C5858B /* dnd.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A465D0F246AB61600C5858B /* dnd.mm */; }; @@ -32,6 +34,8 @@ 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = rendertarget.mm; sourceTree = ""; }; 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; }; + 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; }; + 1AFD334023E03C4F0042899B /* controlhost.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = controlhost.mm; sourceTree = ""; }; 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = ""; }; 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 1A465D0F246AB61600C5858B /* dnd.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dnd.mm; sourceTree = ""; }; @@ -86,11 +90,13 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + 1A1852DB23E05814008F0DED /* deadlock.mm */, 1A002B9D232135EE00021753 /* app.mm */, 37DDA9B121933371002E132B /* AvnString.h */, 37DDA9AF219330F8002E132B /* AvnString.mm */, 37A4E71A2178846A00EACBCD /* headers */, 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */, + 1AFD334023E03C4F0042899B /* controlhost.mm */, 5BF943652167AD1D009CAE35 /* cursor.h */, 5B21A981216530F500CEE36E /* cursor.mm */, 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */, @@ -191,6 +197,7 @@ files = ( 1A002B9E232135EE00021753 /* app.mm in Sources */, 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */, + 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */, 5B21A982216530F500CEE36E /* cursor.mm in Sources */, 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, @@ -199,6 +206,7 @@ 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, + 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */, 1A465D10246AB61600C5858B /* dnd.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 1e74a70e66..814b91cb62 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -29,9 +29,43 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati @end +@interface AvnApplication : NSApplication + + +@end + +@implementation AvnApplication +{ + BOOL _isHandlingSendEvent; +} + +- (void)sendEvent:(NSEvent *)event +{ + bool oldHandling = _isHandlingSendEvent; + _isHandlingSendEvent = true; + @try { + [super sendEvent: event]; + } @finally { + _isHandlingSendEvent = oldHandling; + } +} + +// This is needed for certain embedded controls +- (BOOL) isHandlingSendEvent +{ + return _isHandlingSendEvent; +} + +- (void)setHandlingSendEvent:(BOOL)handlingSendEvent +{ + _isHandlingSendEvent = handlingSendEvent; +} + +@end + extern void InitializeAvnApp() { - NSApplication* app = [NSApplication sharedApplication]; + NSApplication* app = [AvnApplication sharedApplication]; id delegate = [AvnAppDelegate new]; [app setDelegate:delegate]; } diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index df6a7be91c..871bca086d 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -24,6 +24,7 @@ extern IAvnGlDisplay* GetGlDisplay(); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeperator(); +extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); @@ -54,4 +55,12 @@ template inline T* objc_cast(id from) { - (void) action; @end +class AvnInsidePotentialDeadlock +{ +public: + static bool IsInside(); + AvnInsidePotentialDeadlock(); + ~AvnInsidePotentialDeadlock(); +}; + #endif diff --git a/native/Avalonia.Native/src/OSX/controlhost.mm b/native/Avalonia.Native/src/OSX/controlhost.mm new file mode 100644 index 0000000000..315ec2f310 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/controlhost.mm @@ -0,0 +1,145 @@ +#include "common.h" + + +IAvnNativeControlHostTopLevelAttachment* CreateAttachment(); + +class AvnNativeControlHost : + public ComSingleObject +{ +public: + FORWARD_IUNKNOWN(); + NSView* View; + AvnNativeControlHost(NSView* view) + { + View = view; + } + + virtual HRESULT CreateDefaultChild(void* parent, void** retOut) override + { + NSView* view = [NSView new]; + [view setWantsLayer: true]; + + *retOut = (__bridge_retained void*)view; + return S_OK; + }; + + virtual IAvnNativeControlHostTopLevelAttachment* CreateAttachment() override + { + return ::CreateAttachment(); + }; + + virtual void DestroyDefaultChild(void* child) override + { + // ARC will release the object for us + (__bridge_transfer NSView*) child; + } +}; + +class AvnNativeControlHostTopLevelAttachment : +public ComSingleObject +{ + NSView* _holder; + NSView* _child; +public: + FORWARD_IUNKNOWN(); + + AvnNativeControlHostTopLevelAttachment() + { + _holder = [NSView new]; + [_holder setWantsLayer:true]; + } + + virtual ~AvnNativeControlHostTopLevelAttachment() + { + if(_child != nil && [_child superview] == _holder) + { + [_child removeFromSuperview]; + } + + if([_holder superview] != nil) + { + [_holder removeFromSuperview]; + } + } + + virtual void* GetParentHandle() override + { + return (__bridge void*)_holder; + }; + + virtual HRESULT InitializeWithChildHandle(void* child) override + { + if(_child != nil) + return E_FAIL; + _child = (__bridge NSView*)child; + if(_child == nil) + return E_FAIL; + [_holder addSubview:_child]; + [_child setHidden: false]; + return S_OK; + }; + + virtual HRESULT AttachTo(IAvnNativeControlHost* host) override + { + if(host == nil) + { + [_holder removeFromSuperview]; + [_holder setHidden: true]; + } + else + { + AvnNativeControlHost* chost = dynamic_cast(host); + if(chost == nil || chost->View == nil) + return E_FAIL; + [_holder setHidden:true]; + [chost->View addSubview:_holder]; + } + return S_OK; + }; + + virtual void MoveTo(float x, float y, float width, float height) override + { + if(_child == nil) + return; + if(AvnInsidePotentialDeadlock::IsInside()) + { + IAvnNativeControlHostTopLevelAttachment* slf = this; + slf->AddRef(); + dispatch_async(dispatch_get_main_queue(), ^{ + slf->MoveTo(x, y, width, height); + slf->Release(); + }); + return; + } + + NSRect childFrame = {0, 0, width, height}; + NSRect holderFrame = {x, y, width, height}; + + [_child setFrame: childFrame]; + [_holder setFrame: holderFrame]; + [_holder setHidden: false]; + if([_holder superview] != nil) + [[_holder superview] setNeedsDisplay:true]; + } + + virtual void Hide() override + { + [_holder setHidden: true]; + } + + virtual void ReleaseChild() override + { + [_child removeFromSuperview]; + _child = nil; + } +}; + +IAvnNativeControlHostTopLevelAttachment* CreateAttachment() +{ + return new AvnNativeControlHostTopLevelAttachment(); +} + +extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent) +{ + return new AvnNativeControlHost(parent); +} diff --git a/native/Avalonia.Native/src/OSX/deadlock.mm b/native/Avalonia.Native/src/OSX/deadlock.mm new file mode 100644 index 0000000000..cb1767c90f --- /dev/null +++ b/native/Avalonia.Native/src/OSX/deadlock.mm @@ -0,0 +1,17 @@ +#include "common.h" + +static int Counter = 0; +AvnInsidePotentialDeadlock::AvnInsidePotentialDeadlock() +{ + Counter++; +} + +AvnInsidePotentialDeadlock::~AvnInsidePotentialDeadlock() +{ + Counter--; +} + +bool AvnInsidePotentialDeadlock::IsInside() +{ + return Counter!=0; +} diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index f826df4da9..86b3584681 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -390,6 +390,14 @@ public: return *ppv == nil ? E_FAIL : S_OK; } + virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override + { + if(View == NULL) + return E_FAIL; + *retOut = ::CreateNativeControlHost(View); + return S_OK; + } + virtual HRESULT SetBlurEnabled (bool enable) override { [Window setContentView: enable ? VisualEffect : View]; @@ -1060,9 +1068,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); } - - (void)updateLayer { + AvnInsidePotentialDeadlock deadlock; if (_parent == nullptr) { return; @@ -1142,7 +1150,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return; } - [self becomeFirstResponder]; auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; auto avnPoint = [self toAvnPoint:localPoint]; auto point = [self translateLocalPoint:avnPoint]; @@ -1169,7 +1176,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent auto timestamp = [event timestamp] * 1000; auto modifiers = [self getModifiers:[event modifierFlags]]; - [self becomeFirstResponder]; + if(type != AvnRawMouseEventType::Move || + ( + [self window] != nil && + ( + [[self window] firstResponder] == nil + || ![[[self window] firstResponder] isKindOfClass: [NSView class]] + ) + ) + ) + [self becomeFirstResponder]; if(_parent != nullptr) { @@ -1179,6 +1195,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [super mouseMoved:event]; } +- (BOOL) resignFirstResponder +{ + _parent->BaseEvents->LostFocus(); + return YES; +} + - (void)mouseMoved:(NSEvent *)event { [self mouseEvent:event withType:Move]; diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 5460dc7720..890967ae4f 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -101,7 +101,7 @@ partial class Build : NukeBuild .SetProjectFile(projectFile) // This is required for VS2019 image on Azure Pipelines .When(Parameters.IsRunningOnWindows && - Parameters.IsRunningOnAzure, c => c + Parameters.IsRunningOnAzure, _ => _ .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_8_X64"))) .AddProperty("PackageVersion", Parameters.Version) .AddProperty("iOSRoslynPathHackRequired", true) @@ -176,7 +176,7 @@ partial class Build : NukeBuild .SetFramework(fw) .EnableNoBuild() .EnableNoRestore() - .When(Parameters.PublishTestResults, c => c + .When(Parameters.PublishTestResults, _ => _ .SetLogger("trx") .SetResultsDirectory(Parameters.TestResultsRoot))); } diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml index 7e945aeaa9..8b697b7948 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml @@ -35,6 +35,7 @@ + diff --git a/samples/interop/NativeEmbedSample/App.xaml b/samples/interop/NativeEmbedSample/App.xaml new file mode 100644 index 0000000000..e35ade4087 --- /dev/null +++ b/samples/interop/NativeEmbedSample/App.xaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/samples/interop/NativeEmbedSample/App.xaml.cs b/samples/interop/NativeEmbedSample/App.xaml.cs new file mode 100644 index 0000000000..cb17cfc35d --- /dev/null +++ b/samples/interop/NativeEmbedSample/App.xaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace NativeEmbedSample +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + desktopLifetime.MainWindow = new MainWindow(); + + base.OnFrameworkInitializationCompleted(); + } + } +} diff --git a/samples/interop/NativeEmbedSample/EmbedSample.cs b/samples/interop/NativeEmbedSample/EmbedSample.cs new file mode 100644 index 0000000000..ab9df11e19 --- /dev/null +++ b/samples/interop/NativeEmbedSample/EmbedSample.cs @@ -0,0 +1,121 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using Avalonia.Controls; +using Avalonia.Platform; +using Avalonia.Threading; +using MonoMac.AppKit; +using MonoMac.Foundation; +using MonoMac.WebKit; +using Encoding = SharpDX.Text.Encoding; + +namespace NativeEmbedSample +{ + public class EmbedSample : NativeControlHost + { + public bool IsSecond { get; set; } + private Process _mplayer; + + IPlatformHandle CreateLinux(IPlatformHandle parent) + { + if (IsSecond) + { + var chooser = GtkHelper.CreateGtkFileChooser(parent.Handle); + if (chooser != null) + return chooser; + } + + var control = base.CreateNativeControlCore(parent); + var nodes = Path.GetFullPath(Path.Combine(typeof(EmbedSample).Assembly.GetModules()[0].FullyQualifiedName, + "..", + "nodes.mp4")); + _mplayer = Process.Start(new ProcessStartInfo("mplayer", + $"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodes}\"") + { + UseShellExecute = false, + + }); + return control; + } + + void DestroyLinux(IPlatformHandle handle) + { + _mplayer?.Kill(); + _mplayer = null; + base.DestroyNativeControlCore(handle); + } + + private const string RichText = + @"{\rtf1\ansi\ansicpg1251\deff0\nouicompat\deflang1049{\fonttbl{\f0\fnil\fcharset0 Calibri;}} +{\colortbl ;\red255\green0\blue0;\red0\green77\blue187;\red0\green176\blue80;\red155\green0\blue211;\red247\green150\blue70;\red75\green172\blue198;} +{\*\generator Riched20 6.3.9600}\viewkind4\uc1 +\pard\sa200\sl276\slmult1\f0\fs22\lang9 I \i am\i0 a \cf1\b Rich Text \cf0\b0\fs24 control\cf2\fs28 !\cf3\fs32 !\cf4\fs36 !\cf1\fs40 !\cf5\fs44 !\cf6\fs48 !\cf0\fs44\par +}"; + + IPlatformHandle CreateWin32(IPlatformHandle parent) + { + WinApi.LoadLibrary("Msftedit.dll"); + var handle = WinApi.CreateWindowEx(0, "RICHEDIT50W", + @"Rich Edit", + 0x800000 | 0x10000000 | 0x40000000 | 0x800000 | 0x10000 | 0x0004, 0, 0, 1, 1, parent.Handle, + IntPtr.Zero, WinApi.GetModuleHandle(null), IntPtr.Zero); + var st = new WinApi.SETTEXTEX { Codepage = 65001, Flags = 0x00000008 }; + var text = RichText.Replace("", IsSecond ? "\\qr " : ""); + var bytes = Encoding.UTF8.GetBytes(text); + WinApi.SendMessage(handle, 0x0400 + 97, ref st, bytes); + return new PlatformHandle(handle, "HWND"); + + } + + void DestroyWin32(IPlatformHandle handle) + { + WinApi.DestroyWindow(handle.Handle); + } + + IPlatformHandle CreateOSX(IPlatformHandle parent) + { + // Note: We are using MonoMac for example purposes + // It shouldn't be used in production apps + MacHelper.EnsureInitialized(); + + var webView = new WebView(); + Dispatcher.UIThread.Post(() => + { + webView.MainFrame.LoadRequest(new NSUrlRequest(new NSUrl( + IsSecond ? "https://bing.com": "https://google.com/"))); + }); + return new MacOSViewHandle(webView); + + } + + void DestroyOSX(IPlatformHandle handle) + { + ((MacOSViewHandle)handle).Dispose(); + } + + protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return CreateLinux(parent); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return CreateWin32(parent); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return CreateOSX(parent); + return base.CreateNativeControlCore(parent); + } + + protected override void DestroyNativeControlCore(IPlatformHandle control) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + DestroyLinux(control); + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + DestroyWin32(control); + else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + DestroyOSX(control); + else + base.DestroyNativeControlCore(control); + } + } +} diff --git a/samples/interop/NativeEmbedSample/GtkHelper.cs b/samples/interop/NativeEmbedSample/GtkHelper.cs new file mode 100644 index 0000000000..e389a51ef5 --- /dev/null +++ b/samples/interop/NativeEmbedSample/GtkHelper.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.Platform.Interop; +using Avalonia.X11.NativeDialogs; +using static Avalonia.X11.NativeDialogs.Gtk; +using static Avalonia.X11.NativeDialogs.Glib; +namespace NativeEmbedSample +{ + public class GtkHelper + { + private static Task s_gtkTask; + class FileChooser : INativeControlHostDestroyableControlHandle + { + private readonly IntPtr _widget; + + public FileChooser(IntPtr widget, IntPtr xid) + { + _widget = widget; + Handle = xid; + } + + public IntPtr Handle { get; } + public string HandleDescriptor => "XID"; + public void Destroy() + { + RunOnGlibThread(() => + { + gtk_widget_destroy(_widget); + return 0; + }).Wait(); + } + } + + + + public static IPlatformHandle CreateGtkFileChooser(IntPtr parentXid) + { + if (s_gtkTask == null) + s_gtkTask = StartGtk(); + if (!s_gtkTask.Result) + return null; + return RunOnGlibThread(() => + { + using (var title = new Utf8Buffer("Embedded")) + { + var widget = gtk_file_chooser_dialog_new(title, IntPtr.Zero, GtkFileChooserAction.SelectFolder, + IntPtr.Zero); + gtk_widget_realize(widget); + var xid = gdk_x11_window_get_xid(gtk_widget_get_window(widget)); + gtk_window_present(widget); + return new FileChooser(widget, xid); + } + }).Result; + } + } +} diff --git a/samples/interop/NativeEmbedSample/MacHelper.cs b/samples/interop/NativeEmbedSample/MacHelper.cs new file mode 100644 index 0000000000..74a06a0a0c --- /dev/null +++ b/samples/interop/NativeEmbedSample/MacHelper.cs @@ -0,0 +1,39 @@ +using System; +using Avalonia.Platform; +using MonoMac.AppKit; + +namespace NativeEmbedSample +{ + public class MacHelper + { + private static bool _isInitialized; + + public static void EnsureInitialized() + { + if (_isInitialized) + return; + _isInitialized = true; + NSApplication.Init(); + } + } + + class MacOSViewHandle : IPlatformHandle, IDisposable + { + private NSView _view; + + public MacOSViewHandle(NSView view) + { + _view = view; + } + + public IntPtr Handle => _view?.Handle ?? IntPtr.Zero; + public string HandleDescriptor => "NSView"; + + public void Dispose() + { + _view.Dispose(); + _view = null; + } + } + +} diff --git a/samples/interop/NativeEmbedSample/MainWindow.xaml b/samples/interop/NativeEmbedSample/MainWindow.xaml new file mode 100644 index 0000000000..dcec9035e0 --- /dev/null +++ b/samples/interop/NativeEmbedSample/MainWindow.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + Visible + + + + + + + + Visible + + + + + + diff --git a/samples/interop/NativeEmbedSample/MainWindow.xaml.cs b/samples/interop/NativeEmbedSample/MainWindow.xaml.cs new file mode 100644 index 0000000000..4324aa2762 --- /dev/null +++ b/samples/interop/NativeEmbedSample/MainWindow.xaml.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace NativeEmbedSample +{ + public class MainWindow : Window + { + public MainWindow() + { + AvaloniaXamlLoader.Load(this); + this.AttachDevTools(); + } + + public async void ShowPopupDelay(object sender, RoutedEventArgs args) + { + await Task.Delay(3000); + ShowPopup(sender, args); + } + + public void ShowPopup(object sender, RoutedEventArgs args) + { + + new ContextMenu() + { + Items = new List + { + new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" } + } + }.Open((Control)sender); + } + } +} diff --git a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj new file mode 100644 index 0000000000..c623ae68b5 --- /dev/null +++ b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj @@ -0,0 +1,30 @@ + + + + Exe + netcoreapp2.0 + true + true + + + + + + + + + + + Designer + + + + PreserveNewest + + + + + + + + diff --git a/samples/interop/NativeEmbedSample/Program.cs b/samples/interop/NativeEmbedSample/Program.cs new file mode 100644 index 0000000000..baa7837667 --- /dev/null +++ b/samples/interop/NativeEmbedSample/Program.cs @@ -0,0 +1,17 @@ +using Avalonia; + +namespace NativeEmbedSample +{ + class Program + { + static int Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .With(new AvaloniaNativePlatformOptions() + { + }) + .UsePlatformDetect(); + + } +} diff --git a/samples/interop/NativeEmbedSample/WinApi.cs b/samples/interop/NativeEmbedSample/WinApi.cs new file mode 100644 index 0000000000..8e5bcdf49e --- /dev/null +++ b/samples/interop/NativeEmbedSample/WinApi.cs @@ -0,0 +1,74 @@ +using System; +using System.Runtime.InteropServices; + +namespace NativeEmbedSample +{ + public unsafe class WinApi + { + public enum CommonControls : uint + { + ICC_LISTVIEW_CLASSES = 0x00000001, // listview, header + ICC_TREEVIEW_CLASSES = 0x00000002, // treeview, tooltips + ICC_BAR_CLASSES = 0x00000004, // toolbar, statusbar, trackbar, tooltips + ICC_TAB_CLASSES = 0x00000008, // tab, tooltips + ICC_UPDOWN_CLASS = 0x00000010, // updown + ICC_PROGRESS_CLASS = 0x00000020, // progress + ICC_HOTKEY_CLASS = 0x00000040, // hotkey + ICC_ANIMATE_CLASS = 0x00000080, // animate + ICC_WIN95_CLASSES = 0x000000FF, + ICC_DATE_CLASSES = 0x00000100, // month picker, date picker, time picker, updown + ICC_USEREX_CLASSES = 0x00000200, // comboex + ICC_COOL_CLASSES = 0x00000400, // rebar (coolbar) control + ICC_INTERNET_CLASSES = 0x00000800, + ICC_PAGESCROLLER_CLASS = 0x00001000, // page scroller + ICC_NATIVEFNTCTL_CLASS = 0x00002000, // native font control + ICC_STANDARD_CLASSES = 0x00004000, + ICC_LINK_CLASS = 0x00008000 + } + + [StructLayout(LayoutKind.Sequential)] + public struct INITCOMMONCONTROLSEX + { + public int dwSize; + public uint dwICC; + } + + [DllImport("Comctl32.dll")] + public static extern void InitCommonControlsEx(ref INITCOMMONCONTROLSEX init); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool DestroyWindow(IntPtr hwnd); + + [DllImport("kernel32.dll")] + public static extern IntPtr LoadLibrary(string lib); + + + [DllImport("kernel32.dll")] + public static extern IntPtr GetModuleHandle(string lpModuleName); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr CreateWindowEx( + int dwExStyle, + string lpClassName, + string lpWindowName, + uint dwStyle, + int x, + int y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam); + + [StructLayout(LayoutKind.Sequential)] + public struct SETTEXTEX + { + public uint Flags; + public uint Codepage; + } + + [DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "SendMessageW")] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, ref SETTEXTEX wParam, byte[] lParam); + } +} diff --git a/samples/interop/NativeEmbedSample/nodes-license.md b/samples/interop/NativeEmbedSample/nodes-license.md new file mode 100644 index 0000000000..ab165bb9b6 --- /dev/null +++ b/samples/interop/NativeEmbedSample/nodes-license.md @@ -0,0 +1 @@ +nodes.mp4 by beeple is licensed under the creative commons license, downloaded from https://vimeo.com/9936271 diff --git a/samples/interop/NativeEmbedSample/nodes.mp4 b/samples/interop/NativeEmbedSample/nodes.mp4 new file mode 100644 index 0000000000..5afe23488a Binary files /dev/null and b/samples/interop/NativeEmbedSample/nodes.mp4 differ diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index c11cadfbac..2e6f4a67c3 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -67,7 +67,7 @@ namespace Avalonia.Android throw new NotSupportedException(); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index fbe027db00..72732a1f95 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -33,10 +33,8 @@ namespace Avalonia.Android return _view.View.DispatchKeyEvent(e); } - class ViewImpl : TopLevelImpl, IEmbeddableWindowImpl + class ViewImpl : TopLevelImpl { - public event Action LostFocus; - public ViewImpl(Context context) : base(context) { View.Focusable = true; diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 71706676d6..69fd2a9f13 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -194,6 +194,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform } public IPopupImpl CreatePopup() => null; + + public Action LostFocus { get; set; } ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface); } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index a0868152f6..aa4fe55f79 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core @@ -49,6 +50,18 @@ namespace Avalonia.Data.Core var accessor = plugin?.Start(reference, PropertyName); + // We need to handle accessor fallback before handling validation. Validators do not support null accessors. + if (accessor == null) + { + reference.TryGetTarget(out object instance); + + var message = $"Could not find a matching property accessor for '{PropertyName}' on '{instance}'"; + + var exception = new MissingMemberException(message); + + accessor = new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); + } + if (_enableValidation && Next == null) { foreach (var validator in ExpressionObserver.DataValidators) @@ -60,15 +73,9 @@ namespace Avalonia.Data.Core } } - if (accessor == null) + if (accessor is null) { - reference.TryGetTarget(out object instance); - - var message = $"Could not find a matching property accessor for '{PropertyName}' on '{instance}'"; - - var exception = new MissingMemberException(message); - - accessor = new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); + throw new AvaloniaInternalException("Data validators must return non-null accessor."); } _accessor = accessor; diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 2d48a7d33b..8a21d7aa4b 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -10,7 +10,7 @@ namespace Avalonia.Controls.Embedding { public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, IDisposable { - public EmbeddableControlRoot(IEmbeddableWindowImpl impl) : base(impl) + public EmbeddableControlRoot(ITopLevelImpl impl) : base(impl) { } @@ -19,9 +19,6 @@ namespace Avalonia.Controls.Embedding { } - [CanBeNull] - public new IEmbeddableWindowImpl PlatformImpl => (IEmbeddableWindowImpl) base.PlatformImpl; - protected bool EnforceClientSize { get; set; } = true; public void Prepare() diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 09f9ed7c1b..48fc8d182f 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -63,6 +63,7 @@ namespace Avalonia.Controls.Embedding.Offscreen } public Action Closed { get; set; } + public Action LostFocus { get; set; } public abstract IMouseDevice MouseDevice { get; } public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs new file mode 100644 index 0000000000..94ef0b2284 --- /dev/null +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -0,0 +1,141 @@ +using Avalonia.Controls.Platform; +using Avalonia.LogicalTree; +using Avalonia.Platform; +using Avalonia.Threading; + +namespace Avalonia.Controls +{ + public class NativeControlHost : Control + { + private TopLevel _currentRoot; + private INativeControlHostImpl _currentHost; + private INativeControlHostControlTopLevelAttachment _attachment; + private IPlatformHandle _nativeControlHandle; + private bool _queuedForDestruction; + static NativeControlHost() + { + IsVisibleProperty.Changed.AddClassHandler(OnVisibleChanged); + TransformedBoundsProperty.Changed.AddClassHandler(OnBoundsChanged); + } + + private static void OnBoundsChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2) + => host.UpdateHost(); + + private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2) + => host.UpdateHost(); + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + _currentRoot = e.Root as TopLevel; + UpdateHost(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _currentRoot = null; + UpdateHost(); + } + + + void UpdateHost() + { + _currentHost = (_currentRoot?.PlatformImpl as ITopLevelImplWithNativeControlHost)?.NativeControlHost; + var needsAttachment = _currentHost != null; + var needsShow = needsAttachment && IsEffectivelyVisible && TransformedBounds.HasValue; + + if (needsAttachment) + { + // If there is an existing attachment, ensure that we are attached to the proper host or destroy the attachment + if (_attachment != null && _attachment.AttachedTo != _currentHost) + { + if (_attachment != null) + { + if (_attachment.IsCompatibleWith(_currentHost)) + { + _attachment.AttachedTo = _currentHost; + } + else + { + _attachment.Dispose(); + _attachment = null; + } + } + } + + // If there is no attachment, but the control exists, + // attempt to attach to to the current toplevel or destroy the control if it's incompatible + if (_attachment == null && _nativeControlHandle != null) + { + if (_currentHost.IsCompatibleWith(_nativeControlHandle)) + _attachment = _currentHost.CreateNewAttachment(_nativeControlHandle); + else + DestroyNativeControl(); + } + + // There is no control handle an no attachment, create both + if (_nativeControlHandle == null) + { + _attachment = _currentHost.CreateNewAttachment(parent => + _nativeControlHandle = CreateNativeControlCore(parent)); + } + } + else + { + // Immediately detach the control from the current toplevel if there is an existing attachment + if (_attachment != null) + _attachment.AttachedTo = null; + + // Don't destroy the control immediately, it might be just being reparented to another TopLevel + if (_nativeControlHandle != null && !_queuedForDestruction) + { + _queuedForDestruction = true; + Dispatcher.UIThread.Post(CheckDestruction, DispatcherPriority.Background); + } + } + + if (needsShow) + _attachment?.ShowInBounds(TransformedBounds.Value); + else if (needsAttachment) + _attachment?.Hide(); + } + + public bool TryUpdateNativeControlPosition() + { + var needsShow = _currentHost != null && IsEffectivelyVisible && TransformedBounds.HasValue; + + if(needsShow) + _attachment?.ShowInBounds(TransformedBounds.Value); + return needsShow; + } + + void CheckDestruction() + { + _queuedForDestruction = false; + if (_currentRoot == null) + DestroyNativeControl(); + } + + protected virtual IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) + { + return _currentHost.CreateDefaultChild(parent); + } + + void DestroyNativeControl() + { + if (_nativeControlHandle != null) + { + _attachment?.Dispose(); + _attachment = null; + + DestroyNativeControlCore(_nativeControlHandle); + _nativeControlHandle = null; + } + } + + protected virtual void DestroyNativeControlCore(IPlatformHandle control) + { + ((INativeControlHostDestroyableControlHandle)control).Destroy(); + } + + } +} diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 6747ac80cd..2a7ea12d79 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -3,6 +3,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.VisualTree; @@ -67,6 +68,9 @@ namespace Avalonia.Controls.Platform window.Deactivated += WindowDeactivated; } + if (_root is TopLevel tl) + tl.PlatformImpl.LostFocus += TopLevelLostPlatformFocus; + _inputManagerSubscription = InputManager?.Process.Subscribe(RawInput); } @@ -96,6 +100,9 @@ namespace Avalonia.Controls.Platform { root.Deactivated -= WindowDeactivated; } + + if (_root is TopLevel tl) + tl.PlatformImpl.LostFocus -= TopLevelLostPlatformFocus; _inputManagerSubscription?.Dispose(); @@ -409,6 +416,11 @@ namespace Avalonia.Controls.Platform { Menu?.Close(); } + + private void TopLevelLostPlatformFocus() + { + Menu?.Close(); + } protected void Click(IMenuItem item) { diff --git a/src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs b/src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs deleted file mode 100644 index e2d174d9b6..0000000000 --- a/src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Avalonia.Platform -{ - /// - /// Defines a platform-specific embeddable window implementation. - /// - public interface IEmbeddableWindowImpl : ITopLevelImpl - { - event Action LostFocus; - } -} diff --git a/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs b/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs new file mode 100644 index 0000000000..7a4568abc6 --- /dev/null +++ b/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs @@ -0,0 +1,32 @@ +using System; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Platform +{ + public interface INativeControlHostImpl + { + INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent); + INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create); + INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle); + bool IsCompatibleWith(IPlatformHandle handle); + } + + public interface INativeControlHostDestroyableControlHandle : IPlatformHandle + { + void Destroy(); + } + + public interface INativeControlHostControlTopLevelAttachment : IDisposable + { + INativeControlHostImpl AttachedTo { get; set; } + bool IsCompatibleWith(INativeControlHostImpl host); + void Hide(); + void ShowInBounds(TransformedBounds transformedBounds); + } + + public interface ITopLevelImplWithNativeControlHost + { + INativeControlHostImpl NativeControlHost { get; } + } +} diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index c7875f6413..6472fba48e 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -104,6 +104,11 @@ namespace Avalonia.Platform /// Gets or sets a method called when the underlying implementation is destroyed. /// Action Closed { get; set; } + + /// + /// Gets or sets a method called when the input focus is lost. + /// + Action LostFocus { get; set; } /// /// Gets a mouse device associated with toplevel diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index a55bd63c6a..be8939e19a 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -3,6 +3,6 @@ namespace Avalonia.Platform public interface IWindowingPlatform { IWindowImpl CreateWindow(); - IEmbeddableWindowImpl CreateEmbeddableWindow(); + IWindowImpl CreateEmbeddableWindow(); } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index ef453274b8..19d034b4e2 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -34,7 +34,7 @@ namespace Avalonia.Controls.Platform return s_designerMode ? (IWindowImpl)platform.CreateEmbeddableWindow() : platform.CreateWindow(); } - public static IEmbeddableWindowImpl CreateEmbeddableWindow() + public static IWindowImpl CreateEmbeddableWindow() { var platform = AvaloniaLocator.Current.GetService(); if (platform == null) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index ac4f805174..1fcf8d61bc 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -9,6 +9,7 @@ using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Metadata; +using Avalonia.Platform; using Avalonia.VisualTree; #nullable enable @@ -351,6 +352,10 @@ namespace Avalonia.Controls.Primitives DeferCleanup(SubscribeToEventHandler(window, WindowDeactivated, (x, handler) => x.Deactivated += handler, (x, handler) => x.Deactivated -= handler)); + + DeferCleanup(SubscribeToEventHandler(window.PlatformImpl, WindowLostFocus, + (x, handler) => x.LostFocus += handler, + (x, handler) => x.LostFocus -= handler)); } else { @@ -610,6 +615,12 @@ namespace Avalonia.Controls.Primitives Close(); } } + + private void WindowLostFocus() + { + if(!StaysOpen) + Close(); + } private IgnoreIsOpenScope BeginIgnoringIsOpen() { diff --git a/src/Avalonia.Controls/Remote/RemoteServer.cs b/src/Avalonia.Controls/Remote/RemoteServer.cs index e116316904..4f5a7cd311 100644 --- a/src/Avalonia.Controls/Remote/RemoteServer.cs +++ b/src/Avalonia.Controls/Remote/RemoteServer.cs @@ -10,13 +10,13 @@ namespace Avalonia.Controls.Remote { private EmbeddableControlRoot _topLevel; - class EmbeddableRemoteServerTopLevelImpl : RemoteServerTopLevelImpl, IEmbeddableWindowImpl + class EmbeddableRemoteServerTopLevelImpl : RemoteServerTopLevelImpl { public EmbeddableRemoteServerTopLevelImpl(IAvaloniaRemoteTransportConnection transport) : base(transport) { } #pragma warning disable 67 - public event Action LostFocus; + public Action LostFocus { get; set; } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 5d34444eb8..c5491746a3 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -11,6 +11,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; +using Avalonia.VisualTree; using JetBrains.Annotations; namespace Avalonia.Controls @@ -170,6 +171,8 @@ namespace Avalonia.Controls nameof(IResourceHost.ResourcesChanged), this); } + + impl.LostFocus += PlatformImpl_LostFocus; } /// @@ -471,5 +474,17 @@ namespace Avalonia.Controls { (this as IInputRoot).MouseDevice.SceneInvalidated(this, e.DirtyRect); } + + void PlatformImpl_LostFocus() + { + var focused = (IVisual)FocusManager.Instance.Current; + if (focused == null) + return; + while (focused.VisualParent != null) + focused = focused.VisualParent; + + if (focused == this) + KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); + } } } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index cd64af60e2..d42eda6e5e 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -10,7 +10,7 @@ using Avalonia.Threading; namespace Avalonia.DesignerSupport.Remote { - class PreviewerWindowImpl : RemoteServerTopLevelImpl, IWindowImpl, IEmbeddableWindowImpl + class PreviewerWindowImpl : RemoteServerTopLevelImpl, IWindowImpl { private readonly IAvaloniaRemoteTransportConnection _transport; @@ -45,11 +45,6 @@ namespace Avalonia.DesignerSupport.Remote public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } public Size MaxAutoSizeHint { get; } = new Size(4096, 4096); - public event Action LostFocus - { - add {} - remove {} - } protected override void OnMessage(IAvaloniaRemoteTransportConnection transport, object obj) { diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index dcfcd42c04..fe4c580bbb 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -19,7 +19,7 @@ namespace Avalonia.DesignerSupport.Remote public IWindowImpl CreateWindow() => new WindowStub(); - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { if (s_lastWindow != null) { diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index b001bc1b76..39b8d7f076 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -29,6 +29,7 @@ namespace Avalonia.DesignerSupport.Remote public Action ScalingChanged { get; set; } public Func Closing { get; set; } public Action Closed { get; set; } + public Action LostFocus { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); public IPopupImpl CreatePopup() => new WindowStub(this); diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs index 28c0fe8df6..722215f8b7 100644 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ b/src/Avalonia.Input/KeyboardNavigation.cs @@ -30,6 +30,19 @@ namespace Avalonia.Input "TabOnceActiveElement", typeof(KeyboardNavigation)); + + /// + /// Defines the IsTabStop attached property. + /// + /// + /// The IsTabStop attached property determines whether the control is focusable by tab navigation. + /// + public static readonly AttachedProperty IsTabStopProperty = + AvaloniaProperty.RegisterAttached( + "IsTabStop", + typeof(KeyboardNavigation), + true); + /// /// Gets the for a container. /// @@ -69,5 +82,25 @@ namespace Avalonia.Input { element.SetValue(TabOnceActiveElementProperty, value); } + + /// + /// Sets the for a container. + /// + /// The container. + /// Value indicating whether the container is a tab stop. + public static void SetIsTabStop(InputElement element, bool value) + { + element.SetValue(IsTabStopProperty, value); + } + + /// + /// Gets the for a container. + /// + /// The container. + /// Whether the container is a tab stop. + public static bool GetIsTabStop(InputElement element) + { + return element.GetValue(IsTabStopProperty); + } } } diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 596395eac6..dd50ea438a 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -77,7 +77,8 @@ namespace Avalonia.Input.Navigation /// The element. /// The tab direction. Must be Next or Previous. /// The element's focusable descendants. - private static IEnumerable GetFocusableDescendants(IInputElement element, NavigationDirection direction) + private static IEnumerable GetFocusableDescendants(IInputElement element, + NavigationDirection direction) { var mode = KeyboardNavigation.GetTabNavigation((InputElement)element); @@ -113,7 +114,7 @@ namespace Avalonia.Input.Navigation } else { - if (child.CanFocus()) + if (child.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement)child)) { yield return child; } @@ -122,7 +123,10 @@ namespace Avalonia.Input.Navigation { foreach (var descendant in GetFocusableDescendants(child, direction)) { - yield return descendant; + if (KeyboardNavigation.GetIsTabStop((InputElement)descendant)) + { + yield return descendant; + } } } } @@ -167,7 +171,9 @@ namespace Avalonia.Input.Navigation { element = navigable.GetControl(direction, element, false); - if (element != null && element.CanFocus()) + if (element != null && + element.CanFocus() && + KeyboardNavigation.GetIsTabStop((InputElement) element)) { break; } @@ -233,26 +239,22 @@ namespace Avalonia.Input.Navigation return customNext.next; } - if (sibling.CanFocus()) + if (sibling.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement) sibling)) { return sibling; } - else + + next = direction == NavigationDirection.Next ? + GetFocusableDescendants(sibling, direction).FirstOrDefault() : + GetFocusableDescendants(sibling, direction).LastOrDefault(); + + if (next != null) { - next = direction == NavigationDirection.Next ? - GetFocusableDescendants(sibling, direction).FirstOrDefault() : - GetFocusableDescendants(sibling, direction).LastOrDefault(); - if(next != null) - { - return next; - } + return next; } } - if (next == null) - { - next = GetFirstInNextContainer(element, parent, direction); - } + next = GetFirstInNextContainer(element, parent, direction); } else { @@ -264,7 +266,8 @@ namespace Avalonia.Input.Navigation return next; } - private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, NavigationDirection direction) + private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, + NavigationDirection direction) { if (element is ICustomKeyboardNavigation custom) { diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index e46f4d4a15..cfd47d48de 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -122,7 +122,7 @@ namespace Avalonia.Native return new WindowImpl(_factory, _options, _glFeature); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } diff --git a/src/Avalonia.Native/NativeControlHostImpl.cs b/src/Avalonia.Native/NativeControlHostImpl.cs new file mode 100644 index 0000000000..0777c6416b --- /dev/null +++ b/src/Avalonia.Native/NativeControlHostImpl.cs @@ -0,0 +1,136 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Native.Interop; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Native +{ + class NativeControlHostImpl : IDisposable, INativeControlHostImpl + { + private IAvnNativeControlHost _host; + + public NativeControlHostImpl(IAvnNativeControlHost host) + { + _host = host; + } + + public void Dispose() + { + _host?.Dispose(); + _host = null; + } + + class DestroyableNSView : INativeControlHostDestroyableControlHandle + { + private IAvnNativeControlHost _impl; + private IntPtr _nsView; + + public DestroyableNSView(IAvnNativeControlHost impl) + { + _impl = new IAvnNativeControlHost(impl.NativePointer); + _impl.AddRef(); + _nsView = _impl.CreateDefaultChild(IntPtr.Zero); + } + + public IntPtr Handle => _nsView; + public string HandleDescriptor => "NSView"; + public void Destroy() + { + if (_impl != null) + { + _impl.DestroyDefaultChild(_nsView); + _impl.Dispose(); + _impl = null; + _nsView = IntPtr.Zero; + } + } + } + + public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) + => new DestroyableNSView(_host); + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment( + Func create) + { + var a = new Attachment(_host.CreateAttachment()); + try + { + var child = create(a.GetParentHandle()); + a.InitWithChild(child); + a.AttachedTo = this; + return a; + } + catch + { + a.Dispose(); + throw; + } + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) + { + var a = new Attachment(_host.CreateAttachment()); + a.InitWithChild(handle); + a.AttachedTo = this; + return a; + } + + public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == "NSView"; + + class Attachment : INativeControlHostControlTopLevelAttachment + { + private IAvnNativeControlHostTopLevelAttachment _native; + private NativeControlHostImpl _attachedTo; + public IPlatformHandle GetParentHandle() => new PlatformHandle(_native.ParentHandle, "NSView"); + public Attachment(IAvnNativeControlHostTopLevelAttachment native) + { + _native = native; + } + + public void Dispose() + { + if (_native != null) + { + _native.ReleaseChild(); + _native.Dispose(); + _native = null; + } + } + + public INativeControlHostImpl AttachedTo + { + get => _attachedTo; + set + { + var host = (NativeControlHostImpl)value; + if(host == null) + _native.AttachTo(null); + else + _native.AttachTo(host._host); + _attachedTo = host; + } + } + + public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostImpl; + + public void Hide() + { + _native?.Hide(); + } + + public void ShowInBounds(TransformedBounds transformedBounds) + { + if (_attachedTo == null) + throw new InvalidOperationException("Native control isn't attached to a toplevel"); + var bounds = transformedBounds.Bounds.TransformToAABB(transformedBounds.Transform); + bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width), + Math.Max(1, bounds.Height)); + _native.MoveTo((float) bounds.X, (float) bounds.Y, (float) bounds.Width, (float) bounds.Height); + } + + public void InitWithChild(IPlatformHandle handle) + => _native.InitializeWithChildHandle(handle.Handle); + } + } +} diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 9783454e0e..930b5800ba 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Input.Raw; @@ -43,7 +44,7 @@ namespace Avalonia.Native } public abstract class WindowBaseImpl : IWindowBaseImpl, - IFramebufferPlatformSurface + IFramebufferPlatformSurface, ITopLevelImplWithNativeControlHost { IInputRoot _inputRoot; IAvnWindowBase _native; @@ -57,6 +58,7 @@ namespace Avalonia.Native private Size _lastRenderedLogicalSize; private double _savedScaling; private GlPlatformSurface _glSurface; + private NativeControlHostImpl _nativeControlHost; private IGlContext _glContext; internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, GlPlatformFeature glFeature) @@ -80,6 +82,7 @@ namespace Avalonia.Native Screen = new ScreenImpl(screens); _savedLogicalSize = ClientSize; _savedScaling = Scaling; + _nativeControlHost = new NativeControlHostImpl(_native.CreateNativeControlHost()); var monitor = Screen.AllScreens.OrderBy(x => x.PixelDensity) .FirstOrDefault(m => m.Bounds.Contains(Position)); @@ -101,6 +104,8 @@ namespace Avalonia.Native this }; + public INativeControlHostImpl NativeControlHost => _nativeControlHost; + public ILockedFramebuffer Lock() { var w = _savedLogicalSize.Width * _savedScaling; @@ -119,6 +124,8 @@ namespace Avalonia.Native }, (int)w, (int)h, new Vector(dpi, dpi)); } + public Action LostFocus { get; set; } + public Action Paint { get; set; } public Action Resized { get; set; } public Action Closed { get; set; } @@ -201,6 +208,11 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Render); } + + void IAvnWindowBaseEvents.LostFocus() + { + _parent.LostFocus?.Invoke(); + } public AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, AvnInputModifiers modifiers, @@ -288,6 +300,9 @@ namespace Avalonia.Native _native?.Dispose(); _native = null; + _nativeControlHost?.Dispose(); + _nativeControlHost = null; + (Screen as ScreenImpl)?.Dispose(); } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 5da44c5943..872f69c884 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -394,13 +394,8 @@ namespace Avalonia.Rendering.SceneGraph } } - private static bool ShouldStartLayer(IVisual visual) - { - var o = visual as IAvaloniaObject; - return visual.VisualChildren.Count > 0 && - o != null && - o.IsAnimating(Visual.OpacityProperty); - } + // HACK: Disabled layers because they're broken in current renderer. See #2244. + private static bool ShouldStartLayer(IVisual visual) => false; private static IGeometryImpl CreateLayerGeometryClip(VisualNode node) { diff --git a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs index 52fd6abb8b..3aa0392496 100644 --- a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs @@ -76,5 +76,7 @@ namespace Avalonia.VisualTree { return !left.Equals(right); } + + public override string ToString() => $"Bounds: {Bounds} Clip: {Clip} Transform {Transform}"; } } diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index 9a12479fac..82cf3c934f 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -207,6 +207,9 @@ namespace Avalonia.X11.NativeDialogs [DllImport(GtkName)] public static extern void gtk_widget_realize(IntPtr gtkWidget); + + [DllImport(GtkName)] + public static extern void gtk_widget_destroy(IntPtr gtkWidget); [DllImport(GtkName)] public static extern IntPtr gtk_widget_get_window(IntPtr gtkWidget); @@ -219,6 +222,13 @@ namespace Avalonia.X11.NativeDialogs [DllImport(GdkName)] static extern IntPtr gdk_x11_window_foreign_new_for_display(IntPtr display, IntPtr xid); + + [DllImport(GdkName)] + public static extern IntPtr gdk_x11_window_get_xid(IntPtr window); + + + [DllImport(GtkName)] + public static extern IntPtr gtk_container_add(IntPtr container, IntPtr widget); [DllImport(GdkName)] static extern IntPtr gdk_set_allowed_backends(Utf8Buffer backends); diff --git a/src/Avalonia.X11/X11NativeControlHost.cs b/src/Avalonia.X11/X11NativeControlHost.cs new file mode 100644 index 0000000000..add3bc6585 --- /dev/null +++ b/src/Avalonia.X11/X11NativeControlHost.cs @@ -0,0 +1,190 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.VisualTree; +using static Avalonia.X11.XLib; +namespace Avalonia.X11 +{ + // TODO: Actually implement XEmbed instead of simply using XReparentWindow + class X11NativeControlHost : INativeControlHostImpl + { + private readonly AvaloniaX11Platform _platform; + public X11Window Window { get; } + + public X11NativeControlHost(AvaloniaX11Platform platform, X11Window window) + { + _platform = platform; + Window = window; + } + + public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) + { + var ch = new DumbWindow(_platform.Info); + XSync(_platform.Display, false); + return ch; + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create) + { + var holder = new DumbWindow(_platform.Info, Window.Handle.Handle); + Attachment attachment = null; + try + { + var child = create(holder); + // ReSharper disable once UseObjectOrCollectionInitializer + // It has to be assigned to the variable before property setter is called so we dispose it on exception + attachment = new Attachment(_platform.Display, holder, _platform.OrphanedWindow, child); + attachment.AttachedTo = this; + return attachment; + } + catch + { + attachment?.Dispose(); + holder?.Destroy(); + throw; + } + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) + { + if (!IsCompatibleWith(handle)) + throw new ArgumentException(handle.HandleDescriptor + " is not compatible with the current window", + nameof(handle)); + var attachment = new Attachment(_platform.Display, new DumbWindow(_platform.Info, Window.Handle.Handle), + _platform.OrphanedWindow, handle) { AttachedTo = this }; + return attachment; + } + + public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == "XID"; + + class DumbWindow : INativeControlHostDestroyableControlHandle + { + private readonly IntPtr _display; + + public DumbWindow(X11Info x11, IntPtr? parent = null) + { + _display = x11.Display; + /*Handle = XCreateSimpleWindow(x11.Display, XLib.XDefaultRootWindow(_display), + 0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero);*/ + var attr = new XSetWindowAttributes + { + backing_store = 1, + bit_gravity = Gravity.NorthWestGravity, + win_gravity = Gravity.NorthWestGravity, + + }; + + parent = parent ?? XDefaultRootWindow(x11.Display); + + Handle = XCreateWindow(_display, parent.Value, 0, 0, + 1,1, 0, 0, + (int)CreateWindowArgs.InputOutput, + IntPtr.Zero, + new UIntPtr((uint)(SetWindowValuemask.BorderPixel | SetWindowValuemask.BitGravity | + SetWindowValuemask.BackPixel | + SetWindowValuemask.WinGravity | SetWindowValuemask.BackingStore)), ref attr); + } + + public IntPtr Handle { get; private set; } + public string HandleDescriptor => "XID"; + public void Destroy() + { + if (Handle != IntPtr.Zero) + { + XDestroyWindow(_display, Handle); + Handle = IntPtr.Zero; + } + } + } + + class Attachment : INativeControlHostControlTopLevelAttachment + { + private readonly IntPtr _display; + private readonly IntPtr _orphanedWindow; + private DumbWindow _holder; + private IPlatformHandle _child; + private X11NativeControlHost _attachedTo; + private bool _mapped; + + public Attachment(IntPtr display, DumbWindow holder, IntPtr orphanedWindow, IPlatformHandle child) + { + _display = display; + _orphanedWindow = orphanedWindow; + _holder = holder; + _child = child; + XReparentWindow(_display, child.Handle, holder.Handle, 0, 0); + XMapWindow(_display, child.Handle); + } + + public void Dispose() + { + if (_child != null) + { + XReparentWindow(_display, _child.Handle, _orphanedWindow, 0, 0); + _child = null; + } + + _holder?.Destroy(); + _holder = null; + _attachedTo = null; + } + + void CheckDisposed() + { + if (_child == null) + throw new ObjectDisposedException("X11 INativeControlHostControlTopLevelAttachment"); + } + + public INativeControlHostImpl AttachedTo + { + get => _attachedTo; + set + { + CheckDisposed(); + _attachedTo = (X11NativeControlHost)value; + if (value == null) + { + _mapped = false; + XUnmapWindow(_display, _holder.Handle); + XReparentWindow(_display, _holder.Handle, _orphanedWindow, 0, 0); + } + else + { + XReparentWindow(_display, _holder.Handle, _attachedTo.Window.Handle.Handle, 0, 0); + } + } + } + + public bool IsCompatibleWith(INativeControlHostImpl host) => host is X11NativeControlHost; + + public void Hide() + { + if(_attachedTo == null || _child == null) + return; + _mapped = false; + XUnmapWindow(_display, _holder.Handle); + } + + public void ShowInBounds(TransformedBounds transformedBounds) + { + CheckDisposed(); + if (_attachedTo == null) + throw new InvalidOperationException("The control isn't currently attached to a toplevel"); + var bounds = transformedBounds.Bounds.TransformToAABB(transformedBounds.Transform) * + new Vector(_attachedTo.Window.Scaling, _attachedTo.Window.Scaling); + var pixelRect = new PixelRect((int)bounds.X, (int)bounds.Y, Math.Max(1, (int)bounds.Width), + Math.Max(1, (int)bounds.Height)); + XMoveResizeWindow(_display, _child.Handle, 0, 0, pixelRect.Width, pixelRect.Height); + XMoveResizeWindow(_display, _holder.Handle, pixelRect.X, pixelRect.Y, pixelRect.Width, + pixelRect.Height); + if (!_mapped) + { + XMapWindow(_display, _holder.Handle); + XRaiseWindow(_display, _holder.Handle); + _mapped = true; + } + Console.WriteLine($"Moved {_child.Handle} to {pixelRect}"); + } + } + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index c2b3926ffd..9e9c4fd30e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -26,6 +26,7 @@ namespace Avalonia.X11 public IX11Screens X11Screens { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } + public IntPtr OrphanedWindow { get; private set; } public X11Globals Globals { get; private set; } public void Initialize(X11PlatformOptions options) { @@ -33,6 +34,8 @@ namespace Avalonia.X11 XInitThreads(); Display = XOpenDisplay(IntPtr.Zero); DeferredDisplay = XOpenDisplay(IntPtr.Zero); + OrphanedWindow = XCreateSimpleWindow(Display, XDefaultRootWindow(Display), 0, 0, 1, 1, 0, IntPtr.Zero, + IntPtr.Zero); if (Display == IntPtr.Zero) throw new Exception("XOpenDisplay failed"); XError.Init(); @@ -82,7 +85,7 @@ namespace Avalonia.X11 return new X11Window(this, null); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 90064cb28d..499fe5a67a 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -21,7 +21,9 @@ using static Avalonia.X11.XLib; // ReSharper disable StringLiteralTypo namespace Avalonia.X11 { - unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client, ITopLevelImplWithNativeMenuExporter + unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client, + ITopLevelImplWithNativeMenuExporter, + ITopLevelImplWithNativeControlHost { private readonly AvaloniaX11Platform _platform; private readonly IWindowImpl _popupParent; @@ -185,6 +187,7 @@ namespace Avalonia.X11 PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); if (platform.Options.UseDBusMenu) NativeMenuExporter = DBusMenuExporter.TryCreate(_handle); + NativeControlHost = new X11NativeControlHost(_platform, this); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -313,6 +316,7 @@ namespace Avalonia.X11 public Action Closed { get; set; } public Action PositionChanged { get; set; } + public Action LostFocus { get; set; } public IRenderer CreateRenderer(IRenderRoot root) { @@ -1094,7 +1098,7 @@ namespace Avalonia.X11 public IPopupPositioner PopupPositioner { get; } public ITopLevelNativeMenuExporter NativeMenuExporter { get; } - + public INativeControlHostImpl NativeControlHost { get; } public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => _transparencyHelper.SetTransparencyRequest(transparencyLevel); diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index 4e44f55fe0..742973e0da 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -239,13 +239,13 @@ namespace Avalonia.X11 if (ev.Type == XiEventType.XI_ButtonPress && ev.Button >= 4 && ev.Button <= 7 && !ev.Emulated) { - Vector? scrollDelta = ev.Button switch + var scrollDelta = ev.Button switch { 4 => new Vector(0, 1), 5 => new Vector(0, -1), 6 => new Vector(1, 0), 7 => new Vector(-1, 0), - _ => null + _ => (Vector?)null }; if (scrollDelta.HasValue) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index d4a29e39c6..2153344363 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -10,7 +10,7 @@ using Avalonia.Rendering; namespace Avalonia.LinuxFramebuffer { - class FramebufferToplevelImpl : IEmbeddableWindowImpl, IScreenInfoProvider + class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider { private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; @@ -75,11 +75,7 @@ namespace Avalonia.LinuxFramebuffer public Action TransparencyLevelChanged { get; set; } public Action Closed { get; set; } - public event Action LostFocus - { - add {} - remove {} - } + public Action LostFocus { get; set; } public Size ScaledSize => _outputBackend.PixelSize.ToSize(Scaling); diff --git a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs index abace92f08..a8060d3fbf 100644 --- a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs +++ b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs @@ -24,9 +24,7 @@ namespace Avalonia.Win32.Embedding if (_root.IsFocused) FocusManager.Instance.Focus(null); _root.GotFocus += RootGotFocus; - // ReSharper disable once PossibleNullReferenceException - // Always non-null at this point - _root.PlatformImpl.LostFocus += PlatformImpl_LostFocus; + FixPosition(); } @@ -36,23 +34,6 @@ namespace Avalonia.Win32.Embedding set { _root.Content = value; } } - void Unfocus() - { - var focused = (IVisual)FocusManager.Instance.Current; - if (focused == null) - return; - while (focused.VisualParent != null) - focused = focused.VisualParent; - - if (focused == _root) - KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); - } - - private void PlatformImpl_LostFocus() - { - Unfocus(); - } - protected override void Dispose(bool disposing) { if (disposing) diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index b30831667b..f8366abb81 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -18,11 +18,11 @@ using MouseButton = System.Windows.Input.MouseButton; namespace Avalonia.Win32.Interop.Wpf { - class WpfTopLevelImpl : FrameworkElement, IEmbeddableWindowImpl + class WpfTopLevelImpl : FrameworkElement, ITopLevelImpl { private HwndSource _currentHwndSource; private readonly HwndSourceHook _hook; - private readonly IEmbeddableWindowImpl _ttl; + private readonly ITopLevelImpl _ttl; private IInputRoot _inputRoot; private readonly IEnumerable _surfaces; private readonly IMouseDevice _mouse; @@ -241,7 +241,7 @@ namespace Avalonia.Win32.Interop.Wpf Action ITopLevelImpl.TransparencyLevelChanged { get; set; } Action ITopLevelImpl.Closed { get; set; } - public new event Action LostFocus; + public new Action LostFocus { get; set; } internal Vector GetScaling() { diff --git a/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs b/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs index 34f2894657..e08f1b28be 100644 --- a/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs +++ b/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs @@ -6,11 +6,8 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class EmbeddedWindowImpl : WindowImpl, IEmbeddableWindowImpl + class EmbeddedWindowImpl : WindowImpl { - private static IntPtr DefaultParentWindow = CreateParentWindow(); - private static UnmanagedMethods.WndProc _wndProcDelegate; - protected override IntPtr CreateWindowOverride(ushort atom) { var hWnd = UnmanagedMethods.CreateWindowEx( @@ -22,66 +19,12 @@ namespace Avalonia.Win32 0, 640, 480, - DefaultParentWindow, + OffscreenParentWindow.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); return hWnd; } - protected override IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - if (msg == (uint)UnmanagedMethods.WindowsMessage.WM_KILLFOCUS) - LostFocus?.Invoke(); - return base.WndProc(hWnd, msg, wParam, lParam); - } - - public event Action LostFocus; - - private static IntPtr CreateParentWindow() - { - _wndProcDelegate = new UnmanagedMethods.WndProc(ParentWndProc); - - var wndClassEx = new UnmanagedMethods.WNDCLASSEX - { - cbSize = Marshal.SizeOf(), - hInstance = UnmanagedMethods.GetModuleHandle(null), - lpfnWndProc = _wndProcDelegate, - lpszClassName = "AvaloniaEmbeddedWindow-" + Guid.NewGuid(), - }; - - var atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); - - if (atom == 0) - { - throw new Win32Exception(); - } - - var hwnd = UnmanagedMethods.CreateWindowEx( - 0, - atom, - null, - (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, - UnmanagedMethods.CW_USEDEFAULT, - UnmanagedMethods.CW_USEDEFAULT, - UnmanagedMethods.CW_USEDEFAULT, - UnmanagedMethods.CW_USEDEFAULT, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero); - - if (hwnd == IntPtr.Zero) - { - throw new Win32Exception(); - } - - return hwnd; - } - - private static IntPtr ParentWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); - } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index ba3775200b..b3b38db1ab 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -15,7 +15,7 @@ namespace Avalonia.Win32.Interop [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Using Win32 naming for consistency.")] [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements must be documented", Justification = "Look in Win32 docs.")] [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items must be documented", Justification = "Look in Win32 docs.")] - internal static class UnmanagedMethods + internal unsafe static class UnmanagedMethods { public const int CW_USEDEFAULT = unchecked((int)0x80000000); @@ -1001,6 +1001,10 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool InvalidateRect(IntPtr hWnd, ref RECT lpRect, bool bErase); + + [DllImport("user32.dll")] + public static extern bool InvalidateRect(IntPtr hWnd, RECT* lpRect, bool bErase); + [DllImport("user32.dll")] public static extern bool IsWindowEnabled(IntPtr hWnd); @@ -1051,6 +1055,8 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool SetParent(IntPtr hWnd, IntPtr hWndNewParent); [DllImport("user32.dll")] + public static extern IntPtr GetParent(IntPtr hWnd); + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow); [DllImport("kernel32.dll", SetLastError = true)] diff --git a/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs b/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs new file mode 100644 index 0000000000..9de105a3d5 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs @@ -0,0 +1,58 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Avalonia.Platform; +using Avalonia.Win32.Interop; +namespace Avalonia.Win32 +{ + class OffscreenParentWindow + { + public static IntPtr Handle { get; } = CreateParentWindow(); + private static UnmanagedMethods.WndProc s_wndProcDelegate; + private static IntPtr CreateParentWindow() + { + s_wndProcDelegate = new UnmanagedMethods.WndProc(ParentWndProc); + + var wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpfnWndProc = s_wndProcDelegate, + lpszClassName = "AvaloniaEmbeddedWindow-" + Guid.NewGuid(), + }; + + var atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + + if (atom == 0) + { + throw new Win32Exception(); + } + + var hwnd = UnmanagedMethods.CreateWindowEx( + 0, + atom, + null, + (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + + if (hwnd == IntPtr.Zero) + { + throw new Win32Exception(); + } + + return hwnd; + } + + private static IntPtr ParentWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + } +} diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 7f27a9e841..cd25b32ed9 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -10,11 +10,31 @@ namespace Avalonia.Win32 private bool _dropShadowHint = true; private Size? _maxAutoSize; + + // This is needed because we are calling virtual methods from constructors + // One fabulous design decision leads to another, I guess + [ThreadStatic] + private static IntPtr s_parentHandle; + public override void Show() { UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate); + var parent = UnmanagedMethods.GetParent(Handle.Handle); + if (parent != IntPtr.Zero) + { + IntPtr nextParent = parent; + while (nextParent != IntPtr.Zero) + { + parent = nextParent; + nextParent = UnmanagedMethods.GetParent(parent); + } + + UnmanagedMethods.SetFocus(parent); + } } + protected override bool ShouldTakeFocusOnClick => false; + public override Size MaxAutoSizeHint { get @@ -56,10 +76,11 @@ namespace Avalonia.Win32 UnmanagedMethods.CW_USEDEFAULT, UnmanagedMethods.CW_USEDEFAULT, UnmanagedMethods.CW_USEDEFAULT, - IntPtr.Zero, + s_parentHandle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + s_parentHandle = IntPtr.Zero; EnableBoxShadow(result, _dropShadowHint); @@ -80,7 +101,22 @@ namespace Avalonia.Win32 } } - public PopupImpl(IWindowBaseImpl parent) + // This is needed because we are calling virtual methods from constructors + // One fabulous design decision leads to another, I guess + static IWindowBaseImpl SaveParentHandle(IWindowBaseImpl parent) + { + s_parentHandle = parent.Handle.Handle; + return parent; + } + + // This is needed because we are calling virtual methods from constructors + // One fabulous design decision leads to another, I guess + public PopupImpl(IWindowBaseImpl parent) : this(SaveParentHandle(parent), false) + { + + } + + private PopupImpl(IWindowBaseImpl parent, bool dummy) : base() { PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); } diff --git a/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs new file mode 100644 index 0000000000..69fbe30068 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs @@ -0,0 +1,201 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.VisualTree; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + class Win32NativeControlHost : INativeControlHostImpl + { + public WindowImpl Window { get; } + + public Win32NativeControlHost(WindowImpl window) + { + Window = window; + } + + void AssertCompatible(IPlatformHandle handle) + { + if (!IsCompatibleWith(handle)) + throw new ArgumentException($"Don't know what to do with {handle.HandleDescriptor}"); + } + + public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) + { + AssertCompatible(parent); + return new DumbWindow(parent.Handle); + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create) + { + var holder = new DumbWindow(Window.Handle.Handle); + Win32NativeControlAttachment attachment = null; + try + { + var child = create(holder); + // ReSharper disable once UseObjectOrCollectionInitializer + // It has to be assigned to the variable before property setter is called so we dispose it on exception + attachment = new Win32NativeControlAttachment(holder, child); + attachment.AttachedTo = this; + return attachment; + } + catch + { + attachment?.Dispose(); + holder?.Destroy(); + throw; + } + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) + { + AssertCompatible(handle); + return new Win32NativeControlAttachment(new DumbWindow(Window.Handle.Handle), + handle) { AttachedTo = this }; + } + + public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == "HWND"; + + class DumbWindow : IDisposable, INativeControlHostDestroyableControlHandle + { + public IntPtr Handle { get;} + public string HandleDescriptor => "HWND"; + public void Destroy() => Dispose(); + + UnmanagedMethods.WndProc _wndProcDelegate; + private readonly string _className; + + public DumbWindow(IntPtr? parent = null) + { + _wndProcDelegate = WndProc; + var wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpfnWndProc = _wndProcDelegate, + lpszClassName = _className = "AvaloniaDumbWindow-" + Guid.NewGuid(), + }; + + var atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + Handle = UnmanagedMethods.CreateWindowEx( + 0, + atom, + null, + (int)UnmanagedMethods.WindowStyles.WS_CHILD, + 0, + 0, + 640, + 480, + parent ?? OffscreenParentWindow.Handle, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + } + + + + protected virtual unsafe IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + + private void ReleaseUnmanagedResources() + { + UnmanagedMethods.DestroyWindow(Handle); + UnmanagedMethods.UnregisterClass(_className, UnmanagedMethods.GetModuleHandle(null)); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~DumbWindow() + { + ReleaseUnmanagedResources(); + } + } + + class Win32NativeControlAttachment : INativeControlHostControlTopLevelAttachment + { + private DumbWindow _holder; + private IPlatformHandle _child; + private Win32NativeControlHost _attachedTo; + + public Win32NativeControlAttachment(DumbWindow holder, IPlatformHandle child) + { + _holder = holder; + _child = child; + UnmanagedMethods.SetParent(child.Handle, _holder.Handle); + UnmanagedMethods.ShowWindow(child.Handle, UnmanagedMethods.ShowWindowCommand.Show); + } + + void CheckDisposed() + { + if (_holder == null) + throw new ObjectDisposedException(nameof(Win32NativeControlAttachment)); + } + + public void Dispose() + { + if (_child != null) + UnmanagedMethods.SetParent(_child.Handle, OffscreenParentWindow.Handle); + _holder?.Dispose(); + _holder = null; + _child = null; + _attachedTo = null; + } + + public INativeControlHostImpl AttachedTo + { + get => _attachedTo; + set + { + CheckDisposed(); + _attachedTo = (Win32NativeControlHost) value; + if (_attachedTo == null) + { + UnmanagedMethods.ShowWindow(_holder.Handle, UnmanagedMethods.ShowWindowCommand.Hide); + UnmanagedMethods.SetParent(_holder.Handle, OffscreenParentWindow.Handle); + } + else + UnmanagedMethods.SetParent(_holder.Handle, _attachedTo.Window.Handle.Handle); + } + } + + public bool IsCompatibleWith(INativeControlHostImpl host) => host is Win32NativeControlHost; + + public void Hide() + { + UnmanagedMethods.SetWindowPos(_holder.Handle, IntPtr.Zero, + -100, -100, 1, 1, + UnmanagedMethods.SetWindowPosFlags.SWP_HIDEWINDOW | + UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); + } + + public unsafe void ShowInBounds(TransformedBounds transformedBounds) + { + CheckDisposed(); + if (_attachedTo == null) + throw new InvalidOperationException("The control isn't currently attached to a toplevel"); + var bounds = transformedBounds.Bounds.TransformToAABB(transformedBounds.Transform) * + new Vector(_attachedTo.Window.Scaling, _attachedTo.Window.Scaling); + var pixelRect = new PixelRect((int)bounds.X, (int)bounds.Y, Math.Max(1, (int)bounds.Width), + Math.Max(1, (int)bounds.Height)); + + UnmanagedMethods.MoveWindow(_child.Handle, 0, 0, pixelRect.Width, pixelRect.Height, true); + UnmanagedMethods.SetWindowPos(_holder.Handle, IntPtr.Zero, pixelRect.X, pixelRect.Y, pixelRect.Width, + pixelRect.Height, + UnmanagedMethods.SetWindowPosFlags.SWP_SHOWWINDOW + | UnmanagedMethods.SetWindowPosFlags.SWP_NOZORDER + | UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); + + UnmanagedMethods.InvalidateRect(_attachedTo.Window.Handle.Handle, null, false); + } + } + + } +} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index e324febfd5..b7bb0e19ba 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -209,7 +209,7 @@ namespace Avalonia.Win32 return new WindowImpl(); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { var embedded = new EmbeddedWindowImpl(); embedded.Show(); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 50e71aeebe..391abdfc73 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Win32.Input; @@ -22,7 +23,8 @@ namespace Avalonia.Win32 uint timestamp = unchecked((uint)GetMessageTime()); RawInputEventArgs e = null; - + var shouldTakeFocus = false; + switch ((WindowsMessage)msg) { case WindowsMessage.WM_ACTIVATE: @@ -149,6 +151,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONDOWN: case WindowsMessage.WM_XBUTTONDOWN: { + shouldTakeFocus = ShouldTakeFocusOnClick; if (ShouldIgnoreTouchEmulatedMessage()) { break; @@ -177,6 +180,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONUP: case WindowsMessage.WM_XBUTTONUP: { + shouldTakeFocus = ShouldTakeFocusOnClick; if (ShouldIgnoreTouchEmulatedMessage()) { break; @@ -429,12 +433,18 @@ namespace Avalonia.Win32 (Screen as ScreenImpl)?.InvalidateScreensCache(); return IntPtr.Zero; } + case WindowsMessage.WM_KILLFOCUS: + LostFocus?.Invoke(); + break; } #if USE_MANAGED_DRAG if (_managedDrag.PreprocessInputEvent(ref e)) return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); #endif + + if (shouldTakeFocus) + SetFocus(_hwnd); if (e != null && Input != null) { @@ -519,5 +529,9 @@ namespace Avalonia.Win32 return modifiers; } + + public INativeControlHostImpl NativeControlHost => _nativeControlHost; + + protected virtual bool ShouldTakeFocusOnClick => true; } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 6024dc01da..36398eb810 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Media; @@ -18,7 +19,8 @@ namespace Avalonia.Win32 /// /// Window implementation for Win32 platform. /// - public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo + public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, + ITopLevelImplWithNativeControlHost { private static readonly List s_instances = new List(); @@ -52,6 +54,7 @@ namespace Avalonia.Win32 private readonly FramebufferManager _framebuffer; private readonly IGlPlatformSurface _gl; + private Win32NativeControlHost _nativeControlHost; private WndProc _wndProcDelegate; private string _className; private IntPtr _hwnd; @@ -101,6 +104,7 @@ namespace Avalonia.Win32 Screen = new ScreenImpl(); + _nativeControlHost = new Win32NativeControlHost(this); s_instances.Add(this); } @@ -123,6 +127,8 @@ namespace Avalonia.Win32 public Action PositionChanged { get; set; } public Action WindowStateChanged { get; set; } + + public Action LostFocus { get; set; } public Action TransparencyLevelChanged { get; set; } @@ -523,7 +529,7 @@ namespace Avalonia.Win32 0, atom, null, - (int)WindowStyles.WS_OVERLAPPEDWINDOW, + (int)WindowStyles.WS_OVERLAPPEDWINDOW | (int) WindowStyles.WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, diff --git a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs index 838bf49846..d299ff99c1 100644 --- a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs +++ b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs @@ -4,7 +4,7 @@ using Avalonia.Platform; namespace Avalonia.iOS { - class EmbeddableImpl : TopLevelImpl, IEmbeddableWindowImpl + class EmbeddableImpl : TopLevelImpl { public void SetTitle(string title) { @@ -23,11 +23,5 @@ namespace Avalonia.iOS public void SetSystemDecorations(SystemDecorations enabled) { } - - public event Action LostFocus - { - add {} - remove {} - } } } diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index c5b6642982..83a68990d7 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -139,5 +139,7 @@ namespace Avalonia.iOS public ILockedFramebuffer Lock() => new EmulatedFramebuffer(this); public IPopupImpl CreatePopup() => null; + + public Action LostFocus { get; set; } } } diff --git a/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs b/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs index 9a40b7162e..aaca5f53d6 100644 --- a/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs +++ b/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs @@ -10,7 +10,7 @@ namespace Avalonia.iOS throw new NotSupportedException(); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public WindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index df871a67b4..784046ac0b 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -578,6 +578,28 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.True(true); } + [Fact] + public void Should_Not_Throw_Exception_When_Enabling_Data_Validation_On_Missing_Member() + { + var source = new Class1(); + var target = new PropertyAccessorNode("NotFound", true); + + target.Target = new WeakReference(source); + + var result = new List(); + + target.Subscribe(x => result.Add(x)); + + Assert.Equal( + new object[] + { + new BindingNotification( + new MissingMemberException("Could not find a matching property accessor for 'NotFound' on 'Avalonia.Base.UnitTests.Data.Core.ExpressionObserverTests_Property+Class1'"), + BindingErrorType.Error), + }, + result); + } + private interface INext { int PropertyChangedSubscriptionCount { get; } diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs index a9c991b8b9..25e8c82b1a 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs @@ -20,11 +20,11 @@ namespace Avalonia.Controls.UnitTests return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index 01f0ec247f..1efbbed2e8 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -185,6 +185,35 @@ namespace Avalonia.Input.UnitTests Assert.Equal(next, result); } + [Fact] + public void Next_Skips_Non_TabStop_Siblings() + { + Button current; + Button next; + + var top = new StackPanel + { + Children = + { + new StackPanel + { + Children = + { + new Button { Name = "Button1" }, + new Button { Name = "Button2" }, + (current = new Button { Name = "Button3" }), + new Button { Name="Button4", [KeyboardNavigation.IsTabStopProperty] = false } + } + }, + (next = new Button { Name = "Button5" }), + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Equal(next, result); + } + [Fact] public void Next_Continue_Returns_First_Control_In_Next_Uncle_Container() { diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 9bb9fd7145..e3a8f9455f 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -451,6 +451,7 @@ namespace Avalonia.LeakTests AttachShowAndDetachContextMenu(window); + Mock.Get(window.PlatformImpl).ResetCalls(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); dotMemory.Check(memory => @@ -486,6 +487,7 @@ namespace Avalonia.LeakTests BuildAndShowContextMenu(window); BuildAndShowContextMenu(window); + Mock.Get(window.PlatformImpl).ResetCalls(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); dotMemory.Check(memory => diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index ee45433089..48a333dc54 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -118,7 +118,7 @@ namespace Avalonia.UnitTests } } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index 767111b89b..bfcc341eed 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -557,7 +557,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Should_Create_And_Delete_Layers_For_Controls_With_Animated_Opacity() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -599,7 +599,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Should_Not_Create_Layer_For_Childless_Control_With_Animated_Opacity() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -629,7 +629,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Should_Not_Push_Opacity_For_Transparent_Layer_Root_Control() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -658,7 +658,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Should_Draw_Transparent_Layer_With_Correct_Opacity() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index b0f890b484..d93e6c990e 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -810,7 +810,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; Assert.Equal(expected, scene.Layers[tree].Dirty.ToArray()); - Assert.Equal(expected, scene.Layers[border].Dirty.ToArray()); + + // Layers are disabled. See #2244 + // Assert.Equal(expected, scene.Layers[border].Dirty.ToArray()); } } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs index 1bece3ae22..33a0668b64 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs @@ -14,7 +14,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph { public partial class SceneBuilderTests { - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Control_With_Animated_Opacity_And_Children_Should_Start_New_Layer() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -118,7 +118,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Removing_Control_With_Animated_Opacity_Should_Remove_Layers() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -168,7 +168,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Hiding_Transparent_Control_Should_Remove_Layers() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -218,7 +218,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void GeometryClip_Should_Affect_Child_Layers() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))