406 lines
8.5 KiB
Go
406 lines
8.5 KiB
Go
//go:build windows
|
|
|
|
package tray
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
const (
|
|
wmClose = 0x0010
|
|
wmDestroy = 0x0002
|
|
wmApp = 0x8000
|
|
|
|
trayCallbackMessage = wmApp + 1
|
|
|
|
hwndMessage = ^uintptr(2)
|
|
|
|
nimAdd = 0x00000000
|
|
nimDelete = 0x00000002
|
|
nimSetVersion = 0x00000004
|
|
|
|
nifMessage = 0x00000001
|
|
nifIcon = 0x00000002
|
|
nifTip = 0x00000004
|
|
|
|
notifyIconVersion = 4
|
|
|
|
imageIcon = 1
|
|
lrLoadFromFile = 0x00000010
|
|
idiApplication = 32512
|
|
smCxSmallIcon = 49
|
|
smCySmallIcon = 50
|
|
)
|
|
|
|
var (
|
|
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
|
user32 = windows.NewLazySystemDLL("user32.dll")
|
|
shell32 = windows.NewLazySystemDLL("shell32.dll")
|
|
|
|
procGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
|
|
|
|
procRegisterClassEx = user32.NewProc("RegisterClassExW")
|
|
procCreateWindowEx = user32.NewProc("CreateWindowExW")
|
|
procDefWindowProc = user32.NewProc("DefWindowProcW")
|
|
procDestroyWindow = user32.NewProc("DestroyWindow")
|
|
procPostMessage = user32.NewProc("PostMessageW")
|
|
procGetMessage = user32.NewProc("GetMessageW")
|
|
procTranslateMsg = user32.NewProc("TranslateMessage")
|
|
procDispatchMsg = user32.NewProc("DispatchMessageW")
|
|
procPostQuitMessage = user32.NewProc("PostQuitMessage")
|
|
procLoadIcon = user32.NewProc("LoadIconW")
|
|
procLoadImage = user32.NewProc("LoadImageW")
|
|
procDestroyIcon = user32.NewProc("DestroyIcon")
|
|
procGetSystemMetric = user32.NewProc("GetSystemMetrics")
|
|
|
|
procShellNotifyIcon = shell32.NewProc("Shell_NotifyIconW")
|
|
procExtractIconEx = shell32.NewProc("ExtractIconExW")
|
|
)
|
|
|
|
type notifyIconData struct {
|
|
cbSize uint32
|
|
hwnd uintptr
|
|
uid uint32
|
|
flags uint32
|
|
callbackMessage uint32
|
|
icon uintptr
|
|
tip [128]uint16
|
|
state uint32
|
|
stateMask uint32
|
|
info [256]uint16
|
|
version uint32
|
|
infoTitle [64]uint16
|
|
infoFlags uint32
|
|
guidItem windows.GUID
|
|
balloonIcon uintptr
|
|
}
|
|
|
|
type wndClassEx struct {
|
|
cbSize uint32
|
|
style uint32
|
|
wndProc uintptr
|
|
clsExtra int32
|
|
wndExtra int32
|
|
instance uintptr
|
|
icon uintptr
|
|
cursor uintptr
|
|
background uintptr
|
|
menuName *uint16
|
|
className *uint16
|
|
iconSmall uintptr
|
|
}
|
|
|
|
type point struct {
|
|
x int32
|
|
y int32
|
|
}
|
|
|
|
type msg struct {
|
|
hwnd uintptr
|
|
message uint32
|
|
wparam uintptr
|
|
lparam uintptr
|
|
time uint32
|
|
pt point
|
|
}
|
|
|
|
type windowsTray struct {
|
|
controller *Controller
|
|
wndProc uintptr
|
|
hwnd atomic.Uintptr
|
|
icon uintptr
|
|
destroyIcon bool
|
|
ready chan error
|
|
done chan struct{}
|
|
stopOnce sync.Once
|
|
cleanupOnce sync.Once
|
|
}
|
|
|
|
func start(ctx context.Context, controller *Controller) (func(), error) {
|
|
tray := &windowsTray{
|
|
controller: controller,
|
|
ready: make(chan error, 1),
|
|
done: make(chan struct{}),
|
|
}
|
|
|
|
go tray.run()
|
|
|
|
if err := <-tray.ready; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stop := func() {
|
|
tray.stop()
|
|
}
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
stop()
|
|
}()
|
|
|
|
return stop, nil
|
|
}
|
|
|
|
func (t *windowsTray) run() {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
if err := t.init(); err != nil {
|
|
hwnd := t.hwnd.Load()
|
|
t.cleanup()
|
|
destroyWindow(hwnd)
|
|
t.ready <- err
|
|
close(t.done)
|
|
return
|
|
}
|
|
t.ready <- nil
|
|
|
|
t.messageLoop()
|
|
t.cleanup()
|
|
close(t.done)
|
|
}
|
|
|
|
func (t *windowsTray) init() error {
|
|
t.wndProc = windows.NewCallback(t.windowProc)
|
|
|
|
instance, _, _ := procGetModuleHandle.Call(0)
|
|
className, err := windows.UTF16PtrFromString(fmt.Sprintf("SGGAISkillManagerTrayWindow-%d", os.Getpid()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
class := wndClassEx{
|
|
cbSize: uint32(unsafe.Sizeof(wndClassEx{})),
|
|
wndProc: t.wndProc,
|
|
instance: instance,
|
|
className: className,
|
|
}
|
|
if ret, _, callErr := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&class))); ret == 0 {
|
|
return syscallError("RegisterClassExW", callErr)
|
|
}
|
|
|
|
hwnd, _, callErr := procCreateWindowEx.Call(
|
|
0,
|
|
uintptr(unsafe.Pointer(className)),
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
hwndMessage,
|
|
0,
|
|
instance,
|
|
0,
|
|
)
|
|
if hwnd == 0 {
|
|
return syscallError("CreateWindowExW", callErr)
|
|
}
|
|
t.hwnd.Store(hwnd)
|
|
|
|
icon, destroyIcon, err := loadTrayIcon(t.controller.config.IconPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.icon = icon
|
|
t.destroyIcon = destroyIcon
|
|
|
|
nid := t.newNotifyIconData()
|
|
if !shellNotifyIcon(nimAdd, &nid) {
|
|
return fmt.Errorf("Shell_NotifyIconW add failed")
|
|
}
|
|
|
|
nid.version = notifyIconVersion
|
|
if !shellNotifyIcon(nimSetVersion, &nid) {
|
|
return fmt.Errorf("Shell_NotifyIconW set version failed")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *windowsTray) messageLoop() {
|
|
var message msg
|
|
for {
|
|
ret, _, _ := procGetMessage.Call(uintptr(unsafe.Pointer(&message)), 0, 0, 0)
|
|
if int32(ret) <= 0 {
|
|
return
|
|
}
|
|
procTranslateMsg.Call(uintptr(unsafe.Pointer(&message)))
|
|
procDispatchMsg.Call(uintptr(unsafe.Pointer(&message)))
|
|
}
|
|
}
|
|
|
|
func (t *windowsTray) stop() {
|
|
t.stopOnce.Do(func() {
|
|
if hwnd := t.hwnd.Load(); hwnd != 0 {
|
|
procPostMessage.Call(hwnd, wmClose, 0, 0)
|
|
<-t.done
|
|
}
|
|
})
|
|
}
|
|
|
|
func (t *windowsTray) windowProc(hwnd uintptr, message uint32, wparam uintptr, lparam uintptr) uintptr {
|
|
switch message {
|
|
case trayCallbackMessage:
|
|
if t.controller.handleTrayEvent(lparam) {
|
|
return 0
|
|
}
|
|
case wmClose:
|
|
t.cleanup()
|
|
procDestroyWindow.Call(hwnd)
|
|
return 0
|
|
case wmDestroy:
|
|
procPostQuitMessage.Call(0)
|
|
return 0
|
|
}
|
|
|
|
ret, _, _ := procDefWindowProc.Call(hwnd, uintptr(message), wparam, lparam)
|
|
return ret
|
|
}
|
|
|
|
func (t *windowsTray) cleanup() {
|
|
t.cleanupOnce.Do(func() {
|
|
if hwnd := t.hwnd.Load(); hwnd != 0 {
|
|
nid := t.newNotifyIconData()
|
|
shellNotifyIcon(nimDelete, &nid)
|
|
t.hwnd.Store(0)
|
|
}
|
|
if t.icon != 0 && t.destroyIcon {
|
|
procDestroyIcon.Call(t.icon)
|
|
}
|
|
})
|
|
}
|
|
|
|
func destroyWindow(hwnd uintptr) {
|
|
if hwnd != 0 {
|
|
procDestroyWindow.Call(hwnd)
|
|
}
|
|
}
|
|
|
|
func (t *windowsTray) newNotifyIconData() notifyIconData {
|
|
nid := notifyIconData{
|
|
cbSize: uint32(unsafe.Sizeof(notifyIconData{})),
|
|
hwnd: t.hwnd.Load(),
|
|
uid: 1,
|
|
flags: nifMessage | nifIcon | nifTip,
|
|
callbackMessage: trayCallbackMessage,
|
|
icon: t.icon,
|
|
}
|
|
copyUTF16(nid.tip[:], t.controller.config.Tooltip)
|
|
return nid
|
|
}
|
|
|
|
func loadTrayIcon(iconPath string) (uintptr, bool, error) {
|
|
if icon, ok := loadIconFromFile(iconPath); ok {
|
|
return icon, true, nil
|
|
}
|
|
if icon, ok := extractExecutableIcon(); ok {
|
|
return icon, true, nil
|
|
}
|
|
if icon, _, _ := procLoadIcon.Call(0, idiApplication); icon != 0 {
|
|
return icon, false, nil
|
|
}
|
|
return 0, false, fmt.Errorf("load tray icon failed")
|
|
}
|
|
|
|
func loadIconFromFile(iconPath string) (uintptr, bool) {
|
|
if iconPath == "" {
|
|
return 0, false
|
|
}
|
|
absolutePath, err := filepath.Abs(iconPath)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
if _, err := os.Stat(absolutePath); err != nil {
|
|
return 0, false
|
|
}
|
|
path, err := windows.UTF16PtrFromString(absolutePath)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
width := systemMetric(smCxSmallIcon, 16)
|
|
height := systemMetric(smCySmallIcon, 16)
|
|
icon, _, _ := procLoadImage.Call(
|
|
0,
|
|
uintptr(unsafe.Pointer(path)),
|
|
imageIcon,
|
|
uintptr(width),
|
|
uintptr(height),
|
|
lrLoadFromFile,
|
|
)
|
|
return icon, icon != 0
|
|
}
|
|
|
|
func extractExecutableIcon() (uintptr, bool) {
|
|
exePath, err := os.Executable()
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
exe, err := windows.UTF16PtrFromString(exePath)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
|
|
var largeIcon uintptr
|
|
var smallIcon uintptr
|
|
count, _, _ := procExtractIconEx.Call(
|
|
uintptr(unsafe.Pointer(exe)),
|
|
0,
|
|
uintptr(unsafe.Pointer(&largeIcon)),
|
|
uintptr(unsafe.Pointer(&smallIcon)),
|
|
1,
|
|
)
|
|
if count == 0 {
|
|
return 0, false
|
|
}
|
|
if smallIcon != 0 {
|
|
if largeIcon != 0 {
|
|
procDestroyIcon.Call(largeIcon)
|
|
}
|
|
return smallIcon, true
|
|
}
|
|
return largeIcon, largeIcon != 0
|
|
}
|
|
|
|
func systemMetric(index int32, fallback int) int {
|
|
value, _, _ := procGetSystemMetric.Call(uintptr(index))
|
|
if value == 0 {
|
|
return fallback
|
|
}
|
|
return int(value)
|
|
}
|
|
|
|
func shellNotifyIcon(command uintptr, data *notifyIconData) bool {
|
|
ret, _, _ := procShellNotifyIcon.Call(command, uintptr(unsafe.Pointer(data)))
|
|
return ret != 0
|
|
}
|
|
|
|
func copyUTF16(destination []uint16, value string) {
|
|
encoded, err := windows.UTF16FromString(value)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(encoded) > len(destination) {
|
|
encoded = encoded[:len(destination)]
|
|
encoded[len(encoded)-1] = 0
|
|
}
|
|
copy(destination, encoded)
|
|
}
|
|
|
|
func syscallError(operation string, err error) error {
|
|
if errno, ok := err.(syscall.Errno); ok && errno == 0 {
|
|
return fmt.Errorf("%s failed", operation)
|
|
}
|
|
return fmt.Errorf("%s failed: %w", operation, err)
|
|
}
|