Files

300 lines
6.6 KiB
Go
Raw Permalink Normal View History

2023-01-18 21:42:49 +11:00
package application
2023-06-30 21:17:50 +10:00
import (
"fmt"
2023-07-06 20:42:13 +10:00
"runtime"
"sync"
"time"
2023-09-11 17:08:19 -05:00
"github.com/wailsapp/wails/v3/pkg/events"
2023-06-30 21:17:50 +10:00
)
2023-01-18 21:42:49 +11:00
type IconPosition int
const (
NSImageNone = iota
NSImageOnly
NSImageLeft
NSImageRight
NSImageBelow
NSImageAbove
NSImageOverlaps
NSImageLeading
NSImageTrailing
)
type systemTrayImpl interface {
setLabel(label string)
run()
setIcon(icon []byte)
setMenu(menu *Menu)
setIconPosition(position int)
setTemplateIcon(icon []byte)
destroy()
2023-05-07 20:19:37 +10:00
setDarkModeIcon(icon []byte)
2023-06-30 21:17:50 +10:00
bounds() (*Rect, error)
getScreen() (*Screen, error)
positionWindow(window *WebviewWindow, offset int) error
2023-09-21 19:14:44 +10:00
openMenu()
}
type PositionOptions struct {
Buffer int
2023-01-18 21:42:49 +11:00
}
type SystemTray struct {
id uint
label string
icon []byte
2023-05-07 20:19:37 +10:00
darkModeIcon []byte
2023-01-18 21:42:49 +11:00
iconPosition int
clickHandler func()
rightClickHandler func()
doubleClickHandler func()
rightDoubleClickHandler func()
mouseEnterHandler func()
mouseLeaveHandler func()
onMenuOpen func()
onMenuClose func()
2023-05-07 19:13:07 +10:00
2023-01-18 21:42:49 +11:00
// Platform specific implementation
impl systemTrayImpl
menu *Menu
isTemplateIcon bool
2023-07-06 20:42:13 +10:00
attachedWindow WindowAttachConfig
2023-01-18 21:42:49 +11:00
}
2023-09-24 08:57:22 +10:00
func newSystemTray(id uint) *SystemTray {
2023-07-06 20:42:13 +10:00
result := &SystemTray{
2023-01-18 21:42:49 +11:00
id: id,
label: "",
iconPosition: NSImageLeading,
2023-07-06 20:42:13 +10:00
attachedWindow: WindowAttachConfig{
Window: nil,
Offset: 0,
Debounce: 200 * time.Millisecond,
},
2023-01-18 21:42:49 +11:00
}
2023-07-06 20:42:13 +10:00
result.clickHandler = result.defaultClickHandler
return result
2023-01-18 21:42:49 +11:00
}
func (s *SystemTray) SetLabel(label string) {
if s.impl == nil {
s.label = label
return
}
2023-09-24 08:57:22 +10:00
InvokeSync(func() {
2023-05-07 10:34:08 +10:00
s.impl.setLabel(label)
})
2023-01-18 21:42:49 +11:00
}
func (s *SystemTray) Label() string {
return s.label
}
2023-09-28 11:38:59 -05:00
func (s *SystemTray) Run() {
2023-01-18 21:42:49 +11:00
s.impl = newSystemTrayImpl(s)
2023-07-06 20:42:13 +10:00
if s.attachedWindow.Window != nil {
// Setup listener
s.attachedWindow.Window.On(events.Common.WindowLostFocus, func(event *WindowEvent) {
2023-07-06 20:42:13 +10:00
s.attachedWindow.Window.Hide()
// Special handler for Windows
if runtime.GOOS == "windows" {
// We don't do this unless the window has already been shown
if s.attachedWindow.hasBeenShown == false {
return
}
s.attachedWindow.justClosed = true
go func() {
time.Sleep(s.attachedWindow.Debounce)
s.attachedWindow.justClosed = false
}()
}
})
}
2023-09-24 08:57:22 +10:00
InvokeSync(s.impl.run)
2023-01-18 21:42:49 +11:00
}
func (s *SystemTray) PositionWindow(window *WebviewWindow, offset int) error {
2023-06-30 21:17:50 +10:00
if s.impl == nil {
return fmt.Errorf("system tray not running")
}
2023-09-24 08:57:22 +10:00
return InvokeSyncWithError(func() error {
return s.impl.positionWindow(window, offset)
2023-06-30 21:17:50 +10:00
})
}
2023-01-18 21:42:49 +11:00
func (s *SystemTray) SetIcon(icon []byte) *SystemTray {
if s.impl == nil {
s.icon = icon
} else {
2023-09-24 08:57:22 +10:00
InvokeSync(func() {
2023-05-07 10:34:08 +10:00
s.impl.setIcon(icon)
})
2023-01-18 21:42:49 +11:00
}
return s
}
2023-05-07 20:19:37 +10:00
func (s *SystemTray) SetDarkModeIcon(icon []byte) *SystemTray {
if s.impl == nil {
s.darkModeIcon = icon
} else {
2023-09-24 08:57:22 +10:00
InvokeSync(func() {
2023-05-07 20:19:37 +10:00
s.impl.setDarkModeIcon(icon)
})
}
return s
}
2023-01-18 21:42:49 +11:00
func (s *SystemTray) SetMenu(menu *Menu) *SystemTray {
if s.impl == nil {
s.menu = menu
} else {
2023-09-24 08:57:22 +10:00
InvokeSync(func() {
2023-05-07 10:34:08 +10:00
s.impl.setMenu(menu)
})
2023-01-18 21:42:49 +11:00
}
return s
}
func (s *SystemTray) SetIconPosition(iconPosition int) *SystemTray {
if s.impl == nil {
s.iconPosition = iconPosition
} else {
2023-09-24 08:57:22 +10:00
InvokeSync(func() {
2023-05-07 10:34:08 +10:00
s.impl.setIconPosition(iconPosition)
})
2023-01-18 21:42:49 +11:00
}
return s
}
func (s *SystemTray) SetTemplateIcon(icon []byte) *SystemTray {
if s.impl == nil {
s.icon = icon
s.isTemplateIcon = true
} else {
2023-09-24 08:57:22 +10:00
InvokeSync(func() {
2023-05-07 10:34:08 +10:00
s.impl.setTemplateIcon(icon)
})
2023-01-18 21:42:49 +11:00
}
return s
}
func (s *SystemTray) Destroy() {
if s.impl == nil {
return
}
s.impl.destroy()
}
2023-05-08 19:43:58 +10:00
func (s *SystemTray) OnClick(handler func()) *SystemTray {
s.clickHandler = handler
2023-05-08 19:43:58 +10:00
return s
}
func (s *SystemTray) OnRightClick(handler func()) *SystemTray {
s.rightClickHandler = handler
2023-05-08 19:43:58 +10:00
return s
}
func (s *SystemTray) OnDoubleClick(handler func()) *SystemTray {
s.doubleClickHandler = handler
2023-05-08 19:43:58 +10:00
return s
}
func (s *SystemTray) OnRightDoubleClick(handler func()) *SystemTray {
s.rightDoubleClickHandler = handler
2023-05-08 19:43:58 +10:00
return s
}
func (s *SystemTray) OnMouseEnter(handler func()) *SystemTray {
s.mouseEnterHandler = handler
return s
}
func (s *SystemTray) OnMouseLeave(handler func()) *SystemTray {
s.mouseLeaveHandler = handler
return s
}
2023-07-06 20:42:13 +10:00
type WindowAttachConfig struct {
// Window is the window to attach to the system tray. If it's null, the request to attach will be ignored.
Window *WebviewWindow
// Offset indicates the gap in pixels between the system tray and the window
Offset int
// Debounce is used by Windows to indicate how long to wait before responding to a mouse
// up event on the notification icon. See https://stackoverflow.com/questions/4585283/alternate-showing-hiding-window-when-notify-icon-is-clicked
Debounce time.Duration
// Indicates that the window has just been closed
justClosed bool
// Indicates that the window has been shown a first time
hasBeenShown bool
// Used to ensure that the window state is read on first click
initialClick sync.Once
}
// AttachWindow attaches a window to the system tray. The window will be shown when the system tray icon is clicked.
// The window will be hidden when the system tray icon is clicked again, or when the window loses focus.
func (s *SystemTray) AttachWindow(window *WebviewWindow) *SystemTray {
s.attachedWindow.Window = window
return s
}
// WindowOffset sets the gap in pixels between the system tray and the window
func (s *SystemTray) WindowOffset(offset int) *SystemTray {
s.attachedWindow.Offset = offset
return s
}
// WindowDebounce is used by Windows to indicate how long to wait before responding to a mouse
// up event on the notification icon. This prevents the window from being hidden and then immediately
// shown when the user clicks on the system tray icon.
// See https://stackoverflow.com/questions/4585283/alternate-showing-hiding-window-when-notify-icon-is-clicked
func (s *SystemTray) WindowDebounce(debounce time.Duration) *SystemTray {
s.attachedWindow.Debounce = debounce
return s
}
func (s *SystemTray) defaultClickHandler() {
if s.attachedWindow.Window == nil {
s.OpenMenu()
2023-07-06 20:42:13 +10:00
return
}
// Check the initial visibility state
s.attachedWindow.initialClick.Do(func() {
s.attachedWindow.hasBeenShown = s.attachedWindow.Window.IsVisible()
})
if runtime.GOOS == "windows" && s.attachedWindow.justClosed {
return
}
if s.attachedWindow.Window.IsVisible() {
s.attachedWindow.Window.Hide()
} else {
s.attachedWindow.hasBeenShown = true
_ = s.PositionWindow(s.attachedWindow.Window, s.attachedWindow.Offset)
s.attachedWindow.Window.Show().Focus()
}
}
2023-09-21 19:14:44 +10:00
func (s *SystemTray) OpenMenu() {
if s.menu == nil {
return
}
if s.impl == nil {
return
}
2023-09-24 08:57:22 +10:00
InvokeSync(s.impl.openMenu)
2023-09-21 19:14:44 +10:00
}