mirror of
https://github.com/nkanaev/yarr.git
synced 2025-11-07 18:09:36 +00:00
include systray
This commit is contained in:
233
src/systray/systray.go
Normal file
233
src/systray/systray.go
Normal file
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
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()
|
||||
menuItems = make(map[uint32]*MenuItem)
|
||||
menuItemsLock sync.RWMutex
|
||||
|
||||
currentID = uint32(0)
|
||||
quitOnce sync.Once
|
||||
)
|
||||
|
||||
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 func(), onExit func()) {
|
||||
Register(onReady, onExit)
|
||||
nativeLoop()
|
||||
}
|
||||
|
||||
// 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
|
||||
registerSystray()
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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("No menu item with ID %v", id)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case item.ClickedCh <- struct{}{}:
|
||||
// in case no one waiting for the channel
|
||||
default:
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user