diff --git a/Avalonia.sln b/Avalonia.sln index 7ad4a699ec..9917547a67 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -203,6 +203,8 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeEmbedSample", "samples\interop\NativeEmbedSample\NativeEmbedSample.csproj", "{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}" +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}" EndProject Global @@ -1920,6 +1922,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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -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 2ae456750d..11ef85e719 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -24,6 +24,8 @@ struct IAvnGlSurfaceRenderTarget; struct IAvnGlSurfaceRenderingSession; struct IAvnAppMenu; struct IAvnAppMenuItem; +struct IAvnNativeControlHost; +struct IAvnNativeControlHostTopLevelAttachment; struct AvnSize { @@ -215,6 +217,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; }; AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase @@ -250,6 +253,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; }; @@ -396,4 +400,22 @@ AVNCOM(IAvnAppMenuItem, 19) : IUnknown virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 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 50a85bdf9f..28bf88d8fb 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 */; }; 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; }; @@ -31,6 +33,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; }; 37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = ""; }; @@ -84,11 +88,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 */, @@ -188,6 +194,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 */, @@ -196,6 +203,7 @@ 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, + 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, AB661C202148286E00291242 /* window.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 5c50aad4cc..13429170d0 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -24,9 +24,43 @@ extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationA @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 d7eda20f65..dac6a98960 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -21,6 +21,7 @@ extern IAvnGlDisplay* GetGlDisplay(); extern IAvnAppMenu* CreateAppMenu(); extern IAvnAppMenuItem* CreateAppMenuItem(); extern IAvnAppMenuItem* CreateAppMenuItemSeperator(); +extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern void SetAppMenu (NSString* appName, IAvnAppMenu* appMenu); extern IAvnAppMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); @@ -51,4 +52,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 cd42ea0437..c9936fef01 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -393,6 +393,14 @@ public: *ppv = [renderTarget createSurfaceRenderTarget]; 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; + } protected: virtual NSWindowStyleMask GetStyle() @@ -787,9 +795,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); } - - (void)updateLayer { + AvnInsidePotentialDeadlock deadlock; if (_parent == nullptr) { return; @@ -852,7 +860,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent if([self ignoreUserInput]) return; - [self becomeFirstResponder]; auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; auto avnPoint = [self toAvnPoint:localPoint]; auto point = [self translateLocalPoint:avnPoint]; @@ -879,11 +886,27 @@ 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]; + _parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta); [super mouseMoved:event]; } +- (BOOL) resignFirstResponder +{ + _parent->BaseEvents->LostFocus(); + return YES; +} + - (void)mouseMoved:(NSEvent *)event { [self mouseEvent:event withType:Move]; 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..cfd13cf4e4 --- /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..5ac98d6ec2 --- /dev/null +++ b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj @@ -0,0 +1,29 @@ + + + + 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/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 883ad85dc8..566351c280 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; @@ -64,6 +65,9 @@ namespace Avalonia.Controls.Platform window.Deactivated += WindowDeactivated; } + if (_root is TopLevel tl && tl.PlatformImpl is IEmbeddableWindowImpl eimpl) + eimpl.LostFocus += TopLevelLostPlatformFocus; + _inputManagerSubscription = InputManager?.Process.Subscribe(RawInput); } @@ -93,6 +97,9 @@ namespace Avalonia.Controls.Platform { root.Deactivated -= WindowDeactivated; } + + if (_root is TopLevel tl && tl.PlatformImpl is IEmbeddableWindowImpl eimpl) + eimpl.LostFocus -= TopLevelLostPlatformFocus; _inputManagerSubscription?.Dispose(); @@ -401,6 +408,11 @@ namespace Avalonia.Controls.Platform { Menu?.Close(); } + + private void TopLevelLostPlatformFocus() + { + Menu?.Close(); + } protected void Click(IMenuItem item) { 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/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 77febf9384..03ce558c3a 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reactive.Disposables; +using Avalonia.Controls.Platform; using Avalonia.Controls.Presenters; using Avalonia.Data; using Avalonia.Input; @@ -13,6 +14,7 @@ using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Metadata; +using Avalonia.Platform; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -249,6 +251,8 @@ namespace Avalonia.Controls.Primitives if (window != null) { window.Deactivated += WindowDeactivated; + if (window.PlatformImpl is IEmbeddableWindowImpl reportsFocus) + reportsFocus.LostFocus += WindowLostFocus; } else { @@ -300,7 +304,11 @@ namespace Avalonia.Controls.Primitives _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside); var window = _topLevel as Window; if (window != null) + { window.Deactivated -= WindowDeactivated; + if (window.PlatformImpl is IEmbeddableWindowImpl reportsFocus) + reportsFocus.LostFocus -= WindowLostFocus; + } else { var parentPopuproot = _topLevel as PopupRoot; @@ -483,6 +491,12 @@ namespace Avalonia.Controls.Primitives } } + private void WindowLostFocus() + { + if(!StaysOpen) + Close(); + } + private IgnoreIsOpenScope BeginIgnoringIsOpen() { return new IgnoreIsOpenScope(this); diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index a0df186eb7..4a6c7718f9 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -130,6 +130,9 @@ namespace Avalonia.Controls nameof(IResourceProvider.ResourcesChanged), this); } + + if (impl is IEmbeddableWindowImpl embeddableWindowImpl) + embeddableWindowImpl.LostFocus += PlatformImpl_LostFocus; } /// @@ -365,5 +368,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, InputModifiers.None); + } } } 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 331f083e8b..ee171e45ef 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Input.Raw; @@ -45,7 +46,8 @@ namespace Avalonia.Native } public abstract class WindowBaseImpl : IWindowBaseImpl, - IFramebufferPlatformSurface + IFramebufferPlatformSurface, ITopLevelImplWithNativeControlHost, + IEmbeddableWindowImpl { IInputRoot _inputRoot; IAvnWindowBase _native; @@ -59,6 +61,7 @@ namespace Avalonia.Native private Size _lastRenderedLogicalSize; private double _savedScaling; private GlPlatformSurface _glSurface; + private NativeControlHostImpl _nativeControlHost; internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, GlPlatformFeature glFeature) { @@ -80,6 +83,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 +105,8 @@ namespace Avalonia.Native this }; + public INativeControlHostImpl NativeControlHost => _nativeControlHost; + public ILockedFramebuffer Lock() { var w = _savedLogicalSize.Width * _savedScaling; @@ -119,6 +125,8 @@ namespace Avalonia.Native }, (int)w, (int)h, new Vector(dpi, dpi)); } + public event Action LostFocus; + public Action Paint { get; set; } public Action Resized { get; set; } public Action Closed { get; set; } @@ -201,6 +209,11 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Render); } + + void IAvnWindowBaseEvents.LostFocus() + { + _parent.LostFocus?.Invoke(); + } } public void Activate() @@ -264,6 +277,9 @@ namespace Avalonia.Native _native?.Dispose(); _native = null; + _nativeControlHost?.Dispose(); + _nativeControlHost = null; + (Screen as ScreenImpl)?.Dispose(); } diff --git a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs index b2121aa8da..3f91202055 100644 --- a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs @@ -79,5 +79,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 2b5e36c693..694c8cd165 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -26,12 +26,15 @@ 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 void Initialize(X11PlatformOptions options) { Options = options; 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(); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index ff50ec9939..8406caab1c 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; @@ -179,6 +181,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 @@ -1055,5 +1058,6 @@ namespace Avalonia.X11 public IPopupPositioner PopupPositioner { get; } public ITopLevelNativeMenuExporter NativeMenuExporter { get; } + public INativeControlHostImpl NativeControlHost { get; } } } diff --git a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs index fe626f4d38..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, InputModifiers.None); - } - - private void PlatformImpl_LostFocus() - { - Unfocus(); - } - protected override void Dispose(bool disposing) { if (disposing) diff --git a/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs b/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs index 538a51c01e..c081c807e8 100644 --- a/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs +++ b/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs @@ -9,11 +9,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( @@ -25,66 +22,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 904e122382..f41efd91c0 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -18,7 +18,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); @@ -1003,6 +1003,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); @@ -1053,6 +1057,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 c9aa1ce4e7..576c771968 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -10,11 +10,31 @@ namespace Avalonia.Win32 { class PopupImpl : WindowImpl, IPopupImpl { + + // 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; + protected override IntPtr CreateWindowOverride(ushort atom) { UnmanagedMethods.WindowStyles style = @@ -34,10 +54,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; var classes = (int)UnmanagedMethods.GetClassLongPtr(result, (int)UnmanagedMethods.ClassLongIndex.GCL_STYLE); @@ -59,7 +80,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/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 1594a1f467..cb0001b509 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -7,6 +7,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; @@ -18,13 +19,16 @@ using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 { - public class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo + public class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, + ITopLevelImplWithNativeControlHost, + IEmbeddableWindowImpl { private static readonly List s_instances = new List(); private static readonly IntPtr DefaultCursor = UnmanagedMethods.LoadCursor( IntPtr.Zero, new IntPtr((int)UnmanagedMethods.Cursor.IDC_ARROW)); + private Win32NativeControlHost _nativeControlHost; private UnmanagedMethods.WndProc _wndProcDelegate; private string _className; private IntPtr _hwnd; @@ -71,6 +75,7 @@ namespace Avalonia.Win32 Win32GlManager.EglFeature.DeferredContext, this); s_instances.Add(this); + _nativeControlHost = new Win32NativeControlHost(this); } public Action Activated { get; set; } @@ -93,6 +98,8 @@ namespace Avalonia.Win32 public Action WindowStateChanged { get; set; } + public event Action LostFocus; + public Thickness BorderThickness { get @@ -414,7 +421,7 @@ namespace Avalonia.Win32 0, atom, null, - (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, + (int)(WindowStyles.WS_OVERLAPPEDWINDOW | WindowStyles.WS_CLIPCHILDREN), UnmanagedMethods.CW_USEDEFAULT, UnmanagedMethods.CW_USEDEFAULT, UnmanagedMethods.CW_USEDEFAULT, @@ -437,12 +444,11 @@ namespace Avalonia.Win32 [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")] protected virtual unsafe IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { - bool unicode = UnmanagedMethods.IsWindowUnicode(hWnd); - const double wheelDelta = 120.0; uint timestamp = unchecked((uint)UnmanagedMethods.GetMessageTime()); RawInputEventArgs e = null; + var shouldTakeFocus = false; switch ((UnmanagedMethods.WindowsMessage)msg) { @@ -545,6 +551,7 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN: + shouldTakeFocus = ShouldTakeFocusOnClick; if(ShouldIgnoreTouchEmulatedMessage()) break; e = new RawPointerEventArgs( @@ -632,6 +639,7 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN: + shouldTakeFocus = ShouldTakeFocusOnClick; e = new RawPointerEventArgs( _mouseDevice, timestamp, @@ -751,6 +759,10 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_DISPLAYCHANGE: (Screen as ScreenImpl)?.InvalidateScreensCache(); return IntPtr.Zero; + + case UnmanagedMethods.WindowsMessage.WM_KILLFOCUS: + LostFocus?.Invoke(); + break; } #if USE_MANAGED_DRAG @@ -758,6 +770,8 @@ namespace Avalonia.Win32 if (_managedDrag.PreprocessInputEvent(ref e)) return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); #endif + if (shouldTakeFocus) + SetFocus(_hwnd); if (e != null && Input != null) { @@ -1061,5 +1075,8 @@ namespace Avalonia.Win32 } } IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + public INativeControlHostImpl NativeControlHost => _nativeControlHost; + + protected virtual bool ShouldTakeFocusOnClick => true; } }