Compare commits

...

11 Commits

Author SHA1 Message Date
Lea Anthony
55e6a0f312 v2.0.0-alpha.47 2021-03-07 16:24:20 +11:00
Lea Anthony
81e83fdf18 Ensure modifiers are lowercase when parsing 2021-03-07 16:21:30 +11:00
Lea Anthony
f9b79d24f8 Guard against nil url messages 2021-03-06 15:51:06 +11:00
Lea Anthony
0599a47bfe v2.0.0-alpha.46 2021-03-06 15:43:44 +11:00
Lea Anthony
817c55d318 Support base64 images in tray 2021-03-06 15:43:11 +11:00
Lea Anthony
14146c8c0c v2.0.0-alpha.45 2021-03-06 00:29:00 +11:00
Lea Anthony
18adac20d4 Tray menu open/close events 2021-03-06 00:25:34 +11:00
Lea Anthony
eb4bff89da v2.0.0-alpha.44 2021-03-04 06:18:31 +11:00
Lea Anthony
c66dc777f3 Remove debug logging 2021-03-04 06:18:11 +11:00
Lea Anthony
9003462457 v2.0.0-alpha.43 2021-03-04 06:09:17 +11:00
Lea Anthony
e124f0a220 Support Alternative menu items 2021-03-04 06:07:45 +11:00
15 changed files with 158 additions and 24 deletions

View File

@@ -1,3 +1,3 @@
package main package main
var version = "v2.0.0-alpha.42" var version = "v2.0.0-alpha.47"

View File

@@ -24,6 +24,8 @@ struct hashmap_s dialogIconCache;
// Dispatch Method // Dispatch Method
typedef void (^dispatchMethod)(void); typedef void (^dispatchMethod)(void);
TrayMenuStore *TrayMenuStoreSingleton;
// dispatch will execute the given `func` pointer // dispatch will execute the given `func` pointer
void dispatch(dispatchMethod func) { void dispatch(dispatchMethod func) {
dispatch_async(dispatch_get_main_queue(), func); dispatch_async(dispatch_get_main_queue(), func);
@@ -55,6 +57,9 @@ void filelog(const char *message) {
} }
} }
// The delegate class for tray menus
Class trayMenuDelegateClass;
// Utility function to visualise a hashmap // Utility function to visualise a hashmap
void dumpHashmap(const char *name, struct hashmap_s *hashmap) { void dumpHashmap(const char *name, struct hashmap_s *hashmap) {
printf("%s = { ", name); printf("%s = { ", name);
@@ -127,9 +132,6 @@ struct Application {
// Menu // Menu
Menu *applicationMenu; Menu *applicationMenu;
// Tray
TrayMenuStore* trayMenuStore;
// Context Menus // Context Menus
ContextMenuStore *contextMenuStore; ContextMenuStore *contextMenuStore;
@@ -477,7 +479,7 @@ void DestroyApplication(struct Application *app) {
} }
// Delete the tray menu store // Delete the tray menu store
DeleteTrayMenuStore(app->trayMenuStore); DeleteTrayMenuStore(TrayMenuStoreSingleton);
// Delete the context menu store // Delete the context menu store
DeleteContextMenuStore(app->contextMenuStore); DeleteContextMenuStore(app->contextMenuStore);
@@ -1044,7 +1046,7 @@ void AddTrayMenu(struct Application *app, const char *trayMenuJSON) {
// Guard against calling during shutdown // Guard against calling during shutdown
if( app->shuttingDown ) return; if( app->shuttingDown ) return;
AddTrayMenuToStore(app->trayMenuStore, trayMenuJSON); AddTrayMenuToStore(TrayMenuStoreSingleton, trayMenuJSON);
} }
void SetTrayMenu(struct Application *app, const char* trayMenuJSON) { void SetTrayMenu(struct Application *app, const char* trayMenuJSON) {
@@ -1053,13 +1055,13 @@ void SetTrayMenu(struct Application *app, const char* trayMenuJSON) {
if( app->shuttingDown ) return; if( app->shuttingDown ) return;
ON_MAIN_THREAD( ON_MAIN_THREAD(
UpdateTrayMenuInStore(app->trayMenuStore, trayMenuJSON); UpdateTrayMenuInStore(TrayMenuStoreSingleton, trayMenuJSON);
); );
} }
void DeleteTrayMenuByID(struct Application *app, const char *id) { void DeleteTrayMenuByID(struct Application *app, const char *id) {
ON_MAIN_THREAD( ON_MAIN_THREAD(
DeleteTrayMenuInStore(app->trayMenuStore, id); DeleteTrayMenuInStore(TrayMenuStoreSingleton, id);
); );
} }
@@ -1068,7 +1070,7 @@ void UpdateTrayMenuLabel(struct Application* app, const char* JSON) {
if( app->shuttingDown ) return; if( app->shuttingDown ) return;
ON_MAIN_THREAD( ON_MAIN_THREAD(
UpdateTrayMenuLabelInStore(app->trayMenuStore, JSON); UpdateTrayMenuLabelInStore(TrayMenuStoreSingleton, JSON);
); );
} }
@@ -1164,7 +1166,7 @@ void getURL(id self, SEL selector, id event, id replyEvent) {
void createDelegate(struct Application *app) { void createDelegate(struct Application *app) {
// Define delegate // Define delegate
Class delegateClass = objc_allocateClassPair((Class) c("NSObject"), "AppDelegate", 0); Class delegateClass = objc_allocateClassPair((Class) c("NSObject"), "AppDelegate", 0);
bool resultAddProtoc = class_addProtocol(delegateClass, objc_getProtocol("NSApplicationDelegate")); bool resultAddProtoc = class_addProtocol(delegateClass, objc_getProtocol("NSApplicationDelegate"));
class_addMethod(delegateClass, s("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) no, "c@:@"); class_addMethod(delegateClass, s("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) no, "c@:@");
@@ -1661,6 +1663,35 @@ void processUserDialogIcons(struct Application *app) {
} }
void TrayMenuWillOpen(id self, SEL selector, id menu) {
// Extract tray menu id from menu
id trayMenuIDStr = objc_getAssociatedObject(menu, "trayMenuID");
const char* trayMenuID = cstr(trayMenuIDStr);
const char *message = concat("Mo", trayMenuID);
messageFromWindowCallback(message);
MEMFREE(message);
}
void TrayMenuDidClose(id self, SEL selector, id menu) {
// Extract tray menu id from menu
id trayMenuIDStr = objc_getAssociatedObject(menu, "trayMenuID");
const char* trayMenuID = cstr(trayMenuIDStr);
const char *message = concat("Mc", trayMenuID);
messageFromWindowCallback(message);
MEMFREE(message);
}
void createTrayMenuDelegate() {
// Define delegate
trayMenuDelegateClass = objc_allocateClassPair((Class) c("NSObject"), "MenuDelegate", 0);
class_addProtocol(trayMenuDelegateClass, objc_getProtocol("NSMenuDelegate"));
class_addMethod(trayMenuDelegateClass, s("menuWillOpen:"), (IMP) TrayMenuWillOpen, "v@:@");
class_addMethod(trayMenuDelegateClass, s("menuDidClose:"), (IMP) TrayMenuDidClose, "v@:@");
// Script handler
objc_registerClassPair(trayMenuDelegateClass);
}
void Run(struct Application *app, int argc, char **argv) { void Run(struct Application *app, int argc, char **argv) {
@@ -1673,6 +1704,9 @@ void Run(struct Application *app, int argc, char **argv) {
// Define delegate // Define delegate
createDelegate(app); createDelegate(app);
// Define tray delegate
createTrayMenuDelegate();
// Create the main window // Create the main window
createMainWindow(app); createMainWindow(app);
@@ -1843,7 +1877,7 @@ void Run(struct Application *app, int argc, char **argv) {
} }
// Setup initial trays // Setup initial trays
ShowTrayMenusInStore(app->trayMenuStore); ShowTrayMenusInStore(TrayMenuStoreSingleton);
// Process dialog icons // Process dialog icons
processUserDialogIcons(app); processUserDialogIcons(app);
@@ -1920,7 +1954,7 @@ void* NewApplication(const char *title, int width, int height, int resizable, in
result->applicationMenu = NULL; result->applicationMenu = NULL;
// Tray // Tray
result->trayMenuStore = NewTrayMenuStore(); TrayMenuStoreSingleton = NewTrayMenuStore();
// Context Menus // Context Menus
result->contextMenuStore = NewContextMenuStore(); result->contextMenuStore = NewContextMenuStore();

View File

@@ -576,7 +576,7 @@ id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const c
return item; return item;
} }
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage) { id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage, bool alternate) {
id item = ALLOC("NSMenuItem"); id item = ALLOC("NSMenuItem");
// Create a MenuItemCallbackData // Create a MenuItemCallbackData
@@ -585,9 +585,13 @@ id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback); id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId); msg(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey); if( !alternate ) {
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), id key = processAcceleratorKey(acceleratorkey);
s("menuItemCallback:"), key); msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s("menuItemCallback:"), key);
} else {
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(""));
}
if( tooltip != NULL ) { if( tooltip != NULL ) {
msg(item, s("setToolTip:"), str(tooltip)); msg(item, s("setToolTip:"), str(tooltip));
@@ -663,10 +667,16 @@ id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char
msg(item, s("autorelease")); msg(item, s("autorelease"));
// Process modifiers // Process modifiers
if( modifiers != NULL ) { if( modifiers != NULL && !alternate) {
unsigned long modifierFlags = parseModifiers(modifiers); unsigned long modifierFlags = parseModifiers(modifiers);
msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags); msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
} }
// alternate
if( alternate ) {
msg(item, s("setAlternate:"), true);
msg(item, s("setKeyEquivalentModifierMask:"), NSEventModifierFlagOption);
}
msg(parentMenu, s("addItem:"), item); msg(parentMenu, s("addItem:"), item);
return item; return item;
@@ -727,6 +737,11 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
label = "(empty)"; label = "(empty)";
} }
// Is this an alternate menu item?
bool alternate = false;
getJSONBool(item, "MacAlternate", &alternate);
const char *menuid = getJSONString(item, "ID"); const char *menuid = getJSONString(item, "ID");
if ( menuid == NULL) { if ( menuid == NULL) {
menuid = ""; menuid = "";
@@ -782,7 +797,7 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
if( type != NULL ) { if( type != NULL ) {
if( STREQ(type->string_, "Text")) { if( STREQ(type->string_, "Text")) {
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage); processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage, alternate);
} }
else if ( STREQ(type->string_, "Separator")) { else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu); addSeparator(parentMenu);

View File

@@ -105,7 +105,7 @@ id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key); id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key);
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage); id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage, bool alternate);
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item); void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData); void processMenuData(Menu *menu, JsonNode *menuData);

View File

@@ -6,6 +6,8 @@
#include "traymenu_darwin.h" #include "traymenu_darwin.h"
#include "trayicons.h" #include "trayicons.h"
extern Class trayMenuDelegateClass;
// A cache for all our tray menu icons // A cache for all our tray menu icons
// Global because it's a singleton // Global because it's a singleton
struct hashmap_s trayIconCache; struct hashmap_s trayIconCache;
@@ -35,6 +37,8 @@ TrayMenu* NewTrayMenu(const char* menuJSON) {
// Create the menu // Create the menu
result->menu = NewMenu(processedMenu); result->menu = NewMenu(processedMenu);
result->delegate = NULL;
// Init tray status bar item // Init tray status bar item
result->statusbaritem = NULL; result->statusbaritem = NULL;
@@ -78,12 +82,20 @@ void UpdateTrayIcon(TrayMenu *trayMenu) {
} }
id trayImage = hashmap_get(&trayIconCache, trayMenu->icon, strlen(trayMenu->icon)); id trayImage = hashmap_get(&trayIconCache, trayMenu->icon, strlen(trayMenu->icon));
// If we don't have the image in the icon cache then assume it's base64 encoded image data
if (trayImage == NULL) {
id data = ALLOC("NSData");
id imageData = msg(data, s("initWithBase64EncodedString:options:"), str(trayMenu->icon), 0);
trayImage = ALLOC("NSImage");
msg(trayImage, s("initWithData:"), imageData);
}
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition); msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
msg(statusBarButton, s("setImage:"), trayImage); msg(statusBarButton, s("setImage:"), trayImage);
} }
void ShowTrayMenu(TrayMenu* trayMenu) { void ShowTrayMenu(TrayMenu* trayMenu) {
// Create a status bar item if we don't have one // Create a status bar item if we don't have one
@@ -91,7 +103,6 @@ void ShowTrayMenu(TrayMenu* trayMenu) {
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") ); id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength); trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg(trayMenu->statusbaritem, s("retain")); msg(trayMenu->statusbaritem, s("retain"));
} }
id statusBarButton = msg(trayMenu->statusbaritem, s("button")); id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
@@ -105,6 +116,16 @@ void ShowTrayMenu(TrayMenu* trayMenu) {
// Update the menu // Update the menu
id menu = GetMenu(trayMenu->menu); id menu = GetMenu(trayMenu->menu);
objc_setAssociatedObject(menu, "trayMenuID", str(trayMenu->ID), OBJC_ASSOCIATION_ASSIGN);
// Create delegate
id trayMenuDelegate = msg((id)trayMenuDelegateClass, s("new"));
msg(menu, s("setDelegate:"), trayMenuDelegate);
objc_setAssociatedObject(trayMenuDelegate, "menu", menu, OBJC_ASSOCIATION_ASSIGN);
// Create menu delegate
trayMenu->delegate = trayMenuDelegate;
msg(trayMenu->statusbaritem, s("setMenu:"), menu); msg(trayMenu->statusbaritem, s("setMenu:"), menu);
} }
@@ -153,6 +174,10 @@ void DeleteTrayMenu(TrayMenu* trayMenu) {
trayMenu->statusbaritem = NULL; trayMenu->statusbaritem = NULL;
} }
if ( trayMenu->delegate != NULL ) {
msg(trayMenu->delegate, s("release"));
}
// Free the tray menu memory // Free the tray menu memory
MEMFREE(trayMenu); MEMFREE(trayMenu);
} }

View File

@@ -21,6 +21,8 @@ typedef struct {
JsonNode* processedJSON; JsonNode* processedJSON;
id delegate;
} TrayMenu; } TrayMenu;
TrayMenu* NewTrayMenu(const char *trayJSON); TrayMenu* NewTrayMenu(const char *trayJSON);

View File

@@ -5,6 +5,8 @@
#ifndef TRAYMENUSTORE_DARWIN_H #ifndef TRAYMENUSTORE_DARWIN_H
#define TRAYMENUSTORE_DARWIN_H #define TRAYMENUSTORE_DARWIN_H
#include "traymenu_darwin.h"
#include <pthread.h> #include <pthread.h>
typedef struct { typedef struct {
@@ -26,6 +28,8 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON);
void ShowTrayMenusInStore(TrayMenuStore* store); void ShowTrayMenusInStore(TrayMenuStore* store);
void DeleteTrayMenuStore(TrayMenuStore* store); void DeleteTrayMenuStore(TrayMenuStore* store);
TrayMenu* GetTrayMenuByID(TrayMenuStore* store, const char* menuID);
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON); void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON);
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* id); void DeleteTrayMenuInStore(TrayMenuStore* store, const char* id);

View File

@@ -37,6 +37,7 @@ type ProcessedMenuItem struct {
// Image - base64 image data // Image - base64 image data
Image string `json:",omitempty"` Image string `json:",omitempty"`
MacTemplateImage bool `json:", omitempty"` MacTemplateImage bool `json:", omitempty"`
MacAlternate bool `json:", omitempty"`
// Tooltip // Tooltip
Tooltip string `json:",omitempty"` Tooltip string `json:",omitempty"`
@@ -60,6 +61,7 @@ func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *Pr
FontName: menuItem.FontName, FontName: menuItem.FontName,
Image: menuItem.Image, Image: menuItem.Image,
MacTemplateImage: menuItem.MacTemplateImage, MacTemplateImage: menuItem.MacTemplateImage,
MacAlternate: menuItem.MacAlternate,
Tooltip: menuItem.Tooltip, Tooltip: menuItem.Tooltip,
} }

View File

@@ -29,6 +29,7 @@ type TrayMenu struct {
menuItemMap *MenuItemMap menuItemMap *MenuItemMap
menu *menu.Menu menu *menu.Menu
ProcessedMenu *WailsMenu ProcessedMenu *WailsMenu
trayMenu *menu.TrayMenu
} }
func (t *TrayMenu) AsJSON() (string, error) { func (t *TrayMenu) AsJSON() (string, error) {
@@ -46,6 +47,7 @@ func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
Icon: trayMenu.Icon, Icon: trayMenu.Icon,
menu: trayMenu.Menu, menu: trayMenu.Menu,
menuItemMap: NewMenuItemMap(), menuItemMap: NewMenuItemMap(),
trayMenu: trayMenu,
} }
result.menuItemMap.AddMenu(trayMenu.Menu) result.menuItemMap.AddMenu(trayMenu.Menu)
@@ -54,6 +56,28 @@ func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
return result return result
} }
func (m *Manager) OnTrayMenuOpen(id string) {
trayMenu, ok := m.trayMenus[id]
if !ok {
return
}
if trayMenu.trayMenu.OnOpen == nil {
return
}
go trayMenu.trayMenu.OnOpen()
}
func (m *Manager) OnTrayMenuClose(id string) {
trayMenu, ok := m.trayMenus[id]
if !ok {
return
}
if trayMenu.trayMenu.OnClose == nil {
return
}
go trayMenu.trayMenu.OnClose()
}
func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) { func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
newTrayMenu := NewTrayMenu(trayMenu) newTrayMenu := NewTrayMenu(trayMenu)

View File

@@ -32,6 +32,14 @@ func menuMessageParser(message string) (*parsedMessage, error) {
callbackid := message[2:] callbackid := message[2:]
topic = "menu:clicked" topic = "menu:clicked"
data = callbackid data = callbackid
case 'o':
callbackid := message[2:]
topic = "menu:ontrayopen"
data = callbackid
case 'c':
callbackid := message[2:]
topic = "menu:ontrayclose"
data = callbackid
default: default:
return nil, fmt.Errorf("invalid menu message: %s", message) return nil, fmt.Errorf("invalid menu message: %s", message)
} }

View File

@@ -77,6 +77,12 @@ func (m *Menu) Start() error {
splitTopic := strings.Split(menuMessage.Topic(), ":") splitTopic := strings.Split(menuMessage.Topic(), ":")
menuMessageType := splitTopic[1] menuMessageType := splitTopic[1]
switch menuMessageType { switch menuMessageType {
case "ontrayopen":
trayID := menuMessage.Data().(string)
m.menuManager.OnTrayMenuOpen(trayID)
case "ontrayclose":
trayID := menuMessage.Data().(string)
m.menuManager.OnTrayMenuClose(trayID)
case "clicked": case "clicked":
if len(splitTopic) != 2 { if len(splitTopic) != 2 {
m.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic) m.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic)

View File

@@ -68,6 +68,10 @@ func (u *URL) Start() error {
u.wg.Done() u.wg.Done()
return return
case urlMessage := <-u.urlChannel: case urlMessage := <-u.urlChannel:
// Guard against nil messages
if urlMessage == nil {
continue
}
messageType := strings.TrimPrefix(urlMessage.Topic(), "url:") messageType := strings.TrimPrefix(urlMessage.Topic(), "url:")
switch messageType { switch messageType {
case "handler": case "handler":

View File

@@ -30,7 +30,8 @@ var modifierMap = map[string]Modifier{
} }
func parseModifier(text string) (*Modifier, error) { func parseModifier(text string) (*Modifier, error) {
result, valid := modifierMap[text] lowertext := strings.ToLower(text)
result, valid := modifierMap[lowertext]
if !valid { if !valid {
return nil, fmt.Errorf("'%s' is not a valid modifier", text) return nil, fmt.Errorf("'%s' is not a valid modifier", text)
} }

View File

@@ -39,9 +39,12 @@ type MenuItem struct {
// Image - base64 image data // Image - base64 image data
Image string Image string
// MacTemplateImage indicates that on a mac, this image is a template image // MacTemplateImage indicates that on a Mac, this image is a template image
MacTemplateImage bool MacTemplateImage bool
// MacAlternate indicates that this item is an alternative to the previous menu item
MacAlternate bool
// Tooltip // Tooltip
Tooltip string Tooltip string

View File

@@ -14,4 +14,10 @@ type TrayMenu struct {
// Menu is the initial menu we wish to use for the tray // Menu is the initial menu we wish to use for the tray
Menu *Menu Menu *Menu
// OnOpen is called when the Menu is opened
OnOpen func()
// OnClose is called when the Menu is closed
OnClose func()
} }