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 abfae3cf1e..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; @@ -1107,7 +1115,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _lastPixelSize.Width = (int)fsize.width; _lastPixelSize.Height = (int)fsize.height; [self updateRenderTarget]; - _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + + if(_parent != nullptr) + { + _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + } [super viewDidChangeBackingProperties]; } @@ -1138,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]; @@ -1165,11 +1176,31 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent auto timestamp = [event timestamp] * 1000; auto modifiers = [self getModifiers:[event modifierFlags]]; - [self becomeFirstResponder]; - _parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta); + if(type != AvnRawMouseEventType::Move || + ( + [self window] != nil && + ( + [[self window] firstResponder] == nil + || ![[[self window] firstResponder] isKindOfClass: [NSView class]] + ) + ) + ) + [self becomeFirstResponder]; + + if(_parent != nullptr) + { + _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]; @@ -1290,7 +1321,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent auto timestamp = [event timestamp] * 1000; auto modifiers = [self getModifiers:[event modifierFlags]]; - _lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key); + if(_parent != nullptr) + { + _lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key); + } } - (BOOL)performKeyEquivalent:(NSEvent *)event @@ -1381,7 +1415,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if(!_lastKeyHandled) { - _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); + if(_parent != nullptr) + { + _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); + } } } @@ -1677,7 +1714,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { [self showWindowMenuWithAppMenu]; - _parent->BaseEvents->Activated(); + if(_parent != nullptr) + { + _parent->BaseEvents->Activated(); + } + [super becomeKeyWindow]; } } @@ -1794,8 +1835,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)windowDidMove:(NSNotification *)notification { AvnPoint position; - _parent->GetPosition(&position); - _parent->BaseEvents->PositionChanged(position); + + if(_parent != nullptr) + { + _parent->GetPosition(&position); + _parent->BaseEvents->PositionChanged(position); + } } @end 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/readme.md b/readme.md index 9d317cdd06..a9263d4816 100644 --- a/readme.md +++ b/readme.md @@ -31,7 +31,9 @@ Install-Package Avalonia.Desktop Examples of UIs built with Avalonia ![image](https://user-images.githubusercontent.com/4672627/84707589-5b69a880-af35-11ea-87a6-7ad57a31d314.png) -![image](https://user-images.githubusercontent.com/4672627/84708576-28281900-af37-11ea-8c88-e29dfcfa0558.png) +![image](https://user-images.githubusercontent.com/4672627/85069644-d8419000-b18a-11ea-8732-be9055bb61fd.PNG) + +![image](https://user-images.githubusercontent.com/4672627/85069659-dc6dad80-b18a-11ea-8375-39ef95315b5c.PNG) ![image](https://user-images.githubusercontent.com/4672627/84708947-c3b98980-af37-11ea-8c9d-503334615bbf.png) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index aaceb2373c..ada8557d9f 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -53,6 +53,7 @@ + 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/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 028a294492..e600e644af 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -16,6 +16,8 @@ + + - + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index 6fa9fc515e..cce80a2d3c 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -5,23 +5,32 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { public class ItemsRepeaterPage : UserControl { + private readonly ItemsRepeaterPageViewModel _viewModel; private ItemsRepeater _repeater; private ScrollViewer _scroller; + private Button _scrollToLast; + private Button _scrollToRandom; + private Random _random = new Random(0); public ItemsRepeaterPage() { this.InitializeComponent(); _repeater = this.FindControl("repeater"); _scroller = this.FindControl("scroller"); + _scrollToLast = this.FindControl + + + + + + + + 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.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 06a1cd4ae5..2a92e75f58 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -206,39 +206,6 @@ namespace Avalonia.Utilities } } - /// - /// Calculates the value to be used for layout rounding at high DPI. - /// - /// Input value to be rounded. - /// Ratio of screen's DPI to layout DPI - /// Adjusted value that will produce layout rounding on screen at high dpi. - /// This is a layout helper method. It takes DPI into account and also does not return - /// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper associated with - /// UseLayoutRounding property and should not be used as a general rounding utility. - public static double RoundLayoutValue(double value, double dpiScale) - { - double newValue; - - // If DPI == 1, don't use DPI-aware rounding. - if (!MathUtilities.IsOne(dpiScale)) - { - newValue = Math.Round(value * dpiScale) / dpiScale; - // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. - if (double.IsNaN(newValue) || - double.IsInfinity(newValue) || - MathUtilities.AreClose(newValue, double.MaxValue)) - { - newValue = value; - } - } - else - { - newValue = Math.Round(value); - } - - return newValue; - } - /// /// Clamps a value between a minimum and maximum value. /// diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 2d48a7d33b..5ff0fd1feb 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,16 +19,13 @@ namespace Avalonia.Controls.Embedding { } - [CanBeNull] - public new IEmbeddableWindowImpl PlatformImpl => (IEmbeddableWindowImpl) base.PlatformImpl; - protected bool EnforceClientSize { get; set; } = true; public void Prepare() { EnsureInitialized(); ApplyTemplate(); - LayoutManager.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(); } private void EnsureInitialized() diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index d326ab5734..b037dd9901 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls.Embedding.Offscreen { EnsureInitialized(); ApplyTemplate(); - LayoutManager.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(); } private void EnsureInitialized() 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/Grid.cs b/src/Avalonia.Controls/Grid.cs index e10d78917e..6357ec98a8 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -8,10 +8,8 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; -using System.Linq; using System.Threading; -using Avalonia; -using Avalonia.Collections; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -2103,7 +2101,7 @@ namespace Avalonia.Controls for (int i = 0; i < definitions.Count; ++i) { DefinitionBase def = definitions[i]; - double roundedSize = MathUtilities.RoundLayoutValue(def.SizeCache, dpi); + double roundedSize = LayoutHelper.RoundLayoutValue(def.SizeCache, dpi); roundingErrors[i] = (roundedSize - def.SizeCache); def.SizeCache = roundedSize; roundedTakenSize += roundedSize; diff --git a/src/Avalonia.Controls/IScrollAnchorProvider.cs b/src/Avalonia.Controls/IScrollAnchorProvider.cs index 6b5cb2ee25..93f3a0abb8 100644 --- a/src/Avalonia.Controls/IScrollAnchorProvider.cs +++ b/src/Avalonia.Controls/IScrollAnchorProvider.cs @@ -1,9 +1,29 @@ namespace Avalonia.Controls { + /// + /// Specifies a contract for a scrolling control that supports scroll anchoring. + /// public interface IScrollAnchorProvider { + /// + /// The currently chosen anchor element to use for scroll anchoring. + /// IControl CurrentAnchor { get; } + + /// + /// Registers a control as a potential scroll anchor candidate. + /// + /// + /// A control within the subtree of the . + /// void RegisterAnchorCandidate(IControl element); + + /// + /// Unregisters a control as a potential scroll anchor candidate. + /// + /// + /// A control within the subtree of the . + /// void UnregisterAnchorCandidate(IControl element); } } 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 e61e88c22b..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(); @@ -333,6 +340,10 @@ namespace Avalonia.Controls.Platform { item.Parent.SelectedItem = null; } + else if (!item.IsPointerOverSubMenu) + { + item.IsSubMenuOpen = false; + } } } @@ -405,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/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 98a5b10023..327cfb7736 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -3,10 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Runtime.InteropServices.ComTypes; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.LogicalTree; using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters @@ -14,7 +12,7 @@ namespace Avalonia.Controls.Presenters /// /// Presents a scrolling view of content inside a . /// - public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable + public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider { /// /// Defines the property. @@ -58,13 +56,19 @@ namespace Avalonia.Controls.Presenters o => o.Viewport, (o, v) => o.Viewport = v); + // Arbitrary chosen value, probably need to ask ILogicalScrollable + private const int LogicalScrollItemSize = 50; + private bool _canHorizontallyScroll; private bool _canVerticallyScroll; + private bool _arranging; private Size _extent; private Vector _offset; private IDisposable _logicalScrollSubscription; private Size _viewport; private Dictionary _activeLogicalGestureScrolls; + private List _anchorCandidates; + private (IControl control, Rect bounds) _anchor; /// /// Initializes static members of the class. @@ -73,7 +77,6 @@ namespace Avalonia.Controls.Presenters { ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); - AffectsArrange(OffsetProperty); } /// @@ -87,6 +90,8 @@ namespace Avalonia.Controls.Presenters this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } + internal event EventHandler PreArrange; + /// /// Gets or sets a value indicating whether the content can be scrolled horizontally. /// @@ -120,7 +125,7 @@ namespace Avalonia.Controls.Presenters public Vector Offset { get { return _offset; } - set { SetAndRaise(OffsetProperty, ref _offset, value); } + set { SetAndRaise(OffsetProperty, ref _offset, ScrollViewer.CoerceOffset(Extent, Viewport, value)); } } /// @@ -132,6 +137,9 @@ namespace Avalonia.Controls.Presenters private set { SetAndRaise(ViewportProperty, ref _viewport, value); } } + /// + IControl IScrollAnchorProvider.CurrentAnchor => _anchor.control; + /// /// Attempts to bring a portion of the target visual into view by scrolling the content. /// @@ -196,6 +204,30 @@ namespace Avalonia.Controls.Presenters return result; } + /// + void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element) + { + if (!this.IsVisualAncestorOf(element)) + { + throw new InvalidOperationException( + "An anchor control must be a visual descendent of the ScrollContentPresenter."); + } + + _anchorCandidates ??= new List(); + _anchorCandidates.Add(element); + } + + /// + void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element) + { + _anchorCandidates?.Remove(element); + + if (_anchor.control == element) + { + _anchor = default; + } + } + /// protected override Size MeasureOverride(Size availableSize) { @@ -215,22 +247,85 @@ namespace Avalonia.Controls.Presenters /// protected override Size ArrangeOverride(Size finalSize) { + PreArrange?.Invoke(this, new VectorEventArgs + { + Vector = new Vector(finalSize.Width, finalSize.Height), + }); + if (_logicalScrollSubscription != null || Child == null) { return base.ArrangeOverride(finalSize); } + try + { + _arranging = true; + return ArrangeWithAnchoring(finalSize); + } + finally + { + _arranging = false; + } + } + + private Size ArrangeWithAnchoring(Size finalSize) + { var size = new Size( CanHorizontallyScroll ? Math.Max(Child.DesiredSize.Width, finalSize.Width) : finalSize.Width, CanVerticallyScroll ? Math.Max(Child.DesiredSize.Height, finalSize.Height) : finalSize.Height); + + Vector TrackAnchor() + { + // If we have an anchor and its position relative to Child has changed during the + // arrange then that change wasn't just due to scrolling (as scrolling doesn't adjust + // relative positions within Child). + if (_anchor.control != null && + TranslateBounds(_anchor.control, Child, out var updatedBounds) && + updatedBounds.Position != _anchor.bounds.Position) + { + var offset = updatedBounds.Position - _anchor.bounds.Position; + return offset; + } + + return default; + } + + // Calculate the new anchor element. + _anchor = CalculateCurrentAnchor(); + + // Do the arrange. ArrangeOverrideImpl(size, -Offset); + + // If the anchor moved during the arrange, we need to adjust the offset and do another arrange. + var anchorShift = TrackAnchor(); + + if (anchorShift != default) + { + var newOffset = Offset + anchorShift; + var newExtent = Extent; + var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height); + + if (newOffset.X > maxOffset.X) + { + newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width); + } + + if (newOffset.Y > maxOffset.Y) + { + newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height); + } + + Extent = newExtent; + Offset = newOffset; + ArrangeOverrideImpl(size, -Offset); + } + Viewport = finalSize; Extent = Child.Bounds.Size.Inflate(Child.Margin); + return finalSize; } - // Arbitrary chosen value, probably need to ask ILogicalScrollable - private const int LogicalScrollItemSize = 50; private void OnScrollGesture(object sender, ScrollGestureEventArgs e) { if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width) @@ -327,6 +422,16 @@ namespace Avalonia.Controls.Presenters } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == OffsetProperty && !_arranging) + { + InvalidateArrange(); + } + + base.OnPropertyChanged(change); + } + private void BringIntoViewRequested(object sender, RequestBringIntoViewEventArgs e) { e.Handled = BringDescendantIntoView(e.TargetObject, e.TargetRect); @@ -390,5 +495,84 @@ namespace Avalonia.Controls.Presenters Offset = scrollable.Offset; } } + + private (IControl, Rect) CalculateCurrentAnchor() + { + if (_anchorCandidates == null) + { + return default; + } + + var bestCandidate = default(IControl); + var bestCandidateDistance = double.MaxValue; + + // Find the anchor candidate that is scrolled closest to the top-left of this + // ScrollContentPresenter. + foreach (var element in _anchorCandidates) + { + if (element.IsVisible && GetViewportBounds(element, out var bounds)) + { + var distance = (Vector)bounds.Position; + var candidateDistance = Math.Abs(distance.Length); + + if (candidateDistance < bestCandidateDistance) + { + bestCandidate = element; + bestCandidateDistance = candidateDistance; + } + } + } + + if (bestCandidate != null) + { + // We have a candidate, calculate its bounds relative to Child. Because these + // bounds aren't relative to the ScrollContentPresenter itself, if they change + // then we know it wasn't just due to scrolling. + var unscrolledBounds = TranslateBounds(bestCandidate, Child); + return (bestCandidate, unscrolledBounds); + } + + return default; + } + + private bool GetViewportBounds(IControl element, out Rect bounds) + { + if (TranslateBounds(element, Child, out var childBounds)) + { + // We want the bounds relative to the new Offset, regardless of whether the child + // control has actually been arranged to this offset yet, so translate first to the + // child control and then apply Offset rather than translating directly to this + // control. + var thisBounds = new Rect(Bounds.Size); + bounds = new Rect(childBounds.Position - Offset, childBounds.Size); + return bounds.Intersects(thisBounds); + } + + bounds = default; + return false; + } + + private Rect TranslateBounds(IControl control, IControl to) + { + if (TranslateBounds(control, to, out var bounds)) + { + return bounds; + } + + throw new InvalidOperationException("The control's bounds could not be translated to the requested control."); + } + + private bool TranslateBounds(IControl control, IControl to, out Rect bounds) + { + if (!control.IsVisible) + { + bounds = default; + return false; + } + + var p = control.TranslatePoint(default, to); + bounds = p.HasValue ? new Rect(p.Value, control.Bounds.Size) : default; + return p.HasValue; + } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs index 7a43d38d8a..e55bdbb0eb 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs @@ -1,8 +1,9 @@ -using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; +#nullable enable + namespace Avalonia.Controls.Primitives { /// @@ -13,36 +14,36 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty HeaderProperty = - AvaloniaProperty.Register(nameof(Header)); + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); /// /// Defines the property. /// - public static readonly StyledProperty HeaderTemplateProperty = - AvaloniaProperty.Register(nameof(HeaderTemplate)); + public static readonly StyledProperty HeaderTemplateProperty = + AvaloniaProperty.Register(nameof(HeaderTemplate)); /// /// Initializes static members of the class. /// static HeaderedContentControl() { - ContentProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); + HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); } /// /// Gets or sets the header content. /// - public object Header + public object? Header { - get { return GetValue(HeaderProperty); } - set { SetValue(HeaderProperty, value); } + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); } /// /// Gets the header presenter from the control's template. /// - public IContentPresenter HeaderPresenter + public IContentPresenter? HeaderPresenter { get; private set; @@ -51,10 +52,10 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the data template used to display the header content of the control. /// - public IDataTemplate HeaderTemplate + public IDataTemplate? HeaderTemplate { - get { return GetValue(HeaderTemplateProperty); } - set { SetValue(HeaderTemplateProperty, value); } + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); } /// 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/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index f6186a29a9..fc82fcc7a7 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -3,6 +3,7 @@ using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.Threading; namespace Avalonia.Controls.Primitives { @@ -40,10 +41,26 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); + /// + /// Defines the property. + /// + public static readonly DirectProperty IsExpandedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsExpanded), + o => o.IsExpanded); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AllowAutoHideProperty = + AvaloniaProperty.Register(nameof(AllowAutoHide), true); + private Button _lineUpButton; private Button _lineDownButton; private Button _pageUpButton; private Button _pageDownButton; + private DispatcherTimer _timer; + private bool _isExpanded; /// /// Initializes static members of the class. @@ -90,6 +107,24 @@ namespace Avalonia.Controls.Primitives set { SetValue(OrientationProperty, value); } } + /// + /// Gets a value that indicates whether the scrollbar is expanded. + /// + public bool IsExpanded + { + get => _isExpanded; + private set => SetAndRaise(IsExpandedProperty, ref _isExpanded, value); + } + + /// + /// Gets a value that indicates whether the scrollbar can hide itself when user is not interacting with it. + /// + public bool AllowAutoHide + { + get => GetValue(AllowAutoHideProperty); + set => SetValue(AllowAutoHideProperty, value); + } + public event EventHandler Scroll; /// @@ -131,6 +166,10 @@ namespace Avalonia.Controls.Primitives { UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } + else if (change.Property == AllowAutoHideProperty) + { + UpdateIsExpandedState(); + } else { if (change.Property == MinimumProperty || @@ -143,6 +182,26 @@ namespace Avalonia.Controls.Primitives } } + protected override void OnPointerEnter(PointerEventArgs e) + { + base.OnPointerEnter(e); + + if (AllowAutoHide) + { + ExpandAfterDelay(); + } + } + + protected override void OnPointerLeave(PointerEventArgs e) + { + base.OnPointerLeave(e); + + if (AllowAutoHide) + { + CollapseAfterDelay(); + } + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { if (_lineUpButton != null) @@ -170,8 +229,6 @@ namespace Avalonia.Controls.Primitives _pageUpButton = e.NameScope.Find