diff --git a/example/main.go b/example/main.go index c6dbaae..2a9f78c 100644 --- a/example/main.go +++ b/example/main.go @@ -44,6 +44,12 @@ func onReady() { mEnabled.SetTemplateIcon(icon.Data, icon.Data) systray.AddMenuItem("Ignored", "Ignored") + + subMenuTop := systray.AddMenuItem("SubMenu", "SubMenu Test (top)") + subMenuMiddle := subMenuTop.AddSubMenuItem("SubMenu - Level 2", "SubMenu Test (middle)") + subMenuBottom := subMenuMiddle.AddSubMenuItem("SubMenu - Level 3", "SubMenu Test (bottom)") + subMenuBottom2 := subMenuMiddle.AddSubMenuItem("Panic!", "SubMenu Test (bottom)") + mUrl := systray.AddMenuItem("Open UI", "my home") mQuit := systray.AddMenuItem("退出", "Quit the whole app") @@ -53,6 +59,22 @@ func onReady() { systray.AddSeparator() mToggle := systray.AddMenuItem("Toggle", "Toggle the Quit button") shown := true + toggle := func() { + if shown { + subMenuBottom.Check() + subMenuBottom2.Hide() + mQuitOrig.Hide() + mEnabled.Hide() + shown = false + } else { + subMenuBottom.Uncheck() + subMenuBottom2.Show() + mQuitOrig.Show() + mEnabled.Show() + shown = true + } + } + for { select { case <-mChange.ClickedCh: @@ -70,16 +92,12 @@ func onReady() { mEnabled.Disable() case <-mUrl.ClickedCh: systray.ShowAppWindow("https://www.github.com/getlantern/lantern") + case <-subMenuBottom2.ClickedCh: + panic("panic button pressed") + case <-subMenuBottom.ClickedCh: + toggle() case <-mToggle.ClickedCh: - if shown { - mQuitOrig.Hide() - mEnabled.Hide() - shown = false - } else { - mQuitOrig.Show() - mEnabled.Show() - shown = true - } + toggle() case <-mQuit.ClickedCh: systray.Quit() fmt.Println("Quit2 now...") diff --git a/go.sum b/go.sum index 3bdd53e..5219466 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,7 @@ github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgF github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/systray.go b/systray.go index 750e5f3..9e84eaa 100644 --- a/systray.go +++ b/systray.go @@ -1,13 +1,13 @@ /* -Package systray is a cross platfrom Go library to place an icon and menu in the -notification area. -Supports Windows, Mac OSX and Linux currently. +Package systray is a cross-platform Go library to place an icon and menu in the notification area. + Methods can be called from any goroutine except Run(), which should be called at the very beginning of main() to lock at main thread. */ package systray import ( + "fmt" "runtime" "sync" "sync/atomic" @@ -36,6 +36,28 @@ type MenuItem struct { disabled bool // checked menu item has a tick before the title checked bool + // parent item, for sub menus + parent *MenuItem +} + +func (item *MenuItem) String() string { + if item.parent == nil { + return fmt.Sprintf("MenuItem[%d, %q]", item.id, item.title) + } + return fmt.Sprintf("MenuItem[%d, parent %d, %q]", item.id, item.parent.id, item.title) +} + +// newMenuItem returns a populated MenuItem object +func newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem { + return &MenuItem{ + ClickedCh: make(chan struct{}), + id: atomic.AddInt32(¤tID, 1), + title: title, + tooltip: tooltip, + disabled: false, + checked: false, + parent: parent, + } } var ( @@ -92,14 +114,11 @@ func Quit() { } } -// AddMenuItem adds menu item with designated title and tooltip, returning a channel -// that notifies whenever that menu item is clicked. +// AddMenuItem adds a menu item with the designated title and tooltip. // // It can be safely invoked from different goroutines. func AddMenuItem(title string, tooltip string) *MenuItem { - id := atomic.AddInt32(¤tID, 1) - item := &MenuItem{nil, id, title, tooltip, false, false} - item.ClickedCh = make(chan struct{}) + item := newMenuItem(title, tooltip, nil) item.update() return item } @@ -109,6 +128,15 @@ func AddSeparator() { addSeparator(atomic.AddInt32(¤tID, 1)) } +// AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip. +// +// It can be safely invoked from different goroutines. +func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem { + child := newMenuItem(title, tooltip, item) + child.update() + return child +} + // SetTitle set the text to display on a menu item func (item *MenuItem) SetTitle(title string) { item.title = title @@ -121,7 +149,7 @@ func (item *MenuItem) SetTooltip(tooltip string) { item.update() } -// Disabled checkes if the menu item is disabled +// Disabled checks if the menu item is disabled func (item *MenuItem) Disabled() bool { return item.disabled } @@ -165,7 +193,7 @@ func (item *MenuItem) Uncheck() { item.update() } -// update propogates changes on a menu item to systray +// update propagates changes on a menu item to systray func (item *MenuItem) update() { menuItemsLock.Lock() defer menuItemsLock.Unlock() diff --git a/systray.h b/systray.h index 29ba436..c064e4a 100644 --- a/systray.h +++ b/systray.h @@ -11,8 +11,8 @@ void setTitle(char* title); void setTooltip(char* tooltip); void configureAppWindow(char* title, int width, int height); void showAppWindow(char* url); -void add_or_update_menu_item(int menuId, char* title, char* tooltip, short disabled, short checked); +void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked); void add_separator(int menuId); void hide_menu_item(int menuId); void show_menu_item(int menuId); -void quit(); +void quit(); \ No newline at end of file diff --git a/systray_darwin.m b/systray_darwin.m index 84a869f..bd14dcc 100644 --- a/systray_darwin.m +++ b/systray_darwin.m @@ -17,12 +17,14 @@ { @public NSNumber* menuId; + NSNumber* parentMenuId; NSString* title; NSString* tooltip; short disabled; short checked; } -(id) initWithId: (int)theMenuId +withParentMenuId: (int)theParentMenuId withTitle: (const char*)theTitle withTooltip: (const char*)theTooltip withDisabled: (short)theDisabled @@ -30,12 +32,14 @@ @end @implementation MenuItem -(id) initWithId: (int)theMenuId + withParentMenuId: (int)theParentMenuId withTitle: (const char*)theTitle withTooltip: (const char*)theTooltip withDisabled: (short)theDisabled withChecked: (short)theChecked { menuId = [NSNumber numberWithInt:theMenuId]; + parentMenuId = [NSNumber numberWithInt:theParentMenuId]; title = [[NSString alloc] initWithCString:theTitle encoding:NSUTF8StringEncoding]; tooltip = [[NSString alloc] initWithCString:theTooltip @@ -113,20 +117,30 @@ systray_menu_item_selected(menuId.intValue); } -- (void) add_or_update_menu_item:(MenuItem*) item -{ - NSMenuItem* menuItem; - int existedMenuIndex = [menu indexOfItemWithRepresentedObject: item->menuId]; - if (existedMenuIndex == -1) { - menuItem = [menu addItemWithTitle:item->title action:@selector(menuHandler:) keyEquivalent:@""]; - [menuItem setTarget:self]; - [menuItem setRepresentedObject: item->menuId]; - +- (void)add_or_update_menu_item:(MenuItem *)item { + NSMenu *theMenu = self->menu; + NSMenuItem *parentItem; + if ([item->parentMenuId integerValue] > 0) { + parentItem = find_menu_item(menu, item->parentMenuId); + if (parentItem.hasSubmenu) { + theMenu = parentItem.submenu; + } else { + theMenu = [[NSMenu alloc] init]; + [parentItem setSubmenu:theMenu]; + } } - else { - menuItem = [menu itemAtIndex: existedMenuIndex]; - [menuItem setTitle:item->title]; + + NSMenuItem *menuItem; + menuItem = find_menu_item(theMenu, item->menuId); + if (menuItem == NULL) { + menuItem = [theMenu addItemWithTitle:item->title + action:@selector(menuHandler:) + keyEquivalent:@""]; + [menuItem setRepresentedObject:item->menuId]; } + [menuItem setTitle:item->title]; + [menuItem setTag:[item->menuId integerValue]]; + [menuItem setTarget:self]; [menuItem setToolTip:item->tooltip]; if (item->disabled == 1) { menuItem.enabled = FALSE; @@ -140,6 +154,26 @@ } } +NSMenuItem *find_menu_item(NSMenu *ourMenu, NSNumber *menuId) { + NSMenuItem *foundItem = [ourMenu itemWithTag:[menuId integerValue]]; + if (foundItem != NULL) { + return foundItem; + } + NSArray *menu_items = ourMenu.itemArray; + int i; + for (i = 0; i < [menu_items count]; i++) { + NSMenuItem *i_item = [menu_items objectAtIndex:i]; + if (i_item.hasSubmenu) { + foundItem = find_menu_item(i_item.submenu, menuId); + if (foundItem != NULL) { + return foundItem; + } + } + } + + return NULL; +}; + - (void) add_separator:(NSNumber*) menuId { [menu addItem: [NSMenuItem separatorItem]]; @@ -147,37 +181,30 @@ - (void) hide_menu_item:(NSNumber*) menuId { - NSMenuItem* menuItem; - int existedMenuIndex = [menu indexOfItemWithRepresentedObject: menuId]; - if (existedMenuIndex == -1) { - return; + NSMenuItem* menuItem = find_menu_item(menu, menuId); + if (menuItem != NULL) { + [menuItem setHidden:TRUE]; } - menuItem = [menu itemAtIndex: existedMenuIndex]; - [menuItem setHidden:TRUE]; } -- (void)setMenuItemIcon:(NSArray*)imageAndMenuId { +- (void) setMenuItemIcon:(NSArray*)imageAndMenuId { NSImage* image = [imageAndMenuId objectAtIndex:0]; NSNumber* menuId = [imageAndMenuId objectAtIndex:1]; NSMenuItem* menuItem; - int existedMenuIndex = [menu indexOfItemWithRepresentedObject: menuId]; - if (existedMenuIndex == -1) { + menuItem = find_menu_item(menu, menuId); + if (menuItem == NULL) { return; } - menuItem = [menu itemAtIndex: existedMenuIndex]; menuItem.image = image; } - (void) show_menu_item:(NSNumber*) menuId { - NSMenuItem* menuItem; - int existedMenuIndex = [menu indexOfItemWithRepresentedObject: menuId]; - if (existedMenuIndex == -1) { - return; + NSMenuItem* menuItem = find_menu_item(menu, menuId); + if (menuItem != NULL) { + [menuItem setHidden:FALSE]; } - menuItem = [menu itemAtIndex: existedMenuIndex]; - [menuItem setHidden:FALSE]; } - (void) quit @@ -235,8 +262,8 @@ void setTooltip(char* ctooltip) { runInMainThread(@selector(setTooltip:), (id)tooltip); } -void add_or_update_menu_item(int menuId, char* title, char* tooltip, short disabled, short checked) { - MenuItem* item = [[MenuItem alloc] initWithId: menuId withTitle: title withTooltip: tooltip withDisabled: disabled withChecked: checked]; +void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked) { + MenuItem* item = [[MenuItem alloc] initWithId: menuId withParentMenuId: parentMenuId withTitle: title withTooltip: tooltip withDisabled: disabled withChecked: checked]; free(title); free(tooltip); runInMainThread(@selector(add_or_update_menu_item:), (id)item); diff --git a/systray_linux.c b/systray_linux.c index 4439047..50ff62f 100644 --- a/systray_linux.c +++ b/systray_linux.c @@ -189,7 +189,8 @@ void setTooltip(char* ctooltip) { void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template) { } -void add_or_update_menu_item(int menu_id, char* title, char* tooltip, short disabled, short checked) { +void add_or_update_menu_item(int menu_id, int parent_menu_id, char* title, char* tooltip, short disabled, short checked) { + // TODO: add support for sub-menus MenuItemInfo *mii = malloc(sizeof(MenuItemInfo)); mii->menu_id = menu_id; mii->title = title; diff --git a/systray_nonwindows.go b/systray_nonwindows.go index 96337ff..b564879 100644 --- a/systray_nonwindows.go +++ b/systray_nonwindows.go @@ -58,8 +58,13 @@ func addOrUpdateMenuItem(item *MenuItem) { if item.checked { checked = 1 } + var parentID int32 = 0 + if item.parent != nil { + parentID = item.parent.id + } C.add_or_update_menu_item( C.int(item.id), + C.int(parentID), C.CString(item.title), C.CString(item.tooltip), disabled, diff --git a/systray_windows.go b/systray_windows.go index 6e8e3de..e40874a 100644 --- a/systray_windows.go +++ b/systray_windows.go @@ -180,6 +180,7 @@ func addOrUpdateMenuItem(item *MenuItem) { if err != nil { fail("Unable to set menu item enabled", err) } + // TODO: add support for sub-menus } // SetIcon sets the icon of a menu item. Only works on macOS and Windows.