From 2cce28c386e7cf6d7c2fea4bf2da94bad4e44023 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Wed, 5 Apr 2023 16:17:29 -0400 Subject: [PATCH 1/3] begin refactoring to allow instances to be created and closed separately --- menuitem.go | 146 ++++++++++++++++++++++++++++++++++++++++++ systray.go | 166 +++++------------------------------------------- systray_unix.go | 13 ++-- 3 files changed, 169 insertions(+), 156 deletions(-) create mode 100644 menuitem.go diff --git a/menuitem.go b/menuitem.go new file mode 100644 index 00000000..83c865bd --- /dev/null +++ b/menuitem.go @@ -0,0 +1,146 @@ +package systray + +import ( + "fmt" + "sync/atomic" +) + +// MenuItem is used to keep track each menu item of systray. +// Don't create it directly, use the one systray.AddMenuItem() returned +type MenuItem struct { + // ClickedCh is the channel which will be notified when the menu item is clicked + ClickedCh chan struct{} + + // id uniquely identify a menu item, not supposed to be modified + id uint32 + // title is the text shown on menu item + title string + // tooltip is the text shown when pointing to menu item + tooltip string + // disabled menu item is grayed out and has no effect when clicked + disabled bool + // checked menu item has a tick before the title + checked bool + // has the menu item a checkbox (Linux) + isCheckable 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.AddUint32(¤tID, 1), + title: title, + tooltip: tooltip, + disabled: false, + checked: false, + isCheckable: false, + parent: parent, + } +} + +// AddSeparator adds a separator bar to the submenu +func (item *MenuItem) AddSeparator() { + addSeparator(atomic.AddUint32(¤tID, 1), item.id) +} + +// AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip. +// It can be safely invoked from different goroutines. +// Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddSubMenuItemCheckbox +func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem { + child := newMenuItem(title, tooltip, item) + child.update() + return child +} + +// AddSubMenuItemCheckbox adds a nested sub-menu item with the designated title and tooltip and a checkbox for Linux. +// It can be safely invoked from different goroutines. +// On Windows and OSX this is the same as calling AddSubMenuItem +func (item *MenuItem) AddSubMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { + child := newMenuItem(title, tooltip, item) + child.isCheckable = true + child.checked = checked + child.update() + return child +} + +// SetTitle set the text to display on a menu item +func (item *MenuItem) SetTitle(title string) { + item.title = title + item.update() +} + +// SetTooltip set the tooltip to show when mouse hover +func (item *MenuItem) SetTooltip(tooltip string) { + item.tooltip = tooltip + item.update() +} + +// Disabled checks if the menu item is disabled +func (item *MenuItem) Disabled() bool { + return item.disabled +} + +// Enable a menu item regardless if it's previously enabled or not +func (item *MenuItem) Enable() { + item.disabled = false + item.update() +} + +// Disable a menu item regardless if it's previously disabled or not +func (item *MenuItem) Disable() { + item.disabled = true + item.update() +} + +// Hide hides a menu item +func (item *MenuItem) Hide() { + hideMenuItem(item) +} + +// Remove removes a menu item +func (item *MenuItem) Remove() { + removeMenuItem(item) + menuItemsLock.Lock() + delete(menuItems, item.id) + menuItemsLock.Unlock() +} + +// Show shows a previously hidden menu item +func (item *MenuItem) Show() { + showMenuItem(item) +} + +// Checked returns if the menu item has a check mark +func (item *MenuItem) Checked() bool { + return item.checked +} + +// Check a menu item regardless if it's previously checked or not +func (item *MenuItem) Check() { + item.checked = true + item.update() +} + +// Uncheck a menu item regardless if it's previously unchecked or not +func (item *MenuItem) Uncheck() { + item.checked = false + item.update() +} + +// update propagates changes on a menu item to systray +func (item *MenuItem) update() { + menuItemsLock.Lock() + menuItems[item.id] = item + menuItemsLock.Unlock() + addOrUpdateMenuItem(item) +} diff --git a/systray.go b/systray.go index a4ab1c65..afbc8f2f 100644 --- a/systray.go +++ b/systray.go @@ -2,23 +2,29 @@ package systray import ( - "fmt" "log" "runtime" "sync" "sync/atomic" ) -var ( - systrayReady func() - systrayExit func() - systrayExitCalled bool - menuItems = make(map[uint32]*MenuItem) - menuItemsLock sync.RWMutex +type Icon struct { + native *nativeIcon - currentID = uint32(0) - quitOnce sync.Once -) + id uint32 + items sync.Map +} + +func NewIcon() (*Icon, error) { + native, err := newNativeIcon() + if err != nil { + return nil, err + } + + return &Icon{ + native: native, + }, nil +} // This helper function allows us to call systrayExit only once, // without accidentally calling it twice in the same lifetime. @@ -33,49 +39,6 @@ func init() { runtime.LockOSThread() } -// MenuItem is used to keep track each menu item of systray. -// Don't create it directly, use the one systray.AddMenuItem() returned -type MenuItem struct { - // ClickedCh is the channel which will be notified when the menu item is clicked - ClickedCh chan struct{} - - // id uniquely identify a menu item, not supposed to be modified - id uint32 - // title is the text shown on menu item - title string - // tooltip is the text shown when pointing to menu item - tooltip string - // disabled menu item is grayed out and has no effect when clicked - disabled bool - // checked menu item has a tick before the title - checked bool - // has the menu item a checkbox (Linux) - isCheckable 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.AddUint32(¤tID, 1), - title: title, - tooltip: tooltip, - disabled: false, - checked: false, - isCheckable: false, - parent: parent, - } -} - // Run initializes GUI and starts the event loop, then invokes the onReady // callback. It blocks until systray.Quit() is called. func Run(onReady, onExit func()) { @@ -160,103 +123,6 @@ func AddSeparator() { addSeparator(atomic.AddUint32(¤tID, 1), 0) } -// AddSeparator adds a separator bar to the submenu -func (item *MenuItem) AddSeparator() { - addSeparator(atomic.AddUint32(¤tID, 1), item.id) -} - -// AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip. -// It can be safely invoked from different goroutines. -// Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddSubMenuItemCheckbox -func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem { - child := newMenuItem(title, tooltip, item) - child.update() - return child -} - -// AddSubMenuItemCheckbox adds a nested sub-menu item with the designated title and tooltip and a checkbox for Linux. -// It can be safely invoked from different goroutines. -// On Windows and OSX this is the same as calling AddSubMenuItem -func (item *MenuItem) AddSubMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { - child := newMenuItem(title, tooltip, item) - child.isCheckable = true - child.checked = checked - child.update() - return child -} - -// SetTitle set the text to display on a menu item -func (item *MenuItem) SetTitle(title string) { - item.title = title - item.update() -} - -// SetTooltip set the tooltip to show when mouse hover -func (item *MenuItem) SetTooltip(tooltip string) { - item.tooltip = tooltip - item.update() -} - -// Disabled checks if the menu item is disabled -func (item *MenuItem) Disabled() bool { - return item.disabled -} - -// Enable a menu item regardless if it's previously enabled or not -func (item *MenuItem) Enable() { - item.disabled = false - item.update() -} - -// Disable a menu item regardless if it's previously disabled or not -func (item *MenuItem) Disable() { - item.disabled = true - item.update() -} - -// Hide hides a menu item -func (item *MenuItem) Hide() { - hideMenuItem(item) -} - -// Remove removes a menu item -func (item *MenuItem) Remove() { - removeMenuItem(item) - menuItemsLock.Lock() - delete(menuItems, item.id) - menuItemsLock.Unlock() -} - -// Show shows a previously hidden menu item -func (item *MenuItem) Show() { - showMenuItem(item) -} - -// Checked returns if the menu item has a check mark -func (item *MenuItem) Checked() bool { - return item.checked -} - -// Check a menu item regardless if it's previously checked or not -func (item *MenuItem) Check() { - item.checked = true - item.update() -} - -// Uncheck a menu item regardless if it's previously unchecked or not -func (item *MenuItem) Uncheck() { - item.checked = false - item.update() -} - -// update propagates changes on a menu item to systray -func (item *MenuItem) update() { - menuItemsLock.Lock() - menuItems[item.id] = item - menuItemsLock.Unlock() - addOrUpdateMenuItem(item) -} - func systrayMenuItemSelected(id uint32) { menuItemsLock.RLock() item, ok := menuItems[id] diff --git a/systray_unix.go b/systray_unix.go index cd6ecb9b..9affb9da 100644 --- a/systray_unix.go +++ b/systray_unix.go @@ -29,13 +29,14 @@ const ( menuPath = "/StatusNotifierMenu" ) -var ( - // to signal quitting the internal main loop - quitChan = make(chan struct{}) +type nativeIcon struct { + quit chan struct{} + instance *tray +} - // instance is the current instance of our DBus tray server - instance = &tray{menu: &menuLayout{}, menuVersion: 1} -) +func newNativeIcon() (*nativeIcon, error) { + panic("Not implemented.") +} // SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back // to a regular icon on other platforms. From 486902fc35586d49297cc8511caef6dbd4772778 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Tue, 11 Apr 2023 15:52:43 -0400 Subject: [PATCH 2/3] create a global default icon instance and move related functions into a file with it --- default.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++ systray.go | 90 ----------------------------------------- 2 files changed, 115 insertions(+), 90 deletions(-) create mode 100644 default.go diff --git a/default.go b/default.go new file mode 100644 index 00000000..8200d31a --- /dev/null +++ b/default.go @@ -0,0 +1,115 @@ +package systray + +import ( + "sync" + "sync/atomic" +) + +var ( + defaultIcon *Icon + defaultIconOnce sync.Once +) + +func initDefaultIcon() { + defaultIconOnce.Do(func() { + defaultIcon, _ = NewIcon() + }) +} + +// Run initializes GUI and starts the event loop, then invokes the onReady +// callback. It blocks until systray.Quit() is called. +func Run(onReady, onExit func()) { + initDefaultIcon() + + defaultIcon.setInternalLoop(true) + Register(onReady, onExit) + + defaultIcon.nativeLoop() +} + +// RunWithExternalLoop allows the systemtray module to operate with other tookits. +// The returned start and end functions should be called by the toolkit when the application has started and will end. +func RunWithExternalLoop(onReady, onExit func()) (start, end func()) { + initDefaultIcon() + + Register(onReady, onExit) + + return nativeStart, func() { + nativeEnd() + Quit() + } +} + +// Register initializes GUI and registers the callbacks but relies on the +// caller to run the event loop somewhere else. It's useful if the program +// needs to show other UI elements, for example, webview. +// To overcome some OS weirdness, On macOS versions before Catalina, calling +// this does exactly the same as Run(). +func Register(onReady func(), onExit func()) { + initDefaultIcon() + + if onReady == nil { + systrayReady = func() {} + } else { + // Run onReady on separate goroutine to avoid blocking event loop + readyCh := make(chan interface{}) + go func() { + <-readyCh + onReady() + }() + systrayReady = func() { + close(readyCh) + } + } + // unlike onReady, onExit runs in the event loop to make sure it has time to + // finish before the process terminates + if onExit == nil { + onExit = func() {} + } + systrayExit = onExit + systrayExitCalled = false + defaultIcon.registerSystray() +} + +// ResetMenu will remove all menu items +func ResetMenu() { + initDefaultIcon() + + defaultIcon.resetMenu() +} + +// Quit the systray +func Quit() { + defaultIcon.Quit() +} + +// AddMenuItem adds a menu item with the designated title and tooltip. +// It can be safely invoked from different goroutines. +// Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddMenuItemCheckbox +func AddMenuItem(title string, tooltip string) *MenuItem { + initDefaultIcon() + + item := defaultIcon.newMenuItem(title, tooltip, nil) + item.update() + return item +} + +// AddMenuItemCheckbox adds a menu item with the designated title and tooltip and a checkbox for Linux. +// It can be safely invoked from different goroutines. +// On Windows and OSX this is the same as calling AddMenuItem +func AddMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { + initDefaultIcon() + + item := defaultIcon.newMenuItem(title, tooltip, nil) + item.isCheckable = true + item.checked = checked + item.update() + return item +} + +// AddSeparator adds a separator bar to the menu +func AddSeparator() { + initDefaultIcon() + + defaultIcon.addSeparator(atomic.AddUint32(¤tID, 1), 0) +} diff --git a/systray.go b/systray.go index afbc8f2f..861e6dfa 100644 --- a/systray.go +++ b/systray.go @@ -3,9 +3,7 @@ package systray import ( "log" - "runtime" "sync" - "sync/atomic" ) type Icon struct { @@ -35,94 +33,6 @@ func runSystrayExit() { } } -func init() { - runtime.LockOSThread() -} - -// Run initializes GUI and starts the event loop, then invokes the onReady -// callback. It blocks until systray.Quit() is called. -func Run(onReady, onExit func()) { - setInternalLoop(true) - Register(onReady, onExit) - - nativeLoop() -} - -// RunWithExternalLoop allows the systemtray module to operate with other tookits. -// The returned start and end functions should be called by the toolkit when the application has started and will end. -func RunWithExternalLoop(onReady, onExit func()) (start, end func()) { - Register(onReady, onExit) - - return nativeStart, func() { - nativeEnd() - Quit() - } -} - -// Register initializes GUI and registers the callbacks but relies on the -// caller to run the event loop somewhere else. It's useful if the program -// needs to show other UI elements, for example, webview. -// To overcome some OS weirdness, On macOS versions before Catalina, calling -// this does exactly the same as Run(). -func Register(onReady func(), onExit func()) { - if onReady == nil { - systrayReady = func() {} - } else { - // Run onReady on separate goroutine to avoid blocking event loop - readyCh := make(chan interface{}) - go func() { - <-readyCh - onReady() - }() - systrayReady = func() { - close(readyCh) - } - } - // unlike onReady, onExit runs in the event loop to make sure it has time to - // finish before the process terminates - if onExit == nil { - onExit = func() {} - } - systrayExit = onExit - systrayExitCalled = false - registerSystray() -} - -// ResetMenu will remove all menu items -func ResetMenu() { - resetMenu() -} - -// Quit the systray -func Quit() { - quitOnce.Do(quit) -} - -// AddMenuItem adds a menu item with the designated title and tooltip. -// It can be safely invoked from different goroutines. -// Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddMenuItemCheckbox -func AddMenuItem(title string, tooltip string) *MenuItem { - item := newMenuItem(title, tooltip, nil) - item.update() - return item -} - -// AddMenuItemCheckbox adds a menu item with the designated title and tooltip and a checkbox for Linux. -// It can be safely invoked from different goroutines. -// On Windows and OSX this is the same as calling AddMenuItem -func AddMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { - item := newMenuItem(title, tooltip, nil) - item.isCheckable = true - item.checked = checked - item.update() - return item -} - -// AddSeparator adds a separator bar to the menu -func AddSeparator() { - addSeparator(atomic.AddUint32(¤tID, 1), 0) -} - func systrayMenuItemSelected(id uint32) { menuItemsLock.RLock() item, ok := menuItems[id] From 40a64fb67ea67bb68cefbd7b222eeef7d4513d87 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Tue, 11 Apr 2023 16:02:01 -0400 Subject: [PATCH 3/3] instance a few more things --- default.go | 3 +-- menuitem.go | 15 +++++++++------ systray.go | 5 +++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/default.go b/default.go index 8200d31a..93ac495d 100644 --- a/default.go +++ b/default.go @@ -2,7 +2,6 @@ package systray import ( "sync" - "sync/atomic" ) var ( @@ -111,5 +110,5 @@ func AddMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { func AddSeparator() { initDefaultIcon() - defaultIcon.addSeparator(atomic.AddUint32(¤tID, 1), 0) + defaultIcon.addSeparator(0) } diff --git a/menuitem.go b/menuitem.go index 83c865bd..909537b1 100644 --- a/menuitem.go +++ b/menuitem.go @@ -2,7 +2,6 @@ package systray import ( "fmt" - "sync/atomic" ) // MenuItem is used to keep track each menu item of systray. @@ -23,6 +22,8 @@ type MenuItem struct { checked bool // has the menu item a checkbox (Linux) isCheckable bool + // icon is the icon that the item was created from + icon *Icon // parent item, for sub menus parent *MenuItem } @@ -35,29 +36,30 @@ func (item *MenuItem) String() string { } // newMenuItem returns a populated MenuItem object -func newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem { +func (icon *Icon) newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem { return &MenuItem{ ClickedCh: make(chan struct{}), - id: atomic.AddUint32(¤tID, 1), + id: icon.nextID(), title: title, tooltip: tooltip, disabled: false, checked: false, isCheckable: false, + icon: icon, parent: parent, } } // AddSeparator adds a separator bar to the submenu func (item *MenuItem) AddSeparator() { - addSeparator(atomic.AddUint32(¤tID, 1), item.id) + item.icon.addSeparator(item.id) } // AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip. // It can be safely invoked from different goroutines. // Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddSubMenuItemCheckbox func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem { - child := newMenuItem(title, tooltip, item) + child := item.icon.newMenuItem(title, tooltip, item) child.update() return child } @@ -66,7 +68,7 @@ func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem { // It can be safely invoked from different goroutines. // On Windows and OSX this is the same as calling AddSubMenuItem func (item *MenuItem) AddSubMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { - child := newMenuItem(title, tooltip, item) + child := item.icon.newMenuItem(title, tooltip, item) child.isCheckable = true child.checked = checked child.update() @@ -110,6 +112,7 @@ func (item *MenuItem) Hide() { // Remove removes a menu item func (item *MenuItem) Remove() { removeMenuItem(item) + menuItemsLock.Lock() delete(menuItems, item.id) menuItemsLock.Unlock() diff --git a/systray.go b/systray.go index 861e6dfa..18d84711 100644 --- a/systray.go +++ b/systray.go @@ -4,6 +4,7 @@ package systray import ( "log" "sync" + "sync/atomic" ) type Icon struct { @@ -24,6 +25,10 @@ func NewIcon() (*Icon, error) { }, nil } +func (icon *Icon) nextID() uint32 { + return atomic.AddUint32(&icon.id, 1) +} + // This helper function allows us to call systrayExit only once, // without accidentally calling it twice in the same lifetime. func runSystrayExit() {