5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 05:11:29 +08:00
wails/v2/internal/platform/systray/systray_windows.go
Lea Anthony b84a2e5255
Windows tray menus (#2181)
* Add example

* Add windows systray

* Add gitkeep

* use windows.GUID
2022-12-06 20:55:56 +11:00

433 lines
9.3 KiB
Go

//go:build windows
/*
* Based on code originally from https://github.com/tadvi/systray. Copyright (C) 2019 The Systray Authors. All Rights Reserved.
*/
package systray
import (
"errors"
"github.com/samber/lo"
"github.com/wailsapp/wails/v2/internal/platform/win32"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
"syscall"
"unsafe"
)
var (
user32 = syscall.MustLoadDLL("user32.dll")
DefWindowProc = user32.MustFindProc("DefWindowProcW")
RegisterClassEx = user32.MustFindProc("RegisterClassExW")
CreateWindowEx = user32.MustFindProc("CreateWindowExW")
windowClasses = map[string]win32.HINSTANCE{}
)
type Systray struct {
id uint32
mhwnd win32.HWND // main window handle
hwnd win32.HWND
hinst win32.HINSTANCE
lclick func()
rclick func()
ldblclick func()
rdblclick func()
onMenuClose func()
onMenuOpen func()
appIcon win32.HICON
lightModeIcon win32.HICON
darkModeIcon win32.HICON
currentIcon win32.HICON
menu *PopupMenu
quit chan struct{}
icon *options.SystemTrayIcon
}
func (p *Systray) Close() {
err := p.Stop()
if err != nil {
println(err.Error())
}
}
func (p *Systray) Update() error {
// Delete old menu
if p.menu != nil {
p.menu.Destroy()
}
return p.menu.Update()
}
// SetTitle is unused on Windows
func (p *Systray) SetTitle(_ string) {}
func New() (*Systray, error) {
ni := &Systray{}
ni.lclick = func() {
if ni.menu != nil {
_ = ni.menu.ShowAtCursor()
}
}
ni.rclick = func() {
if ni.menu != nil {
_ = ni.menu.ShowAtCursor()
}
}
MainClassName := "WailsSystray"
ni.hinst, _ = RegisterWindow(MainClassName, ni.WinProc)
ni.mhwnd = win32.CreateWindowEx(
win32.WS_EX_CONTROLPARENT,
win32.MustStringToUTF16Ptr(MainClassName),
win32.MustStringToUTF16Ptr(""),
win32.WS_OVERLAPPEDWINDOW|win32.WS_CLIPSIBLINGS,
win32.CW_USEDEFAULT,
win32.CW_USEDEFAULT,
win32.CW_USEDEFAULT,
win32.CW_USEDEFAULT,
0,
0,
0,
unsafe.Pointer(nil))
if ni.mhwnd == 0 {
return nil, errors.New("create main win failed")
}
NotifyIconClassName := "NotifyIconForm"
_, err := RegisterWindow(NotifyIconClassName, ni.WinProc)
if err != nil {
return nil, err
}
hwnd, _, _ := CreateWindowEx.Call(
0,
uintptr(unsafe.Pointer(win32.MustStringToUTF16Ptr(NotifyIconClassName))),
0,
0,
0,
0,
0,
0,
uintptr(win32.HWND_MESSAGE),
0,
0,
0)
if hwnd == 0 {
return nil, errors.New("create notify win failed")
}
ni.hwnd = win32.HWND(hwnd) // Important to keep this inside struct.
nid := win32.NOTIFYICONDATA{
HWnd: win32.HWND(hwnd),
UFlags: win32.NIF_MESSAGE | win32.NIF_STATE,
DwState: win32.NIS_HIDDEN,
DwStateMask: win32.NIS_HIDDEN,
UCallbackMessage: win32.NotifyIconMessageId,
}
nid.CbSize = uint32(unsafe.Sizeof(nid))
if !win32.ShellNotifyIcon(win32.NIM_ADD, &nid) {
return nil, errors.New("shell notify create failed")
}
nid.UVersion = win32.NOTIFYICON_VERSION
if !win32.ShellNotifyIcon(win32.NIM_SETVERSION, &nid) {
return nil, errors.New("shell notify version failed")
}
ni.appIcon = win32.LoadIconWithResourceID(0, uintptr(win32.IDI_APPLICATION))
ni.lightModeIcon = ni.appIcon
ni.darkModeIcon = ni.appIcon
ni.id = nid.UID
return ni, nil
}
func (p *Systray) HWND() win32.HWND {
return p.hwnd
}
func (p *Systray) SetMenu(popupMenu *menu.Menu) (err error) {
p.menu, err = NewPopupMenu(p.hwnd, popupMenu)
p.menu.OnMenuClose(p.onMenuClose)
p.menu.OnMenuOpen(p.onMenuOpen)
return
}
func (p *Systray) Stop() error {
nid := p.newNotifyIconData()
win32.PostQuitMessage(0)
if !win32.ShellNotifyIcon(win32.NIM_DELETE, &nid) {
return errors.New("shell notify delete failed")
}
return nil
}
func (p *Systray) OnLeftClick(fn func()) {
if fn != nil {
p.lclick = fn
}
}
func (p *Systray) OnRightClick(fn func()) {
if fn != nil {
p.rclick = fn
}
}
func (p *Systray) OnLeftDoubleClick(fn func()) {
if fn != nil {
p.ldblclick = fn
}
}
func (p *Systray) OnRightDoubleClick(fn func()) {
if fn != nil {
p.rdblclick = fn
}
}
func (p *Systray) OnMenuClose(fn func()) {
if fn != nil {
p.onMenuClose = fn
}
}
func (p *Systray) OnMenuOpen(fn func()) {
if fn != nil {
p.onMenuOpen = fn
}
}
func (p *Systray) SetTooltip(tooltip string) error {
nid := p.newNotifyIconData()
nid.UFlags = win32.NIF_TIP
copy(nid.SzTip[:], win32.MustUTF16FromString(tooltip))
if !win32.ShellNotifyIcon(win32.NIM_MODIFY, &nid) {
return errors.New("shell notify tooltip failed")
}
return nil
}
func (p *Systray) ShowMessage(title, msg string, bigIcon bool) error {
nid := p.newNotifyIconData()
if bigIcon == true {
nid.DwInfoFlags = win32.NIIF_USER
}
nid.CbSize = uint32(unsafe.Sizeof(nid))
nid.UFlags = win32.NIF_INFO
copy(nid.SzInfoTitle[:], win32.MustUTF16FromString(title))
copy(nid.SzInfo[:], win32.MustUTF16FromString(msg))
if !win32.ShellNotifyIcon(win32.NIM_MODIFY, &nid) {
return errors.New("shell notify tooltip failed")
}
return nil
}
func (p *Systray) newNotifyIconData() win32.NOTIFYICONDATA {
nid := win32.NOTIFYICONDATA{
UID: p.id,
HWnd: p.hwnd,
}
nid.CbSize = uint32(unsafe.Sizeof(nid))
return nid
}
func (p *Systray) Show() error {
return p.setVisible(true)
}
func (p *Systray) Hide() error {
return p.setVisible(false)
}
func (p *Systray) setVisible(visible bool) error {
nid := p.newNotifyIconData()
nid.UFlags = win32.NIF_STATE
nid.DwStateMask = win32.NIS_HIDDEN
if !visible {
nid.DwState = win32.NIS_HIDDEN
}
if !win32.ShellNotifyIcon(win32.NIM_MODIFY, &nid) {
return errors.New("shell notify tooltip failed")
}
return nil
}
func (p *Systray) SetIcons(lightModeIcon, darkModeIcon *options.SystemTrayIcon) error {
var newLightModeIcon, newDarkModeIcon win32.HICON
if lightModeIcon != nil && lightModeIcon.Data != nil {
newLightModeIcon = p.getIcon(lightModeIcon.Data)
}
if darkModeIcon != nil && darkModeIcon.Data != nil {
newDarkModeIcon = p.getIcon(darkModeIcon.Data)
}
p.lightModeIcon, _ = lo.Coalesce(newLightModeIcon, newDarkModeIcon, p.appIcon)
p.darkModeIcon, _ = lo.Coalesce(newDarkModeIcon, newLightModeIcon, p.appIcon)
return p.updateIcon()
}
func (p *Systray) getIcon(icon []byte) win32.HICON {
result, err := win32.CreateHIconFromPNG(icon)
if err != nil {
result = p.appIcon
}
return result
}
func (p *Systray) setIcon(hicon win32.HICON) error {
nid := p.newNotifyIconData()
nid.UFlags = win32.NIF_ICON
if hicon == 0 {
nid.HIcon = 0
} else {
nid.HIcon = hicon
}
if !win32.ShellNotifyIcon(win32.NIM_MODIFY, &nid) {
return errors.New("shell notify icon failed")
}
return nil
}
func (p *Systray) WinProc(hwnd win32.HWND, msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case win32.NotifyIconMessageId:
switch lparam {
case win32.WM_LBUTTONUP:
if p.lclick != nil {
println("left click")
p.lclick()
}
case win32.WM_RBUTTONUP:
if p.rclick != nil {
println("right click")
p.rclick()
}
case win32.WM_LBUTTONDBLCLK:
if p.ldblclick != nil {
p.ldblclick()
}
case win32.WM_RBUTTONDBLCLK:
if p.rdblclick != nil {
p.rdblclick()
}
default:
//println(win32.WMMessageToString(lparam))
}
case win32.WM_SETTINGCHANGE:
settingChanged := win32.UTF16PtrToString(lparam)
if settingChanged == "ImmersiveColorSet" {
err := p.updateIcon()
if err != nil {
println("update icon failed", err.Error())
}
}
return 0
case win32.WM_COMMAND:
cmdMsgID := int(wparam & 0xffff)
switch cmdMsgID {
default:
p.menu.ProcessCommand(cmdMsgID)
}
default:
//msg := int(wparam & 0xffff)
//println(win32.WMMessageToString(uintptr(msg)))
}
result, _, _ := DefWindowProc.Call(uintptr(hwnd), uintptr(msg), wparam, lparam)
return result
}
func (p *Systray) Run() error {
var msg win32.MSG
for {
rt := win32.GetMessage(&msg)
switch int(rt) {
case 0:
return nil
case -1:
return errors.New("run failed")
}
if win32.IsDialogMessage(p.hwnd, &msg) == 0 {
win32.TranslateMessage(&msg)
win32.DispatchMessage(&msg)
}
}
}
func (p *Systray) updateIcon() error {
var newIcon win32.HICON
if win32.IsCurrentlyDarkMode() {
newIcon = p.darkModeIcon
} else {
newIcon = p.lightModeIcon
}
if p.currentIcon == newIcon {
return nil
}
p.currentIcon = newIcon
return p.setIcon(newIcon)
}
func (p *Systray) updateTheme() {
//win32.SetTheme(p.hwnd, win32.IsCurrentlyDarkMode())
}
func RegisterWindow(name string, proc win32.WindowProc) (win32.HINSTANCE, error) {
instance, exists := windowClasses[name]
if exists {
return instance, nil
}
hinst := win32.GetModuleHandle(0)
if hinst == 0 {
return 0, errors.New("get module handle failed")
}
hicon := win32.LoadIconWithResourceID(0, uintptr(win32.IDI_APPLICATION))
if hicon == 0 {
return 0, errors.New("load icon failed")
}
hcursor := win32.LoadCursorWithResourceID(0, uintptr(win32.IDC_ARROW))
if hcursor == 0 {
return 0, errors.New("load cursor failed")
}
hi := win32.HINSTANCE(hinst)
var wc win32.WNDCLASSEX
wc.CbSize = uint32(unsafe.Sizeof(wc))
wc.LpfnWndProc = syscall.NewCallback(proc)
wc.HInstance = win32.HINSTANCE(hinst)
wc.HIcon = hicon
wc.HCursor = hcursor
wc.HbrBackground = win32.COLOR_BTNFACE + 1
wc.LpszClassName = win32.MustStringToUTF16Ptr(name)
atom, _, e := RegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
if atom == 0 {
println(e.Error())
return 0, errors.New("register class failed")
}
windowClasses[name] = hi
return hi, nil
}