From 6e0edab57f4b8d51338fee9db788a1dfd46aa301 Mon Sep 17 00:00:00 2001 From: evan-choi Date: Thu, 2 Sep 2021 15:09:48 +0900 Subject: [PATCH 001/198] Fix OpenBrowser on Linux --- src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs index 55e30396e1..bd7e7c9c3e 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using Avalonia.Controls; using Avalonia.Markup.Xaml; @@ -42,7 +43,8 @@ namespace Avalonia.Dialogs private static void ShellExec(string cmd, bool waitForExit = true) { - var escapedArgs = cmd.Replace("\"", "\\\""); + var escapedArgs = Regex.Replace(cmd, "(?=[`~!#&*()|;'<>])", "\\") + .Replace("\"", "\\\\\\\""); using (var process = Process.Start( new ProcessStartInfo From 3438ac149befdb21161d434f9f88af7c1b09b0eb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 11:42:16 +0100 Subject: [PATCH 002/198] initial implementation of tray icon. --- samples/ControlCatalog/App.xaml.cs | 9 + src/Avalonia.Controls/ApiCompatBaseline.txt | 3 +- .../Platform/ITrayIconImpl.cs | 22 ++ .../Platform/IWindowingPlatform.cs | 3 + .../Platform/PlatformManager.cs | 13 + src/Avalonia.Controls/TrayIcon.cs | 107 ++++++++ .../Remote/PreviewerWindowingPlatform.cs | 4 +- .../Remote/TrayIconStub.cs | 24 ++ .../AvaloniaHeadlessPlatform.cs | 5 + src/Avalonia.Native/AvaloniaNativePlatform.cs | 5 + src/Avalonia.X11/X11Platform.cs | 6 + .../Interop/UnmanagedMethods.cs | 60 +++++ src/Windows/Avalonia.Win32/TrayIconImpl.cs | 252 ++++++++++++++++++ src/Windows/Avalonia.Win32/Win32Platform.cs | 5 + 14 files changed, 516 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Controls/Platform/ITrayIconImpl.cs create mode 100644 src/Avalonia.Controls/TrayIcon.cs create mode 100644 src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs create mode 100644 src/Windows/Avalonia.Win32/TrayIconImpl.cs diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index f3ec7b48aa..008ef6570b 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,5 +1,6 @@ using System; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; @@ -92,12 +93,20 @@ namespace ControlCatalog Styles.Insert(0, FluentLight); AvaloniaXamlLoader.Load(this); + + } public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { desktopLifetime.MainWindow = new MainWindow(); + + var trayIcon = new TrayIcon(); + + trayIcon.Icon = desktopLifetime.MainWindow.Icon; + } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index fac5923db5..4046900a3a 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -55,4 +55,5 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. -Total Issues: 56 +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. +Total Issues: 57 diff --git a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs new file mode 100644 index 0000000000..9690c26296 --- /dev/null +++ b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs @@ -0,0 +1,22 @@ +using System; + +namespace Avalonia.Platform +{ + public interface ITrayIconImpl + { + /// + /// Sets the icon of this tray icon. + /// + void SetIcon(IWindowIconImpl icon); + + /// + /// Sets the icon of this tray icon. + /// + void SetToolTipText(string? text); + + /// + /// Sets if the tray icon is visible or not. + /// + void SetIsVisible (bool visible); + } +} diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index be8939e19a..4efa92cc6b 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -3,6 +3,9 @@ namespace Avalonia.Platform public interface IWindowingPlatform { IWindowImpl CreateWindow(); + IWindowImpl CreateEmbeddableWindow(); + + ITrayIconImpl CreateTrayIcon(); } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index 19d034b4e2..fe83e37909 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -22,6 +22,19 @@ namespace Avalonia.Controls.Platform { } + public static ITrayIconImpl CreateTrayIcon () + { + var platform = AvaloniaLocator.Current.GetService(); + + if (platform == null) + { + throw new Exception("Could not CreateWindow(): IWindowingPlatform is not registered."); + } + + return s_designerMode ? null : platform.CreateTrayIcon(); + } + + public static IWindowImpl CreateWindow() { var platform = AvaloniaLocator.Current.GetService(); diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs new file mode 100644 index 0000000000..8cb1951c43 --- /dev/null +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -0,0 +1,107 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Controls +{ + public class TrayIcon : AvaloniaObject, IDataContextProvider + { + private readonly ITrayIconImpl _impl; + + private TrayIcon(ITrayIconImpl impl) + { + _impl = impl; + } + + public TrayIcon () : this(PlatformManager.CreateTrayIcon()) + { + + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty DataContextProperty = + StyledElement.DataContextProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IconProperty = + Window.IconProperty.AddOwner(); + + + public static readonly StyledProperty ToolTipTextProperty = + AvaloniaProperty.Register(nameof(ToolTipText)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsVisibleProperty = + Visual.IsVisibleProperty.AddOwner(); + + /// + /// Removes the notify icon from the taskbar notification area. + /// + public void Remove() + { + + } + + + public new ITrayIconImpl PlatformImpl => _impl; + + + /// + /// Gets or sets the Applications's data context. + /// + /// + /// The data context property specifies the default object that will + /// be used for data binding. + /// + public object? DataContext + { + get => GetValue(DataContextProperty); + set => SetValue(DataContextProperty, value); + } + + /// + /// Gets or sets the icon of the TrayIcon. + /// + public WindowIcon Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + /// + /// Gets or sets the tooltip text of the TrayIcon. + /// + public string? ToolTipText + { + get => GetValue(ToolTipTextProperty); + set => SetValue(ToolTipTextProperty, value); + } + + /// + /// Gets or sets the visibility of the TrayIcon. + /// + public bool IsVisible + { + get => GetValue(IsVisibleProperty); + set => SetValue(IsVisibleProperty, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if(change.Property == IconProperty) + { + _impl.SetIcon(Icon.PlatformImpl); + } + } + } +} diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 67b832318a..caca15b3a3 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -16,7 +16,9 @@ namespace Avalonia.DesignerSupport.Remote private static DetachableTransportConnection s_lastWindowTransport; private static PreviewerWindowImpl s_lastWindow; public static List PreFlightMessages = new List(); - + + public ITrayIconImpl CreateTrayIcon() => new TrayIconStub(); + public IWindowImpl CreateWindow() => new WindowStub(); public IWindowImpl CreateEmbeddableWindow() diff --git a/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs b/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs new file mode 100644 index 0000000000..939cf16824 --- /dev/null +++ b/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia.Platform; + +namespace Avalonia.DesignerSupport.Remote +{ + class TrayIconStub : ITrayIconImpl + { + public Action Clicked { get; set; } + public Action DoubleClicked { get; set; } + public Action RightClicked { get; set; } + + public void SetIcon(IWindowIconImpl icon) + { + } + + public void SetIsVisible(bool visible) + { + } + + public void SetToolTipText(string text) + { + } + } +} diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index fca2a1336f..afaec3a8a0 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -51,6 +51,11 @@ namespace Avalonia.Headless public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException(); public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); + + public ITrayIconImpl CreateTrayIcon() + { + throw new NotImplementedException(); + } } internal static void Initialize() diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index a7d05e416f..c98c56fcb1 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -134,6 +134,11 @@ namespace Avalonia.Native } } + public ITrayIconImpl CreateTrayIcon () + { + throw new NotImplementedException(); + } + public IWindowImpl CreateWindow() { return new WindowImpl(_factory, _options, _platformGl); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 3a919c8814..c6cea60efd 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -100,6 +100,12 @@ namespace Avalonia.X11 public IntPtr DeferredDisplay { get; set; } public IntPtr Display { get; set; } + + public ITrayIconImpl CreateTrayIcon () + { + throw new NotImplementedException(); + } + public IWindowImpl CreateWindow() { return new X11Window(this, null); diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index ad409810b8..22f46ae5cb 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1172,6 +1172,9 @@ namespace Avalonia.Win32.Interop GCW_ATOM = -32 } + [DllImport("shell32", CharSet = CharSet.Auto)] + public static extern int Shell_NotifyIcon(NIM dwMessage, NOTIFYICONDATA lpData); + [DllImport("user32.dll", EntryPoint = "SetClassLongPtr")] private static extern IntPtr SetClassLong64(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong); @@ -2271,4 +2274,61 @@ namespace Avalonia.Win32.Interop public uint VisibleMask; public uint DamageMask; } + + public enum NIM : uint + { + ADD = 0x00000000, + MODIFY = 0x00000001, + DELETE = 0x00000002, + SETFOCUS = 0x00000003, + SETVERSION = 0x00000004 + } + + [Flags] + public enum NIF : uint + { + MESSAGE = 0x00000001, + ICON = 0x00000002, + TIP = 0x00000004, + STATE = 0x00000008, + INFO = 0x00000010, + GUID = 0x00000020, + REALTIME = 0x00000040, + SHOWTIP = 0x00000080 + } + + [Flags] + public enum NIIF : uint + { + NONE = 0x00000000, + INFO = 0x00000001, + WARNING = 0x00000002, + ERROR = 0x00000003, + USER = 0x00000004, + ICON_MASK = 0x0000000F, + NOSOUND = 0x00000010, + LARGE_ICON = 0x00000020, + RESPECT_QUIET_TIME = 0x00000080 + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public class NOTIFYICONDATA + { + public int cbSize = Marshal.SizeOf(); + public IntPtr hWnd; + public int uID; + public NIF uFlags; + public int uCallbackMessage; + public IntPtr hIcon; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szTip; + public int dwState = 0; + public int dwStateMask = 0; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string szInfo; + public int uTimeoutOrVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string szInfoTitle; + public NIIF dwInfoFlags; + } } diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs new file mode 100644 index 0000000000..576dcd9d33 --- /dev/null +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Controls; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Platform; +using Avalonia.Win32.Interop; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32 +{ + /// + /// Custom Win32 window messages for the NotifyIcon + /// + public enum CustomWindowsMessage : uint + { + WM_TRAYICON = (uint)WindowsMessage.WM_APP + 1024, + WM_TRAYMOUSE = (uint)WindowsMessage.WM_USER + 1024 + } + + public class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup + { + public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); + private readonly MoveResizeDelegate _moveResize; + private Window _hiddenWindow; + + public TrayIconManagedPopupPositionerPopupImplHelper(MoveResizeDelegate moveResize) + { + _moveResize = moveResize; + _hiddenWindow = new Window(); + } + + public IReadOnlyList Screens => + _hiddenWindow.Screens.All.Select(s => new ManagedPopupPositionerScreenInfo( + s.Bounds.ToRect(1), s.Bounds.ToRect(1))).ToList(); + + public Rect ParentClientAreaScreenGeometry + { + get + { + var point = _hiddenWindow.Screens.Primary.Bounds.TopLeft; + var size = _hiddenWindow.Screens.Primary.Bounds.Size; + return new Rect(point.X, point.Y, size.Width * _hiddenWindow.Screens.Primary.PixelDensity, size.Height * _hiddenWindow.Screens.Primary.PixelDensity); + } + } + + public void MoveAndResize(Point devicePoint, Size virtualSize) + { + _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _hiddenWindow.Screens.Primary.PixelDensity); + } + + public virtual double Scaling => _hiddenWindow.Screens.Primary.PixelDensity; + } + + public class TrayPopupRoot : Window + { + private ManagedPopupPositioner _positioner; + + public TrayPopupRoot() + { + _positioner = new ManagedPopupPositioner(new TrayIconManagedPopupPositionerPopupImplHelper(MoveResize)); + Topmost = true; + + LostFocus += TrayPopupRoot_LostFocus; + } + + private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e) + { + Close(); + } + + private void MoveResize(PixelPoint position, Size size, double scaling) + { + PlatformImpl.Move(position); + PlatformImpl.Resize(size, PlatformResizeReason.Layout); + } + + protected override void ArrangeCore(Rect finalRect) + { + base.ArrangeCore(finalRect); + + _positioner.Update(new PopupPositionerParameters + { + Anchor = PopupAnchor.TopLeft, + Gravity = PopupGravity.BottomRight, + AnchorRectangle = new Rect(Position.ToPoint(1) / Screens.Primary.PixelDensity, new Size(1, 1)), + Size = finalRect.Size, + ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY | PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY, + }); + } + } + + public class TrayIconImpl : ITrayIconImpl + { + private readonly int _uniqueId = 0; + private static int _nextUniqueId = 0; + private WndProc _wndProcDelegate; + private IntPtr _hwnd; + private bool _iconAdded; + private IconImpl _icon; + + public TrayIconImpl() + { + _uniqueId = ++_nextUniqueId; + + CreateMessageWindow(); + + UpdateIcon(); + } + + + ~TrayIconImpl() + { + UpdateIcon(false); + } + + private void CreateMessageWindow() + { + // Ensure that the delegate doesn't get garbage collected by storing it as a field. + _wndProcDelegate = new UnmanagedMethods.WndProc(WndProc); + + UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + lpfnWndProc = _wndProcDelegate, + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpszClassName = "AvaloniaMessageWindow " + Guid.NewGuid(), + }; + + ushort atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + + if (atom == 0) + { + throw new Win32Exception(); + } + + _hwnd = UnmanagedMethods.CreateWindowEx(0, atom, null, 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + + if (_hwnd == IntPtr.Zero) + { + throw new Win32Exception(); + } + } + + public void SetIcon(IWindowIconImpl icon) + { + _icon = icon as IconImpl; + UpdateIcon(); + } + + public void SetIsVisible(bool visible) + { + if (visible) + { + + } + } + + public void SetToolTipText(string text) + { + throw new NotImplementedException(); + } + + private void UpdateIcon(bool remove = false) + { + var iconData = new NOTIFYICONDATA() + { + hWnd = _hwnd, + uID = _uniqueId, + uFlags = NIF.TIP | NIF.MESSAGE, + uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE, + hIcon = _icon?.HIcon ?? new IconImpl(new System.Drawing.Bitmap(32, 32)).HIcon, + szTip = "Tool tip text here." + }; + + if (!remove) + { + iconData.uFlags |= NIF.ICON; + + if (!_iconAdded) + { + UnmanagedMethods.Shell_NotifyIcon(NIM.ADD, iconData); + _iconAdded = true; + } + else + { + UnmanagedMethods.Shell_NotifyIcon(NIM.MODIFY, iconData); + } + } + else + { + UnmanagedMethods.Shell_NotifyIcon(NIM.DELETE, iconData); + _iconAdded = false; + } + } + + private void OnRightClicked() + { + UnmanagedMethods.GetCursorPos(out UnmanagedMethods.POINT pt); + var cursor = new PixelPoint(pt.X, pt.Y); + + var trayMenu = new TrayPopupRoot() + { + Position = cursor, + SystemDecorations = SystemDecorations.None, + SizeToContent = SizeToContent.WidthAndHeight, + Background = null, + TransparencyLevelHint = WindowTransparencyLevel.Transparent, + Content = new MenuFlyoutPresenter() + { + Items = new List + { + new MenuItem { Header = "Item 1"}, + new MenuItem { Header = "Item 2"}, + new MenuItem { Header = "Item 3"}, + new MenuItem { Header = "Item 4"}, + new MenuItem { Header = "Item 5"} + } + } + }; + + trayMenu.Show(); + } + + private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if (msg == (uint)CustomWindowsMessage.WM_TRAYMOUSE) + { + // Determine the type of message and call the matching event handlers + switch (lParam.ToInt32()) + { + case (int)WindowsMessage.WM_LBUTTONUP: + break; + + case (int)WindowsMessage.WM_LBUTTONDBLCLK: + break; + + case (int)WindowsMessage.WM_RBUTTONUP: + OnRightClicked(); + break; + + default: + break; + } + } + + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index c011a458c3..9316c9805c 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -293,6 +293,11 @@ namespace Avalonia.Win32 } } + public ITrayIconImpl CreateTrayIcon () + { + return new TrayIconImpl(); + } + public IWindowImpl CreateWindow() { return new WindowImpl(); From 0b9601dddb601cae4d471f73eeaec8c2d3218575 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 11:52:34 +0100 Subject: [PATCH 003/198] tidy win32 tray menu impl. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 102 +++++++++++---------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 576dcd9d33..1219e5eba5 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -16,8 +16,9 @@ namespace Avalonia.Win32 /// public enum CustomWindowsMessage : uint { - WM_TRAYICON = (uint)WindowsMessage.WM_APP + 1024, - WM_TRAYMOUSE = (uint)WindowsMessage.WM_USER + 1024 + WM_TRAYICON = WindowsMessage.WM_APP + 1024, + WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 + } public class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup @@ -66,17 +67,17 @@ namespace Avalonia.Win32 LostFocus += TrayPopupRoot_LostFocus; } - private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e) - { - Close(); - } - private void MoveResize(PixelPoint position, Size size, double scaling) { PlatformImpl.Move(position); PlatformImpl.Resize(size, PlatformResizeReason.Layout); } + private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e) + { + Close(); + } + protected override void ArrangeCore(Rect finalRect) { base.ArrangeCore(finalRect); @@ -87,7 +88,7 @@ namespace Avalonia.Win32 Gravity = PopupGravity.BottomRight, AnchorRectangle = new Rect(Position.ToPoint(1) / Screens.Primary.PixelDensity, new Size(1, 1)), Size = finalRect.Size, - ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY | PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY, + ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY, }); } } @@ -119,24 +120,24 @@ namespace Avalonia.Win32 private void CreateMessageWindow() { // Ensure that the delegate doesn't get garbage collected by storing it as a field. - _wndProcDelegate = new UnmanagedMethods.WndProc(WndProc); + _wndProcDelegate = new WndProc(WndProc); - UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX + WNDCLASSEX wndClassEx = new WNDCLASSEX { - cbSize = Marshal.SizeOf(), + cbSize = Marshal.SizeOf(), lpfnWndProc = _wndProcDelegate, - hInstance = UnmanagedMethods.GetModuleHandle(null), + hInstance = GetModuleHandle(null), lpszClassName = "AvaloniaMessageWindow " + Guid.NewGuid(), }; - ushort atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + ushort atom = RegisterClassEx(ref wndClassEx); if (atom == 0) { throw new Win32Exception(); } - _hwnd = UnmanagedMethods.CreateWindowEx(0, atom, null, 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + _hwnd = CreateWindowEx(0, atom, null, 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (_hwnd == IntPtr.Zero) { @@ -181,49 +182,21 @@ namespace Avalonia.Win32 if (!_iconAdded) { - UnmanagedMethods.Shell_NotifyIcon(NIM.ADD, iconData); + Shell_NotifyIcon(NIM.ADD, iconData); _iconAdded = true; } else { - UnmanagedMethods.Shell_NotifyIcon(NIM.MODIFY, iconData); + Shell_NotifyIcon(NIM.MODIFY, iconData); } } else { - UnmanagedMethods.Shell_NotifyIcon(NIM.DELETE, iconData); + Shell_NotifyIcon(NIM.DELETE, iconData); _iconAdded = false; } } - private void OnRightClicked() - { - UnmanagedMethods.GetCursorPos(out UnmanagedMethods.POINT pt); - var cursor = new PixelPoint(pt.X, pt.Y); - - var trayMenu = new TrayPopupRoot() - { - Position = cursor, - SystemDecorations = SystemDecorations.None, - SizeToContent = SizeToContent.WidthAndHeight, - Background = null, - TransparencyLevelHint = WindowTransparencyLevel.Transparent, - Content = new MenuFlyoutPresenter() - { - Items = new List - { - new MenuItem { Header = "Item 1"}, - new MenuItem { Header = "Item 2"}, - new MenuItem { Header = "Item 3"}, - new MenuItem { Header = "Item 4"}, - new MenuItem { Header = "Item 5"} - } - } - }; - - trayMenu.Show(); - } - private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { if (msg == (uint)CustomWindowsMessage.WM_TRAYMOUSE) @@ -232,9 +205,18 @@ namespace Avalonia.Win32 switch (lParam.ToInt32()) { case (int)WindowsMessage.WM_LBUTTONUP: + //if (!_doubleClick) + //{ + // Click?.Invoke(this, new EventArgs()); + //} + //_doubleClick = false; + + Debug.WriteLine($"Clicked {lParam:X}"); break; case (int)WindowsMessage.WM_LBUTTONDBLCLK: + //DoubleClick?.Invoke(this, new EventArgs()); + //_doubleClick = true; break; case (int)WindowsMessage.WM_RBUTTONUP: @@ -246,7 +228,35 @@ namespace Avalonia.Win32 } } - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + return DefWindowProc(hWnd, msg, wParam, lParam); + } + + private static void OnRightClicked() + { + var _trayMenu = new TrayPopupRoot() + { + SystemDecorations = SystemDecorations.None, + SizeToContent = SizeToContent.WidthAndHeight, + Background = null, + TransparencyLevelHint = WindowTransparencyLevel.Transparent, + Content = new MenuFlyoutPresenter() + { + Items = new List + { + new MenuItem { Header = "Item 1"}, + new MenuItem { Header = "Item 2"}, + new MenuItem { Header = "Item 3"}, + new MenuItem { Header = "Item 4"}, + new MenuItem { Header = "Item 5"} + } + } + }; + + GetCursorPos(out UnmanagedMethods.POINT pt); + + _trayMenu.Position = new PixelPoint(pt.X, pt.Y); + + _trayMenu.Show(); } } } From 2d2d8fa5a76a0fde45aaf5b93f3b0ae3da93b730 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 13:02:29 +0100 Subject: [PATCH 004/198] add example win32 tray icon. --- samples/ControlCatalog/App.xaml.cs | 8 +- samples/ControlCatalog/MainWindow.xaml.cs | 2 + src/Windows/Avalonia.Win32/TrayIconImpl.cs | 182 +++++++++++---------- 3 files changed, 103 insertions(+), 89 deletions(-) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 008ef6570b..663451e9c1 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -103,9 +103,13 @@ namespace ControlCatalog { desktopLifetime.MainWindow = new MainWindow(); - var trayIcon = new TrayIcon(); + var trayIcon1 = new TrayIcon(); - trayIcon.Icon = desktopLifetime.MainWindow.Icon; + trayIcon1.Icon = desktopLifetime.MainWindow.Icon; + + var trayIcon2 = new TrayIcon(); + + trayIcon2.Icon = desktopLifetime.MainWindow.Icon; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 2446c0e1c9..a9900471c5 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -35,6 +35,8 @@ namespace ControlCatalog var mainMenu = this.FindControl("MainMenu"); mainMenu.AttachedToVisualTree += MenuAttached; + + ExtendClientAreaChromeHints = Avalonia.Platform.ExtendClientAreaChromeHints.OSXThickTitleBar; } public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit"; diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 1219e5eba5..a27a913974 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -6,93 +6,12 @@ using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Platform; +using Avalonia.Threading; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 { - /// - /// Custom Win32 window messages for the NotifyIcon - /// - public enum CustomWindowsMessage : uint - { - WM_TRAYICON = WindowsMessage.WM_APP + 1024, - WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 - - } - - public class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup - { - public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); - private readonly MoveResizeDelegate _moveResize; - private Window _hiddenWindow; - - public TrayIconManagedPopupPositionerPopupImplHelper(MoveResizeDelegate moveResize) - { - _moveResize = moveResize; - _hiddenWindow = new Window(); - } - - public IReadOnlyList Screens => - _hiddenWindow.Screens.All.Select(s => new ManagedPopupPositionerScreenInfo( - s.Bounds.ToRect(1), s.Bounds.ToRect(1))).ToList(); - - public Rect ParentClientAreaScreenGeometry - { - get - { - var point = _hiddenWindow.Screens.Primary.Bounds.TopLeft; - var size = _hiddenWindow.Screens.Primary.Bounds.Size; - return new Rect(point.X, point.Y, size.Width * _hiddenWindow.Screens.Primary.PixelDensity, size.Height * _hiddenWindow.Screens.Primary.PixelDensity); - } - } - - public void MoveAndResize(Point devicePoint, Size virtualSize) - { - _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _hiddenWindow.Screens.Primary.PixelDensity); - } - - public virtual double Scaling => _hiddenWindow.Screens.Primary.PixelDensity; - } - - public class TrayPopupRoot : Window - { - private ManagedPopupPositioner _positioner; - - public TrayPopupRoot() - { - _positioner = new ManagedPopupPositioner(new TrayIconManagedPopupPositionerPopupImplHelper(MoveResize)); - Topmost = true; - - LostFocus += TrayPopupRoot_LostFocus; - } - - private void MoveResize(PixelPoint position, Size size, double scaling) - { - PlatformImpl.Move(position); - PlatformImpl.Resize(size, PlatformResizeReason.Layout); - } - - private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e) - { - Close(); - } - - protected override void ArrangeCore(Rect finalRect) - { - base.ArrangeCore(finalRect); - - _positioner.Update(new PopupPositionerParameters - { - Anchor = PopupAnchor.TopLeft, - Gravity = PopupGravity.BottomRight, - AnchorRectangle = new Rect(Position.ToPoint(1) / Screens.Primary.PixelDensity, new Size(1, 1)), - Size = finalRect.Size, - ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY, - }); - } - } - public class TrayIconImpl : ITrayIconImpl { private readonly int _uniqueId = 0; @@ -163,7 +82,14 @@ namespace Avalonia.Win32 { throw new NotImplementedException(); } - + /// + /// Custom Win32 window messages for the NotifyIcon + /// + public enum CustomWindowsMessage : uint + { + WM_TRAYICON = WindowsMessage.WM_APP + 1024, + WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 + } private void UpdateIcon(bool remove = false) { var iconData = new NOTIFYICONDATA() @@ -210,8 +136,6 @@ namespace Avalonia.Win32 // Click?.Invoke(this, new EventArgs()); //} //_doubleClick = false; - - Debug.WriteLine($"Clicked {lParam:X}"); break; case (int)WindowsMessage.WM_LBUTTONDBLCLK: @@ -226,9 +150,13 @@ namespace Avalonia.Win32 default: break; } - } - return DefWindowProc(hWnd, msg, wParam, lParam); + return IntPtr.Zero; + } + else + { + return DefWindowProc(hWnd, msg, wParam, lParam); + } } private static void OnRightClicked() @@ -258,5 +186,85 @@ namespace Avalonia.Win32 _trayMenu.Show(); } + + class TrayPopupRoot : Window + { + private ManagedPopupPositioner _positioner; + + public TrayPopupRoot() + { + _positioner = new ManagedPopupPositioner(new TrayIconManagedPopupPositionerPopupImplHelper(MoveResize)); + Topmost = true; + + Deactivated += TrayPopupRoot_Deactivated; + } + + private void TrayPopupRoot_Deactivated(object sender, EventArgs e) + { + Dispatcher.UIThread.Post(() => + { + Close(); + }); + } + + private void MoveResize(PixelPoint position, Size size, double scaling) + { + PlatformImpl.Move(position); + PlatformImpl.Resize(size, PlatformResizeReason.Layout); + } + + private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e) + { + Close(); + } + + protected override void ArrangeCore(Rect finalRect) + { + base.ArrangeCore(finalRect); + + _positioner.Update(new PopupPositionerParameters + { + Anchor = PopupAnchor.TopLeft, + Gravity = PopupGravity.BottomRight, + AnchorRectangle = new Rect(Position.ToPoint(1) / Screens.Primary.PixelDensity, new Size(1, 1)), + Size = finalRect.Size, + ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY, + }); + } + + class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup + { + public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); + private readonly MoveResizeDelegate _moveResize; + private Window _hiddenWindow; + + public TrayIconManagedPopupPositionerPopupImplHelper(MoveResizeDelegate moveResize) + { + _moveResize = moveResize; + _hiddenWindow = new Window(); + } + + public IReadOnlyList Screens => + _hiddenWindow.Screens.All.Select(s => new ManagedPopupPositionerScreenInfo( + s.Bounds.ToRect(1), s.Bounds.ToRect(1))).ToList(); + + public Rect ParentClientAreaScreenGeometry + { + get + { + var point = _hiddenWindow.Screens.Primary.Bounds.TopLeft; + var size = _hiddenWindow.Screens.Primary.Bounds.Size; + return new Rect(point.X, point.Y, size.Width * _hiddenWindow.Screens.Primary.PixelDensity, size.Height * _hiddenWindow.Screens.Primary.PixelDensity); + } + } + + public void MoveAndResize(Point devicePoint, Size virtualSize) + { + _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _hiddenWindow.Screens.Primary.PixelDensity); + } + + public virtual double Scaling => _hiddenWindow.Screens.Primary.PixelDensity; + } + } } } From bc772cce40204da51167e5e15256231ee7e93cfe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 13:45:36 +0100 Subject: [PATCH 005/198] dont create multiple message queues for tray icons. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 50 +++++++-------------- src/Windows/Avalonia.Win32/Win32Platform.cs | 6 +++ 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index a27a913974..f058759279 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using Avalonia.Controls; @@ -16,16 +17,24 @@ namespace Avalonia.Win32 { private readonly int _uniqueId = 0; private static int _nextUniqueId = 0; - private WndProc _wndProcDelegate; - private IntPtr _hwnd; private bool _iconAdded; private IconImpl _icon; + private static Dictionary s_trayIcons = new Dictionary(); + + internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if(msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.ContainsKey(wParam.ToInt32())) + { + s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam); + } + } + public TrayIconImpl() { _uniqueId = ++_nextUniqueId; - CreateMessageWindow(); + s_trayIcons.Add(_uniqueId, this); UpdateIcon(); } @@ -36,34 +45,6 @@ namespace Avalonia.Win32 UpdateIcon(false); } - private void CreateMessageWindow() - { - // Ensure that the delegate doesn't get garbage collected by storing it as a field. - _wndProcDelegate = new WndProc(WndProc); - - WNDCLASSEX wndClassEx = new WNDCLASSEX - { - cbSize = Marshal.SizeOf(), - lpfnWndProc = _wndProcDelegate, - hInstance = GetModuleHandle(null), - lpszClassName = "AvaloniaMessageWindow " + Guid.NewGuid(), - }; - - ushort atom = RegisterClassEx(ref wndClassEx); - - if (atom == 0) - { - throw new Win32Exception(); - } - - _hwnd = CreateWindowEx(0, atom, null, 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - - if (_hwnd == IntPtr.Zero) - { - throw new Win32Exception(); - } - } - public void SetIcon(IWindowIconImpl icon) { _icon = icon as IconImpl; @@ -94,7 +75,7 @@ namespace Avalonia.Win32 { var iconData = new NOTIFYICONDATA() { - hWnd = _hwnd, + hWnd = Win32Platform.Instance.Handle, uID = _uniqueId, uFlags = NIF.TIP | NIF.MESSAGE, uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE, @@ -125,6 +106,7 @@ namespace Avalonia.Win32 private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { + Debug.WriteLine(wParam); if (msg == (uint)CustomWindowsMessage.WM_TRAYMOUSE) { // Determine the type of message and call the matching event handlers @@ -180,7 +162,7 @@ namespace Avalonia.Win32 } }; - GetCursorPos(out UnmanagedMethods.POINT pt); + GetCursorPos(out POINT pt); _trayMenu.Position = new PixelPoint(pt.X, pt.Y); @@ -197,6 +179,8 @@ namespace Avalonia.Win32 Topmost = true; Deactivated += TrayPopupRoot_Deactivated; + + ShowInTaskbar = false; } private void TrayPopupRoot_Deactivated(object sender, EventArgs e) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 9316c9805c..45fa8f44ce 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -108,6 +108,10 @@ namespace Avalonia.Win32 CreateMessageWindow(); } + internal static Win32Platform Instance => s_instance; + + internal IntPtr Handle => _hwnd; + /// /// Gets the actual WindowsVersion. Same as the info returned from RtlGetVersion. /// @@ -261,6 +265,8 @@ namespace Avalonia.Win32 } } } + + TrayIconImpl.ProcWnd(hWnd, msg, wParam, lParam); return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); } From 35dc6ced0395410ea919cf7382cba9ac11bcea5e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 13:49:12 +0100 Subject: [PATCH 006/198] remove unused code. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 32 ++++++++-------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index f058759279..54337b57be 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Platform; @@ -24,7 +21,7 @@ namespace Avalonia.Win32 internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { - if(msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.ContainsKey(wParam.ToInt32())) + if (msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.ContainsKey(wParam.ToInt32())) { s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam); } @@ -63,14 +60,8 @@ namespace Avalonia.Win32 { throw new NotImplementedException(); } - /// - /// Custom Win32 window messages for the NotifyIcon - /// - public enum CustomWindowsMessage : uint - { - WM_TRAYICON = WindowsMessage.WM_APP + 1024, - WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 - } + + private void UpdateIcon(bool remove = false) { var iconData = new NOTIFYICONDATA() @@ -106,23 +97,15 @@ namespace Avalonia.Win32 private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { - Debug.WriteLine(wParam); if (msg == (uint)CustomWindowsMessage.WM_TRAYMOUSE) { // Determine the type of message and call the matching event handlers switch (lParam.ToInt32()) { case (int)WindowsMessage.WM_LBUTTONUP: - //if (!_doubleClick) - //{ - // Click?.Invoke(this, new EventArgs()); - //} - //_doubleClick = false; break; case (int)WindowsMessage.WM_LBUTTONDBLCLK: - //DoubleClick?.Invoke(this, new EventArgs()); - //_doubleClick = true; break; case (int)WindowsMessage.WM_RBUTTONUP: @@ -169,6 +152,15 @@ namespace Avalonia.Win32 _trayMenu.Show(); } + /// + /// Custom Win32 window messages for the NotifyIcon + /// + enum CustomWindowsMessage : uint + { + WM_TRAYICON = WindowsMessage.WM_APP + 1024, + WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 + } + class TrayPopupRoot : Window { private ManagedPopupPositioner _positioner; From 26e221e7f5fd613d918b4dd4ca24110f56ac19d7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 14:38:45 +0100 Subject: [PATCH 007/198] Win32 trayicon, make menu close when item is clicked. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 54337b57be..7f1f8763c0 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.LogicalTree; using Avalonia.Platform; +using Avalonia.Styling; using Avalonia.Threading; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -132,7 +134,7 @@ namespace Avalonia.Win32 SizeToContent = SizeToContent.WidthAndHeight, Background = null, TransparencyLevelHint = WindowTransparencyLevel.Transparent, - Content = new MenuFlyoutPresenter() + Content = new TrayIconMenuFlyoutPresenter() { Items = new List { @@ -161,6 +163,22 @@ namespace Avalonia.Win32 WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 } + class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable + { + Type IStyleable.StyleKey => typeof(MenuFlyoutPresenter); + + public override void Close() + { + // DefaultMenuInteractionHandler calls this + var host = this.FindLogicalAncestorOfType(); + if (host != null) + { + SelectedIndex = -1; + host.Close(); + } + } + } + class TrayPopupRoot : Window { private ManagedPopupPositioner _positioner; From 30f6145d73cccacaaf2bd617352d00325f8e2326 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 15:52:42 +0100 Subject: [PATCH 008/198] implement visible changed and tooltip. --- samples/ControlCatalog/App.xaml | 11 ++++ samples/ControlCatalog/App.xaml.cs | 10 ---- .../Platform/ITrayIconImpl.cs | 5 +- src/Avalonia.Controls/TrayIcon.cs | 55 +++++++++++++++++++ src/Windows/Avalonia.Win32/TrayIconImpl.cs | 21 ++++--- 5 files changed, 80 insertions(+), 22 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 6aad44c0d5..ec3734f4f5 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -1,6 +1,17 @@ + + + + + + + + + + + - - - - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 5f7dc248c0..36b6fc2dcd 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -5,11 +5,17 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; +using ControlCatalog.ViewModels; namespace ControlCatalog { public class App : Application { + public App() + { + DataContext = new ApplicationViewModel(); + } + private static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml") diff --git a/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs new file mode 100644 index 0000000000..c96872ef7f --- /dev/null +++ b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ApplicationViewModel : ViewModelBase + { + public ApplicationViewModel() + { + ExitCommand = MiniCommand.Create(() => + { + if(Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.Shutdown(); + } + }); + } + + public MiniCommand ExitCommand { get; } + } +} diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index dfa71dcb1e..bdfea40b5e 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls { } - public class TrayIcon : AvaloniaObject, IDataContextProvider, INativeMenuExporterProvider, IDisposable + public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable { private readonly ITrayIconImpl _impl; @@ -79,12 +79,6 @@ namespace Avalonia.Controls public static readonly AttachedProperty TrayIconsProperty = AvaloniaProperty.RegisterAttached("TrayIcons"); - /// - /// Defines the property. - /// - public static readonly StyledProperty DataContextProperty = - StyledElement.DataContextProperty.AddOwner(); - /// /// Defines the property. /// @@ -117,20 +111,6 @@ namespace Avalonia.Controls public new ITrayIconImpl PlatformImpl => _impl; - - /// - /// Gets or sets the Applications's data context. - /// - /// - /// The data context property specifies the default object that will - /// be used for data binding. - /// - public object? DataContext - { - get => GetValue(DataContextProperty); - set => SetValue(DataContextProperty, value); - } - /// /// Gets or sets the icon of the TrayIcon. /// From 5e87cffc9cc9cdf25987b508fa76f0536dc164b3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 16:02:58 +0100 Subject: [PATCH 012/198] CompiledBinding correctly locates IDataContextProvider as anchor. (implementation was in ReflectionBinding but missing from CompiledBinding) --- .../MarkupExtensions/CompiledBindingExtension.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index 17d2ea7ae9..5c4d9315d5 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -44,6 +44,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions // the context. object anchor = provider.GetFirstParent(); + if (anchor is null) + { + // Try to find IDataContextProvider, this was added to allow us to find + // a datacontext for Application class when using NativeMenuItems. + anchor = provider.GetFirstParent(); + } + // If a control was not found, then try to find the highest-level style as the XAML // file could be a XAML file containing only styles. return anchor ?? From 0c55449d8b5578678c287393ae16d88bcee86d9b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 16:15:50 +0100 Subject: [PATCH 013/198] restore removed code. --- samples/ControlCatalog/App.xaml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 7a5dc0d9ec..d12eef6b4c 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -4,7 +4,29 @@ x:DataType="vm:ApplicationViewModel" x:CompileBindings="True" x:Class="ControlCatalog.App"> - + + + + + + + + + From 6ae59214a45eab407c7b9c4e1ef4a6994cb5a520 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 17:33:26 +0100 Subject: [PATCH 014/198] add initial implementation for osx tray icon support. --- .../project.pbxproj | 6 ++ native/Avalonia.Native/src/OSX/common.h | 1 + native/Avalonia.Native/src/OSX/main.mm | 11 +++ native/Avalonia.Native/src/OSX/trayicon.h | 30 ++++++++ native/Avalonia.Native/src/OSX/trayicon.mm | 59 +++++++++++++++ src/Avalonia.Native/AvaloniaNativePlatform.cs | 2 +- src/Avalonia.Native/TrayIconImpl.cs | 75 +++++++++++++++++++ src/Avalonia.Native/avn.idl | 15 ++++ .../Win32NativeToManagedMenuExporter.cs | 2 +- 9 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/trayicon.h create mode 100644 native/Avalonia.Native/src/OSX/trayicon.mm create mode 100644 src/Avalonia.Native/TrayIconImpl.cs diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index dba3ee6d31..85fcf20034 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; }; 520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; }; 522D5959258159C1006F7F7A /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 522D5958258159C1006F7F7A /* Carbon.framework */; }; + 523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */ = {isa = PBXBuildFile; fileRef = 523484C926EA688F00EA0C2C /* trayicon.mm */; }; 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; @@ -51,6 +52,8 @@ 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; }; 520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = ""; }; 522D5958258159C1006F7F7A /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; + 523484C926EA688F00EA0C2C /* trayicon.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = trayicon.mm; sourceTree = ""; }; + 523484CB26EA68AA00EA0C2C /* trayicon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = trayicon.h; sourceTree = ""; }; 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; @@ -114,6 +117,8 @@ AB00E4F62147CA920032A60A /* main.mm */, 37155CE3233C00EB0034DCE9 /* menu.h */, 520624B222973F4100C4DCEF /* menu.mm */, + 523484C926EA688F00EA0C2C /* trayicon.mm */, + 523484CB26EA68AA00EA0C2C /* trayicon.h */, 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */, 37A517B22159597E00FBA241 /* Screens.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, @@ -204,6 +209,7 @@ 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */, 5B21A982216530F500CEE36E /* cursor.mm in Sources */, 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, + 523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */, AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */, 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index c082003ccf..5c174eb663 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -22,6 +22,7 @@ extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); +extern IAvnTrayIcon* CreateTrayIcon(IAvnTrayIconEvents* events); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 3e152a6125..f179d4f049 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -303,6 +303,17 @@ public: } } + virtual HRESULT CreateTrayIcon (IAvnTrayIconEvents*cb, IAvnTrayIcon** ppv) override + { + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateTrayIcon(cb); + return S_OK; + } + } + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override { START_COM_CALL; diff --git a/native/Avalonia.Native/src/OSX/trayicon.h b/native/Avalonia.Native/src/OSX/trayicon.h new file mode 100644 index 0000000000..4329668cbd --- /dev/null +++ b/native/Avalonia.Native/src/OSX/trayicon.h @@ -0,0 +1,30 @@ +// +// trayicon.h +// Avalonia.Native.OSX +// +// Created by Dan Walmsley on 09/09/2021. +// Copyright © 2021 Avalonia. All rights reserved. +// + +#ifndef trayicon_h +#define trayicon_h + +#include "common.h" + +class AvnTrayIcon : public ComSingleObject +{ +private: + NSStatusItem* _native; + ComPtr _events; + +public: + FORWARD_IUNKNOWN() + + AvnTrayIcon(IAvnTrayIconEvents* events); + + virtual HRESULT SetIcon (void* data, size_t length) override; + + virtual HRESULT SetMenu (IAvnMenu* menu) override; +}; + +#endif /* trayicon_h */ diff --git a/native/Avalonia.Native/src/OSX/trayicon.mm b/native/Avalonia.Native/src/OSX/trayicon.mm new file mode 100644 index 0000000000..959762a663 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/trayicon.mm @@ -0,0 +1,59 @@ +#include "common.h" +#include "trayicon.h" + +extern IAvnTrayIcon* CreateTrayIcon(IAvnTrayIconEvents* cb) +{ + @autoreleasepool + { + return new AvnTrayIcon(cb); + } +} + +AvnTrayIcon::AvnTrayIcon(IAvnTrayIconEvents* events) +{ + _events = events; + + _native = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength]; +} + +HRESULT AvnTrayIcon::SetIcon (void* data, size_t length) +{ + START_COM_CALL; + + @autoreleasepool + { + if(data != nullptr) + { + NSData *imageData = [NSData dataWithBytes:data length:length]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + NSSize originalSize = [image size]; + + NSSize size; + size.height = [[NSFont menuFontOfSize:0] pointSize] * 1.333333; + + auto scaleFactor = size.height / originalSize.height; + size.width = originalSize.width * scaleFactor; + + [image setSize: size]; + [_native setImage:image]; + } + else + { + [_native setImage:nullptr]; + } + return S_OK; + } +} + +HRESULT AvnTrayIcon::SetMenu (IAvnMenu* menu) +{ + START_COM_CALL; + + @autoreleasepool + { + + } + + return S_OK; +} diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index c98c56fcb1..eaf4d0e2e4 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -136,7 +136,7 @@ namespace Avalonia.Native public ITrayIconImpl CreateTrayIcon () { - throw new NotImplementedException(); + return new TrayIconImpl(_factory); } public IWindowImpl CreateWindow() diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs new file mode 100644 index 0000000000..bbeb6c4452 --- /dev/null +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using Avalonia.Controls.Platform; +using Avalonia.Native.Interop; +using Avalonia.Platform; + +namespace Avalonia.Native +{ + class TrayIconEvents : CallbackBase, IAvnTrayIconEvents + { + private TrayIconImpl _parent; + + public TrayIconEvents (TrayIconImpl parent) + { + _parent = parent; + } + + public void Clicked() + { + } + + public void DoubleClicked() + { + } + } + + internal class TrayIconImpl : ITrayIconImpl + { + private readonly IAvnTrayIcon _native; + + public TrayIconImpl(IAvaloniaNativeFactory factory) + { + _native = factory.CreateTrayIcon(new TrayIconEvents(this)); + } + + public void Dispose() + { + + } + + public unsafe void SetIcon(IWindowIconImpl? icon) + { + if(icon is null) + { + _native.SetIcon(null, IntPtr.Zero); + } + else + { + using (var ms = new MemoryStream()) + { + icon.Save(ms); + + var imageData = ms.ToArray(); + + fixed(void* ptr = imageData) + { + _native.SetIcon(ptr, new IntPtr(imageData.Length)); + } + } + } + } + + public void SetToolTipText(string? text) + { + // NOP + } + + public void SetIsVisible(bool visible) + { + + } + + public INativeMenuExporter? MenuExporter { get; } + } +} diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 70d85dacdd..47ed7116a7 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -427,6 +427,7 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv); HRESULT CreateMenuItem(IAvnMenuItem** ppv); HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); + HRESULT CreateTrayIcon(IAvnTrayIconEvents* cb, IAvnTrayIcon** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -665,6 +666,20 @@ interface IAvnGlSurfaceRenderingSession : IUnknown HRESULT GetScaling(double* ret); } +[uuid(60992d19-38f0-4141-a0a9-76ac303801f3)] +interface IAvnTrayIcon : IUnknown +{ + HRESULT SetIcon(void* data, size_t length); + HRESULT SetMenu(IAvnMenu* menu); +} + +[uuid(a687a6d9-73aa-4fef-9b4a-61587d7285d3)] +interface IAvnTrayIconEvents : IUnknown +{ + void Clicked (); + void DoubleClicked (); +} + [uuid(a7724dc1-cf6b-4fa8-9d23-228bf2593edc)] interface IAvnMenu : IUnknown { diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index 57fccad633..8663aec773 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -35,7 +35,7 @@ namespace Avalonia.Win32 } else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) { - newItem.Click += (_,_) => bridge.RaiseClicked(); + newItem.Click += (s, e) => bridge.RaiseClicked(); } items.Add(newItem); From 0e703c9209e4e12def5ade921e5bb743a35fb911 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 17:53:30 +0100 Subject: [PATCH 015/198] Add trayicon menu export support for osx. --- native/Avalonia.Native/src/OSX/trayicon.mm | 6 +++ .../AvaloniaNativeMenuExporter.cs | 52 ++++++++++++++++--- src/Avalonia.Native/TrayIconImpl.cs | 2 + 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/trayicon.mm b/native/Avalonia.Native/src/OSX/trayicon.mm index 959762a663..67b6bd4874 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.mm +++ b/native/Avalonia.Native/src/OSX/trayicon.mm @@ -1,5 +1,6 @@ #include "common.h" #include "trayicon.h" +#include "menu.h" extern IAvnTrayIcon* CreateTrayIcon(IAvnTrayIconEvents* cb) { @@ -52,7 +53,12 @@ HRESULT AvnTrayIcon::SetMenu (IAvnMenu* menu) @autoreleasepool { + auto appMenu = dynamic_cast(menu); + if(appMenu != nullptr) + { + [_native setMenu:appMenu->GetNative()]; + } } return S_OK; diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 89efa6af0c..dd52bd3544 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -17,6 +17,7 @@ namespace Avalonia.Native private IAvnWindow _nativeWindow; private NativeMenu _menu; private __MicroComIAvnMenuProxy _nativeMenu; + private IAvnTrayIcon _trayIcon; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -33,6 +34,14 @@ namespace Avalonia.Native DoLayoutReset(); } + public AvaloniaNativeMenuExporter(IAvnTrayIcon trayIcon, IAvaloniaNativeFactory factory) + { + _factory = factory; + _trayIcon = trayIcon; + + DoLayoutReset(); + } + public bool IsNativeMenuExported => _exported; public event EventHandler OnIsNativeMenuExportedChanged; @@ -82,15 +91,25 @@ namespace Avalonia.Native if (_nativeWindow is null) { - var appMenu = NativeMenu.GetMenu(Application.Current); + if (_trayIcon is null) + { + var appMenu = NativeMenu.GetMenu(Application.Current); + + if (appMenu == null) + { + appMenu = CreateDefaultAppMenu(); + NativeMenu.SetMenu(Application.Current, appMenu); + } - if (appMenu == null) + SetMenu(appMenu); + } + else { - appMenu = CreateDefaultAppMenu(); - NativeMenu.SetMenu(Application.Current, appMenu); + if (_menu != null) + { + SetMenu(_trayIcon, _menu); + } } - - SetMenu(appMenu); } else { @@ -171,5 +190,26 @@ namespace Avalonia.Native avnWindow.SetMainMenu(_nativeMenu); } } + + private void SetMenu(IAvnTrayIcon trayIcon, NativeMenu menu) + { + var setMenu = false; + + if (_nativeMenu is null) + { + _nativeMenu = __MicroComIAvnMenuProxy.Create(_factory); + + _nativeMenu.Initialize(this, menu, ""); + + setMenu = true; + } + + _nativeMenu.Update(_factory, menu); + + if(setMenu) + { + trayIcon.SetMenu(_nativeMenu); + } + } } } diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs index bbeb6c4452..b5cb0d8c08 100644 --- a/src/Avalonia.Native/TrayIconImpl.cs +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -31,6 +31,8 @@ namespace Avalonia.Native public TrayIconImpl(IAvaloniaNativeFactory factory) { _native = factory.CreateTrayIcon(new TrayIconEvents(this)); + + MenuExporter = new AvaloniaNativeMenuExporter(_native, factory); } public void Dispose() From f4031c67a28defa20ff7035524d2a250bc956188 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 19:56:15 +0100 Subject: [PATCH 016/198] more complex test menu for tray icons. --- samples/ControlCatalog/App.xaml | 11 ++++++++++- .../ControlCatalog/ViewModels/ApplicationViewModel.cs | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index d12eef6b4c..07737a087c 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -25,13 +25,22 @@ - + + + + + + + + + + diff --git a/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs index c96872ef7f..6cd44eecaf 100644 --- a/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs @@ -15,8 +15,12 @@ namespace ControlCatalog.ViewModels lifetime.Shutdown(); } }); + + ToggleCommand = MiniCommand.Create(() => { }); } public MiniCommand ExitCommand { get; } + + public MiniCommand ToggleCommand { get; } } } From a8c435ebbb74c5221eb139bc31217a106f60ef34 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 19:59:59 +0100 Subject: [PATCH 017/198] show checks in tray menu. --- samples/ControlCatalog/App.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 07737a087c..3f8b768f6b 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -34,10 +34,10 @@ - - + + - + From 9c0c6f7efc2941d332e00d76b2c5e0d7e0f55387 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 20:40:41 +0100 Subject: [PATCH 018/198] fix sub-menus win32 trayicons --- src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index 8663aec773..72a7a6ff35 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -31,7 +31,7 @@ namespace Avalonia.Win32 if(item.Menu != null) { - newItem.ContextMenu = new ContextMenu() { Items = Populate(item.Menu) }; + newItem.Items = Populate(item.Menu); } else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) { From 59e66f72f2a681dcf5cd69118bf94215bc9fab82 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 11:39:22 +0100 Subject: [PATCH 019/198] add some documentation. --- src/Avalonia.Controls/TrayIcon.cs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index bdfea40b5e..73f1fcb006 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -71,11 +71,13 @@ namespace Avalonia.Controls { foreach(var icon in icons) { - icon.Remove(); + icon.Dispose(); } } - + /// + /// Defines the attached property. + /// public static readonly AttachedProperty TrayIconsProperty = AvaloniaProperty.RegisterAttached("TrayIcons"); @@ -85,29 +87,22 @@ namespace Avalonia.Controls public static readonly StyledProperty IconProperty = Window.IconProperty.AddOwner(); - + /// + /// Defines the property. + /// public static readonly StyledProperty ToolTipTextProperty = AvaloniaProperty.Register(nameof(ToolTipText)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty IsVisibleProperty = Visual.IsVisibleProperty.AddOwner(); - private bool _disposedValue; public static void SetTrayIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(TrayIconsProperty, trayIcons); public static TrayIcons GetTrayIcons(AvaloniaObject o) => o.GetValue(TrayIconsProperty); - /// - /// Removes the notify icon from the taskbar notification area. - /// - public void Remove() - { - - } - public new ITrayIconImpl PlatformImpl => _impl; @@ -158,6 +153,9 @@ namespace Avalonia.Controls } } + /// + /// Disposes the tray icon (removing it from the tray area). + /// public void Dispose() => _impl.Dispose(); } } From 59f3ce055eb71a7c0f11f50b248646467680092a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 11:51:50 +0100 Subject: [PATCH 020/198] remove unused property. --- src/Avalonia.Controls/TrayIcon.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 73f1fcb006..cfff568a4a 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -103,9 +103,6 @@ namespace Avalonia.Controls public static TrayIcons GetTrayIcons(AvaloniaObject o) => o.GetValue(TrayIconsProperty); - - public new ITrayIconImpl PlatformImpl => _impl; - /// /// Gets or sets the icon of the TrayIcon. /// From abf4242280b71160ec7845f42a051b87b0534927 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 12:08:54 +0100 Subject: [PATCH 021/198] support trayicon clicked on osx. --- .../Platform/ITrayIconImpl.cs | 8 +++ src/Avalonia.Controls/TrayIcon.cs | 60 ++++++++++++------- .../Remote/TrayIconStub.cs | 4 +- src/Avalonia.Native/TrayIconImpl.cs | 6 +- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 36 ++++++----- 5 files changed, 71 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs index 013aff13ee..12a32ec64b 100644 --- a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs +++ b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs @@ -23,6 +23,14 @@ namespace Avalonia.Platform /// void SetIsVisible (bool visible); + /// + /// Gets the MenuExporter to allow native menus to be exported to the TrayIcon. + /// INativeMenuExporter? MenuExporter { get; } + + /// + /// Gets or Sets the Action that is called when the TrayIcon is clicked. + /// + Action? OnClicked { get; set; } } } diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index cfff568a4a..4d86f9ddc1 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -22,6 +22,15 @@ namespace Avalonia.Controls _impl = impl; _impl.SetIsVisible(IsVisible); + + _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty); + + Clicked += TrayIcon_Clicked; + } + + private void TrayIcon_Clicked(object sender, EventArgs e) + { + } public TrayIcon () : this(PlatformManager.CreateTrayIcon()) @@ -52,28 +61,12 @@ namespace Avalonia.Controls } } - private static void Lifetime_Exit(object sender, ControlledApplicationLifetimeExitEventArgs e) - { - var trayIcons = GetTrayIcons(Application.Current); - - foreach(var icon in trayIcons) - { - icon.Dispose(); - } - } - - private static void Icons_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) - { - - } - - private static void RemoveIcons (IEnumerable icons) - { - foreach(var icon in icons) - { - icon.Dispose(); - } - } + /// + /// Raised when the TrayIcon is clicked. + /// Note, this is only supported on Win32. + /// Linux and OSX this event is not raised. + /// + public event EventHandler? Clicked; /// /// Defines the attached property. @@ -132,6 +125,29 @@ namespace Avalonia.Controls public INativeMenuExporter? NativeMenuExporter => _impl.MenuExporter; + private static void Lifetime_Exit(object sender, ControlledApplicationLifetimeExitEventArgs e) + { + var trayIcons = GetTrayIcons(Application.Current); + + foreach (var icon in trayIcons) + { + icon.Dispose(); + } + } + + private static void Icons_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + + } + + private static void RemoveIcons(IEnumerable icons) + { + foreach (var icon in icons) + { + icon.Dispose(); + } + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs b/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs index 6fd70f203c..88ca076f8a 100644 --- a/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs +++ b/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs @@ -11,7 +11,9 @@ namespace Avalonia.DesignerSupport.Remote public Action DoubleClicked { get; set; } public Action RightClicked { get; set; } - public INativeMenuExporter MenuExporter => throw new NotImplementedException(); + public INativeMenuExporter MenuExporter => null; + + public Action OnClicked { get; set; } public void Dispose() { diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs index b5cb0d8c08..7e2ade901c 100644 --- a/src/Avalonia.Native/TrayIconImpl.cs +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -4,6 +4,8 @@ using Avalonia.Controls.Platform; using Avalonia.Native.Interop; using Avalonia.Platform; +#nullable enable + namespace Avalonia.Native { class TrayIconEvents : CallbackBase, IAvnTrayIconEvents @@ -34,7 +36,9 @@ namespace Avalonia.Native MenuExporter = new AvaloniaNativeMenuExporter(_native, factory); } - + + public Action? OnClicked { get; set; } + public void Dispose() { diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index c1286f8436..ba208a4b74 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -23,10 +23,20 @@ namespace Avalonia.Win32 private IconImpl? _icon; private string? _tooltipText; private readonly Win32NativeToManagedMenuExporter _exporter; - private static Dictionary s_trayIcons = new Dictionary(); private bool _disposedValue; + public TrayIconImpl() + { + _exporter = new Win32NativeToManagedMenuExporter(); + + _uniqueId = ++_nextUniqueId; + + s_trayIcons.Add(_uniqueId, this); + } + + public Action? OnClicked { get; set; } + public INativeMenuExporter MenuExporter => _exporter; internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) @@ -37,15 +47,6 @@ namespace Avalonia.Win32 } } - public TrayIconImpl() - { - _exporter = new Win32NativeToManagedMenuExporter(); - - _uniqueId = ++_nextUniqueId; - - s_trayIcons.Add(_uniqueId, this); - } - public void SetIcon(IWindowIconImpl? icon) { _icon = icon as IconImpl; @@ -63,7 +64,6 @@ namespace Avalonia.Win32 UpdateIcon(!_iconAdded); } - private void UpdateIcon(bool remove = false) { var iconData = new NOTIFYICONDATA() @@ -105,9 +105,7 @@ namespace Avalonia.Win32 switch (lParam.ToInt32()) { case (int)WindowsMessage.WM_LBUTTONUP: - break; - - case (int)WindowsMessage.WM_LBUTTONDBLCLK: + OnClicked?.Invoke(); break; case (int)WindowsMessage.WM_RBUTTONUP: @@ -264,11 +262,11 @@ namespace Avalonia.Win32 } } - ~TrayIconImpl() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: false); - } + ~TrayIconImpl() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } public void Dispose() { From 5d568d104aa53a95943f91ab4aa4dc8e3660eb52 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 14:41:20 +0100 Subject: [PATCH 022/198] handle items being programatically removed. --- src/Avalonia.Controls/TrayIcon.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 4d86f9ddc1..fdf30846f4 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -129,15 +129,12 @@ namespace Avalonia.Controls { var trayIcons = GetTrayIcons(Application.Current); - foreach (var icon in trayIcons) - { - icon.Dispose(); - } + RemoveIcons(trayIcons); } private static void Icons_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { - + RemoveIcons(e.OldItems.Cast()); } private static void RemoveIcons(IEnumerable icons) From 4e350e64d86e11d2a70ec5d94f7f3681811d46bb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 14:41:35 +0100 Subject: [PATCH 023/198] demo tray icon constantly shown and hidden. --- src/Avalonia.Controls/TrayIcon.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index fdf30846f4..e1c70afae9 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Platform; +using Avalonia.Threading; #nullable enable @@ -25,12 +27,16 @@ namespace Avalonia.Controls _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty); - Clicked += TrayIcon_Clicked; + var timer = new DispatcherTimer(); + timer.Interval = TimeSpan.FromSeconds(1); + timer.Tick += Timer_Tick; + + timer.Start(); } - private void TrayIcon_Clicked(object sender, EventArgs e) + private void Timer_Tick(object sender, EventArgs e) { - + IsVisible = !IsVisible; } public TrayIcon () : this(PlatformManager.CreateTrayIcon()) From a3c8396cf5a221edacefa63b6b743ffc00253615 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 15:03:07 +0100 Subject: [PATCH 024/198] Tray icon osx, implement visibility toggle and lifetime management. --- native/Avalonia.Native/src/OSX/trayicon.h | 4 ++++ native/Avalonia.Native/src/OSX/trayicon.mm | 26 ++++++++++++++++++++-- src/Avalonia.Controls/TrayIcon.cs | 11 --------- src/Avalonia.Native/TrayIconImpl.cs | 4 ++-- src/Avalonia.Native/avn.idl | 1 + 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/trayicon.h b/native/Avalonia.Native/src/OSX/trayicon.h index 4329668cbd..11ad71756a 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.h +++ b/native/Avalonia.Native/src/OSX/trayicon.h @@ -22,9 +22,13 @@ public: AvnTrayIcon(IAvnTrayIconEvents* events); + ~AvnTrayIcon (); + virtual HRESULT SetIcon (void* data, size_t length) override; virtual HRESULT SetMenu (IAvnMenu* menu) override; + + virtual HRESULT SetIsVisible (bool isVisible) override; }; #endif /* trayicon_h */ diff --git a/native/Avalonia.Native/src/OSX/trayicon.mm b/native/Avalonia.Native/src/OSX/trayicon.mm index 67b6bd4874..79b16f82c6 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.mm +++ b/native/Avalonia.Native/src/OSX/trayicon.mm @@ -14,7 +14,17 @@ AvnTrayIcon::AvnTrayIcon(IAvnTrayIconEvents* events) { _events = events; - _native = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength]; + _native = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength]; + +} + +AvnTrayIcon::~AvnTrayIcon() +{ + if(_native != nullptr) + { + [[_native statusBar] removeStatusItem:_native]; + _native = nullptr; + } } HRESULT AvnTrayIcon::SetIcon (void* data, size_t length) @@ -57,9 +67,21 @@ HRESULT AvnTrayIcon::SetMenu (IAvnMenu* menu) if(appMenu != nullptr) { - [_native setMenu:appMenu->GetNative()]; + [_native setMenu:appMenu->GetNative()]; } } return S_OK; } + +HRESULT AvnTrayIcon::SetIsVisible(bool isVisible) +{ + START_COM_CALL; + + @autoreleasepool + { + [_native setVisible:isVisible]; + } + + return S_OK; +} diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index e1c70afae9..bd346c1e5d 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -26,17 +26,6 @@ namespace Avalonia.Controls _impl.SetIsVisible(IsVisible); _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty); - - var timer = new DispatcherTimer(); - timer.Interval = TimeSpan.FromSeconds(1); - timer.Tick += Timer_Tick; - - timer.Start(); - } - - private void Timer_Tick(object sender, EventArgs e) - { - IsVisible = !IsVisible; } public TrayIcon () : this(PlatformManager.CreateTrayIcon()) diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs index 7e2ade901c..b8b81214f1 100644 --- a/src/Avalonia.Native/TrayIconImpl.cs +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -41,7 +41,7 @@ namespace Avalonia.Native public void Dispose() { - + _native.Dispose(); } public unsafe void SetIcon(IWindowIconImpl? icon) @@ -73,7 +73,7 @@ namespace Avalonia.Native public void SetIsVisible(bool visible) { - + _native.SetIsVisible(visible.AsComBool()); } public INativeMenuExporter? MenuExporter { get; } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 47ed7116a7..c6fd3850c5 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -671,6 +671,7 @@ interface IAvnTrayIcon : IUnknown { HRESULT SetIcon(void* data, size_t length); HRESULT SetMenu(IAvnMenu* menu); + HRESULT SetIsVisible(bool isVisible); } [uuid(a687a6d9-73aa-4fef-9b4a-61587d7285d3)] From 33f4bb64188a35328357a38df121ea52fdf7c145 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 21:32:51 +0100 Subject: [PATCH 025/198] fix unit tests. --- tests/Avalonia.UnitTests/MockWindowingPlatform.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index bc003537f4..4074885505 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -126,6 +126,11 @@ namespace Avalonia.UnitTests throw new NotImplementedException(); } + public ITrayIconImpl CreateTrayIcon() + { + throw new NotImplementedException(); + } + private static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl { mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice()); From 00b82a215d30ceedb464360443eafb12a3a3a3a5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 23:07:38 +0100 Subject: [PATCH 026/198] fix unit tests. --- tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs index bf1322afbc..5c5ec8be90 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs @@ -25,6 +25,11 @@ namespace Avalonia.Controls.UnitTests throw new NotImplementedException(); } + public ITrayIconImpl CreateTrayIcon() + { + throw new NotImplementedException(); + } + public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.RenderScaling == 1); } } From 9c7d91d101aa69c4544d4087f02b73e73b91911e Mon Sep 17 00:00:00 2001 From: Adir Date: Sat, 11 Sep 2021 21:37:55 +0300 Subject: [PATCH 027/198] Added support for Mica backdrop brush in Windows 11 Added support for creating rounded corner backdrop brushes in Windows 10/11 --- src/Windows/Avalonia.Win32/Win32GlManager.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 6 + .../Composition/WinUICompositedWindow.cs | 19 ++- .../Composition/WinUICompositorConnection.cs | 122 ++++++++++----- .../WinUiCompositedWindowSurface.cs | 10 +- src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs | 9 +- src/Windows/Avalonia.Win32/WinRT/winrt.idl | 123 ++++++++++++++- src/Windows/Avalonia.Win32/WindowImpl.cs | 143 ++++++++++-------- 8 files changed, 320 insertions(+), 114 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index e70ea52106..289c100d51 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -27,7 +27,7 @@ namespace Avalonia.Win32 if (egl != null && opts?.UseWindowsUIComposition == true) { - WinUICompositorConnection.TryCreateAndRegister(egl); + WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); } return egl; diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index c011a458c3..9d56306c59 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -89,6 +89,12 @@ namespace Avalonia /// This is recommended if you need to use AcrylicBlur or acrylic in your applications. /// public bool UseWindowsUIComposition { get; set; } = true; + + /// + /// When enabled, create rounded corner blur brushes + /// If set to zero the brushes will be created using default settings (sharp corners) + /// + public float CompositionBackdropCornerRadius { get; set; } } } diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs index 4ae9c08410..1162cf9d70 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs @@ -13,6 +13,8 @@ namespace Avalonia.Win32.WinRT.Composition { private EglContext _syncContext; private readonly object _pumpLock; + private readonly IVisual _micaVisual; + private readonly ICompositionRoundedRectangleGeometry _roundedRectangleGeometry; private readonly IVisual _blurVisual; private ICompositionTarget _compositionTarget; private IVisual _contentVisual; @@ -28,11 +30,14 @@ namespace Avalonia.Win32.WinRT.Composition object pumpLock, ICompositionTarget compositionTarget, ICompositionDrawingSurfaceInterop surfaceInterop, - IVisual contentVisual, IVisual blurVisual) + IVisual contentVisual, IVisual blurVisual, IVisual micaVisual, + ICompositionRoundedRectangleGeometry roundedRectangleGeometry) { _compositor = compositor.CloneReference(); _syncContext = syncContext; _pumpLock = pumpLock; + _micaVisual = micaVisual; + _roundedRectangleGeometry = roundedRectangleGeometry; _blurVisual = blurVisual.CloneReference(); _compositionTarget = compositionTarget.CloneReference(); _contentVisual = contentVisual.CloneReference(); @@ -48,6 +53,7 @@ namespace Avalonia.Win32.WinRT.Composition { _surfaceInterop.Resize(new UnmanagedMethods.POINT { X = size.Width, Y = size.Height }); _contentVisual.SetSize(new Vector2(size.Width, size.Height)); + _roundedRectangleGeometry?.SetSize(new Vector2(size.Width, size.Height)); _size = size; } } @@ -57,7 +63,7 @@ namespace Avalonia.Win32.WinRT.Composition { if (!_syncContext.IsCurrent) throw new InvalidOperationException(); - + var iid = IID_ID3D11Texture2D; void* pTexture; var off = _surfaceInterop.BeginDraw(null, &iid, &pTexture); @@ -72,10 +78,13 @@ namespace Avalonia.Win32.WinRT.Composition _surfaceInterop.EndDraw(); } - public void SetBlur(bool enable) + public void SetBlur(BlurEffect blurEffect) { using (_syncContext.EnsureLocked()) - _blurVisual.SetIsVisible(enable ? 1 : 0); + { + _blurVisual.SetIsVisible(blurEffect == BlurEffect.Acrylic ? 1 : 0); + _micaVisual?.SetIsVisible(blurEffect == BlurEffect.Mica ? 1 : 0); + } } public IDisposable BeginTransaction() @@ -83,7 +92,7 @@ namespace Avalonia.Win32.WinRT.Composition Monitor.Enter(_pumpLock); return Disposable.Create(() => Monitor.Exit(_pumpLock)); } - + public void Dispose() { if (_syncContext == null) diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index bc8f5a606c..ebdbae06d7 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Numerics; using System.Runtime.InteropServices; @@ -26,12 +27,15 @@ namespace Avalonia.Win32.WinRT.Composition private EglPlatformOpenGlInterface _gl; private ICompositorDesktopInterop _compositorDesktopInterop; private ICompositionBrush _blurBrush; + private readonly ICompositionBrush _micaBrush; private object _pumpLock = new object(); + private readonly float _backdropCornerRadius; - public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock) + public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock, float backdropCornerRadius) { _gl = gl; _pumpLock = pumpLock; + _backdropCornerRadius = backdropCornerRadius; _syncContext = _gl.PrimaryEglContext; _angle = (AngleWin32EglDisplay)_gl.Display; _compositor = NativeWinRTMethods.CreateInstance("Windows.UI.Composition.Compositor"); @@ -40,15 +44,15 @@ namespace Avalonia.Win32.WinRT.Composition _compositorInterop = _compositor.QueryInterface(); _compositorDesktopInterop = _compositor.QueryInterface(); using var device = MicroComRuntime.CreateProxyFor(_angle.GetDirect3DDevice(), true); - + _device = _compositorInterop.CreateGraphicsDevice(device); - _blurBrush = CreateBlurBrush(); - + _blurBrush = CreateAcrylicBlurBackdropBrush(); + _micaBrush = CreateMicaBackdropBrush(); } public EglPlatformOpenGlInterface Egl => _gl; - static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle) + static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle, float backdropCornerRadius) { var tcs = new TaskCompletionSource(); var pumpLock = new object(); @@ -63,22 +67,19 @@ namespace Avalonia.Win32.WinRT.Composition dwSize = Marshal.SizeOf(), threadType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_TYPE.DQTYPE_THREAD_CURRENT }); - connect = new WinUICompositorConnection(angle, pumpLock); + connect = new WinUICompositorConnection(angle, pumpLock, backdropCornerRadius); AvaloniaLocator.CurrentMutable.BindToSelf(connect); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); tcs.SetResult(true); - } catch (Exception e) { tcs.SetException(e); return; } + connect.RunLoop(); - }) - { - IsBackground = true - }; + }) { IsBackground = true }; th.SetApartmentState(ApartmentState.STA); th.Start(); return tcs.Task.Result; @@ -93,9 +94,9 @@ namespace Avalonia.Win32.WinRT.Composition { _parent = parent; } + public void Dispose() { - } public void Invoke(IAsyncAction asyncInfo, AsyncStatus asyncStatus) @@ -106,6 +107,7 @@ namespace Avalonia.Win32.WinRT.Composition } public MicroComShadow Shadow { get; set; } + public void OnReferencedFromNative() { } @@ -114,12 +116,12 @@ namespace Avalonia.Win32.WinRT.Composition { } } - + private void RunLoop() { { var st = Stopwatch.StartNew(); - using (var act = _compositor5.RequestCommitAsync()) + using (var act = _compositor5.RequestCommitAsync()) act.SetCompleted(new RunLoopHandler(this)); while (true) { @@ -130,7 +132,8 @@ namespace Avalonia.Win32.WinRT.Composition } } - public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle) + public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle, + float backdropCornerRadius) { const int majorRequired = 10; const int buildRequired = 17134; @@ -143,14 +146,12 @@ namespace Avalonia.Win32.WinRT.Composition { try { - TryCreateAndRegisterCore(angle); - return; + TryCreateAndRegisterCore(angle, backdropCornerRadius); } catch (Exception e) { Logger.TryGet(LogEventLevel.Error, "WinUIComposition") ?.Log(null, "Unable to initialize WinUI compositor: {0}", e); - } } @@ -167,17 +168,19 @@ namespace Avalonia.Win32.WinRT.Composition using var sc = _syncContext.EnsureLocked(); using var desktopTarget = _compositorDesktopInterop.CreateDesktopWindowTarget(hWnd, 0); using var target = desktopTarget.QueryInterface(); - - using var drawingSurface = _device.CreateDrawingSurface(new UnmanagedMethods.SIZE(), DirectXPixelFormat.B8G8R8A8UIntNormalized, + + using var drawingSurface = _device.CreateDrawingSurface(new UnmanagedMethods.SIZE(), + DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); using var surface = drawingSurface.QueryInterface(); using var surfaceInterop = drawingSurface.QueryInterface(); - + using var surfaceBrush = _compositor.CreateSurfaceBrushWithSurface(surface); using var brush = surfaceBrush.QueryInterface(); using var spriteVisual = _compositor.CreateSpriteVisual(); spriteVisual.SetBrush(brush); + using var visual = spriteVisual.QueryInterface(); using var visual2 = spriteVisual.QueryInterface(); using var container = _compositor.CreateContainerVisual(); @@ -185,47 +188,90 @@ namespace Avalonia.Win32.WinRT.Composition using var containerVisual2 = container.QueryInterface(); containerVisual2.SetRelativeSizeAdjustment(new Vector2(1, 1)); using var containerChildren = container.Children; - + target.SetRoot(containerVisual); - using var blur = CreateBlurVisual(); - + using var blur = CreateBlurVisual(_blurBrush); + IVisual mica = null; + if (_micaBrush != null) + { + mica = CreateBlurVisual(_micaBrush); + containerChildren.InsertAtTop(mica); + } + + var compositionRoundedRectangleGeometry = ClipVisual(blur, mica); + containerChildren.InsertAtTop(blur); containerChildren.InsertAtTop(visual); - - return new WinUICompositedWindow(_syncContext, _compositor, _pumpLock, target, surfaceInterop, visual, blur); + + return new WinUICompositedWindow(_syncContext, _compositor, _pumpLock, target, surfaceInterop, visual, + blur, mica, compositionRoundedRectangleGeometry); } + private ICompositionBrush CreateMicaBackdropBrush() + { + if (Win32Platform.WindowsVersion.Build < 22000) + return null; + + using var compositorWithBlurredWallpaperBackdropBrush = + _compositor.QueryInterface(); + using var blurredWallpaperBackdropBrush = + compositorWithBlurredWallpaperBackdropBrush?.TryCreateBlurredWallpaperBackdropBrush(); + using var micaBackdropBrush = blurredWallpaperBackdropBrush?.QueryInterface(); + return micaBackdropBrush.CloneReference(); + } - private unsafe ICompositionBrush CreateBlurBrush() + private unsafe ICompositionBrush CreateAcrylicBlurBackdropBrush() { - using var backDropParameterFactory = NativeWinRTMethods.CreateActivationFactory( - "Windows.UI.Composition.CompositionEffectSourceParameter"); + using var backDropParameterFactory = + NativeWinRTMethods.CreateActivationFactory( + "Windows.UI.Composition.CompositionEffectSourceParameter"); using var backdropString = new HStringInterop("backdrop"); using var backDropParameter = backDropParameterFactory.Create(backdropString.Handle); using var backDropParameterAsSource = backDropParameter.QueryInterface(); var blurEffect = new WinUIGaussianBlurEffect(backDropParameterAsSource); using var blurEffectFactory = _compositor.CreateEffectFactory(blurEffect); + using var compositionEffectBrush = blurEffectFactory.CreateBrush(); using var backdrop = _compositor2.CreateBackdropBrush(); using var backdropBrush = backdrop.QueryInterface(); - - + var saturateEffect = new SaturationEffect(blurEffect); using var satEffectFactory = _compositor.CreateEffectFactory(saturateEffect); using var sat = satEffectFactory.CreateBrush(); - sat.SetSourceParameter(backdropString.Handle, backdropBrush); - return sat.QueryInterface(); + compositionEffectBrush.SetSourceParameter(backdropString.Handle, backdropBrush); + return compositionEffectBrush.QueryInterface(); + } + + private ICompositionRoundedRectangleGeometry ClipVisual(params IVisual[] containerVisuals) + { + if (_backdropCornerRadius == 0) + return null; + using var roundedRectangleGeometry = _compositor5.CreateRoundedRectangleGeometry(); + roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius, _backdropCornerRadius)); + + using var compositor6 = _compositor.QueryInterface(); + using var compositionGeometry = roundedRectangleGeometry + .QueryInterface(); + + using var geometricClipWithGeometry = + compositor6.CreateGeometricClipWithGeometry(compositionGeometry); + foreach (var visual in containerVisuals) + { + visual?.SetClip(geometricClipWithGeometry.QueryInterface()); + } + + return roundedRectangleGeometry.CloneReference(); } - - private unsafe IVisual CreateBlurVisual() + + private unsafe IVisual CreateBlurVisual(ICompositionBrush compositionBrush) { using var spriteVisual = _compositor.CreateSpriteVisual(); using var visual = spriteVisual.QueryInterface(); using var visual2 = spriteVisual.QueryInterface(); - - - spriteVisual.SetBrush(_blurBrush); + + + spriteVisual.SetBrush(compositionBrush); visual.SetIsVisible(0); visual2.SetRelativeSizeAdjustment(new Vector2(1.0f, 1.0f)); diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs index f59d50860a..4ed882552b 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs @@ -15,7 +15,7 @@ namespace Avalonia.Win32.WinRT.Composition private EglPlatformOpenGlInterface _egl; private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; private IRef _window; - private bool _enableBlur; + private BlurEffect _blurEffect; public WinUiCompositedWindowSurface(WinUICompositorConnection connection, IEglWindowGlPlatformSurfaceInfo info) : base() { @@ -31,7 +31,7 @@ namespace Avalonia.Win32.WinRT.Composition if (_window?.Item == null) { _window = RefCountable.Create(_connection.CreateWindow(_info.Handle)); - _window.Item.SetBlur(_enableBlur); + _window.Item.SetBlur(_blurEffect); } return new CompositionRenderTarget(_egl, _window, _info); @@ -100,10 +100,10 @@ namespace Avalonia.Win32.WinRT.Composition } } - public void SetBlur(bool enable) + public void SetBlur(BlurEffect blurEffect) { - _enableBlur = enable; - _window?.Item?.SetBlur(enable); + _blurEffect = blurEffect; + _window?.Item?.SetBlur(blurEffect); } public void Dispose() diff --git a/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs b/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs index 81c0e3e185..ea5dcdeeba 100644 --- a/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs +++ b/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs @@ -1,7 +1,14 @@ namespace Avalonia.Win32.WinRT { + public enum BlurEffect + { + None, + Acrylic, + Mica + } + public interface IBlurHost { - void SetBlur(bool enable); + void SetBlur(BlurEffect enable); } } diff --git a/src/Windows/Avalonia.Win32/WinRT/winrt.idl b/src/Windows/Avalonia.Win32/WinRT/winrt.idl index 929377999c..18a9a26fca 100644 --- a/src/Windows/Avalonia.Win32/WinRT/winrt.idl +++ b/src/Windows/Avalonia.Win32/WinRT/winrt.idl @@ -358,6 +358,12 @@ interface ICompositor2 : IInspectable [overload("CreateStepEasingFunction")] HRESULT CreateStepEasingFunctionWithStepCount([in] INT32 stepCount, [out] [retval] void** result); } +[uuid(0D8FB190-F122-5B8D-9FDD-543B0D8EB7F3)] +interface ICompositorWithBlurredWallpaperBackdropBrush : IInspectable +{ + HRESULT TryCreateBlurredWallpaperBackdropBrush([out] [retval] ICompositionBackdropBrush** result); +} + [uuid(08E05581-1AD1-4F97-9757-402D76E4233B)] interface ISpriteVisual : IInspectable { @@ -520,6 +526,13 @@ enum CompositionCompositeMode MinBlend, } +[contract(Windows.Foundation.UniversalApiContract, 2.0)] +[exclusiveto(Windows.UI.Composition.CompositionClip)] +[uuid(1CCD2A52-CFC7-4ACE-9983-146BB8EB6A3C)] +interface ICompositionClip : IInspectable +{ +} + [uuid(117E202D-A859-4C89-873B-C2AA566788E3)] interface IVisual : IInspectable { @@ -531,8 +544,8 @@ interface IVisual : IInspectable [propput] HRESULT BorderMode([in] CompositionBorderMode value); [propget] HRESULT CenterPoint([out] [retval] Vector3* value); [propput] HRESULT CenterPoint([in] Vector3 value); - [propget] HRESULT Clip([out] [retval]void** value); - [propput] HRESULT Clip([in] void* value); + [propget] HRESULT Clip([out] [retval] ICompositionClip** value); + [propput] HRESULT Clip([in] ICompositionClip* value); [propget] HRESULT CompositeMode([out] [retval] CompositionCompositeMode* value); [propput] HRESULT CompositeMode([in] CompositionCompositeMode value); [propget] HRESULT IsVisible([out] [retval] boolean* value); @@ -692,6 +705,99 @@ interface ICompositionScopedBatch : IInspectable [eventremove] HRESULT RemoveCompleted([in] int token); } +[contract(Windows.Foundation.UniversalApiContract, 6.0)] +[exclusiveto(Windows.UI.Composition.CompositionRoundedRectangleGeometry)] +[uuid(8770C822-1D50-4B8B-B013-7C9A0E46935F)] +interface ICompositionRoundedRectangleGeometry : IInspectable +{ + [propget] HRESULT CornerRadius([out] [retval] Vector2* value); + [propput] HRESULT CornerRadius([in] Vector2 value); + [propget] HRESULT Offset([out] [retval] Vector2* value); + [propput] HRESULT Offset([in] Vector2 value); + [propget] HRESULT Size([out] [retval] Vector2* value); + [propput] HRESULT Size([in] Vector2 value); +} + +[contract(Windows.Foundation.UniversalApiContract, 6.0)] +[exclusiveto(Windows.UI.Composition.CompositionGeometry)] +[uuid(E985217C-6A17-4207-ABD8-5FD3DD612A9D)] +interface ICompositionGeometry : IInspectable +{ + [propget] HRESULT TrimEnd([out] [retval] FLOAT* value); + [propput] HRESULT TrimEnd([in] FLOAT value); + [propget] HRESULT TrimOffset([out] [retval] FLOAT* value); + [propput] HRESULT TrimOffset([in] FLOAT value); + [propget] HRESULT TrimStart([out] [retval] FLOAT* value); + [propput] HRESULT TrimStart([in] FLOAT value); +} + +[uuid(401B61BB-0007-4363-B1F3-6BCC003FB83E)] +interface ICompositionSpriteShape : IInspectable +{ + [propget] HRESULT GetFillBrush([out] [retval] ICompositionBrush** value); + [propput] HRESULT SetFillBrush([in] ICompositionBrush* value); + [propget] HRESULT Geometry([out] [retval] ICompositionGeometry** value); + [propput] HRESULT Geometry([in] ICompositionGeometry* value); + [propget] HRESULT IsStrokeNonScaling([out] [retval] boolean* value); + [propput] HRESULT IsStrokeNonScaling([in] boolean value); + [propget] HRESULT StrokeBrush([out] [retval] ICompositionBrush** value); + [propput] HRESULT StrokeBrush([in] ICompositionBrush* value); + [propget] HRESULT StrokeDashArray(); + [propget] HRESULT StrokeDashCap(); + [propput] HRESULT StrokeDashCap(); + [propget] HRESULT StrokeDashOffset(); + [propput] HRESULT StrokeDashOffset(); + [propget] HRESULT StrokeEndCap(); + [propput] HRESULT StrokeEndCap(); + [propget] HRESULT StrokeLineJoin(); + [propput] HRESULT StrokeLineJoin(); + [propget] HRESULT StrokeMiterLimit(); + [propput] HRESULT StrokeMiterLimit(); + [propget] HRESULT StrokeStartCap(); + [propput] HRESULT StrokeStartCap(); + [propget] HRESULT StrokeThickness(); + [propput] HRESULT StrokeThickness(); +} + +[contract(Windows.Foundation.UniversalApiContract, 6.0)] +[exclusiveto(Windows.UI.Composition.CompositionShape)] +[uuid(B47CE2F7-9A88-42C4-9E87-2E500CA8688C)] +interface ICompositionShape : IInspectable +{ + [propget] HRESULT CenterPoint([out] [retval] Vector2* value); + [propput] HRESULT CenterPoint([in] Vector2 value); +} + +[uuid(42d4219a-be1b-5091-8f1e-90270840fc2d)] +interface IVectorOfCompositionShape : IInspectable +{ + HRESULT GetAt(); + [propget] HRESULT GetSize(); + HRESULT GetView(); + HRESULT IndexOf(); + HRESULT SetAt(); + HRESULT InsertAt(); + HRESULT RemoveAt(); + HRESULT Append([in] ICompositionShape* value); + HRESULT RemoveAtEnd(); + HRESULT Clear(); +} + +[contract(Windows.Foundation.UniversalApiContract, 7.0)] +[exclusiveto(Windows.UI.Composition.CompositionGeometricClip)] +[uuid(C840B581-81C9-4444-A2C1-CCAECE3A50E5)] +interface ICompositionGeometricClip : IInspectable +{ + [propget] HRESULT Geometry([out] [retval] ICompositionGeometry** value); + [propput] HRESULT Geometry([in] ICompositionGeometry* value); +} + +[uuid(F2BD13C3-BA7E-4B0F-9126-FFB7536B8176)] +interface IShapeVisual : IInspectable +{ + [propget] HRESULT Shapes([out] [retval] IUnknown** value); +} + [uuid(48EA31AD-7FCD-4076-A79C-90CC4B852C9B)] interface ICompositor5 : IInspectable { @@ -709,10 +815,19 @@ interface ICompositor5 : IInspectable [overload("CreatePathGeometry")] HRESULT CreatePathGeometryWithPath([in] void* path, [out] [retval] void** result); HRESULT CreatePathKeyFrameAnimation([out] [retval] void** result); HRESULT CreateRectangleGeometry([out] [retval] void** result); - HRESULT CreateRoundedRectangleGeometry([out] [retval] void** result); - HRESULT CreateShapeVisual([out] [retval] void** result); + HRESULT CreateRoundedRectangleGeometry([out] [retval] ICompositionRoundedRectangleGeometry** result); + HRESULT CreateShapeVisual([out] [retval] IShapeVisual** result); [overload("CreateSpriteShape")] HRESULT CreateSpriteShape([out] [retval] void** result); [overload("CreateSpriteShape")] HRESULT CreateSpriteShapeWithGeometry([in] void* geometry, [out] [retval] void** result); HRESULT CreateViewBox([out] [retval] void** result); HRESULT RequestCommitAsync([out] [retval] IAsyncAction** operation); } + +[contract(Windows.Foundation.UniversalApiContract, 7.0)] +[exclusiveto(Windows.UI.Composition.Compositor)] +[uuid(7A38B2BD-CEC8-4EEB-830F-D8D07AEDEBC3)] +interface ICompositor6 : IInspectable +{ + [overload("CreateGeometricClip")] HRESULT CreateGeometricClip([out] [retval] ICompositionGeometricClip** result); + [overload("CreateGeometricClip")] HRESULT CreateGeometricClipWithGeometry([in] ICompositionGeometry* geometry, [out] [retval] ICompositionGeometricClip** result); +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 8fc25f8cfa..0a0f600520 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -82,7 +82,7 @@ namespace Avalonia.Win32 private Size _minSize; private Size _maxSize; private POINT _maxTrackSize; - private WindowImpl _parent; + private WindowImpl _parent; private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; private bool _isCloseRequested; private bool _shown; @@ -105,9 +105,7 @@ namespace Avalonia.Win32 _windowProperties = new WindowProperties { - ShowInTaskbar = false, - IsResizable = true, - Decorations = SystemDecorations.Full + ShowInTaskbar = false, IsResizable = true, Decorations = SystemDecorations.Full }; _rendererLock = new ManagedDeferredRendererLock(); @@ -116,13 +114,13 @@ namespace Avalonia.Win32 var compositionConnector = AvaloniaLocator.Current.GetService(); _isUsingComposition = compositionConnector is { } && - glPlatform is EglPlatformOpenGlInterface egl && - egl.Display is AngleWin32EglDisplay angleDisplay && - angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11; + glPlatform is EglPlatformOpenGlInterface egl && + egl.Display is AngleWin32EglDisplay angleDisplay && + angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11; CreateWindow(); _framebuffer = new FramebufferManager(_hwnd); - + if (glPlatform != null) { if (_isUsingComposition) @@ -168,7 +166,7 @@ namespace Avalonia.Win32 public Action PositionChanged { get; set; } public Action WindowStateChanged { get; set; } - + public Action LostFocus { get; set; } public Action TransparencyLevelChanged { get; set; } @@ -224,7 +222,8 @@ namespace Avalonia.Win32 return new Size(rcWindow.Width, rcWindow.Height) / RenderScaling; } - DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, Marshal.SizeOf(typeof(RECT))); + DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, + Marshal.SizeOf(typeof(RECT))); return new Size(rect.Width, rect.Height) / RenderScaling; } } @@ -233,7 +232,8 @@ namespace Avalonia.Win32 public IPlatformHandle Handle { get; private set; } - public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / RenderScaling, _maxTrackSize.Y / RenderScaling); + public virtual Size MaxAutoSizeHint => + new Size(_maxTrackSize.X / RenderScaling, _maxTrackSize.Y / RenderScaling); public IMouseDevice MouseDevice => _mouseDevice; @@ -241,7 +241,7 @@ namespace Avalonia.Win32 { get { - if(_isFullScreenActive) + if (_isFullScreenActive) { return WindowState.FullScreen; } @@ -264,7 +264,7 @@ namespace Avalonia.Win32 ShowWindow(value, true); } - _showWindowState = value; + _showWindowState = value; } } @@ -272,7 +272,7 @@ namespace Avalonia.Win32 protected IntPtr Hwnd => _hwnd; - public void SetTransparencyLevelHint (WindowTransparencyLevel transparencyLevel) + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { TransparencyLevel = EnableBlur(transparencyLevel); } @@ -312,12 +312,12 @@ namespace Avalonia.Win32 } var blurInfo = new DWM_BLURBEHIND(false); - + if (transparencyLevel == WindowTransparencyLevel.Blur) { blurInfo = new DWM_BLURBEHIND(true); } - + DwmEnableBlurBehindWindow(_hwnd, ref blurInfo); if (transparencyLevel == WindowTransparencyLevel.Transparent) @@ -373,13 +373,20 @@ namespace Avalonia.Win32 { if (_isUsingComposition) { - _blurHost?.SetBlur(transparencyLevel >= WindowTransparencyLevel.Blur); + _blurHost?.SetBlur(transparencyLevel switch + { + WindowTransparencyLevel.Mica => BlurEffect.Mica, + WindowTransparencyLevel.AcrylicBlur => BlurEffect.Acrylic, + WindowTransparencyLevel.Blur => BlurEffect.Acrylic, + _ => BlurEffect.None + }); return transparencyLevel; } else { - bool canUseAcrylic = Win32Platform.WindowsVersion.Major > 10 || Win32Platform.WindowsVersion.Build >= 19628; + bool canUseAcrylic = Win32Platform.WindowsVersion.Major > 10 || + Win32Platform.WindowsVersion.Build >= 19628; var accent = new AccentPolicy(); var accentStructSize = Marshal.SizeOf(accent); @@ -530,7 +537,7 @@ namespace Avalonia.Win32 { BeforeCloseCleanup(true); } - + DestroyWindow(_hwnd); _hwnd = IntPtr.Zero; } @@ -596,7 +603,7 @@ namespace Avalonia.Win32 public void SetParent(IWindowImpl parent) { _parent = (WindowImpl)parent; - + var parentHwnd = _parent?._hwnd ?? IntPtr.Zero; if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar) @@ -707,7 +714,7 @@ namespace Avalonia.Win32 _isUsingComposition ? (int)WindowStyles.WS_EX_NOREDIRECTIONBITMAP : 0, atom, null, - (int)WindowStyles.WS_OVERLAPPEDWINDOW | (int) WindowStyles.WS_CLIPCHILDREN, + (int)WindowStyles.WS_OVERLAPPEDWINDOW | (int)WindowStyles.WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, @@ -763,7 +770,7 @@ namespace Avalonia.Win32 } if (ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8) - { + { var monitor = MonitorFromWindow( _hwnd, MONITOR.MONITOR_DEFAULTTONEAREST); @@ -809,7 +816,9 @@ namespace Avalonia.Win32 // Set new window style and size. SetStyle(current & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME), false); - SetExtendedStyle(currentEx & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE), false); + SetExtendedStyle( + currentEx & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | + WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE), false); // On expand, if we're given a window_rect, grow to it, otherwise do // not resize. @@ -819,8 +828,9 @@ namespace Avalonia.Win32 var window_rect = monitor_info.rcMonitor.ToPixelRect(); SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y, - window_rect.Width, window_rect.Height, - SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + window_rect.Width, window_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | + SetWindowPosFlags.SWP_FRAMECHANGED); _isFullScreenActive = true; } @@ -839,21 +849,22 @@ namespace Avalonia.Win32 var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, - new_rect.Height, - SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + new_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | + SetWindowPosFlags.SWP_FRAMECHANGED); UpdateWindowProperties(_windowProperties, true); } TaskBarList.MarkFullscreen(_hwnd, fullscreen); - + ExtendClientArea(); } private MARGINS UpdateExtendMargins() { RECT borderThickness = new RECT(); - RECT borderCaptionThickness = new RECT(); + RECT borderCaptionThickness = new RECT(); AdjustWindowRectEx(ref borderCaptionThickness, (uint)(GetStyle()), false, 0); AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle() & ~WindowStyles.WS_CAPTION), false, 0); @@ -862,7 +873,8 @@ namespace Avalonia.Win32 borderCaptionThickness.left *= -1; borderCaptionThickness.top *= -1; - bool wantsTitleBar = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) || _extendTitleBarHint == -1; + bool wantsTitleBar = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) || + _extendTitleBarHint == -1; if (!wantsTitleBar) { @@ -876,15 +888,22 @@ namespace Avalonia.Win32 if (_extendTitleBarHint != -1) { - borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling); + borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling); } - margins.cyTopHeight = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : 1; + margins.cyTopHeight = + _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && + !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome) ? + borderCaptionThickness.top : + 1; if (WindowState == WindowState.Maximized) { - _extendedMargins = new Thickness(0, (borderCaptionThickness.top - borderThickness.top) / RenderScaling, 0, 0); - _offScreenMargin = new Thickness(borderThickness.left / RenderScaling, borderThickness.top / RenderScaling, borderThickness.right / RenderScaling, borderThickness.bottom / RenderScaling); + _extendedMargins = new Thickness(0, (borderCaptionThickness.top - borderThickness.top) / RenderScaling, + 0, 0); + _offScreenMargin = new Thickness(borderThickness.left / RenderScaling, + borderThickness.top / RenderScaling, borderThickness.right / RenderScaling, + borderThickness.bottom / RenderScaling); } else { @@ -901,12 +920,13 @@ namespace Avalonia.Win32 { return; } - + if (DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled) { _isClientAreaExtended = false; return; } + GetClientRect(_hwnd, out var rcClient); GetWindowRect(_hwnd, out var rcWindow); @@ -929,12 +949,14 @@ namespace Avalonia.Win32 _offScreenMargin = new Thickness(); _extendedMargins = new Thickness(); - - Resize(new Size(rcWindow.Width/ RenderScaling, rcWindow.Height / RenderScaling), PlatformResizeReason.Layout); + + Resize(new Size(rcWindow.Width / RenderScaling, rcWindow.Height / RenderScaling), + PlatformResizeReason.Layout); } - if(!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && - !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome))) + if (!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && + !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints + .PreferSystemChrome))) { EnableCloseButton(_hwnd); } @@ -949,12 +971,12 @@ namespace Avalonia.Win32 private void ShowWindow(WindowState state, bool activate) { _shown = true; - + if (_isClientAreaExtended) { ExtendClientArea(); } - + ShowWindowCommand? command; var newWindowProperties = _windowProperties; @@ -972,7 +994,7 @@ namespace Avalonia.Win32 case WindowState.Normal: newWindowProperties.IsFullScreen = false; - command = IsWindowVisible(_hwnd) ? ShowWindowCommand.Restore : + command = IsWindowVisible(_hwnd) ? ShowWindowCommand.Restore : activate ? ShowWindowCommand.Normal : ShowWindowCommand.ShowNoActivate; break; @@ -1002,7 +1024,7 @@ namespace Avalonia.Win32 SetFocus(_hwnd); } } - + private void BeforeCloseCleanup(bool isDisposing) { // Based on https://github.com/dotnet/wpf/blob/master/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Window.cs#L4270-L4337 @@ -1020,7 +1042,7 @@ namespace Avalonia.Win32 // Our window closed callback will set enabled state to a correct value after child window gets destroyed. _parent.SetEnabled(true); } - + // We also need to activate our parent window since again OS might try to activate a window behind if it is not set. if (wasActive) { @@ -1047,7 +1069,7 @@ namespace Avalonia.Win32 SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); } } - } + } private WindowStyles GetWindowStateStyles() { @@ -1200,10 +1222,7 @@ namespace Avalonia.Win32 var margins = new MARGINS { - cyBottomHeight = margin, - cxRightWidth = margin, - cxLeftWidth = margin, - cyTopHeight = margin + cyBottomHeight = margin, cxRightWidth = margin, cxLeftWidth = margin, cyTopHeight = margin }; DwmExtendFrameIntoClientArea(_hwnd, ref margins); @@ -1224,7 +1243,7 @@ namespace Avalonia.Win32 SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); } - } + } } private const int MF_BYCOMMAND = 0x0; @@ -1238,12 +1257,13 @@ namespace Avalonia.Win32 void DisableCloseButton(IntPtr hwnd) { EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE, - MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); + MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); } + void EnableCloseButton(IntPtr hwnd) { EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE, - MF_BYCOMMAND | MF_ENABLED); + MF_BYCOMMAND | MF_ENABLED); } #if USE_MANAGED_DRAG @@ -1274,9 +1294,9 @@ namespace Avalonia.Win32 public void SetExtendClientAreaToDecorationsHint(bool hint) { _isClientAreaExtended = hint; - - ExtendClientArea(); - } + + ExtendClientArea(); + } public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) { @@ -1284,7 +1304,7 @@ namespace Avalonia.Win32 ExtendClientArea(); } - + /// public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) { @@ -1298,9 +1318,11 @@ namespace Avalonia.Win32 /// public Action ExtendClientAreaToDecorationsChanged { get; set; } - + /// - public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome); + public bool NeedsManagedDecorations => _isClientAreaExtended && + _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints + .PreferSystemChrome); /// public Thickness ExtendedMargins => _extendedMargins; @@ -1309,7 +1331,8 @@ namespace Avalonia.Win32 public Thickness OffScreenMargin => _offScreenMargin; /// - public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = + new AcrylicPlatformCompensationLevels(1, 0.8, 0); private ResizeReasonScope SetResizeReason(PlatformResizeReason reason) { @@ -1337,7 +1360,7 @@ namespace Avalonia.Win32 { private readonly WindowImpl _owner; private readonly PlatformResizeReason _restore; - + public ResizeReasonScope(WindowImpl owner, PlatformResizeReason restore) { _owner = owner; From e61147edc181a9fb1ff1bed5a5b4927ab376cdc4 Mon Sep 17 00:00:00 2001 From: Adir Date: Sat, 11 Sep 2021 21:53:12 +0300 Subject: [PATCH 028/198] Added Mica enum value to WindowTransparencyLevel.cs Added Mica as selected option --- samples/ControlCatalog/MainView.xaml | 1 + samples/ControlCatalog/MainWindow.xaml | 1 + samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml | 1 + src/Avalonia.Controls/WindowTransparencyLevel.cs | 7 ++++++- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 6537c470d5..f61b59e6cd 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -98,6 +98,7 @@ Transparent Blur AcrylicBlur + Mica diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index a107ee2163..ee42e7a54b 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -12,6 +12,7 @@ ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" TransparencyLevelHint="{Binding TransparencyLevel}" x:Name="MainWindow" + Background="Transparent" x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"> diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index b90f43c3b6..caab42e98c 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -14,6 +14,7 @@ Transparent Blur AcrylicBlur + Mica diff --git a/src/Avalonia.Controls/WindowTransparencyLevel.cs b/src/Avalonia.Controls/WindowTransparencyLevel.cs index ce7c03efbb..f416b5de91 100644 --- a/src/Avalonia.Controls/WindowTransparencyLevel.cs +++ b/src/Avalonia.Controls/WindowTransparencyLevel.cs @@ -20,6 +20,11 @@ /// /// The window background is a blur-behind with a high blur radius. This level may fallback to Blur. /// - AcrylicBlur + AcrylicBlur, + + /// + /// The window background is based on desktop wallpaper tint with a blur. This will only work on Windows 11 + /// + Mica } } From c03ff60b2d7056685953a32efa615f805ac83ce1 Mon Sep 17 00:00:00 2001 From: Adir Date: Sat, 11 Sep 2021 22:07:14 +0300 Subject: [PATCH 029/198] Reverted code formatting --- .../Composition/WinUICompositedWindow.cs | 4 +- .../Composition/WinUICompositorConnection.cs | 47 +++--- src/Windows/Avalonia.Win32/WindowImpl.cs | 135 ++++++++---------- 3 files changed, 85 insertions(+), 101 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs index 1162cf9d70..a09918a3a6 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs @@ -63,7 +63,7 @@ namespace Avalonia.Win32.WinRT.Composition { if (!_syncContext.IsCurrent) throw new InvalidOperationException(); - + var iid = IID_ID3D11Texture2D; void* pTexture; var off = _surfaceInterop.BeginDraw(null, &iid, &pTexture); @@ -92,7 +92,7 @@ namespace Avalonia.Win32.WinRT.Composition Monitor.Enter(_pumpLock); return Disposable.Create(() => Monitor.Exit(_pumpLock)); } - + public void Dispose() { if (_syncContext == null) diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index ebdbae06d7..f5706b6fb5 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -44,7 +44,7 @@ namespace Avalonia.Win32.WinRT.Composition _compositorInterop = _compositor.QueryInterface(); _compositorDesktopInterop = _compositor.QueryInterface(); using var device = MicroComRuntime.CreateProxyFor(_angle.GetDirect3DDevice(), true); - + _device = _compositorInterop.CreateGraphicsDevice(device); _blurBrush = CreateAcrylicBlurBackdropBrush(); _micaBrush = CreateMicaBackdropBrush(); @@ -71,15 +71,18 @@ namespace Avalonia.Win32.WinRT.Composition AvaloniaLocator.CurrentMutable.BindToSelf(connect); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); tcs.SetResult(true); + } catch (Exception e) { tcs.SetException(e); return; } - connect.RunLoop(); - }) { IsBackground = true }; + }) + { + IsBackground = true + }; th.SetApartmentState(ApartmentState.STA); th.Start(); return tcs.Task.Result; @@ -94,9 +97,9 @@ namespace Avalonia.Win32.WinRT.Composition { _parent = parent; } - public void Dispose() { + } public void Invoke(IAsyncAction asyncInfo, AsyncStatus asyncStatus) @@ -107,7 +110,6 @@ namespace Avalonia.Win32.WinRT.Composition } public MicroComShadow Shadow { get; set; } - public void OnReferencedFromNative() { } @@ -116,12 +118,12 @@ namespace Avalonia.Win32.WinRT.Composition { } } - + private void RunLoop() { { var st = Stopwatch.StartNew(); - using (var act = _compositor5.RequestCommitAsync()) + using (var act = _compositor5.RequestCommitAsync()) act.SetCompleted(new RunLoopHandler(this)); while (true) { @@ -152,6 +154,7 @@ namespace Avalonia.Win32.WinRT.Composition { Logger.TryGet(LogEventLevel.Error, "WinUIComposition") ?.Log(null, "Unable to initialize WinUI compositor: {0}", e); + } } @@ -168,19 +171,17 @@ namespace Avalonia.Win32.WinRT.Composition using var sc = _syncContext.EnsureLocked(); using var desktopTarget = _compositorDesktopInterop.CreateDesktopWindowTarget(hWnd, 0); using var target = desktopTarget.QueryInterface(); - - using var drawingSurface = _device.CreateDrawingSurface(new UnmanagedMethods.SIZE(), - DirectXPixelFormat.B8G8R8A8UIntNormalized, + + using var drawingSurface = _device.CreateDrawingSurface(new UnmanagedMethods.SIZE(), DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); using var surface = drawingSurface.QueryInterface(); using var surfaceInterop = drawingSurface.QueryInterface(); - + using var surfaceBrush = _compositor.CreateSurfaceBrushWithSurface(surface); using var brush = surfaceBrush.QueryInterface(); using var spriteVisual = _compositor.CreateSpriteVisual(); spriteVisual.SetBrush(brush); - using var visual = spriteVisual.QueryInterface(); using var visual2 = spriteVisual.QueryInterface(); using var container = _compositor.CreateContainerVisual(); @@ -188,7 +189,7 @@ namespace Avalonia.Win32.WinRT.Composition using var containerVisual2 = container.QueryInterface(); containerVisual2.SetRelativeSizeAdjustment(new Vector2(1, 1)); using var containerChildren = container.Children; - + target.SetRoot(containerVisual); using var blur = CreateBlurVisual(_blurBrush); @@ -200,10 +201,10 @@ namespace Avalonia.Win32.WinRT.Composition } var compositionRoundedRectangleGeometry = ClipVisual(blur, mica); - + containerChildren.InsertAtTop(blur); containerChildren.InsertAtTop(visual); - + return new WinUICompositedWindow(_syncContext, _compositor, _pumpLock, target, surfaceInterop, visual, blur, mica, compositionRoundedRectangleGeometry); } @@ -223,9 +224,8 @@ namespace Avalonia.Win32.WinRT.Composition private unsafe ICompositionBrush CreateAcrylicBlurBackdropBrush() { - using var backDropParameterFactory = - NativeWinRTMethods.CreateActivationFactory( - "Windows.UI.Composition.CompositionEffectSourceParameter"); + using var backDropParameterFactory = NativeWinRTMethods.CreateActivationFactory( + "Windows.UI.Composition.CompositionEffectSourceParameter"); using var backdropString = new HStringInterop("backdrop"); using var backDropParameter = backDropParameterFactory.Create(backdropString.Handle); @@ -235,7 +235,8 @@ namespace Avalonia.Win32.WinRT.Composition using var compositionEffectBrush = blurEffectFactory.CreateBrush(); using var backdrop = _compositor2.CreateBackdropBrush(); using var backdropBrush = backdrop.QueryInterface(); - + + var saturateEffect = new SaturationEffect(blurEffect); using var satEffectFactory = _compositor.CreateEffectFactory(saturateEffect); using var sat = satEffectFactory.CreateBrush(); @@ -259,8 +260,8 @@ namespace Avalonia.Win32.WinRT.Composition foreach (var visual in containerVisuals) { visual?.SetClip(geometricClipWithGeometry.QueryInterface()); - } - + } + return roundedRectangleGeometry.CloneReference(); } @@ -269,8 +270,8 @@ namespace Avalonia.Win32.WinRT.Composition using var spriteVisual = _compositor.CreateSpriteVisual(); using var visual = spriteVisual.QueryInterface(); using var visual2 = spriteVisual.QueryInterface(); - - + + spriteVisual.SetBrush(compositionBrush); visual.SetIsVisible(0); visual2.SetRelativeSizeAdjustment(new Vector2(1.0f, 1.0f)); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 0a0f600520..651d3adb11 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -82,7 +82,7 @@ namespace Avalonia.Win32 private Size _minSize; private Size _maxSize; private POINT _maxTrackSize; - private WindowImpl _parent; + private WindowImpl _parent; private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; private bool _isCloseRequested; private bool _shown; @@ -105,7 +105,9 @@ namespace Avalonia.Win32 _windowProperties = new WindowProperties { - ShowInTaskbar = false, IsResizable = true, Decorations = SystemDecorations.Full + ShowInTaskbar = false, + IsResizable = true, + Decorations = SystemDecorations.Full }; _rendererLock = new ManagedDeferredRendererLock(); @@ -114,13 +116,13 @@ namespace Avalonia.Win32 var compositionConnector = AvaloniaLocator.Current.GetService(); _isUsingComposition = compositionConnector is { } && - glPlatform is EglPlatformOpenGlInterface egl && - egl.Display is AngleWin32EglDisplay angleDisplay && - angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11; + glPlatform is EglPlatformOpenGlInterface egl && + egl.Display is AngleWin32EglDisplay angleDisplay && + angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11; CreateWindow(); _framebuffer = new FramebufferManager(_hwnd); - + if (glPlatform != null) { if (_isUsingComposition) @@ -166,7 +168,7 @@ namespace Avalonia.Win32 public Action PositionChanged { get; set; } public Action WindowStateChanged { get; set; } - + public Action LostFocus { get; set; } public Action TransparencyLevelChanged { get; set; } @@ -222,8 +224,7 @@ namespace Avalonia.Win32 return new Size(rcWindow.Width, rcWindow.Height) / RenderScaling; } - DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, - Marshal.SizeOf(typeof(RECT))); + DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, Marshal.SizeOf(typeof(RECT))); return new Size(rect.Width, rect.Height) / RenderScaling; } } @@ -232,8 +233,7 @@ namespace Avalonia.Win32 public IPlatformHandle Handle { get; private set; } - public virtual Size MaxAutoSizeHint => - new Size(_maxTrackSize.X / RenderScaling, _maxTrackSize.Y / RenderScaling); + public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / RenderScaling, _maxTrackSize.Y / RenderScaling); public IMouseDevice MouseDevice => _mouseDevice; @@ -241,7 +241,7 @@ namespace Avalonia.Win32 { get { - if (_isFullScreenActive) + if(_isFullScreenActive) { return WindowState.FullScreen; } @@ -264,7 +264,7 @@ namespace Avalonia.Win32 ShowWindow(value, true); } - _showWindowState = value; + _showWindowState = value; } } @@ -272,7 +272,7 @@ namespace Avalonia.Win32 protected IntPtr Hwnd => _hwnd; - public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) + public void SetTransparencyLevelHint (WindowTransparencyLevel transparencyLevel) { TransparencyLevel = EnableBlur(transparencyLevel); } @@ -312,12 +312,12 @@ namespace Avalonia.Win32 } var blurInfo = new DWM_BLURBEHIND(false); - + if (transparencyLevel == WindowTransparencyLevel.Blur) { blurInfo = new DWM_BLURBEHIND(true); } - + DwmEnableBlurBehindWindow(_hwnd, ref blurInfo); if (transparencyLevel == WindowTransparencyLevel.Transparent) @@ -385,8 +385,7 @@ namespace Avalonia.Win32 } else { - bool canUseAcrylic = Win32Platform.WindowsVersion.Major > 10 || - Win32Platform.WindowsVersion.Build >= 19628; + bool canUseAcrylic = Win32Platform.WindowsVersion.Major > 10 || Win32Platform.WindowsVersion.Build >= 19628; var accent = new AccentPolicy(); var accentStructSize = Marshal.SizeOf(accent); @@ -537,7 +536,7 @@ namespace Avalonia.Win32 { BeforeCloseCleanup(true); } - + DestroyWindow(_hwnd); _hwnd = IntPtr.Zero; } @@ -603,7 +602,7 @@ namespace Avalonia.Win32 public void SetParent(IWindowImpl parent) { _parent = (WindowImpl)parent; - + var parentHwnd = _parent?._hwnd ?? IntPtr.Zero; if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar) @@ -714,7 +713,7 @@ namespace Avalonia.Win32 _isUsingComposition ? (int)WindowStyles.WS_EX_NOREDIRECTIONBITMAP : 0, atom, null, - (int)WindowStyles.WS_OVERLAPPEDWINDOW | (int)WindowStyles.WS_CLIPCHILDREN, + (int)WindowStyles.WS_OVERLAPPEDWINDOW | (int) WindowStyles.WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, @@ -770,7 +769,7 @@ namespace Avalonia.Win32 } if (ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8) - { + { var monitor = MonitorFromWindow( _hwnd, MONITOR.MONITOR_DEFAULTTONEAREST); @@ -816,9 +815,7 @@ namespace Avalonia.Win32 // Set new window style and size. SetStyle(current & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME), false); - SetExtendedStyle( - currentEx & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | - WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE), false); + SetExtendedStyle(currentEx & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE), false); // On expand, if we're given a window_rect, grow to it, otherwise do // not resize. @@ -828,9 +825,8 @@ namespace Avalonia.Win32 var window_rect = monitor_info.rcMonitor.ToPixelRect(); SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y, - window_rect.Width, window_rect.Height, - SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | - SetWindowPosFlags.SWP_FRAMECHANGED); + window_rect.Width, window_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); _isFullScreenActive = true; } @@ -849,22 +845,21 @@ namespace Avalonia.Win32 var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, - new_rect.Height, - SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | - SetWindowPosFlags.SWP_FRAMECHANGED); + new_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); UpdateWindowProperties(_windowProperties, true); } TaskBarList.MarkFullscreen(_hwnd, fullscreen); - + ExtendClientArea(); } private MARGINS UpdateExtendMargins() { RECT borderThickness = new RECT(); - RECT borderCaptionThickness = new RECT(); + RECT borderCaptionThickness = new RECT(); AdjustWindowRectEx(ref borderCaptionThickness, (uint)(GetStyle()), false, 0); AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle() & ~WindowStyles.WS_CAPTION), false, 0); @@ -873,8 +868,7 @@ namespace Avalonia.Win32 borderCaptionThickness.left *= -1; borderCaptionThickness.top *= -1; - bool wantsTitleBar = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) || - _extendTitleBarHint == -1; + bool wantsTitleBar = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) || _extendTitleBarHint == -1; if (!wantsTitleBar) { @@ -888,22 +882,15 @@ namespace Avalonia.Win32 if (_extendTitleBarHint != -1) { - borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling); + borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling); } - margins.cyTopHeight = - _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && - !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome) ? - borderCaptionThickness.top : - 1; + margins.cyTopHeight = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : 1; if (WindowState == WindowState.Maximized) { - _extendedMargins = new Thickness(0, (borderCaptionThickness.top - borderThickness.top) / RenderScaling, - 0, 0); - _offScreenMargin = new Thickness(borderThickness.left / RenderScaling, - borderThickness.top / RenderScaling, borderThickness.right / RenderScaling, - borderThickness.bottom / RenderScaling); + _extendedMargins = new Thickness(0, (borderCaptionThickness.top - borderThickness.top) / RenderScaling, 0, 0); + _offScreenMargin = new Thickness(borderThickness.left / RenderScaling, borderThickness.top / RenderScaling, borderThickness.right / RenderScaling, borderThickness.bottom / RenderScaling); } else { @@ -920,13 +907,12 @@ namespace Avalonia.Win32 { return; } - + if (DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled) { _isClientAreaExtended = false; return; } - GetClientRect(_hwnd, out var rcClient); GetWindowRect(_hwnd, out var rcWindow); @@ -949,14 +935,12 @@ namespace Avalonia.Win32 _offScreenMargin = new Thickness(); _extendedMargins = new Thickness(); - - Resize(new Size(rcWindow.Width / RenderScaling, rcWindow.Height / RenderScaling), - PlatformResizeReason.Layout); + + Resize(new Size(rcWindow.Width/ RenderScaling, rcWindow.Height / RenderScaling), PlatformResizeReason.Layout); } - if (!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && - !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints - .PreferSystemChrome))) + if(!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && + !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome))) { EnableCloseButton(_hwnd); } @@ -971,12 +955,12 @@ namespace Avalonia.Win32 private void ShowWindow(WindowState state, bool activate) { _shown = true; - + if (_isClientAreaExtended) { ExtendClientArea(); } - + ShowWindowCommand? command; var newWindowProperties = _windowProperties; @@ -994,7 +978,7 @@ namespace Avalonia.Win32 case WindowState.Normal: newWindowProperties.IsFullScreen = false; - command = IsWindowVisible(_hwnd) ? ShowWindowCommand.Restore : + command = IsWindowVisible(_hwnd) ? ShowWindowCommand.Restore : activate ? ShowWindowCommand.Normal : ShowWindowCommand.ShowNoActivate; break; @@ -1024,7 +1008,7 @@ namespace Avalonia.Win32 SetFocus(_hwnd); } } - + private void BeforeCloseCleanup(bool isDisposing) { // Based on https://github.com/dotnet/wpf/blob/master/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Window.cs#L4270-L4337 @@ -1042,7 +1026,7 @@ namespace Avalonia.Win32 // Our window closed callback will set enabled state to a correct value after child window gets destroyed. _parent.SetEnabled(true); } - + // We also need to activate our parent window since again OS might try to activate a window behind if it is not set. if (wasActive) { @@ -1069,7 +1053,7 @@ namespace Avalonia.Win32 SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); } } - } + } private WindowStyles GetWindowStateStyles() { @@ -1222,7 +1206,10 @@ namespace Avalonia.Win32 var margins = new MARGINS { - cyBottomHeight = margin, cxRightWidth = margin, cxLeftWidth = margin, cyTopHeight = margin + cyBottomHeight = margin, + cxRightWidth = margin, + cxLeftWidth = margin, + cyTopHeight = margin }; DwmExtendFrameIntoClientArea(_hwnd, ref margins); @@ -1243,7 +1230,7 @@ namespace Avalonia.Win32 SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); } - } + } } private const int MF_BYCOMMAND = 0x0; @@ -1257,13 +1244,12 @@ namespace Avalonia.Win32 void DisableCloseButton(IntPtr hwnd) { EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE, - MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); + MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); } - void EnableCloseButton(IntPtr hwnd) { EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE, - MF_BYCOMMAND | MF_ENABLED); + MF_BYCOMMAND | MF_ENABLED); } #if USE_MANAGED_DRAG @@ -1294,9 +1280,9 @@ namespace Avalonia.Win32 public void SetExtendClientAreaToDecorationsHint(bool hint) { _isClientAreaExtended = hint; - - ExtendClientArea(); - } + + ExtendClientArea(); + } public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) { @@ -1304,7 +1290,7 @@ namespace Avalonia.Win32 ExtendClientArea(); } - + /// public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) { @@ -1318,11 +1304,9 @@ namespace Avalonia.Win32 /// public Action ExtendClientAreaToDecorationsChanged { get; set; } - + /// - public bool NeedsManagedDecorations => _isClientAreaExtended && - _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints - .PreferSystemChrome); + public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome); /// public Thickness ExtendedMargins => _extendedMargins; @@ -1331,8 +1315,7 @@ namespace Avalonia.Win32 public Thickness OffScreenMargin => _offScreenMargin; /// - public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = - new AcrylicPlatformCompensationLevels(1, 0.8, 0); + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); private ResizeReasonScope SetResizeReason(PlatformResizeReason reason) { @@ -1360,7 +1343,7 @@ namespace Avalonia.Win32 { private readonly WindowImpl _owner; private readonly PlatformResizeReason _restore; - + public ResizeReasonScope(WindowImpl owner, PlatformResizeReason restore) { _owner = owner; From fdb453cfbfa4656307cac3343e521304628437bc Mon Sep 17 00:00:00 2001 From: Adir Date: Sat, 11 Sep 2021 22:09:17 +0300 Subject: [PATCH 030/198] Added missing return on WinUICompositorConnection.cs --- .../WinRT/Composition/WinUICompositorConnection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index f5706b6fb5..21de6f169b 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -149,6 +149,7 @@ namespace Avalonia.Win32.WinRT.Composition try { TryCreateAndRegisterCore(angle, backdropCornerRadius); + return; } catch (Exception e) { From 3342d41a128c53a6868805ed589f6e716ed1eedf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 12 Sep 2021 10:37:37 +0100 Subject: [PATCH 031/198] fix ios stub. --- src/iOS/Avalonia.iOS/Stubs.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/iOS/Avalonia.iOS/Stubs.cs b/src/iOS/Avalonia.iOS/Stubs.cs index c2526d7d9f..b13dfd39e0 100644 --- a/src/iOS/Avalonia.iOS/Stubs.cs +++ b/src/iOS/Avalonia.iOS/Stubs.cs @@ -21,6 +21,8 @@ namespace Avalonia.iOS public IWindowImpl CreateWindow() => throw new NotSupportedException(); public IWindowImpl CreateEmbeddableWindow() => throw new NotSupportedException(); + + public ITrayIconImpl CreateTrayIcon() => throw new NotSupportedException(); } class PlatformIconLoaderStub : IPlatformIconLoader From ae6a9a01affdde6b6f33689a9f2dbc427212d7b8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 14 Sep 2021 11:10:22 +0100 Subject: [PATCH 032/198] add a tray icon stub to x11 platform. --- src/Avalonia.X11/X11Platform.cs | 2 +- src/Avalonia.X11/X11TrayIconImpl.cs | 33 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.X11/X11TrayIconImpl.cs diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index c6cea60efd..5d80c860a7 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -103,7 +103,7 @@ namespace Avalonia.X11 public ITrayIconImpl CreateTrayIcon () { - throw new NotImplementedException(); + return new X11TrayIconImpl(); } public IWindowImpl CreateWindow() diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs new file mode 100644 index 0000000000..8909a4604b --- /dev/null +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -0,0 +1,33 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Platform; + +namespace Avalonia.X11 +{ + class X11TrayIconImpl : ITrayIconImpl + { + public INativeMenuExporter MenuExporter => null; + + public Action OnClicked { get; set; } + + public void Dispose() + { + + } + + public void SetIcon(IWindowIconImpl icon) + { + + } + + public void SetIsVisible(bool visible) + { + + } + + public void SetToolTipText(string text) + { + + } + } +} From 95f4ed1a2100844849fa56ee879fac5c8f0ac683 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 14 Sep 2021 11:41:49 +0100 Subject: [PATCH 033/198] add diagnostics for debugging window deactivated issue. --- samples/ControlCatalog/App.xaml.cs | 1 + src/Windows/Avalonia.Win32/TrayIconImpl.cs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 36b6fc2dcd..e044987a27 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -106,6 +106,7 @@ namespace ControlCatalog if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) { desktopLifetime.MainWindow = new MainWindow(); + desktopLifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index ba208a4b74..fce56bcb21 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; @@ -179,13 +181,28 @@ namespace Avalonia.Win32 _positioner = new ManagedPopupPositioner(new TrayIconManagedPopupPositionerPopupImplHelper(MoveResize)); Topmost = true; + Activated += TrayPopupRoot_Activated; Deactivated += TrayPopupRoot_Deactivated; + LostFocus += TrayPopupRoot_LostFocus1; + ShowInTaskbar = false; } + private void TrayPopupRoot_LostFocus1(object sender, Interactivity.RoutedEventArgs e) + { + Debug.WriteLine("TrayIcon - Lost Focus"); + } + + private void TrayPopupRoot_Activated(object sender, EventArgs e) + { + Debug.WriteLine("TrayIcon - Activated"); + } + private void TrayPopupRoot_Deactivated(object sender, EventArgs e) { + Debug.WriteLine("TrayIcon - Deactivated"); + Dispatcher.UIThread.Post(() => { Close(); From a189f52bcbdb06bd50c3265a3636f5f11c07d40d Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 17 Sep 2021 15:01:43 +0800 Subject: [PATCH 034/198] testing stuff for now --- .../DBusSystemTray/SNIDBus.cs | 194 ++++++++++++++++++ src/Avalonia.X11/X11TrayIconImpl.cs | 7 +- 2 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs new file mode 100644 index 0000000000..a4bc659d20 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] +namespace Avalonia.FreeDesktop.DBusSystemTray +{ + public class SNIDBus + { + public SNIDBus() + { + + } + + public async void Initialize() + { + + var serviceName = $"AvaloniaSNITest_{Guid.NewGuid()}"; + + var path = + Connection.Session.CreateProxy("org.kde.StatusNotifierWatcher", + "org.kde.StatusNotifierWatcher"); + await path.RegisterStatusNotifierHostAsync(serviceName); + + await path.WatchStatusNotifierHostRegisteredAsync(() => + { + + }, z => + { + + }); + + + + } + } + + [DBusInterface("org.kde.StatusNotifierWatcher")] + interface IStatusNotifierWatcher : IDBusObject + { + Task RegisterStatusNotifierItemAsync(string Service); + Task RegisterStatusNotifierHostAsync(string Service); + Task WatchStatusNotifierItemRegisteredAsync(Action handler, Action onError = null); + Task WatchStatusNotifierItemUnregisteredAsync(Action handler, Action onError = null); + Task WatchStatusNotifierHostRegisteredAsync(Action handler, Action onError = null); + Task GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action handler); + } + + [Dictionary] + class StatusNotifierWatcherProperties + { + private string[] _RegisteredStatusNotifierItems = default(string[]); + public string[] RegisteredStatusNotifierItems + { + get + { + return _RegisteredStatusNotifierItems; + } + + set + { + _RegisteredStatusNotifierItems = (value); + } + } + + private bool _IsStatusNotifierHostRegistered = default(bool); + public bool IsStatusNotifierHostRegistered + { + get + { + return _IsStatusNotifierHostRegistered; + } + + set + { + _IsStatusNotifierHostRegistered = (value); + } + } + + private int _ProtocolVersion = default(int); + public int ProtocolVersion + { + get + { + return _ProtocolVersion; + } + + set + { + _ProtocolVersion = (value); + } + } + } + + static class StatusNotifierWatcherExtensions + { + public static Task GetRegisteredStatusNotifierItemsAsync(this IStatusNotifierWatcher o) => o.GetAsync("RegisteredStatusNotifierItems"); + public static Task GetIsStatusNotifierHostRegisteredAsync(this IStatusNotifierWatcher o) => o.GetAsync("IsStatusNotifierHostRegistered"); + public static Task GetProtocolVersionAsync(this IStatusNotifierWatcher o) => o.GetAsync("ProtocolVersion"); + } + + [DBusInterface("org.gtk.Actions")] + interface IActions : IDBusObject + { + Task ListAsync(); + Task<(bool description, Signature, object[])> DescribeAsync(string ActionName); + Task> DescribeAllAsync(); + Task ActivateAsync(string ActionName, object[] Parameter, IDictionary PlatformData); + Task SetStateAsync(string ActionName, object Value, IDictionary PlatformData); + Task WatchChangedAsync(Action<(string[] removals, IDictionary enableChanges, IDictionary stateChanges, IDictionary additions)> handler, Action onError = null); + } + + [DBusInterface("org.gtk.Application")] + interface IApplication : IDBusObject + { + Task ActivateAsync(IDictionary PlatformData); + Task OpenAsync(string[] Uris, string Hint, IDictionary PlatformData); + Task CommandLineAsync(ObjectPath Path, byte[][] Arguments, IDictionary PlatformData); + Task GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action handler); + } + + [Dictionary] + class ApplicationProperties + { + private bool _Busy = default(bool); + public bool Busy + { + get + { + return _Busy; + } + + set + { + _Busy = (value); + } + } + } + + static class ApplicationExtensions + { + public static Task GetBusyAsync(this IApplication o) => o.GetAsync("Busy"); + } + + [DBusInterface("org.freedesktop.Application")] + interface IApplication0 : IDBusObject + { + Task ActivateAsync(IDictionary PlatformData); + Task OpenAsync(string[] Uris, IDictionary PlatformData); + Task ActivateActionAsync(string ActionName, object[] Parameter, IDictionary PlatformData); + } + + [DBusInterface("org.gnome.Sysprof3.Profiler")] + interface IProfiler : IDBusObject + { + Task StartAsync(IDictionary Options, CloseSafeHandle Fd); + Task StopAsync(); + Task GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action handler); + } + + [Dictionary] + class ProfilerProperties + { + private IDictionary _Capabilities = default(IDictionary); + public IDictionary Capabilities + { + get + { + return _Capabilities; + } + + set + { + _Capabilities = (value); + } + } + } + + static class ProfilerExtensions + { + public static Task> GetCapabilitiesAsync(this IProfiler o) => o.GetAsync>("Capabilities"); + } +} diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 8909a4604b..0ab34434fb 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls.Platform; +using Avalonia.FreeDesktop.DBusSystemTray; using Avalonia.Platform; namespace Avalonia.X11 @@ -9,10 +10,12 @@ namespace Avalonia.X11 public INativeMenuExporter MenuExporter => null; public Action OnClicked { get; set; } - + private SNIDBus sni = new SNIDBus(); + public void Dispose() { + } public void SetIcon(IWindowIconImpl icon) @@ -27,7 +30,7 @@ namespace Avalonia.X11 public void SetToolTipText(string text) { - + sni.Initialize(); } } } From 6016c56e6d627c54a41e6584f4fda386a6aa2f13 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 17 Sep 2021 15:17:31 +0800 Subject: [PATCH 035/198] connection made --- src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs index a4bc659d20..f3d1073e29 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs @@ -21,9 +21,15 @@ namespace Avalonia.FreeDesktop.DBusSystemTray var path = Connection.Session.CreateProxy("org.kde.StatusNotifierWatcher", - "org.kde.StatusNotifierWatcher"); + "/StatusNotifierWatcher"); + await path.RegisterStatusNotifierHostAsync(serviceName); + await path.WatchPropertiesAsync(x => + { + + }); + await path.WatchStatusNotifierHostRegisteredAsync(() => { From b216b1afbe79d2028880abcea4005c059ce9d2b5 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 17 Sep 2021 20:03:43 +0800 Subject: [PATCH 036/198] some progress.... --- .../DBusSystemTray/SNIDBus.cs | 358 +++++++++++++----- src/Avalonia.X11/X11Platform.cs | 2 +- src/Avalonia.X11/X11TrayIconImpl.cs | 11 +- 3 files changed, 278 insertions(+), 93 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs index f3d1073e29..fc62d24a7d 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs @@ -1,55 +1,229 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Reactive.Disposables; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Tmds.DBus; [assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] + namespace Avalonia.FreeDesktop.DBusSystemTray { public class SNIDBus { public SNIDBus() { - } - public async void Initialize() + private static int trayinstanceID = 0; + + private static int GetTID() + { + trayinstanceID = (int)new Random().Next(0, 100); + return trayinstanceID; + } + + public async void Initialize() { + var x = Process.GetCurrentProcess().Id; + var y = GetTID(); - var serviceName = $"AvaloniaSNITest_{Guid.NewGuid()}"; + var sysTraySrvName = $"org.kde.StatusNotifierItem-{x}-{y}"; + var tx = new StatusNotifierItem(); - var path = - Connection.Session.CreateProxy("org.kde.StatusNotifierWatcher", + await DBusHelper.Connection.RegisterObjectAsync(tx); + + await DBusHelper.Connection.RegisterServiceAsync(sysTraySrvName, () => + { + }); + + while (!await DBusHelper.Connection.IsServiceActiveAsync(sysTraySrvName)) + { + await Task.Delay(1000); + } + + var yx = DBusHelper.Connection.CreateProxy(sysTraySrvName, tx.ObjectPath); + + var snw = + DBusHelper.Connection.CreateProxy("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); - - await path.RegisterStatusNotifierHostAsync(serviceName); - await path.WatchPropertiesAsync(x => - { + while (!await DBusHelper.Connection.IsServiceActiveAsync("org.kde.StatusNotifierWatcher")) + { + await Task.Delay(1000); + } + + await snw.RegisterStatusNotifierItemAsync(sysTraySrvName); + + tx.Ready(); + + await yx.ActivateAsync(1, 1); + } + } + + internal class StatusNotifierItem : IStatusNotifierItem + { + private event Action OnPropertyChange; + + public event Action OnTitleChanged; + public event Action OnIconChanged; + public event Action OnAttentionIconChanged; + public event Action OnOverlayIconChanged; - }); - await path.WatchStatusNotifierHostRegisteredAsync(() => - { + public Action NewToolTipAsync; + public ObjectPath ObjectPath { get; } - }, z => - { + readonly StatusNotifierItemProperties props; + + public StatusNotifierItem() + { + var ID = Guid.NewGuid().ToString().Replace("-", ""); + ObjectPath = new ObjectPath($"/StatusNotifierItem"); + props = new StatusNotifierItemProperties(); + + props.Title = "Avalonia Test Tray"; + props.Id = ID; +// + // + // public string Category { get; set; } = default; + // + // public string Id { get; set; } = default; + // + // public string Title { get; set; } = default; + // + // public string Status { get; set; } = default; + // + // public int WindowId { get; set; } = default; + } - }); - - + // static class StatusNotifierItemExtensions + // { + // public static Task GetCategoryAsync(this IStatusNotifierItem o) => o.GetAsync("Category"); + // public static Task GetIdAsync(this IStatusNotifierItem o) => o.GetAsync("Id"); + // public static Task GetTitleAsync(this IStatusNotifierItem o) => o.GetAsync("Title"); + // public static Task GetStatusAsync(this IStatusNotifierItem o) => o.GetAsync("Status"); + // public static Task GetWindowIdAsync(this IStatusNotifierItem o) => o.GetAsync("WindowId"); + // + // public static Task GetIconThemePathAsync(this IStatusNotifierItem o) => + // o.GetAsync("IconThemePath"); + // + // public static Task GetMenuAsync(this IStatusNotifierItem o) => o.GetAsync("Menu"); + // public static Task GetItemIsMenuAsync(this IStatusNotifierItem o) => o.GetAsync("ItemIsMenu"); + // public static Task GetIconNameAsync(this IStatusNotifierItem o) => o.GetAsync("IconName"); + // + // public static Task<(int, int, byte[])[]> GetIconPixmapAsync(this IStatusNotifierItem o) => + // o.GetAsync<(int, int, byte[])[]>("IconPixmap"); + // + // public static Task GetOverlayIconNameAsync(this IStatusNotifierItem o) => + // o.GetAsync("OverlayIconName"); + // + // public static Task<(int, int, byte[])[]> GetOverlayIconPixmapAsync(this IStatusNotifierItem o) => + // o.GetAsync<(int, int, byte[])[]>("OverlayIconPixmap"); + // + // public static Task GetAttentionIconNameAsync(this IStatusNotifierItem o) => + // o.GetAsync("AttentionIconName"); + // + // public static Task<(int, int, byte[])[]> GetAttentionIconPixmapAsync(this IStatusNotifierItem o) => + // o.GetAsync<(int, int, byte[])[]>("AttentionIconPixmap"); + // + // public static Task GetAttentionMovieNameAsync(this IStatusNotifierItem o) => + // o.GetAsync("AttentionMovieName"); + // + // public static Task<(string, (int, int, byte[])[], string, string)> + // GetToolTipAsync(this IStatusNotifierItem o) => + // o.GetAsync<(string, (int, int, byte[])[], string, string)>("ToolTip"); + // } + + public async Task ContextMenuAsync(int X, int Y) + { + } + + public async Task ActivateAsync(int X, int Y) + { + // OnPropertyChange?.Invoke(new PropertyChanges()); + } + + public async Task SecondaryActivateAsync(int X, int Y) + { + throw new NotImplementedException(); + } + + public async Task ScrollAsync(int Delta, string Orientation) + { + } + + public async Task WatchNewTitleAsync(Action handler, Action onError = null) + { + OnTitleChanged += handler; + return Disposable.Create(() => OnTitleChanged -= handler); + } + + + public async Task WatchNewIconAsync(Action handler, Action onError = null) + { + OnIconChanged += handler; + return Disposable.Create(() => OnIconChanged -= handler); + } + + public async Task WatchNewAttentionIconAsync(Action handler, Action onError = null) + { + OnAttentionIconChanged += handler; + return Disposable.Create(() => OnAttentionIconChanged -= handler); + } + + + public async Task WatchNewOverlayIconAsync(Action handler, Action onError = null) + { + OnOverlayIconChanged += handler; + return Disposable.Create(() => OnOverlayIconChanged -= handler); + } + + public async Task WatchNewToolTipAsync(Action handler, Action onError = null) + { + NewToolTipAsync += handler; + return Disposable.Create(() => NewToolTipAsync -= handler); + } + + public async Task WatchNewStatusAsync(Action handler, Action onError = null) + { + NewStatusAsync += handler; + return Disposable.Create(() => NewStatusAsync -= handler); + } + + public Action NewStatusAsync { get; set; } + + public async Task GetAllAsync() + { + return props; + } + + public async Task WatchPropertiesAsync(Action handler) + { + OnPropertyChange += handler; + return Disposable.Create(() => OnPropertyChange -= handler); + } + + public void Ready() + { + OnTitleChanged?.Invoke(); } } - + [DBusInterface("org.kde.StatusNotifierWatcher")] interface IStatusNotifierWatcher : IDBusObject { Task RegisterStatusNotifierItemAsync(string Service); Task RegisterStatusNotifierHostAsync(string Service); - Task WatchStatusNotifierItemRegisteredAsync(Action handler, Action onError = null); - Task WatchStatusNotifierItemUnregisteredAsync(Action handler, Action onError = null); + + Task WatchStatusNotifierItemRegisteredAsync(Action handler, + Action onError = null); + + Task WatchStatusNotifierItemUnregisteredAsync(Action handler, + Action onError = null); + Task WatchStatusNotifierHostRegisteredAsync(Action handler, Action onError = null); Task GetAsync(string prop); Task GetAllAsync(); @@ -60,54 +234,23 @@ namespace Avalonia.FreeDesktop.DBusSystemTray [Dictionary] class StatusNotifierWatcherProperties { - private string[] _RegisteredStatusNotifierItems = default(string[]); - public string[] RegisteredStatusNotifierItems - { - get - { - return _RegisteredStatusNotifierItems; - } + public string[] RegisteredStatusNotifierItems { get; set; } = default; - set - { - _RegisteredStatusNotifierItems = (value); - } - } - - private bool _IsStatusNotifierHostRegistered = default(bool); - public bool IsStatusNotifierHostRegistered - { - get - { - return _IsStatusNotifierHostRegistered; - } + public bool IsStatusNotifierHostRegistered { get; set; } = default; - set - { - _IsStatusNotifierHostRegistered = (value); - } - } - - private int _ProtocolVersion = default(int); - public int ProtocolVersion - { - get - { - return _ProtocolVersion; - } - - set - { - _ProtocolVersion = (value); - } - } + public int ProtocolVersion { get; set; } = default; } static class StatusNotifierWatcherExtensions { - public static Task GetRegisteredStatusNotifierItemsAsync(this IStatusNotifierWatcher o) => o.GetAsync("RegisteredStatusNotifierItems"); - public static Task GetIsStatusNotifierHostRegisteredAsync(this IStatusNotifierWatcher o) => o.GetAsync("IsStatusNotifierHostRegistered"); - public static Task GetProtocolVersionAsync(this IStatusNotifierWatcher o) => o.GetAsync("ProtocolVersion"); + public static Task GetRegisteredStatusNotifierItemsAsync(this IStatusNotifierWatcher o) => + o.GetAsync("RegisteredStatusNotifierItems"); + + public static Task GetIsStatusNotifierHostRegisteredAsync(this IStatusNotifierWatcher o) => + o.GetAsync("IsStatusNotifierHostRegistered"); + + public static Task GetProtocolVersionAsync(this IStatusNotifierWatcher o) => + o.GetAsync("ProtocolVersion"); } [DBusInterface("org.gtk.Actions")] @@ -118,7 +261,11 @@ namespace Avalonia.FreeDesktop.DBusSystemTray Task> DescribeAllAsync(); Task ActivateAsync(string ActionName, object[] Parameter, IDictionary PlatformData); Task SetStateAsync(string ActionName, object Value, IDictionary PlatformData); - Task WatchChangedAsync(Action<(string[] removals, IDictionary enableChanges, IDictionary stateChanges, IDictionary additions)> handler, Action onError = null); + + Task WatchChangedAsync( + Action<(string[] removals, IDictionary enableChanges, IDictionary stateChanges + , IDictionary additions)> handler, + Action onError = null); } [DBusInterface("org.gtk.Application")] @@ -136,19 +283,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray [Dictionary] class ApplicationProperties { - private bool _Busy = default(bool); - public bool Busy - { - get - { - return _Busy; - } - - set - { - _Busy = (value); - } - } + public bool Busy { get; set; } = default; } static class ApplicationExtensions @@ -178,23 +313,66 @@ namespace Avalonia.FreeDesktop.DBusSystemTray [Dictionary] class ProfilerProperties { - private IDictionary _Capabilities = default(IDictionary); - public IDictionary Capabilities - { - get - { - return _Capabilities; - } - - set - { - _Capabilities = (value); - } - } + public IDictionary Capabilities { get; set; } = default; } static class ProfilerExtensions { - public static Task> GetCapabilitiesAsync(this IProfiler o) => o.GetAsync>("Capabilities"); + public static Task> GetCapabilitiesAsync(this IProfiler o) => + o.GetAsync>("Capabilities"); + } + + [DBusInterface("org.kde.StatusNotifierItem")] + interface IStatusNotifierItem : IDBusObject + { + Task ContextMenuAsync(int X, int Y); + Task ActivateAsync(int X, int Y); + Task SecondaryActivateAsync(int X, int Y); + Task ScrollAsync(int Delta, string Orientation); + Task WatchNewTitleAsync(Action handler, Action onError = null); + Task WatchNewIconAsync(Action handler, Action onError = null); + Task WatchNewAttentionIconAsync(Action handler, Action onError = null); + Task WatchNewOverlayIconAsync(Action handler, Action onError = null); + Task WatchNewToolTipAsync(Action handler, Action onError = null); + Task WatchNewStatusAsync(Action handler, Action onError = null); + Task GetAllAsync(); + Task WatchPropertiesAsync(Action handler); + } + + + [Dictionary] + class StatusNotifierItemProperties + { + public string Category { get; set; } = default; + + public string Id { get; set; } = default; + + public string Title { get; set; } = default; + + public string Status { get; set; } = default; + + public int WindowId { get; set; } = default; + + public string IconThemePath { get; set; } = default; + + public ObjectPath Menu { get; set; } = default; + + public bool ItemIsMenu { get; set; } = default; + + public string IconName { get; set; } = default; + + public (int, int, byte[])[] IconPixmap { get; set; } = default; + + public string OverlayIconName { get; set; } = default; + + public (int, int, byte[])[] OverlayIconPixmap { get; set; } = default; + + public string AttentionIconName { get; set; } = default; + + public (int, int, byte[])[] AttentionIconPixmap { get; set; } = default; + + public string AttentionMovieName { get; set; } = default; + + public (string, (int, int, byte[])[], string, string) ToolTip { get; set; } = default; } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 5d80c860a7..eb805c0f0b 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -103,7 +103,7 @@ namespace Avalonia.X11 public ITrayIconImpl CreateTrayIcon () { - return new X11TrayIconImpl(); + return new X11TrayIconImpl(this); } public IWindowImpl CreateWindow() diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 0ab34434fb..0f42a15cae 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop.DBusSystemTray; using Avalonia.Platform; @@ -7,11 +8,17 @@ namespace Avalonia.X11 { class X11TrayIconImpl : ITrayIconImpl { + private readonly AvaloniaX11Platform _avaloniaX11Platform; public INativeMenuExporter MenuExporter => null; public Action OnClicked { get; set; } private SNIDBus sni = new SNIDBus(); - + + public X11TrayIconImpl(AvaloniaX11Platform avaloniaX11Platform) + { + _avaloniaX11Platform = avaloniaX11Platform; + } + public void Dispose() { @@ -30,7 +37,7 @@ namespace Avalonia.X11 public void SetToolTipText(string text) { - sni.Initialize(); + sni.Initialize(); } } } From a19b2027071b003083deabca49f4db09b7db62b7 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 17 Sep 2021 21:54:24 +0800 Subject: [PATCH 037/198] more updates --- .../DBusSystemTray/SNIDBus.cs | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs index fc62d24a7d..f659c710a1 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs @@ -55,10 +55,6 @@ namespace Avalonia.FreeDesktop.DBusSystemTray } await snw.RegisterStatusNotifierItemAsync(sysTraySrvName); - - tx.Ready(); - - await yx.ActivateAsync(1, 1); } } @@ -83,9 +79,19 @@ namespace Avalonia.FreeDesktop.DBusSystemTray ObjectPath = new ObjectPath($"/StatusNotifierItem"); props = new StatusNotifierItemProperties(); + var dummyicons = new[] { (1, 1, new byte[8 * 4]), (1, 1, new byte[8 * 4]), }; + + + props.Title = "Avalonia Test Tray"; + props.IconPixmap = dummyicons; props.Id = ID; -// + props.IconName = "Avalonia"; + props.ToolTip = ("Avalonia Test Tooltip", dummyicons, "Avalonia Test Tooltip Message", "And another one"); + + + + // // // public string Category { get; set; } = default; // @@ -98,6 +104,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray // public int WindowId { get; set; } = default; } + // static class StatusNotifierItemExtensions // { @@ -148,7 +155,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public async Task SecondaryActivateAsync(int X, int Y) { - throw new NotImplementedException(); + //throw new NotImplementedException(); } public async Task ScrollAsync(int Delta, string Orientation) @@ -193,6 +200,11 @@ namespace Avalonia.FreeDesktop.DBusSystemTray return Disposable.Create(() => NewStatusAsync -= handler); } + public async Task GetAsync(string prop) + { + return default; + } + public Action NewStatusAsync { get; set; } public async Task GetAllAsync() @@ -200,6 +212,11 @@ namespace Avalonia.FreeDesktop.DBusSystemTray return props; } + public async Task SetAsync(string prop, object val) + { + throw new NotImplementedException(); + } + public async Task WatchPropertiesAsync(Action handler) { OnPropertyChange += handler; @@ -335,7 +352,9 @@ namespace Avalonia.FreeDesktop.DBusSystemTray Task WatchNewOverlayIconAsync(Action handler, Action onError = null); Task WatchNewToolTipAsync(Action handler, Action onError = null); Task WatchNewStatusAsync(Action handler, Action onError = null); + Task GetAsync(string prop); Task GetAllAsync(); + Task SetAsync(string prop, object val); Task WatchPropertiesAsync(Action handler); } From 7656558b9647fee89e4054d7463053ed3088b9ee Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 00:03:07 +0800 Subject: [PATCH 038/198] converting x11 icons to pixmap --- .../DBusSystemTray/SNIDBus.cs | 216 +++++++++--------- src/Avalonia.X11/X11TrayIconImpl.cs | 61 ++++- 2 files changed, 166 insertions(+), 111 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs index f659c710a1..bc0350f321 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs @@ -10,13 +10,16 @@ using Tmds.DBus; namespace Avalonia.FreeDesktop.DBusSystemTray { - public class SNIDBus + public class SNIDBus : IDisposable { public SNIDBus() { } private static int trayinstanceID = 0; + private IStatusNotifierWatcher _snw; + private string _sysTraySrvName; + private StatusNotifierItem _statusNotifierItem; private static int GetTID() { @@ -29,32 +32,55 @@ namespace Avalonia.FreeDesktop.DBusSystemTray var x = Process.GetCurrentProcess().Id; var y = GetTID(); - var sysTraySrvName = $"org.kde.StatusNotifierItem-{x}-{y}"; - var tx = new StatusNotifierItem(); + _sysTraySrvName = $"org.kde.StatusNotifierItem-{x}-{y}"; + _statusNotifierItem = new StatusNotifierItem(); + var con = DBusHelper.Connection; - await DBusHelper.Connection.RegisterObjectAsync(tx); + await con.RegisterObjectAsync(_statusNotifierItem); - await DBusHelper.Connection.RegisterServiceAsync(sysTraySrvName, () => + await con.RegisterServiceAsync(_sysTraySrvName, () => { }); - while (!await DBusHelper.Connection.IsServiceActiveAsync(sysTraySrvName)) + while (!await con.IsServiceActiveAsync(_sysTraySrvName)) { - await Task.Delay(1000); + await Task.Delay(150); } - var yx = DBusHelper.Connection.CreateProxy(sysTraySrvName, tx.ObjectPath); + var yx = con.CreateProxy(_sysTraySrvName, _statusNotifierItem.ObjectPath); - var snw = + _snw = DBusHelper.Connection.CreateProxy("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); while (!await DBusHelper.Connection.IsServiceActiveAsync("org.kde.StatusNotifierWatcher")) { - await Task.Delay(1000); + await Task.Delay(150); } - await snw.RegisterStatusNotifierItemAsync(sysTraySrvName); + await _snw.RegisterStatusNotifierItemAsync(_sysTraySrvName); + // + // Task.Run(async () => + // { + // await Task.Delay(2000); + // tx.InvalidateAll(); + // }); + + } + + public async void Dispose() + { + var con = DBusHelper.Connection; + + if (await con.UnregisterServiceAsync(_sysTraySrvName)) + { + con.UnregisterObject(_statusNotifierItem); + } + } + + public void SetIcon(Pixmap pixmap) + { + _statusNotifierItem.SetIcon(pixmap); } } @@ -77,73 +103,15 @@ namespace Avalonia.FreeDesktop.DBusSystemTray { var ID = Guid.NewGuid().ToString().Replace("-", ""); ObjectPath = new ObjectPath($"/StatusNotifierItem"); - props = new StatusNotifierItemProperties(); - var dummyicons = new[] { (1, 1, new byte[8 * 4]), (1, 1, new byte[8 * 4]), }; - - - - props.Title = "Avalonia Test Tray"; - props.IconPixmap = dummyicons; - props.Id = ID; - props.IconName = "Avalonia"; - props.ToolTip = ("Avalonia Test Tooltip", dummyicons, "Avalonia Test Tooltip Message", "And another one"); - - - - // - // - // public string Category { get; set; } = default; - // - // public string Id { get; set; } = default; - // - // public string Title { get; set; } = default; - // - // public string Status { get; set; } = default; - // - // public int WindowId { get; set; } = default; + props = new StatusNotifierItemProperties + { + Title = "Avalonia Test Tray", + Status = "Avalonia Test Tray", + Id = "Avalonia Test Tray" + }; } - - - // static class StatusNotifierItemExtensions - // { - // public static Task GetCategoryAsync(this IStatusNotifierItem o) => o.GetAsync("Category"); - // public static Task GetIdAsync(this IStatusNotifierItem o) => o.GetAsync("Id"); - // public static Task GetTitleAsync(this IStatusNotifierItem o) => o.GetAsync("Title"); - // public static Task GetStatusAsync(this IStatusNotifierItem o) => o.GetAsync("Status"); - // public static Task GetWindowIdAsync(this IStatusNotifierItem o) => o.GetAsync("WindowId"); - // - // public static Task GetIconThemePathAsync(this IStatusNotifierItem o) => - // o.GetAsync("IconThemePath"); - // - // public static Task GetMenuAsync(this IStatusNotifierItem o) => o.GetAsync("Menu"); - // public static Task GetItemIsMenuAsync(this IStatusNotifierItem o) => o.GetAsync("ItemIsMenu"); - // public static Task GetIconNameAsync(this IStatusNotifierItem o) => o.GetAsync("IconName"); - // - // public static Task<(int, int, byte[])[]> GetIconPixmapAsync(this IStatusNotifierItem o) => - // o.GetAsync<(int, int, byte[])[]>("IconPixmap"); - // - // public static Task GetOverlayIconNameAsync(this IStatusNotifierItem o) => - // o.GetAsync("OverlayIconName"); - // - // public static Task<(int, int, byte[])[]> GetOverlayIconPixmapAsync(this IStatusNotifierItem o) => - // o.GetAsync<(int, int, byte[])[]>("OverlayIconPixmap"); - // - // public static Task GetAttentionIconNameAsync(this IStatusNotifierItem o) => - // o.GetAsync("AttentionIconName"); - // - // public static Task<(int, int, byte[])[]> GetAttentionIconPixmapAsync(this IStatusNotifierItem o) => - // o.GetAsync<(int, int, byte[])[]>("AttentionIconPixmap"); - // - // public static Task GetAttentionMovieNameAsync(this IStatusNotifierItem o) => - // o.GetAsync("AttentionMovieName"); - // - // public static Task<(string, (int, int, byte[])[], string, string)> - // GetToolTipAsync(this IStatusNotifierItem o) => - // o.GetAsync<(string, (int, int, byte[])[], string, string)>("ToolTip"); - // } - public async Task ContextMenuAsync(int X, int Y) { } @@ -162,6 +130,14 @@ namespace Avalonia.FreeDesktop.DBusSystemTray { } + + public void InvalidateAll() + { + OnTitleChanged?.Invoke(); + OnIconChanged?.Invoke(); + OnOverlayIconChanged?.Invoke(); + } + public async Task WatchNewTitleAsync(Action handler, Action onError = null) { OnTitleChanged += handler; @@ -202,20 +178,35 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public async Task GetAsync(string prop) { - return default; + return prop switch + { + "Category" => props.Category, + "Id" => props.Id, + "Title" => props.Title, + "Status" => props.Status, + "WindowId" => props.WindowId, + "IconThemePath" => props.IconThemePath, + "ItemIsMenu" => props.ItemIsMenu, + "IconName" => props.IconName, + "IconPixmap" => props.IconPixmap, + "OverlayIconName" => props.OverlayIconName, + "OverlayIconPixmap" => props.OverlayIconPixmap, + "AttentionIconName" => props.AttentionIconName, + "AttentionIconPixmap" => props.AttentionIconPixmap, + "AttentionMovieName" => props.AttentionMovieName, + "ToolTip" => props.ToolTip, + _ => default + }; } - public Action NewStatusAsync { get; set; } - public async Task GetAllAsync() { return props; } - public async Task SetAsync(string prop, object val) - { - throw new NotImplementedException(); - } + public Action NewStatusAsync { get; set; } + + public Task SetAsync(string prop, object val) => Task.CompletedTask; public async Task WatchPropertiesAsync(Action handler) { @@ -223,9 +214,10 @@ namespace Avalonia.FreeDesktop.DBusSystemTray return Disposable.Create(() => OnPropertyChange -= handler); } - public void Ready() + public void SetIcon(Pixmap pixmap) { - OnTitleChanged?.Invoke(); + props.IconPixmap = pixmap; + InvalidateAll(); } } @@ -251,11 +243,11 @@ namespace Avalonia.FreeDesktop.DBusSystemTray [Dictionary] class StatusNotifierWatcherProperties { - public string[] RegisteredStatusNotifierItems { get; set; } = default; + public string[] RegisteredStatusNotifierItems; - public bool IsStatusNotifierHostRegistered { get; set; } = default; + public bool IsStatusNotifierHostRegistered; - public int ProtocolVersion { get; set; } = default; + public int ProtocolVersion; } static class StatusNotifierWatcherExtensions @@ -300,7 +292,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray [Dictionary] class ApplicationProperties { - public bool Busy { get; set; } = default; + public bool Busy; } static class ApplicationExtensions @@ -330,7 +322,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray [Dictionary] class ProfilerProperties { - public IDictionary Capabilities { get; set; } = default; + public IDictionary Capabilities; } static class ProfilerExtensions @@ -362,36 +354,50 @@ namespace Avalonia.FreeDesktop.DBusSystemTray [Dictionary] class StatusNotifierItemProperties { - public string Category { get; set; } = default; + public string Category; - public string Id { get; set; } = default; + public string Id; - public string Title { get; set; } = default; + public string Title; - public string Status { get; set; } = default; + public string Status; - public int WindowId { get; set; } = default; + public int WindowId; - public string IconThemePath { get; set; } = default; + public string IconThemePath; - public ObjectPath Menu { get; set; } = default; + public ObjectPath Menu; - public bool ItemIsMenu { get; set; } = default; + public bool ItemIsMenu; - public string IconName { get; set; } = default; + public string IconName; - public (int, int, byte[])[] IconPixmap { get; set; } = default; + public Pixmap IconPixmap; - public string OverlayIconName { get; set; } = default; + public string OverlayIconName; - public (int, int, byte[])[] OverlayIconPixmap { get; set; } = default; + public Pixmap OverlayIconPixmap; - public string AttentionIconName { get; set; } = default; + public string AttentionIconName; - public (int, int, byte[])[] AttentionIconPixmap { get; set; } = default; + public Pixmap AttentionIconPixmap; - public string AttentionMovieName { get; set; } = default; + public string AttentionMovieName; - public (string, (int, int, byte[])[], string, string) ToolTip { get; set; } = default; + public (string, Pixmap, string, string) ToolTip; } + + public struct Pixmap + { + private readonly int Width; + private readonly int Height; + private readonly byte[] Data; + + public Pixmap(int width, int height, byte[] data) + { + Width = width; + Height = height; + Data = data; + } + } } diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 0f42a15cae..3eb8e8d1b6 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop.DBusSystemTray; using Avalonia.Platform; @@ -17,27 +19,74 @@ namespace Avalonia.X11 public X11TrayIconImpl(AvaloniaX11Platform avaloniaX11Platform) { _avaloniaX11Platform = avaloniaX11Platform; - } + sni.Initialize(); + } public void Dispose() { - - + sni?.Dispose(); + } + + [StructLayout(LayoutKind.Explicit)] + readonly struct BGRA32 + { + [FieldOffset(3)] public readonly byte A; + + [FieldOffset(2)] public readonly byte R; + + [FieldOffset(1)] public readonly byte G; + + [FieldOffset(0)] public readonly byte B; + } + + static unsafe class X11IconToPixmap + { } public void SetIcon(IWindowIconImpl icon) { - + if (icon is X11IconData x11icon) + { + unsafe + { + using var l = x11icon.Lock(); + + if (l.Format != PixelFormat.Bgra8888) return; + var h = l.Size.Height; + var w = l.Size.Width; + + var totalPixels = w * h; + var totalBytes = totalPixels * 4; + var _bgraBuf = new BGRA32[totalPixels]; + var byteBuf = new byte[totalBytes]; + + fixed (void* src = &x11icon.Data[0]) + fixed (void* dst = &_bgraBuf[0]) + Buffer.MemoryCopy(src, dst, (uint)totalBytes, (uint)totalBytes); + + var byteCount = 0; + + foreach (var curPix in _bgraBuf) + { + byteBuf[byteCount++] = curPix.A; + byteBuf[byteCount++] = curPix.R; + byteBuf[byteCount++] = curPix.G; + byteBuf[byteCount++] = curPix.B; + } + + sni.SetIcon(new Pixmap(w, h, byteBuf)); + } + } + + ; } public void SetIsVisible(bool visible) { - } public void SetToolTipText(string text) { - sni.Initialize(); } } } From e2dc0ead8b38ab444c2b5e240654a5a0e6f28fba Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 00:40:24 +0800 Subject: [PATCH 039/198] sigh... icons still not working here --- .../DBusSystemTray/SNIDBus.cs | 74 ++++++++++++------- src/Avalonia.X11/X11TrayIconImpl.cs | 2 - 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs index bc0350f321..ab0294e2d3 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs @@ -23,7 +23,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray private static int GetTID() { - trayinstanceID = (int)new Random().Next(0, 100); + trayinstanceID = 4; return trayinstanceID; } @@ -65,13 +65,12 @@ namespace Avalonia.FreeDesktop.DBusSystemTray // await Task.Delay(2000); // tx.InvalidateAll(); // }); - } public async void Dispose() { var con = DBusHelper.Connection; - + if (await con.UnregisterServiceAsync(_sysTraySrvName)) { con.UnregisterObject(_statusNotifierItem); @@ -107,8 +106,10 @@ namespace Avalonia.FreeDesktop.DBusSystemTray props = new StatusNotifierItemProperties { Title = "Avalonia Test Tray", - Status = "Avalonia Test Tray", - Id = "Avalonia Test Tray" + Status = "Avalonia Test Tray", + Id = "Avalonia Test Tray", + AttentionIconPixmap = new[] { new Pixmap(0, 0, new byte[] { }), new Pixmap(0, 0, new byte[] { }) }, + // IconPixmap = new[] { new Pixmap(1, 1, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }) } }; } @@ -123,21 +124,21 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public async Task SecondaryActivateAsync(int X, int Y) { - //throw new NotImplementedException(); + //throw new NotImplementedException();5 } public async Task ScrollAsync(int Delta, string Orientation) { } - - + public void InvalidateAll() { OnTitleChanged?.Invoke(); OnIconChanged?.Invoke(); OnOverlayIconChanged?.Invoke(); + OnAttentionIconChanged?.Invoke(); } - + public async Task WatchNewTitleAsync(Action handler, Action onError = null) { OnTitleChanged += handler; @@ -216,8 +217,8 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public void SetIcon(Pixmap pixmap) { - props.IconPixmap = pixmap; - InvalidateAll(); + props.IconPixmap = new[] { pixmap }; + InvalidateAll(); } } @@ -372,32 +373,49 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public string IconName; - public Pixmap IconPixmap; + public Pixmap[] IconPixmap; public string OverlayIconName; - public Pixmap OverlayIconPixmap; + public Pixmap[] OverlayIconPixmap; public string AttentionIconName; - public Pixmap AttentionIconPixmap; + public Pixmap[] AttentionIconPixmap; public string AttentionMovieName; - public (string, Pixmap, string, string) ToolTip; + public ToolTip ToolTip; } - public struct Pixmap - { - private readonly int Width; - private readonly int Height; - private readonly byte[] Data; - - public Pixmap(int width, int height, byte[] data) - { - Width = width; - Height = height; - Data = data; - } - } + public readonly struct ToolTip + { + public readonly string First; + public readonly Pixmap[] Second; + public readonly string Third; + public readonly string Fourth; + + public ToolTip(string first, Pixmap[] second, string third, string fourth) + { + First = first; + Second = second; + Third = third; + Fourth = fourth; + } + } + + + public readonly struct Pixmap + { + public readonly int Width; + public readonly int Height; + public readonly byte[] Data; + + public Pixmap(int width, int height, byte[] data) + { + Width = width; + Height = height; + Data = data; + } + } } diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 3eb8e8d1b6..6006107d21 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -77,8 +77,6 @@ namespace Avalonia.X11 sni.SetIcon(new Pixmap(w, h, byteBuf)); } } - - ; } public void SetIsVisible(bool visible) From a582f0fa103dd12dcdaab87d9aa6a10161f439eb Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 00:49:40 +0800 Subject: [PATCH 040/198] clean stuff up --- .../DBusSystemTray/SNIDBus.cs | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs index ab0294e2d3..f0476f6b52 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs @@ -29,42 +29,24 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public async void Initialize() { + var con = DBusHelper.Connection; + + _snw = con.CreateProxy("org.kde.StatusNotifierWatcher", + "/StatusNotifierWatcher"); + var x = Process.GetCurrentProcess().Id; var y = GetTID(); _sysTraySrvName = $"org.kde.StatusNotifierItem-{x}-{y}"; _statusNotifierItem = new StatusNotifierItem(); - var con = DBusHelper.Connection; await con.RegisterObjectAsync(_statusNotifierItem); await con.RegisterServiceAsync(_sysTraySrvName, () => { }); - - while (!await con.IsServiceActiveAsync(_sysTraySrvName)) - { - await Task.Delay(150); - } - - var yx = con.CreateProxy(_sysTraySrvName, _statusNotifierItem.ObjectPath); - - _snw = - DBusHelper.Connection.CreateProxy("org.kde.StatusNotifierWatcher", - "/StatusNotifierWatcher"); - - while (!await DBusHelper.Connection.IsServiceActiveAsync("org.kde.StatusNotifierWatcher")) - { - await Task.Delay(150); - } - - await _snw.RegisterStatusNotifierItemAsync(_sysTraySrvName); - // - // Task.Run(async () => - // { - // await Task.Delay(2000); - // tx.InvalidateAll(); - // }); + + await _snw.RegisterStatusNotifierItemAsync(_sysTraySrvName); } public async void Dispose() @@ -108,7 +90,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray Title = "Avalonia Test Tray", Status = "Avalonia Test Tray", Id = "Avalonia Test Tray", - AttentionIconPixmap = new[] { new Pixmap(0, 0, new byte[] { }), new Pixmap(0, 0, new byte[] { }) }, + AttentionIconPixmap = new[] { new Pixmap(0, 0, new byte[] { }), new Pixmap(0, 0, new byte[] { }) }, // IconPixmap = new[] { new Pixmap(1, 1, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }) } }; } @@ -130,7 +112,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public async Task ScrollAsync(int Delta, string Orientation) { } - + public void InvalidateAll() { OnTitleChanged?.Invoke(); @@ -217,8 +199,8 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public void SetIcon(Pixmap pixmap) { - props.IconPixmap = new[] { pixmap }; - InvalidateAll(); + props.IconPixmap = new[] { pixmap }; + InvalidateAll(); } } From efb260f3fb8adcc02873f0696b97c219f29627e8 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 08:49:07 +0800 Subject: [PATCH 041/198] test upload --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 2 ++ src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index b5e35db969..1c58ff3ecf 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -65,6 +65,8 @@ namespace Avalonia.FreeDesktop // and it's not important to know if it succeeds // since even if we register the window it's not guaranteed that // menu will be actually exported + + Dispose(); } } diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs index f0476f6b52..210f9e3159 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs @@ -93,6 +93,8 @@ namespace Avalonia.FreeDesktop.DBusSystemTray AttentionIconPixmap = new[] { new Pixmap(0, 0, new byte[] { }), new Pixmap(0, 0, new byte[] { }) }, // IconPixmap = new[] { new Pixmap(1, 1, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }) } }; + + InvalidateAll(); } public async Task ContextMenuAsync(int X, int Y) From d32714559bb5cc0b56f262159d1b7b4127ce10a7 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 12:05:16 +0800 Subject: [PATCH 042/198] more stuff --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 47 ++++++-- .../DBusSystemTray/SNIDBus.cs | 109 ++++-------------- src/Avalonia.X11/X11IconLoader.cs | 4 +- src/Avalonia.X11/X11TrayIconImpl.cs | 82 ++++++++----- src/Avalonia.X11/X11Window.cs | 2 +- 5 files changed, 121 insertions(+), 123 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 1c58ff3ecf..95aea86dfe 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -16,16 +16,30 @@ namespace Avalonia.FreeDesktop { public class DBusMenuExporter { - public static ITopLevelNativeMenuExporter TryCreate(IntPtr xid) + public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid) { + return null; + if (DBusHelper.Connection == null) return null; return new DBusMenuExporterImpl(DBusHelper.Connection, xid); } + + public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path) + { + if (DBusHelper.Connection == null) + return null; + + return new DBusMenuExporterImpl(DBusHelper.Connection, path); + } + + public static ObjectPath GenerateDBusMenuObjPath = "/net/avaloniaui/dbusmenu/" + + Guid.NewGuid().ToString().Replace("-", ""); class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable { + private readonly string _targetServiceName; private readonly Connection _dbus; private readonly uint _xid; private IRegistrar _registar; @@ -37,26 +51,41 @@ namespace Avalonia.FreeDesktop private readonly HashSet _menus = new HashSet(); private bool _resetQueued; private int _nextId = 1; + private bool AppMenu = true; public DBusMenuExporterImpl(Connection dbus, IntPtr xid) { _dbus = dbus; _xid = (uint)xid.ToInt32(); - ObjectPath = new ObjectPath("/net/avaloniaui/dbusmenu/" - + Guid.NewGuid().ToString().Replace("-", "")); + ObjectPath = GenerateDBusMenuObjPath; SetNativeMenu(new NativeMenu()); Init(); } + public DBusMenuExporterImpl(Connection dbus, ObjectPath path) + { + _dbus = dbus; + AppMenu = false; + ObjectPath = path; + SetNativeMenu(new NativeMenu()); + Init(); + } async void Init() { try { - await _dbus.RegisterObjectAsync(this); - _registar = DBusHelper.Connection.CreateProxy( - "com.canonical.AppMenu.Registrar", - "/com/canonical/AppMenu/Registrar"); - if (!_disposed) - await _registar.RegisterWindowAsync(_xid, ObjectPath); + if (AppMenu) + { + await _dbus.RegisterObjectAsync(this); + _registar = DBusHelper.Connection.CreateProxy( + "com.canonical.AppMenu.Registrar", + "/com/canonical/AppMenu/Registrar"); + if (!_disposed) + await _registar.RegisterWindowAsync(_xid, ObjectPath); + } + else + { + await _dbus.RegisterObjectAsync(this); + } } catch (Exception e) { diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs index 210f9e3159..398411a81a 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Reactive.Disposables; using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Avalonia.Controls.Platform; using Tmds.DBus; [assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] @@ -20,11 +21,11 @@ namespace Avalonia.FreeDesktop.DBusSystemTray private IStatusNotifierWatcher _snw; private string _sysTraySrvName; private StatusNotifierItem _statusNotifierItem; + public INativeMenuExporter NativeMenuExporter; private static int GetTID() { - trayinstanceID = 4; - return trayinstanceID; + return trayinstanceID = new Random().Next(0, 100); } public async void Initialize() @@ -42,11 +43,12 @@ namespace Avalonia.FreeDesktop.DBusSystemTray await con.RegisterObjectAsync(_statusNotifierItem); - await con.RegisterServiceAsync(_sysTraySrvName, () => - { - }); - - await _snw.RegisterStatusNotifierItemAsync(_sysTraySrvName); + await con.RegisterServiceAsync(_sysTraySrvName); + + await _snw.RegisterStatusNotifierItemAsync(_sysTraySrvName); + + NativeMenuExporter = _statusNotifierItem.NativeMenuExporter; + } public async void Dispose() @@ -68,7 +70,6 @@ namespace Avalonia.FreeDesktop.DBusSystemTray internal class StatusNotifierItem : IStatusNotifierItem { private event Action OnPropertyChange; - public event Action OnTitleChanged; public event Action OnIconChanged; public event Action OnAttentionIconChanged; @@ -84,19 +85,26 @@ namespace Avalonia.FreeDesktop.DBusSystemTray { var ID = Guid.NewGuid().ToString().Replace("-", ""); ObjectPath = new ObjectPath($"/StatusNotifierItem"); + var blankPixmaps = new[] { new Pixmap(0, 0, new byte[] { }), new Pixmap(0, 0, new byte[] { }) }; + + var dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; + + NativeMenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(dbusmenuPath); props = new StatusNotifierItemProperties { + Menu = "/MenuBar", // Needs a dbus menu somehow + ItemIsMenu = false, + ToolTip = new ToolTip("", blankPixmaps, "Avalonia Test Tray", ""), + Category = "", Title = "Avalonia Test Tray", Status = "Avalonia Test Tray", Id = "Avalonia Test Tray", - AttentionIconPixmap = new[] { new Pixmap(0, 0, new byte[] { }), new Pixmap(0, 0, new byte[] { }) }, - // IconPixmap = new[] { new Pixmap(1, 1, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }) } }; - - InvalidateAll(); } + public INativeMenuExporter NativeMenuExporter; + public async Task ContextMenuAsync(int X, int Y) { } @@ -207,7 +215,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray } [DBusInterface("org.kde.StatusNotifierWatcher")] - interface IStatusNotifierWatcher : IDBusObject + internal interface IStatusNotifierWatcher : IDBusObject { Task RegisterStatusNotifierItemAsync(string Service); Task RegisterStatusNotifierHostAsync(string Service); @@ -226,7 +234,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray } [Dictionary] - class StatusNotifierWatcherProperties + internal class StatusNotifierWatcherProperties { public string[] RegisteredStatusNotifierItems; @@ -235,7 +243,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public int ProtocolVersion; } - static class StatusNotifierWatcherExtensions + internal static class StatusNotifierWatcherExtensions { public static Task GetRegisteredStatusNotifierItemsAsync(this IStatusNotifierWatcher o) => o.GetAsync("RegisteredStatusNotifierItems"); @@ -247,74 +255,6 @@ namespace Avalonia.FreeDesktop.DBusSystemTray o.GetAsync("ProtocolVersion"); } - [DBusInterface("org.gtk.Actions")] - interface IActions : IDBusObject - { - Task ListAsync(); - Task<(bool description, Signature, object[])> DescribeAsync(string ActionName); - Task> DescribeAllAsync(); - Task ActivateAsync(string ActionName, object[] Parameter, IDictionary PlatformData); - Task SetStateAsync(string ActionName, object Value, IDictionary PlatformData); - - Task WatchChangedAsync( - Action<(string[] removals, IDictionary enableChanges, IDictionary stateChanges - , IDictionary additions)> handler, - Action onError = null); - } - - [DBusInterface("org.gtk.Application")] - interface IApplication : IDBusObject - { - Task ActivateAsync(IDictionary PlatformData); - Task OpenAsync(string[] Uris, string Hint, IDictionary PlatformData); - Task CommandLineAsync(ObjectPath Path, byte[][] Arguments, IDictionary PlatformData); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [Dictionary] - class ApplicationProperties - { - public bool Busy; - } - - static class ApplicationExtensions - { - public static Task GetBusyAsync(this IApplication o) => o.GetAsync("Busy"); - } - - [DBusInterface("org.freedesktop.Application")] - interface IApplication0 : IDBusObject - { - Task ActivateAsync(IDictionary PlatformData); - Task OpenAsync(string[] Uris, IDictionary PlatformData); - Task ActivateActionAsync(string ActionName, object[] Parameter, IDictionary PlatformData); - } - - [DBusInterface("org.gnome.Sysprof3.Profiler")] - interface IProfiler : IDBusObject - { - Task StartAsync(IDictionary Options, CloseSafeHandle Fd); - Task StopAsync(); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [Dictionary] - class ProfilerProperties - { - public IDictionary Capabilities; - } - - static class ProfilerExtensions - { - public static Task> GetCapabilitiesAsync(this IProfiler o) => - o.GetAsync>("Capabilities"); - } [DBusInterface("org.kde.StatusNotifierItem")] interface IStatusNotifierItem : IDBusObject @@ -337,7 +277,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray [Dictionary] - class StatusNotifierItemProperties + internal class StatusNotifierItemProperties { public string Category; @@ -388,7 +328,6 @@ namespace Avalonia.FreeDesktop.DBusSystemTray } } - public readonly struct Pixmap { public readonly int Width; diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index 093f2b12c1..27843fd354 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -48,8 +48,8 @@ namespace Avalonia.X11 public X11IconData(Bitmap bitmap) { - _width = Math.Min(bitmap.PixelSize.Width, 128); - _height = Math.Min(bitmap.PixelSize.Height, 128); + _width = Math.Min(bitmap.PixelSize.Width, 256); + _height = Math.Min(bitmap.PixelSize.Height, 256); _bdata = new uint[_width * _height]; fixed (void* ptr = _bdata) { diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 6006107d21..352ac2d833 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -11,7 +12,7 @@ namespace Avalonia.X11 class X11TrayIconImpl : ITrayIconImpl { private readonly AvaloniaX11Platform _avaloniaX11Platform; - public INativeMenuExporter MenuExporter => null; + public INativeMenuExporter MenuExporter { get; } public Action OnClicked { get; set; } private SNIDBus sni = new SNIDBus(); @@ -20,6 +21,7 @@ namespace Avalonia.X11 { _avaloniaX11Platform = avaloniaX11Platform; sni.Initialize(); + MenuExporter = sni.NativeMenuExporter; } public void Dispose() @@ -37,6 +39,35 @@ namespace Avalonia.X11 [FieldOffset(1)] public readonly byte G; [FieldOffset(0)] public readonly byte B; + + public BGRA32(byte a, byte r, byte g, byte b) + { + A = a; + R = r; + G = g; + B = b; + } + } + + + [StructLayout(LayoutKind.Explicit)] + readonly struct ARGB32 + { + [FieldOffset(0)] public readonly byte A; + + [FieldOffset(1)] public readonly byte R; + + [FieldOffset(2)] public readonly byte G; + + [FieldOffset(3)] public readonly byte B; + + public ARGB32(byte a, byte r, byte g, byte b) + { + A = a; + R = r; + G = g; + B = b; + } } static unsafe class X11IconToPixmap @@ -47,38 +78,37 @@ namespace Avalonia.X11 { if (icon is X11IconData x11icon) { - unsafe + var w = 6; + var h = 6; + var rb = 4; + var pixelBuf = new ARGB32[w * h]; + + + var gold = new ARGB32(255, 212, 175, 55); + var red = new ARGB32(255, 255, 0, 0); + var blue = new ARGB32(255, 0, 0, 255); + + var ix = 0; + for (var y = 0; y < h; y++) { - using var l = x11icon.Lock(); - - if (l.Format != PixelFormat.Bgra8888) return; - var h = l.Size.Height; - var w = l.Size.Width; - - var totalPixels = w * h; - var totalBytes = totalPixels * 4; - var _bgraBuf = new BGRA32[totalPixels]; - var byteBuf = new byte[totalBytes]; - - fixed (void* src = &x11icon.Data[0]) - fixed (void* dst = &_bgraBuf[0]) - Buffer.MemoryCopy(src, dst, (uint)totalBytes, (uint)totalBytes); - - var byteCount = 0; - - foreach (var curPix in _bgraBuf) + var offset = y * w; + for (var x = 0; x < w; x++) { - byteBuf[byteCount++] = curPix.A; - byteBuf[byteCount++] = curPix.R; - byteBuf[byteCount++] = curPix.G; - byteBuf[byteCount++] = curPix.B; + pixelBuf[offset + x] = (ix % 2 == 1) ? gold : blue; + ix++; } - - sni.SetIcon(new Pixmap(w, h, byteBuf)); + ix++; } + + var pixmapBytes = MemoryMarshal.Cast(pixelBuf.AsSpan()).ToArray(); + + sni.SetIcon(new Pixmap(w, h, pixmapBytes)); } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int PixCoord(int x, int y, int w) => x + (y *w); + public void SetIsVisible(bool visible) { } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index f0d2d5ca8a..e39be6fc04 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -190,7 +190,7 @@ namespace Avalonia.X11 if(_popup) PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); if (platform.Options.UseDBusMenu) - NativeMenuExporter = DBusMenuExporter.TryCreate(_handle); + NativeMenuExporter = DBusMenuExporter.TryCreateTopLevelNativeMenu(_handle); NativeControlHost = new X11NativeControlHost(_platform, this); DispatcherTimer.Run(() => { From 98d8b20e716535529fae9111e10aa44bbd717494 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 12:34:08 +0800 Subject: [PATCH 043/198] dont throw the underlying data in X11IconLoader.cs --- src/Avalonia.X11/X11IconLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index 27843fd354..9a202bcf9e 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -57,10 +57,12 @@ namespace Avalonia.X11 iptr[0] = _width; iptr[1] = _height; } + using(var rt = AvaloniaLocator.Current.GetService().CreateRenderTarget(new[]{this})) using (var ctx = rt.CreateDrawingContext(null)) ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); + Data = new UIntPtr[_width * _height + 2]; Data[0] = new UIntPtr((uint)_width); Data[1] = new UIntPtr((uint)_height); @@ -70,8 +72,6 @@ namespace Avalonia.X11 for (var x = 0; x < _width; x++) Data[r + x] = new UIntPtr(_bdata[r + x]); } - - _bdata = null; } public void Save(Stream outputStream) From a70997b68b679124893885489cb8cff306f99b72 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 13:03:06 +0800 Subject: [PATCH 044/198] fix conversion --- src/Avalonia.X11/X11TrayIconImpl.cs | 51 ++++++++++------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 352ac2d833..caaf31862a 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,6 +1,4 @@ using System; -using System.IO; -using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Avalonia.Controls.Platform; @@ -40,16 +38,13 @@ namespace Avalonia.X11 [FieldOffset(0)] public readonly byte B; - public BGRA32(byte a, byte r, byte g, byte b) + public ARGB32 ToARGB32() { - A = a; - R = r; - G = g; - B = b; + return new ARGB32(A, R, G, B); } } - - + + [StructLayout(LayoutKind.Explicit)] readonly struct ARGB32 { @@ -70,44 +65,32 @@ namespace Avalonia.X11 } } - static unsafe class X11IconToPixmap - { - } - public void SetIcon(IWindowIconImpl icon) { if (icon is X11IconData x11icon) { - var w = 6; - var h = 6; - var rb = 4; - var pixelBuf = new ARGB32[w * h]; - - - var gold = new ARGB32(255, 212, 175, 55); - var red = new ARGB32(255, 255, 0, 0); - var blue = new ARGB32(255, 0, 0, 255); + var w = (int)x11icon.Data[0]; + var h = (int)x11icon.Data[1]; + + using var fb = x11icon.Lock(); - var ix = 0; - for (var y = 0; y < h; y++) + var pixLength = w * h; + var pixelArray = new ARGB32[pixLength]; + + for (var i = 0; i < pixLength; i++) { - var offset = y * w; - for (var x = 0; x < w; x++) - { - pixelBuf[offset + x] = (ix % 2 == 1) ? gold : blue; - ix++; - } - ix++; + var ins = new IntPtr(fb.Address.ToInt64() + i * 4); + pixelArray[i] = Marshal.PtrToStructure(ins).ToARGB32(); } - - var pixmapBytes = MemoryMarshal.Cast(pixelBuf.AsSpan()).ToArray(); + + var pixmapBytes = MemoryMarshal.Cast(pixelArray).ToArray(); sni.SetIcon(new Pixmap(w, h, pixmapBytes)); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int PixCoord(int x, int y, int w) => x + (y *w); + private int PixCoord(int x, int y, int w) => x + (y * w); public void SetIsVisible(bool visible) { From eae2f5a6d3b4348a8b9c4ac1b6eb7073da37cfba Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 16:16:44 +0800 Subject: [PATCH 045/198] reuse Data array from X11IconLoader.cs --- src/Avalonia.X11/X11IconLoader.cs | 8 ++--- src/Avalonia.X11/X11TrayIconImpl.cs | 45 ++++++++++++++--------------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index 9a202bcf9e..093f2b12c1 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -48,8 +48,8 @@ namespace Avalonia.X11 public X11IconData(Bitmap bitmap) { - _width = Math.Min(bitmap.PixelSize.Width, 256); - _height = Math.Min(bitmap.PixelSize.Height, 256); + _width = Math.Min(bitmap.PixelSize.Width, 128); + _height = Math.Min(bitmap.PixelSize.Height, 128); _bdata = new uint[_width * _height]; fixed (void* ptr = _bdata) { @@ -57,12 +57,10 @@ namespace Avalonia.X11 iptr[0] = _width; iptr[1] = _height; } - using(var rt = AvaloniaLocator.Current.GetService().CreateRenderTarget(new[]{this})) using (var ctx = rt.CreateDrawingContext(null)) ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); - Data = new UIntPtr[_width * _height + 2]; Data[0] = new UIntPtr((uint)_width); Data[1] = new UIntPtr((uint)_height); @@ -72,6 +70,8 @@ namespace Avalonia.X11 for (var x = 0; x < _width; x++) Data[r + x] = new UIntPtr(_bdata[r + x]); } + + _bdata = null; } public void Save(Stream outputStream) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index caaf31862a..8fa5cea427 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -43,8 +43,8 @@ namespace Avalonia.X11 return new ARGB32(A, R, G, B); } } - - + + [StructLayout(LayoutKind.Explicit)] readonly struct ARGB32 { @@ -67,31 +67,30 @@ namespace Avalonia.X11 public void SetIcon(IWindowIconImpl icon) { - if (icon is X11IconData x11icon) + if (!(icon is X11IconData x11icon)) return; + + var w = (int)x11icon.Data[0]; + var h = (int)x11icon.Data[1]; + + var rx = x11icon.Data.AsSpan(2); + var pixLength = w * h; + var pixelArray = new ARGB32[pixLength]; + + for (var i = 0; i < pixLength; i++) { - var w = (int)x11icon.Data[0]; - var h = (int)x11icon.Data[1]; - - using var fb = x11icon.Lock(); - - var pixLength = w * h; - var pixelArray = new ARGB32[pixLength]; - - for (var i = 0; i < pixLength; i++) - { - var ins = new IntPtr(fb.Address.ToInt64() + i * 4); - pixelArray[i] = Marshal.PtrToStructure(ins).ToARGB32(); - } - - var pixmapBytes = MemoryMarshal.Cast(pixelArray).ToArray(); - - sni.SetIcon(new Pixmap(w, h, pixmapBytes)); + var u = rx[i].ToUInt32(); + var a = (byte)((u & 0xFF000000) >> 24); + var r = (byte)((u & 0x00FF0000) >> 16); + var g = (byte)((u & 0x0000FF00) >> 8); + var b = (byte)((u & 0x000000FF)); + pixelArray[i] = new ARGB32(a, r, g, b); } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int PixCoord(int x, int y, int w) => x + (y * w); + var pixmapBytes = MemoryMarshal.Cast(pixelArray).ToArray(); + sni.SetIcon(new Pixmap(w, h, pixmapBytes)); + } + public void SetIsVisible(bool visible) { } From 060e2b6600a450c5209402940f4a73f64f433112 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 16:33:22 +0800 Subject: [PATCH 046/198] simplifying stuff --- .../{SNIDBus.cs => DBusSysTray.cs} | 28 +++---- src/Avalonia.X11/X11Platform.cs | 2 +- src/Avalonia.X11/X11TrayIconImpl.cs | 73 +++++-------------- 3 files changed, 29 insertions(+), 74 deletions(-) rename src/Avalonia.FreeDesktop/DBusSystemTray/{SNIDBus.cs => DBusSysTray.cs} (94%) diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs similarity index 94% rename from src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs rename to src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs index 398411a81a..82091d3af7 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/SNIDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs @@ -8,24 +8,19 @@ using Avalonia.Controls.Platform; using Tmds.DBus; [assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] - namespace Avalonia.FreeDesktop.DBusSystemTray { - public class SNIDBus : IDisposable - { - public SNIDBus() - { - } - + public class DBusSysTray : IDisposable + { private static int trayinstanceID = 0; private IStatusNotifierWatcher _snw; private string _sysTraySrvName; - private StatusNotifierItem _statusNotifierItem; + private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; public INativeMenuExporter NativeMenuExporter; private static int GetTID() { - return trayinstanceID = new Random().Next(0, 100); + return trayinstanceID++; } public async void Initialize() @@ -39,16 +34,15 @@ namespace Avalonia.FreeDesktop.DBusSystemTray var y = GetTID(); _sysTraySrvName = $"org.kde.StatusNotifierItem-{x}-{y}"; - _statusNotifierItem = new StatusNotifierItem(); + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(); - await con.RegisterObjectAsync(_statusNotifierItem); + await con.RegisterObjectAsync(_statusNotifierItemDbusObj); await con.RegisterServiceAsync(_sysTraySrvName); await _snw.RegisterStatusNotifierItemAsync(_sysTraySrvName); - NativeMenuExporter = _statusNotifierItem.NativeMenuExporter; - + NativeMenuExporter = _statusNotifierItemDbusObj.NativeMenuExporter; } public async void Dispose() @@ -57,17 +51,17 @@ namespace Avalonia.FreeDesktop.DBusSystemTray if (await con.UnregisterServiceAsync(_sysTraySrvName)) { - con.UnregisterObject(_statusNotifierItem); + con.UnregisterObject(_statusNotifierItemDbusObj); } } public void SetIcon(Pixmap pixmap) { - _statusNotifierItem.SetIcon(pixmap); + _statusNotifierItemDbusObj.SetIcon(pixmap); } } - internal class StatusNotifierItem : IStatusNotifierItem + internal class StatusNotifierItemDbusObj : IStatusNotifierItem { private event Action OnPropertyChange; public event Action OnTitleChanged; @@ -81,7 +75,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray readonly StatusNotifierItemProperties props; - public StatusNotifierItem() + public StatusNotifierItemDbusObj() { var ID = Guid.NewGuid().ToString().Replace("-", ""); ObjectPath = new ObjectPath($"/StatusNotifierItem"); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index eb805c0f0b..5d80c860a7 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -103,7 +103,7 @@ namespace Avalonia.X11 public ITrayIconImpl CreateTrayIcon () { - return new X11TrayIconImpl(this); + return new X11TrayIconImpl(); } public IWindowImpl CreateWindow() diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 8fa5cea427..459bc72342 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -9,60 +9,21 @@ namespace Avalonia.X11 { class X11TrayIconImpl : ITrayIconImpl { - private readonly AvaloniaX11Platform _avaloniaX11Platform; public INativeMenuExporter MenuExporter { get; } - public Action OnClicked { get; set; } - private SNIDBus sni = new SNIDBus(); - - public X11TrayIconImpl(AvaloniaX11Platform avaloniaX11Platform) - { - _avaloniaX11Platform = avaloniaX11Platform; - sni.Initialize(); - MenuExporter = sni.NativeMenuExporter; - } - public void Dispose() - { - sni?.Dispose(); - } + private DBusSysTray dBusSysTray; - [StructLayout(LayoutKind.Explicit)] - readonly struct BGRA32 + public X11TrayIconImpl() { - [FieldOffset(3)] public readonly byte A; - - [FieldOffset(2)] public readonly byte R; - - [FieldOffset(1)] public readonly byte G; - - [FieldOffset(0)] public readonly byte B; - - public ARGB32 ToARGB32() - { - return new ARGB32(A, R, G, B); - } + dBusSysTray = new DBusSysTray(); + dBusSysTray.Initialize(); + MenuExporter = dBusSysTray.NativeMenuExporter; } - - [StructLayout(LayoutKind.Explicit)] - readonly struct ARGB32 + public void Dispose() { - [FieldOffset(0)] public readonly byte A; - - [FieldOffset(1)] public readonly byte R; - - [FieldOffset(2)] public readonly byte G; - - [FieldOffset(3)] public readonly byte B; - - public ARGB32(byte a, byte r, byte g, byte b) - { - A = a; - R = r; - G = g; - B = b; - } + dBusSysTray?.Dispose(); } public void SetIcon(IWindowIconImpl icon) @@ -74,25 +35,25 @@ namespace Avalonia.X11 var rx = x11icon.Data.AsSpan(2); var pixLength = w * h; - var pixelArray = new ARGB32[pixLength]; + + var pixByteArrayCounter = 0; + var pixByteArray = new byte[w * h * 4]; for (var i = 0; i < pixLength; i++) { var u = rx[i].ToUInt32(); - var a = (byte)((u & 0xFF000000) >> 24); - var r = (byte)((u & 0x00FF0000) >> 16); - var g = (byte)((u & 0x0000FF00) >> 8); - var b = (byte)((u & 0x000000FF)); - pixelArray[i] = new ARGB32(a, r, g, b); + pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF000000) >> 24); + pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF0000) >> 16); + pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF00) >> 8); + pixByteArray[pixByteArrayCounter++] = (byte)(u & 0xFF); } - var pixmapBytes = MemoryMarshal.Cast(pixelArray).ToArray(); - - sni.SetIcon(new Pixmap(w, h, pixmapBytes)); + dBusSysTray.SetIcon(new Pixmap(w, h, pixByteArray)); } - + public void SetIsVisible(bool visible) { + } public void SetToolTipText(string text) From f4624819d275620c0c32d89d3cf628aaee5be895 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 16:47:42 +0800 Subject: [PATCH 047/198] Make title/tooltip work --- .../DBusSystemTray/DBusSysTray.cs | 118 ++++++++++-------- src/Avalonia.X11/X11TrayIconImpl.cs | 13 +- 2 files changed, 76 insertions(+), 55 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs index 82091d3af7..596efd4a92 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs @@ -8,39 +8,38 @@ using Avalonia.Controls.Platform; using Tmds.DBus; [assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] + namespace Avalonia.FreeDesktop.DBusSystemTray { public class DBusSysTray : IDisposable - { - private static int trayinstanceID = 0; - private IStatusNotifierWatcher _snw; - private string _sysTraySrvName; + { + private static int s_trayIconInstanceId = 0; + private IStatusNotifierWatcher _statusNotifierWatcher; + private string _sysTrayServiceName; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - public INativeMenuExporter NativeMenuExporter; - private static int GetTID() - { - return trayinstanceID++; - } + public INativeMenuExporter NativeMenuExporter { get; private set; } + + private static int GetTID() => s_trayIconInstanceId++; public async void Initialize() { var con = DBusHelper.Connection; - _snw = con.CreateProxy("org.kde.StatusNotifierWatcher", + _statusNotifierWatcher = con.CreateProxy("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); - var x = Process.GetCurrentProcess().Id; - var y = GetTID(); + var pid = Process.GetCurrentProcess().Id; + var tid = GetTID(); - _sysTraySrvName = $"org.kde.StatusNotifierItem-{x}-{y}"; + _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(); await con.RegisterObjectAsync(_statusNotifierItemDbusObj); - await con.RegisterServiceAsync(_sysTraySrvName); + await con.RegisterServiceAsync(_sysTrayServiceName); - await _snw.RegisterStatusNotifierItemAsync(_sysTraySrvName); + await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); NativeMenuExporter = _statusNotifierItemDbusObj.NativeMenuExporter; } @@ -49,7 +48,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray { var con = DBusHelper.Connection; - if (await con.UnregisterServiceAsync(_sysTraySrvName)) + if (await con.UnregisterServiceAsync(_sysTrayServiceName)) { con.UnregisterObject(_statusNotifierItemDbusObj); } @@ -59,6 +58,11 @@ namespace Avalonia.FreeDesktop.DBusSystemTray { _statusNotifierItemDbusObj.SetIcon(pixmap); } + + public void SetTitleAndTooltip(string text) + { + _statusNotifierItemDbusObj.SetTitleAndTooltip(text); + } } internal class StatusNotifierItemDbusObj : IStatusNotifierItem @@ -68,32 +72,26 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public event Action OnIconChanged; public event Action OnAttentionIconChanged; public event Action OnOverlayIconChanged; - - - public Action NewToolTipAsync; + public event Action OnTooltipChanged; + public ObjectPath ObjectPath { get; } - readonly StatusNotifierItemProperties props; + readonly StatusNotifierItemProperties _backingProperties; public StatusNotifierItemDbusObj() { var ID = Guid.NewGuid().ToString().Replace("-", ""); ObjectPath = new ObjectPath($"/StatusNotifierItem"); - var blankPixmaps = new[] { new Pixmap(0, 0, new byte[] { }), new Pixmap(0, 0, new byte[] { }) }; var dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; NativeMenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(dbusmenuPath); - props = new StatusNotifierItemProperties + _backingProperties = new StatusNotifierItemProperties { Menu = "/MenuBar", // Needs a dbus menu somehow ItemIsMenu = false, - ToolTip = new ToolTip("", blankPixmaps, "Avalonia Test Tray", ""), - Category = "", - Title = "Avalonia Test Tray", - Status = "Avalonia Test Tray", - Id = "Avalonia Test Tray", + ToolTip = new ToolTip("") }; } @@ -123,6 +121,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray OnIconChanged?.Invoke(); OnOverlayIconChanged?.Invoke(); OnAttentionIconChanged?.Invoke(); + OnTooltipChanged?.Invoke(); } public async Task WatchNewTitleAsync(Action handler, Action onError = null) @@ -143,8 +142,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray OnAttentionIconChanged += handler; return Disposable.Create(() => OnAttentionIconChanged -= handler); } - - + public async Task WatchNewOverlayIconAsync(Action handler, Action onError = null) { OnOverlayIconChanged += handler; @@ -153,8 +151,8 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public async Task WatchNewToolTipAsync(Action handler, Action onError = null) { - NewToolTipAsync += handler; - return Disposable.Create(() => NewToolTipAsync -= handler); + OnTooltipChanged += handler; + return Disposable.Create(() => OnTooltipChanged -= handler); } public async Task WatchNewStatusAsync(Action handler, Action onError = null) @@ -167,28 +165,28 @@ namespace Avalonia.FreeDesktop.DBusSystemTray { return prop switch { - "Category" => props.Category, - "Id" => props.Id, - "Title" => props.Title, - "Status" => props.Status, - "WindowId" => props.WindowId, - "IconThemePath" => props.IconThemePath, - "ItemIsMenu" => props.ItemIsMenu, - "IconName" => props.IconName, - "IconPixmap" => props.IconPixmap, - "OverlayIconName" => props.OverlayIconName, - "OverlayIconPixmap" => props.OverlayIconPixmap, - "AttentionIconName" => props.AttentionIconName, - "AttentionIconPixmap" => props.AttentionIconPixmap, - "AttentionMovieName" => props.AttentionMovieName, - "ToolTip" => props.ToolTip, + "Category" => _backingProperties.Category, + "Id" => _backingProperties.Id, + "Title" => _backingProperties.Title, + "Status" => _backingProperties.Status, + "WindowId" => _backingProperties.WindowId, + "IconThemePath" => _backingProperties.IconThemePath, + "ItemIsMenu" => _backingProperties.ItemIsMenu, + "IconName" => _backingProperties.IconName, + "IconPixmap" => _backingProperties.IconPixmap, + "OverlayIconName" => _backingProperties.OverlayIconName, + "OverlayIconPixmap" => _backingProperties.OverlayIconPixmap, + "AttentionIconName" => _backingProperties.AttentionIconName, + "AttentionIconPixmap" => _backingProperties.AttentionIconPixmap, + "AttentionMovieName" => _backingProperties.AttentionMovieName, + "ToolTip" => _backingProperties.ToolTip, _ => default }; } public async Task GetAllAsync() { - return props; + return _backingProperties; } public Action NewStatusAsync { get; set; } @@ -203,7 +201,18 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public void SetIcon(Pixmap pixmap) { - props.IconPixmap = new[] { pixmap }; + _backingProperties.IconPixmap = new[] { pixmap }; + InvalidateAll(); + } + + public void SetTitleAndTooltip(string text) + { + _backingProperties.Id = text; + _backingProperties.Category = "ApplicationStatus"; + _backingProperties.Status = text; + _backingProperties.Title = text; + _backingProperties.ToolTip = new ToolTip(text); + InvalidateAll(); } } @@ -306,13 +315,24 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public ToolTip ToolTip; } - public readonly struct ToolTip + public struct ToolTip { public readonly string First; public readonly Pixmap[] Second; public readonly string Third; public readonly string Fourth; + private static readonly Pixmap[] s_blankPixmaps = + { + new Pixmap(0, 0, new byte[] { }), + new Pixmap(0, 0, new byte[] { }) + }; + + public ToolTip(string message) : this("", s_blankPixmaps, message, "") + { + + } + public ToolTip(string first, Pixmap[] second, string third, string fourth) { First = first; diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 459bc72342..069b501245 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -12,18 +12,18 @@ namespace Avalonia.X11 public INativeMenuExporter MenuExporter { get; } public Action OnClicked { get; set; } - private DBusSysTray dBusSysTray; + private readonly DBusSysTray _dBusSysTray; public X11TrayIconImpl() { - dBusSysTray = new DBusSysTray(); - dBusSysTray.Initialize(); - MenuExporter = dBusSysTray.NativeMenuExporter; + _dBusSysTray = new DBusSysTray(); + _dBusSysTray.Initialize(); + MenuExporter = _dBusSysTray.NativeMenuExporter; } public void Dispose() { - dBusSysTray?.Dispose(); + _dBusSysTray?.Dispose(); } public void SetIcon(IWindowIconImpl icon) @@ -48,7 +48,7 @@ namespace Avalonia.X11 pixByteArray[pixByteArrayCounter++] = (byte)(u & 0xFF); } - dBusSysTray.SetIcon(new Pixmap(w, h, pixByteArray)); + _dBusSysTray.SetIcon(new Pixmap(w, h, pixByteArray)); } public void SetIsVisible(bool visible) @@ -58,6 +58,7 @@ namespace Avalonia.X11 public void SetToolTipText(string text) { + _dBusSysTray.SetTitleAndTooltip(text); } } } From 0ae373f597fe37f56ccb6c8857fa901cb905dd58 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 16:48:30 +0800 Subject: [PATCH 048/198] remove unnecessary code --- .../DBusSystemTray/DBusSysTray.cs | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs index 596efd4a92..83ae9686d1 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs @@ -163,25 +163,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public async Task GetAsync(string prop) { - return prop switch - { - "Category" => _backingProperties.Category, - "Id" => _backingProperties.Id, - "Title" => _backingProperties.Title, - "Status" => _backingProperties.Status, - "WindowId" => _backingProperties.WindowId, - "IconThemePath" => _backingProperties.IconThemePath, - "ItemIsMenu" => _backingProperties.ItemIsMenu, - "IconName" => _backingProperties.IconName, - "IconPixmap" => _backingProperties.IconPixmap, - "OverlayIconName" => _backingProperties.OverlayIconName, - "OverlayIconPixmap" => _backingProperties.OverlayIconPixmap, - "AttentionIconName" => _backingProperties.AttentionIconName, - "AttentionIconPixmap" => _backingProperties.AttentionIconPixmap, - "AttentionMovieName" => _backingProperties.AttentionMovieName, - "ToolTip" => _backingProperties.ToolTip, - _ => default - }; + return default; } public async Task GetAllAsync() From f3a9a3b73b693092bdc974736a2260722a038e03 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 17:04:41 +0800 Subject: [PATCH 049/198] 80% done --- .../DBusSystemTray/DBusSysTray.cs | 33 +++++++++++-------- src/Avalonia.X11/X11TrayIconImpl.cs | 19 ++++++++++- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs index 83ae9686d1..8fd324e84c 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs @@ -17,6 +17,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray private IStatusNotifierWatcher _statusNotifierWatcher; private string _sysTrayServiceName; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; + private Action _activateDelegate; public INativeMenuExporter NativeMenuExporter { get; private set; } @@ -63,21 +64,33 @@ namespace Avalonia.FreeDesktop.DBusSystemTray { _statusNotifierItemDbusObj.SetTitleAndTooltip(text); } - } + public void SetActivationDelegate(Action activationDelegate) + { + _statusNotifierItemDbusObj.ActivationDelegate = activationDelegate; + } + } + + /// + /// DBus Object used for setting system tray icons. + /// + /// + /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html + /// internal class StatusNotifierItemDbusObj : IStatusNotifierItem { + private readonly StatusNotifierItemProperties _backingProperties; private event Action OnPropertyChange; public event Action OnTitleChanged; public event Action OnIconChanged; public event Action OnAttentionIconChanged; public event Action OnOverlayIconChanged; public event Action OnTooltipChanged; - + public INativeMenuExporter NativeMenuExporter { get; set; } + public Action NewStatusAsync { get; set; } + public Action ActivationDelegate { get; set; } public ObjectPath ObjectPath { get; } - - readonly StatusNotifierItemProperties _backingProperties; - + public StatusNotifierItemDbusObj() { var ID = Guid.NewGuid().ToString().Replace("-", ""); @@ -95,20 +108,17 @@ namespace Avalonia.FreeDesktop.DBusSystemTray }; } - public INativeMenuExporter NativeMenuExporter; - public async Task ContextMenuAsync(int X, int Y) { } public async Task ActivateAsync(int X, int Y) { - // OnPropertyChange?.Invoke(new PropertyChanges()); + ActivationDelegate?.Invoke(); } public async Task SecondaryActivateAsync(int X, int Y) { - //throw new NotImplementedException();5 } public async Task ScrollAsync(int Delta, string Orientation) @@ -129,8 +139,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray OnTitleChanged += handler; return Disposable.Create(() => OnTitleChanged -= handler); } - - + public async Task WatchNewIconAsync(Action handler, Action onError = null) { OnIconChanged += handler; @@ -171,8 +180,6 @@ namespace Avalonia.FreeDesktop.DBusSystemTray return _backingProperties; } - public Action NewStatusAsync { get; set; } - public Task SetAsync(string prop, object val) => Task.CompletedTask; public async Task WatchPropertiesAsync(Action handler) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 069b501245..0971ee51ee 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -14,6 +14,9 @@ namespace Avalonia.X11 private readonly DBusSysTray _dBusSysTray; + private X11IconData lastIcon; + + public X11TrayIconImpl() { _dBusSysTray = new DBusSysTray(); @@ -30,6 +33,8 @@ namespace Avalonia.X11 { if (!(icon is X11IconData x11icon)) return; + lastIcon = x11icon; + var w = (int)x11icon.Data[0]; var h = (int)x11icon.Data[1]; @@ -49,11 +54,23 @@ namespace Avalonia.X11 } _dBusSysTray.SetIcon(new Pixmap(w, h, pixByteArray)); + + _dBusSysTray.SetActivationDelegate(() => + { + OnClicked?.Invoke(); + }); } public void SetIsVisible(bool visible) { - + if (visible && lastIcon != null) + { + SetIcon(lastIcon); + } + else + { + _dBusSysTray.SetIcon(new Pixmap(1, 1, new byte[] { 0, 0, 0, 0 })); + } } public void SetToolTipText(string text) From f6a4bd90eb69e9125502878b8ec33aa875b3552c Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 17:41:01 +0800 Subject: [PATCH 050/198] some fixes --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 4 - .../DBusSystemTray/DBusSysTray.cs | 89 ++++++------------- src/Avalonia.X11/X11TrayIconImpl.cs | 16 ++-- 3 files changed, 36 insertions(+), 73 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 95aea86dfe..52450560c0 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -18,8 +18,6 @@ namespace Avalonia.FreeDesktop { public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid) { - return null; - if (DBusHelper.Connection == null) return null; @@ -94,8 +92,6 @@ namespace Avalonia.FreeDesktop // and it's not important to know if it succeeds // since even if we register the window it's not guaranteed that // menu will be actually exported - - Dispose(); } } diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs index 8fd324e84c..dc5b98a41f 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs @@ -55,9 +55,9 @@ namespace Avalonia.FreeDesktop.DBusSystemTray } } - public void SetIcon(Pixmap pixmap) + public void SetIcon(DbusPixmap dbusPixmap) { - _statusNotifierItemDbusObj.SetIcon(pixmap); + _statusNotifierItemDbusObj.SetIcon(dbusPixmap); } public void SetTitleAndTooltip(string text) @@ -70,7 +70,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray _statusNotifierItemDbusObj.ActivationDelegate = activationDelegate; } } - + /// /// DBus Object used for setting system tray icons. /// @@ -90,7 +90,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public Action NewStatusAsync { get; set; } public Action ActivationDelegate { get; set; } public ObjectPath ObjectPath { get; } - + public StatusNotifierItemDbusObj() { var ID = Guid.NewGuid().ToString().Replace("-", ""); @@ -102,10 +102,11 @@ namespace Avalonia.FreeDesktop.DBusSystemTray _backingProperties = new StatusNotifierItemProperties { - Menu = "/MenuBar", // Needs a dbus menu somehow - ItemIsMenu = false, + Menu = dbusmenuPath, // Needs a dbus menu somehow ToolTip = new ToolTip("") }; + + InvalidateAll(); } public async Task ContextMenuAsync(int X, int Y) @@ -139,7 +140,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray OnTitleChanged += handler; return Disposable.Create(() => OnTitleChanged -= handler); } - + public async Task WatchNewIconAsync(Action handler, Action onError = null) { OnIconChanged += handler; @@ -151,7 +152,7 @@ namespace Avalonia.FreeDesktop.DBusSystemTray OnAttentionIconChanged += handler; return Disposable.Create(() => OnAttentionIconChanged -= handler); } - + public async Task WatchNewOverlayIconAsync(Action handler, Action onError = null) { OnOverlayIconChanged += handler; @@ -172,6 +173,10 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public async Task GetAsync(string prop) { + if (prop.Contains("Menu")) + { + return _backingProperties.Menu; + } return default; } @@ -188,9 +193,9 @@ namespace Avalonia.FreeDesktop.DBusSystemTray return Disposable.Create(() => OnPropertyChange -= handler); } - public void SetIcon(Pixmap pixmap) + public void SetIcon(DbusPixmap dbusPixmap) { - _backingProperties.IconPixmap = new[] { pixmap }; + _backingProperties.IconPixmap = new[] { dbusPixmap }; InvalidateAll(); } @@ -211,43 +216,8 @@ namespace Avalonia.FreeDesktop.DBusSystemTray { Task RegisterStatusNotifierItemAsync(string Service); Task RegisterStatusNotifierHostAsync(string Service); - - Task WatchStatusNotifierItemRegisteredAsync(Action handler, - Action onError = null); - - Task WatchStatusNotifierItemUnregisteredAsync(Action handler, - Action onError = null); - - Task WatchStatusNotifierHostRegisteredAsync(Action handler, Action onError = null); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); } - [Dictionary] - internal class StatusNotifierWatcherProperties - { - public string[] RegisteredStatusNotifierItems; - - public bool IsStatusNotifierHostRegistered; - - public int ProtocolVersion; - } - - internal static class StatusNotifierWatcherExtensions - { - public static Task GetRegisteredStatusNotifierItemsAsync(this IStatusNotifierWatcher o) => - o.GetAsync("RegisteredStatusNotifierItems"); - - public static Task GetIsStatusNotifierHostRegisteredAsync(this IStatusNotifierWatcher o) => - o.GetAsync("IsStatusNotifierHostRegistered"); - - public static Task GetProtocolVersionAsync(this IStatusNotifierWatcher o) => - o.GetAsync("ProtocolVersion"); - } - - [DBusInterface("org.kde.StatusNotifierItem")] interface IStatusNotifierItem : IDBusObject { @@ -267,7 +237,6 @@ namespace Avalonia.FreeDesktop.DBusSystemTray Task WatchPropertiesAsync(Action handler); } - [Dictionary] internal class StatusNotifierItemProperties { @@ -289,40 +258,38 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public string IconName; - public Pixmap[] IconPixmap; + public DbusPixmap[] IconPixmap; public string OverlayIconName; - public Pixmap[] OverlayIconPixmap; + public DbusPixmap[] OverlayIconPixmap; public string AttentionIconName; - public Pixmap[] AttentionIconPixmap; + public DbusPixmap[] AttentionIconPixmap; public string AttentionMovieName; public ToolTip ToolTip; } - public struct ToolTip + internal struct ToolTip { public readonly string First; - public readonly Pixmap[] Second; + public readonly DbusPixmap[] Second; public readonly string Third; public readonly string Fourth; - private static readonly Pixmap[] s_blankPixmaps = + private static readonly DbusPixmap[] s_blank = { - new Pixmap(0, 0, new byte[] { }), - new Pixmap(0, 0, new byte[] { }) + new DbusPixmap(0, 0, new byte[] { }), new DbusPixmap(0, 0, new byte[] { }) }; - - public ToolTip(string message) : this("", s_blankPixmaps, message, "") + + public ToolTip(string message) : this("", s_blank, message, "") { - } - - public ToolTip(string first, Pixmap[] second, string third, string fourth) + + public ToolTip(string first, DbusPixmap[] second, string third, string fourth) { First = first; Second = second; @@ -331,13 +298,13 @@ namespace Avalonia.FreeDesktop.DBusSystemTray } } - public readonly struct Pixmap + public readonly struct DbusPixmap { public readonly int Width; public readonly int Height; public readonly byte[] Data; - public Pixmap(int width, int height, byte[] data) + public DbusPixmap(int width, int height, byte[] data) { Width = width; Height = height; diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 0971ee51ee..74aae49606 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -7,16 +7,16 @@ using Avalonia.Platform; namespace Avalonia.X11 { - class X11TrayIconImpl : ITrayIconImpl + internal class X11TrayIconImpl : ITrayIconImpl { - public INativeMenuExporter MenuExporter { get; } - public Action OnClicked { get; set; } - + private readonly DBusSysTray _dBusSysTray; - private X11IconData lastIcon; + + public INativeMenuExporter MenuExporter { get; } + public Action OnClicked { get; set; } - + public X11TrayIconImpl() { _dBusSysTray = new DBusSysTray(); @@ -53,7 +53,7 @@ namespace Avalonia.X11 pixByteArray[pixByteArrayCounter++] = (byte)(u & 0xFF); } - _dBusSysTray.SetIcon(new Pixmap(w, h, pixByteArray)); + _dBusSysTray.SetIcon(new DbusPixmap(w, h, pixByteArray)); _dBusSysTray.SetActivationDelegate(() => { @@ -69,7 +69,7 @@ namespace Avalonia.X11 } else { - _dBusSysTray.SetIcon(new Pixmap(1, 1, new byte[] { 0, 0, 0, 0 })); + _dBusSysTray.SetIcon(new DbusPixmap(1, 1, new byte[] { 0, 0, 0, 0 })); } } From f811655ae718f3d4c74972e6ff02c13337be4cc5 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 17:41:23 +0800 Subject: [PATCH 051/198] some fixes --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 52450560c0..c08e4da02c 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -37,7 +37,6 @@ namespace Avalonia.FreeDesktop class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable { - private readonly string _targetServiceName; private readonly Connection _dbus; private readonly uint _xid; private IRegistrar _registar; From 557e35eac019485bb060eee21b3a21ac4a0da9f9 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 19:52:58 +0800 Subject: [PATCH 052/198] generate instead of set on guid --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index c08e4da02c..52bb7c7ad9 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -32,10 +32,10 @@ namespace Avalonia.FreeDesktop return new DBusMenuExporterImpl(DBusHelper.Connection, path); } - public static ObjectPath GenerateDBusMenuObjPath = "/net/avaloniaui/dbusmenu/" + public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" + Guid.NewGuid().ToString().Replace("-", ""); - class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable + private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable { private readonly Connection _dbus; private readonly uint _xid; From 602dcff35dc686f9094d5baeb2f6046e48947487 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 20:03:20 +0800 Subject: [PATCH 053/198] make native menu work --- .../DBusSystemTray/DBusSysTray.cs | 28 +++++++------------ src/Avalonia.X11/X11TrayIconImpl.cs | 11 ++++++-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs index dc5b98a41f..7c0b0f6e55 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs @@ -17,13 +17,10 @@ namespace Avalonia.FreeDesktop.DBusSystemTray private IStatusNotifierWatcher _statusNotifierWatcher; private string _sysTrayServiceName; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - private Action _activateDelegate; - - public INativeMenuExporter NativeMenuExporter { get; private set; } - + private static int GetTID() => s_trayIconInstanceId++; - public async void Initialize() + public async void Initialize(ObjectPath dbusmenuPath) { var con = DBusHelper.Connection; @@ -34,16 +31,14 @@ namespace Avalonia.FreeDesktop.DBusSystemTray var tid = GetTID(); _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; - _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(); + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(dbusmenuPath); await con.RegisterObjectAsync(_statusNotifierItemDbusObj); await con.RegisterServiceAsync(_sysTrayServiceName); await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); - - NativeMenuExporter = _statusNotifierItemDbusObj.NativeMenuExporter; - } + } public async void Dispose() { @@ -86,26 +81,22 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public event Action OnAttentionIconChanged; public event Action OnOverlayIconChanged; public event Action OnTooltipChanged; - public INativeMenuExporter NativeMenuExporter { get; set; } + public Action SetNativeMenuExporter { get; set; } public Action NewStatusAsync { get; set; } public Action ActivationDelegate { get; set; } public ObjectPath ObjectPath { get; } - public StatusNotifierItemDbusObj() + public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) { var ID = Guid.NewGuid().ToString().Replace("-", ""); ObjectPath = new ObjectPath($"/StatusNotifierItem"); - - var dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; - - NativeMenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(dbusmenuPath); - + _backingProperties = new StatusNotifierItemProperties { Menu = dbusmenuPath, // Needs a dbus menu somehow ToolTip = new ToolTip("") }; - + InvalidateAll(); } @@ -175,8 +166,9 @@ namespace Avalonia.FreeDesktop.DBusSystemTray { if (prop.Contains("Menu")) { - return _backingProperties.Menu; + return _backingProperties.Menu; } + return default; } diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 74aae49606..9dcce74e90 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Avalonia.Controls.Platform; +using Avalonia.FreeDesktop; using Avalonia.FreeDesktop.DBusSystemTray; using Avalonia.Platform; @@ -20,8 +21,14 @@ namespace Avalonia.X11 public X11TrayIconImpl() { _dBusSysTray = new DBusSysTray(); - _dBusSysTray.Initialize(); - MenuExporter = _dBusSysTray.NativeMenuExporter; + + + var dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(dbusmenuPath); + + + _dBusSysTray.Initialize(dbusmenuPath); + } public void Dispose() From 691c4b78fee34e2465582f3fee19040800aa283d Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sat, 18 Sep 2021 20:46:46 +0800 Subject: [PATCH 054/198] test change --- src/Avalonia.X11/X11TrayIconImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 9dcce74e90..dc81a85b7a 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -9,7 +9,7 @@ using Avalonia.Platform; namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl - { + { private readonly DBusSysTray _dBusSysTray; private X11IconData lastIcon; From 069a8d4ba57af72e89e42ee55a0563c7c7208054 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 21 Sep 2021 23:28:46 +0800 Subject: [PATCH 055/198] fix dbus menu icon loading courtesy of @danwalmsley --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 52bb7c7ad9..b4b3b23942 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -8,6 +8,7 @@ using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop.DBusMenu; using Avalonia.Input; +using Avalonia.Platform; using Avalonia.Threading; using Tmds.DBus; #pragma warning disable 1998 @@ -66,6 +67,7 @@ namespace Avalonia.FreeDesktop SetNativeMenu(new NativeMenu()); Init(); } + async void Init() { try @@ -274,17 +276,24 @@ namespace Avalonia.FreeDesktop if (item.ToggleType != NativeMenuItemToggleType.None) return item.IsChecked ? 1 : 0; } - + if (name == "icon-data") { if (item.Icon != null) { - var ms = new MemoryStream(); - item.Icon.Save(ms); - return ms.ToArray(); + var loader = AvaloniaLocator.Current.GetService(); + + if (loader != null) + { + var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item); + + using var ms = new MemoryStream(); + icon.Save(ms); + return ms.ToArray(); + } } } - + if (name == "children-display") return menu != null ? "submenu" : null; } From ce7333413bbfb927a57e64c2d2990ba564b094e3 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Thu, 23 Sep 2021 10:14:07 +0800 Subject: [PATCH 056/198] Make a separate DBus connections so that things doesnt mix up in one session and cause ObjectPath conflicts. --- src/Avalonia.FreeDesktop/DBusHelper.cs | 7 ++- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 7 +-- .../DBusSystemTray/DBusSysTray.cs | 13 +++-- src/Avalonia.X11/X11TrayIconImpl.cs | 47 +++++++++---------- 4 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index 7996a94dd0..4e23711ed4 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -51,8 +51,11 @@ namespace Avalonia.FreeDesktop public static Connection TryInitialize(string dbusAddress = null) { - if (Connection != null) - return Connection; + return Connection ?? TryGetConnection(dbusAddress); + } + + public static Connection TryGetConnection(string dbusAddress = null) + { var oldContext = SynchronizationContext.Current; try { diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index b4b3b23942..dbfa04c1f0 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -25,12 +25,9 @@ namespace Avalonia.FreeDesktop return new DBusMenuExporterImpl(DBusHelper.Connection, xid); } - public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path) + public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConection) { - if (DBusHelper.Connection == null) - return null; - - return new DBusMenuExporterImpl(DBusHelper.Connection, path); + return new DBusMenuExporterImpl(currentConection, path); } public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs index 7c0b0f6e55..ce3e7da726 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs @@ -14,16 +14,22 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public class DBusSysTray : IDisposable { private static int s_trayIconInstanceId = 0; + private IStatusNotifierWatcher _statusNotifierWatcher; private string _sysTrayServiceName; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - + + private Connection con; + private static int GetTID() => s_trayIconInstanceId++; + public DBusSysTray(Connection connection) + { + con = connection; + } + public async void Initialize(ObjectPath dbusmenuPath) { - var con = DBusHelper.Connection; - _statusNotifierWatcher = con.CreateProxy("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); @@ -88,7 +94,6 @@ namespace Avalonia.FreeDesktop.DBusSystemTray public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) { - var ID = Guid.NewGuid().ToString().Replace("-", ""); ObjectPath = new ObjectPath($"/StatusNotifierItem"); _backingProperties = new StatusNotifierItemProperties diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index dc81a85b7a..3d6a3cc20b 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -5,30 +5,33 @@ using Avalonia.Controls.Platform; using Avalonia.FreeDesktop; using Avalonia.FreeDesktop.DBusSystemTray; using Avalonia.Platform; +using Tmds.DBus; namespace Avalonia.X11 { - internal class X11TrayIconImpl : ITrayIconImpl - { - - private readonly DBusSysTray _dBusSysTray; - private X11IconData lastIcon; - + internal class X11TrayIconImpl : ITrayIconImpl + { + private DBusSysTray _dBusSysTray; + private readonly ObjectPath _dbusmenuPath; + public INativeMenuExporter MenuExporter { get; } public Action OnClicked { get; set; } - + public X11TrayIconImpl() { - _dBusSysTray = new DBusSysTray(); - + var con = DBusHelper.TryGetConnection(); - var dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; - MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(dbusmenuPath); + _dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusmenuPath, con); - - _dBusSysTray.Initialize(dbusmenuPath); - + _dBusSysTray = new DBusSysTray(con); + _dBusSysTray.Initialize(_dbusmenuPath); + + _dBusSysTray.SetActivationDelegate(() => + { + OnClicked?.Invoke(); + }); } public void Dispose() @@ -39,9 +42,7 @@ namespace Avalonia.X11 public void SetIcon(IWindowIconImpl icon) { if (!(icon is X11IconData x11icon)) return; - - lastIcon = x11icon; - + var w = (int)x11icon.Data[0]; var h = (int)x11icon.Data[1]; @@ -61,22 +62,18 @@ namespace Avalonia.X11 } _dBusSysTray.SetIcon(new DbusPixmap(w, h, pixByteArray)); - - _dBusSysTray.SetActivationDelegate(() => - { - OnClicked?.Invoke(); - }); } public void SetIsVisible(bool visible) { - if (visible && lastIcon != null) + if (visible) { - SetIcon(lastIcon); + // _dBusSysTray = new DBusSysTray(); + // _dBusSysTray.Initialize(_dbusmenuPath); } else { - _dBusSysTray.SetIcon(new DbusPixmap(1, 1, new byte[] { 0, 0, 0, 0 })); + // _dBusSysTray?.Dispose(); } } From a06198d4c52a694e04b161ec1afe49937b268cc9 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Thu, 23 Sep 2021 11:06:52 +0800 Subject: [PATCH 057/198] make visibility work somewhat --- .../DBusSystemTray/DBusSysTray.cs | 311 ----------------- src/Avalonia.X11/X11TrayIconImpl.cs | 329 +++++++++++++++++- 2 files changed, 312 insertions(+), 328 deletions(-) delete mode 100644 src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs diff --git a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs b/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs deleted file mode 100644 index ce3e7da726..0000000000 --- a/src/Avalonia.FreeDesktop/DBusSystemTray/DBusSysTray.cs +++ /dev/null @@ -1,311 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Reactive.Disposables; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Avalonia.Controls.Platform; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] - -namespace Avalonia.FreeDesktop.DBusSystemTray -{ - public class DBusSysTray : IDisposable - { - private static int s_trayIconInstanceId = 0; - - private IStatusNotifierWatcher _statusNotifierWatcher; - private string _sysTrayServiceName; - private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - - private Connection con; - - private static int GetTID() => s_trayIconInstanceId++; - - public DBusSysTray(Connection connection) - { - con = connection; - } - - public async void Initialize(ObjectPath dbusmenuPath) - { - _statusNotifierWatcher = con.CreateProxy("org.kde.StatusNotifierWatcher", - "/StatusNotifierWatcher"); - - var pid = Process.GetCurrentProcess().Id; - var tid = GetTID(); - - _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; - _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(dbusmenuPath); - - await con.RegisterObjectAsync(_statusNotifierItemDbusObj); - - await con.RegisterServiceAsync(_sysTrayServiceName); - - await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); - } - - public async void Dispose() - { - var con = DBusHelper.Connection; - - if (await con.UnregisterServiceAsync(_sysTrayServiceName)) - { - con.UnregisterObject(_statusNotifierItemDbusObj); - } - } - - public void SetIcon(DbusPixmap dbusPixmap) - { - _statusNotifierItemDbusObj.SetIcon(dbusPixmap); - } - - public void SetTitleAndTooltip(string text) - { - _statusNotifierItemDbusObj.SetTitleAndTooltip(text); - } - - public void SetActivationDelegate(Action activationDelegate) - { - _statusNotifierItemDbusObj.ActivationDelegate = activationDelegate; - } - } - - /// - /// DBus Object used for setting system tray icons. - /// - /// - /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html - /// - internal class StatusNotifierItemDbusObj : IStatusNotifierItem - { - private readonly StatusNotifierItemProperties _backingProperties; - private event Action OnPropertyChange; - public event Action OnTitleChanged; - public event Action OnIconChanged; - public event Action OnAttentionIconChanged; - public event Action OnOverlayIconChanged; - public event Action OnTooltipChanged; - public Action SetNativeMenuExporter { get; set; } - public Action NewStatusAsync { get; set; } - public Action ActivationDelegate { get; set; } - public ObjectPath ObjectPath { get; } - - public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) - { - ObjectPath = new ObjectPath($"/StatusNotifierItem"); - - _backingProperties = new StatusNotifierItemProperties - { - Menu = dbusmenuPath, // Needs a dbus menu somehow - ToolTip = new ToolTip("") - }; - - InvalidateAll(); - } - - public async Task ContextMenuAsync(int X, int Y) - { - } - - public async Task ActivateAsync(int X, int Y) - { - ActivationDelegate?.Invoke(); - } - - public async Task SecondaryActivateAsync(int X, int Y) - { - } - - public async Task ScrollAsync(int Delta, string Orientation) - { - } - - public void InvalidateAll() - { - OnTitleChanged?.Invoke(); - OnIconChanged?.Invoke(); - OnOverlayIconChanged?.Invoke(); - OnAttentionIconChanged?.Invoke(); - OnTooltipChanged?.Invoke(); - } - - public async Task WatchNewTitleAsync(Action handler, Action onError = null) - { - OnTitleChanged += handler; - return Disposable.Create(() => OnTitleChanged -= handler); - } - - public async Task WatchNewIconAsync(Action handler, Action onError = null) - { - OnIconChanged += handler; - return Disposable.Create(() => OnIconChanged -= handler); - } - - public async Task WatchNewAttentionIconAsync(Action handler, Action onError = null) - { - OnAttentionIconChanged += handler; - return Disposable.Create(() => OnAttentionIconChanged -= handler); - } - - public async Task WatchNewOverlayIconAsync(Action handler, Action onError = null) - { - OnOverlayIconChanged += handler; - return Disposable.Create(() => OnOverlayIconChanged -= handler); - } - - public async Task WatchNewToolTipAsync(Action handler, Action onError = null) - { - OnTooltipChanged += handler; - return Disposable.Create(() => OnTooltipChanged -= handler); - } - - public async Task WatchNewStatusAsync(Action handler, Action onError = null) - { - NewStatusAsync += handler; - return Disposable.Create(() => NewStatusAsync -= handler); - } - - public async Task GetAsync(string prop) - { - if (prop.Contains("Menu")) - { - return _backingProperties.Menu; - } - - return default; - } - - public async Task GetAllAsync() - { - return _backingProperties; - } - - public Task SetAsync(string prop, object val) => Task.CompletedTask; - - public async Task WatchPropertiesAsync(Action handler) - { - OnPropertyChange += handler; - return Disposable.Create(() => OnPropertyChange -= handler); - } - - public void SetIcon(DbusPixmap dbusPixmap) - { - _backingProperties.IconPixmap = new[] { dbusPixmap }; - InvalidateAll(); - } - - public void SetTitleAndTooltip(string text) - { - _backingProperties.Id = text; - _backingProperties.Category = "ApplicationStatus"; - _backingProperties.Status = text; - _backingProperties.Title = text; - _backingProperties.ToolTip = new ToolTip(text); - - InvalidateAll(); - } - } - - [DBusInterface("org.kde.StatusNotifierWatcher")] - internal interface IStatusNotifierWatcher : IDBusObject - { - Task RegisterStatusNotifierItemAsync(string Service); - Task RegisterStatusNotifierHostAsync(string Service); - } - - [DBusInterface("org.kde.StatusNotifierItem")] - interface IStatusNotifierItem : IDBusObject - { - Task ContextMenuAsync(int X, int Y); - Task ActivateAsync(int X, int Y); - Task SecondaryActivateAsync(int X, int Y); - Task ScrollAsync(int Delta, string Orientation); - Task WatchNewTitleAsync(Action handler, Action onError = null); - Task WatchNewIconAsync(Action handler, Action onError = null); - Task WatchNewAttentionIconAsync(Action handler, Action onError = null); - Task WatchNewOverlayIconAsync(Action handler, Action onError = null); - Task WatchNewToolTipAsync(Action handler, Action onError = null); - Task WatchNewStatusAsync(Action handler, Action onError = null); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [Dictionary] - internal class StatusNotifierItemProperties - { - public string Category; - - public string Id; - - public string Title; - - public string Status; - - public int WindowId; - - public string IconThemePath; - - public ObjectPath Menu; - - public bool ItemIsMenu; - - public string IconName; - - public DbusPixmap[] IconPixmap; - - public string OverlayIconName; - - public DbusPixmap[] OverlayIconPixmap; - - public string AttentionIconName; - - public DbusPixmap[] AttentionIconPixmap; - - public string AttentionMovieName; - - public ToolTip ToolTip; - } - - internal struct ToolTip - { - public readonly string First; - public readonly DbusPixmap[] Second; - public readonly string Third; - public readonly string Fourth; - - private static readonly DbusPixmap[] s_blank = - { - new DbusPixmap(0, 0, new byte[] { }), new DbusPixmap(0, 0, new byte[] { }) - }; - - public ToolTip(string message) : this("", s_blank, message, "") - { - } - - public ToolTip(string first, DbusPixmap[] second, string third, string fourth) - { - First = first; - Second = second; - Third = third; - Fourth = fourth; - } - } - - public readonly struct DbusPixmap - { - public readonly int Width; - public readonly int Height; - public readonly byte[] Data; - - public DbusPixmap(int width, int height, byte[] data) - { - Width = width; - Height = height; - Data = data; - } - } -} diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 3d6a3cc20b..682e431c9a 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,48 +1,99 @@ using System; +using System.Diagnostics; +using System.Reactive.Disposables; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop; -using Avalonia.FreeDesktop.DBusSystemTray; using Avalonia.Platform; using Tmds.DBus; +[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] + namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private DBusSysTray _dBusSysTray; private readonly ObjectPath _dbusmenuPath; + private static int s_trayIconInstanceId = 0; + + private IStatusNotifierWatcher _statusNotifierWatcher; + private string _sysTrayServiceName; + private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; + + private Connection con; + private DbusPixmap _icon; + private string _tooltipText; + private bool _isActive; + private bool _isDisposed; + private readonly bool _ctorFinished; + + private static int GetTID() => s_trayIconInstanceId++; + public INativeMenuExporter MenuExporter { get; } public Action OnClicked { get; set; } public X11TrayIconImpl() { - var con = DBusHelper.TryGetConnection(); - + con = DBusHelper.TryGetConnection(); _dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusmenuPath, con); + CreateTrayIcon(); + _ctorFinished = true; + } + - _dBusSysTray = new DBusSysTray(con); - _dBusSysTray.Initialize(_dbusmenuPath); + public async void CreateTrayIcon() + { + _statusNotifierWatcher = con.CreateProxy("org.kde.StatusNotifierWatcher", + "/StatusNotifierWatcher"); + + var pid = Process.GetCurrentProcess().Id; + var tid = GetTID(); + + _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusmenuPath); + + await con.RegisterObjectAsync(_statusNotifierItemDbusObj); - _dBusSysTray.SetActivationDelegate(() => + await con.RegisterServiceAsync(_sysTrayServiceName); + + await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + + _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); + _statusNotifierItemDbusObj.SetIcon(_icon); + + _statusNotifierItemDbusObj.ActivationDelegate = () => { OnClicked?.Invoke(); - }); + }; + + _isActive = true; } + public async void DestroyTrayIcon() + { + con.UnregisterObject(_statusNotifierItemDbusObj); + await con.UnregisterServiceAsync(_sysTrayServiceName); + _isActive = false; + } + + public void Dispose() { - _dBusSysTray?.Dispose(); + _isDisposed = true; + DestroyTrayIcon(); + con.Dispose(); } public void SetIcon(IWindowIconImpl icon) { + if (con == null || _isDisposed) return; if (!(icon is X11IconData x11icon)) return; - + var w = (int)x11icon.Data[0]; var h = (int)x11icon.Data[1]; @@ -61,25 +112,269 @@ namespace Avalonia.X11 pixByteArray[pixByteArrayCounter++] = (byte)(u & 0xFF); } - _dBusSysTray.SetIcon(new DbusPixmap(w, h, pixByteArray)); + _icon = new DbusPixmap(w, h, pixByteArray); + _statusNotifierItemDbusObj.SetIcon(_icon); } + public void SetIsVisible(bool visible) { - if (visible) + if (con == null || _isDisposed || !_ctorFinished) return; + + if (visible & !_isActive) { - // _dBusSysTray = new DBusSysTray(); - // _dBusSysTray.Initialize(_dbusmenuPath); + DestroyTrayIcon(); + CreateTrayIcon(); } - else + else if (!visible & _isActive) { - // _dBusSysTray?.Dispose(); + DestroyTrayIcon(); } } public void SetToolTipText(string text) { - _dBusSysTray.SetTitleAndTooltip(text); + if (con == null || _isDisposed) return; + _tooltipText = text; + _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); + } + } + + + /// + /// DBus Object used for setting system tray icons. + /// + /// + /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html + /// + internal class StatusNotifierItemDbusObj : IStatusNotifierItem + { + private readonly StatusNotifierItemProperties _backingProperties; + private event Action OnPropertyChange; + public event Action OnTitleChanged; + public event Action OnIconChanged; + public event Action OnAttentionIconChanged; + public event Action OnOverlayIconChanged; + public event Action OnTooltipChanged; + public Action SetNativeMenuExporter { get; set; } + public Action NewStatusAsync { get; set; } + public Action ActivationDelegate { get; set; } + public ObjectPath ObjectPath { get; } + + public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) + { + ObjectPath = new ObjectPath($"/StatusNotifierItem"); + + _backingProperties = new StatusNotifierItemProperties + { + Menu = dbusmenuPath, // Needs a dbus menu somehow + ToolTip = new ToolTip("") + }; + + InvalidateAll(); + } + + public async Task ContextMenuAsync(int X, int Y) + { + } + + public async Task ActivateAsync(int X, int Y) + { + ActivationDelegate?.Invoke(); + } + + public async Task SecondaryActivateAsync(int X, int Y) + { + } + + public async Task ScrollAsync(int Delta, string Orientation) + { + } + + public void InvalidateAll() + { + OnTitleChanged?.Invoke(); + OnIconChanged?.Invoke(); + OnOverlayIconChanged?.Invoke(); + OnAttentionIconChanged?.Invoke(); + OnTooltipChanged?.Invoke(); + } + + public async Task WatchNewTitleAsync(Action handler, Action onError = null) + { + OnTitleChanged += handler; + return Disposable.Create(() => OnTitleChanged -= handler); + } + + public async Task WatchNewIconAsync(Action handler, Action onError = null) + { + OnIconChanged += handler; + return Disposable.Create(() => OnIconChanged -= handler); + } + + public async Task WatchNewAttentionIconAsync(Action handler, Action onError = null) + { + OnAttentionIconChanged += handler; + return Disposable.Create(() => OnAttentionIconChanged -= handler); + } + + public async Task WatchNewOverlayIconAsync(Action handler, Action onError = null) + { + OnOverlayIconChanged += handler; + return Disposable.Create(() => OnOverlayIconChanged -= handler); + } + + public async Task WatchNewToolTipAsync(Action handler, Action onError = null) + { + OnTooltipChanged += handler; + return Disposable.Create(() => OnTooltipChanged -= handler); + } + + public async Task WatchNewStatusAsync(Action handler, Action onError = null) + { + NewStatusAsync += handler; + return Disposable.Create(() => NewStatusAsync -= handler); + } + + public async Task GetAsync(string prop) + { + if (prop.Contains("Menu")) + { + return _backingProperties.Menu; + } + + return default; + } + + public async Task GetAllAsync() + { + return _backingProperties; + } + + public Task SetAsync(string prop, object val) => Task.CompletedTask; + + public async Task WatchPropertiesAsync(Action handler) + { + OnPropertyChange += handler; + return Disposable.Create(() => OnPropertyChange -= handler); + } + + public void SetIcon(DbusPixmap dbusPixmap) + { + _backingProperties.IconPixmap = new[] { dbusPixmap }; + InvalidateAll(); + } + + public void SetTitleAndTooltip(string text) + { + _backingProperties.Id = text; + _backingProperties.Category = "ApplicationStatus"; + _backingProperties.Status = text; + _backingProperties.Title = text; + _backingProperties.ToolTip = new ToolTip(text); + + InvalidateAll(); + } + } + + [DBusInterface("org.kde.StatusNotifierWatcher")] + internal interface IStatusNotifierWatcher : IDBusObject + { + Task RegisterStatusNotifierItemAsync(string Service); + Task RegisterStatusNotifierHostAsync(string Service); + } + + [DBusInterface("org.kde.StatusNotifierItem")] + interface IStatusNotifierItem : IDBusObject + { + Task ContextMenuAsync(int X, int Y); + Task ActivateAsync(int X, int Y); + Task SecondaryActivateAsync(int X, int Y); + Task ScrollAsync(int Delta, string Orientation); + Task WatchNewTitleAsync(Action handler, Action onError = null); + Task WatchNewIconAsync(Action handler, Action onError = null); + Task WatchNewAttentionIconAsync(Action handler, Action onError = null); + Task WatchNewOverlayIconAsync(Action handler, Action onError = null); + Task WatchNewToolTipAsync(Action handler, Action onError = null); + Task WatchNewStatusAsync(Action handler, Action onError = null); + Task GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action handler); + } + + [Dictionary] + internal class StatusNotifierItemProperties + { + public string Category; + + public string Id; + + public string Title; + + public string Status; + + public int WindowId; + + public string IconThemePath; + + public ObjectPath Menu; + + public bool ItemIsMenu; + + public string IconName; + + public DbusPixmap[] IconPixmap; + + public string OverlayIconName; + + public DbusPixmap[] OverlayIconPixmap; + + public string AttentionIconName; + + public DbusPixmap[] AttentionIconPixmap; + + public string AttentionMovieName; + + public ToolTip ToolTip; + } + + internal struct ToolTip + { + public readonly string First; + public readonly DbusPixmap[] Second; + public readonly string Third; + public readonly string Fourth; + + private static readonly DbusPixmap[] s_blank = + { + new DbusPixmap(0, 0, new byte[] { }), new DbusPixmap(0, 0, new byte[] { }) + }; + + public ToolTip(string message) : this("", s_blank, message, "") + { + } + + public ToolTip(string first, DbusPixmap[] second, string third, string fourth) + { + First = first; + Second = second; + Third = third; + Fourth = fourth; + } + } + + public readonly struct DbusPixmap + { + public readonly int Width; + public readonly int Height; + public readonly byte[] Data; + + public DbusPixmap(int width, int height, byte[] data) + { + Width = width; + Height = height; + Data = data; } } } From a7f3fbe988614ccd71ed7f636825be21e67d684c Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Thu, 23 Sep 2021 11:08:00 +0800 Subject: [PATCH 058/198] more refactoring --- src/Avalonia.X11/X11TrayIconImpl.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 682e431c9a..1d8db8f929 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -120,15 +120,15 @@ namespace Avalonia.X11 public void SetIsVisible(bool visible) { if (con == null || _isDisposed || !_ctorFinished) return; - + if (visible & !_isActive) { - DestroyTrayIcon(); - CreateTrayIcon(); + DestroyTrayIcon(); + CreateTrayIcon(); } else if (!visible & _isActive) { - DestroyTrayIcon(); + DestroyTrayIcon(); } } @@ -139,8 +139,7 @@ namespace Avalonia.X11 _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } } - - + /// /// DBus Object used for setting system tray icons. /// @@ -285,7 +284,7 @@ namespace Avalonia.X11 } [DBusInterface("org.kde.StatusNotifierItem")] - interface IStatusNotifierItem : IDBusObject + internal interface IStatusNotifierItem : IDBusObject { Task ContextMenuAsync(int X, int Y); Task ActivateAsync(int X, int Y); @@ -364,7 +363,7 @@ namespace Avalonia.X11 } } - public readonly struct DbusPixmap + internal readonly struct DbusPixmap { public readonly int Width; public readonly int Height; From 19003c0f2e9e1ca27883e543e872b9f98bcef7c2 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Mon, 27 Sep 2021 22:22:56 +0800 Subject: [PATCH 059/198] Use single connection + new guid per SNI object... --- src/Avalonia.X11/X11TrayIconImpl.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 1d8db8f929..49ac1a0757 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -38,7 +38,7 @@ namespace Avalonia.X11 public X11TrayIconImpl() { - con = DBusHelper.TryGetConnection(); + con = DBusHelper.TryInitialize(); _dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusmenuPath, con); CreateTrayIcon(); @@ -113,7 +113,7 @@ namespace Avalonia.X11 } _icon = new DbusPixmap(w, h, pixByteArray); - _statusNotifierItemDbusObj.SetIcon(_icon); + _statusNotifierItemDbusObj?.SetIcon(_icon); } @@ -139,7 +139,7 @@ namespace Avalonia.X11 _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } } - + /// /// DBus Object used for setting system tray icons. /// @@ -162,7 +162,8 @@ namespace Avalonia.X11 public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) { - ObjectPath = new ObjectPath($"/StatusNotifierItem"); + var guid = Guid.NewGuid().ToString().Replace("-", ""); + ObjectPath = new ObjectPath($"/net/avaloniaui/sni/{guid}"); _backingProperties = new StatusNotifierItemProperties { From fa6a12c5ef371253331b9009737aff25ab5b4e5e Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Mon, 27 Sep 2021 22:26:36 +0800 Subject: [PATCH 060/198] finally silence that good ol appmenu error in linux --- src/Avalonia.Base/Logging/LogArea.cs | 5 +++++ src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 2ad220dddd..08ed8669ef 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -39,5 +39,10 @@ namespace Avalonia.Logging /// The log event comes from Win32Platform. /// public const string Win32Platform = nameof(Win32Platform); + + /// + /// The log event comes from Win32Platform. + /// + public const string X11Platform = nameof(X11Platform); } } diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index dbfa04c1f0..932b876088 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -85,7 +85,9 @@ namespace Avalonia.FreeDesktop } catch (Exception e) { - Console.Error.WriteLine(e); + Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.X11Platform) + ?.Log(this, e.Message); + // It's not really important if this code succeeds, // and it's not important to know if it succeeds // since even if we register the window it's not guaranteed that From 891fd8dec4208bf856386a47b1ad5cf01c4cf089 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Sep 2021 08:44:59 +0200 Subject: [PATCH 061/198] Try to load a custom font with all possible weights and styles --- .../Avalonia.Skia/SKTypefaceCollection.cs | 7 +++--- .../Media/SKTypefaceCollectionCacheTests.cs | 22 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 7c4ff4edc0..21b2959089 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -27,15 +27,16 @@ namespace Avalonia.Skia { return typeface; } + + var initialWeight = (int)key.Weight; var weight = (int)key.Weight; - weight -= weight % 100; // make sure we start at a full weight + weight -= weight % 50; // make sure we start at a full weight for (var i = 0; i < 2; i++) { - // only try 2 font weights in each direction - for (var j = 0; j < 200; j += 100) + for (var j = 0; j < initialWeight; j += 50) { if (weight - j >= 100) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs index 68813f28ab..ddf4a36dcd 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs @@ -6,18 +6,24 @@ namespace Avalonia.Skia.UnitTests.Media { public class SKTypefaceCollectionCacheTests { - [Fact] - public void Should_Get_Near_Matching_Typeface() + private const string s_notoMono = + "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"; + + [InlineData(s_notoMono, FontWeight.SemiLight, FontStyle.Normal)] + [InlineData(s_notoMono, FontWeight.Bold, FontStyle.Italic)] + [InlineData(s_notoMono, FontWeight.Heavy, FontStyle.Oblique)] + [Theory] + public void Should_Get_Near_Matching_Typeface(string familyName, FontWeight fontWeight, FontStyle fontStyle) { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { - var notoMono = - new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); - - var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono); + var fontFamily = new FontFamily(familyName); + + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily); - Assert.Equal("Noto Mono", - notoMonoCollection.Get(new Typeface(notoMono, weight: FontWeight.Bold)).FamilyName); + var actual = typefaceCollection.Get(new Typeface(fontFamily, fontStyle, fontWeight))?.FamilyName; + + Assert.Equal("Noto Mono", actual); } } From 1a97c6ce39f19cf30512bd1cce706ac0e1b2455c Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 29 Sep 2021 20:19:10 +0800 Subject: [PATCH 062/198] Revert "Use single connection + new guid per SNI object..." This reverts commit 19003c0f --- src/Avalonia.X11/X11TrayIconImpl.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 49ac1a0757..1d8db8f929 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -38,7 +38,7 @@ namespace Avalonia.X11 public X11TrayIconImpl() { - con = DBusHelper.TryInitialize(); + con = DBusHelper.TryGetConnection(); _dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusmenuPath, con); CreateTrayIcon(); @@ -113,7 +113,7 @@ namespace Avalonia.X11 } _icon = new DbusPixmap(w, h, pixByteArray); - _statusNotifierItemDbusObj?.SetIcon(_icon); + _statusNotifierItemDbusObj.SetIcon(_icon); } @@ -139,7 +139,7 @@ namespace Avalonia.X11 _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } } - + /// /// DBus Object used for setting system tray icons. /// @@ -162,8 +162,7 @@ namespace Avalonia.X11 public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) { - var guid = Guid.NewGuid().ToString().Replace("-", ""); - ObjectPath = new ObjectPath($"/net/avaloniaui/sni/{guid}"); + ObjectPath = new ObjectPath($"/StatusNotifierItem"); _backingProperties = new StatusNotifierItemProperties { From f570e3528d84191c9a81b60104a07e3d90b4da9c Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 29 Sep 2021 20:30:26 +0800 Subject: [PATCH 063/198] Clean up a little bit --- src/Avalonia.X11/X11TrayIconImpl.cs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 1d8db8f929..99a850dabb 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -15,27 +15,25 @@ namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private readonly ObjectPath _dbusmenuPath; - + private static int s_trayIconInstanceId = 0; - - private IStatusNotifierWatcher _statusNotifierWatcher; - private string _sysTrayServiceName; + private static int GetTID() => s_trayIconInstanceId++; + private ObjectPath _dbusmenuPath; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - private Connection con; private DbusPixmap _icon; + + private IStatusNotifierWatcher _statusNotifierWatcher; + + private string _sysTrayServiceName; private string _tooltipText; private bool _isActive; private bool _isDisposed; private readonly bool _ctorFinished; - - private static int GetTID() => s_trayIconInstanceId++; - + public INativeMenuExporter MenuExporter { get; } public Action OnClicked { get; set; } - - + public X11TrayIconImpl() { con = DBusHelper.TryGetConnection(); @@ -44,8 +42,7 @@ namespace Avalonia.X11 CreateTrayIcon(); _ctorFinished = true; } - - + public async void CreateTrayIcon() { _statusNotifierWatcher = con.CreateProxy("org.kde.StatusNotifierWatcher", @@ -81,7 +78,6 @@ namespace Avalonia.X11 _isActive = false; } - public void Dispose() { _isDisposed = true; @@ -115,8 +111,7 @@ namespace Avalonia.X11 _icon = new DbusPixmap(w, h, pixByteArray); _statusNotifierItemDbusObj.SetIcon(_icon); } - - + public void SetIsVisible(bool visible) { if (con == null || _isDisposed || !_ctorFinished) return; From c0bda39b60388be3416defb1e2bd31bfe1fc23ed Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Sep 2021 16:35:55 +0200 Subject: [PATCH 064/198] Improve scroll gesture with virtualization. Try to improve scrolling with gestures on virtualized lists. Instead of using a fixed item size, try to calculate an item size by dividing the control bounds by the logical viewport size. Fixes #3429 --- .../Presenters/ScrollContentPresenter.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index b0b52812b9..a62ba306ab 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -60,9 +60,6 @@ namespace Avalonia.Controls.Presenters o => o.Viewport, (o, v) => o.Viewport = v); - // Arbitrary chosen value, probably need to ask ILogicalScrollable - private const int LogicalScrollItemSize = 50; - private bool _canHorizontallyScroll; private bool _canVerticallyScroll; private bool _arranging; @@ -351,7 +348,8 @@ namespace Avalonia.Controls.Presenters if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width) { var scrollable = Child as ILogicalScrollable; - bool isLogical = scrollable?.IsLogicalScrollEnabled == true; + var isLogical = scrollable?.IsLogicalScrollEnabled == true; + var logicalScrollItemSize = new Vector(1, 1); double x = Offset.X; double y = Offset.Y; @@ -361,13 +359,18 @@ namespace Avalonia.Controls.Presenters _activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta); delta += e.Delta; + if (isLogical && scrollable is object) + { + logicalScrollItemSize = Bounds.Size / scrollable.Viewport; + } + if (Extent.Height > Viewport.Height) { double dy; if (isLogical) { - var logicalUnits = delta.Y / LogicalScrollItemSize; - delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize); + var logicalUnits = delta.Y / logicalScrollItemSize.Y; + delta = delta.WithY(delta.Y - logicalUnits * logicalScrollItemSize.Y); dy = logicalUnits * scrollable!.ScrollSize.Height; } else @@ -384,8 +387,8 @@ namespace Avalonia.Controls.Presenters double dx; if (isLogical) { - var logicalUnits = delta.X / LogicalScrollItemSize; - delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize); + var logicalUnits = delta.X / logicalScrollItemSize.X; + delta = delta.WithX(delta.X - logicalUnits * logicalScrollItemSize.X); dx = logicalUnits * scrollable!.ScrollSize.Width; } else From 4077e69ddaa79e4081af9911d4746c21c4821299 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 1 Oct 2021 06:21:45 +0200 Subject: [PATCH 065/198] Prevent the renderer from crashing when a VisualBrush is used as a OpacityMask --- src/Avalonia.Visuals/Rendering/DeferredRenderer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 6c84cfd55c..fe63fdec46 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -279,13 +279,13 @@ namespace Avalonia.Rendering /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; + return (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; } /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { - var childScene = (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]; + var childScene = (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]; if (childScene != null) { From 5ede6f9ae23a8f5bd963b5510c973525b0573820 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 2 Oct 2021 01:23:17 +0300 Subject: [PATCH 066/198] Added support for non-control templates in XAML --- src/Avalonia.Base/Metadata/TemplateContent.cs | 1 + .../Templates/IControlTemplate.cs | 9 ++- .../Templates/TemplateResult.cs | 20 ++++++ .../AvaloniaXamlIlLanguage.cs | 6 +- .../Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- .../Templates/TemplateContent.cs | 12 ++++ .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 12 +++- .../Xaml/GenericTemplateTests.cs | 62 +++++++++++++++++++ 8 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 src/Avalonia.Controls/Templates/TemplateResult.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs diff --git a/src/Avalonia.Base/Metadata/TemplateContent.cs b/src/Avalonia.Base/Metadata/TemplateContent.cs index fcd7d69e7b..7f9e878419 100644 --- a/src/Avalonia.Base/Metadata/TemplateContent.cs +++ b/src/Avalonia.Base/Metadata/TemplateContent.cs @@ -8,5 +8,6 @@ namespace Avalonia.Metadata [AttributeUsage(AttributeTargets.Property)] public class TemplateContentAttribute : Attribute { + public Type TemplateResultType { get; set; } } } diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs index 7414f438a1..ab46884402 100644 --- a/src/Avalonia.Controls/Templates/IControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/IControlTemplate.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Controls.Primitives; using Avalonia.Styling; @@ -10,18 +11,16 @@ namespace Avalonia.Controls.Templates { } - public class ControlTemplateResult + public class ControlTemplateResult : TemplateResult { public IControl Control { get; } - public INameScope NameScope { get; } - public ControlTemplateResult(IControl control, INameScope nameScope) + public ControlTemplateResult(IControl control, INameScope nameScope) : base(control, nameScope) { Control = control; - NameScope = nameScope; } - public void Deconstruct(out IControl control, out INameScope scope) + public new void Deconstruct(out IControl control, out INameScope scope) { control = Control; scope = NameScope; diff --git a/src/Avalonia.Controls/Templates/TemplateResult.cs b/src/Avalonia.Controls/Templates/TemplateResult.cs new file mode 100644 index 0000000000..770aecc329 --- /dev/null +++ b/src/Avalonia.Controls/Templates/TemplateResult.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Controls.Templates +{ + public class TemplateResult + { + public T Result { get; } + public INameScope NameScope { get; } + + public TemplateResult(T result, INameScope nameScope) + { + Result = result; + NameScope = nameScope; + } + + public void Deconstruct(out T result, out INameScope scope) + { + result = Result; + scope = NameScope; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index a82f5b9e60..e9bf20a626 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -49,8 +49,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XmlNamespaceInfoProvider = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlXmlNamespaceInfoProvider"), DeferredContentPropertyAttributes = {typeSystem.GetType("Avalonia.Metadata.TemplateContentAttribute")}, + DeferredContentExecutorCustomizationTypeParameterDeferredContentAttributePropertyNames = new List + { + "TemplateResultType" + }, DeferredContentExecutorCustomization = - runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV1"), + runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV2"), UsableDuringInitializationAttributes = { typeSystem.GetType("Avalonia.Metadata.UsableDuringInitializationAttribute"), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index f4ac681b91..22766f0922 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit f4ac681b91a9dc7a7a095d1050a683de23d86b72 +Subproject commit 22766f092201ea634356b5f1ef2193b0f0d6695c diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 483a1a5d06..07c79d7077 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -7,6 +7,7 @@ namespace Avalonia.Markup.Xaml.Templates public static class TemplateContent { public static ControlTemplateResult Load(object templateContent) + { if (templateContent is Func direct) { @@ -20,5 +21,16 @@ namespace Avalonia.Markup.Xaml.Templates throw new ArgumentException(nameof(templateContent)); } + + public static TemplateResult Load(object templateContent) + { + if (templateContent is Func direct) + return (TemplateResult)direct(null); + + if (templateContent is null) + return null; + + throw new ArgumentException(nameof(templateContent)); + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index 83d70122b3..c48f386ffd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -15,6 +15,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { public static Func DeferredTransformationFactoryV1(Func builder, IServiceProvider provider) + { + return DeferredTransformationFactoryV2(builder, provider); + } + + public static Func DeferredTransformationFactoryV2(Func builder, + IServiceProvider provider) { var resourceNodes = provider.GetService().Parents .OfType().ToList(); @@ -25,7 +31,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope(); var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope)); scope.Complete(); - return new ControlTemplateResult((IControl)obj, scope); + + if(typeof(T) == typeof(IControl)) + return new ControlTemplateResult((IControl)obj, scope); + + return new TemplateResult((T)obj, scope); }; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs new file mode 100644 index 0000000000..9fee5285aa --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Metadata; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class SampleTemplatedObject : StyledElement + { + [Content] public List Content { get; set; } = new List(); + public string Foo { get; set; } + } + + public class SampleTemplatedObjectTemplate + { + [Content] + [TemplateContent(TemplateResultType = typeof(SampleTemplatedObject))] + public object Content { get; set; } + } + + public class SampleTemplatedObjectContainer + { + public SampleTemplatedObjectTemplate Template { get; set; } + } + + public class GenericTemplateTests + { + [Fact] + public void DataTemplate_Can_Be_Empty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var container = + (SampleTemplatedObjectContainer)AvaloniaRuntimeXamlLoader.Load(xaml, + typeof(GenericTemplateTests).Assembly); + var res = TemplateContent.Load(container.Template.Content); + Assert.Equal(res.Result, res.NameScope.Find("root")); + Assert.Equal(res.Result.Content[0], res.NameScope.Find("child1")); + Assert.Equal(res.Result.Content[1], res.NameScope.Find("child2")); + Assert.Equal("foo", res.Result.Content[0].Foo); + Assert.Equal("bar", res.Result.Content[1].Foo); + } + } + } +} From dca9dec664d94a7e3207cf7832ca517120b56274 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 2 Oct 2021 12:41:16 +0300 Subject: [PATCH 067/198] Fixed the previous usage scenario --- .../CompilerExtensions/AvaloniaXamlIlLanguage.cs | 1 + src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index e9bf20a626..1db0208310 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -49,6 +49,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XmlNamespaceInfoProvider = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlXmlNamespaceInfoProvider"), DeferredContentPropertyAttributes = {typeSystem.GetType("Avalonia.Metadata.TemplateContentAttribute")}, + DeferredContentExecutorCustomizationDefaultTypeParameter = typeSystem.GetType("Avalonia.Controls.IControl"), DeferredContentExecutorCustomizationTypeParameterDeferredContentAttributePropertyNames = new List { "TemplateResultType" diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index 22766f0922..8e20d65eb5 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit 22766f092201ea634356b5f1ef2193b0f0d6695c +Subproject commit 8e20d65eb5f1efbae08e49b18f39bfdce32df7b3 From 55361882c569b98b28a83f7b54cb53dc51d076ca Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 4 Oct 2021 10:42:41 +0300 Subject: [PATCH 068/198] Use XC_hand2 for hand cursor --- src/Avalonia.X11/X11CursorFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index f95d4320fe..d677ababef 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -25,7 +25,7 @@ namespace Avalonia.X11 { {StandardCursorType.Arrow, CursorFontShape.XC_top_left_arrow}, {StandardCursorType.Cross, CursorFontShape.XC_cross}, - {StandardCursorType.Hand, CursorFontShape.XC_hand1}, + {StandardCursorType.Hand, CursorFontShape.XC_hand2}, {StandardCursorType.Help, CursorFontShape.XC_question_arrow}, {StandardCursorType.Ibeam, CursorFontShape.XC_xterm}, {StandardCursorType.No, CursorFontShape.XC_X_cursor}, From 69693d47ca0d71f2a953a777940cd31954b8d7ef Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Oct 2021 14:22:05 +0100 Subject: [PATCH 069/198] fix breaking change. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 1 - src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index dd41c30e85..83ab978ad5 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -37,7 +37,6 @@ MembersMustExist : Member 'public System.Action Avalonia.Controls MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs index 5e5f7b18ec..a8e5eb68d1 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs @@ -12,6 +12,8 @@ namespace Avalonia.Controls.Platform public interface ITopLevelNativeMenuExporter : INativeMenuExporter { + new void SetNativeMenu(NativeMenu menu); + bool IsNativeMenuExported { get; } event EventHandler OnIsNativeMenuExportedChanged; From f86bb8cbde64b687ed23e6290ef9058657a59172 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Oct 2021 14:22:19 +0100 Subject: [PATCH 070/198] update documentation. --- src/Avalonia.Controls/TrayIcon.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index bd346c1e5d..73a0856359 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -5,7 +5,6 @@ using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Platform; -using Avalonia.Threading; #nullable enable @@ -58,8 +57,8 @@ namespace Avalonia.Controls /// /// Raised when the TrayIcon is clicked. - /// Note, this is only supported on Win32. - /// Linux and OSX this event is not raised. + /// Note, this is only supported on Win32 and some Linux DEs, + /// on OSX this event is not raised. /// public event EventHandler? Clicked; From a93d29991cae189329f5d95a21ff3c588502ad04 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Oct 2021 14:37:28 +0100 Subject: [PATCH 071/198] acceptable interface change. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 1 + src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 83ab978ad5..dd41c30e85 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -37,6 +37,7 @@ MembersMustExist : Member 'public System.Action Avalonia.Controls MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs index a8e5eb68d1..5e5f7b18ec 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs @@ -12,8 +12,6 @@ namespace Avalonia.Controls.Platform public interface ITopLevelNativeMenuExporter : INativeMenuExporter { - new void SetNativeMenu(NativeMenu menu); - bool IsNativeMenuExported { get; } event EventHandler OnIsNativeMenuExportedChanged; From d6dd8238ecc54c7b9a31cf7bb450871e418ba227 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Oct 2021 14:37:49 +0100 Subject: [PATCH 072/198] rename property. --- samples/ControlCatalog/App.xaml | 4 ++-- samples/ControlCatalog/App.xaml.cs | 1 - src/Avalonia.Controls/TrayIcon.cs | 12 ++++++------ 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 3f8b768f6b..845413a455 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -27,7 +27,7 @@ - + @@ -46,5 +46,5 @@ - + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index e044987a27..36b6fc2dcd 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -106,7 +106,6 @@ namespace ControlCatalog if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) { desktopLifetime.MainWindow = new MainWindow(); - desktopLifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 73a0856359..7b400d3600 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -33,7 +33,7 @@ namespace Avalonia.Controls static TrayIcon () { - TrayIconsProperty.Changed.Subscribe(args => + IconsProperty.Changed.Subscribe(args => { if (args.Sender is Application application) { @@ -65,8 +65,8 @@ namespace Avalonia.Controls /// /// Defines the attached property. /// - public static readonly AttachedProperty TrayIconsProperty - = AvaloniaProperty.RegisterAttached("TrayIcons"); + public static readonly AttachedProperty IconsProperty + = AvaloniaProperty.RegisterAttached("Icons"); /// /// Defines the property. @@ -86,9 +86,9 @@ namespace Avalonia.Controls public static readonly StyledProperty IsVisibleProperty = Visual.IsVisibleProperty.AddOwner(); - public static void SetTrayIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(TrayIconsProperty, trayIcons); + public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons); - public static TrayIcons GetTrayIcons(AvaloniaObject o) => o.GetValue(TrayIconsProperty); + public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty); /// /// Gets or sets the icon of the TrayIcon. @@ -121,7 +121,7 @@ namespace Avalonia.Controls private static void Lifetime_Exit(object sender, ControlledApplicationLifetimeExitEventArgs e) { - var trayIcons = GetTrayIcons(Application.Current); + var trayIcons = GetIcons(Application.Current); RemoveIcons(trayIcons); } From 40fef4976cbc7e5396c8a441d8ce50f874e73625 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Oct 2021 16:19:22 +0100 Subject: [PATCH 073/198] fix comment. --- src/Avalonia.Base/Logging/LogArea.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 08ed8669ef..c049f9e763 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -41,7 +41,7 @@ namespace Avalonia.Logging public const string Win32Platform = nameof(Win32Platform); /// - /// The log event comes from Win32Platform. + /// The log event comes from X11Platform. /// public const string X11Platform = nameof(X11Platform); } From 34b96f45f30bc35c43eebee3796feba4513501cc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Oct 2021 17:55:30 +0100 Subject: [PATCH 074/198] make the trayicon menu property an explicit member of trayicon. --- samples/ControlCatalog/App.xaml | 4 ++-- .../Platform/ITopLevelNativeMenuExporter.cs | 2 +- src/Avalonia.Controls/TrayIcon.cs | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 845413a455..6e57686e00 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -30,7 +30,7 @@ - + @@ -43,7 +43,7 @@ - + diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs index 5e5f7b18ec..9b779054f3 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs @@ -7,7 +7,7 @@ namespace Avalonia.Controls.Platform { public interface INativeMenuExporter { - void SetNativeMenu(NativeMenu menu); + void SetNativeMenu(NativeMenu? menu); } public interface ITopLevelNativeMenuExporter : INativeMenuExporter diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 7b400d3600..5b10fa20ea 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -68,6 +68,12 @@ namespace Avalonia.Controls public static readonly AttachedProperty IconsProperty = AvaloniaProperty.RegisterAttached("Icons"); + /// + /// Defines the property. + /// + public static readonly StyledProperty MenuProperty + = AvaloniaProperty.Register(nameof(Menu)); + /// /// Defines the property. /// @@ -90,6 +96,15 @@ namespace Avalonia.Controls public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty); + /// + /// Gets or sets the Menu of the TrayIcon. + /// + public NativeMenu? Menu + { + get => GetValue(MenuProperty); + set => SetValue(MenuProperty, value); + } + /// /// Gets or sets the icon of the TrayIcon. /// @@ -155,6 +170,10 @@ namespace Avalonia.Controls { _impl.SetToolTipText(change.NewValue.GetValueOrDefault()); } + else if (change.Property == MenuProperty) + { + _impl.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault()); + } } /// From 786375aff9016f8c5bb95265a5f2edf4097e33f8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Oct 2021 18:08:48 +0100 Subject: [PATCH 075/198] handle platforms that return null for the trayiconimpl. --- .../Platform/IWindowingPlatform.cs | 4 +- .../Platform/PlatformManager.cs | 5 ++- src/Avalonia.Controls/TrayIcon.cs | 23 ++++++----- .../Remote/PreviewerWindowingPlatform.cs | 2 +- .../Remote/TrayIconStub.cs | 40 ------------------- .../AvaloniaHeadlessPlatform.cs | 5 +-- src/iOS/Avalonia.iOS/Stubs.cs | 2 +- .../WindowingPlatformMock.cs | 2 +- .../MockWindowingPlatform.cs | 2 +- 9 files changed, 24 insertions(+), 61 deletions(-) delete mode 100644 src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index 4efa92cc6b..21882b1271 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace Avalonia.Platform { public interface IWindowingPlatform @@ -6,6 +8,6 @@ namespace Avalonia.Platform IWindowImpl CreateEmbeddableWindow(); - ITrayIconImpl CreateTrayIcon(); + ITrayIconImpl? CreateTrayIcon(); } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index fe83e37909..054f823d6d 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -1,8 +1,9 @@ using System; using System.Reactive.Disposables; -using Avalonia.Media; using Avalonia.Platform; +#nullable enable + namespace Avalonia.Controls.Platform { public static partial class PlatformManager @@ -22,7 +23,7 @@ namespace Avalonia.Controls.Platform { } - public static ITrayIconImpl CreateTrayIcon () + public static ITrayIconImpl? CreateTrayIcon () { var platform = AvaloniaLocator.Current.GetService(); diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 5b10fa20ea..b98e342735 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -16,15 +16,18 @@ namespace Avalonia.Controls public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable { - private readonly ITrayIconImpl _impl; + private readonly ITrayIconImpl? _impl; - private TrayIcon(ITrayIconImpl impl) + private TrayIcon(ITrayIconImpl? impl) { - _impl = impl; + if (impl != null) + { + _impl = impl; - _impl.SetIsVisible(IsVisible); + _impl.SetIsVisible(IsVisible); - _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty); + _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty); + } } public TrayIcon () : this(PlatformManager.CreateTrayIcon()) @@ -160,25 +163,25 @@ namespace Avalonia.Controls if(change.Property == IconProperty) { - _impl.SetIcon(Icon.PlatformImpl); + _impl?.SetIcon(Icon.PlatformImpl); } else if (change.Property == IsVisibleProperty) { - _impl.SetIsVisible(change.NewValue.GetValueOrDefault()); + _impl?.SetIsVisible(change.NewValue.GetValueOrDefault()); } else if (change.Property == ToolTipTextProperty) { - _impl.SetToolTipText(change.NewValue.GetValueOrDefault()); + _impl?.SetToolTipText(change.NewValue.GetValueOrDefault()); } else if (change.Property == MenuProperty) { - _impl.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault()); + _impl?.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault()); } } /// /// Disposes the tray icon (removing it from the tray area). /// - public void Dispose() => _impl.Dispose(); + public void Dispose() => _impl?.Dispose(); } } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index caca15b3a3..ada63f5326 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -17,7 +17,7 @@ namespace Avalonia.DesignerSupport.Remote private static PreviewerWindowImpl s_lastWindow; public static List PreFlightMessages = new List(); - public ITrayIconImpl CreateTrayIcon() => new TrayIconStub(); + public ITrayIconImpl CreateTrayIcon() => null; public IWindowImpl CreateWindow() => new WindowStub(); diff --git a/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs b/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs deleted file mode 100644 index 88ca076f8a..0000000000 --- a/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Controls.Platform; -using Avalonia.Platform; - -namespace Avalonia.DesignerSupport.Remote -{ - class TrayIconStub : ITrayIconImpl - { - public Action Clicked { get; set; } - public Action DoubleClicked { get; set; } - public Action RightClicked { get; set; } - - public INativeMenuExporter MenuExporter => null; - - public Action OnClicked { get; set; } - - public void Dispose() - { - throw new NotImplementedException(); - } - - public void SetIcon(IWindowIconImpl icon) - { - } - - public void SetIsVisible(bool visible) - { - } - - public void SetMenu(NativeMenu menu) - { - throw new NotImplementedException(); - } - - public void SetToolTipText(string text) - { - } - } -} diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index afaec3a8a0..4f0b9e9e8d 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -52,10 +52,7 @@ namespace Avalonia.Headless public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); - public ITrayIconImpl CreateTrayIcon() - { - throw new NotImplementedException(); - } + public ITrayIconImpl CreateTrayIcon() => return null; } internal static void Initialize() diff --git a/src/iOS/Avalonia.iOS/Stubs.cs b/src/iOS/Avalonia.iOS/Stubs.cs index b13dfd39e0..9c46aa78cc 100644 --- a/src/iOS/Avalonia.iOS/Stubs.cs +++ b/src/iOS/Avalonia.iOS/Stubs.cs @@ -22,7 +22,7 @@ namespace Avalonia.iOS public IWindowImpl CreateEmbeddableWindow() => throw new NotSupportedException(); - public ITrayIconImpl CreateTrayIcon() => throw new NotSupportedException(); + public ITrayIconImpl CreateTrayIcon() => null; } class PlatformIconLoaderStub : IPlatformIconLoader diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs index 5c5ec8be90..e8471d41fb 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls.UnitTests public ITrayIconImpl CreateTrayIcon() { - throw new NotImplementedException(); + return null; } public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.RenderScaling == 1); diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 4074885505..eb18030ca8 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -128,7 +128,7 @@ namespace Avalonia.UnitTests public ITrayIconImpl CreateTrayIcon() { - throw new NotImplementedException(); + return null; } private static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl From f3436f16c9f4b0d565daaf367b28304fb104207d Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 5 Oct 2021 21:57:38 +0800 Subject: [PATCH 076/198] remove debug code --- src/Avalonia.X11/X11TrayIconImpl.cs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 99a850dabb..c1011271f3 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -15,14 +15,13 @@ namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int s_trayIconInstanceId = 0; private static int GetTID() => s_trayIconInstanceId++; private ObjectPath _dbusmenuPath; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; private Connection con; private DbusPixmap _icon; - + private IStatusNotifierWatcher _statusNotifierWatcher; private string _sysTrayServiceName; @@ -30,10 +29,10 @@ namespace Avalonia.X11 private bool _isActive; private bool _isDisposed; private readonly bool _ctorFinished; - + public INativeMenuExporter MenuExporter { get; } public Action OnClicked { get; set; } - + public X11TrayIconImpl() { con = DBusHelper.TryGetConnection(); @@ -42,7 +41,7 @@ namespace Avalonia.X11 CreateTrayIcon(); _ctorFinished = true; } - + public async void CreateTrayIcon() { _statusNotifierWatcher = con.CreateProxy("org.kde.StatusNotifierWatcher", @@ -111,7 +110,7 @@ namespace Avalonia.X11 _icon = new DbusPixmap(w, h, pixByteArray); _statusNotifierItemDbusObj.SetIcon(_icon); } - + public void SetIsVisible(bool visible) { if (con == null || _isDisposed || !_ctorFinished) return; @@ -134,7 +133,7 @@ namespace Avalonia.X11 _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } } - + /// /// DBus Object used for setting system tray icons. /// @@ -232,12 +231,7 @@ namespace Avalonia.X11 public async Task GetAsync(string prop) { - if (prop.Contains("Menu")) - { - return _backingProperties.Menu; - } - - return default; + return null; } public async Task GetAllAsync() From 7a547025df3fd774d985176968506d06d9b3aaca Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 5 Oct 2021 22:02:17 +0800 Subject: [PATCH 077/198] handle if we're unable to get a dbus connection --- src/Avalonia.X11/X11TrayIconImpl.cs | 37 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index c1011271f3..8f550f4299 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop; +using Avalonia.Logging; using Avalonia.Platform; using Tmds.DBus; @@ -19,7 +20,7 @@ namespace Avalonia.X11 private static int GetTID() => s_trayIconInstanceId++; private ObjectPath _dbusmenuPath; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - private Connection con; + private Connection _con; private DbusPixmap _icon; private IStatusNotifierWatcher _statusNotifierWatcher; @@ -35,16 +36,26 @@ namespace Avalonia.X11 public X11TrayIconImpl() { - con = DBusHelper.TryGetConnection(); + _con = DBusHelper.TryGetConnection(); + + if (_con is null) + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, "Unable to get a dbus connection for system tray icons."); + return; + } + _dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; - MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusmenuPath, con); + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusmenuPath, _con); CreateTrayIcon(); _ctorFinished = true; } public async void CreateTrayIcon() { - _statusNotifierWatcher = con.CreateProxy("org.kde.StatusNotifierWatcher", + if(_con is null) return; + + _statusNotifierWatcher = _con.CreateProxy("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); var pid = Process.GetCurrentProcess().Id; @@ -53,9 +64,9 @@ namespace Avalonia.X11 _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusmenuPath); - await con.RegisterObjectAsync(_statusNotifierItemDbusObj); + await _con.RegisterObjectAsync(_statusNotifierItemDbusObj); - await con.RegisterServiceAsync(_sysTrayServiceName); + await _con.RegisterServiceAsync(_sysTrayServiceName); await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); @@ -72,8 +83,10 @@ namespace Avalonia.X11 public async void DestroyTrayIcon() { - con.UnregisterObject(_statusNotifierItemDbusObj); - await con.UnregisterServiceAsync(_sysTrayServiceName); + if(_con is null) return; + + _con.UnregisterObject(_statusNotifierItemDbusObj); + await _con.UnregisterServiceAsync(_sysTrayServiceName); _isActive = false; } @@ -81,12 +94,12 @@ namespace Avalonia.X11 { _isDisposed = true; DestroyTrayIcon(); - con.Dispose(); + _con?.Dispose(); } public void SetIcon(IWindowIconImpl icon) { - if (con == null || _isDisposed) return; + if (_con is null || _isDisposed) return; if (!(icon is X11IconData x11icon)) return; var w = (int)x11icon.Data[0]; @@ -113,7 +126,7 @@ namespace Avalonia.X11 public void SetIsVisible(bool visible) { - if (con == null || _isDisposed || !_ctorFinished) return; + if (_con is null || _isDisposed || !_ctorFinished) return; if (visible & !_isActive) { @@ -128,7 +141,7 @@ namespace Avalonia.X11 public void SetToolTipText(string text) { - if (con == null || _isDisposed) return; + if (_con is null || _isDisposed) return; _tooltipText = text; _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } From f97252caa62142ef5393ff918c780352ff7c68c7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Oct 2021 15:19:31 +0100 Subject: [PATCH 078/198] fix warning. --- src/Avalonia.Controls/TrayIcon.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index b98e342735..ad9a668cf2 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -38,7 +38,7 @@ namespace Avalonia.Controls { IconsProperty.Changed.Subscribe(args => { - if (args.Sender is Application application) + if (args.Sender is Application) { if(args.OldValue.Value != null) { @@ -135,7 +135,7 @@ namespace Avalonia.Controls set => SetValue(IsVisibleProperty, value); } - public INativeMenuExporter? NativeMenuExporter => _impl.MenuExporter; + public INativeMenuExporter? NativeMenuExporter => _impl?.MenuExporter; private static void Lifetime_Exit(object sender, ControlledApplicationLifetimeExitEventArgs e) { From 65beb89ee300c04ef4b1fd7edc5b574ae810d5cf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Oct 2021 15:20:17 +0100 Subject: [PATCH 079/198] fix compiler error. --- src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 4f0b9e9e8d..0ca2733cde 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -52,7 +52,7 @@ namespace Avalonia.Headless public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); - public ITrayIconImpl CreateTrayIcon() => return null; + public ITrayIconImpl CreateTrayIcon() => null; } internal static void Initialize() From 6978eababad2fe96427d954e7dd4a869ce404064 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Oct 2021 15:24:26 +0100 Subject: [PATCH 080/198] fix some warnings. --- src/Avalonia.X11/X11TrayIconImpl.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 8f550f4299..032cd07296 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -2,7 +2,6 @@ using System.Diagnostics; using System.Reactive.Disposables; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop; @@ -10,17 +9,17 @@ using Avalonia.Logging; using Avalonia.Platform; using Tmds.DBus; -[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] +[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int s_trayIconInstanceId = 0; + private static int s_trayIconInstanceId; private static int GetTID() => s_trayIconInstanceId++; - private ObjectPath _dbusmenuPath; + private readonly ObjectPath _dbusMenuPath; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - private Connection _con; + private readonly Connection _con; private DbusPixmap _icon; private IStatusNotifierWatcher _statusNotifierWatcher; @@ -45,8 +44,8 @@ namespace Avalonia.X11 return; } - _dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; - MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusmenuPath, _con); + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _con); CreateTrayIcon(); _ctorFinished = true; } @@ -62,7 +61,7 @@ namespace Avalonia.X11 var tid = GetTID(); _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; - _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusmenuPath); + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); await _con.RegisterObjectAsync(_statusNotifierItemDbusObj); From 285e483cf976fbdb15f99dea565e1c74f17e0362 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 5 Oct 2021 22:35:54 +0800 Subject: [PATCH 081/198] remove unnecessary async Tasks and replace them with non-async Task.FromResult --- src/Avalonia.X11/X11TrayIconImpl.cs | 61 ++++++++++++----------------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 032cd07296..20599c4629 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -43,7 +43,7 @@ namespace Avalonia.X11 ?.Log(this, "Unable to get a dbus connection for system tray icons."); return; } - + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _con); CreateTrayIcon(); @@ -52,8 +52,8 @@ namespace Avalonia.X11 public async void CreateTrayIcon() { - if(_con is null) return; - + if (_con is null) return; + _statusNotifierWatcher = _con.CreateProxy("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); @@ -82,7 +82,7 @@ namespace Avalonia.X11 public async void DestroyTrayIcon() { - if(_con is null) return; + if (_con is null) return; _con.UnregisterObject(_statusNotifierItemDbusObj); await _con.UnregisterServiceAsync(_sysTrayServiceName); @@ -179,22 +179,17 @@ namespace Avalonia.X11 InvalidateAll(); } - public async Task ContextMenuAsync(int X, int Y) - { - } + public Task ContextMenuAsync(int X, int Y) => Task.CompletedTask; - public async Task ActivateAsync(int X, int Y) + public Task ActivateAsync(int X, int Y) { ActivationDelegate?.Invoke(); + return Task.CompletedTask; } - public async Task SecondaryActivateAsync(int X, int Y) - { - } + public Task SecondaryActivateAsync(int X, int Y) => Task.CompletedTask; - public async Task ScrollAsync(int Delta, string Orientation) - { - } + public Task ScrollAsync(int Delta, string Orientation) => Task.CompletedTask; public void InvalidateAll() { @@ -205,58 +200,52 @@ namespace Avalonia.X11 OnTooltipChanged?.Invoke(); } - public async Task WatchNewTitleAsync(Action handler, Action onError = null) + public Task WatchNewTitleAsync(Action handler, Action onError = null) { OnTitleChanged += handler; - return Disposable.Create(() => OnTitleChanged -= handler); + return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); } - public async Task WatchNewIconAsync(Action handler, Action onError = null) + public Task WatchNewIconAsync(Action handler, Action onError = null) { OnIconChanged += handler; - return Disposable.Create(() => OnIconChanged -= handler); + return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); } - public async Task WatchNewAttentionIconAsync(Action handler, Action onError = null) + public Task WatchNewAttentionIconAsync(Action handler, Action onError = null) { OnAttentionIconChanged += handler; - return Disposable.Create(() => OnAttentionIconChanged -= handler); + return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); } - public async Task WatchNewOverlayIconAsync(Action handler, Action onError = null) + public Task WatchNewOverlayIconAsync(Action handler, Action onError = null) { OnOverlayIconChanged += handler; - return Disposable.Create(() => OnOverlayIconChanged -= handler); + return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); } - public async Task WatchNewToolTipAsync(Action handler, Action onError = null) + public Task WatchNewToolTipAsync(Action handler, Action onError = null) { OnTooltipChanged += handler; - return Disposable.Create(() => OnTooltipChanged -= handler); + return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); } - public async Task WatchNewStatusAsync(Action handler, Action onError = null) + public Task WatchNewStatusAsync(Action handler, Action onError = null) { NewStatusAsync += handler; - return Disposable.Create(() => NewStatusAsync -= handler); + return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); } - public async Task GetAsync(string prop) - { - return null; - } + public Task GetAsync(string prop) => Task.FromResult(new object()); - public async Task GetAllAsync() - { - return _backingProperties; - } + public Task GetAllAsync() => Task.FromResult(_backingProperties); public Task SetAsync(string prop, object val) => Task.CompletedTask; - public async Task WatchPropertiesAsync(Action handler) + public Task WatchPropertiesAsync(Action handler) { OnPropertyChange += handler; - return Disposable.Create(() => OnPropertyChange -= handler); + return Task.FromResult(Disposable.Create(() => OnPropertyChange -= handler)); } public void SetIcon(DbusPixmap dbusPixmap) From b5b614bb60def1a1d85e14556a068700534d310f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Oct 2021 16:26:55 +0100 Subject: [PATCH 082/198] fix warnings. --- .../Avalonia.Win32/Win32NativeToManagedMenuExporter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index 72a7a6ff35..f8ae128725 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -10,18 +10,18 @@ namespace Avalonia.Win32 { private NativeMenu? _nativeMenu; - public void SetNativeMenu(NativeMenu nativeMenu) + public void SetNativeMenu(NativeMenu? nativeMenu) { _nativeMenu = nativeMenu; } - private IEnumerable? Populate (NativeMenu nativeMenu) + private IEnumerable Populate (NativeMenu nativeMenu) { var items = new List(); foreach (var menuItem in nativeMenu.Items) { - if (menuItem is NativeMenuItemSeparator separator) + if (menuItem is NativeMenuItemSeparator) { items.Add(new MenuItem { Header = "-" }); } From 10a748a235ab22e65729fb8a45242589148a7c73 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Oct 2021 16:38:08 +0100 Subject: [PATCH 083/198] make new win32 types internal. --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 7057199c52..8cb7b42833 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -2300,7 +2300,7 @@ namespace Avalonia.Win32.Interop public uint DamageMask; } - public enum NIM : uint + internal enum NIM : uint { ADD = 0x00000000, MODIFY = 0x00000001, @@ -2310,7 +2310,7 @@ namespace Avalonia.Win32.Interop } [Flags] - public enum NIF : uint + internal enum NIF : uint { MESSAGE = 0x00000001, ICON = 0x00000002, @@ -2323,7 +2323,7 @@ namespace Avalonia.Win32.Interop } [Flags] - public enum NIIF : uint + internal enum NIIF : uint { NONE = 0x00000000, INFO = 0x00000001, @@ -2337,7 +2337,7 @@ namespace Avalonia.Win32.Interop } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] - public class NOTIFYICONDATA + internal class NOTIFYICONDATA { public int cbSize = Marshal.SizeOf(); public IntPtr hWnd; From 52e188507c7a45d9cf06af180ae86f86397c1c3e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 13:26:03 +0100 Subject: [PATCH 084/198] win32 - fix activate method, now same as wpf. --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 3 +++ src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 8cb7b42833..938f4222e0 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1110,6 +1110,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr SetActiveWindow(IntPtr hWnd); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] public static extern IntPtr SetCapture(IntPtr hWnd); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 8fc25f8cfa..8b4703d2ec 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -507,7 +507,7 @@ namespace Avalonia.Win32 public void Activate() { - SetActiveWindow(_hwnd); + SetForegroundWindow(_hwnd); } public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this); From d13fc38e6928746d8ac3772f18a9aef9967cafc5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 13:39:07 +0100 Subject: [PATCH 085/198] win32 - actually activate window correctly during show --- src/Windows/Avalonia.Win32/WindowImpl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 8b4703d2ec..d1b2115cf6 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1000,6 +1000,7 @@ namespace Avalonia.Win32 if (!Design.IsDesignMode && activate) { SetFocus(_hwnd); + SetForegroundWindow(_hwnd); } } From 261a0f0c2319b9849a3a2f1ae70e4bd6d0b83a79 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 13:39:17 +0100 Subject: [PATCH 086/198] fix tray icon closing. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 25 ++-------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index fce56bcb21..e8fc00fb74 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -181,32 +181,16 @@ namespace Avalonia.Win32 _positioner = new ManagedPopupPositioner(new TrayIconManagedPopupPositionerPopupImplHelper(MoveResize)); Topmost = true; - Activated += TrayPopupRoot_Activated; Deactivated += TrayPopupRoot_Deactivated; - LostFocus += TrayPopupRoot_LostFocus1; - ShowInTaskbar = false; - } - private void TrayPopupRoot_LostFocus1(object sender, Interactivity.RoutedEventArgs e) - { - Debug.WriteLine("TrayIcon - Lost Focus"); - } - - private void TrayPopupRoot_Activated(object sender, EventArgs e) - { - Debug.WriteLine("TrayIcon - Activated"); + ShowActivated = true; } private void TrayPopupRoot_Deactivated(object sender, EventArgs e) { - Debug.WriteLine("TrayIcon - Deactivated"); - - Dispatcher.UIThread.Post(() => - { - Close(); - }); + Close(); } private void MoveResize(PixelPoint position, Size size, double scaling) @@ -215,11 +199,6 @@ namespace Avalonia.Win32 PlatformImpl.Resize(size, PlatformResizeReason.Layout); } - private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e) - { - Close(); - } - protected override void ArrangeCore(Rect finalRect) { base.ArrangeCore(finalRect); From ffc79482bed8caa68e0c6cfb2af03e615c0a1564 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 6 Oct 2021 21:25:40 +0800 Subject: [PATCH 087/198] fix review comments on X11TrayIconImpl.cs --- src/Avalonia.X11/X11TrayIconImpl.cs | 69 ++++++++++++++--------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 20599c4629..160537cb26 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Diagnostics; using System.Reactive.Disposables; using System.Runtime.CompilerServices; @@ -15,11 +17,10 @@ namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int s_trayIconInstanceId; - private static int GetTID() => s_trayIconInstanceId++; + private static int trayIconInstanceId; private readonly ObjectPath _dbusMenuPath; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - private readonly Connection _con; + private readonly Connection _connection; private DbusPixmap _icon; private IStatusNotifierWatcher _statusNotifierWatcher; @@ -35,9 +36,9 @@ namespace Avalonia.X11 public X11TrayIconImpl() { - _con = DBusHelper.TryGetConnection(); + _connection = DBusHelper.TryGetConnection(); - if (_con is null) + if (_connection is null) { Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, "Unable to get a dbus connection for system tray icons."); @@ -45,27 +46,27 @@ namespace Avalonia.X11 } _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; - MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _con); + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); CreateTrayIcon(); _ctorFinished = true; } public async void CreateTrayIcon() { - if (_con is null) return; + if (_connection is null) return; - _statusNotifierWatcher = _con.CreateProxy("org.kde.StatusNotifierWatcher", + _statusNotifierWatcher = _connection.CreateProxy("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); var pid = Process.GetCurrentProcess().Id; - var tid = GetTID(); + var tid = trayIconInstanceId++; _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); - await _con.RegisterObjectAsync(_statusNotifierItemDbusObj); + await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); - await _con.RegisterServiceAsync(_sysTrayServiceName); + await _connection.RegisterServiceAsync(_sysTrayServiceName); await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); @@ -82,10 +83,10 @@ namespace Avalonia.X11 public async void DestroyTrayIcon() { - if (_con is null) return; + if (_connection is null) return; - _con.UnregisterObject(_statusNotifierItemDbusObj); - await _con.UnregisterServiceAsync(_sysTrayServiceName); + _connection.UnregisterObject(_statusNotifierItemDbusObj); + await _connection.UnregisterServiceAsync(_sysTrayServiceName); _isActive = false; } @@ -93,30 +94,28 @@ namespace Avalonia.X11 { _isDisposed = true; DestroyTrayIcon(); - _con?.Dispose(); + _connection?.Dispose(); } public void SetIcon(IWindowIconImpl icon) { - if (_con is null || _isDisposed) return; + if (_connection is null || _isDisposed) return; if (!(icon is X11IconData x11icon)) return; var w = (int)x11icon.Data[0]; var h = (int)x11icon.Data[1]; - var rx = x11icon.Data.AsSpan(2); var pixLength = w * h; - var pixByteArrayCounter = 0; var pixByteArray = new byte[w * h * 4]; for (var i = 0; i < pixLength; i++) { - var u = rx[i].ToUInt32(); - pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF000000) >> 24); - pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF0000) >> 16); - pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF00) >> 8); - pixByteArray[pixByteArrayCounter++] = (byte)(u & 0xFF); + var rawPixel = x11icon.Data[i+2].ToUInt32(); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8); + pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); } _icon = new DbusPixmap(w, h, pixByteArray); @@ -125,7 +124,7 @@ namespace Avalonia.X11 public void SetIsVisible(bool visible) { - if (_con is null || _isDisposed || !_ctorFinished) return; + if (_connection is null || _isDisposed || !_ctorFinished) return; if (visible & !_isActive) { @@ -140,7 +139,7 @@ namespace Avalonia.X11 public void SetToolTipText(string text) { - if (_con is null || _isDisposed) return; + if (_connection is null || _isDisposed) return; _tooltipText = text; _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } @@ -179,17 +178,17 @@ namespace Avalonia.X11 InvalidateAll(); } - public Task ContextMenuAsync(int X, int Y) => Task.CompletedTask; + public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; - public Task ActivateAsync(int X, int Y) + public Task ActivateAsync(int x, int y) { ActivationDelegate?.Invoke(); return Task.CompletedTask; } - public Task SecondaryActivateAsync(int X, int Y) => Task.CompletedTask; + public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; - public Task ScrollAsync(int Delta, string Orientation) => Task.CompletedTask; + public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; public void InvalidateAll() { @@ -276,10 +275,10 @@ namespace Avalonia.X11 [DBusInterface("org.kde.StatusNotifierItem")] internal interface IStatusNotifierItem : IDBusObject { - Task ContextMenuAsync(int X, int Y); - Task ActivateAsync(int X, int Y); - Task SecondaryActivateAsync(int X, int Y); - Task ScrollAsync(int Delta, string Orientation); + Task ContextMenuAsync(int x, int y); + Task ActivateAsync(int x, int y); + Task SecondaryActivateAsync(int x, int y); + Task ScrollAsync(int delta, string orientation); Task WatchNewTitleAsync(Action handler, Action onError = null); Task WatchNewIconAsync(Action handler, Action onError = null); Task WatchNewAttentionIconAsync(Action handler, Action onError = null); @@ -337,7 +336,7 @@ namespace Avalonia.X11 private static readonly DbusPixmap[] s_blank = { - new DbusPixmap(0, 0, new byte[] { }), new DbusPixmap(0, 0, new byte[] { }) + new DbusPixmap(0, 0, Array.Empty()), new DbusPixmap(0, 0, Array.Empty()) }; public ToolTip(string message) : this("", s_blank, message, "") From 4288565590329400903813f9cfee056cd73e878b Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 6 Oct 2021 21:51:56 +0800 Subject: [PATCH 088/198] fix nullable warnings --- src/Avalonia.X11/X11TrayIconImpl.cs | 113 +++++++++++----------------- 1 file changed, 45 insertions(+), 68 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 160537cb26..42016ed52a 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -19,20 +19,20 @@ namespace Avalonia.X11 { private static int trayIconInstanceId; private readonly ObjectPath _dbusMenuPath; - private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - private readonly Connection _connection; + private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; + private readonly Connection? _connection; private DbusPixmap _icon; private IStatusNotifierWatcher _statusNotifierWatcher; - private string _sysTrayServiceName; - private string _tooltipText; + private string? _sysTrayServiceName; + private string? _tooltipText; private bool _isActive; private bool _isDisposed; private readonly bool _ctorFinished; - public INativeMenuExporter MenuExporter { get; } - public Action OnClicked { get; set; } + public INativeMenuExporter? MenuExporter { get; } + public Action? OnClicked { get; set; } public X11TrayIconImpl() { @@ -53,8 +53,8 @@ namespace Avalonia.X11 public async void CreateTrayIcon() { - if (_connection is null) return; - + if(_connection is null) return; + _statusNotifierWatcher = _connection.CreateProxy("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); @@ -83,8 +83,7 @@ namespace Avalonia.X11 public async void DestroyTrayIcon() { - if (_connection is null) return; - + if(_connection is null) return; _connection.UnregisterObject(_statusNotifierItemDbusObj); await _connection.UnregisterServiceAsync(_sysTrayServiceName); _isActive = false; @@ -97,9 +96,9 @@ namespace Avalonia.X11 _connection?.Dispose(); } - public void SetIcon(IWindowIconImpl icon) + public void SetIcon(IWindowIconImpl? icon) { - if (_connection is null || _isDisposed) return; + if (_isDisposed) return; if (!(icon is X11IconData x11icon)) return; var w = (int)x11icon.Data[0]; @@ -119,12 +118,12 @@ namespace Avalonia.X11 } _icon = new DbusPixmap(w, h, pixByteArray); - _statusNotifierItemDbusObj.SetIcon(_icon); + _statusNotifierItemDbusObj?.SetIcon(_icon); } public void SetIsVisible(bool visible) { - if (_connection is null || _isDisposed || !_ctorFinished) return; + if (_isDisposed || !_ctorFinished) return; if (visible & !_isActive) { @@ -137,9 +136,9 @@ namespace Avalonia.X11 } } - public void SetToolTipText(string text) + public void SetToolTipText(string? text) { - if (_connection is null || _isDisposed) return; + if (_isDisposed || text is null) return; _tooltipText = text; _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } @@ -154,15 +153,13 @@ namespace Avalonia.X11 internal class StatusNotifierItemDbusObj : IStatusNotifierItem { private readonly StatusNotifierItemProperties _backingProperties; - private event Action OnPropertyChange; - public event Action OnTitleChanged; - public event Action OnIconChanged; - public event Action OnAttentionIconChanged; - public event Action OnOverlayIconChanged; - public event Action OnTooltipChanged; - public Action SetNativeMenuExporter { get; set; } - public Action NewStatusAsync { get; set; } - public Action ActivationDelegate { get; set; } + public event Action? OnTitleChanged; + public event Action? OnIconChanged; + public event Action? OnAttentionIconChanged; + public event Action? OnOverlayIconChanged; + public event Action? OnTooltipChanged; + public Action? NewStatusAsync { get; set; } + public Action? ActivationDelegate { get; set; } public ObjectPath ObjectPath { get; } public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) @@ -199,37 +196,37 @@ namespace Avalonia.X11 OnTooltipChanged?.Invoke(); } - public Task WatchNewTitleAsync(Action handler, Action onError = null) + public Task WatchNewTitleAsync(Action handler, Action onError) { OnTitleChanged += handler; return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); } - public Task WatchNewIconAsync(Action handler, Action onError = null) + public Task WatchNewIconAsync(Action handler, Action onError) { OnIconChanged += handler; return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); } - public Task WatchNewAttentionIconAsync(Action handler, Action onError = null) + public Task WatchNewAttentionIconAsync(Action handler, Action onError) { OnAttentionIconChanged += handler; return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); } - public Task WatchNewOverlayIconAsync(Action handler, Action onError = null) + public Task WatchNewOverlayIconAsync(Action handler, Action onError) { OnOverlayIconChanged += handler; return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); } - public Task WatchNewToolTipAsync(Action handler, Action onError = null) + public Task WatchNewToolTipAsync(Action handler, Action onError) { OnTooltipChanged += handler; return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); } - public Task WatchNewStatusAsync(Action handler, Action onError = null) + public Task WatchNewStatusAsync(Action handler, Action onError) { NewStatusAsync += handler; return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); @@ -241,11 +238,7 @@ namespace Avalonia.X11 public Task SetAsync(string prop, object val) => Task.CompletedTask; - public Task WatchPropertiesAsync(Action handler) - { - OnPropertyChange += handler; - return Task.FromResult(Disposable.Create(() => OnPropertyChange -= handler)); - } + public Task WatchPropertiesAsync(Action handler) => Task.FromResult(Disposable.Empty); public void SetIcon(DbusPixmap dbusPixmap) { @@ -253,8 +246,10 @@ namespace Avalonia.X11 InvalidateAll(); } - public void SetTitleAndTooltip(string text) + public void SetTitleAndTooltip(string? text) { + if (text is null) return; + _backingProperties.Id = text; _backingProperties.Category = "ApplicationStatus"; _backingProperties.Status = text; @@ -279,12 +274,12 @@ namespace Avalonia.X11 Task ActivateAsync(int x, int y); Task SecondaryActivateAsync(int x, int y); Task ScrollAsync(int delta, string orientation); - Task WatchNewTitleAsync(Action handler, Action onError = null); - Task WatchNewIconAsync(Action handler, Action onError = null); - Task WatchNewAttentionIconAsync(Action handler, Action onError = null); - Task WatchNewOverlayIconAsync(Action handler, Action onError = null); - Task WatchNewToolTipAsync(Action handler, Action onError = null); - Task WatchNewStatusAsync(Action handler, Action onError = null); + Task WatchNewTitleAsync(Action handler, Action onError); + Task WatchNewIconAsync(Action handler, Action onError); + Task WatchNewAttentionIconAsync(Action handler, Action onError); + Task WatchNewOverlayIconAsync(Action handler, Action onError); + Task WatchNewToolTipAsync(Action handler, Action onError); + Task WatchNewStatusAsync(Action handler, Action onError); Task GetAsync(string prop); Task GetAllAsync(); Task SetAsync(string prop, object val); @@ -294,36 +289,18 @@ namespace Avalonia.X11 [Dictionary] internal class StatusNotifierItemProperties { - public string Category; - - public string Id; + public string? Category; - public string Title; + public string? Id; - public string Status; + public string? Title; - public int WindowId; - - public string IconThemePath; + public string? Status; public ObjectPath Menu; - - public bool ItemIsMenu; - - public string IconName; - - public DbusPixmap[] IconPixmap; - - public string OverlayIconName; - - public DbusPixmap[] OverlayIconPixmap; - - public string AttentionIconName; - - public DbusPixmap[] AttentionIconPixmap; - - public string AttentionMovieName; - + + public DbusPixmap[]? IconPixmap; + public ToolTip ToolTip; } From 63e616c32ffdd7d5b81642e2f939b5e11b8bf832 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 6 Oct 2021 21:55:05 +0800 Subject: [PATCH 089/198] zero warnings on X11TrayIconImpl.cs --- src/Avalonia.X11/X11TrayIconImpl.cs | 37 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 42016ed52a..9cba24e9f9 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -17,13 +17,13 @@ namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int trayIconInstanceId; + private static int trayIconInstanceId; private readonly ObjectPath _dbusMenuPath; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private readonly Connection? _connection; private DbusPixmap _icon; - private IStatusNotifierWatcher _statusNotifierWatcher; + private IStatusNotifierWatcher? _statusNotifierWatcher; private string? _sysTrayServiceName; private string? _tooltipText; @@ -53,10 +53,22 @@ namespace Avalonia.X11 public async void CreateTrayIcon() { - if(_connection is null) return; - - _statusNotifierWatcher = _connection.CreateProxy("org.kde.StatusNotifierWatcher", - "/StatusNotifierWatcher"); + if (_connection is null) return; + + try + { + _statusNotifierWatcher = _connection.CreateProxy( + "org.kde.StatusNotifierWatcher", + "/StatusNotifierWatcher"); + } + catch (Exception) + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, + "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); + } + + if (_statusNotifierWatcher is null) return; var pid = Process.GetCurrentProcess().Id; var tid = trayIconInstanceId++; @@ -83,7 +95,7 @@ namespace Avalonia.X11 public async void DestroyTrayIcon() { - if(_connection is null) return; + if (_connection is null) return; _connection.UnregisterObject(_statusNotifierItemDbusObj); await _connection.UnregisterServiceAsync(_sysTrayServiceName); _isActive = false; @@ -110,7 +122,7 @@ namespace Avalonia.X11 for (var i = 0; i < pixLength; i++) { - var rawPixel = x11icon.Data[i+2].ToUInt32(); + var rawPixel = x11icon.Data[i + 2].ToUInt32(); pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24); pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16); pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8); @@ -238,7 +250,8 @@ namespace Avalonia.X11 public Task SetAsync(string prop, object val) => Task.CompletedTask; - public Task WatchPropertiesAsync(Action handler) => Task.FromResult(Disposable.Empty); + public Task WatchPropertiesAsync(Action handler) => + Task.FromResult(Disposable.Empty); public void SetIcon(DbusPixmap dbusPixmap) { @@ -249,7 +262,7 @@ namespace Avalonia.X11 public void SetTitleAndTooltip(string? text) { if (text is null) return; - + _backingProperties.Id = text; _backingProperties.Category = "ApplicationStatus"; _backingProperties.Status = text; @@ -298,9 +311,9 @@ namespace Avalonia.X11 public string? Status; public ObjectPath Menu; - + public DbusPixmap[]? IconPixmap; - + public ToolTip ToolTip; } From 8c4a702a40b579ca0f7b3a4036bf0a5f764b16bb Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 6 Oct 2021 21:58:20 +0800 Subject: [PATCH 090/198] use less verbose guid generation code --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 932b876088..d5916348be 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -31,7 +31,7 @@ namespace Avalonia.FreeDesktop } public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" - + Guid.NewGuid().ToString().Replace("-", ""); + + Guid.NewGuid().ToString("N"); private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable { From 8183d24200935f7af6c2a7242ee1e229d77bcf14 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 6 Oct 2021 22:12:23 +0800 Subject: [PATCH 091/198] add a comment re: SNIItemProps --- src/Avalonia.X11/X11TrayIconImpl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 9cba24e9f9..fe36e9540e 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -300,6 +300,10 @@ namespace Avalonia.X11 } [Dictionary] + /// This class is used by Tmds.Dbus to ferry properties + /// from the SNI spec. + /// Don't change this to actual C# properties since + /// Tmds.Dbus will get confused. internal class StatusNotifierItemProperties { public string? Category; From daddc71372bc99742e61c292de4c798991bb25ea Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 15:28:10 +0100 Subject: [PATCH 092/198] remove trayicon events from osx. --- native/Avalonia.Native/src/OSX/common.h | 2 +- native/Avalonia.Native/src/OSX/main.mm | 4 ++-- native/Avalonia.Native/src/OSX/trayicon.h | 3 +-- native/Avalonia.Native/src/OSX/trayicon.mm | 8 +++----- src/Avalonia.Native/TrayIconImpl.cs | 20 +------------------- src/Avalonia.Native/avn.idl | 9 +-------- 6 files changed, 9 insertions(+), 37 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 5c174eb663..8896fbe88b 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -22,7 +22,7 @@ extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); -extern IAvnTrayIcon* CreateTrayIcon(IAvnTrayIconEvents* events); +extern IAvnTrayIcon* CreateTrayIcon(); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index f179d4f049..eeaaecfdbd 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -303,13 +303,13 @@ public: } } - virtual HRESULT CreateTrayIcon (IAvnTrayIconEvents*cb, IAvnTrayIcon** ppv) override + virtual HRESULT CreateTrayIcon (IAvnTrayIcon** ppv) override { START_COM_CALL; @autoreleasepool { - *ppv = ::CreateTrayIcon(cb); + *ppv = ::CreateTrayIcon(); return S_OK; } } diff --git a/native/Avalonia.Native/src/OSX/trayicon.h b/native/Avalonia.Native/src/OSX/trayicon.h index 11ad71756a..f94f9a871b 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.h +++ b/native/Avalonia.Native/src/OSX/trayicon.h @@ -15,12 +15,11 @@ class AvnTrayIcon : public ComSingleObject { private: NSStatusItem* _native; - ComPtr _events; public: FORWARD_IUNKNOWN() - AvnTrayIcon(IAvnTrayIconEvents* events); + AvnTrayIcon(); ~AvnTrayIcon (); diff --git a/native/Avalonia.Native/src/OSX/trayicon.mm b/native/Avalonia.Native/src/OSX/trayicon.mm index 79b16f82c6..151990cfb1 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.mm +++ b/native/Avalonia.Native/src/OSX/trayicon.mm @@ -2,18 +2,16 @@ #include "trayicon.h" #include "menu.h" -extern IAvnTrayIcon* CreateTrayIcon(IAvnTrayIconEvents* cb) +extern IAvnTrayIcon* CreateTrayIcon() { @autoreleasepool { - return new AvnTrayIcon(cb); + return new AvnTrayIcon(); } } -AvnTrayIcon::AvnTrayIcon(IAvnTrayIconEvents* events) +AvnTrayIcon::AvnTrayIcon() { - _events = events; - _native = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength]; } diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs index b8b81214f1..951bbc496e 100644 --- a/src/Avalonia.Native/TrayIconImpl.cs +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -8,31 +8,13 @@ using Avalonia.Platform; namespace Avalonia.Native { - class TrayIconEvents : CallbackBase, IAvnTrayIconEvents - { - private TrayIconImpl _parent; - - public TrayIconEvents (TrayIconImpl parent) - { - _parent = parent; - } - - public void Clicked() - { - } - - public void DoubleClicked() - { - } - } - internal class TrayIconImpl : ITrayIconImpl { private readonly IAvnTrayIcon _native; public TrayIconImpl(IAvaloniaNativeFactory factory) { - _native = factory.CreateTrayIcon(new TrayIconEvents(this)); + _native = factory.CreateTrayIcon(); MenuExporter = new AvaloniaNativeMenuExporter(_native, factory); } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index c6fd3850c5..00c54750a4 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -427,7 +427,7 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv); HRESULT CreateMenuItem(IAvnMenuItem** ppv); HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); - HRESULT CreateTrayIcon(IAvnTrayIconEvents* cb, IAvnTrayIcon** ppv); + HRESULT CreateTrayIcon(IAvnTrayIcon** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -674,13 +674,6 @@ interface IAvnTrayIcon : IUnknown HRESULT SetIsVisible(bool isVisible); } -[uuid(a687a6d9-73aa-4fef-9b4a-61587d7285d3)] -interface IAvnTrayIconEvents : IUnknown -{ - void Clicked (); - void DoubleClicked (); -} - [uuid(a7724dc1-cf6b-4fa8-9d23-228bf2593edc)] interface IAvnMenu : IUnknown { From ca5d78d507141f5a9b7fdd4c7369ead51dbbb2c0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 15:32:55 +0100 Subject: [PATCH 093/198] fix warnings and use readonly fields. --- .../AvaloniaNativeMenuExporter.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index dd52bd3544..e89a4bf59e 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -9,15 +9,15 @@ using Avalonia.Threading; namespace Avalonia.Native { - class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter + internal class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter { - private IAvaloniaNativeFactory _factory; + private readonly IAvaloniaNativeFactory _factory; private bool _resetQueued = true; - private bool _exported = false; - private IAvnWindow _nativeWindow; + private bool _exported; + private readonly IAvnWindow _nativeWindow; private NativeMenu _menu; private __MicroComIAvnMenuProxy _nativeMenu; - private IAvnTrayIcon _trayIcon; + private readonly IAvnTrayIcon _trayIcon; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -48,7 +48,7 @@ namespace Avalonia.Native public void SetNativeMenu(NativeMenu menu) { - _menu = menu == null ? new NativeMenu() : menu; + _menu = menu ?? new NativeMenu(); DoLayoutReset(true); } @@ -137,7 +137,7 @@ namespace Avalonia.Native var appMenuHolder = menuItem?.Parent; - if (menu.Parent is null) + if (menuItem is null) { menuItem = new NativeMenuItem(); } @@ -155,7 +155,7 @@ namespace Avalonia.Native if (_nativeMenu is null) { - _nativeMenu = (__MicroComIAvnMenuProxy)__MicroComIAvnMenuProxy.Create(_factory); + _nativeMenu = __MicroComIAvnMenuProxy.Create(_factory); _nativeMenu.Initialize(this, appMenuHolder, ""); From f0dcaea4046fcb03018d2da2a845e5c994ab2cf1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 15:38:45 +0100 Subject: [PATCH 094/198] fix warnings and issues. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 36 +++++++++------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index e8fc00fb74..2dbc844ab3 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.LogicalTree; using Avalonia.Platform; using Avalonia.Styling; -using Avalonia.Threading; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -19,20 +16,20 @@ namespace Avalonia.Win32 { public class TrayIconImpl : ITrayIconImpl { - private readonly int _uniqueId = 0; - private static int _nextUniqueId = 0; + private readonly int _uniqueId; + private static int s_nextUniqueId; private bool _iconAdded; private IconImpl? _icon; private string? _tooltipText; private readonly Win32NativeToManagedMenuExporter _exporter; - private static Dictionary s_trayIcons = new Dictionary(); + private static readonly Dictionary s_trayIcons = new Dictionary(); private bool _disposedValue; public TrayIconImpl() { _exporter = new Win32NativeToManagedMenuExporter(); - _uniqueId = ++_nextUniqueId; + _uniqueId = ++s_nextUniqueId; s_trayIcons.Add(_uniqueId, this); } @@ -113,17 +110,12 @@ namespace Avalonia.Win32 case (int)WindowsMessage.WM_RBUTTONUP: OnRightClicked(); break; - - default: - break; } return IntPtr.Zero; } - else - { - return DefWindowProc(hWnd, msg, wParam, lParam); - } + + return DefWindowProc(hWnd, msg, wParam, lParam); } private void OnRightClicked() @@ -150,13 +142,13 @@ namespace Avalonia.Win32 /// /// Custom Win32 window messages for the NotifyIcon /// - enum CustomWindowsMessage : uint + private enum CustomWindowsMessage : uint { WM_TRAYICON = WindowsMessage.WM_APP + 1024, WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 } - class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable + private class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable { Type IStyleable.StyleKey => typeof(MenuFlyoutPresenter); @@ -172,9 +164,9 @@ namespace Avalonia.Win32 } } - class TrayPopupRoot : Window + private class TrayPopupRoot : Window { - private ManagedPopupPositioner _positioner; + private readonly ManagedPopupPositioner _positioner; public TrayPopupRoot() { @@ -195,8 +187,8 @@ namespace Avalonia.Win32 private void MoveResize(PixelPoint position, Size size, double scaling) { - PlatformImpl.Move(position); - PlatformImpl.Resize(size, PlatformResizeReason.Layout); + PlatformImpl!.Move(position); + PlatformImpl!.Resize(size, PlatformResizeReason.Layout); } protected override void ArrangeCore(Rect finalRect) @@ -217,7 +209,7 @@ namespace Avalonia.Win32 { public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); private readonly MoveResizeDelegate _moveResize; - private Window _hiddenWindow; + private readonly Window _hiddenWindow; public TrayIconManagedPopupPositionerPopupImplHelper(MoveResizeDelegate moveResize) { @@ -244,7 +236,7 @@ namespace Avalonia.Win32 _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _hiddenWindow.Screens.Primary.PixelDensity); } - public virtual double Scaling => _hiddenWindow.Screens.Primary.PixelDensity; + public double Scaling => _hiddenWindow.Screens.Primary.PixelDensity; } } From 89f77429097c7c8a01361b34dc6ec56a6c3bc523 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 15:41:08 +0100 Subject: [PATCH 095/198] more review fixes. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index e89a4bf59e..4431e108ed 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -103,12 +103,9 @@ namespace Avalonia.Native SetMenu(appMenu); } - else + else if (_menu != null) { - if (_menu != null) - { - SetMenu(_trayIcon, _menu); - } + SetMenu(_trayIcon, _menu); } } else From 597239c92a4203bf2e9dd273977645189f10d8eb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 15:47:07 +0100 Subject: [PATCH 096/198] more review fixes. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 7 +++---- .../Avalonia.Win32/Win32NativeToManagedMenuExporter.cs | 10 +++------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 2dbc844ab3..23395dd9b5 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -205,13 +205,12 @@ namespace Avalonia.Win32 }); } - class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup + private class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup { - public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); - private readonly MoveResizeDelegate _moveResize; + private readonly Action _moveResize; private readonly Window _hiddenWindow; - public TrayIconManagedPopupPositionerPopupImplHelper(MoveResizeDelegate moveResize) + public TrayIconManagedPopupPositionerPopupImplHelper(Action moveResize) { _moveResize = moveResize; _hiddenWindow = new Window(); diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index f8ae128725..7ac6e542ac 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -17,13 +17,11 @@ namespace Avalonia.Win32 private IEnumerable Populate (NativeMenu nativeMenu) { - var items = new List(); - foreach (var menuItem in nativeMenu.Items) { if (menuItem is NativeMenuItemSeparator) { - items.Add(new MenuItem { Header = "-" }); + yield return new MenuItem { Header = "-" }; } else if (menuItem is NativeMenuItem item) { @@ -35,14 +33,12 @@ namespace Avalonia.Win32 } else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) { - newItem.Click += (s, e) => bridge.RaiseClicked(); + newItem.Click += (_, __) => bridge.RaiseClicked(); } - items.Add(newItem); + yield return newItem; } } - - return items; } public IEnumerable? GetMenu () From 15829fb1e967fda9a714ea7363cbe744c963f4d4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 17:10:03 +0100 Subject: [PATCH 097/198] formatting. --- .../Avalonia.Win32/Win32NativeToManagedMenuExporter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index 7ac6e542ac..f1c5791359 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -15,7 +15,7 @@ namespace Avalonia.Win32 _nativeMenu = nativeMenu; } - private IEnumerable Populate (NativeMenu nativeMenu) + private IEnumerable Populate(NativeMenu nativeMenu) { foreach (var menuItem in nativeMenu.Items) { @@ -41,7 +41,7 @@ namespace Avalonia.Win32 } } - public IEnumerable? GetMenu () + public IEnumerable? GetMenu() { if(_nativeMenu != null) { From 24faaee758480be538568548922e9be6e8e6dc6b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 17:12:26 +0100 Subject: [PATCH 098/198] fix static variable. --- src/Avalonia.X11/X11TrayIconImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index fe36e9540e..f58e462edf 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -17,7 +17,7 @@ namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int trayIconInstanceId; + private static int s_TrayIconInstanceId; private readonly ObjectPath _dbusMenuPath; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private readonly Connection? _connection; @@ -71,7 +71,7 @@ namespace Avalonia.X11 if (_statusNotifierWatcher is null) return; var pid = Process.GetCurrentProcess().Id; - var tid = trayIconInstanceId++; + var tid = s_TrayIconInstanceId++; _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); From ca39b411f54d4e34c8ac27b579994ca5e2e2ad6f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 17:13:33 +0100 Subject: [PATCH 099/198] use subscribe instead of assign. --- src/Avalonia.X11/X11TrayIconImpl.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index f58e462edf..70b4587b08 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -85,10 +85,7 @@ namespace Avalonia.X11 _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); _statusNotifierItemDbusObj.SetIcon(_icon); - _statusNotifierItemDbusObj.ActivationDelegate = () => - { - OnClicked?.Invoke(); - }; + _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; _isActive = true; } From dd3d3944a6554364077193d03c8222f592a78134 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 17:15:11 +0100 Subject: [PATCH 100/198] fix variable name. --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index d5916348be..9e426688d8 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -37,16 +37,17 @@ namespace Avalonia.FreeDesktop { private readonly Connection _dbus; private readonly uint _xid; - private IRegistrar _registar; + private IRegistrar _registrar; private bool _disposed; private uint _revision = 1; private NativeMenu _menu; - private Dictionary _idsToItems = new Dictionary(); - private Dictionary _itemsToIds = new Dictionary(); + private readonly Dictionary _idsToItems = new Dictionary(); + private readonly Dictionary _itemsToIds = new Dictionary(); private readonly HashSet _menus = new HashSet(); private bool _resetQueued; private int _nextId = 1; - private bool AppMenu = true; + private bool _appMenu = true; + public DBusMenuExporterImpl(Connection dbus, IntPtr xid) { _dbus = dbus; @@ -59,7 +60,7 @@ namespace Avalonia.FreeDesktop public DBusMenuExporterImpl(Connection dbus, ObjectPath path) { _dbus = dbus; - AppMenu = false; + _appMenu = false; ObjectPath = path; SetNativeMenu(new NativeMenu()); Init(); @@ -69,14 +70,14 @@ namespace Avalonia.FreeDesktop { try { - if (AppMenu) + if (_appMenu) { await _dbus.RegisterObjectAsync(this); - _registar = DBusHelper.Connection.CreateProxy( + _registrar = DBusHelper.Connection.CreateProxy( "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); if (!_disposed) - await _registar.RegisterWindowAsync(_xid, ObjectPath); + await _registrar.RegisterWindowAsync(_xid, ObjectPath); } else { @@ -102,7 +103,7 @@ namespace Avalonia.FreeDesktop _disposed = true; _dbus.UnregisterObject(this); // Fire and forget - _registar?.UnregisterWindowAsync(_xid); + _registrar?.UnregisterWindowAsync(_xid); } From afd720308727ad08f8f92294e50897d77fb941c5 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 7 Oct 2021 12:09:52 +0300 Subject: [PATCH 101/198] fix formatting --- .../ViewModels/ApplicationViewModel.cs | 6 ++--- .../Platform/ITrayIconImpl.cs | 3 +-- src/Avalonia.Controls/TrayIcon.cs | 10 ++++---- src/Avalonia.Native/TrayIconImpl.cs | 6 ++--- src/Avalonia.X11/X11TrayIconImpl.cs | 24 ++++++++++++------- .../Win32NativeToManagedMenuExporter.cs | 6 ++--- 6 files changed, 31 insertions(+), 24 deletions(-) diff --git a/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs index 6cd44eecaf..7eea7b0657 100644 --- a/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs @@ -10,17 +10,17 @@ namespace ControlCatalog.ViewModels { ExitCommand = MiniCommand.Create(() => { - if(Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) { lifetime.Shutdown(); } }); - + ToggleCommand = MiniCommand.Create(() => { }); } public MiniCommand ExitCommand { get; } - + public MiniCommand ToggleCommand { get; } } } diff --git a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs index 12a32ec64b..9768d149f0 100644 --- a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs +++ b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Controls; using Avalonia.Controls.Platform; #nullable enable @@ -21,7 +20,7 @@ namespace Avalonia.Platform /// /// Sets if the tray icon is visible or not. /// - void SetIsVisible (bool visible); + void SetIsVisible(bool visible); /// /// Gets the MenuExporter to allow native menus to be exported to the TrayIcon. diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index ad9a668cf2..6bfddfa877 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -30,22 +30,22 @@ namespace Avalonia.Controls } } - public TrayIcon () : this(PlatformManager.CreateTrayIcon()) + public TrayIcon() : this(PlatformManager.CreateTrayIcon()) { } - static TrayIcon () + static TrayIcon() { IconsProperty.Changed.Subscribe(args => { if (args.Sender is Application) { - if(args.OldValue.Value != null) + if (args.OldValue.Value != null) { RemoveIcons(args.OldValue.Value); } - if(args.NewValue.Value != null) + if (args.NewValue.Value != null) { args.NewValue.Value.CollectionChanged += Icons_CollectionChanged; } @@ -161,7 +161,7 @@ namespace Avalonia.Controls { base.OnPropertyChanged(change); - if(change.Property == IconProperty) + if (change.Property == IconProperty) { _impl?.SetIcon(Icon.PlatformImpl); } diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs index 951bbc496e..abcc61d950 100644 --- a/src/Avalonia.Native/TrayIconImpl.cs +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.Native internal class TrayIconImpl : ITrayIconImpl { private readonly IAvnTrayIcon _native; - + public TrayIconImpl(IAvaloniaNativeFactory factory) { _native = factory.CreateTrayIcon(); @@ -28,7 +28,7 @@ namespace Avalonia.Native public unsafe void SetIcon(IWindowIconImpl? icon) { - if(icon is null) + if (icon is null) { _native.SetIcon(null, IntPtr.Zero); } @@ -40,7 +40,7 @@ namespace Avalonia.Native var imageData = ms.ToArray(); - fixed(void* ptr = imageData) + fixed (void* ptr = imageData) { _native.SetIcon(ptr, new IntPtr(imageData.Length)); } diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 70b4587b08..3469bd7bcf 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -53,7 +53,8 @@ namespace Avalonia.X11 public async void CreateTrayIcon() { - if (_connection is null) return; + if (_connection is null) + return; try { @@ -68,7 +69,8 @@ namespace Avalonia.X11 "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); } - if (_statusNotifierWatcher is null) return; + if (_statusNotifierWatcher is null) + return; var pid = Process.GetCurrentProcess().Id; var tid = s_TrayIconInstanceId++; @@ -92,7 +94,8 @@ namespace Avalonia.X11 public async void DestroyTrayIcon() { - if (_connection is null) return; + if (_connection is null) + return; _connection.UnregisterObject(_statusNotifierItemDbusObj); await _connection.UnregisterServiceAsync(_sysTrayServiceName); _isActive = false; @@ -107,8 +110,10 @@ namespace Avalonia.X11 public void SetIcon(IWindowIconImpl? icon) { - if (_isDisposed) return; - if (!(icon is X11IconData x11icon)) return; + if (_isDisposed) + return; + if (!(icon is X11IconData x11icon)) + return; var w = (int)x11icon.Data[0]; var h = (int)x11icon.Data[1]; @@ -132,7 +137,8 @@ namespace Avalonia.X11 public void SetIsVisible(bool visible) { - if (_isDisposed || !_ctorFinished) return; + if (_isDisposed || !_ctorFinished) + return; if (visible & !_isActive) { @@ -147,7 +153,8 @@ namespace Avalonia.X11 public void SetToolTipText(string? text) { - if (_isDisposed || text is null) return; + if (_isDisposed || text is null) + return; _tooltipText = text; _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } @@ -258,7 +265,8 @@ namespace Avalonia.X11 public void SetTitleAndTooltip(string? text) { - if (text is null) return; + if (text is null) + return; _backingProperties.Id = text; _backingProperties.Category = "ApplicationStatus"; diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index f1c5791359..fa6f9927b5 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -21,13 +21,13 @@ namespace Avalonia.Win32 { if (menuItem is NativeMenuItemSeparator) { - yield return new MenuItem { Header = "-" }; + yield return new MenuItem { Header = "-" }; } else if (menuItem is NativeMenuItem item) { var newItem = new MenuItem { Header = item.Header, Icon = item.Icon, Command = item.Command, CommandParameter = item.CommandParameter }; - if(item.Menu != null) + if (item.Menu != null) { newItem.Items = Populate(item.Menu); } @@ -43,7 +43,7 @@ namespace Avalonia.Win32 public IEnumerable? GetMenu() { - if(_nativeMenu != null) + if (_nativeMenu != null) { return Populate(_nativeMenu); } From 7b0fbe6d3aef66b117baa1f6628dcf2f23921d83 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 7 Oct 2021 12:16:19 +0300 Subject: [PATCH 102/198] minor nits --- src/Avalonia.X11/X11TrayIconImpl.cs | 6 +++--- .../Avalonia.Win32/Win32NativeToManagedMenuExporter.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 3469bd7bcf..ea3653fc83 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -17,7 +17,7 @@ namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int s_TrayIconInstanceId; + private static int s_trayIconInstanceId; private readonly ObjectPath _dbusMenuPath; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private readonly Connection? _connection; @@ -62,7 +62,7 @@ namespace Avalonia.X11 "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); } - catch (Exception) + catch { Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, @@ -73,7 +73,7 @@ namespace Avalonia.X11 return; var pid = Process.GetCurrentProcess().Id; - var tid = s_TrayIconInstanceId++; + var tid = s_trayIconInstanceId++; _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index fa6f9927b5..69b8e91962 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -33,7 +33,7 @@ namespace Avalonia.Win32 } else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) { - newItem.Click += (_, __) => bridge.RaiseClicked(); + newItem.Click += (_, _) => bridge.RaiseClicked(); } yield return newItem; From 71616ac7f90344fa788271482add7578e10b69f1 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 7 Oct 2021 12:20:08 +0300 Subject: [PATCH 103/198] nit --- src/Avalonia.Controls/Platform/PlatformManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index 054f823d6d..e39f0b1e99 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.Platform { } - public static ITrayIconImpl? CreateTrayIcon () + public static ITrayIconImpl? CreateTrayIcon() { var platform = AvaloniaLocator.Current.GetService(); From 99d983499f5412febf07aafe2bf03872319b412b Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 7 Oct 2021 12:45:28 +0300 Subject: [PATCH 104/198] fix errors related to old sdk --- src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index 69b8e91962..fa6f9927b5 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -33,7 +33,7 @@ namespace Avalonia.Win32 } else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) { - newItem.Click += (_, _) => bridge.RaiseClicked(); + newItem.Click += (_, __) => bridge.RaiseClicked(); } yield return newItem; From 257edd40ff204499c4971156076f9b40aaf34d49 Mon Sep 17 00:00:00 2001 From: Adir Date: Thu, 7 Oct 2021 14:03:31 +0300 Subject: [PATCH 105/198] Changed CompositionBackdropCornerRadius to be nullable --- src/Windows/Avalonia.Win32/Win32Platform.cs | 5 +++-- .../WinRT/Composition/WinUICompositorConnection.cs | 14 +++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 9d56306c59..c84ccde653 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -92,9 +92,10 @@ namespace Avalonia /// /// When enabled, create rounded corner blur brushes - /// If set to zero the brushes will be created using default settings (sharp corners) + /// If set to null the brushes will be created using default settings (sharp corners) + /// This can be useful when you need a rounded-corner blurred Windows 10 app, or borderless Windows 11 app /// - public float CompositionBackdropCornerRadius { get; set; } + public float? CompositionBackdropCornerRadius { get; set; } } } diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 21de6f169b..57b0f71306 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -17,7 +17,9 @@ namespace Avalonia.Win32.WinRT.Composition { class WinUICompositorConnection : IRenderTimer { + private readonly float? _backdropCornerRadius; private readonly EglContext _syncContext; + private readonly ICompositionBrush _micaBrush; private ICompositor _compositor; private ICompositor2 _compositor2; private ICompositor5 _compositor5; @@ -27,11 +29,9 @@ namespace Avalonia.Win32.WinRT.Composition private EglPlatformOpenGlInterface _gl; private ICompositorDesktopInterop _compositorDesktopInterop; private ICompositionBrush _blurBrush; - private readonly ICompositionBrush _micaBrush; private object _pumpLock = new object(); - private readonly float _backdropCornerRadius; - public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock, float backdropCornerRadius) + public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock, float? backdropCornerRadius) { _gl = gl; _pumpLock = pumpLock; @@ -52,7 +52,7 @@ namespace Avalonia.Win32.WinRT.Composition public EglPlatformOpenGlInterface Egl => _gl; - static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle, float backdropCornerRadius) + static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle, float? backdropCornerRadius) { var tcs = new TaskCompletionSource(); var pumpLock = new object(); @@ -135,7 +135,7 @@ namespace Avalonia.Win32.WinRT.Composition } public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle, - float backdropCornerRadius) + float? backdropCornerRadius) { const int majorRequired = 10; const int buildRequired = 17134; @@ -247,10 +247,10 @@ namespace Avalonia.Win32.WinRT.Composition private ICompositionRoundedRectangleGeometry ClipVisual(params IVisual[] containerVisuals) { - if (_backdropCornerRadius == 0) + if (!_backdropCornerRadius.HasValue) return null; using var roundedRectangleGeometry = _compositor5.CreateRoundedRectangleGeometry(); - roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius, _backdropCornerRadius)); + roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius.Value, _backdropCornerRadius.Value)); using var compositor6 = _compositor.QueryInterface(); using var compositionGeometry = roundedRectangleGeometry From 88d94379fe4384e7d4eff577bbda2dba131a6c44 Mon Sep 17 00:00:00 2001 From: Lighto Date: Thu, 7 Oct 2021 23:24:42 +0300 Subject: [PATCH 106/198] Allow creating custom Datagrid columns (#6689) * Change BindingTarget of DataGridBoundColumn to public, this will allow custom columns * Changed modifier to protected --- src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 90401a00a2..97e247bdc6 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -133,7 +133,7 @@ namespace Avalonia.Controls protected abstract IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem); - internal AvaloniaProperty BindingTarget { get; set; } + protected AvaloniaProperty BindingTarget { get; set; } internal void SetHeaderFromBinding() { From b8059ef35fb0935d20fd8bc3b9c24024b1406e7c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 11 Oct 2021 10:56:48 +0100 Subject: [PATCH 107/198] implement non-client clicks on osx. --- native/Avalonia.Native/src/OSX/window.h | 1 + native/Avalonia.Native/src/OSX/window.mm | 26 +++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 3a54bd4b79..1dc091a48d 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -12,6 +12,7 @@ class WindowBaseImpl; -(AvnPixelSize) getPixelSize; -(AvnPlatformResizeReason) getResizeReason; -(void) setResizeReason:(AvnPlatformResizeReason)reason; ++ (AvnPoint)toAvnPoint:(CGPoint)p; @end @interface AutoFitContentView : NSView diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 7a6e7dc72f..d9b42e5ca3 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1541,7 +1541,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return pt; } -- (AvnPoint)toAvnPoint:(CGPoint)p ++ (AvnPoint)toAvnPoint:(CGPoint)p { AvnPoint result; @@ -1598,7 +1598,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; - auto avnPoint = [self toAvnPoint:localPoint]; + auto avnPoint = [AvnView toAvnPoint:localPoint]; auto point = [self translateLocalPoint:avnPoint]; AvnVector delta; @@ -1943,7 +1943,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info { auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; - auto avnPoint = [self toAvnPoint:localPoint]; + auto avnPoint = [AvnView toAvnPoint:localPoint]; auto point = [self translateLocalPoint:avnPoint]; auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; NSDragOperation nsop = [info draggingSourceOperationMask]; @@ -2376,6 +2376,26 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent->BaseEvents->PositionChanged(position); } } + +- (AvnPoint) translateLocalPoint:(AvnPoint)pt +{ + pt.Y = [self frame].size.height - pt.Y; + return pt; +} + +- (void)sendEvent:(NSEvent *)event +{ + if(event.type == NSEventTypeLeftMouseDown && _parent != nullptr) + { + auto avnPoint = [AvnView toAvnPoint:[event locationInWindow]]; + auto point = [self translateLocalPoint:avnPoint]; + AvnVector delta; + + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); + } + + [super sendEvent:event]; +} @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup From 00765d53b2eeed4550785b850acf82d71da3479c Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Mon, 11 Oct 2021 20:46:58 +0300 Subject: [PATCH 108/198] Fixed handled event misses in dev tools --- .../Diagnostics/Models/EventChainLink.cs | 2 +- .../Diagnostics/ViewModels/EventTreeNode.cs | 26 +++++++++++++++++++ .../Diagnostics/ViewModels/FiredEvent.cs | 5 ++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs index 4f493bdcc2..d986a11c45 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs @@ -29,7 +29,7 @@ namespace Avalonia.Diagnostics.Models } } - public bool Handled { get; } + public bool Handled { get; set; } public RoutingStrategies Route { get; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs index 65fd81cc78..a79816390d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs @@ -55,6 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels // FIXME: This leaks event handlers. Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true); + Event.RouteFinished.Subscribe(HandleRouteFinished); + _isRegistered = true; } } @@ -92,6 +94,30 @@ namespace Avalonia.Diagnostics.ViewModels else handler(); } + + private void HandleRouteFinished(RoutedEventArgs e) + { + if (!_isRegistered || IsEnabled == false) + return; + if (e.Source is IVisual v && BelongsToDevTool(v)) + return; + + var s = e.Source; + var handled = e.Handled; + var route = e.Route; + + void handler() + { + if (_currentEvent != null && handled) + { + var linkIndex = _currentEvent.EventChain.Count - 1; + var link = _currentEvent.EventChain[linkIndex]; + + link.Handled = true; + _currentEvent.HandledBy = link; + } + } + } private static bool BelongsToDevTool(IVisual v) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs index 32df2f8745..8069300922 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs @@ -63,8 +63,8 @@ namespace Avalonia.Diagnostics.ViewModels { if (EventChain.Count > 0) { - var prevLink = EventChain[EventChain.Count-1]; - + var prevLink = EventChain[EventChain.Count - 1]; + if (prevLink.Route != link.Route) { link.BeginsNewRoute = true; @@ -72,6 +72,7 @@ namespace Avalonia.Diagnostics.ViewModels } EventChain.Add(link); + if (HandledBy == null && link.Handled) HandledBy = link; } From b77978c5586ad40075a17f10c745f7152ff22fe3 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 12 Oct 2021 14:45:50 +0200 Subject: [PATCH 109/198] Simulate font weight bold and font style italic when a fallback is used --- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 6 +++++- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 10 +++++++++- src/Skia/Avalonia.Skia/PlatformRenderInterface.cs | 2 ++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 178ee8c544..6b560ac739 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -140,7 +140,11 @@ namespace Avalonia.Skia $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); } - return new GlyphTypefaceImpl(skTypeface); + var isFakeBold = (int)typeface.Weight >= 600 && !skTypeface.IsBold; + + var isFakeItalic = typeface.Style == FontStyle.Italic && !skTypeface.IsItalic; + + return new GlyphTypefaceImpl(skTypeface, isFakeBold, isFakeItalic); } } } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index ceccc481f9..9601fece25 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -10,7 +10,7 @@ namespace Avalonia.Skia { private bool _isDisposed; - public GlyphTypefaceImpl(SKTypeface typeface) + public GlyphTypefaceImpl(SKTypeface typeface, bool isFakeBold = false, bool isFakeItalic = false) { Typeface = typeface ?? throw new ArgumentNullException(nameof(typeface)); @@ -52,6 +52,10 @@ namespace Avalonia.Skia 0; IsFixedPitch = Typeface.IsFixedPitch; + + IsFakeBold = isFakeBold; + + IsFakeItalic = isFakeItalic; } public Face Face { get; } @@ -86,6 +90,10 @@ namespace Avalonia.Skia /// public bool IsFixedPitch { get; } + + public bool IsFakeBold { get; } + + public bool IsFakeItalic { get; } /// public ushort GetGlyph(uint codepoint) diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index e2175f1145..4130d38f17 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -217,6 +217,8 @@ namespace Avalonia.Skia s_font.Size = (float)glyphRun.FontRenderingEmSize; s_font.Typeface = typeface; + s_font.Embolden = glyphTypeface.IsFakeBold; + s_font.SkewX = glyphTypeface.IsFakeItalic ? -0.2f : 0; SKTextBlob textBlob; From fc3412383068643e342ea5e4cac4f683e01db68a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 13 Oct 2021 19:24:36 -0400 Subject: [PATCH 110/198] Disable WindowManagerAddShadowHint by default It makes more sense to be disabled by default. --- src/Avalonia.Controls/Primitives/Popup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index a5cdeefb0e..856bcd1079 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.Primitives public class Popup : Control, IVisualTreeHost, IPopupHostProvider { public static readonly StyledProperty WindowManagerAddShadowHintProperty = - AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), true); + AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), false); /// /// Defines the property. From 74c72f7aee00a6a616dd01b5b719af6b8e8459be Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 14 Oct 2021 18:00:23 +0300 Subject: [PATCH 111/198] [X11] Check for XOpenDisplay error _before_ trying to use display --- src/Avalonia.X11/X11Platform.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 5d80c860a7..d3aeefd088 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -52,11 +52,14 @@ namespace Avalonia.X11 XInitThreads(); Display = XOpenDisplay(IntPtr.Zero); + if (Display == IntPtr.Zero) + throw new Exception("XOpenDisplay failed"); DeferredDisplay = XOpenDisplay(IntPtr.Zero); + if (DeferredDisplay == IntPtr.Zero) + throw new Exception("XOpenDisplay failed"); + OrphanedWindow = XCreateSimpleWindow(Display, XDefaultRootWindow(Display), 0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero); - if (Display == IntPtr.Zero) - throw new Exception("XOpenDisplay failed"); XError.Init(); Info = new X11Info(Display, DeferredDisplay, useXim); From b4738926493db56ab99eb96b361a6b2c55487300 Mon Sep 17 00:00:00 2001 From: 0x90d <46010672+0x90d@users.noreply.github.com> Date: Fri, 15 Oct 2021 05:29:42 +0200 Subject: [PATCH 112/198] Update DataGridColumn.cs --- .../DataGridColumn.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 4ab2869138..07adac597f 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -653,6 +653,26 @@ namespace Avalonia.Controls return null; } + /// + /// Switches the current state of sort direction + /// + /// Clear the current sort direction instead + public void PerformSort(bool clear) + { + //InvokeProcessSort is already validating if sorting is possible + _headerCell?.InvokeProcessSort(clear ? Input.KeyModifiers.Control : Input.KeyModifiers.None); + } + + /// + /// Changes the sort direction of this column + /// + /// New sort direction + public void PerformSort(ListSortDirection direction) + { + //InvokeProcessSort is already validating if sorting is possible + _headerCell?.InvokeProcessSort(Input.KeyModifiers.None, direction); + } + /// /// When overridden in a derived class, causes the column cell being edited to revert to the unedited value. /// From 68a3f7fa975617fac4134b576b147fc028fede3d Mon Sep 17 00:00:00 2001 From: 0x90d <46010672+0x90d@users.noreply.github.com> Date: Fri, 15 Oct 2021 05:35:19 +0200 Subject: [PATCH 113/198] Update DataGridColumnHeader.cs --- .../DataGridColumnHeader.cs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 6f957497cb..85fd55800a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -201,21 +201,21 @@ namespace Avalonia.Controls handled = true; } - internal void InvokeProcessSort(KeyModifiers keyModifiers) + internal void InvokeProcessSort(KeyModifiers keyModifiers, ListSortDirection? forcedDirection = null) { Debug.Assert(OwningGrid != null); - if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers))) + if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers, forcedDirection))) { return; } if (OwningGrid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) { - Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers)); + Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers, forcedDirection)); } } //TODO GroupSorting - internal void ProcessSort(KeyModifiers keyModifiers) + internal void ProcessSort(KeyModifiers keyModifiers, ListSortDirection? forcedDirection = null) { // if we can sort: // - AllowUserToSortColumns and CanSort are true, and @@ -259,7 +259,14 @@ namespace Avalonia.Controls { if (sort != null) { - newSort = sort.SwitchSortDirection(); + if (forcedDirection == null || sort.Direction != forcedDirection) + { + newSort = sort.SwitchSortDirection(); + } + else + { + newSort = sort; + } // changing direction should not affect sort order, so we replace this column's // sort description instead of just adding it to the end of the collection @@ -276,7 +283,10 @@ namespace Avalonia.Controls } else if (OwningColumn.CustomSortComparer != null) { - newSort = DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer); + newSort = forcedDirection != null ? + DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer, forcedDirection.Value) : + DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer); + owningGrid.DataConnection.SortDescriptions.Add(newSort); } @@ -290,6 +300,10 @@ namespace Avalonia.Controls } newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture); + if (forcedDirection != null && newSort.Direction != forcedDirection) + { + newSort = newSort.SwitchSortDirection(); + } owningGrid.DataConnection.SortDescriptions.Add(newSort); } From d138924a95527bc55908a3b7374e9ec1517797d9 Mon Sep 17 00:00:00 2001 From: 0x90d <46010672+0x90d@users.noreply.github.com> Date: Fri, 15 Oct 2021 05:35:55 +0200 Subject: [PATCH 114/198] Update DataGridColumn.cs --- src/Avalonia.Controls.DataGrid/DataGridColumn.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 07adac597f..7ad06c194d 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -657,10 +657,10 @@ namespace Avalonia.Controls /// Switches the current state of sort direction /// /// Clear the current sort direction instead - public void PerformSort(bool clear) + public void PerformSort(bool? clear) { //InvokeProcessSort is already validating if sorting is possible - _headerCell?.InvokeProcessSort(clear ? Input.KeyModifiers.Control : Input.KeyModifiers.None); + _headerCell?.InvokeProcessSort(clear == true ? Input.KeyModifiers.Control : Input.KeyModifiers.None); } /// From e564a01663c1d538329f2052c1718063d696fc01 Mon Sep 17 00:00:00 2001 From: 0x90d <46010672+0x90d@users.noreply.github.com> Date: Fri, 15 Oct 2021 06:36:07 +0200 Subject: [PATCH 115/198] Update DataGridColumn.cs --- src/Avalonia.Controls.DataGrid/DataGridColumn.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 7ad06c194d..f275d7cc94 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -653,21 +653,29 @@ namespace Avalonia.Controls return null; } + /// + /// Clears the current sort direction + /// + public void ClearSort() + { + //InvokeProcessSort is already validating if sorting is possible + _headerCell?.InvokeProcessSort(Input.KeyModifiers.Control); + } + /// /// Switches the current state of sort direction /// - /// Clear the current sort direction instead - public void PerformSort(bool? clear) + public void Sort() { //InvokeProcessSort is already validating if sorting is possible - _headerCell?.InvokeProcessSort(clear == true ? Input.KeyModifiers.Control : Input.KeyModifiers.None); + _headerCell?.InvokeProcessSort(Input.KeyModifiers.None); } /// /// Changes the sort direction of this column /// /// New sort direction - public void PerformSort(ListSortDirection direction) + public void Sort(ListSortDirection direction) { //InvokeProcessSort is already validating if sorting is possible _headerCell?.InvokeProcessSort(Input.KeyModifiers.None, direction); From f6406cecf0d4f39d829f2fb192eff03ade26b50f Mon Sep 17 00:00:00 2001 From: 0x90d <46010672+0x90d@users.noreply.github.com> Date: Fri, 15 Oct 2021 06:47:09 +0200 Subject: [PATCH 116/198] Update DataGridColumn.cs --- src/Avalonia.Controls.DataGrid/DataGridColumn.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index f275d7cc94..5499257171 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -9,6 +9,7 @@ using Avalonia.VisualTree; using Avalonia.Collections; using Avalonia.Utilities; using System; +using System.ComponentModel; using System.Linq; using System.Diagnostics; using Avalonia.Controls.Utils; From 4e65b0296b9113e56d54b63577e6c56ce28860e6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Oct 2021 09:09:24 +0200 Subject: [PATCH 117/198] Added failing tests for #6729. --- .../ItemsRepeaterTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs new file mode 100644 index 0000000000..321676abc0 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs @@ -0,0 +1,24 @@ +using System.Collections.ObjectModel; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ItemsRepeaterTests + { + [Fact] + public void Can_Reassign_Items() + { + var target = new ItemsRepeater(); + target.Items = new ObservableCollection(); + target.Items = new ObservableCollection(); + } + + [Fact] + public void Can_Reassign_Items_To_Null() + { + var target = new ItemsRepeater(); + target.Items = new ObservableCollection(); + target.Items = null; + } + } +} From e2a8b56aad64a5da1962331648c75871eb111b96 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Oct 2021 09:18:12 +0200 Subject: [PATCH 118/198] Unsubscribe from ItemsSourceView before disposing it. --- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 01200e87e3..0ff8fcbd28 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -588,14 +588,14 @@ namespace Avalonia.Controls throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout."); } - ItemsSourceView?.Dispose(); - ItemsSourceView = newValue; - if (oldValue != null) { oldValue.CollectionChanged -= OnItemsSourceViewChanged; } + ItemsSourceView?.Dispose(); + ItemsSourceView = newValue; + if (newValue != null) { newValue.CollectionChanged += OnItemsSourceViewChanged; From 8322d4f428ee0b682f8663ac6157865b0a51e93e Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Fri, 15 Oct 2021 12:13:58 +0300 Subject: [PATCH 119/198] fix --- src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs b/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs index ea5dcdeeba..526be6e0f0 100644 --- a/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs +++ b/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs @@ -7,7 +7,7 @@ Mica } - public interface IBlurHost + internal interface IBlurHost { void SetBlur(BlurEffect enable); } From 2d14a049d7089d2cff8f3b003ca3b5bc6e4446e3 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Fri, 15 Oct 2021 13:05:07 +0300 Subject: [PATCH 120/198] add ctor to DrawingImage which accepts Drawing --- src/Avalonia.Visuals/Media/DrawingImage.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Visuals/Media/DrawingImage.cs b/src/Avalonia.Visuals/Media/DrawingImage.cs index 56c883014a..6fa8d397a5 100644 --- a/src/Avalonia.Visuals/Media/DrawingImage.cs +++ b/src/Avalonia.Visuals/Media/DrawingImage.cs @@ -11,6 +11,14 @@ namespace Avalonia.Media /// public class DrawingImage : AvaloniaObject, IImage, IAffectsRender { + public DrawingImage() + { + } + + public DrawingImage(Drawing drawing) + { + Drawing = drawing; + } /// /// Defines the property. /// From e775ce4b0d9456bbe060e82e5d0a14e5ad484955 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 15 Oct 2021 16:06:33 +0100 Subject: [PATCH 121/198] add missing START_COM_CALL --- native/Avalonia.Native/src/OSX/window.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index d9b42e5ca3..558cd1cc57 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -231,6 +231,8 @@ public: virtual HRESULT GetFrameSize(AvnSize* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) From e44e0308ff04936249b183db80a87b40295d054c Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 15 Oct 2021 17:12:46 +0200 Subject: [PATCH 122/198] fix: XML Comment --- src/Avalonia.X11/X11TrayIconImpl.cs | 8 ++++---- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index ea3653fc83..371ff75408 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -305,10 +305,10 @@ namespace Avalonia.X11 } [Dictionary] - /// This class is used by Tmds.Dbus to ferry properties - /// from the SNI spec. - /// Don't change this to actual C# properties since - /// Tmds.Dbus will get confused. + // This class is used by Tmds.Dbus to ferry properties + // from the SNI spec. + // Don't change this to actual C# properties since + // Tmds.Dbus will get confused. internal class StatusNotifierItemProperties { public string? Category; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index d812f6a059..7026e6d9ce 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -591,7 +591,7 @@ namespace Avalonia.Skia /// Configure paint wrapper for using gradient brush. /// /// Paint wrapper. - /// Target bound rect. + /// Target size. /// Gradient brush. private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush) { From a3dc8ca85badb8ea9f38f9a4abe764d11d632ad5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 15 Oct 2021 17:03:57 +0100 Subject: [PATCH 123/198] osx - handle restoring arrow cursor if app set non-standard cursor at edge of client area. --- native/Avalonia.Native/src/OSX/window.mm | 34 +++++++++++++++++++----- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 558cd1cc57..bd93de0e78 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -2387,13 +2387,35 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)sendEvent:(NSEvent *)event { - if(event.type == NSEventTypeLeftMouseDown && _parent != nullptr) + if(_parent != nullptr) { - auto avnPoint = [AvnView toAvnPoint:[event locationInWindow]]; - auto point = [self translateLocalPoint:avnPoint]; - AvnVector delta; - - _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); + switch(event.type) + { + case NSEventTypeLeftMouseDown: + { + auto avnPoint = [AvnView toAvnPoint:[event locationInWindow]]; + auto point = [self translateLocalPoint:avnPoint]; + AvnVector delta; + + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); + } + break; + + case NSEventTypeMouseEntered: + { + _parent->UpdateCursor(); + } + break; + + case NSEventTypeMouseExited: + { + [[NSCursor arrowCursor] set]; + } + break; + + default: + break; + } } [super sendEvent:event]; From b6e60b932576ba826feaa52e49b980c6f613265a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Fri, 15 Oct 2021 19:37:22 +0100 Subject: [PATCH 124/198] Changed X11 default cursor to XC_left_ptr. --- src/Avalonia.X11/X11CursorFactory.cs | 4 ++-- src/Avalonia.X11/X11Info.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index d677ababef..9c37de86bf 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -23,7 +23,7 @@ namespace Avalonia.X11 private static readonly Dictionary s_mapping = new Dictionary { - {StandardCursorType.Arrow, CursorFontShape.XC_top_left_arrow}, + {StandardCursorType.Arrow, CursorFontShape.XC_left_ptr}, {StandardCursorType.Cross, CursorFontShape.XC_cross}, {StandardCursorType.Hand, CursorFontShape.XC_hand2}, {StandardCursorType.Help, CursorFontShape.XC_question_arrow}, @@ -67,7 +67,7 @@ namespace Avalonia.X11 { handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] - : _cursors[CursorFontShape.XC_top_left_arrow]; + : _cursors[CursorFontShape.XC_left_ptr]; } return new CursorImpl(handle); } diff --git a/src/Avalonia.X11/X11Info.cs b/src/Avalonia.X11/X11Info.cs index 3bded4cce1..9920907601 100644 --- a/src/Avalonia.X11/X11Info.cs +++ b/src/Avalonia.X11/X11Info.cs @@ -42,7 +42,7 @@ namespace Avalonia.X11 DefaultScreen = XDefaultScreen(display); BlackPixel = XBlackPixel(display, DefaultScreen); RootWindow = XRootWindow(display, DefaultScreen); - DefaultCursor = XCreateFontCursor(display, CursorFontShape.XC_top_left_arrow); + DefaultCursor = XCreateFontCursor(display, CursorFontShape.XC_left_ptr); DefaultRootWindow = XDefaultRootWindow(display); Atoms = new X11Atoms(display); From fd34b52bba48e87c23c869c061964a8595584239 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 00:06:15 +0300 Subject: [PATCH 125/198] LineNode hit test --- .../Rendering/SceneGraph/LineNode.cs | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 54a9ff733d..3d5de96bf6 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -82,8 +83,32 @@ namespace Avalonia.Rendering.SceneGraph public override bool HitTest(Point p) { - // TODO: Implement line hit testing. - return false; + var a = P1; + var b = P2; + + //If dot1 or dot2 is negative, then the angle between the perpendicular and the segment is obtuse. + //The distance from a point to a straight line is defined as the + //length of the vector formed by the point and the closest point of the segment + + Vector ap = p - a; + var dot1 = Vector.Dot(b - a, ap); + + if (dot1 < 0) + return ap.Length <= Pen.Thickness/2; + + Vector bp = p - b; + var dot2 = Vector.Dot(a - b, bp); + + if(dot2 < 0) + return bp.Length <= Pen.Thickness/2; + + var bXaX = b.X - a.X; + var bYaY = b.Y - a.Y; + + var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / + (Math.Sqrt(Math.Pow(bXaX, 2) + Math.Pow(bYaY, 2))); + + return Math.Abs(distance) <= Pen.Thickness/2; } } } From ee84eae5cfcebda0f8559e1ed41aacf9cc5c468c Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 00:15:50 +0300 Subject: [PATCH 126/198] Spaces --- .../Rendering/SceneGraph/LineNode.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 3d5de96bf6..5d7df770aa 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -85,30 +85,30 @@ namespace Avalonia.Rendering.SceneGraph { var a = P1; var b = P2; - + //If dot1 or dot2 is negative, then the angle between the perpendicular and the segment is obtuse. //The distance from a point to a straight line is defined as the //length of the vector formed by the point and the closest point of the segment - + Vector ap = p - a; var dot1 = Vector.Dot(b - a, ap); - + if (dot1 < 0) - return ap.Length <= Pen.Thickness/2; + return ap.Length <= Pen.Thickness / 2; Vector bp = p - b; var dot2 = Vector.Dot(a - b, bp); - - if(dot2 < 0) - return bp.Length <= Pen.Thickness/2; + + if (dot2 < 0) + return bp.Length <= Pen.Thickness / 2; var bXaX = b.X - a.X; var bYaY = b.Y - a.Y; - + var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / (Math.Sqrt(Math.Pow(bXaX, 2) + Math.Pow(bYaY, 2))); - - return Math.Abs(distance) <= Pen.Thickness/2; + + return Math.Abs(distance) <= Pen.Thickness / 2; } } } From 22ff7af2ae1156ed32ebc981c232111feea88d5c Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 03:37:13 +0300 Subject: [PATCH 127/198] replace Math.Pow(x, 2) on x*x --- src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 5d7df770aa..19b751f66c 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -106,7 +106,7 @@ namespace Avalonia.Rendering.SceneGraph var bYaY = b.Y - a.Y; var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / - (Math.Sqrt(Math.Pow(bXaX, 2) + Math.Pow(bYaY, 2))); + (Math.Sqrt(bXaX * bXaX + bYaY * bYaY)); return Math.Abs(distance) <= Pen.Thickness / 2; } From 721e088911c03490be9b54550521cbbe43d268b1 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 03:37:34 +0300 Subject: [PATCH 128/198] LineNode tests --- .../Rendering/SceneGraph/LineNodeTests.cs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs new file mode 100644 index 0000000000..f065e7def0 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Rendering.SceneGraph; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph +{ + public class LineNodeTests + { + [Fact] + public void HitTest_Should_Be_True() + { + var lineNode = new LineNode( + Matrix.Identity, + new Pen(Brushes.Black, 3), + new Point(15, 15), + new Point(150, 150)); + + + List pointsInside = new() + { + new Point(14, 14), + new Point(15, 15), + new Point(32.1, 30), + new Point(30, 32.1), + new Point(150, 150), + new Point(151, 151), + }; + + foreach (var point in pointsInside) + { + Assert.True(lineNode.HitTest(point)); + } + } + + [Fact] + public void HitTest_Should_Be_False() + { + var lineNode = new LineNode( + Matrix.Identity, + new Pen(Brushes.Black, 3), + new Point(15, 15), + new Point(150, 150)); + + + List pointsOutside= new() + { + new Point(13.9, 13.9), + new Point(30, 32.2), + new Point(32.2, 30), + new Point(151.1, 151.1), + new Point(200, 200), + }; + + foreach (var point in pointsOutside) + { + Assert.False(lineNode.HitTest(point)); + } + } + } +} From ce21eee1554d7e31afff4c09e699af05a5fad577 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 03:45:25 +0300 Subject: [PATCH 129/198] replace target-typed object creation --- .../Rendering/SceneGraph/LineNodeTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs index f065e7def0..d4d4a29dfd 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs @@ -17,7 +17,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph new Point(150, 150)); - List pointsInside = new() + var pointsInside = new List() { new Point(14, 14), new Point(15, 15), @@ -43,7 +43,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph new Point(150, 150)); - List pointsOutside= new() + var pointsOutside= new List() { new Point(13.9, 13.9), new Point(30, 32.2), From 339ddca0271247c5e4edb46605f81fa5615d0bbd Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 13:04:41 +0300 Subject: [PATCH 130/198] Sharper angle of a LineNode in Tests --- .../Rendering/SceneGraph/LineNodeTests.cs | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs index d4d4a29dfd..2713e4460e 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs @@ -13,18 +13,17 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var lineNode = new LineNode( Matrix.Identity, new Pen(Brushes.Black, 3), - new Point(15, 15), - new Point(150, 150)); - - + new Point(15, 10), + new Point(150, 73)); + var pointsInside = new List() { - new Point(14, 14), - new Point(15, 15), - new Point(32.1, 30), - new Point(30, 32.1), - new Point(150, 150), - new Point(151, 151), + new Point(14, 8.9), + new Point(15, 10), + new Point(30, 15.5), + new Point(30, 18.5), + new Point(150, 73), + new Point(151, 71.9), }; foreach (var point in pointsInside) @@ -39,17 +38,17 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var lineNode = new LineNode( Matrix.Identity, new Pen(Brushes.Black, 3), - new Point(15, 15), - new Point(150, 150)); - + new Point(15, 10), + new Point(150, 73)); - var pointsOutside= new List() + var pointsOutside = new List() { - new Point(13.9, 13.9), - new Point(30, 32.2), - new Point(32.2, 30), - new Point(151.1, 151.1), - new Point(200, 200), + new Point(14, 8), + new Point(14, 8.8), + new Point(30, 15.3), + new Point(30, 18.7), + new Point(151, 71.8), + new Point(155, 75), }; foreach (var point in pointsOutside) From 0a0bba8de4489a343c25274155e1d23c05b7cd41 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 13:06:49 +0300 Subject: [PATCH 131/198] Check transform invert and bounds test for LineNode hitTest --- .../Rendering/SceneGraph/LineNode.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 19b751f66c..3dc6d5f50e 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -83,6 +83,20 @@ namespace Avalonia.Rendering.SceneGraph public override bool HitTest(Point p) { + if (!Transform.HasInverse) + return false; + + p *= Transform.Invert(); + + var halfThickness = Pen.Thickness / 2; + var minX = Math.Min(P1.X, P2.X) - halfThickness; + var maxX = Math.Max(P1.X, P2.X) + halfThickness; + var minY = Math.Min(P1.Y, P2.Y) - halfThickness; + var maxY = Math.Max(P1.Y, P2.Y) + halfThickness; + + if (p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY) + return false; + var a = P1; var b = P2; @@ -100,7 +114,7 @@ namespace Avalonia.Rendering.SceneGraph var dot2 = Vector.Dot(a - b, bp); if (dot2 < 0) - return bp.Length <= Pen.Thickness / 2; + return bp.Length <= halfThickness; var bXaX = b.X - a.X; var bYaY = b.Y - a.Y; @@ -108,7 +122,7 @@ namespace Avalonia.Rendering.SceneGraph var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / (Math.Sqrt(bXaX * bXaX + bYaY * bYaY)); - return Math.Abs(distance) <= Pen.Thickness / 2; + return Math.Abs(distance) <= halfThickness; } } } From 5407e6293f0a2e9f1bbf1859966437fa86c5d1d5 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 13:07:06 +0300 Subject: [PATCH 132/198] spaces --- .../Rendering/SceneGraph/LineNodeTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs index 2713e4460e..56f2b03932 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs @@ -11,11 +11,11 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void HitTest_Should_Be_True() { var lineNode = new LineNode( - Matrix.Identity, + Matrix.Identity, new Pen(Brushes.Black, 3), new Point(15, 10), new Point(150, 73)); - + var pointsInside = new List() { new Point(14, 8.9), @@ -28,15 +28,15 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph foreach (var point in pointsInside) { - Assert.True(lineNode.HitTest(point)); + Assert.True(lineNode.HitTest(point)); } } - + [Fact] public void HitTest_Should_Be_False() { var lineNode = new LineNode( - Matrix.Identity, + Matrix.Identity, new Pen(Brushes.Black, 3), new Point(15, 10), new Point(150, 73)); @@ -53,7 +53,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph foreach (var point in pointsOutside) { - Assert.False(lineNode.HitTest(point)); + Assert.False(lineNode.HitTest(point)); } } } From 1a8ec1ce0ef9c7a9d03f4b84c9511bbcf6261400 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Oct 2021 22:54:09 -0400 Subject: [PATCH 133/198] Fix DataGrid headers horizontal scrolling --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index fea02dabf4..10c7c16488 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2223,6 +2223,7 @@ namespace Avalonia.Controls if (IsEnabled && DisplayData.NumDisplayedScrollingElements > 0) { var handled = false; + var ignoreInvalidate = false; var scrollHeight = 0d; // Vertical scroll handling @@ -2252,8 +2253,7 @@ namespace Avalonia.Controls // Horizontal scroll handling if (delta.X != 0) { - var originalHorizontalOffset = HorizontalOffset; - var horizontalOffset = originalHorizontalOffset - delta.X; + var horizontalOffset = HorizontalOffset - delta.X; var widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth); if (horizontalOffset < 0) @@ -2265,16 +2265,20 @@ namespace Avalonia.Controls horizontalOffset = widthNotVisible; } - if (horizontalOffset != originalHorizontalOffset) + if (UpdateHorizontalOffset(horizontalOffset)) { - HorizontalOffset = horizontalOffset; + // We don't need to invalidate once again after UpdateHorizontalOffset. + ignoreInvalidate = true; handled = true; } } if (handled) { - InvalidateRowsMeasure(invalidateIndividualElements: false); + if (!ignoreInvalidate) + { + InvalidateRowsMeasure(invalidateIndividualElements: false); + } return true; } } @@ -2932,7 +2936,7 @@ namespace Avalonia.Controls return SetCurrentCellCore(columnIndex, slot, commitEdit: true, endRowEdit: true); } - internal void UpdateHorizontalOffset(double newValue) + internal bool UpdateHorizontalOffset(double newValue) { if (HorizontalOffset != newValue) { @@ -2940,7 +2944,9 @@ namespace Avalonia.Controls InvalidateColumnHeadersMeasure(); InvalidateRowsMeasure(true); + return true; } + return false; } internal bool UpdateSelectionAndCurrency(int columnIndex, int slot, DataGridSelectionAction action, bool scrollIntoView) From 9f5f0aa84d082add0ebca3babcd2732c1c83112b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Oct 2021 23:35:27 -0400 Subject: [PATCH 134/198] Do not inherit text properties in Popup --- samples/ControlCatalog/Pages/TextBoxPage.xaml | 10 +++++++++- src/Avalonia.Themes.Default/OverlayPopupHost.xaml | 9 ++++++++- src/Avalonia.Themes.Default/PopupRoot.xaml | 9 ++++++++- .../Controls/OverlayPopupHost.xaml | 5 +++++ src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml | 5 +++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index f631c40eb1..233b309caf 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -11,7 +11,15 @@ HorizontalAlignment="Center" Spacing="16"> - + + + + Custom context flyout + + + + + ListBox Hosts a collection of ListBoxItem. + Each 2nd item is highlighted Multiple diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 55645d4dbb..9c86aeb0c8 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -13,6 +13,7 @@ using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; +using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -21,7 +22,7 @@ namespace Avalonia.Controls /// Displays a collection of items. /// [PseudoClasses(":empty", ":singleitem")] - public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener + public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider { /// /// The default value for the property. @@ -506,5 +507,21 @@ namespace Avalonia.Controls return null; } + + (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + { + if (Presenter is IChildIndexProvider innerProvider) + { + return innerProvider.GetChildIndex(child); + } + + if (child is IControl control) + { + var index = ItemContainerGenerator.IndexFromContainer(control); + return (index, ItemCount); + } + + return (-1, ItemCount); + } } } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index b7eeb065da..7a3e93ffc2 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -2,8 +2,12 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; + +using Avalonia.Controls.Presenters; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -14,7 +18,7 @@ namespace Avalonia.Controls /// Controls can be added to a by adding them to its /// collection. All children are layed out to fill the panel. /// - public class Panel : Control, IPanel + public class Panel : Control, IPanel, IChildIndexProvider { /// /// Defines the property. @@ -160,5 +164,16 @@ namespace Avalonia.Controls var panel = control?.VisualParent as TPanel; panel?.InvalidateMeasure(); } + + (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + { + if (child is IControl control) + { + var index = Children.IndexOf(control); + return (index, Children.Count); + } + + return (-1, Children.Count); + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 52f173fc71..cf5fb8ac42 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -5,6 +5,7 @@ using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; +using Avalonia.LogicalTree; using Avalonia.Styling; namespace Avalonia.Controls.Presenters @@ -12,7 +13,7 @@ namespace Avalonia.Controls.Presenters /// /// Base class for controls that present items inside an . /// - public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl + public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl, IChildIndexProvider { /// /// Defines the property. @@ -248,5 +249,27 @@ namespace Avalonia.Controls.Presenters { (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this); } + + (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + { + int? totalCount = null; + if (Items.TryGetCountFast(out var count)) + { + totalCount = count; + } + + if (child is IControl control) + { + + if (ItemContainerGenerator is { } generator) + { + var index = ItemContainerGenerator.IndexFromContainer(control); + + return (index, totalCount); + } + } + + return (-1, totalCount); + } } } diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 9614d079d9..fa5a09e245 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs @@ -12,23 +12,36 @@ namespace Avalonia.Controls.Utils return items.IndexOf(item) != -1; } - public static int Count(this IEnumerable items) + public static bool TryGetCountFast(this IEnumerable items, out int count) { if (items != null) { if (items is ICollection collection) { - return collection.Count; + count = collection.Count; + return true; } else if (items is IReadOnlyCollection readOnly) { - return readOnly.Count; - } - else - { - return Enumerable.Count(items.Cast()); + count = readOnly.Count; + return true; } } + + count = 0; + return false; + } + + public static int Count(this IEnumerable items) + { + if (TryGetCountFast(items, out var count)) + { + return count; + } + else if (items != null) + { + return Enumerable.Count(items.Cast()); + } else { return 0; diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs new file mode 100644 index 0000000000..a6b91dea5f --- /dev/null +++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs @@ -0,0 +1,134 @@ +#nullable enable +using System; +using System.Text; + +using Avalonia.LogicalTree; + +namespace Avalonia.Styling +{ + public interface IChildIndexProvider + { + (int Index, int? TotalCount) GetChildIndex(ILogical child); + } + + public class NthLastChildSelector : NthChildSelector + { + public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) + { + } + } + + public class NthChildSelector : Selector + { + private const string NthChildSelectorName = "nth-child"; + private const string NthLastChildSelectorName = "nth-last-child"; + private readonly Selector? _previous; + private readonly bool _reversed; + + internal protected NthChildSelector(Selector? previous, int step, int offset, bool reversed) + { + _previous = previous; + Step = step; + Offset = offset; + _reversed = reversed; + } + + public NthChildSelector(Selector? previous, int step, int offset) + : this(previous, step, offset, false) + { + + } + + public override bool InTemplate => _previous?.InTemplate ?? false; + + public override bool IsCombinator => false; + + public override Type? TargetType => _previous?.TargetType; + + public int Step { get; } + public int Offset { get; } + + protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + { + var logical = (ILogical)control; + var controlParent = logical.LogicalParent; + + if (controlParent is IChildIndexProvider childIndexProvider) + { + var (index, totalCount) = childIndexProvider.GetChildIndex(logical); + if (index < 0) + { + return SelectorMatch.NeverThisInstance; + } + + if (_reversed) + { + if (totalCount is int totalCountValue) + { + index = totalCountValue - index; + } + else + { + return SelectorMatch.NeverThisInstance; + } + } + else + { + // nth child index is 1-based + index += 1; + } + + + var n = Math.Sign(Step); + + var diff = index - Offset; + var match = diff == 0 || (Math.Sign(diff) == n && diff % Step == 0); + + return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; + } + else + { + return SelectorMatch.NeverThisInstance; + } + + } + + protected override Selector? MovePrevious() => _previous; + + public override string ToString() + { + var expectedCapacity = NthLastChildSelectorName.Length + 8; + var stringBuilder = new StringBuilder(_previous?.ToString(), expectedCapacity); + + stringBuilder.Append(':'); + stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName); + stringBuilder.Append('('); + + var hasStep = false; + if (Step != 0) + { + hasStep = true; + stringBuilder.Append(Step); + stringBuilder.Append('n'); + } + + if (Offset > 0) + { + if (hasStep) + { + stringBuilder.Append('+'); + } + stringBuilder.Append(Offset); + } + else if (Offset < 0) + { + stringBuilder.Append('-'); + stringBuilder.Append(-Offset); + } + + stringBuilder.Append(')'); + + return stringBuilder.ToString(); + } + } +} diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index 762ed7b58c..0bccccbd7c 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -123,6 +123,16 @@ namespace Avalonia.Styling return new NotSelector(previous, argument); } + public static Selector NthChild(this Selector previous, int step, int offset) + { + return new NthChildSelector(previous, step, offset); + } + + public static Selector NthLastChild(this Selector previous, int step, int offset) + { + return new NthLastChildSelector(previous, step, offset); + } + /// /// Returns a selector which matches a type. /// diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index b81d25d613..dfabd66d17 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -97,6 +97,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers case SelectorGrammar.NotSyntax not: result = new XamlIlNotSelector(result, Create(not.Argument, typeResolver)); break; + case SelectorGrammar.NthChildSyntax nth: + result = new XamlIlNthChildSelector(result, nth.Step, nth.Offset, XamlIlNthChildSelector.SelectorType.NthChild); + break; + case SelectorGrammar.NthLastChildSyntax nth: + result = new XamlIlNthChildSelector(result, nth.Step, nth.Offset, XamlIlNthChildSelector.SelectorType.NthLastChild); + break; case SelectorGrammar.CommaSyntax comma: if (results == null) results = new XamlIlOrSelectorNode(node, selectorType); @@ -273,6 +279,35 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } + class XamlIlNthChildSelector : XamlIlSelectorNode + { + private readonly int _step; + private readonly int _offset; + private readonly SelectorType _type; + + public enum SelectorType + { + NthChild, + NthLastChild + } + + public XamlIlNthChildSelector(XamlIlSelectorNode previous, int step, int offset, SelectorType type) : base(previous) + { + _step = step; + _offset = offset; + _type = type; + } + + public override IXamlType TargetType => Previous?.TargetType; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldc_I4(_step); + codeGen.Ldc_I4(_offset); + EmitCall(context, codeGen, + m => m.Name == _type.ToString() && m.Parameters.Count == 3); + } + } + class XamlIlPropertyEqualsSelector : XamlIlSelectorNode { public XamlIlPropertyEqualsSelector(XamlIlSelectorNode previous, diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 9d03341f92..56e64329b7 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -160,11 +160,13 @@ namespace Avalonia.Markup.Parsers if (identifier.IsEmpty) { - throw new ExpressionParseException(r.Position, "Expected class name or is selector after ':'."); + throw new ExpressionParseException(r.Position, "Expected class name, is, nth-child or nth-last-child selector after ':'."); } const string IsKeyword = "is"; const string NotKeyword = "not"; + const string NthChildKeyword = "nth-child"; + const string NthLastChildKeyword = "nth-last-child"; if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('(')) { @@ -181,6 +183,20 @@ namespace Avalonia.Markup.Parsers var syntax = new NotSyntax { Argument = argument }; return (State.Middle, syntax); } + if (identifier.SequenceEqual(NthChildKeyword.AsSpan()) && r.TakeIf('(')) + { + var (step, offset) = ParseNthChildArguments(ref r); + + var syntax = new NthChildSyntax { Step = step, Offset = offset }; + return (State.Middle, syntax); + } + if (identifier.SequenceEqual(NthLastChildKeyword.AsSpan()) && r.TakeIf('(')) + { + var (step, offset) = ParseNthChildArguments(ref r); + + var syntax = new NthLastChildSyntax { Step = step, Offset = offset }; + return (State.Middle, syntax); + } else { return ( @@ -191,7 +207,6 @@ namespace Avalonia.Markup.Parsers }); } } - private static (State, ISyntax?) ParseTraversal(ref CharacterReader r) { r.SkipWhitespace(); @@ -302,6 +317,70 @@ namespace Avalonia.Markup.Parsers return syntax; } + private static (int step, int offset) ParseNthChildArguments(ref CharacterReader r) + { + int step = 0; + int offset = 0; + + if (r.Peek == 'o') + { + var constArg = r.TakeUntil(')').ToString().Trim(); + if (constArg.Equals("odd", StringComparison.Ordinal)) + { + step = 2; + offset = 1; + } + else + { + throw new ExpressionParseException(r.Position, $"Expected nth-child(odd). Actual '{constArg}'."); + } + } + else if (r.Peek == 'e') + { + var constArg = r.TakeUntil(')').ToString().Trim(); + if (constArg.Equals("even", StringComparison.Ordinal)) + { + step = 2; + offset = 0; + } + else + { + throw new ExpressionParseException(r.Position, $"Expected nth-child(even). Actual '{constArg}'."); + } + } + else + { + var stepOrOffsetSpan = r.TakeWhile(c => c != ')' && c != 'n'); + if (!int.TryParse(stepOrOffsetSpan.ToString().Trim(), out var stepOrOffset)) + { + throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step or offset value. Integer was expected."); + } + + if (r.Peek == ')') + { + step = 0; + offset = stepOrOffset; + } + else + { + step = stepOrOffset; + + r.Skip(1); // skip 'n' + var offsetSpan = r.TakeUntil(')').TrimStart(); + + if (offsetSpan.Length != 0 + && !int.TryParse(offsetSpan.ToString().Trim(), out offset)) + { + throw new ExpressionParseException(r.Position, "Couldn't parse nth-child offset value. Integer was expected."); + } + } + } + + Expect(ref r, ')'); + + return (step, offset); + } + private static void Expect(ref CharacterReader r, char c) { if (r.End) @@ -419,6 +498,28 @@ namespace Avalonia.Markup.Parsers } } + public class NthChildSyntax : ISyntax + { + public int Offset { get; set; } + public int Step { get; set; } + + public override bool Equals(object? obj) + { + return (obj is NthChildSyntax nth) && nth.Offset == Offset && nth.Step == Step; + } + } + + public class NthLastChildSyntax : ISyntax + { + public int Offset { get; set; } + public int Step { get; set; } + + public override bool Equals(object? obj) + { + return (obj is NthLastChildSyntax nth) && nth.Offset == Offset && nth.Step == Step; + } + } + public class CommaSyntax : ISyntax { public override bool Equals(object? obj) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index 92ba744ee1..11fb287d46 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -104,6 +104,12 @@ namespace Avalonia.Markup.Parsers case SelectorGrammar.NotSyntax not: result = result.Not(x => Create(not.Argument)); break; + case SelectorGrammar.NthChildSyntax nth: + result = result.NthChild(nth.Step, nth.Offset); + break; + case SelectorGrammar.NthLastChildSyntax nth: + result = result.NthLastChild(nth.Step, nth.Offset); + break; case SelectorGrammar.CommaSyntax comma: if (results == null) { diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 03f1120796..543d44c492 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -236,6 +236,96 @@ namespace Avalonia.Markup.UnitTests.Parsers result); } + [Fact] + public void OfType_NthChild() + { + var result = SelectorGrammar.Parse("Button:nth-child(2n+1)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NthChildSyntax() + { + Step = 2, + Offset = 1 + } + }, + result); + } + + [Fact] + public void OfType_NthChild_Without_Offset() + { + var result = SelectorGrammar.Parse("Button:nth-child(2147483647n)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NthChildSyntax() + { + Step = int.MaxValue, + Offset = 0 + } + }, + result); + } + + [Fact] + public void OfType_NthLastChild() + { + var result = SelectorGrammar.Parse("Button:nth-last-child(2n+1)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NthLastChildSyntax() + { + Step = 2, + Offset = 1 + } + }, + result); + } + + [Fact] + public void OfType_NthChild_Odd() + { + var result = SelectorGrammar.Parse("Button:nth-child(odd)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NthChildSyntax() + { + Step = 2, + Offset = 1 + } + }, + result); + } + + [Fact] + public void OfType_NthChild_Even() + { + var result = SelectorGrammar.Parse("Button:nth-child(even)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NthChildSyntax() + { + Step = 2, + Offset = 0 + } + }, + result); + } + [Fact] public void Is_Descendent_Not_OfType_Class() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 06b494c3d8..3824b79708 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -267,6 +267,65 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void Style_Can_Use_NthChild_Selector() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var b1 = window.FindControl("b1"); + var b2 = window.FindControl("b2"); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)b1.Background).Color); + Assert.Null(b2.Background); + } + } + + [Fact] + public void Style_Can_Use_NthChild_Selector_After_Reoder() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + + var parent = window.FindControl("parent"); + var b1 = window.FindControl("b1"); + var b2 = window.FindControl("b2"); + + parent.Children.Remove(b1); + parent.Children.Add(b1); + + Assert.Null(b1.Background); + Assert.Equal(Colors.Red, ((ISolidColorBrush)b2.Background).Color); + } + } + [Fact] public void Style_Can_Use_Or_Selector_1() { diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs new file mode 100644 index 0000000000..b72e980821 --- /dev/null +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -0,0 +1,217 @@ +using Avalonia.Controls; +using Xunit; + +namespace Avalonia.Styling.UnitTests +{ + public class SelectorTests_NthChild + { + [Theory] + [InlineData(2, 0, ":nth-child(2n)")] + [InlineData(2, 1, ":nth-child(2n+1)")] + [InlineData(1, 0, ":nth-child(1n)")] + [InlineData(4, -1, ":nth-child(4n-1)")] + [InlineData(0, 1, ":nth-child(1)")] + [InlineData(0, -1, ":nth-child(-1)")] + [InlineData(int.MaxValue, int.MinValue + 1, ":nth-child(2147483647n-2147483647)")] + public void Not_Selector_Should_Have_Correct_String_Representation(int step, int offset, string expected) + { + var target = default(Selector).NthChild(step, offset); + + Assert.Equal(expected, target.ToString()); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(2, 0); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(2, 1); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Negative_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(4, -1); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Singular_Step() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(1, 2); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(1, -1); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(0, 2); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(0, -2); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Previous_Selector() + { + Border b1, b2; + Button b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new Control[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Button(), + b4 = new Button() + }); + + var previous = default(Selector).OfType(); + var target = previous.NthChild(2, 0); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + { + Border b1; + var contentControl = new ContentControl(); + contentControl.Content = b1 = new Border(); + + var target = default(Selector).NthChild(1, 0); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + } + + [Fact] + public void Returns_Correct_TargetType() + { + var target = new NthChildSelector(default(Selector).OfType(), 1, 0); + + Assert.Equal(typeof(Control1), target.TargetType); + } + + public class Control1 : Control + { + } + } +} diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs new file mode 100644 index 0000000000..3698e07d3e --- /dev/null +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs @@ -0,0 +1,217 @@ +using Avalonia.Controls; +using Xunit; + +namespace Avalonia.Styling.UnitTests +{ + public class SelectorTests_NthLastChild + { + [Theory] + [InlineData(2, 0, ":nth-last-child(2n)")] + [InlineData(2, 1, ":nth-last-child(2n+1)")] + [InlineData(1, 0, ":nth-last-child(1n)")] + [InlineData(4, -1, ":nth-last-child(4n-1)")] + [InlineData(0, 1, ":nth-last-child(1)")] + [InlineData(0, -1, ":nth-last-child(-1)")] + [InlineData(int.MaxValue, int.MinValue + 1, ":nth-last-child(2147483647n-2147483647)")] + public void Not_Selector_Should_Have_Correct_String_Representation(int step, int offset, string expected) + { + var target = default(Selector).NthLastChild(step, offset); + + Assert.Equal(expected, target.ToString()); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(2, 0); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(2, 1); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Negative_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(4, -1); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Singular_Step() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(1, 2); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(1, -2); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(0, 2); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(0, -2); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Previous_Selector() + { + Border b1, b2; + Button b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new Control[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Button(), + b4 = new Button() + }); + + var previous = default(Selector).OfType(); + var target = previous.NthLastChild(2, 0); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + { + Border b1; + var contentControl = new ContentControl(); + contentControl.Content = b1 = new Border(); + + var target = default(Selector).NthLastChild(1, 0); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + } + + [Fact] + public void Returns_Correct_TargetType() + { + var target = new NthLastChildSelector(default(Selector).OfType(), 1, 0); + + Assert.Equal(typeof(Control1), target.TargetType); + } + + public class Control1 : Control + { + } + } +} From e5ca5c38e8c6cff3f0fd6494578b83a8f4df6d22 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 10 Sep 2021 02:06:02 -0400 Subject: [PATCH 147/198] Update IChildIndexProvider interface with ChildIndexChanged and implement in on items controls --- .../Pages/ItemsRepeaterPage.xaml | 19 ++- src/Avalonia.Controls/ItemsControl.cs | 39 ++++-- src/Avalonia.Controls/Panel.cs | 21 ++-- .../Presenters/ItemsPresenterBase.cs | 43 ++++--- .../Repeater/ItemsRepeater.cs | 29 ++++- .../LogicalTree/ChildIndexChangedEventArgs.cs | 23 ++++ .../LogicalTree/IChildIndexProvider.cs | 14 +++ .../Styling/Activators/NthChildActivator.cs | 56 +++++++++ .../Styling/NthChildSelector.cs | 70 +++++------ .../Styling/NthLastChildSelector.cs | 11 ++ .../Xaml/StyleTests.cs | 119 +++++++++++++++++- .../SelectorTests_NthChild.cs | 85 +++++++------ .../SelectorTests_NthLastChild.cs | 84 +++++++------ .../StyleActivatorExtensions.cs | 7 +- 14 files changed, 456 insertions(+), 164 deletions(-) create mode 100644 src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs create mode 100644 src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs create mode 100644 src/Avalonia.Styling/Styling/NthLastChildSelector.cs diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 392ccb57c3..93f3c33434 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -1,17 +1,28 @@ + + + + + - - diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 9c86aeb0c8..7b28335a6d 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -57,6 +57,7 @@ namespace Avalonia.Controls private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; + private EventHandler _childIndexChanged; /// /// Initializes static members of the class. @@ -146,11 +147,30 @@ namespace Avalonia.Controls protected set; } + int? IChildIndexProvider.TotalCount => (Presenter as IChildIndexProvider)?.TotalCount ?? ItemCount; + + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter) { + if (Presenter is IChildIndexProvider oldInnerProvider) + { + oldInnerProvider.ChildIndexChanged -= PresenterChildIndexChanged; + } + Presenter = presenter; ItemContainerGenerator.Clear(); + + if (Presenter is IChildIndexProvider innerProvider) + { + innerProvider.ChildIndexChanged += PresenterChildIndexChanged; + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); + } } void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) @@ -508,20 +528,15 @@ namespace Avalonia.Controls return null; } - (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + private void PresenterChildIndexChanged(object sender, ChildIndexChangedEventArgs e) { - if (Presenter is IChildIndexProvider innerProvider) - { - return innerProvider.GetChildIndex(child); - } - - if (child is IControl control) - { - var index = ItemContainerGenerator.IndexFromContainer(control); - return (index, ItemCount); - } + _childIndexChanged?.Invoke(this, e); + } - return (-1, ItemCount); + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return Presenter is IChildIndexProvider innerProvider + ? innerProvider.GetChildIndex(child) : -1; } } } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 7a3e93ffc2..9c93126506 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -34,6 +34,8 @@ namespace Avalonia.Controls AffectsRender(BackgroundProperty); } + private EventHandler _childIndexChanged; + /// /// Initializes a new instance of the class. /// @@ -57,6 +59,14 @@ namespace Avalonia.Controls set { SetValue(BackgroundProperty, value); } } + int? IChildIndexProvider.TotalCount => Children.Count; + + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// /// Renders the visual to a . /// @@ -141,6 +151,7 @@ namespace Avalonia.Controls throw new NotSupportedException(); } + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); InvalidateMeasureOnChildrenChanged(); } @@ -165,15 +176,9 @@ namespace Avalonia.Controls panel?.InvalidateMeasure(); } - (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + int IChildIndexProvider.GetChildIndex(ILogical child) { - if (child is IControl control) - { - var index = Children.IndexOf(control); - return (index, Children.Count); - } - - return (-1, Children.Count); + return child is IControl control ? Children.IndexOf(control) : -1; } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index cf5fb8ac42..d58ef2e510 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -1,6 +1,8 @@ using System; using System.Collections; using System.Collections.Specialized; +using System.Linq; + using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; @@ -37,6 +39,7 @@ namespace Avalonia.Controls.Presenters private IDisposable _itemsSubscription; private bool _createdPanel; private IItemContainerGenerator _generator; + private EventHandler _childIndexChanged; /// /// Initializes static members of the class. @@ -130,6 +133,14 @@ namespace Avalonia.Controls.Presenters protected bool IsHosted => TemplatedParent is IItemsPresenterHost; + int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : null; + + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// public override sealed void ApplyTemplate() { @@ -170,9 +181,21 @@ namespace Avalonia.Controls.Presenters result.ItemTemplate = ItemTemplate; } + result.Materialized += ContainerActionHandler; + result.Dematerialized += ContainerActionHandler; + result.Recycled += ContainerActionHandler; + return result; } + private void ContainerActionHandler(object sender, ItemContainerEventArgs e) + { + for (var i = 0; i < e.Containers.Count; i++) + { + _childIndexChanged?.Invoke(sender, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl)); + } + } + /// protected override Size MeasureOverride(Size availableSize) { @@ -250,26 +273,16 @@ namespace Avalonia.Controls.Presenters (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this); } - (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + int IChildIndexProvider.GetChildIndex(ILogical child) { - int? totalCount = null; - if (Items.TryGetCountFast(out var count)) - { - totalCount = count; - } - - if (child is IControl control) + if (child is IControl control && ItemContainerGenerator is { } generator) { + var index = ItemContainerGenerator.IndexFromContainer(control); - if (ItemContainerGenerator is { } generator) - { - var index = ItemContainerGenerator.IndexFromContainer(control); - - return (index, totalCount); - } + return index; } - return (-1, totalCount); + return -1; } } } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 0ff8fcbd28..6d89a70670 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -6,10 +6,13 @@ using System; using System.Collections; using System.Collections.Specialized; + using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.LogicalTree; +using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -19,7 +22,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel + public class ItemsRepeater : Panel, IChildIndexProvider { /// /// Defines the property. @@ -61,8 +64,9 @@ namespace Avalonia.Controls private readonly ViewportManager _viewportManager; private IEnumerable _items; private VirtualizingLayoutContext _layoutContext; - private NotifyCollectionChangedEventArgs _processingItemsSourceChange; + private EventHandler _childIndexChanged; private bool _isLayoutInProgress; + private NotifyCollectionChangedEventArgs _processingItemsSourceChange; private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; @@ -163,6 +167,21 @@ namespace Avalonia.Controls } } + int? IChildIndexProvider.TotalCount => ItemsSourceView.Count; + + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return child is IControl control + ? GetElementIndex(control) + : -1; + } + /// /// Occurs each time an element is cleared and made available to be re-used. /// @@ -545,6 +564,8 @@ namespace Avalonia.Controls ElementPrepared(this, _elementPreparedArgs); } + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); } internal void OnElementClearing(IControl element) @@ -562,6 +583,8 @@ namespace Avalonia.Controls ElementClearing(this, _elementClearingArgs); } + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); } internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex) @@ -579,6 +602,8 @@ namespace Avalonia.Controls ElementIndexChanged(this, _elementIndexChangedArgs); } + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); } private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue) diff --git a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs new file mode 100644 index 0000000000..1c90851e13 --- /dev/null +++ b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs @@ -0,0 +1,23 @@ +#nullable enable +using System; + +namespace Avalonia.LogicalTree +{ + public class ChildIndexChangedEventArgs : EventArgs + { + public ChildIndexChangedEventArgs() + { + } + + public ChildIndexChangedEventArgs(ILogical child) + { + Child = child; + } + + /// + /// Logical child which index was changed. + /// If null, all children should be reset. + /// + public ILogical? Child { get; } + } +} diff --git a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs new file mode 100644 index 0000000000..fdba99baa2 --- /dev/null +++ b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs @@ -0,0 +1,14 @@ +#nullable enable +using System; + +namespace Avalonia.LogicalTree +{ + public interface IChildIndexProvider + { + int GetChildIndex(ILogical child); + + int? TotalCount { get; } + + event EventHandler? ChildIndexChanged; + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs new file mode 100644 index 0000000000..34cca1a396 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs @@ -0,0 +1,56 @@ +#nullable enable +using System; + +using Avalonia.LogicalTree; + +namespace Avalonia.Styling.Activators +{ + /// + /// An which is active when control's index was changed. + /// + internal sealed class NthChildActivator : StyleActivatorBase + { + private readonly ILogical _control; + private readonly IChildIndexProvider _provider; + private readonly int _step; + private readonly int _offset; + private readonly bool _reversed; + private EventHandler? _childIndexChangedHandler; + + public NthChildActivator( + ILogical control, + IChildIndexProvider provider, + int step, int offset, bool reversed) + { + _control = control; + _provider = provider; + _step = step; + _offset = offset; + _reversed = reversed; + } + + private EventHandler ChildIndexChangedHandler => _childIndexChangedHandler ??= ChildIndexChanged; + + protected override void Initialize() + { + PublishNext(IsMatching()); + _provider.ChildIndexChanged += ChildIndexChangedHandler; + } + + protected override void Deinitialize() + { + _provider.ChildIndexChanged -= ChildIndexChangedHandler; + } + + private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e) + { + if (e.Child is null + || e.Child == _control) + { + PublishNext(IsMatching()); + } + } + + private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch; + } +} diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs index a6b91dea5f..16b97e22f6 100644 --- a/src/Avalonia.Styling/Styling/NthChildSelector.cs +++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs @@ -3,21 +3,10 @@ using System; using System.Text; using Avalonia.LogicalTree; +using Avalonia.Styling.Activators; namespace Avalonia.Styling { - public interface IChildIndexProvider - { - (int Index, int? TotalCount) GetChildIndex(ILogical child); - } - - public class NthLastChildSelector : NthChildSelector - { - public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) - { - } - } - public class NthChildSelector : Selector { private const string NthChildSelectorName = "nth-child"; @@ -55,42 +44,49 @@ namespace Avalonia.Styling if (controlParent is IChildIndexProvider childIndexProvider) { - var (index, totalCount) = childIndexProvider.GetChildIndex(logical); - if (index < 0) - { - return SelectorMatch.NeverThisInstance; - } + return subscribe + ? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed)) + : Evaluate(logical, childIndexProvider, Step, Offset, _reversed); + } + else + { + return SelectorMatch.NeverThisInstance; + } + } + + internal static SelectorMatch Evaluate( + ILogical logical, IChildIndexProvider childIndexProvider, + int step, int offset, bool reversed) + { + var index = childIndexProvider.GetChildIndex(logical); + if (index < 0) + { + return SelectorMatch.NeverThisInstance; + } - if (_reversed) + if (reversed) + { + if (childIndexProvider.TotalCount is int totalCountValue) { - if (totalCount is int totalCountValue) - { - index = totalCountValue - index; - } - else - { - return SelectorMatch.NeverThisInstance; - } + index = totalCountValue - index; } else { - // nth child index is 1-based - index += 1; + return SelectorMatch.NeverThisInstance; } - - - var n = Math.Sign(Step); - - var diff = index - Offset; - var match = diff == 0 || (Math.Sign(diff) == n && diff % Step == 0); - - return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; } else { - return SelectorMatch.NeverThisInstance; + // nth child index is 1-based + index += 1; } + var n = Math.Sign(step); + + var diff = index - offset; + var match = diff == 0 || (Math.Sign(diff) == n && diff % step == 0); + + return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; } protected override Selector? MovePrevious() => _previous; diff --git a/src/Avalonia.Styling/Styling/NthLastChildSelector.cs b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs new file mode 100644 index 0000000000..ff7cf0faa1 --- /dev/null +++ b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Avalonia.Styling +{ + public class NthLastChildSelector : NthChildSelector + { + public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) + { + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 3824b79708..ee633ee66f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -1,3 +1,6 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; using System.Xml; using Avalonia.Controls; using Avalonia.Markup.Data; @@ -289,7 +292,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var b1 = window.FindControl("b1"); var b2 = window.FindControl("b2"); - Assert.Equal(Colors.Red, ((ISolidColorBrush)b1.Background).Color); + Assert.Equal(Brushes.Red, b1.Background); Assert.Null(b2.Background); } } @@ -303,7 +306,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml - @@ -318,11 +321,119 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var b1 = window.FindControl("b1"); var b2 = window.FindControl("b2"); + Assert.Null(b1.Background); + Assert.Equal(Brushes.Red, b2.Background); + parent.Children.Remove(b1); - parent.Children.Add(b1); Assert.Null(b1.Background); - Assert.Equal(Colors.Red, ((ISolidColorBrush)b2.Background).Color); + Assert.Null(b2.Background); + + parent.Children.Add(b1); + + Assert.Equal(Brushes.Red, b1.Background); + Assert.Null(b2.Background); + } + } + + + [Fact] + public void Style_Can_Use_NthChild_Selector_With_ListBox() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var collection = new ObservableCollection() + { + Brushes.Red, Brushes.Green, Brushes.Blue + }; + + var list = window.FindControl("list"); + list.VirtualizationMode = ItemVirtualizationMode.Simple; + list.Items = collection; + + window.Show(); + + var items = list.Presenter.Panel.Children.Cast(); + ListBoxItem At(int index) => items.ElementAt(index); + + Assert.Equal(Brushes.Transparent, At(0).Background); + Assert.Equal(Brushes.Green, At(1).Background); + Assert.Equal(Brushes.Transparent, At(2).Background); + + collection.Remove(Brushes.Green); + + Assert.Equal(Brushes.Transparent, At(0).Background); + Assert.Equal(Brushes.Blue, At(1).Background); + + collection.Add(Brushes.Violet); + collection.Add(Brushes.Black); + + Assert.Equal(Brushes.Transparent, At(0).Background); + Assert.Equal(Brushes.Blue, At(1).Background); + Assert.Equal(Brushes.Transparent, At(2).Background); + Assert.Equal(Brushes.Black, At(3).Background); + } + } + + [Fact] + public void Style_Can_Use_NthChild_Selector_With_ItemsRepeater() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var collection = new ObservableCollection() + { + Brushes.Red, Brushes.Green, Brushes.Blue + }; + + var list = window.FindControl("list"); + list.Items = collection; + + window.Show(); + + var items = list.Children; + TextBlock At(int index) => (TextBlock)list.GetOrCreateElement(index); + + Assert.Equal(Brushes.Transparent, At(0).Foreground); + Assert.Equal(Brushes.Green, At(1).Foreground); + Assert.Equal(Brushes.Transparent, At(2).Foreground); + + collection.Remove(Brushes.Green); + + Assert.Equal(Brushes.Transparent, At(0).Foreground); + Assert.Equal(Brushes.Blue, At(1).Foreground); + + collection.Add(Brushes.Violet); + collection.Add(Brushes.Black); + + Assert.Equal(Brushes.Transparent, At(0).Foreground); + Assert.Equal(Brushes.Blue, At(1).Foreground); + Assert.Equal(Brushes.Transparent, At(2).Foreground); + Assert.Equal(Brushes.Black, At(3).Foreground); } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs index b72e980821..a70b3c9f29 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -1,4 +1,7 @@ +using System.Threading.Tasks; + using Avalonia.Controls; + using Xunit; namespace Avalonia.Styling.UnitTests @@ -21,7 +24,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Nth_Child_Match_Control_In_Panel() + public async Task Nth_Child_Match_Control_In_Panel() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -35,14 +38,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(2, 0); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.False(await target.Match(b3).Activator!.Take(1)); + Assert.True(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -56,14 +59,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(2, 1); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.True(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Negative_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -77,14 +80,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(4, -1); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Singular_Step() + public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -98,14 +101,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(1, 2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.True(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -119,14 +122,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(1, -1); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + Assert.True(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.True(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -140,14 +143,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(0, 2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.False(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() + public async Task Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -161,14 +164,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(0, -2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.False(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Previous_Selector() + public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector() { Border b1, b2; Button b3, b4; @@ -184,14 +187,16 @@ namespace Avalonia.Styling.UnitTests var previous = default(Selector).OfType(); var target = previous.NthChild(2, 0); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.Null(target.Match(b3).Activator); Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result); + Assert.Null(target.Match(b4).Activator); Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result); } [Fact] - public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() { Border b1; var contentControl = new ContentControl(); @@ -199,7 +204,7 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(1, 0); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); } [Fact] diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs index 3698e07d3e..ed88106295 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; + using Avalonia.Controls; using Xunit; @@ -21,7 +23,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Nth_Child_Match_Control_In_Panel() + public async Task Nth_Child_Match_Control_In_Panel() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -35,14 +37,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(2, 0); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.True(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -56,14 +58,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(2, 1); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.False(await target.Match(b3).Activator!.Take(1)); + Assert.True(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Negative_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -77,14 +79,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(4, -1); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.False(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Singular_Step() + public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -98,14 +100,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(1, 2); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.True(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -119,14 +121,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(1, -2); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + Assert.True(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.True(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -140,14 +142,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(0, 2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() + public async Task Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -161,14 +163,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(0, -2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.False(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Previous_Selector() + public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector() { Border b1, b2; Button b3, b4; @@ -184,14 +186,16 @@ namespace Avalonia.Styling.UnitTests var previous = default(Selector).OfType(); var target = previous.NthLastChild(2, 0); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.True(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.Null(target.Match(b3).Activator); Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result); + Assert.Null(target.Match(b4).Activator); Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result); } [Fact] - public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() { Border b1; var contentControl = new ContentControl(); @@ -199,7 +203,7 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(1, 0); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); } [Fact] diff --git a/tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs b/tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs index eb3dabce0b..22f4db79d1 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs +++ b/tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs @@ -20,13 +20,17 @@ namespace Avalonia.Styling.UnitTests public static IObservable ToObservable(this IStyleActivator activator) { + if (activator == null) + { + throw new ArgumentNullException(nameof(activator)); + } + return new ObservableAdapter(activator); } private class ObservableAdapter : LightweightObservableBase, IStyleActivatorSink { private readonly IStyleActivator _source; - private bool _value; public ObservableAdapter(IStyleActivator source) => _source = source; protected override void Initialize() => _source.Subscribe(this); @@ -34,7 +38,6 @@ namespace Avalonia.Styling.UnitTests void IStyleActivatorSink.OnNext(bool value, int tag) { - _value = value; PublishNext(value); } } From 031e8ac2f0e45c91150f48f1d1d2393c7ae9606c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Oct 2021 22:15:16 -0400 Subject: [PATCH 148/198] Complete --- .../Pages/ItemsRepeaterPage.xaml | 5 ++ samples/ControlCatalog/Pages/ListBoxPage.xaml | 7 +- .../Presenters/ItemsPresenterBase.cs | 7 +- .../LogicalTree/ChildIndexChangedEventArgs.cs | 3 + .../LogicalTree/IChildIndexProvider.cs | 18 +++++ .../Styling/Activators/NthChildActivator.cs | 7 +- .../Styling/NthChildSelector.cs | 12 ++++ .../Styling/NthLastChildSelector.cs | 12 ++++ src/Avalonia.Styling/Styling/Selectors.cs | 6 ++ .../Xaml/StyleTests.cs | 71 +++++++++++++------ .../SelectorTests_NthChild.cs | 4 +- .../SelectorTests_NthLastChild.cs | 4 +- 12 files changed, 125 insertions(+), 31 deletions(-) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 93f3c33434..4d0bd663df 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -12,6 +12,11 @@ + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 897134badb..cb29f54c94 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -3,8 +3,13 @@ x:Class="ControlCatalog.Pages.ListBoxPage"> - + diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index d58ef2e510..b92af1eb9c 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; @@ -133,7 +132,7 @@ namespace Avalonia.Controls.Presenters protected bool IsHosted => TemplatedParent is IItemsPresenterHost; - int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : null; + int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : (int?)null; event EventHandler IChildIndexProvider.ChildIndexChanged { @@ -161,6 +160,8 @@ namespace Avalonia.Controls.Presenters if (Panel != null) { ItemsChanged(e); + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); } } @@ -192,7 +193,7 @@ namespace Avalonia.Controls.Presenters { for (var i = 0; i < e.Containers.Count; i++) { - _childIndexChanged?.Invoke(sender, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl)); } } diff --git a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs index 1c90851e13..de41f5292c 100644 --- a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs +++ b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs @@ -3,6 +3,9 @@ using System; namespace Avalonia.LogicalTree { + /// + /// Event args for event. + /// public class ChildIndexChangedEventArgs : EventArgs { public ChildIndexChangedEventArgs() diff --git a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs index fdba99baa2..53e2199d28 100644 --- a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs +++ b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs @@ -3,12 +3,30 @@ using System; namespace Avalonia.LogicalTree { + /// + /// Child's index and total count information provider used by list-controls (ListBox, StackPanel, etc.) + /// + /// + /// Used by nth-child and nth-last-child selectors. + /// public interface IChildIndexProvider { + /// + /// Gets child's actual index in order of the original source. + /// + /// Logical child. + /// Index or -1 if child was not found. int GetChildIndex(ILogical child); + /// + /// Total children count or null if source is infinite. + /// Some Avalonia features might not work if is null, for instance: nth-last-child selector. + /// int? TotalCount { get; } + /// + /// Notifies subscriber when child's index or total count was changed. + /// event EventHandler? ChildIndexChanged; } } diff --git a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs index 34cca1a396..5d23d1ffd1 100644 --- a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs @@ -44,7 +44,12 @@ namespace Avalonia.Styling.Activators private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e) { - if (e.Child is null + // Run matching again if: + // 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index. + // 2. e.Child is null, when all children indeces were changed. + // 3. Subscribed child index was changed. + if (_reversed + || e.Child is null || e.Child == _control) { PublishNext(IsMatching()); diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs index 16b97e22f6..e844fb51f8 100644 --- a/src/Avalonia.Styling/Styling/NthChildSelector.cs +++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs @@ -7,6 +7,12 @@ using Avalonia.Styling.Activators; namespace Avalonia.Styling { + /// + /// The :nth-child() pseudo-class matches elements based on their position in a group of siblings. + /// + /// + /// Element indices are 1-based. + /// public class NthChildSelector : Selector { private const string NthChildSelectorName = "nth-child"; @@ -22,6 +28,12 @@ namespace Avalonia.Styling _reversed = reversed; } + /// + /// Creates an instance of + /// + /// Previous selector. + /// Position step. + /// Initial index offset. public NthChildSelector(Selector? previous, int step, int offset) : this(previous, step, offset, false) { diff --git a/src/Avalonia.Styling/Styling/NthLastChildSelector.cs b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs index ff7cf0faa1..6f6abbae6a 100644 --- a/src/Avalonia.Styling/Styling/NthLastChildSelector.cs +++ b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs @@ -2,8 +2,20 @@ namespace Avalonia.Styling { + /// + /// The :nth-child() pseudo-class matches elements based on their position among a group of siblings, counting from the end. + /// + /// + /// Element indices are 1-based. + /// public class NthLastChildSelector : NthChildSelector { + /// + /// Creates an instance of + /// + /// Previous selector. + /// Position step. + /// Initial index offset, counting from the end. public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) { } diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index 0bccccbd7c..64d0a0e96b 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -123,11 +123,17 @@ namespace Avalonia.Styling return new NotSelector(previous, argument); } + /// + /// + /// The selector. public static Selector NthChild(this Selector previous, int step, int offset) { return new NthChildSelector(previous, step, offset); } + /// + /// + /// The selector. public static Selector NthLastChild(this Selector previous, int step, int offset) { return new NthLastChildSelector(previous, step, offset); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index ee633ee66f..28960c8bf6 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; @@ -336,6 +337,45 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void Style_Can_Use_NthLastChild_Selector_After_Reoder() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + + var parent = window.FindControl("parent"); + var b1 = window.FindControl("b1"); + var b2 = window.FindControl("b2"); + + Assert.Equal(Brushes.Red, b1.Background); + Assert.Null(b2.Background); + + parent.Children.Remove(b1); + + Assert.Null(b1.Background); + Assert.Null(b2.Background); + + parent.Children.Add(b1); + + Assert.Null(b1.Background); + Assert.Equal(Brushes.Red, b2.Background); + } + } + [Fact] public void Style_Can_Use_NthChild_Selector_With_ListBox() @@ -364,25 +404,18 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml window.Show(); - var items = list.Presenter.Panel.Children.Cast(); - ListBoxItem At(int index) => items.ElementAt(index); + IEnumerable GetColors() => list.Presenter.Panel.Children.Cast().Select(t => t.Background); - Assert.Equal(Brushes.Transparent, At(0).Background); - Assert.Equal(Brushes.Green, At(1).Background); - Assert.Equal(Brushes.Transparent, At(2).Background); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors()); collection.Remove(Brushes.Green); - Assert.Equal(Brushes.Transparent, At(0).Background); - Assert.Equal(Brushes.Blue, At(1).Background); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors()); collection.Add(Brushes.Violet); collection.Add(Brushes.Black); - Assert.Equal(Brushes.Transparent, At(0).Background); - Assert.Equal(Brushes.Blue, At(1).Background); - Assert.Equal(Brushes.Transparent, At(2).Background); - Assert.Equal(Brushes.Black, At(3).Background); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors()); } } @@ -415,25 +448,19 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml window.Show(); - var items = list.Children; - TextBlock At(int index) => (TextBlock)list.GetOrCreateElement(index); + IEnumerable GetColors() => Enumerable.Range(0, list.ItemsSourceView.Count) + .Select(t => (list.GetOrCreateElement(t) as TextBlock)!.Foreground); - Assert.Equal(Brushes.Transparent, At(0).Foreground); - Assert.Equal(Brushes.Green, At(1).Foreground); - Assert.Equal(Brushes.Transparent, At(2).Foreground); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors()); collection.Remove(Brushes.Green); - Assert.Equal(Brushes.Transparent, At(0).Foreground); - Assert.Equal(Brushes.Blue, At(1).Foreground); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors()); collection.Add(Brushes.Violet); collection.Add(Brushes.Black); - Assert.Equal(Brushes.Transparent, At(0).Foreground); - Assert.Equal(Brushes.Blue, At(1).Foreground); - Assert.Equal(Brushes.Transparent, At(2).Foreground); - Assert.Equal(Brushes.Black, At(3).Foreground); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors()); } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs index a70b3c9f29..8a8e46fc4b 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -196,7 +196,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() { Border b1; var contentControl = new ContentControl(); @@ -204,7 +204,7 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(1, 0); - Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1)); } [Fact] diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs index ed88106295..8d9d490724 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs @@ -195,7 +195,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() { Border b1; var contentControl = new ContentControl(); @@ -203,7 +203,7 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(1, 0); - Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1)); } [Fact] From f276c4ed8b992017003d94a863128effdd068fe2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 21 Oct 2021 18:50:02 -0400 Subject: [PATCH 149/198] Changes after review --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 2 +- src/Avalonia.Controls/ItemsControl.cs | 15 ++++++++++++--- src/Avalonia.Controls/Panel.cs | 10 ++++++---- .../Presenters/ItemsPresenterBase.cs | 8 +++++--- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 10 ++++++---- .../LogicalTree/IChildIndexProvider.cs | 4 ++-- .../Styling/Activators/NthChildActivator.cs | 9 ++------- src/Avalonia.Styling/Styling/NthChildSelector.cs | 9 ++++++--- .../Xaml/StyleTests.cs | 2 -- .../SelectorTests_NthChild.cs | 2 -- .../SelectorTests_NthLastChild.cs | 1 - 11 files changed, 40 insertions(+), 32 deletions(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index cb29f54c94..b36629fb2a 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -15,7 +15,7 @@ ListBox Hosts a collection of ListBoxItem. - Each 2nd item is highlighted + Each 5th item is highlighted with nth-child(5n+3) and nth-last-child(5n+4) rules. Multiple diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 7b28335a6d..1ff49326b6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -13,7 +13,6 @@ using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; -using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -147,8 +146,6 @@ namespace Avalonia.Controls protected set; } - int? IChildIndexProvider.TotalCount => (Presenter as IChildIndexProvider)?.TotalCount ?? ItemCount; - event EventHandler IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -538,5 +535,17 @@ namespace Avalonia.Controls return Presenter is IChildIndexProvider innerProvider ? innerProvider.GetChildIndex(child) : -1; } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + if (Presenter is IChildIndexProvider presenter + && presenter.TryGetTotalCount(out count)) + { + return true; + } + + count = ItemCount; + return true; + } } } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 9c93126506..b182f9d261 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; - -using Avalonia.Controls.Presenters; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; @@ -59,8 +57,6 @@ namespace Avalonia.Controls set { SetValue(BackgroundProperty, value); } } - int? IChildIndexProvider.TotalCount => Children.Count; - event EventHandler IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -180,5 +176,11 @@ namespace Avalonia.Controls { return child is IControl control ? Children.IndexOf(control) : -1; } + + public bool TryGetTotalCount(out int count) + { + count = Children.Count; + return true; + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index b92af1eb9c..aeead7bfd0 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Specialized; - using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; @@ -132,8 +131,6 @@ namespace Avalonia.Controls.Presenters protected bool IsHosted => TemplatedParent is IItemsPresenterHost; - int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : (int?)null; - event EventHandler IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -285,5 +282,10 @@ namespace Avalonia.Controls.Presenters return -1; } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + return Items.TryGetCountFast(out count); + } } } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 6d89a70670..ecc0fa3a48 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -6,13 +6,11 @@ using System; using System.Collections; using System.Collections.Specialized; - using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; using Avalonia.LogicalTree; -using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -167,8 +165,6 @@ namespace Avalonia.Controls } } - int? IChildIndexProvider.TotalCount => ItemsSourceView.Count; - event EventHandler IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -182,6 +178,12 @@ namespace Avalonia.Controls : -1; } + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + count = ItemsSourceView.Count; + return true; + } + /// /// Occurs each time an element is cleared and made available to be re-used. /// diff --git a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs index 53e2199d28..7fcd73273c 100644 --- a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs +++ b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs @@ -20,9 +20,9 @@ namespace Avalonia.LogicalTree /// /// Total children count or null if source is infinite. - /// Some Avalonia features might not work if is null, for instance: nth-last-child selector. + /// Some Avalonia features might not work if returns false, for instance: nth-last-child selector. /// - int? TotalCount { get; } + bool TryGetTotalCount(out int count); /// /// Notifies subscriber when child's index or total count was changed. diff --git a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs index 5d23d1ffd1..803809a8ce 100644 --- a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs @@ -1,6 +1,4 @@ #nullable enable -using System; - using Avalonia.LogicalTree; namespace Avalonia.Styling.Activators @@ -15,7 +13,6 @@ namespace Avalonia.Styling.Activators private readonly int _step; private readonly int _offset; private readonly bool _reversed; - private EventHandler? _childIndexChangedHandler; public NthChildActivator( ILogical control, @@ -29,17 +26,15 @@ namespace Avalonia.Styling.Activators _reversed = reversed; } - private EventHandler ChildIndexChangedHandler => _childIndexChangedHandler ??= ChildIndexChanged; - protected override void Initialize() { PublishNext(IsMatching()); - _provider.ChildIndexChanged += ChildIndexChangedHandler; + _provider.ChildIndexChanged += ChildIndexChanged; } protected override void Deinitialize() { - _provider.ChildIndexChanged -= ChildIndexChangedHandler; + _provider.ChildIndexChanged -= ChildIndexChanged; } private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e) diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs index e844fb51f8..aff34ea17c 100644 --- a/src/Avalonia.Styling/Styling/NthChildSelector.cs +++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs @@ -1,7 +1,6 @@ #nullable enable using System; using System.Text; - using Avalonia.LogicalTree; using Avalonia.Styling.Activators; @@ -51,7 +50,11 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - var logical = (ILogical)control; + if (!(control is ILogical logical)) + { + return SelectorMatch.NeverThisType; + } + var controlParent = logical.LogicalParent; if (controlParent is IChildIndexProvider childIndexProvider) @@ -78,7 +81,7 @@ namespace Avalonia.Styling if (reversed) { - if (childIndexProvider.TotalCount is int totalCountValue) + if (childIndexProvider.TryGetTotalCount(out var totalCountValue)) { index = totalCountValue - index; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 28960c8bf6..022ff0c3a4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -1,10 +1,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Threading.Tasks; using System.Xml; using Avalonia.Controls; -using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.Styling; using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs index 8a8e46fc4b..1d101b8ea0 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -1,7 +1,5 @@ using System.Threading.Tasks; - using Avalonia.Controls; - using Xunit; namespace Avalonia.Styling.UnitTests diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs index 8d9d490724..00a99523c7 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - using Avalonia.Controls; using Xunit; From d64a700b4fd2fae11ee3de45f83e18c4c3984562 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 21 Oct 2021 20:08:12 -0400 Subject: [PATCH 150/198] Imrpove nth-child parsing --- .../Markup/Parsers/SelectorGrammar.cs | 56 +++++++++++++-- .../Parsers/SelectorGrammarTests.cs | 69 +++++++++++++++++++ 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 56e64329b7..953a7e9a15 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -350,12 +350,28 @@ namespace Avalonia.Markup.Parsers } else { - var stepOrOffsetSpan = r.TakeWhile(c => c != ')' && c != 'n'); - if (!int.TryParse(stepOrOffsetSpan.ToString().Trim(), out var stepOrOffset)) + r.SkipWhitespace(); + + var stepOrOffset = 0; + var stepOrOffsetStr = r.TakeWhile(c => char.IsDigit(c) || c == '-' || c == '+').ToString(); + if (stepOrOffsetStr.Length == 0 + || (stepOrOffsetStr.Length == 1 + && stepOrOffsetStr[0] == '+')) + { + stepOrOffset = 1; + } + else if (stepOrOffsetStr.Length == 1 + && stepOrOffsetStr[0] == '-') + { + stepOrOffset = -1; + } + else if (!int.TryParse(stepOrOffsetStr.ToString(), out stepOrOffset)) { throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step or offset value. Integer was expected."); } + r.SkipWhitespace(); + if (r.Peek == ')') { step = 0; @@ -365,13 +381,41 @@ namespace Avalonia.Markup.Parsers { step = stepOrOffset; + if (r.Peek != 'n') + { + throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step value, \"xn+y\" pattern was expected."); + } + r.Skip(1); // skip 'n' - var offsetSpan = r.TakeUntil(')').TrimStart(); - if (offsetSpan.Length != 0 - && !int.TryParse(offsetSpan.ToString().Trim(), out offset)) + r.SkipWhitespace(); + + if (r.Peek != ')') { - throw new ExpressionParseException(r.Position, "Couldn't parse nth-child offset value. Integer was expected."); + int sign; + var nextChar = r.Take(); + if (nextChar == '+') + { + sign = 1; + } + else if (nextChar == '-') + { + sign = -1; + } + else + { + throw new ExpressionParseException(r.Position, "Couldn't parse nth-child sign. '+' or '-' was expected."); + } + + r.SkipWhitespace(); + + if (sign != 0 + && !int.TryParse(r.TakeUntil(')').ToString(), out offset)) + { + throw new ExpressionParseException(r.Position, "Couldn't parse nth-child offset value. Integer was expected."); + } + + offset *= sign; } } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 543d44c492..568f6deaf2 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -236,6 +236,75 @@ namespace Avalonia.Markup.UnitTests.Parsers result); } + [Theory] + [InlineData(":nth-child(xn+2)")] + [InlineData(":nth-child(2n+b)")] + [InlineData(":nth-child(2n+)")] + [InlineData(":nth-child(2na)")] + [InlineData(":nth-child(2x+1)")] + public void NthChild_Invalid_Inputs(string input) + { + Assert.Throws(() => SelectorGrammar.Parse(input)); + } + + [Theory] + [InlineData(":nth-child(+1)", 0, 1)] + [InlineData(":nth-child(1)", 0, 1)] + [InlineData(":nth-child(-1)", 0, -1)] + [InlineData(":nth-child(2n+1)", 2, 1)] + [InlineData(":nth-child(n)", 1, 0)] + [InlineData(":nth-child(+n)", 1, 0)] + [InlineData(":nth-child(-n)", -1, 0)] + [InlineData(":nth-child(-2n)", -2, 0)] + [InlineData(":nth-child(n+5)", 1, 5)] + [InlineData(":nth-child(n-5)", 1, -5)] + [InlineData(":nth-child( 2n + 1 )", 2, 1)] + [InlineData(":nth-child( 2n - 1 )", 2, -1)] + public void NthChild_Variations(string input, int step, int offset) + { + var result = SelectorGrammar.Parse(input); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NthChildSyntax() + { + Step = step, + Offset = offset + } + }, + result); + } + + [Theory] + [InlineData(":nth-last-child(+1)", 0, 1)] + [InlineData(":nth-last-child(1)", 0, 1)] + [InlineData(":nth-last-child(-1)", 0, -1)] + [InlineData(":nth-last-child(2n+1)", 2, 1)] + [InlineData(":nth-last-child(n)", 1, 0)] + [InlineData(":nth-last-child(+n)", 1, 0)] + [InlineData(":nth-last-child(-n)", -1, 0)] + [InlineData(":nth-last-child(-2n)", -2, 0)] + [InlineData(":nth-last-child(n+5)", 1, 5)] + [InlineData(":nth-last-child(n-5)", 1, -5)] + [InlineData(":nth-last-child( 2n + 1 )", 2, 1)] + [InlineData(":nth-last-child( 2n - 1 )", 2, -1)] + public void NthLastChild_Variations(string input, int step, int offset) + { + var result = SelectorGrammar.Parse(input); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NthLastChildSyntax() + { + Step = step, + Offset = offset + } + }, + result); + } + [Fact] public void OfType_NthChild() { From bab044980569a7f80ead99792d48da4969a2fd0f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 21 Oct 2021 20:46:24 -0400 Subject: [PATCH 151/198] Added tests from nthmaster.com --- .../SelectorTests_NthChild.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs index 1d101b8ea0..e1507be110 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Xunit; @@ -205,6 +206,76 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1)); } + + [Theory] // http://nthmaster.com/ + [InlineData(+0, 8, false, false, false, false, false, false, false, true , false, false, false)] + [InlineData(+1, 6, false, false, false, false, false, true , true , true , true , true , true )] + [InlineData(-1, 9, true , true , true , true , true , true , true , true , true , false, false)] + public async Task Nth_Child_Master_Com_Test_Sigle_Selector( + int step, int offset, params bool[] items) + { + var panel = new StackPanel(); + panel.Children.AddRange(items.Select(_ => new Border())); + + var previous = default(Selector).OfType(); + var target = previous.NthChild(step, offset); + + var results = new bool[items.Length]; + for (int index = 0; index < items.Length; index++) + { + var border = panel.Children[index]; + results[index] = await target.Match(border).Activator!.Take(1); + } + + Assert.Equal(items, results); + } + + [Theory] // http://nthmaster.com/ + [InlineData(+1, 4, -1, 8, false, false, false, true , true , true , true , true , false, false, false)] + [InlineData(+3, 1, +2, 0, false, false, false, true , false, false, false, false, false, true , false)] + public async Task Nth_Child_Master_Com_Test_Double_Selector( + int step1, int offset1, int step2, int offset2, params bool[] items) + { + var panel = new StackPanel(); + panel.Children.AddRange(items.Select(_ => new Border())); + + var previous = default(Selector).OfType(); + var middle = previous.NthChild(step1, offset1); + var target = middle.NthChild(step2, offset2); + + var results = new bool[items.Length]; + for (int index = 0; index < items.Length; index++) + { + var border = panel.Children[index]; + results[index] = await target.Match(border).Activator!.Take(1); + } + + Assert.Equal(items, results); + } + + [Theory] // http://nthmaster.com/ + [InlineData(+1, 2, 2, 1, -1, 9, false, false, true , false, true , false, true , false, true , false, false)] + public async Task Nth_Child_Master_Com_Test_Triple_Selector( + int step1, int offset1, int step2, int offset2, int step3, int offset3, params bool[] items) + { + var panel = new StackPanel(); + panel.Children.AddRange(items.Select(_ => new Border())); + + var previous = default(Selector).OfType(); + var middle1 = previous.NthChild(step1, offset1); + var middle2 = middle1.NthChild(step2, offset2); + var target = middle2.NthChild(step3, offset3); + + var results = new bool[items.Length]; + for (int index = 0; index < items.Length; index++) + { + var border = panel.Children[index]; + results[index] = await target.Match(border).Activator!.Take(1); + } + + Assert.Equal(items, results); + } + [Fact] public void Returns_Correct_TargetType() { From 141e749226c95557f4b44347525abbe3fa2db230 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:04:09 +0800 Subject: [PATCH 152/198] Initial Commit for handling DBus SNI Tray Icons gracefully and also making a skeleton class for the future XEmbed Tray Icon impl. --- .../DbusSNITrayIconImpl.cs | 358 +++++++++++++++++ src/Avalonia.X11/X11TrayIconImpl.cs | 379 ++++-------------- src/Avalonia.X11/XEmbedTrayIconImpl.cs | 36 ++ 3 files changed, 462 insertions(+), 311 deletions(-) create mode 100644 src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs create mode 100644 src/Avalonia.X11/XEmbedTrayIconImpl.cs diff --git a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs new file mode 100644 index 0000000000..1fb74f132a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs @@ -0,0 +1,358 @@ +#nullable enable + +using System; +using System.Diagnostics; +using System.Reactive.Disposables; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Avalonia.Controls.Platform; +using Avalonia.FreeDesktop; +using Avalonia.Logging; +using Avalonia.Platform; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] + +namespace Avalonia.FreeDesktop +{ + public class DbusSNITrayIconImpl + { + private static int s_trayIconInstanceId; + private readonly ObjectPath _dbusMenuPath; + private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; + private readonly Connection? _connection; + private DbusPixmap _icon; + + private IStatusNotifierWatcher? _statusNotifierWatcher; + + private string? _sysTrayServiceName; + private string? _tooltipText; + private bool _isActive; + private bool _isDisposed; + private readonly bool _ctorFinished; + + public INativeMenuExporter? MenuExporter { get; } + public Action? OnClicked { get; set; } + + public bool IsActive => _isActive; + + public DbusSNITrayIconImpl(Connection connection) + { + _connection = connection; + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); + CreateTrayIcon(); + _ctorFinished = true; + } + + public async void CreateTrayIcon() + { + if (_connection is null) + return; + + try + { + _statusNotifierWatcher = _connection.CreateProxy( + "org.kde.StatusNotifierWatcher", + "/StatusNotifierWatcher"); + } + catch + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, + "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); + } + + if (_statusNotifierWatcher is null) + return; + + var pid = Process.GetCurrentProcess().Id; + var tid = s_trayIconInstanceId++; + + _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); + + await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); + + await _connection.RegisterServiceAsync(_sysTrayServiceName); + + await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + + _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); + _statusNotifierItemDbusObj.SetIcon(_icon); + + _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; + + _isActive = true; + } + + public async void DestroyTrayIcon() + { + if (_connection is null) + return; + _connection.UnregisterObject(_statusNotifierItemDbusObj); + await _connection.UnregisterServiceAsync(_sysTrayServiceName); + _isActive = false; + } + + public void Dispose() + { + _isDisposed = true; + DestroyTrayIcon(); + _connection?.Dispose(); + } + + public void SetIcon(UIntPtr[] x11iconData) + { + if (_isDisposed) + return; + var w = (int)x11iconData[0]; + var h = (int)x11iconData[1]; + + var pixLength = w * h; + var pixByteArrayCounter = 0; + var pixByteArray = new byte[w * h * 4]; + + for (var i = 0; i < pixLength; i++) + { + var rawPixel = x11iconData[i + 2].ToUInt32(); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8); + pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); + } + + _icon = new DbusPixmap(w, h, pixByteArray); + _statusNotifierItemDbusObj?.SetIcon(_icon); + } + + public void SetIsVisible(bool visible) + { + if (_isDisposed || !_ctorFinished) + return; + + if (visible & !_isActive) + { + DestroyTrayIcon(); + CreateTrayIcon(); + } + else if (!visible & _isActive) + { + DestroyTrayIcon(); + } + } + + public void SetToolTipText(string? text) + { + if (_isDisposed || text is null) + return; + _tooltipText = text; + _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); + } + } + + /// + /// DBus Object used for setting system tray icons. + /// + /// + /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html + /// + internal class StatusNotifierItemDbusObj : IStatusNotifierItem + { + private readonly StatusNotifierItemProperties _backingProperties; + public event Action? OnTitleChanged; + public event Action? OnIconChanged; + public event Action? OnAttentionIconChanged; + public event Action? OnOverlayIconChanged; + public event Action? OnTooltipChanged; + public Action? NewStatusAsync { get; set; } + public Action? ActivationDelegate { get; set; } + public ObjectPath ObjectPath { get; } + + public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) + { + ObjectPath = new ObjectPath($"/StatusNotifierItem"); + + _backingProperties = new StatusNotifierItemProperties + { + Menu = dbusmenuPath, // Needs a dbus menu somehow + ToolTip = new ToolTip("") + }; + + InvalidateAll(); + } + + public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; + + public Task ActivateAsync(int x, int y) + { + ActivationDelegate?.Invoke(); + return Task.CompletedTask; + } + + public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; + + public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; + + public void InvalidateAll() + { + OnTitleChanged?.Invoke(); + OnIconChanged?.Invoke(); + OnOverlayIconChanged?.Invoke(); + OnAttentionIconChanged?.Invoke(); + OnTooltipChanged?.Invoke(); + } + + public Task WatchNewTitleAsync(Action handler, Action onError) + { + OnTitleChanged += handler; + return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); + } + + public Task WatchNewIconAsync(Action handler, Action onError) + { + OnIconChanged += handler; + return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); + } + + public Task WatchNewAttentionIconAsync(Action handler, Action onError) + { + OnAttentionIconChanged += handler; + return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); + } + + public Task WatchNewOverlayIconAsync(Action handler, Action onError) + { + OnOverlayIconChanged += handler; + return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); + } + + public Task WatchNewToolTipAsync(Action handler, Action onError) + { + OnTooltipChanged += handler; + return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); + } + + public Task WatchNewStatusAsync(Action handler, Action onError) + { + NewStatusAsync += handler; + return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); + } + + public Task GetAsync(string prop) => Task.FromResult(new object()); + + public Task GetAllAsync() => Task.FromResult(_backingProperties); + + public Task SetAsync(string prop, object val) => Task.CompletedTask; + + public Task WatchPropertiesAsync(Action handler) => + Task.FromResult(Disposable.Empty); + + public void SetIcon(DbusPixmap dbusPixmap) + { + _backingProperties.IconPixmap = new[] { dbusPixmap }; + InvalidateAll(); + } + + public void SetTitleAndTooltip(string? text) + { + if (text is null) + return; + + _backingProperties.Id = text; + _backingProperties.Category = "ApplicationStatus"; + _backingProperties.Status = text; + _backingProperties.Title = text; + _backingProperties.ToolTip = new ToolTip(text); + + InvalidateAll(); + } + } + + [DBusInterface("org.kde.StatusNotifierWatcher")] + internal interface IStatusNotifierWatcher : IDBusObject + { + Task RegisterStatusNotifierItemAsync(string Service); + Task RegisterStatusNotifierHostAsync(string Service); + } + + [DBusInterface("org.kde.StatusNotifierItem")] + internal interface IStatusNotifierItem : IDBusObject + { + Task ContextMenuAsync(int x, int y); + Task ActivateAsync(int x, int y); + Task SecondaryActivateAsync(int x, int y); + Task ScrollAsync(int delta, string orientation); + Task WatchNewTitleAsync(Action handler, Action onError); + Task WatchNewIconAsync(Action handler, Action onError); + Task WatchNewAttentionIconAsync(Action handler, Action onError); + Task WatchNewOverlayIconAsync(Action handler, Action onError); + Task WatchNewToolTipAsync(Action handler, Action onError); + Task WatchNewStatusAsync(Action handler, Action onError); + Task GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action handler); + } + + [Dictionary] + // This class is used by Tmds.Dbus to ferry properties + // from the SNI spec. + // Don't change this to actual C# properties since + // Tmds.Dbus will get confused. + internal class StatusNotifierItemProperties + { + public string? Category; + + public string? Id; + + public string? Title; + + public string? Status; + + public ObjectPath Menu; + + public DbusPixmap[]? IconPixmap; + + public ToolTip ToolTip; + } + + internal struct ToolTip + { + public readonly string First; + public readonly DbusPixmap[] Second; + public readonly string Third; + public readonly string Fourth; + + private static readonly DbusPixmap[] s_blank = + { + new DbusPixmap(0, 0, Array.Empty()), new DbusPixmap(0, 0, Array.Empty()) + }; + + public ToolTip(string message) : this("", s_blank, message, "") + { + } + + public ToolTip(string first, DbusPixmap[] second, string third, string fourth) + { + First = first; + Second = second; + Third = third; + Fourth = fourth; + } + } + + internal readonly struct DbusPixmap + { + public readonly int Width; + public readonly int Height; + public readonly byte[] Data; + + public DbusPixmap(int width, int height, byte[] data) + { + Width = width; + Height = height; + Data = data; + } + } +} diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 371ff75408..ca8ed8ec35 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,367 +1,124 @@ -#nullable enable - using System; -using System.Diagnostics; -using System.Reactive.Disposables; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop; using Avalonia.Logging; using Avalonia.Platform; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int s_trayIconInstanceId; - private readonly ObjectPath _dbusMenuPath; - private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; - private readonly Connection? _connection; - private DbusPixmap _icon; - - private IStatusNotifierWatcher? _statusNotifierWatcher; - - private string? _sysTrayServiceName; - private string? _tooltipText; - private bool _isActive; - private bool _isDisposed; - private readonly bool _ctorFinished; - - public INativeMenuExporter? MenuExporter { get; } - public Action? OnClicked { get; set; } - public X11TrayIconImpl() { - _connection = DBusHelper.TryGetConnection(); + _xEmbedTrayIcon = new XEmbedTrayIconImpl(); + + var _connection = DBusHelper.TryGetConnection(); if (_connection is null) { Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, "Unable to get a dbus connection for system tray icons."); - return; } - _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; - MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); - CreateTrayIcon(); - _ctorFinished = true; + _dbusSniTrayIcon = new DbusSNITrayIconImpl(_connection); } - public async void CreateTrayIcon() - { - if (_connection is null) - return; - - try - { - _statusNotifierWatcher = _connection.CreateProxy( - "org.kde.StatusNotifierWatcher", - "/StatusNotifierWatcher"); - } - catch - { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) - ?.Log(this, - "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); - } - - if (_statusNotifierWatcher is null) - return; - - var pid = Process.GetCurrentProcess().Id; - var tid = s_trayIconInstanceId++; - - _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; - _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); - - await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); - - await _connection.RegisterServiceAsync(_sysTrayServiceName); - - await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); - - _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); - _statusNotifierItemDbusObj.SetIcon(_icon); + private readonly DbusSNITrayIconImpl _dbusSniTrayIcon; - _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; - - _isActive = true; - } - - public async void DestroyTrayIcon() - { - if (_connection is null) - return; - _connection.UnregisterObject(_statusNotifierItemDbusObj); - await _connection.UnregisterServiceAsync(_sysTrayServiceName); - _isActive = false; - } + private readonly XEmbedTrayIconImpl _xEmbedTrayIcon; + private bool _isDisposed; public void Dispose() { + _dbusSniTrayIcon?.Dispose(); + _xEmbedTrayIcon?.Dispose(); _isDisposed = true; - DestroyTrayIcon(); - _connection?.Dispose(); } public void SetIcon(IWindowIconImpl? icon) { - if (_isDisposed) - return; - if (!(icon is X11IconData x11icon)) - return; + if (_isDisposed) return; - var w = (int)x11icon.Data[0]; - var h = (int)x11icon.Data[1]; - - var pixLength = w * h; - var pixByteArrayCounter = 0; - var pixByteArray = new byte[w * h * 4]; + if (_dbusSniTrayIcon?.IsActive ?? false) + { + if (!(icon is X11IconData x11icon)) + return; - for (var i = 0; i < pixLength; i++) + _dbusSniTrayIcon.SetIcon(x11icon.Data); + } + else { - var rawPixel = x11icon.Data[i + 2].ToUInt32(); - pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24); - pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16); - pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8); - pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); + _xEmbedTrayIcon.SetIcon(icon); } - - _icon = new DbusPixmap(w, h, pixByteArray); - _statusNotifierItemDbusObj?.SetIcon(_icon); } - public void SetIsVisible(bool visible) + public void SetToolTipText(string? text) { - if (_isDisposed || !_ctorFinished) - return; + if (_isDisposed) return; - if (visible & !_isActive) + if (_dbusSniTrayIcon?.IsActive ?? false) { - DestroyTrayIcon(); - CreateTrayIcon(); + _dbusSniTrayIcon.SetToolTipText(text); } - else if (!visible & _isActive) + else { - DestroyTrayIcon(); + _xEmbedTrayIcon.SetToolTipText(text); } } - public void SetToolTipText(string? text) - { - if (_isDisposed || text is null) - return; - _tooltipText = text; - _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); - } - } - - /// - /// DBus Object used for setting system tray icons. - /// - /// - /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html - /// - internal class StatusNotifierItemDbusObj : IStatusNotifierItem - { - private readonly StatusNotifierItemProperties _backingProperties; - public event Action? OnTitleChanged; - public event Action? OnIconChanged; - public event Action? OnAttentionIconChanged; - public event Action? OnOverlayIconChanged; - public event Action? OnTooltipChanged; - public Action? NewStatusAsync { get; set; } - public Action? ActivationDelegate { get; set; } - public ObjectPath ObjectPath { get; } - - public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) + public void SetIsVisible(bool visible) { - ObjectPath = new ObjectPath($"/StatusNotifierItem"); + if (_isDisposed) return; - _backingProperties = new StatusNotifierItemProperties + if (_dbusSniTrayIcon?.IsActive ?? false) { - Menu = dbusmenuPath, // Needs a dbus menu somehow - ToolTip = new ToolTip("") - }; - - InvalidateAll(); - } - - public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; - - public Task ActivateAsync(int x, int y) - { - ActivationDelegate?.Invoke(); - return Task.CompletedTask; - } - - public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; - - public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; - - public void InvalidateAll() - { - OnTitleChanged?.Invoke(); - OnIconChanged?.Invoke(); - OnOverlayIconChanged?.Invoke(); - OnAttentionIconChanged?.Invoke(); - OnTooltipChanged?.Invoke(); - } - - public Task WatchNewTitleAsync(Action handler, Action onError) - { - OnTitleChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); - } - - public Task WatchNewIconAsync(Action handler, Action onError) - { - OnIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); - } - - public Task WatchNewAttentionIconAsync(Action handler, Action onError) - { - OnAttentionIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); - } - - public Task WatchNewOverlayIconAsync(Action handler, Action onError) - { - OnOverlayIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); - } - - public Task WatchNewToolTipAsync(Action handler, Action onError) - { - OnTooltipChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); - } - - public Task WatchNewStatusAsync(Action handler, Action onError) - { - NewStatusAsync += handler; - return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); - } - - public Task GetAsync(string prop) => Task.FromResult(new object()); - - public Task GetAllAsync() => Task.FromResult(_backingProperties); - - public Task SetAsync(string prop, object val) => Task.CompletedTask; - - public Task WatchPropertiesAsync(Action handler) => - Task.FromResult(Disposable.Empty); - - public void SetIcon(DbusPixmap dbusPixmap) - { - _backingProperties.IconPixmap = new[] { dbusPixmap }; - InvalidateAll(); - } - - public void SetTitleAndTooltip(string? text) - { - if (text is null) - return; - - _backingProperties.Id = text; - _backingProperties.Category = "ApplicationStatus"; - _backingProperties.Status = text; - _backingProperties.Title = text; - _backingProperties.ToolTip = new ToolTip(text); - - InvalidateAll(); - } - } - - [DBusInterface("org.kde.StatusNotifierWatcher")] - internal interface IStatusNotifierWatcher : IDBusObject - { - Task RegisterStatusNotifierItemAsync(string Service); - Task RegisterStatusNotifierHostAsync(string Service); - } - - [DBusInterface("org.kde.StatusNotifierItem")] - internal interface IStatusNotifierItem : IDBusObject - { - Task ContextMenuAsync(int x, int y); - Task ActivateAsync(int x, int y); - Task SecondaryActivateAsync(int x, int y); - Task ScrollAsync(int delta, string orientation); - Task WatchNewTitleAsync(Action handler, Action onError); - Task WatchNewIconAsync(Action handler, Action onError); - Task WatchNewAttentionIconAsync(Action handler, Action onError); - Task WatchNewOverlayIconAsync(Action handler, Action onError); - Task WatchNewToolTipAsync(Action handler, Action onError); - Task WatchNewStatusAsync(Action handler, Action onError); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [Dictionary] - // This class is used by Tmds.Dbus to ferry properties - // from the SNI spec. - // Don't change this to actual C# properties since - // Tmds.Dbus will get confused. - internal class StatusNotifierItemProperties - { - public string? Category; - - public string? Id; - - public string? Title; - - public string? Status; - - public ObjectPath Menu; - - public DbusPixmap[]? IconPixmap; - - public ToolTip ToolTip; - } - - internal struct ToolTip - { - public readonly string First; - public readonly DbusPixmap[] Second; - public readonly string Third; - public readonly string Fourth; - - private static readonly DbusPixmap[] s_blank = - { - new DbusPixmap(0, 0, Array.Empty()), new DbusPixmap(0, 0, Array.Empty()) - }; - - public ToolTip(string message) : this("", s_blank, message, "") - { + _dbusSniTrayIcon.SetIsVisible(visible); + } + else + { + _xEmbedTrayIcon.SetIsVisible(visible); + } } - public ToolTip(string first, DbusPixmap[] second, string third, string fourth) + public INativeMenuExporter? MenuExporter { - First = first; - Second = second; - Third = third; - Fourth = fourth; + get + { + if (_dbusSniTrayIcon?.IsActive ?? false) + { + return _dbusSniTrayIcon.MenuExporter; + } + else + { + return _xEmbedTrayIcon.MenuExporter; + } + } } - } - - internal readonly struct DbusPixmap - { - public readonly int Width; - public readonly int Height; - public readonly byte[] Data; - public DbusPixmap(int width, int height, byte[] data) + public Action? OnClicked { - Width = width; - Height = height; - Data = data; + get + { + if (_dbusSniTrayIcon?.IsActive ?? false) + { + return _dbusSniTrayIcon.OnClicked; + } + else + { + return _xEmbedTrayIcon.OnClicked; + } + } + set + { + if (_dbusSniTrayIcon?.IsActive ?? false) + { + _dbusSniTrayIcon.OnClicked = value; + } + else + { + _xEmbedTrayIcon.OnClicked = value; + } + } } } } diff --git a/src/Avalonia.X11/XEmbedTrayIconImpl.cs b/src/Avalonia.X11/XEmbedTrayIconImpl.cs new file mode 100644 index 0000000000..4b5f0d0a57 --- /dev/null +++ b/src/Avalonia.X11/XEmbedTrayIconImpl.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Logging; +using Avalonia.Platform; + +namespace Avalonia.X11 +{ + internal class XEmbedTrayIconImpl + { + public XEmbedTrayIconImpl() + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, + "TODO: XEmbed System Tray Icons is not implemented yet. Tray icons won't be available on this system."); + } + + public void Dispose() + { + } + + public void SetIcon(IWindowIconImpl? icon) + { + } + + public void SetToolTipText(string? text) + { + } + + public void SetIsVisible(bool visible) + { + } + + public INativeMenuExporter? MenuExporter { get; } + public Action? OnClicked { get; set; } + } +} From c4b0b99027491c78988a81559244eddc99151e4c Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 22 Oct 2021 15:05:35 +0800 Subject: [PATCH 153/198] Gracefully handle tray service restarts --- .../DbusSNITrayIconImpl.cs | 129 +++++++++++++----- src/Avalonia.X11/X11TrayIconImpl.cs | 11 +- src/Avalonia.X11/XEmbedTrayIconImpl.cs | 16 ++- 3 files changed, 113 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs index 1fb74f132a..6ca05efe50 100644 --- a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs @@ -6,49 +6,55 @@ using System.Reactive.Disposables; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Avalonia.Controls.Platform; -using Avalonia.FreeDesktop; using Avalonia.Logging; -using Avalonia.Platform; using Tmds.DBus; [assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] namespace Avalonia.FreeDesktop { - public class DbusSNITrayIconImpl + public class DbusSNITrayIconImpl { - private static int s_trayIconInstanceId; + private static int s_trayIconInstanceId = 0; private readonly ObjectPath _dbusMenuPath; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private readonly Connection? _connection; private DbusPixmap _icon; - private IStatusNotifierWatcher? _statusNotifierWatcher; - private string? _sysTrayServiceName; private string? _tooltipText; - private bool _isActive; private bool _isDisposed; - private readonly bool _ctorFinished; + private bool _serviceConnected; + private readonly IDisposable _serviceWatchDisposable; + private bool _isVisible; public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } - public bool IsActive => _isActive; - - public DbusSNITrayIconImpl(Connection connection) + public bool IsActive => _serviceConnected; + + public DbusSNITrayIconImpl() { - _connection = connection; + _connection = DBusHelper.TryGetConnection(); + + if (_connection is null) + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, "Unable to get a dbus connection for system tray icons."); + + return; + } + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); + InitializeSNWService(); CreateTrayIcon(); - _ctorFinished = true; + _serviceWatchDisposable = Watch(); } - public async void CreateTrayIcon() + private void InitializeSNWService() { - if (_connection is null) - return; + if (_connection is null || _isDisposed) return; try { @@ -61,38 +67,83 @@ namespace Avalonia.FreeDesktop Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); + + return; } - if (_statusNotifierWatcher is null) + _serviceConnected = true; + } + + + private async Task Watch() => + await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; + + + private void OnNameChange(ServiceOwnerChangedEventArgs obj) + { + if (_isDisposed) return; + if (!_serviceConnected & obj.NewOwner != null) + { + _serviceConnected = true; + + if (_isVisible) + { + DestroyTrayIcon(); + CreateTrayIcon(); + } + else + { + DestroyTrayIcon(); + } + } + else if (_serviceConnected & obj.NewOwner is null) + { + s_trayIconInstanceId = 0; + _serviceConnected = false; + } + } + + public void CreateTrayIcon() + { + if (_connection is null || !_serviceConnected || _isDisposed) + return; + + var pid = Process.GetCurrentProcess().Id; var tid = s_trayIconInstanceId++; _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); - await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); - - await _connection.RegisterServiceAsync(_sysTrayServiceName); - await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + try + { + _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); + _connection.RegisterServiceAsync(_sysTrayServiceName); + _statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + } + catch (Exception e) + { + _serviceConnected = false; + } _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); _statusNotifierItemDbusObj.SetIcon(_icon); _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; - - _isActive = true; + _isVisible = true; } - public async void DestroyTrayIcon() + public void DestroyTrayIcon() { - if (_connection is null) + if (_connection is null || !_serviceConnected || _isDisposed) return; + _connection.UnregisterObject(_statusNotifierItemDbusObj); - await _connection.UnregisterServiceAsync(_sysTrayServiceName); - _isActive = false; + _connection.UnregisterServiceAsync(_sysTrayServiceName); + _isVisible = false; } public void Dispose() @@ -100,12 +151,13 @@ namespace Avalonia.FreeDesktop _isDisposed = true; DestroyTrayIcon(); _connection?.Dispose(); + _serviceWatchDisposable?.Dispose(); } public void SetIcon(UIntPtr[] x11iconData) { if (_isDisposed) - return; + return; var w = (int)x11iconData[0]; var h = (int)x11iconData[1]; @@ -128,15 +180,15 @@ namespace Avalonia.FreeDesktop public void SetIsVisible(bool visible) { - if (_isDisposed || !_ctorFinished) + if (_isDisposed) return; - if (visible & !_isActive) + if (visible && !_isVisible) { DestroyTrayIcon(); CreateTrayIcon(); } - else if (!visible & _isActive) + else if (!visible && _isVisible) { DestroyTrayIcon(); } @@ -239,7 +291,20 @@ namespace Avalonia.FreeDesktop return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); } - public Task GetAsync(string prop) => Task.FromResult(new object()); + public async Task GetAsync(string prop) + { + return prop switch + { + nameof(_backingProperties.Category) => _backingProperties.Category, + nameof(_backingProperties.Id) => _backingProperties.Id, + nameof(_backingProperties.Menu) => _backingProperties.Menu, + nameof(_backingProperties.IconPixmap) => _backingProperties.IconPixmap, + nameof(_backingProperties.Status) => _backingProperties.Status, + nameof(_backingProperties.Title) => _backingProperties.Title, + nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip, + _ => null + }; + } public Task GetAllAsync() => Task.FromResult(_backingProperties); diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index ca8ed8ec35..9e03dcd604 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -11,16 +11,7 @@ namespace Avalonia.X11 public X11TrayIconImpl() { _xEmbedTrayIcon = new XEmbedTrayIconImpl(); - - var _connection = DBusHelper.TryGetConnection(); - - if (_connection is null) - { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) - ?.Log(this, "Unable to get a dbus connection for system tray icons."); - } - - _dbusSniTrayIcon = new DbusSNITrayIconImpl(_connection); + _dbusSniTrayIcon = new DbusSNITrayIconImpl(); } private readonly DbusSNITrayIconImpl _dbusSniTrayIcon; diff --git a/src/Avalonia.X11/XEmbedTrayIconImpl.cs b/src/Avalonia.X11/XEmbedTrayIconImpl.cs index 4b5f0d0a57..c2247565be 100644 --- a/src/Avalonia.X11/XEmbedTrayIconImpl.cs +++ b/src/Avalonia.X11/XEmbedTrayIconImpl.cs @@ -9,25 +9,39 @@ namespace Avalonia.X11 { public XEmbedTrayIconImpl() { + } + + private bool IsCalled; + + private void NotImplemented() + { + if(IsCalled) return; + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, "TODO: XEmbed System Tray Icons is not implemented yet. Tray icons won't be available on this system."); - } + IsCalled = true; + } + public void Dispose() { + NotImplemented(); } public void SetIcon(IWindowIconImpl? icon) { + NotImplemented(); } public void SetToolTipText(string? text) { + NotImplemented(); } public void SetIsVisible(bool visible) { + NotImplemented(); } public INativeMenuExporter? MenuExporter { get; } From ebd1f5366739447e2d11ff94ecb840d81040c865 Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Fri, 22 Oct 2021 12:42:29 +0200 Subject: [PATCH 154/198] DataGrid minimum distance threshold when dragging headers --- .../DataGridColumnHeader.cs | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 85fd55800a..915b36687c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -35,6 +35,7 @@ namespace Avalonia.Controls private const int DATAGRIDCOLUMNHEADER_resizeRegionWidth = 5; private const double DATAGRIDCOLUMNHEADER_separatorThickness = 1; + private const int DATAGRIDCOLUMNHEADER_columnsDragTreshold = 5; private bool _areHandlersSuspended; private static DragMode _dragMode; @@ -448,19 +449,6 @@ namespace Avalonia.Controls OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders, distanceFromLeft, distanceFromRight); - // if we still haven't done anything about moving the mouse while - // the button is down, we remember that we're dragging, but we don't - // claim to have actually handled the event - if (_dragMode == DragMode.MouseDown) - { - _dragMode = DragMode.Drag; - } - - _lastMousePositionHeaders = mousePositionHeaders; - - if (args.Pointer.Captured != this && _dragMode == DragMode.Drag) - args.Pointer.Capture(this); - SetDragCursor(mousePosition); } @@ -732,15 +720,19 @@ namespace Avalonia.Controls { return; } - + //handle entry into reorder mode - if (_dragMode == DragMode.MouseDown && _dragColumn == null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth)) + if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth)) { - handled = CanReorderColumn(OwningColumn); - - if (handled) + var distanceFromInitial = (Vector)(mousePositionHeaders - _lastMousePositionHeaders); + if (distanceFromInitial.Length > DATAGRIDCOLUMNHEADER_columnsDragTreshold) { - OnMouseMove_BeginReorder(mousePosition); + handled = CanReorderColumn(OwningColumn); + + if (handled) + { + OnMouseMove_BeginReorder(mousePosition); + } } } From f3abb8ed64506cc32ece834ee558319d354e27f7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Oct 2021 17:48:12 +0200 Subject: [PATCH 155/198] Display access key in Buttons. Set `RecognizesAccessKey` on button content presenter. --- samples/ControlCatalog/Pages/ButtonPage.xaml | 2 +- src/Avalonia.Themes.Default/Button.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/Button.xaml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml index be114bbbc9..b35c112a68 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml @@ -10,7 +10,7 @@ HorizontalAlignment="Center" Spacing="16"> - + diff --git a/src/Avalonia.Themes.Default/Button.xaml b/src/Avalonia.Themes.Default/Button.xaml index 81d96aaa14..da36abe7ec 100644 --- a/src/Avalonia.Themes.Default/Button.xaml +++ b/src/Avalonia.Themes.Default/Button.xaml @@ -17,6 +17,7 @@ ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" + RecognizesAccessKey="True" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 53d53ef127..533fabfb44 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -34,6 +34,7 @@ Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" Padding="{TemplateBinding Padding}" + RecognizesAccessKey="True" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> From 90e43897ee1473fa0b20395edc35f4133ae4915e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Oct 2021 17:48:31 +0200 Subject: [PATCH 156/198] Add access keys for menu items. --- samples/ControlCatalog/MainWindow.xaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index ee42e7a54b..375345f64e 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -63,11 +63,11 @@ - - + + - - + + From b663afe06b585bbd776dc14850f984c10d233a52 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Oct 2021 17:56:44 +0200 Subject: [PATCH 157/198] Make sure underline is drawn within bounds. --- src/Avalonia.Controls/Primitives/AccessText.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index c42c6f100c..3c82386991 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -68,7 +68,7 @@ namespace Avalonia.Controls.Primitives if (underscore != -1 && ShowAccessKey) { var rect = TextLayout.HitTestTextPosition(underscore); - var offset = new Vector(0, -0.5); + var offset = new Vector(0, -1.5); context.DrawLine( new Pen(Foreground, 1), rect.BottomLeft + offset, From fbfc1e4eb0ba33d32b60ec5879d4d6b5a25f4267 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 Oct 2021 16:57:11 +0100 Subject: [PATCH 158/198] restore osx window shadow fix. --- native/Avalonia.Native/src/OSX/window.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index bd93de0e78..26c065fe11 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -52,7 +52,6 @@ public: [Window setBackingType:NSBackingStoreBuffered]; [Window setOpaque:false]; - [Window setContentView: StandardContainer]; } virtual HRESULT ObtainNSWindowHandle(void** ret) override @@ -125,6 +124,8 @@ public: SetPosition(lastPositionSet); UpdateStyle(); + [Window setContentView: StandardContainer]; + [Window setTitle:_lastTitle]; if(ShouldTakeFocusOnShow() && activate) @@ -323,6 +324,7 @@ public: BaseEvents->Resized(AvnSize{x,y}, reason); } + [StandardContainer setFrameSize:NSSize{x,y}]; [Window setContentSize:NSSize{x, y}]; } @finally From f98070fb4dcc281f85fa51a256d9762c772f3388 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Oct 2021 14:37:48 +0200 Subject: [PATCH 159/198] Display access key in checkbox. --- src/Avalonia.Themes.Default/CheckBox.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Avalonia.Themes.Default/CheckBox.xaml b/src/Avalonia.Themes.Default/CheckBox.xaml index 5e10b319a7..75d6f853be 100644 --- a/src/Avalonia.Themes.Default/CheckBox.xaml +++ b/src/Avalonia.Themes.Default/CheckBox.xaml @@ -41,6 +41,7 @@ ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" + RecognizesAccessKey="True" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsVisible="{TemplateBinding Content, Converter={x:Static ObjectConverters.IsNotNull}}" diff --git a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml index 7969cec947..ef28593711 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml @@ -44,6 +44,7 @@ ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" + RecognizesAccessKey="True" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Grid.Column="1" /> From d0037f1df50ddc33dacbe1639f8ab2aa6fa29ed9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Oct 2021 14:38:54 +0200 Subject: [PATCH 160/198] Display access key in ControlCatalog pages. --- .../ControlCatalog/Pages/CheckBoxPage.xaml | 6 +++--- samples/ControlCatalog/Pages/DialogsPage.xaml | 20 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/samples/ControlCatalog/Pages/CheckBoxPage.xaml b/samples/ControlCatalog/Pages/CheckBoxPage.xaml index 1359cfa2ef..769ef26699 100644 --- a/samples/ControlCatalog/Pages/CheckBoxPage.xaml +++ b/samples/ControlCatalog/Pages/CheckBoxPage.xaml @@ -11,9 +11,9 @@ Spacing="16"> - Unchecked - Checked - Indeterminate + _Unchecked + _Checked + _Indeterminate Disabled Use filters - - - - - - - - - - + + + + + + + + + + From 7f89c2a0dc97cea114b3315db9914a9d5c9115c0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Oct 2021 14:38:57 +0200 Subject: [PATCH 161/198] Make button respond to access key. --- src/Avalonia.Controls/Button.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 614a18c6b5..8b22cdd4ec 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -99,6 +99,7 @@ namespace Avalonia.Controls CommandParameterProperty.Changed.Subscribe(CommandParameterChanged); IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); IsCancelProperty.Changed.Subscribe(IsCancelChanged); + AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler