From 7b9d32af85aa08580380ce81d0a999db727a3240 Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Sun, 9 Feb 2020 15:50:42 +0000 Subject: [PATCH 01/55] Rework system decorations --- native/Avalonia.Native/inc/avalonia-native.h | 2 +- native/Avalonia.Native/src/OSX/window.mm | 41 +++++++++++++---- samples/ControlCatalog/MainView.xaml | 11 ++++- samples/ControlCatalog/MainView.xaml.cs | 14 ++++++ src/Avalonia.Controls/Platform/IWindowImpl.cs | 2 +- src/Avalonia.Controls/Window.cs | 44 ++++++++++++++++++- .../Remote/PreviewerWindowImpl.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Native/WindowImpl.cs | 4 +- src/Avalonia.X11/X11Window.cs | 16 ++++--- .../Interop/UnmanagedMethods.cs | 10 +++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 20 +++++---- src/iOS/Avalonia.iOS/EmbeddableImpl.cs | 2 +- 13 files changed, 137 insertions(+), 33 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 4a960d47a1..ce4a592d67 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -236,7 +236,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; - virtual HRESULT SetHasDecorations(bool value) = 0; + virtual HRESULT SetHasDecorations(int value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index b6ce172ffa..317d03162b 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -115,7 +115,6 @@ public: [NSApp activateIgnoringOtherApps:YES]; [Window setTitle:_lastTitle]; - [Window setTitleVisibility:NSWindowTitleVisible]; return S_OK; } @@ -411,7 +410,7 @@ class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, pub { private: bool _canResize = true; - bool _hasDecorations = true; + int _hasDecorations = 2; CGRect _lastUndecoratedFrame; AvnWindowState _lastWindowState; @@ -476,12 +475,12 @@ private: bool IsZoomed () { - return _hasDecorations ? [Window isZoomed] : UndecoratedIsMaximized(); + return _hasDecorations > 0 ? [Window isZoomed] : UndecoratedIsMaximized(); } void DoZoom() { - if (_hasDecorations) + if (_hasDecorations > 0) { [Window performZoom:Window]; } @@ -506,13 +505,36 @@ private: } } - virtual HRESULT SetHasDecorations(bool value) override + virtual HRESULT SetHasDecorations(int value) override { @autoreleasepool { _hasDecorations = value; UpdateStyle(); + // full + if (_hasDecorations == 2) + { + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + } + // border only + else if (_hasDecorations == 1) + { + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + } + // none + else + { + [Window setHasShadow:NO]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + } + return S_OK; } } @@ -523,7 +545,6 @@ private: { _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; [Window setTitle:_lastTitle]; - [Window setTitleVisibility:NSWindowTitleVisible]; return S_OK; } @@ -645,9 +666,11 @@ protected: virtual NSWindowStyleMask GetStyle() override { unsigned long s = NSWindowStyleMaskBorderless; - if(_hasDecorations) - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; - if(_canResize) + if(_hasDecorations == 1) + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + if(_hasDecorations == 2) + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + if(_hasDecorations == 2 && _canResize) s = s | NSWindowStyleMaskResizable; return s; } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index cbe2c62890..1c2653f73a 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -58,10 +58,17 @@ - + + + No Decorations + Border Only + Full Decorations + + Light Dark - + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index acb9bc5bc6..5f71b2ecd9 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -56,6 +56,20 @@ namespace ControlCatalog } }; Styles.Add(light); + + var decorations = this.Find("Decorations"); + decorations.SelectionChanged += (sender, e) => + { + Window window = (Window)VisualRoot; + window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex; + }; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + var decorations = this.Find("Decorations"); + decorations.SelectedIndex = (int)((Window)VisualRoot).SystemDecorations; } } } diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 91b895f38a..238070bbef 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -36,7 +36,7 @@ namespace Avalonia.Platform /// /// Enables or disables system window decorations (title bar, buttons, etc) /// - void SetSystemDecorations(bool enabled); + void SetSystemDecorations(SystemDecorations enabled); /// /// Sets the icon of this window. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index f66a248aaf..853347bf41 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -45,6 +45,27 @@ namespace Avalonia.Controls WidthAndHeight = 3, } + /// + /// Determines system decorations (title bar, border, etc) for a + /// + public enum SystemDecorations + { + /// + /// No decorations + /// + None = 0, + + /// + /// Window border without titlebar + /// + BorderOnly = 1, + + /// + /// Fully decorated (default) + /// + Full = 2 + } + /// /// A top-level window. /// @@ -59,9 +80,16 @@ namespace Avalonia.Controls /// /// Enables or disables system window decorations (title bar, buttons, etc) /// + [Obsolete("Use SystemDecorationsProperty instead")] public static readonly StyledProperty HasSystemDecorationsProperty = AvaloniaProperty.Register(nameof(HasSystemDecorations), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty SystemDecorationsProperty = + AvaloniaProperty.Register(nameof(SystemDecorations), SystemDecorations.Full); + /// /// Enables or disables the taskbar icon /// @@ -125,7 +153,9 @@ namespace Avalonia.Controls BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue)); + (s, e) => s.PlatformImpl?.SetSystemDecorations(((bool)e.NewValue) ? SystemDecorations.Full : SystemDecorations.None)); + SystemDecorationsProperty.Changed.AddClassHandler( + (s, e) => s.PlatformImpl?.SetSystemDecorations((SystemDecorations)e.NewValue)); ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); @@ -140,7 +170,6 @@ namespace Avalonia.Controls MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); - } /// @@ -192,12 +221,23 @@ namespace Avalonia.Controls /// Enables or disables system window decorations (title bar, buttons, etc) /// /// + [Obsolete("Use SystemDecorations instead")] public bool HasSystemDecorations { get { return GetValue(HasSystemDecorationsProperty); } set { SetValue(HasSystemDecorationsProperty, value); } } + /// + /// Sets the system decorations (title bar, border, etc) + /// + /// + public SystemDecorations SystemDecorations + { + get { return GetValue(SystemDecorationsProperty); } + set { SetValue(SystemDecorationsProperty, value); } + } + /// /// Enables or disables the taskbar icon /// diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 86e34ca6d4..7480b3519c 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -96,7 +96,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 4bba5ef41b..7bf1d236bd 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -110,7 +110,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index c757576017..a540b026fa 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -68,9 +68,9 @@ namespace Avalonia.Native _native.CanResize = value; } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { - _native.HasDecorations = enabled; + _native.HasDecorations = (int)enabled; } public void SetTitleBarColor (Avalonia.Media.Color color) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 919abae243..b091ee212f 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -173,6 +173,7 @@ namespace Avalonia.X11 Surfaces = surfaces.ToArray(); UpdateMotifHints(); + UpdateSizeHints(null); _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing, XNames.XNClientWindow, _handle, IntPtr.Zero); XFlush(_x11.Display); @@ -219,12 +220,16 @@ namespace Avalonia.X11 var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border | MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH; - if (_popup || !_systemDecorations) + if (_popup || _systemDecorations == SystemDecorations.None) { decorations = 0; } + else if (_systemDecorations == SystemDecorations.BorderOnly) + { + decorations = MotifDecorations.Border; + } - if (!_canResize) + if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) { functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize); decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH); @@ -247,7 +252,7 @@ namespace Avalonia.X11 var min = _minMaxSize.minSize; var max = _minMaxSize.maxSize; - if (!_canResize) + if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) max = min = _realSize; if (preResize.HasValue) @@ -621,7 +626,7 @@ namespace Avalonia.X11 return rv; } - private bool _systemDecorations = true; + private SystemDecorations _systemDecorations = SystemDecorations.Full; private bool _canResize = true; private const int MaxWindowDimension = 100000; @@ -777,10 +782,11 @@ namespace Avalonia.X11 (int)(point.X * Scaling + Position.X), (int)(point.Y * Scaling + Position.Y)); - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { _systemDecorations = enabled; UpdateMotifHints(); + UpdateSizeHints(null); } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 904e122382..50b568cab2 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1298,7 +1298,17 @@ namespace Avalonia.Win32.Interop [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)] internal static extern void DoDragDrop(IOleDataObject dataObject, IDropSource dropSource, int allowedEffects, out int finalEffect); + [DllImport("dwmapi.dll")] + public static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); + [StructLayout(LayoutKind.Sequential)] + internal struct MARGINS + { + public int cxLeftWidth; + public int cxRightWidth; + public int cyTopHeight; + public int cyBottomHeight; + } public enum MONITOR { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index c16b76b539..d13c07279c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -34,7 +34,7 @@ namespace Avalonia.Win32 private IInputRoot _owner; private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock(); private bool _trackingMouse; - private bool _decorated = true; + private SystemDecorations _decorated = SystemDecorations.Full; private bool _resizable = true; private bool _topmost = false; private bool _taskbarIcon = true; @@ -97,7 +97,7 @@ namespace Avalonia.Win32 { get { - if (_decorated) + if (_decorated == SystemDecorations.Full) { var style = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); var exStyle = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE); @@ -281,7 +281,7 @@ namespace Avalonia.Win32 UnmanagedMethods.ShowWindow(_hwnd, UnmanagedMethods.ShowWindowCommand.Hide); } - public void SetSystemDecorations(bool value) + public void SetSystemDecorations(SystemDecorations value) { if (value == _decorated) { @@ -464,7 +464,7 @@ namespace Avalonia.Win32 return IntPtr.Zero; case WindowsMessage.WM_NCCALCSIZE: - if (ToInt32(wParam) == 1 && !_decorated) + if (ToInt32(wParam) == 1 && _decorated != SystemDecorations.Full) { return IntPtr.Zero; } @@ -682,14 +682,14 @@ namespace Avalonia.Win32 break; case WindowsMessage.WM_NCPAINT: - if (!_decorated) + if (_decorated != SystemDecorations.Full) { return IntPtr.Zero; } break; case WindowsMessage.WM_NCACTIVATE: - if (!_decorated) + if (_decorated != SystemDecorations.Full) { return new IntPtr(1); } @@ -1001,7 +1001,7 @@ namespace Avalonia.Win32 style |= WindowStyles.WS_OVERLAPPEDWINDOW; - if (!_decorated) + if (_decorated != SystemDecorations.Full) { style ^= (WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU); } @@ -1011,6 +1011,10 @@ namespace Avalonia.Win32 style ^= (WindowStyles.WS_SIZEFRAME); } + MARGINS margins = new MARGINS(); + margins.cyBottomHeight = _decorated == SystemDecorations.BorderOnly ? 1 : 0; + UnmanagedMethods.DwmExtendFrameIntoClientArea(_hwnd, ref margins); + GetClientRect(_hwnd, out var oldClientRect); var oldClientRectOrigin = new UnmanagedMethods.POINT(); ClientToScreen(_hwnd, ref oldClientRectOrigin); @@ -1024,7 +1028,7 @@ namespace Avalonia.Win32 if (oldDecorated != _decorated) { var newRect = oldClientRect; - if (_decorated) + if (_decorated == SystemDecorations.Full) AdjustWindowRectEx(ref newRect, (uint)style, false, GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE)); SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height, diff --git a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs index 65a6c15971..838bf49846 100644 --- a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs +++ b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs @@ -20,7 +20,7 @@ namespace Avalonia.iOS return Disposable.Empty; } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } From d44ad423a0b2fec410a3917dcfb27d2187e9c8b4 Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Mon, 10 Feb 2020 09:08:33 +0000 Subject: [PATCH 02/55] Use enum in macOS native --- native/Avalonia.Native/src/OSX/window.h | 6 ++ native/Avalonia.Native/src/OSX/window.mm | 100 +++++++++++++---------- 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 3e626675d2..23e3c22db7 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -36,4 +36,10 @@ struct IWindowStateChanged virtual void WindowStateChanged () = 0; }; +typedef NS_ENUM(NSInteger, SystemDecorations) { + SystemDecorationsNone = 0, + SystemDecorationsBorderOnly = 1, + SystemDecorationsFull = 2, +}; + #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 317d03162b..4c70f661b7 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -410,7 +410,7 @@ class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, pub { private: bool _canResize = true; - int _hasDecorations = 2; + SystemDecorations _hasDecorations = SystemDecorationsFull; CGRect _lastUndecoratedFrame; AvnWindowState _lastWindowState; @@ -475,23 +475,26 @@ private: bool IsZoomed () { - return _hasDecorations > 0 ? [Window isZoomed] : UndecoratedIsMaximized(); + return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized(); } void DoZoom() { - if (_hasDecorations > 0) + switch (_hasDecorations) { - [Window performZoom:Window]; - } - else - { - if (!UndecoratedIsMaximized()) - { - _lastUndecoratedFrame = [Window frame]; - } - - [Window zoom:Window]; + case SystemDecorationsNone: + if (!UndecoratedIsMaximized()) + { + _lastUndecoratedFrame = [Window frame]; + } + + [Window zoom:Window]; + break; + + case SystemDecorationsBorderOnly: + case SystemDecorationsFull: + [Window performZoom:Window]; + break; } } @@ -509,32 +512,31 @@ private: { @autoreleasepool { - _hasDecorations = value; + _hasDecorations = (SystemDecorations)value; UpdateStyle(); - - // full - if (_hasDecorations == 2) - { - [Window setHasShadow:YES]; - [Window setTitleVisibility:NSWindowTitleVisible]; - [Window setTitlebarAppearsTransparent:NO]; - [Window setTitle:_lastTitle]; - } - // border only - else if (_hasDecorations == 1) - { - [Window setHasShadow:YES]; - [Window setTitleVisibility:NSWindowTitleHidden]; - [Window setTitlebarAppearsTransparent:YES]; - } - // none - else + + switch (_hasDecorations) { - [Window setHasShadow:NO]; - [Window setTitleVisibility:NSWindowTitleHidden]; - [Window setTitlebarAppearsTransparent:YES]; + case SystemDecorationsNone: + [Window setHasShadow:NO]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + break; + + case SystemDecorationsBorderOnly: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + break; + + case SystemDecorationsFull: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + break; } - + return S_OK; } } @@ -666,12 +668,26 @@ protected: virtual NSWindowStyleMask GetStyle() override { unsigned long s = NSWindowStyleMaskBorderless; - if(_hasDecorations == 1) - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; - if(_hasDecorations == 2) - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; - if(_hasDecorations == 2 && _canResize) - s = s | NSWindowStyleMaskResizable; + + switch (_hasDecorations) + { + case SystemDecorationsNone: + break; + + case SystemDecorationsBorderOnly: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsFull: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + if(_canResize) + { + s = s | NSWindowStyleMaskResizable; + } + + break; + } + return s; } }; From c0e337d61f1df5a8b53135b1179c449b3f6544dc Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Mon, 10 Feb 2020 09:44:20 +0000 Subject: [PATCH 03/55] Use enum in mac interop --- native/Avalonia.Native/inc/avalonia-native.h | 8 +++++++- native/Avalonia.Native/src/OSX/window.h | 6 ------ native/Avalonia.Native/src/OSX/window.mm | 4 ++-- src/Avalonia.Native/WindowImpl.cs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index ce4a592d67..ee57f54e59 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -25,6 +25,12 @@ struct IAvnGlSurfaceRenderingSession; struct IAvnAppMenu; struct IAvnAppMenuItem; +enum SystemDecorations { + SystemDecorationsNone = 0, + SystemDecorationsBorderOnly = 1, + SystemDecorationsFull = 2, +}; + struct AvnSize { double Width, Height; @@ -236,7 +242,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; - virtual HRESULT SetHasDecorations(int value) = 0; + virtual HRESULT SetHasDecorations(SystemDecorations value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 23e3c22db7..3e626675d2 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -36,10 +36,4 @@ struct IWindowStateChanged virtual void WindowStateChanged () = 0; }; -typedef NS_ENUM(NSInteger, SystemDecorations) { - SystemDecorationsNone = 0, - SystemDecorationsBorderOnly = 1, - SystemDecorationsFull = 2, -}; - #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 4c70f661b7..2c03407732 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -508,11 +508,11 @@ private: } } - virtual HRESULT SetHasDecorations(int value) override + virtual HRESULT SetHasDecorations(SystemDecorations value) override { @autoreleasepool { - _hasDecorations = (SystemDecorations)value; + _hasDecorations = value; UpdateStyle(); switch (_hasDecorations) diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index a540b026fa..73ec81ce57 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -68,9 +68,9 @@ namespace Avalonia.Native _native.CanResize = value; } - public void SetSystemDecorations(SystemDecorations enabled) + public void SetSystemDecorations(Controls.SystemDecorations enabled) { - _native.HasDecorations = (int)enabled; + _native.HasDecorations = (Interop.SystemDecorations)enabled; } public void SetTitleBarColor (Avalonia.Media.Color color) From f564fd4ed9bf302f13c88d1a15ae7e2ff3f3b9e4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 13 Feb 2020 12:40:57 +0100 Subject: [PATCH 04/55] Update readme.md --- readme.md | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/readme.md b/readme.md index 42b1e52205..40471b3b28 100644 --- a/readme.md +++ b/readme.md @@ -8,25 +8,21 @@ ## About -**Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS and with experimental support for Android and iOS. +**Avalonia** is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS. -**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and [breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). +**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. -| Control catalog | Desktop platforms | Mobile platforms | -|---|---|---| -| | | | +To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). -[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is curated list of awesome Avalonia UI tools, libraries, projects and resources. +You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. -## Getting Started - -Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (screenshot). Now you can write code and markup that will work on multiple platforms! +[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia! -For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). +## Getting Started -If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen. +The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project). -Avalonia is delivered via NuGet package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/) +Avalonia is delivered via NuGet package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/ Use these commands in the Package Manager console to install Avalonia manually: ``` @@ -34,18 +30,17 @@ Install-Package Avalonia Install-Package Avalonia.Desktop ``` -## Bleeding Edge Builds +## JetBrains Rider -or use nightly build feeds as described here: -https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed +If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen. -## Documentation +## Bleeding Edge Builds -You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). +We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) which tracks the current state of master. Although these packages are less stable than the release on NuGet.org, you'll get all the latest features and bugfixes right away and many of our users actually prefer this feed! -There's also a high-level [architecture document](http://avaloniaui.net/architecture/project-structure) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/. +## Documentation -Contributions for our docs are always welcome! +Documentation can be found on our website at http://avaloniaui.net/docs/. We also have a [tutorial](http://avaloniaui.net/docs/tutorial/) over there for newcomers. ## Building and Using @@ -60,14 +55,12 @@ Please read the [contribution guidelines](http://avaloniaui.net/contributing/con This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)]. - ### Backers Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)] - ### Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/Avalonia#sponsor)] From c34bfc56f8d3baa8b7f3e6d116e27edaa9a46a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D0=B4=D0=B8=D0=BC=20=D0=9C=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Tue, 18 Feb 2020 16:25:28 +0300 Subject: [PATCH 05/55] Add class CroppedBitmap --- samples/ControlCatalog/Pages/ImagePage.xaml | 15 +++++- .../ControlCatalog/Pages/ImagePage.xaml.cs | 41 ++++++++++++++++ .../Media/Imaging/CroppedBitmap.cs | 47 +++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index 9b8f8af765..c20f76cedd 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -7,7 +7,7 @@ Displays an image - + Bitmap @@ -22,6 +22,19 @@ + Crop + + None + Center + TopLeft + TopRight + BottomLeft + BottomRight + + + + + Drawing None diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index bbe89d1dfd..d637c88102 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -1,6 +1,10 @@ +using System; +using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; namespace ControlCatalog.Pages { @@ -8,12 +12,17 @@ namespace ControlCatalog.Pages { private readonly Image _bitmapImage; private readonly Image _drawingImage; + private readonly Image _croppedImage; + private readonly IBitmap _croppedBitmapSource; public ImagePage() { InitializeComponent(); _bitmapImage = this.FindControl("bitmapImage"); _drawingImage = this.FindControl("drawingImage"); + _croppedImage = this.FindControl("croppedImage"); + _croppedBitmapSource = LoadBitmap("avares://ControlCatalog/Assets/delicate-arch-896885_640.jpg"); + _croppedImage.Source = new CroppedBitmap(_croppedBitmapSource, default); } private void InitializeComponent() @@ -38,5 +47,37 @@ namespace ControlCatalog.Pages _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex; } } + + public void BitmapCropChanged(object sender, SelectionChangedEventArgs e) + { + if (_croppedImage != null) + { + var comboxBox = (ComboBox)sender; + _croppedImage.Source = new CroppedBitmap( _croppedBitmapSource, GetCropRect(comboxBox.SelectedIndex)); + } + } + + private PixelRect GetCropRect(int index) + { + var bitmapWidth = _croppedBitmapSource.PixelSize.Width; + var bitmapHeight = _croppedBitmapSource.PixelSize.Height; + var cropSize = new PixelSize(bitmapWidth / 2, bitmapHeight / 2); + return index switch + { + 1 => new PixelRect(new PixelPoint((bitmapWidth - cropSize.Width) / 2, (bitmapHeight - cropSize.Width) / 2), cropSize), + 2 => new PixelRect(new PixelPoint(0, 0), cropSize), + 3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize), + 4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize), + 5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize), + _ => PixelRect.Empty + }; + + } + + private IBitmap LoadBitmap(string uri) + { + var assets = AvaloniaLocator.Current.GetService(); + return new Bitmap(assets.Open(new Uri(uri))); + } } } diff --git a/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs new file mode 100644 index 0000000000..6bdee24c03 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media.Imaging +{ + public class CroppedBitmap : IImage, IDisposable + { + public CroppedBitmap() + { + Source = null; + SourceRect = default; + } + public CroppedBitmap(IBitmap source, PixelRect sourceRect) + { + Source = source; + SourceRect = sourceRect; + } + public virtual void Dispose() + { + Source?.Dispose(); + } + + public Size Size { + get + { + if (Source == null) + return Size.Empty; + if (SourceRect.IsEmpty) + return Source.Size; + return SourceRect.Size.ToSizeWithDpi(Source.Dpi); + } + } + + public void Draw(DrawingContext context, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + { + if (Source == null) + return; + var topLeft = SourceRect.TopLeft.ToPointWithDpi(Source.Dpi); + Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect, bitmapInterpolationMode); + } + + public IBitmap Source { get; } + public PixelRect SourceRect { get; } + } +} From e870f6c6e492f7df704d491c0a7b310d2bdffefd Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Thu, 20 Feb 2020 14:00:39 +0100 Subject: [PATCH 06/55] Update NUKE to 0.24 --- nukebuild/Build.cs | 54 ++++++++++++++++++++++------------------- nukebuild/Shims.cs | 8 +++--- nukebuild/_build.csproj | 8 +++--- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 7b3b8465ce..b14b78065b 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -26,7 +26,7 @@ using static Nuke.Common.Tools.VSWhere.VSWhereTasks; running and debugging a particular target (optionally without deps) would be way easier ReSharper/Rider - https://plugins.jetbrains.com/plugin/10803-nuke-support VSCode - https://marketplace.visualstudio.com/items?itemName=nuke.support - + */ partial class Build : NukeBuild @@ -54,7 +54,7 @@ partial class Build : NukeBuild protected override void OnBuildInitialized() { Parameters = new BuildParameters(this); - Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", + Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", Parameters.Version, Parameters.Configuration, typeof(NukeBuild).Assembly.GetName().Version.ToString()); @@ -93,8 +93,10 @@ partial class Build : NukeBuild string projectFile, Configure configurator = null) { - return MSBuild(projectFile, c => + return MSBuild(c => { + c = c.SetProjectFile(projectFile); + // This is required for VS2019 image on Azure Pipelines if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) { @@ -114,8 +116,8 @@ partial class Build : NukeBuild } Target Clean => _ => _.Executes(() => { - DeleteDirectories(Parameters.BuildDirs); - EnsureCleanDirectories(Parameters.BuildDirs); + Parameters.BuildDirs.ForEach(DeleteDirectory); + Parameters.BuildDirs.ForEach(DeleteDirectory); EnsureCleanDirectory(Parameters.ArtifactsDir); EnsureCleanDirectory(Parameters.NugetIntermediateRoot); EnsureCleanDirectory(Parameters.NugetRoot); @@ -134,12 +136,13 @@ partial class Build : NukeBuild ); else - DotNetBuild(Parameters.MSBuildSolution, c => c + DotNetBuild(c => c + .SetProjectFile(Parameters.MSBuildSolution) .AddProperty("PackageVersion", Parameters.Version) .SetConfiguration(Parameters.Configuration) ); }); - + void RunCoreTest(string project) { if(!project.EndsWith(".csproj")) @@ -153,13 +156,13 @@ partial class Build : NukeBuild var targets = xdoc.Root.Descendants("TargetFrameworks").FirstOrDefault(); if (targets != null) frameworks = targets.Value.Split(';').Where(f => !string.IsNullOrWhiteSpace(f)).ToList(); - else + else frameworks = new List {xdoc.Root.Descendants("TargetFramework").First().Value}; - + foreach(var fw in frameworks) { if (fw.StartsWith("net4") - && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") { Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969"); @@ -184,7 +187,7 @@ partial class Build : NukeBuild } Target RunCoreLibsTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests) + .OnlyWhenStatic(() => !Parameters.SkipTests) .DependsOn(Compile) .Executes(() => { @@ -204,7 +207,7 @@ partial class Build : NukeBuild }); Target RunRenderTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests) + .OnlyWhenStatic(() => !Parameters.SkipTests) .DependsOn(Compile) .Executes(() => { @@ -212,9 +215,9 @@ partial class Build : NukeBuild if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj"); }); - + Target RunDesignerTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .DependsOn(Compile) .Executes(() => { @@ -224,7 +227,7 @@ partial class Build : NukeBuild [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; Target RunLeakTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .DependsOn(Compile) .Executes(() => { @@ -235,7 +238,7 @@ partial class Build : NukeBuild }); Target ZipFiles => _ => _ - .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) + .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) .Executes(() => { var data = Parameters; @@ -259,9 +262,10 @@ partial class Build : NukeBuild MsBuildCommon(Parameters.MSBuildSolution, c => c .AddTargets("Pack")); else - DotNetPack(Parameters.MSBuildSolution, c => - c.SetConfiguration(Parameters.Configuration) - .AddProperty("PackageVersion", Parameters.Version)); + DotNetPack(c => c + .SetProject(Parameters.MSBuildSolution) + .SetConfiguration(Parameters.Configuration) + .AddProperty("PackageVersion", Parameters.Version)); }); Target CreateNugetPackages => _ => _ @@ -274,29 +278,29 @@ partial class Build : NukeBuild new NumergeNukeLogger())) throw new Exception("Package merge failed"); }); - + Target RunTests => _ => _ .DependsOn(RunCoreLibsTests) .DependsOn(RunRenderTests) .DependsOn(RunDesignerTests) .DependsOn(RunLeakTests); - + Target Package => _ => _ .DependsOn(RunTests) .DependsOn(CreateNugetPackages); - + Target CiAzureLinux => _ => _ .DependsOn(RunTests); - + Target CiAzureOSX => _ => _ .DependsOn(Package) .DependsOn(ZipFiles); - + Target CiAzureWindows => _ => _ .DependsOn(Package) .DependsOn(ZipFiles); - + public static int Main() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Execute(x => x.Package) diff --git a/nukebuild/Shims.cs b/nukebuild/Shims.cs index 461d617643..1ac14bf622 100644 --- a/nukebuild/Shims.cs +++ b/nukebuild/Shims.cs @@ -19,9 +19,9 @@ public partial class Build Logger.Info(info, args); } - private void Zip(PathConstruction.AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); + private void Zip(AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); - private void Zip(PathConstruction.AbsolutePath target, IEnumerable paths) + private void Zip(AbsolutePath target, IEnumerable paths) { var targetPath = target.ToString(); bool finished = false, atLeastOneFileAdded = false; @@ -38,7 +38,7 @@ public partial class Build fileStream.CopyTo(entryStream); atLeastOneFileAdded = true; } - + foreach (var path in paths) { if (Directory.Exists(path)) @@ -64,7 +64,7 @@ public partial class Build finished = true; } - finally + finally { try { diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 2a736e4653..f26bf7137e 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp3.0 false False @@ -10,7 +10,7 @@ - + @@ -20,11 +20,11 @@ - + - + From 367548c1bece9d3bfc4b9d54bddaeb0fd55e6e12 Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Thu, 20 Feb 2020 14:21:55 +0100 Subject: [PATCH 07/55] Minor cleanups --- nukebuild/Build.cs | 118 +++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 64 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index b14b78065b..1a924733b2 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -13,6 +13,7 @@ using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; using Nuke.Common.Tools.MSBuild; using Nuke.Common.Utilities; +using Nuke.Common.Utilities.Collections; using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; @@ -31,6 +32,8 @@ using static Nuke.Common.Tools.VSWhere.VSWhereTasks; partial class Build : NukeBuild { + [Solution("Avalonia.sln")] readonly Solution Solution; + static Lazy MsBuildExe = new Lazy(() => { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -93,27 +96,20 @@ partial class Build : NukeBuild string projectFile, Configure configurator = null) { - return MSBuild(c => - { - c = c.SetProjectFile(projectFile); - + return MSBuild(c => c + .SetProjectFile(projectFile) // This is required for VS2019 image on Azure Pipelines - if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) - { - var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64"); - if (javaSdk != null) - c = c.AddProperty("JavaSdkDirectory", javaSdk); - } - - c = c.AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal); - c = configurator?.Invoke(c) ?? c; - return c; - }); + .When(Parameters.IsRunningOnWindows && + Parameters.IsRunningOnAzure, c => c + .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_8_X64"))) + .AddProperty("PackageVersion", Parameters.Version) + .AddProperty("iOSRoslynPathHackRequired", true) + .SetToolPath(MsBuildExe.Value) + .SetConfiguration(Parameters.Configuration) + .SetVerbosity(MSBuildVerbosity.Minimal) + .Apply(configurator)); } + Target Clean => _ => _.Executes(() => { Parameters.BuildDirs.ForEach(DeleteDirectory); @@ -143,23 +139,12 @@ partial class Build : NukeBuild ); }); - void RunCoreTest(string project) + void RunCoreTest(string projectName) { - if(!project.EndsWith(".csproj")) - project = System.IO.Path.Combine(project, System.IO.Path.GetFileName(project)+".csproj"); - Information("Running tests from " + project); - XDocument xdoc; - using (var s = File.OpenRead(project)) - xdoc = XDocument.Load(s); - - List frameworks = null; - var targets = xdoc.Root.Descendants("TargetFrameworks").FirstOrDefault(); - if (targets != null) - frameworks = targets.Value.Split(';').Where(f => !string.IsNullOrWhiteSpace(f)).ToList(); - else - frameworks = new List {xdoc.Root.Descendants("TargetFramework").First().Value}; - - foreach(var fw in frameworks) + Information($"Running tests from {projectName}"); + var project = Solution.GetProject(projectName).NotNull("project != null"); + + foreach (var fw in project.GetTargetFrameworks()) { if (fw.StartsWith("net4") && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) @@ -170,19 +155,16 @@ partial class Build : NukeBuild } Information("Running for " + fw); - DotNetTest(c => - { - c = c - .SetProjectFile(project) - .SetConfiguration(Parameters.Configuration) - .SetFramework(fw) - .EnableNoBuild() - .EnableNoRestore(); - // NOTE: I can see that we could maybe add another extension method "Switch" or "If" to make this more convenient - if (Parameters.PublishTestResults) - c = c.SetLogger("trx").SetResultsDirectory(Parameters.TestResultsRoot); - return c; - }); + + DotNetTest(c => c + .SetProjectFile(project) + .SetConfiguration(Parameters.Configuration) + .SetFramework(fw) + .EnableNoBuild() + .EnableNoRestore() + .When(Parameters.PublishTestResults, c => c + .SetLogger("trx") + .SetResultsDirectory(Parameters.TestResultsRoot))); } } @@ -191,19 +173,19 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.Animation.UnitTests"); - RunCoreTest("./tests/Avalonia.Base.UnitTests"); - RunCoreTest("./tests/Avalonia.Controls.UnitTests"); - RunCoreTest("./tests/Avalonia.Controls.DataGrid.UnitTests"); - RunCoreTest("./tests/Avalonia.Input.UnitTests"); - RunCoreTest("./tests/Avalonia.Interactivity.UnitTests"); - RunCoreTest("./tests/Avalonia.Layout.UnitTests"); - RunCoreTest("./tests/Avalonia.Markup.UnitTests"); - RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests"); - RunCoreTest("./tests/Avalonia.Styling.UnitTests"); - RunCoreTest("./tests/Avalonia.Visuals.UnitTests"); - RunCoreTest("./tests/Avalonia.Skia.UnitTests"); - RunCoreTest("./tests/Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.Animation.UnitTests"); + RunCoreTest("Avalonia.Base.UnitTests"); + RunCoreTest("Avalonia.Controls.UnitTests"); + RunCoreTest("Avalonia.Controls.DataGrid.UnitTests"); + RunCoreTest("Avalonia.Input.UnitTests"); + RunCoreTest("Avalonia.Interactivity.UnitTests"); + RunCoreTest("Avalonia.Layout.UnitTests"); + RunCoreTest("Avalonia.Markup.UnitTests"); + RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); + RunCoreTest("Avalonia.Styling.UnitTests"); + RunCoreTest("Avalonia.Visuals.UnitTests"); + RunCoreTest("Avalonia.Skia.UnitTests"); + RunCoreTest("Avalonia.ReactiveUI.UnitTests"); }); Target RunRenderTests => _ => _ @@ -211,9 +193,9 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj"); + RunCoreTest("Avalonia.Skia.RenderTests"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj"); + RunCoreTest("Avalonia.Direct2D1.RenderTests"); }); Target RunDesignerTests => _ => _ @@ -221,7 +203,7 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.DesignerSupport.Tests"); + RunCoreTest("Avalonia.DesignerSupport.Tests"); }); [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; @@ -307,3 +289,11 @@ partial class Build : NukeBuild : Execute(x => x.RunTests); } + +public static class ToolSettingsExtensions +{ + public static T Apply(this T settings, Configure configurator) + { + return configurator != null ? configurator(settings) : settings; + } +} From d60603d52968ad5c89ab7ef6594a57caf002474d Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Fri, 21 Feb 2020 11:55:54 +0100 Subject: [PATCH 08/55] Fix information output --- nukebuild/Build.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 1a924733b2..c2fa54ba2b 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -150,11 +150,11 @@ partial class Build : NukeBuild && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") { - Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969"); + Information($"Skipping {projectName} ({fw}) tests on Linux - https://github.com/mono/mono/issues/13969"); continue; } - Information("Running for " + fw); + Information($"Running for {projectName} ({fw}) ..."); DotNetTest(c => c .SetProjectFile(project) From 8f68d2d12407dfe1b41bfaa7fdd728f76f8ccc6c Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Fri, 21 Feb 2020 11:56:34 +0100 Subject: [PATCH 09/55] Fix ensuring directory --- nukebuild/Build.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index c2fa54ba2b..358846a14e 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -113,7 +113,7 @@ partial class Build : NukeBuild Target Clean => _ => _.Executes(() => { Parameters.BuildDirs.ForEach(DeleteDirectory); - Parameters.BuildDirs.ForEach(DeleteDirectory); + Parameters.BuildDirs.ForEach(EnsureCleanDirectory); EnsureCleanDirectory(Parameters.ArtifactsDir); EnsureCleanDirectory(Parameters.NugetIntermediateRoot); EnsureCleanDirectory(Parameters.NugetRoot); From 924064be139c9a2847d066aba19e3d5896219b42 Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Fri, 21 Feb 2020 12:31:39 +0100 Subject: [PATCH 10/55] Use netcoreapp3.1 --- nukebuild/_build.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index f26bf7137e..584c36d033 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.0 + netcoreapp3.1 false False From 2bf2e60ae04604ded6bd0dbce0807ca1b8762711 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 22 Feb 2020 10:00:45 +0100 Subject: [PATCH 11/55] Don't add extra pixel to AccessText measurement. It's not needed; the underscore can be drawn in the descender space. --- src/Avalonia.Controls/Primitives/AccessText.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 5adc8d2448..f6fea89ec9 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -84,17 +84,6 @@ namespace Avalonia.Controls.Primitives return base.CreateFormattedText(constraint, StripAccessKey(text)); } - /// - /// Measures the control. - /// - /// The available size for the control. - /// The desired size. - protected override Size MeasureOverride(Size availableSize) - { - var result = base.MeasureOverride(availableSize); - return result.WithHeight(result.Height + 1); - } - /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { From 8a0ccea2731e2df48d105f9e5fea25e6f1361fb9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 22 Feb 2020 10:02:19 +0100 Subject: [PATCH 12/55] Add MenuItem.InputGestureText. --- src/Avalonia.Controls/MenuItem.cs | 51 +++++++++++++++++++++++ src/Avalonia.Themes.Default/MenuItem.xaml | 26 ++++++++---- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index e0baa5e679..ae36b5d830 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -13,6 +13,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -48,6 +49,12 @@ namespace Avalonia.Controls public static readonly StyledProperty IconProperty = AvaloniaProperty.Register(nameof(Icon)); + /// + /// Defines the property. + /// + public static readonly StyledProperty InputGestureTextProperty = + AvaloniaProperty.Register(nameof(InputGestureText)); + /// /// Defines the property. /// @@ -93,6 +100,7 @@ namespace Avalonia.Controls private ICommand _command; private bool _commandCanExecute = true; private Popup _popup; + private IDisposable _gridHack; /// /// Initializes static members of the class. @@ -194,6 +202,19 @@ namespace Avalonia.Controls set { SetValue(IconProperty, value); } } + /// + /// Gets or sets the input gesture that will be displayed in the menu item. + /// + /// + /// Setting this property does not cause the input gesture to be handled by the menu item, + /// it simply displays the gesture text in the menu. + /// + public object InputGestureText + { + get { return GetValue(InputGestureTextProperty); } + set { SetValue(InputGestureTextProperty, value); } + } + /// /// Gets or sets a value indicating whether the is currently selected. /// @@ -306,6 +327,36 @@ namespace Avalonia.Controls } } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (this.GetVisualParent() is IControl parent) + { + // HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached + // property to store SharedSizeGroup state, except property inheritance is done + // down the logical tree. In this case, the control which is setting + // Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing + // the way Grid stores shared size state, the developers of WPF just created a + // binding of the internal state of the visual parent to the menu item. We don't + // have much choice but to do the same for now unless we want to refactor Grid, + // which I honestly am not brave enough to do right now. Here's the same hack in + // the WPF codebase: + // + // https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141 + _gridHack = Bind( + DefinitionBase.PrivateSharedSizeScopeProperty, + parent.GetBindingObservable(DefinitionBase.PrivateSharedSizeScopeProperty)); + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _gridHack.Dispose(); + _gridHack = null; + } + /// /// Called when the is clicked. /// diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 93989d3782..431adacb47 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -11,7 +11,14 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - + + + + + + + + + + Grid.Column="4"/> + Margin="4 2" + Grid.IsSharedSizeScope="True"/> @@ -102,10 +113,11 @@ BorderThickness="{TemplateBinding BorderThickness}"> + Items="{TemplateBinding Items}" + ItemsPanel="{TemplateBinding ItemsPanel}" + ItemTemplate="{TemplateBinding ItemTemplate}" + Margin="2" + Grid.IsSharedSizeScope="True"/> From deebe6090ffddfff6e5da007d9c56a68d2ffe5a5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 22 Feb 2020 10:02:33 +0100 Subject: [PATCH 13/55] Show input gesture text in control catalog. --- samples/ControlCatalog/Pages/MenuPage.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index 868f0df6ad..cae5ab54b1 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -16,13 +16,13 @@ Defined in XAML - + - + From 467288ca998a5585955d8cbcf63903c5b3a7c4de Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Sat, 22 Feb 2020 18:42:25 +0100 Subject: [PATCH 14/55] Update NUKE to 0.24 --- nukebuild/BuildParameters.cs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 65ba5e9756..149716b416 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -4,24 +4,21 @@ using System.Linq; using System.Runtime.InteropServices; using System.Xml.Linq; using Nuke.Common; -using Nuke.Common.BuildServers; -using Nuke.Common.Execution; +using Nuke.Common.CI.AzurePipelines; using Nuke.Common.IO; -using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; -using static Nuke.Common.Tools.MSBuild.MSBuildTasks; public partial class Build { [Parameter("configuration")] public string Configuration { get; set; } - + [Parameter("skip-tests")] public bool SkipTests { get; set; } - + [Parameter("force-nuget-version")] public string ForceNugetVersion { get; set; } - + public class BuildParameters { public string Configuration { get; } @@ -79,15 +76,15 @@ public partial class Build IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - IsRunningOnAzure = Host == HostType.TeamServices || + IsRunningOnAzure = Host == HostType.AzurePipelines || Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; if (IsRunningOnAzure) { - RepositoryName = TeamServices.Instance.RepositoryUri; - RepositoryBranch = TeamServices.Instance.SourceBranch; - IsPullRequest = TeamServices.Instance.PullRequestId.HasValue; - IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, TeamServices.Instance.RepositoryUri); + RepositoryName = AzurePipelines.Instance.RepositoryUri; + RepositoryBranch = AzurePipelines.Instance.SourceBranch; + IsPullRequest = AzurePipelines.Instance.PullRequestId.HasValue; + IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, AzurePipelines.Instance.RepositoryUri); } IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, From b966bd390c7d310590ef98baa0c9aa69440cacce Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 23 Feb 2020 15:34:04 +0100 Subject: [PATCH 15/55] Enable NRT in Avalonia.Interactivity. --- .../Avalonia.Interactivity.csproj | 6 +- .../EventSubscription.cs | 20 ++++-- src/Avalonia.Interactivity/IInteractive.cs | 2 +- src/Avalonia.Interactivity/Interactive.cs | 68 ++++++++----------- .../InteractiveExtensions.cs | 5 +- src/Avalonia.Interactivity/RoutedEvent.cs | 18 ++--- src/Avalonia.Interactivity/RoutedEventArgs.cs | 8 +-- .../RoutedEventRegistry.cs | 6 +- 8 files changed, 69 insertions(+), 64 deletions(-) diff --git a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj index 66f1e8cc26..730ca2bd6e 100644 --- a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj +++ b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj @@ -1,6 +1,8 @@  netstandard2.0 + Enable + CS8600;CS8602;CS8603 @@ -9,6 +11,4 @@ - - - + \ No newline at end of file diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs index e8fb1bfaf1..d363e3f6fa 100644 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ b/src/Avalonia.Interactivity/EventSubscription.cs @@ -9,12 +9,24 @@ namespace Avalonia.Interactivity internal class EventSubscription { - public HandlerInvokeSignature InvokeAdapter { get; set; } + public EventSubscription( + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo, + HandlerInvokeSignature? invokeAdapter = null) + { + Handler = handler; + Routes = routes; + HandledEventsToo = handledEventsToo; + InvokeAdapter = invokeAdapter; + } - public Delegate Handler { get; set; } + public HandlerInvokeSignature? InvokeAdapter { get; } - public RoutingStrategies Routes { get; set; } + public Delegate Handler { get; } - public bool AlsoIfHandled { get; set; } + public RoutingStrategies Routes { get; } + + public bool HandledEventsToo { get; } } } diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs index 47046b58e2..6524794733 100644 --- a/src/Avalonia.Interactivity/IInteractive.cs +++ b/src/Avalonia.Interactivity/IInteractive.cs @@ -13,7 +13,7 @@ namespace Avalonia.Interactivity /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// - IInteractive InteractiveParent { get; } + IInteractive? InteractiveParent { get; } /// /// Adds a handler for the specified routed event. diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 27ece25183..0c4649a1ca 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -15,16 +15,16 @@ namespace Avalonia.Interactivity /// public class Interactive : Layoutable, IInteractive { - private Dictionary> _eventHandlers; + private Dictionary>? _eventHandlers; private static readonly Dictionary s_invokeHandlerCache = new Dictionary(); /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// - IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; + IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; - private Dictionary> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary>()); + private Dictionary> EventHandlers => _eventHandlers ??= new Dictionary>(); /// /// Adds a handler for the specified routed event. @@ -40,16 +40,10 @@ namespace Avalonia.Interactivity RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); - - var subscription = new EventSubscription - { - Handler = handler, - Routes = routes, - AlsoIfHandled = handledEventsToo, - }; + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); + var subscription = new EventSubscription(handler, routes, handledEventsToo); return AddEventSubscription(routedEvent, subscription); } @@ -68,12 +62,12 @@ namespace Avalonia.Interactivity RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) where TEventArgs : RoutedEventArgs { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); // EventHandler delegate is not covariant, this forces us to create small wrapper // that will cast our type erased instance and invoke it. - Type eventArgsType = routedEvent.EventArgsType; + var eventArgsType = routedEvent.EventArgsType; if (!s_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter)) { @@ -90,14 +84,7 @@ namespace Avalonia.Interactivity s_invokeHandlerCache.Add(eventArgsType, invokeAdapter); } - var subscription = new EventSubscription - { - InvokeAdapter = invokeAdapter, - Handler = handler, - Routes = routes, - AlsoIfHandled = handledEventsToo, - }; - + var subscription = new EventSubscription(handler, routes, handledEventsToo, invokeAdapter); return AddEventSubscription(routedEvent, subscription); } @@ -108,12 +95,11 @@ namespace Avalonia.Interactivity /// The handler. public void RemoveHandler(RoutedEvent routedEvent, Delegate handler) { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); - - List subscriptions = null; + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); - if (_eventHandlers?.TryGetValue(routedEvent, out subscriptions) == true) + if (_eventHandlers is object && + _eventHandlers.TryGetValue(routedEvent, out var subscriptions) == true) { subscriptions.RemoveAll(x => x.Handler == handler); } @@ -137,9 +123,14 @@ namespace Avalonia.Interactivity /// The event args. public void RaiseEvent(RoutedEventArgs e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); - e.Source = e.Source ?? this; + if (e.RoutedEvent == null) + { + throw new ArgumentException("Cannot raise an event whose RoutedEvent is null."); + } + + e.Source ??= this; if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct) { @@ -167,7 +158,7 @@ namespace Avalonia.Interactivity /// The event args. private void BubbleEvent(RoutedEventArgs e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); e.Route = RoutingStrategies.Bubble; @@ -182,7 +173,7 @@ namespace Avalonia.Interactivity /// The event args. private void TunnelEvent(RoutedEventArgs e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); e.Route = RoutingStrategies.Tunnel; @@ -197,18 +188,17 @@ namespace Avalonia.Interactivity /// The event args. private void RaiseEventImpl(RoutedEventArgs e) { - Contract.Requires(e != null); - - e.RoutedEvent.InvokeRaised(this, e); + e = e ?? throw new ArgumentNullException(nameof(e)); - List subscriptions = null; + e.RoutedEvent!.InvokeRaised(this, e); - if (_eventHandlers?.TryGetValue(e.RoutedEvent, out subscriptions) == true) + if (_eventHandlers is object && + _eventHandlers.TryGetValue(e.RoutedEvent, out var subscriptions) == true) { foreach (var sub in subscriptions.ToList()) { bool correctRoute = (e.Route & sub.Routes) != 0; - bool notFinished = !e.Handled || sub.AlsoIfHandled; + bool notFinished = !e.Handled || sub.HandledEventsToo; if (correctRoute && notFinished) { @@ -313,7 +303,7 @@ namespace Avalonia.Interactivity { _preTraverse.Execute(target, _args); - IInteractive parent = target.InteractiveParent; + var parent = target.InteractiveParent; if (parent != null) { diff --git a/src/Avalonia.Interactivity/InteractiveExtensions.cs b/src/Avalonia.Interactivity/InteractiveExtensions.cs index 07e4029240..414c408080 100644 --- a/src/Avalonia.Interactivity/InteractiveExtensions.cs +++ b/src/Avalonia.Interactivity/InteractiveExtensions.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Linq; using System.Reactive.Linq; namespace Avalonia.Interactivity @@ -30,6 +28,9 @@ namespace Avalonia.Interactivity bool handledEventsToo = false) where TEventArgs : RoutedEventArgs { + o = o ?? throw new ArgumentNullException(nameof(o)); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + return Observable.Create(x => o.AddHandler( routedEvent, (_, e) => x.OnNext(e), diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index 55d9e61d87..164a86fab7 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -25,10 +25,14 @@ namespace Avalonia.Interactivity Type eventArgsType, Type ownerType) { - Contract.Requires(name != null); - Contract.Requires(eventArgsType != null); - Contract.Requires(ownerType != null); - Contract.Requires(typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType)); + name = name ?? throw new ArgumentNullException(nameof(name)); + eventArgsType = eventArgsType ?? throw new ArgumentNullException(nameof(name)); + ownerType = ownerType ?? throw new ArgumentNullException(nameof(name)); + + if (!typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType)) + { + throw new InvalidCastException("eventArgsType must be derived from RoutedEventArgs."); + } EventArgsType = eventArgsType; Name = name; @@ -52,7 +56,7 @@ namespace Avalonia.Interactivity RoutingStrategies routingStrategy) where TEventArgs : RoutedEventArgs { - Contract.Requires(name != null); + name = name ?? throw new ArgumentNullException(nameof(name)); var routedEvent = new RoutedEvent(name, routingStrategy, typeof(TOwner)); RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent); @@ -65,7 +69,7 @@ namespace Avalonia.Interactivity Type ownerType) where TEventArgs : RoutedEventArgs { - Contract.Requires(name != null); + name = name ?? throw new ArgumentNullException(nameof(name)); var routedEvent = new RoutedEvent(name, routingStrategy, ownerType); RoutedEventRegistry.Instance.Register(ownerType, routedEvent); @@ -108,8 +112,6 @@ namespace Avalonia.Interactivity public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType) : base(name, routingStrategies, typeof(TEventArgs), ownerType) { - Contract.Requires(name != null); - Contract.Requires(ownerType != null); } [Obsolete("Use overload taking Action.")] diff --git a/src/Avalonia.Interactivity/RoutedEventArgs.cs b/src/Avalonia.Interactivity/RoutedEventArgs.cs index 05bbf7b6a3..e00393322d 100644 --- a/src/Avalonia.Interactivity/RoutedEventArgs.cs +++ b/src/Avalonia.Interactivity/RoutedEventArgs.cs @@ -11,12 +11,12 @@ namespace Avalonia.Interactivity { } - public RoutedEventArgs(RoutedEvent routedEvent) + public RoutedEventArgs(RoutedEvent? routedEvent) { RoutedEvent = routedEvent; } - public RoutedEventArgs(RoutedEvent routedEvent, IInteractive source) + public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source) { RoutedEvent = routedEvent; Source = source; @@ -24,10 +24,10 @@ namespace Avalonia.Interactivity public bool Handled { get; set; } - public RoutedEvent RoutedEvent { get; set; } + public RoutedEvent? RoutedEvent { get; set; } public RoutingStrategies Route { get; set; } - public IInteractive Source { get; set; } + public IInteractive? Source { get; set; } } } diff --git a/src/Avalonia.Interactivity/RoutedEventRegistry.cs b/src/Avalonia.Interactivity/RoutedEventRegistry.cs index 34c970a806..0111b115e6 100644 --- a/src/Avalonia.Interactivity/RoutedEventRegistry.cs +++ b/src/Avalonia.Interactivity/RoutedEventRegistry.cs @@ -32,8 +32,8 @@ namespace Avalonia.Interactivity /// public void Register(Type type, RoutedEvent @event) { - Contract.Requires(type != null); - Contract.Requires(@event != null); + type = type ?? throw new ArgumentNullException(nameof(type)); + @event = @event ?? throw new ArgumentNullException(nameof(@event)); if (!_registeredRoutedEvents.TryGetValue(type, out var list)) { @@ -66,7 +66,7 @@ namespace Avalonia.Interactivity /// All routed events registered with the provided type. public IReadOnlyList GetRegistered(Type type) { - Contract.Requires(type != null); + type = type ?? throw new ArgumentNullException(nameof(type)); if (_registeredRoutedEvents.TryGetValue(type, out var events)) { From 4e62ff3ffb9bb68dd13153f357298ac6eeddcf0e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Feb 2020 11:04:20 +0100 Subject: [PATCH 16/55] Added failing test for #3176. --- .../InteractiveTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs index 414e67bb94..0355078a05 100644 --- a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs @@ -358,6 +358,29 @@ namespace Avalonia.Interactivity.UnitTests Assert.Equal(1, called); } + [Fact] + public void Removing_Control_In_Handler_Should_Not_Stop_Event() + { + // Issue #3176 + var ev = new RoutedEvent("test", RoutingStrategies.Bubble, typeof(RoutedEventArgs), typeof(TestInteractive)); + var invoked = new List(); + EventHandler handler = (s, e) => invoked.Add(((TestInteractive)s).Name); + var parent = CreateTree(ev, handler, RoutingStrategies.Bubble | RoutingStrategies.Tunnel); + var target = (IInteractive)parent.GetVisualChildren().Single(); + + EventHandler removeHandler = (s, e) => + { + parent.Children = Array.Empty(); + }; + + target.AddHandler(ev, removeHandler); + + var args = new RoutedEventArgs(ev, target); + target.RaiseEvent(args); + + Assert.Equal(new[] { "3", "2b", "1" }, invoked); + } + private TestInteractive CreateTree( RoutedEvent ev, EventHandler handler, @@ -414,6 +437,7 @@ namespace Avalonia.Interactivity.UnitTests set { + VisualChildren.Clear(); VisualChildren.AddRange(value.Cast()); } } From cca4247c05ce516a904b149ceea972b0ac81dbb4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Feb 2020 10:40:34 +0100 Subject: [PATCH 17/55] Added EventRoute. Instead of traversing the tree while raising an event, instead first build an event route and then raise the event along it. Fixes #3176 --- src/Avalonia.Interactivity/EventRoute.cs | 200 ++++++++++++++++++ .../EventSubscription.cs | 6 +- src/Avalonia.Interactivity/IInteractive.cs | 7 + src/Avalonia.Interactivity/Interactive.cs | 195 +++++------------ src/Avalonia.Interactivity/RoutedEvent.cs | 2 + .../InteractiveTests.cs | 1 - tests/Avalonia.UnitTests/MouseTestHelper.cs | 2 +- 7 files changed, 266 insertions(+), 147 deletions(-) create mode 100644 src/Avalonia.Interactivity/EventRoute.cs diff --git a/src/Avalonia.Interactivity/EventRoute.cs b/src/Avalonia.Interactivity/EventRoute.cs new file mode 100644 index 0000000000..85ba33d7ba --- /dev/null +++ b/src/Avalonia.Interactivity/EventRoute.cs @@ -0,0 +1,200 @@ +using System; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Interactivity +{ + /// + /// Holds the route for a routed event and supports raising an event on that route. + /// + public class EventRoute : IDisposable + { + private readonly RoutedEvent _event; + private PooledList? _route; + + /// + /// Initializes a new instance of the class. + /// + /// The routed event to be raised. + public EventRoute(RoutedEvent e) + { + e = e ?? throw new ArgumentNullException(nameof(e)); + + _event = e; + _route = null; + } + + /// + /// Gets a value indicating whether the route has any handlers. + /// + public bool HasHandlers => _route?.Count > 0; + + /// + /// Adds a handler to the route. + /// + /// The target on which the event should be raised. + /// The handler for the event. + /// The routing strategies to listen to. + /// + /// If true the handler will be raised even when the routed event is marked as handled. + /// + /// + /// An optional adapter which if supplied, will be called with + /// and the parameters for the event. This adapter can be used to avoid calling + /// `DynamicInvoke` on the handler. + /// + public void Add( + IInteractive target, + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo = false, + Action? adapter = null) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); + + _route ??= new PooledList(16); + _route.Add(new RouteItem(target, handler, adapter, routes, handledEventsToo)); + } + + /// + /// Adds a class handler to the route. + /// + /// The target on which the event should be raised. + public void AddClassHandler(IInteractive target) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + + _route ??= new PooledList(16); + _route.Add(new RouteItem(target, null, null, 0, false)); + } + + /// + /// Raises an event along the route. + /// + /// The event source. + /// The event args. + public void RaiseEvent(IInteractive source, RoutedEventArgs e) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + e = e ?? throw new ArgumentNullException(nameof(e)); + + e.Source = source; + + if (_event.RoutingStrategies == RoutingStrategies.Direct) + { + e.Route = RoutingStrategies.Direct; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + else + { + if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel)) + { + e.Route = RoutingStrategies.Tunnel; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + + if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble)) + { + e.Route = RoutingStrategies.Bubble; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + } + } + + /// + /// Disposes of the event route. + /// + public void Dispose() + { + _route?.Dispose(); + _route = null; + } + + private void RaiseEventImpl(RoutedEventArgs e) + { + if (_route is null) + { + return; + } + + if (e.Source is null) + { + throw new ArgumentException("Event source may not be null", nameof(e)); + } + + IInteractive? lastTarget = null; + var start = 0; + var end = _route.Count; + var step = 1; + + if (e.Route == RoutingStrategies.Tunnel) + { + start = end - 1; + step = end = -1; + } + + for (var i = start; i != end; i += step) + { + var entry = _route[i]; + + // If we've got to a new control then call any RoutedEvent.Raised listeners. + if (entry.Target != lastTarget) + { + if (!e.Handled) + { + _event.InvokeRaised(entry.Target, e); + } + + // If this is a direct event and we've already raised events then we're finished. + if (e.Route == RoutingStrategies.Direct && lastTarget is object) + { + return; + } + + lastTarget = entry.Target; + } + + // Raise the event handler. + if (entry.Handler is object && + entry.Routes.HasFlagCustom(e.Route) && + (!e.Handled || entry.HandledEventsToo)) + { + if (entry.Adapter is object) + { + entry.Adapter(entry.Handler, entry.Target, e); + } + else + { + entry.Handler.DynamicInvoke(entry.Target, e); + } + } + } + } + + private readonly struct RouteItem + { + public RouteItem( + IInteractive target, + Delegate? handler, + Action? adapter, + RoutingStrategies routes, + bool handledEventsToo) + { + Target = target; + Handler = handler; + Adapter = adapter; + Routes = routes; + HandledEventsToo = handledEventsToo; + } + + public IInteractive Target { get; } + public Delegate? Handler { get; } + public Action? Adapter { get; } + public RoutingStrategies Routes { get; } + public bool HandledEventsToo { get; } + } + } +} diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs index d363e3f6fa..50f64f49ee 100644 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ b/src/Avalonia.Interactivity/EventSubscription.cs @@ -5,15 +5,13 @@ using System; namespace Avalonia.Interactivity { - internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args); - internal class EventSubscription { public EventSubscription( Delegate handler, RoutingStrategies routes, bool handledEventsToo, - HandlerInvokeSignature? invokeAdapter = null) + Action? invokeAdapter = null) { Handler = handler; Routes = routes; @@ -21,7 +19,7 @@ namespace Avalonia.Interactivity InvokeAdapter = invokeAdapter; } - public HandlerInvokeSignature? InvokeAdapter { get; } + public Action? InvokeAdapter { get; } public Delegate Handler { get; } diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs index 6524794733..33baa9453a 100644 --- a/src/Avalonia.Interactivity/IInteractive.cs +++ b/src/Avalonia.Interactivity/IInteractive.cs @@ -60,6 +60,13 @@ namespace Avalonia.Interactivity void RemoveHandler(RoutedEvent routedEvent, EventHandler handler) where TEventArgs : RoutedEventArgs; + /// + /// Adds the object's handlers for a routed event to an event route. + /// + /// The event. + /// The event route. + void AddToEventRoute(RoutedEvent routedEvent, EventRoute route); + /// /// Raises a routed event. /// diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 0c4649a1ca..5a27192c87 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; using Avalonia.Layout; using Avalonia.VisualTree; @@ -17,15 +15,14 @@ namespace Avalonia.Interactivity { private Dictionary>? _eventHandlers; - private static readonly Dictionary s_invokeHandlerCache = new Dictionary(); + private static readonly Dictionary> s_invokeHandlerCache + = new Dictionary>(); /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; - private Dictionary> EventHandlers => _eventHandlers ??= new Dictionary>(); - /// /// Adds a handler for the specified routed event. /// @@ -130,105 +127,83 @@ namespace Avalonia.Interactivity throw new ArgumentException("Cannot raise an event whose RoutedEvent is null."); } - e.Source ??= this; - - if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct) - { - e.Route = RoutingStrategies.Direct; - RaiseEventImpl(e); - e.RoutedEvent.InvokeRouteFinished(e); - } - - if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0) - { - TunnelEvent(e); - e.RoutedEvent.InvokeRouteFinished(e); - } - - if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0) - { - BubbleEvent(e); - e.RoutedEvent.InvokeRouteFinished(e); - } + using var route = BuildEventRoute(e.RoutedEvent); + route.RaiseEvent(this, e); } - /// - /// Bubbles an event. - /// - /// The event args. - private void BubbleEvent(RoutedEventArgs e) + void IInteractive.AddToEventRoute(RoutedEvent routedEvent, EventRoute route) { - e = e ?? throw new ArgumentNullException(nameof(e)); - - e.Route = RoutingStrategies.Bubble; - - var traverser = HierarchyTraverser.Create(e); - - traverser.Traverse(this); - } - - /// - /// Tunnels an event. - /// - /// The event args. - private void TunnelEvent(RoutedEventArgs e) - { - e = e ?? throw new ArgumentNullException(nameof(e)); - - e.Route = RoutingStrategies.Tunnel; - - var traverser = HierarchyTraverser.Create(e); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + route = route ?? throw new ArgumentNullException(nameof(route)); - traverser.Traverse(this); + if (_eventHandlers != null && + _eventHandlers.TryGetValue(routedEvent, out var subscriptions)) + { + foreach (var sub in subscriptions) + { + route.Add(this, sub.Handler, sub.Routes, sub.HandledEventsToo, sub.InvokeAdapter); + } + } } /// - /// Carries out the actual invocation of an event on this object. + /// Builds an event route for a routed event. /// - /// The event args. - private void RaiseEventImpl(RoutedEventArgs e) + /// The routed event. + /// An describing the route. + /// + /// Usually, calling is sufficent to raise a routed + /// event, however there are situations in which the construction of the event args is expensive + /// and should be avoided if there are no handlers for an event. In these cases you can call + /// this method to build the event route and check the + /// property to see if there are any handlers registered on the route. If there are, call + /// to raise the event. + /// + protected EventRoute BuildEventRoute(RoutedEvent e) { e = e ?? throw new ArgumentNullException(nameof(e)); - e.RoutedEvent!.InvokeRaised(this, e); + var result = new EventRoute(e); + var hasClassHandlers = e.HasRaisedSubscriptions; - if (_eventHandlers is object && - _eventHandlers.TryGetValue(e.RoutedEvent, out var subscriptions) == true) + if (e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble) || + e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel)) { - foreach (var sub in subscriptions.ToList()) - { - bool correctRoute = (e.Route & sub.Routes) != 0; - bool notFinished = !e.Handled || sub.HandledEventsToo; + IInteractive? element = this; - if (correctRoute && notFinished) + while (element != null) + { + if (hasClassHandlers) { - if (sub.InvokeAdapter != null) - { - sub.InvokeAdapter(sub.Handler, this, e); - } - else - { - sub.Handler.DynamicInvoke(this, e); - } + result.AddClassHandler(element); } + + element.AddToEventRoute(e, result); + element = element.InteractiveParent; } } - } - - private List GetEventSubscriptions(RoutedEvent routedEvent) - { - if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions)) + else { - subscriptions = new List(); - EventHandlers.Add(routedEvent, subscriptions); + if (hasClassHandlers) + { + result.AddClassHandler(this); + } + + ((IInteractive)this).AddToEventRoute(e, result); } - return subscriptions; + return result; } private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) { - List subscriptions = GetEventSubscriptions(routedEvent); + _eventHandlers ??= new Dictionary>(); + + if (!_eventHandlers.TryGetValue(routedEvent, out var subscriptions)) + { + subscriptions = new List(); + _eventHandlers.Add(routedEvent, subscriptions); + } subscriptions.Add(subscription); @@ -251,67 +226,5 @@ namespace Avalonia.Interactivity _subscriptions.Remove(_subscription); } } - - private interface ITraverse - { - void Execute(IInteractive target, RoutedEventArgs e); - } - - private struct NopTraverse : ITraverse - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Execute(IInteractive target, RoutedEventArgs e) - { - } - } - - private struct RaiseEventTraverse : ITraverse - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Execute(IInteractive target, RoutedEventArgs e) - { - ((Interactive)target).RaiseEventImpl(e); - } - } - - /// - /// Traverses interactive hierarchy allowing for raising events. - /// - /// Called before parent is traversed. - /// Called after parent has been traversed. - private struct HierarchyTraverser - where TPreTraverse : struct, ITraverse - where TPostTraverse : struct, ITraverse - { - private TPreTraverse _preTraverse; - private TPostTraverse _postTraverse; - private readonly RoutedEventArgs _args; - - private HierarchyTraverser(TPreTraverse preTraverse, TPostTraverse postTraverse, RoutedEventArgs args) - { - _preTraverse = preTraverse; - _postTraverse = postTraverse; - _args = args; - } - - public static HierarchyTraverser Create(RoutedEventArgs args) - { - return new HierarchyTraverser(new TPreTraverse(), new TPostTraverse(), args); - } - - public void Traverse(IInteractive target) - { - _preTraverse.Execute(target, _args); - - var parent = target.InteractiveParent; - - if (parent != null) - { - Traverse(parent); - } - - _postTraverse.Execute(target, _args); - } - } } } diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index 164a86fab7..e515efd3b4 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -48,6 +48,8 @@ namespace Avalonia.Interactivity public RoutingStrategies RoutingStrategies { get; } + public bool HasRaisedSubscriptions => _raised.HasObservers; + public IObservable<(object, RoutedEventArgs)> Raised => _raised; public IObservable RouteFinished => _routeFinished; diff --git a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs index 0355078a05..ef3770d1d9 100644 --- a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Interactivity; using Avalonia.VisualTree; using Xunit; diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index f6454a9cd2..bf75b40a72 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -56,7 +56,7 @@ namespace Avalonia.UnitTests { _pressedButton = mouseButton; _pointer.Capture((IInputElement)target); - target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props, + source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props, GetModifiers(modifiers), clickCount)); } } From 0f7e3e1b8286c15aa85eaa56cbb8cce1e86b989c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Feb 2020 11:09:24 +0100 Subject: [PATCH 18/55] Make EventSubscription a private class. --- .../EventSubscription.cs | 30 ------------------- src/Avalonia.Interactivity/Interactive.cs | 23 ++++++++++++++ 2 files changed, 23 insertions(+), 30 deletions(-) delete mode 100644 src/Avalonia.Interactivity/EventSubscription.cs diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs deleted file mode 100644 index 50f64f49ee..0000000000 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; - -namespace Avalonia.Interactivity -{ - internal class EventSubscription - { - public EventSubscription( - Delegate handler, - RoutingStrategies routes, - bool handledEventsToo, - Action? invokeAdapter = null) - { - Handler = handler; - Routes = routes; - HandledEventsToo = handledEventsToo; - InvokeAdapter = invokeAdapter; - } - - public Action? InvokeAdapter { get; } - - public Delegate Handler { get; } - - public RoutingStrategies Routes { get; } - - public bool HandledEventsToo { get; } - } -} diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 5a27192c87..6992ebcf34 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -210,6 +210,29 @@ namespace Avalonia.Interactivity return new UnsubscribeDisposable(subscriptions, subscription); } + private sealed class EventSubscription + { + public EventSubscription( + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo, + Action? invokeAdapter = null) + { + Handler = handler; + Routes = routes; + HandledEventsToo = handledEventsToo; + InvokeAdapter = invokeAdapter; + } + + public Action? InvokeAdapter { get; } + + public Delegate Handler { get; } + + public RoutingStrategies Routes { get; } + + public bool HandledEventsToo { get; } + } + private sealed class UnsubscribeDisposable : IDisposable { private readonly List _subscriptions; From f5c9539c7a0365adaee6bd597c5df894fb26912a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Feb 2020 09:31:58 +0100 Subject: [PATCH 19/55] Use correct property type. --- src/Avalonia.Controls/MenuItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index ae36b5d830..1479d737a6 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -209,7 +209,7 @@ namespace Avalonia.Controls /// Setting this property does not cause the input gesture to be handled by the menu item, /// it simply displays the gesture text in the menu. /// - public object InputGestureText + public string InputGestureText { get { return GetValue(InputGestureTextProperty); } set { SetValue(InputGestureTextProperty, value); } From 2944099428f56240f3f35c55add86abf894b8064 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Feb 2020 10:50:39 +0100 Subject: [PATCH 20/55] Display gestures in NativeMenuBar. To do this we needed to change the `KeyGesture.ToString()` method to produce input gesture text suitable for menus. Also demonstrated in `MainWindow` how to produce different gestures/headers depending on platform. --- samples/ControlCatalog/MainWindow.xaml | 6 ++- samples/ControlCatalog/MainWindow.xaml.cs | 14 ++++--- src/Avalonia.Input/KeyGesture.cs | 42 +++++++++++++++---- .../NativeMenuBar.xaml | 1 + .../KeyGestureTests.cs | 31 +++++++++++--- 5 files changed, 73 insertions(+), 21 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 248f94082d..d25de9c1f5 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -14,7 +14,7 @@ - + @@ -22,7 +22,9 @@ - + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 38cbde9d92..b40fdb4a17 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -1,13 +1,11 @@ +using System; +using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Notifications; -using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Markup.Xaml; -using Avalonia.Threading; using ControlCatalog.ViewModels; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; namespace ControlCatalog { @@ -35,6 +33,12 @@ namespace ControlCatalog mainMenu.AttachedToVisualTree += MenuAttached; } + public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit"; + + public static KeyGesture MenuQuitGesture => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + new KeyGesture(Key.Q, KeyModifiers.Meta) : + new KeyGesture(Key.F4, KeyModifiers.Alt); + public void MenuAttached(object sender, VisualTreeAttachmentEventArgs e) { if (NativeMenu.GetIsNativeMenuExported(this) && sender is Menu mainMenu) diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index 490c31bef9..36920fdaad 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -1,9 +1,10 @@ -// Copyright (c) The Avalonia Project. All rights reserved. +// Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; using System.Collections.Generic; using System.Linq; +using System.Text; namespace Avalonia.Input { @@ -108,19 +109,43 @@ namespace Avalonia.Input public override string ToString() { - var parts = new List(); + var s = new StringBuilder(); - foreach (var flag in Enum.GetValues(typeof(KeyModifiers)).Cast()) + static void Plus(StringBuilder s) { - if (KeyModifiers.HasFlag(flag) && flag != KeyModifiers.None) + if (s.Length > 0) { - parts.Add(flag.ToString()); + s.Append("+"); } } - parts.Add(Key.ToString()); + if (KeyModifiers.HasFlagCustom(KeyModifiers.Control)) + { + s.Append("Ctrl"); + } + + if (KeyModifiers.HasFlagCustom(KeyModifiers.Shift)) + { + Plus(s); + s.Append("Shift"); + } + + if (KeyModifiers.HasFlagCustom(KeyModifiers.Alt)) + { + Plus(s); + s.Append("Alt"); + } + + if (KeyModifiers.HasFlagCustom(KeyModifiers.Meta)) + { + Plus(s); + s.Append("⌘"); + } + + Plus(s); + s.Append(Key); - return string.Join(" + ", parts); + return s.ToString(); } public bool Matches(KeyEventArgs keyEvent) => ResolveNumPadOperationKey(keyEvent.Key) == Key && keyEvent.KeyModifiers == KeyModifiers; @@ -141,7 +166,8 @@ namespace Avalonia.Input return KeyModifiers.Control; } - if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase)) + if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase) || + modifier.Equals("⌘".AsSpan(), StringComparison.OrdinalIgnoreCase)) { return KeyModifiers.Meta; } diff --git a/src/Avalonia.Themes.Default/NativeMenuBar.xaml b/src/Avalonia.Themes.Default/NativeMenuBar.xaml index 2832bab226..5d21378a64 100644 --- a/src/Avalonia.Themes.Default/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Default/NativeMenuBar.xaml @@ -13,6 +13,7 @@ + +