| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- // Package systray is a cross-platform Go library to place an icon and menu in the notification area.
- package systray
- import (
- "fmt"
- "log"
- "runtime"
- "sync"
- "sync/atomic"
- )
- var (
- systrayReady func()
- systrayExit func()
- systrayExitCalled bool
- menuItems = make(map[uint32]*MenuItem)
- menuItemsLock sync.RWMutex
- currentID = uint32(0)
- quitOnce sync.Once
- )
- // This helper function allows us to call systrayExit only once,
- // without accidentally calling it twice in the same lifetime.
- func runSystrayExit() {
- if !systrayExitCalled {
- systrayExitCalled = true
- systrayExit()
- }
- }
- 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()) {
- 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)
- }
- // 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]
- menuItemsLock.RUnlock()
- if !ok {
- log.Printf("systray error: no menu item with ID %d\n", id)
- return
- }
- select {
- case item.ClickedCh <- struct{}{}:
- // in case no one waiting for the channel
- default:
- }
- }
|