From 28e8b7966825d057d9b8c3e3d297cedb47945321 Mon Sep 17 00:00:00 2001 From: Romuald R Date: Sat, 30 Nov 2019 18:34:23 +0100 Subject: [PATCH] [feat] Windows support for submenus --- systray.go | 37 ++++++++++++++++++++++++++++++++++--- systray_windows.go | 46 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/systray.go b/systray.go index 750e5f3..7b4aea6 100644 --- a/systray.go +++ b/systray.go @@ -8,6 +8,7 @@ at the very beginning of main() to lock at main thread. package systray import ( + "fmt" "runtime" "sync" "sync/atomic" @@ -36,6 +37,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 +120,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 +130,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 diff --git a/systray_windows.go b/systray_windows.go index 11893b1..04be6f1 100644 --- a/systray_windows.go +++ b/systray_windows.go @@ -18,6 +18,7 @@ var ( notifyIcon *walk.NotifyIcon actions = make(map[int32]*walk.Action) + menus = make(map[int32]*walk.Menu) nextActionId int32 okayToClose int32 @@ -119,11 +120,39 @@ func ShowAppWindow(url string) { mainWindow.SetVisible(true) } -func addOrUpdateMenuItem(item *MenuItem) { +func getOrCreateMenu(item *MenuItem) *walk.Menu { + if item == nil { + return notifyIcon.ContextMenu() + } + menu := menus[item.id] + if menu != nil { + return menu + } + menu, err := walk.NewMenu() + if err != nil { + fail("Unable to create new menu", err) + } + menus[item.id] = menu + action := actions[item.id] + // If we already have an action in array, it means an action is already created (as a simple action) + // Get parent menu to remove it and create a menu entry instead + if action != nil { + parent := getOrCreateMenu(item.parent) + parent.Actions().Remove(action) + actions[item.id] = nil + updateAction(item, getOrCreateAction(item, menu)) + } + return menu +} + +func getOrCreateAction(item *MenuItem, menu *walk.Menu) *walk.Action { action := actions[item.id] if action == nil { - item.id = nextActionId - action = walk.NewAction() + if menu != nil { + action = walk.NewMenuAction(menu) + } else { + action = walk.NewAction() + } action.Triggered().Attach(func() { select { case item.ClickedCh <- struct{}{}: @@ -132,12 +161,15 @@ func addOrUpdateMenuItem(item *MenuItem) { // no listener, ignore } }) - if err := notifyIcon.ContextMenu().Actions().Add(action); err != nil { + if err := getOrCreateMenu(item.parent).Actions().Add(action); err != nil { fail("Unable to add menu item to systray", err) } actions[item.id] = action - atomic.AddInt32(&nextActionId, 1) } + return action +} + +func updateAction(item *MenuItem, action *walk.Action) { err := action.SetText(item.title) if err != nil { fail("Unable to set menu item text", err) @@ -152,6 +184,10 @@ func addOrUpdateMenuItem(item *MenuItem) { } } +func addOrUpdateMenuItem(item *MenuItem) { + updateAction(item, getOrCreateAction(item, nil)) +} + func (item *MenuItem) SetIcon(iconBytes []byte) { filename := fmt.Sprintf("systray.%d.ico", item.id) err := ioutil.WriteFile(filename, iconBytes, 0644)