Merge support for nested menus on macOS, from https://github.com/Arshiamidos/systray

This commit is contained in:
Thomas Stromberg
2019-11-13 12:12:33 -08:00
parent 1a6b33f303
commit e67244fe47
8 changed files with 192 additions and 30 deletions
+8
View File
@@ -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()
+4
View File
@@ -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=
+38 -8
View File
@@ -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(&currentID, 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(&currentID, 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(&currentID, 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()
+2 -1
View File
@@ -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();
+116 -14
View File
@@ -48,6 +48,7 @@
@interface AppDelegate: NSObject <NSApplicationDelegate>
- (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);
+5
View File
@@ -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;
+18 -7
View File
@@ -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.
+1
View File
@@ -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) {