mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-04 23:59:52 +08:00
334 lines
7.2 KiB
Go
334 lines
7.2 KiB
Go
package application
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/wailsapp/wails/v3/pkg/events"
|
|
)
|
|
|
|
type IconPosition int
|
|
|
|
const (
|
|
NSImageNone = iota
|
|
NSImageOnly
|
|
NSImageLeft
|
|
NSImageRight
|
|
NSImageBelow
|
|
NSImageAbove
|
|
NSImageOverlaps
|
|
NSImageLeading
|
|
NSImageTrailing
|
|
)
|
|
|
|
type systemTrayImpl interface {
|
|
setLabel(label string)
|
|
setTooltip(tooltip string)
|
|
run()
|
|
setIcon(icon []byte)
|
|
setMenu(menu *Menu)
|
|
setIconPosition(position IconPosition)
|
|
setTemplateIcon(icon []byte)
|
|
destroy()
|
|
setDarkModeIcon(icon []byte)
|
|
bounds() (*Rect, error)
|
|
getScreen() (*Screen, error)
|
|
positionWindow(window *WebviewWindow, offset int) error
|
|
openMenu()
|
|
Show()
|
|
Hide()
|
|
}
|
|
|
|
type SystemTray struct {
|
|
id uint
|
|
label string
|
|
tooltip string
|
|
icon []byte
|
|
darkModeIcon []byte
|
|
iconPosition IconPosition
|
|
|
|
clickHandler func()
|
|
rightClickHandler func()
|
|
doubleClickHandler func()
|
|
rightDoubleClickHandler func()
|
|
mouseEnterHandler func()
|
|
mouseLeaveHandler func()
|
|
onMenuOpen func()
|
|
onMenuClose func()
|
|
|
|
// Platform specific implementation
|
|
impl systemTrayImpl
|
|
menu *Menu
|
|
isTemplateIcon bool
|
|
attachedWindow WindowAttachConfig
|
|
}
|
|
|
|
func newSystemTray(id uint) *SystemTray {
|
|
result := &SystemTray{
|
|
id: id,
|
|
label: "",
|
|
tooltip: "",
|
|
iconPosition: NSImageLeading,
|
|
attachedWindow: WindowAttachConfig{
|
|
Window: nil,
|
|
Offset: 0,
|
|
Debounce: 200 * time.Millisecond,
|
|
},
|
|
}
|
|
result.clickHandler = result.defaultClickHandler
|
|
return result
|
|
}
|
|
|
|
func (s *SystemTray) SetLabel(label string) {
|
|
if s.impl == nil {
|
|
s.label = label
|
|
return
|
|
}
|
|
InvokeSync(func() {
|
|
s.impl.setLabel(label)
|
|
})
|
|
}
|
|
|
|
func (s *SystemTray) Label() string {
|
|
return s.label
|
|
}
|
|
|
|
func (s *SystemTray) Run() {
|
|
s.impl = newSystemTrayImpl(s)
|
|
|
|
if s.attachedWindow.Window != nil {
|
|
// Setup listener
|
|
s.attachedWindow.Window.OnWindowEvent(events.Common.WindowLostFocus, func(event *WindowEvent) {
|
|
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() {
|
|
defer handlePanic()
|
|
time.Sleep(s.attachedWindow.Debounce)
|
|
s.attachedWindow.justClosed = false
|
|
}()
|
|
}
|
|
})
|
|
}
|
|
|
|
InvokeSync(s.impl.run)
|
|
}
|
|
|
|
func (s *SystemTray) PositionWindow(window *WebviewWindow, offset int) error {
|
|
if s.impl == nil {
|
|
return fmt.Errorf("system tray not running")
|
|
}
|
|
return InvokeSyncWithError(func() error {
|
|
return s.impl.positionWindow(window, offset)
|
|
})
|
|
}
|
|
|
|
func (s *SystemTray) SetIcon(icon []byte) *SystemTray {
|
|
if s.impl == nil {
|
|
s.icon = icon
|
|
} else {
|
|
InvokeSync(func() {
|
|
s.impl.setIcon(icon)
|
|
})
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *SystemTray) SetDarkModeIcon(icon []byte) *SystemTray {
|
|
if s.impl == nil {
|
|
s.darkModeIcon = icon
|
|
} else {
|
|
InvokeSync(func() {
|
|
s.impl.setDarkModeIcon(icon)
|
|
})
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *SystemTray) SetMenu(menu *Menu) *SystemTray {
|
|
if s.impl == nil {
|
|
s.menu = menu
|
|
} else {
|
|
InvokeSync(func() {
|
|
s.impl.setMenu(menu)
|
|
})
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *SystemTray) SetIconPosition(iconPosition IconPosition) *SystemTray {
|
|
if s.impl == nil {
|
|
s.iconPosition = iconPosition
|
|
} else {
|
|
InvokeSync(func() {
|
|
s.impl.setIconPosition(iconPosition)
|
|
})
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *SystemTray) SetTemplateIcon(icon []byte) *SystemTray {
|
|
if s.impl == nil {
|
|
s.icon = icon
|
|
s.isTemplateIcon = true
|
|
} else {
|
|
InvokeSync(func() {
|
|
s.impl.setTemplateIcon(icon)
|
|
})
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *SystemTray) SetTooltip(tooltip string) {
|
|
if s.impl == nil {
|
|
s.tooltip = tooltip
|
|
return
|
|
}
|
|
InvokeSync(func() {
|
|
s.impl.setTooltip(tooltip)
|
|
})
|
|
}
|
|
|
|
func (s *SystemTray) Destroy() {
|
|
globalApplication.destroySystemTray(s)
|
|
}
|
|
|
|
func (s *SystemTray) destroy() {
|
|
if s.impl == nil {
|
|
return
|
|
}
|
|
s.impl.destroy()
|
|
}
|
|
|
|
func (s *SystemTray) OnClick(handler func()) *SystemTray {
|
|
s.clickHandler = handler
|
|
return s
|
|
}
|
|
|
|
func (s *SystemTray) OnRightClick(handler func()) *SystemTray {
|
|
s.rightClickHandler = handler
|
|
return s
|
|
}
|
|
|
|
func (s *SystemTray) OnDoubleClick(handler func()) *SystemTray {
|
|
s.doubleClickHandler = handler
|
|
return s
|
|
}
|
|
|
|
func (s *SystemTray) OnRightDoubleClick(handler func()) *SystemTray {
|
|
s.rightDoubleClickHandler = handler
|
|
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
|
|
}
|
|
|
|
func (s *SystemTray) Show() {
|
|
if s.impl == nil {
|
|
return
|
|
}
|
|
InvokeSync(func() {
|
|
s.impl.Show()
|
|
})
|
|
}
|
|
|
|
func (s *SystemTray) Hide() {
|
|
if s.impl == nil {
|
|
return
|
|
}
|
|
InvokeSync(func() {
|
|
s.impl.Hide()
|
|
})
|
|
}
|
|
|
|
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()
|
|
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()
|
|
}
|
|
}
|
|
|
|
func (s *SystemTray) OpenMenu() {
|
|
if s.menu == nil {
|
|
return
|
|
}
|
|
if s.impl == nil {
|
|
return
|
|
}
|
|
InvokeSync(s.impl.openMenu)
|
|
}
|