diff --git a/example/main.go b/example/main.go index 5efccbd..1736b9a 100644 --- a/example/main.go +++ b/example/main.go @@ -41,6 +41,12 @@ func onReady() { mChecked := systray.AddMenuItem("Unchecked", "Check Me") mEnabled := systray.AddMenuItem("Enabled", "Enabled") systray.AddMenuItem("Ignored", "Ignored") + + subMenuTop := systray.AddMenuItem("SubMenu", "SubMenu Test (top)") + subMenuMiddle := subMenuTop.AddSubMenuItem("SubMenu - Level 2", "SubMenu Test (middle") + 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") @@ -67,6 +73,8 @@ func onReady() { mEnabled.Disable() case <-mUrl.ClickedCh: systray.ShowAppWindow("https://www.getlantern.org") + case <-subMenuBottom2.ClickedCh: + panic("panic button pressed") case <-mToggle.ClickedCh: if shown { mQuitOrig.Hide() diff --git a/go.sum b/go.sum index 9e9e928..2dc9320 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= @@ -21,10 +22,13 @@ github.com/lxn/win v0.0.0-20190919090605-24c5960b03d8 h1:RVMGIuuNgrpGB7I79f6xfhG github.com/lxn/win v0.0.0-20190919090605-24c5960b03d8/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= 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/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e h1:VAzdS5Nw68fbf5RZ8RDVlUvPXNU6Z3jtPCK/qvm4FoQ= github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +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= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 h1:/zi0zzlPHWXYXrO1LjNRByFu8sdGgCkj2JLDdBIB84k= diff --git a/systray.go b/systray.go index 750e5f3..0a1ef36 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 ( @@ -97,9 +119,7 @@ func Quit() { // // 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 +129,16 @@ func AddSeparator() { addSeparator(atomic.AddInt32(¤tID, 1)) } +// AddSubMenuItem adds nested sub-menu item with designated title and tooltip, returning a channel +// that notifies whenever that menu item is clicked. +// +// 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 +151,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 +195,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 128315f..6b92cff 100644 --- a/systray.h +++ b/systray.h @@ -10,7 +10,8 @@ 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_submenu_item(int parent,int menuId, 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 fff2465..d9c6758 100644 --- a/systray_darwin.m +++ b/systray_darwin.m @@ -48,6 +48,7 @@ @interface AppDelegate: NSObject - (void) add_or_update_menu_item:(MenuItem*) item; + - (void) add_or_update_submenu_item:(NSArray*) imageAndMenuId; - (IBAction)menuHandler:(id)sender; @property (assign) IBOutlet NSWindow *window; @end @@ -112,20 +113,21 @@ NSNumber* menuId = [sender representedObject]; 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:@""]; +- (void)add_or_update_menu_item:(MenuItem *)item { + NSMenuItem *menuItem; + menuItem = find_menu_with_parent(menu, item->menuId); + if (menuItem == NULL) { + menuItem = [menu addItemWithTitle:item->title + action:@selector(menuHandler:) + keyEquivalent:@""]; [menuItem setTarget:self]; - [menuItem setRepresentedObject: item->menuId]; + [menuItem setRepresentedObject:item->menuId]; + [menuItem setTag:[item->menuId integerValue]]; - } - else { - menuItem = [menu itemAtIndex: existedMenuIndex]; + } else { [menuItem setTitle:item->title]; + [menuItem setTag:[item->menuId integerValue]]; + [menuItem setTarget:self]; } [menuItem setToolTip:item->tooltip]; if (item->disabled == 1) { @@ -140,6 +142,94 @@ } } +NSMenuItem *find_menu_with_parent(NSMenu *ourMenu, NSNumber *parent) { + NSMenuItem *foundItem = [ourMenu itemWithTag:[parent integerValue]]; + if (foundItem == NULL) { + 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) { + NSMenuItem *foundItem2 = find_menu_with_parent(i_item.submenu, parent); + if (foundItem2 == NULL) { + + } else { + foundItem = foundItem2; + break; + } + } + } + return foundItem; + } else { + return foundItem; + } +}; + +- (void)add_or_update_submenu_item:(NSArray *)imageAndMenuId { + + NSNumber *parent = [imageAndMenuId objectAtIndex:0]; + MenuItem *newItem = [imageAndMenuId objectAtIndex:1]; + + NSMenuItem *foundItem = find_menu_with_parent(menu, parent); + + if (foundItem == NULL) { + NSLog(@"%s", ">>> foundItem == NULL - this should not occur!"); + } + + if (foundItem.hasSubmenu) { + NSMenu *oldMenu = foundItem.submenu; + + NSMenuItem *tempItem = [oldMenu addItemWithTitle:newItem->title + action:@selector(menuHandler:) + keyEquivalent:@""]; + tempItem.tag = [newItem->menuId integerValue]; + tempItem.title = newItem->title; + tempItem.action = @selector(menuHandler:); + tempItem.target = self; + tempItem.representedObject = newItem->menuId; + + //[oldMenu addItem:tempItem]; + if (newItem->disabled == 1) { + tempItem.enabled = FALSE; + } else { + tempItem.enabled = TRUE; + } + if (newItem->checked == 1) { + tempItem.state = NSControlStateValueOn; + } else { + tempItem.state = NSControlStateValueOff; + } + [foundItem setSubmenu:oldMenu]; + } else { + + NSMenu *newMenu = [[NSMenu alloc] init]; + NSMenuItem *tempItem = [newMenu addItemWithTitle:newItem->title + action:@selector(menuHandler:) + keyEquivalent:@""]; + + tempItem.tag = [newItem->menuId integerValue]; + tempItem.title = newItem->title; + tempItem.toolTip = newItem->tooltip; + tempItem.representedObject = newItem->menuId; + + [tempItem setTarget:self]; + + if (newItem->disabled == 1) { + tempItem.enabled = FALSE; + } else { + tempItem.enabled = TRUE; + } + if (newItem->checked == 1) { + tempItem.state = NSControlStateValueOn; + } else { + tempItem.state = NSControlStateValueOff; + } + [foundItem setSubmenu:newMenu]; + } +} + + + - (void) add_separator:(NSNumber*) menuId { [menu addItem: [NSMenuItem separatorItem]]; @@ -161,11 +251,10 @@ NSNumber* menuId = [imageAndMenuId objectAtIndex:1]; NSMenuItem* menuItem; - int existedMenuIndex = [menu indexOfItemWithRepresentedObject: menuId]; - if (existedMenuIndex == -1) { + menuItem = find_menu_with_parent(menu, menuId); + if (menuItem == NULL) { return; } - menuItem = [menu itemAtIndex: existedMenuIndex]; menuItem.image = image; } @@ -243,6 +332,19 @@ void add_or_update_menu_item(int menuId, char* title, char* tooltip, short disab runInMainThread(@selector(add_or_update_menu_item:), (id)item); } +void add_or_update_submenu_item(int parent, int menuId, char *title, + char *tooltip, short disabled, short checked) { + MenuItem *item = [[MenuItem alloc] initWithId:menuId + withTitle:title + withTooltip:tooltip + withDisabled:disabled + withChecked:checked]; + free(title); + free(tooltip); + NSNumber *parent2 = [NSNumber numberWithInt:parent]; + runInMainThread(@selector(add_or_update_submenu_item:), + @[ parent2, (id)item ]); +} void add_separator(int menuId) { NSNumber *mId = [NSNumber numberWithInt:menuId]; runInMainThread(@selector(add_separator:), (id)mId); diff --git a/systray_linux.c b/systray_linux.c index 7b045f2..eddb194 100644 --- a/systray_linux.c +++ b/systray_linux.c @@ -199,6 +199,11 @@ void add_or_update_menu_item(int menu_id, char* title, char* tooltip, short disa g_idle_add(do_add_or_update_menu_item, mii); } +void add_or_update_submenu_item(int parent_id, int menu_id, char* title, char* tooltip, short disabled, short checked) { + // TODO: add support for sub-menus + add_or_update_menu_item(int menu_id, char* title, char* tooltip, short disabled, short checked) ; +} + void add_separator(int menu_id) { MenuItemInfo *mii = malloc(sizeof(MenuItemInfo)); mii->menu_id = menu_id; diff --git a/systray_nonwindows.go b/systray_nonwindows.go index 598df9e..74e0a35 100644 --- a/systray_nonwindows.go +++ b/systray_nonwindows.go @@ -58,13 +58,24 @@ func addOrUpdateMenuItem(item *MenuItem) { if item.checked { checked = 1 } - C.add_or_update_menu_item( - C.int(item.id), - C.CString(item.title), - C.CString(item.tooltip), - disabled, - checked, - ) + if item.parent == nil { + C.add_or_update_menu_item( + C.int(item.id), + C.CString(item.title), + C.CString(item.tooltip), + disabled, + checked, + ) + } else { + C.add_or_update_submenu_item( + C.int(item.parent.id), + C.int(item.id), + C.CString(item.title), + C.CString(item.tooltip), + disabled, + checked, + ) + } } // SetIcon sets the icon of a menu item. Only available on Mac. diff --git a/systray_windows.go b/systray_windows.go index 4f8e731..5ec0367 100644 --- a/systray_windows.go +++ b/systray_windows.go @@ -144,6 +144,7 @@ func addOrUpdateMenuItem(item *MenuItem) { if err != nil { fail("Unable to set menu item enabled", err) } + // TODO: add support for sub-menus } func (item *MenuItem) SetIcon(iconBytes []byte) {