From 3438ac149befdb21161d434f9f88af7c1b09b0eb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 11:42:16 +0100 Subject: [PATCH 01/93] 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 02/93] 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 03/93] 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 04/93] 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 05/93] 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 06/93] 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 07/93] 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 11/93] 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 12/93] 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 13/93] 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 14/93] 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 15/93] 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 16/93] 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 17/93] 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 18/93] 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 19/93] 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 20/93] 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 21/93] 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 22/93] 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 23/93] 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 24/93] 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 25/93] 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 3342d41a128c53a6868805ed589f6e716ed1eedf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 12 Sep 2021 10:37:37 +0100 Subject: [PATCH 26/93] 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 27/93] 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 28/93] 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 29/93] 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 30/93] 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 31/93] 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 32/93] 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 33/93] 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 34/93] 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 35/93] 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 36/93] 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 37/93] 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 38/93] 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 39/93] 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 40/93] 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 41/93] 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 42/93] 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 43/93] 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 44/93] 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 45/93] 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 46/93] 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 47/93] 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 48/93] 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 49/93] 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 50/93] 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 51/93] 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 52/93] 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 53/93] 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 54/93] 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 55/93] 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 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 56/93] 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 57/93] 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 69693d47ca0d71f2a953a777940cd31954b8d7ef Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Oct 2021 14:22:05 +0100 Subject: [PATCH 58/93] 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 59/93] 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 60/93] 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 61/93] 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 62/93] 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 63/93] 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 64/93] 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 65/93] 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 66/93] 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 67/93] 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 68/93] 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 69/93] 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 70/93] 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 71/93] 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 72/93] 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 73/93] 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 74/93] 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 75/93] 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 76/93] 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 77/93] 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 78/93] 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 79/93] 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 80/93] 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 81/93] 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 82/93] 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 83/93] 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 84/93] 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 85/93] 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 86/93] 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 87/93] 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 88/93] 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 89/93] 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 90/93] 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 91/93] 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 92/93] 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 93/93] 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;