From 6ae59214a45eab407c7b9c4e1ef4a6994cb5a520 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 17:33:26 +0100 Subject: [PATCH] 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);