454 lines
13 KiB
Go
454 lines
13 KiB
Go
//go:build windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image/png"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
const (
|
|
trayCallbackMessage = 0x8001
|
|
trayQuitMessage = 0x8002
|
|
trayIconID = 1
|
|
menuExitID = 1
|
|
)
|
|
|
|
type winPoint struct{ X, Y int32 }
|
|
type winRect struct{ Left, Top, Right, Bottom int32 }
|
|
type winMessage struct {
|
|
Window uintptr
|
|
Message uint32
|
|
WParam, LParam uintptr
|
|
Time uint32
|
|
Point winPoint
|
|
Private uint32
|
|
}
|
|
type windowClass struct {
|
|
Size, Style uint32
|
|
Proc uintptr
|
|
ClassExtra, WindowExtra int32
|
|
Instance, Icon, Cursor, Background uintptr
|
|
MenuName, ClassName *uint16
|
|
SmallIcon uintptr
|
|
}
|
|
type notifyIconData struct {
|
|
Size uint32
|
|
Window uintptr
|
|
ID, Flags, Callback uint32
|
|
Icon uintptr
|
|
Tip [128]uint16
|
|
State, StateMask uint32
|
|
Info [256]uint16
|
|
Version uint32
|
|
InfoTitle [64]uint16
|
|
InfoFlags uint32
|
|
GUID [16]byte
|
|
BalloonIcon uintptr
|
|
}
|
|
type notifyIconIdentifier struct {
|
|
Size uint32
|
|
Window uintptr
|
|
ID uint32
|
|
GUID [16]byte
|
|
}
|
|
type menuItemInfo struct {
|
|
Size, Mask, Type, State, ID uint32
|
|
Submenu, Checked, Unchecked, Data uintptr
|
|
Text *uint16
|
|
TextLen uint32
|
|
Bitmap uintptr
|
|
}
|
|
type bitmapHeader struct {
|
|
Size uint32
|
|
Width, Height int32
|
|
Planes, BitCount uint16
|
|
Compression, ImageSize uint32
|
|
XPels, YPels int32
|
|
ColorsUsed, ColorsImportant uint32
|
|
}
|
|
type iconInfo struct {
|
|
Icon int32
|
|
HotspotX, HotspotY uint32
|
|
Mask, Color uintptr
|
|
}
|
|
|
|
func systemDLL(name string) *syscall.LazyDLL {
|
|
var dir [32768]uint16
|
|
r, _, err := k32.NewProc("GetSystemDirectoryW").Call(uintptr(unsafe.Pointer(&dir[0])), uintptr(len(dir)))
|
|
if r == 0 || r >= uintptr(len(dir)) {
|
|
panic(fmt.Sprintf("GetSystemDirectoryW: %v", err))
|
|
}
|
|
return syscall.NewLazyDLL(filepath.Join(syscall.UTF16ToString(dir[:r]), name))
|
|
}
|
|
|
|
var (
|
|
u32 = systemDLL("user32.dll")
|
|
shell32 = systemDLL("shell32.dll")
|
|
gdi32 = systemDLL("gdi32.dll")
|
|
pRegisterClass = u32.NewProc("RegisterClassExW")
|
|
pUnregisterClass = u32.NewProc("UnregisterClassW")
|
|
pCreateWindow = u32.NewProc("CreateWindowExW")
|
|
pDestroyWindow = u32.NewProc("DestroyWindow")
|
|
pDefWindowProc = u32.NewProc("DefWindowProcW")
|
|
pGetMessage = u32.NewProc("GetMessageW")
|
|
pTranslateMessage = u32.NewProc("TranslateMessage")
|
|
pDispatchMessage = u32.NewProc("DispatchMessageW")
|
|
pPostQuit = u32.NewProc("PostQuitMessage")
|
|
pPostMessage = u32.NewProc("PostMessageW")
|
|
pCreatePopup = u32.NewProc("CreatePopupMenu")
|
|
pDestroyMenu = u32.NewProc("DestroyMenu")
|
|
pInsertMenuItem = u32.NewProc("InsertMenuItemW")
|
|
pTrackPopup = u32.NewProc("TrackPopupMenu")
|
|
pForeground = u32.NewProc("SetForegroundWindow")
|
|
pEndMenu = u32.NewProc("EndMenu")
|
|
pCursorPos = u32.NewProc("GetCursorPos")
|
|
pRegisterMessage = u32.NewProc("RegisterWindowMessageW")
|
|
pMessageFilter = u32.NewProc("ChangeWindowMessageFilterEx")
|
|
pCreateIcon = u32.NewProc("CreateIconIndirect")
|
|
pDestroyIcon = u32.NewProc("DestroyIcon")
|
|
pCreateDIB = gdi32.NewProc("CreateDIBSection")
|
|
pCreateBitmap = gdi32.NewProc("CreateBitmap")
|
|
pDeleteObject = gdi32.NewProc("DeleteObject")
|
|
pShellNotify = shell32.NewProc("Shell_NotifyIconW")
|
|
pNotifyRect = shell32.NewProc("Shell_NotifyIconGetRect")
|
|
)
|
|
|
|
type tray struct {
|
|
hwnd atomic.Uintptr
|
|
menu uintptr
|
|
data notifyIconData
|
|
taskbar uint32
|
|
version4 bool
|
|
menuOpen atomic.Bool
|
|
done chan struct{}
|
|
err error
|
|
failure error // message-loop thread only
|
|
onExit func()
|
|
restored <-chan struct{}
|
|
exitOnce sync.Once
|
|
added bool
|
|
}
|
|
|
|
func newTray(onExit func(), restored <-chan struct{}) (*tray, error) {
|
|
t := &tray{done: make(chan struct{}), onExit: onExit, restored: restored}
|
|
ready := make(chan error, 1)
|
|
go func() {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
t.err = t.loop(ready)
|
|
t.requestExit()
|
|
close(t.done)
|
|
}()
|
|
if err := <-ready; err != nil {
|
|
<-t.done
|
|
return nil, err
|
|
}
|
|
return t, nil
|
|
}
|
|
func (t *tray) Close() error {
|
|
select {
|
|
case <-t.done:
|
|
return t.err
|
|
default:
|
|
}
|
|
if h := t.hwnd.Load(); h != 0 {
|
|
r, _, e := pPostMessage.Call(h, trayQuitMessage, 0, 0)
|
|
if r == 0 {
|
|
return fmt.Errorf("PostMessage(close): %v", e)
|
|
}
|
|
}
|
|
<-t.done
|
|
return t.err
|
|
}
|
|
|
|
var trayWindows sync.Map
|
|
var trayWindowCallback = syscall.NewCallback(trayWindowProc)
|
|
|
|
func (t *tray) requestExit() {
|
|
t.exitOnce.Do(func() {
|
|
if t.onExit != nil {
|
|
t.onExit()
|
|
}
|
|
})
|
|
}
|
|
func (t *tray) fail(err error) {
|
|
if t.failure == nil {
|
|
t.failure = err
|
|
}
|
|
t.requestExit()
|
|
}
|
|
func pngBitmap(data []byte) (uintptr, error) {
|
|
image, err := png.Decode(bytes.NewReader(data))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if image.Bounds().Dx() != 16 || image.Bounds().Dy() != 16 {
|
|
return 0, fmt.Errorf("expected a 16x16 Silk icon")
|
|
}
|
|
header := bitmapHeader{Size: uint32(unsafe.Sizeof(bitmapHeader{})), Width: 16, Height: -16, Planes: 1, BitCount: 32}
|
|
var bits unsafe.Pointer
|
|
bitmap, _, e := pCreateDIB.Call(0, uintptr(unsafe.Pointer(&header)), 0, uintptr(unsafe.Pointer(&bits)), 0, 0)
|
|
if bitmap == 0 {
|
|
return 0, fmt.Errorf("CreateDIBSection: %v", e)
|
|
}
|
|
if bits == nil {
|
|
pDeleteObject.Call(bitmap)
|
|
return 0, fmt.Errorf("CreateDIBSection returned no pixels")
|
|
}
|
|
pixels := unsafe.Slice((*byte)(bits), 16*16*4)
|
|
for y := 0; y < 16; y++ {
|
|
for x := 0; x < 16; x++ {
|
|
r, g, b, a := image.At(x, y).RGBA()
|
|
offset := (y*16 + x) * 4
|
|
// RGBA() is premultiplied; native menu bitmaps require premultiplied BGRA.
|
|
pixels[offset] = byte(b >> 8)
|
|
pixels[offset+1] = byte(g >> 8)
|
|
pixels[offset+2] = byte(r >> 8)
|
|
pixels[offset+3] = byte(a >> 8)
|
|
}
|
|
}
|
|
return bitmap, nil
|
|
}
|
|
func pngIcon(data []byte) (uintptr, error) {
|
|
color, err := pngBitmap(data)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer pDeleteObject.Call(color)
|
|
var maskBits [32]byte
|
|
mask, _, e := pCreateBitmap.Call(16, 16, 1, 1, uintptr(unsafe.Pointer(&maskBits[0])))
|
|
if mask == 0 {
|
|
return 0, fmt.Errorf("CreateBitmap(mask): %v", e)
|
|
}
|
|
defer pDeleteObject.Call(mask)
|
|
info := iconInfo{Icon: 1, Mask: mask, Color: color}
|
|
icon, _, e := pCreateIcon.Call(uintptr(unsafe.Pointer(&info)))
|
|
if icon == 0 {
|
|
return 0, fmt.Errorf("CreateIconIndirect: %v", e)
|
|
}
|
|
return icon, nil
|
|
}
|
|
func (t *tray) addIcon() error {
|
|
t.data.Flags = 0x87 // NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_SHOWTIP
|
|
ok, _, _ := pShellNotify.Call(0, uintptr(unsafe.Pointer(&t.data))) // NIM_ADD
|
|
if ok == 0 {
|
|
ok, _, _ = pShellNotify.Call(1, uintptr(unsafe.Pointer(&t.data)))
|
|
} // existing icon: NIM_MODIFY
|
|
if ok == 0 {
|
|
return fmt.Errorf("Shell_NotifyIcon: notification area unavailable")
|
|
}
|
|
t.added = true
|
|
t.data.Version = 4
|
|
ok, _, _ = pShellNotify.Call(4, uintptr(unsafe.Pointer(&t.data))) // NIM_SETVERSION, after EVERY add
|
|
t.version4 = ok != 0
|
|
return nil
|
|
}
|
|
func (t *tray) removeIcon() {
|
|
if t.added {
|
|
pShellNotify.Call(2, uintptr(unsafe.Pointer(&t.data)))
|
|
t.added = false
|
|
}
|
|
}
|
|
func (t *tray) loop(ready chan<- error) (result error) {
|
|
sent := false
|
|
defer func() {
|
|
if !sent {
|
|
ready <- result
|
|
}
|
|
}()
|
|
icon, err := pngIcon(trayPNG)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer pDestroyIcon.Call(icon)
|
|
cross, err := pngBitmap(exitPNG)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer pDeleteObject.Call(cross)
|
|
menu, _, e := pCreatePopup.Call()
|
|
if menu == 0 {
|
|
return fmt.Errorf("CreatePopupMenu: %v", e)
|
|
}
|
|
t.menu = menu
|
|
defer pDestroyMenu.Call(menu)
|
|
label, _ := syscall.UTF16PtrFromString("Выход")
|
|
item := menuItemInfo{Size: uint32(unsafe.Sizeof(menuItemInfo{})), Mask: 0x2 | 0x40 | 0x80, ID: menuExitID, Text: label, Bitmap: cross}
|
|
ok, _, e := pInsertMenuItem.Call(menu, 0, 1, uintptr(unsafe.Pointer(&item)))
|
|
if ok == 0 {
|
|
return fmt.Errorf("InsertMenuItemW: %v", e)
|
|
}
|
|
instance, _, e := k32.NewProc("GetModuleHandleW").Call(0)
|
|
if instance == 0 {
|
|
return fmt.Errorf("GetModuleHandleW: %v", e)
|
|
}
|
|
className, _ := syscall.UTF16PtrFromString(fmt.Sprintf("autoPriority.Tray.%d", os.Getpid()))
|
|
wc := windowClass{Size: uint32(unsafe.Sizeof(windowClass{})), Proc: trayWindowCallback, Instance: instance, ClassName: className, Icon: icon, SmallIcon: icon}
|
|
atom, _, e := pRegisterClass.Call(uintptr(unsafe.Pointer(&wc)))
|
|
if atom == 0 {
|
|
return fmt.Errorf("RegisterClassExW: %v", e)
|
|
}
|
|
defer pUnregisterClass.Call(uintptr(unsafe.Pointer(className)), instance)
|
|
// Hidden TOP-LEVEL window: a message-only window would miss TaskbarCreated.
|
|
h, _, e := pCreateWindow.Call(0, uintptr(unsafe.Pointer(className)), 0, 0, 0, 0, 0, 0, 0, 0, instance, 0)
|
|
if h == 0 {
|
|
return fmt.Errorf("CreateWindowExW: %v", e)
|
|
}
|
|
t.hwnd.Store(h)
|
|
trayWindows.Store(h, t)
|
|
defer func() {
|
|
if t.hwnd.Load() != 0 {
|
|
pDestroyWindow.Call(h)
|
|
}
|
|
trayWindows.Delete(h)
|
|
}()
|
|
taskbarName, _ := syscall.UTF16PtrFromString("TaskbarCreated")
|
|
taskbar, _, e := pRegisterMessage.Call(uintptr(unsafe.Pointer(taskbarName)))
|
|
if taskbar == 0 {
|
|
return fmt.Errorf("RegisterWindowMessageW: %v", e)
|
|
}
|
|
t.taskbar = uint32(taskbar)
|
|
// Only the Explorer restart message crosses an elevated window's UIPI boundary.
|
|
ok, _, e = pMessageFilter.Call(h, taskbar, 1, 0)
|
|
if ok == 0 {
|
|
return fmt.Errorf("ChangeWindowMessageFilterEx: %v", e)
|
|
}
|
|
t.data = notifyIconData{Size: uint32(unsafe.Sizeof(notifyIconData{})), Window: h, ID: trayIconID, Callback: trayCallbackMessage, Icon: icon}
|
|
tip, _ := syscall.UTF16FromString("autoPriority")
|
|
copy(t.data.Tip[:], tip)
|
|
if err = t.addIcon(); err != nil {
|
|
return err
|
|
}
|
|
defer t.removeIcon()
|
|
ready <- nil
|
|
sent = true
|
|
for {
|
|
var msg winMessage
|
|
got, _, e := pGetMessage.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
|
|
if int32(got) == -1 {
|
|
return fmt.Errorf("GetMessageW: %v", e)
|
|
}
|
|
if got == 0 {
|
|
return t.failure
|
|
}
|
|
pTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
|
|
pDispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
|
|
}
|
|
}
|
|
func trayEvent(version4 bool, wp, lp uintptr) (event, id uint32, point winPoint, anchored bool) {
|
|
event = uint32(lp)
|
|
id = uint32(wp)
|
|
if version4 {
|
|
event = uint32(lp) & 0xffff
|
|
id = uint32(lp>>16) & 0xffff
|
|
if event == 0x400 || event == 0x401 || (event >= 0x200 && event <= 0x20e) {
|
|
point = winPoint{int32(int16(wp & 0xffff)), int32(int16((wp >> 16) & 0xffff))}
|
|
anchored = true
|
|
}
|
|
}
|
|
return
|
|
}
|
|
func (t *tray) popup(point winPoint, anchored bool) {
|
|
if t.menuOpen.Swap(true) {
|
|
return
|
|
}
|
|
defer t.menuOpen.Store(false)
|
|
if !anchored {
|
|
ok, _, _ := pCursorPos.Call(uintptr(unsafe.Pointer(&point)))
|
|
if ok == 0 {
|
|
point = winPoint{}
|
|
}
|
|
}
|
|
h := t.hwnd.Load()
|
|
if h == 0 {
|
|
return
|
|
}
|
|
pForeground.Call(h)
|
|
k32.NewProc("SetLastError").Call(0)
|
|
command, _, e := pTrackPopup.Call(t.menu, 0x100|0x80|0x2, uintptr(point.X), uintptr(point.Y), 0, h, 0)
|
|
pPostMessage.Call(h, 0, 0, 0) // documented focus workaround after TrackPopupMenu
|
|
if command == menuExitID {
|
|
t.requestExit()
|
|
} else if command == 0 && e != syscall.Errno(0) {
|
|
t.fail(fmt.Errorf("TrackPopupMenu: %v", e))
|
|
}
|
|
pShellNotify.Call(3, uintptr(unsafe.Pointer(&t.data))) // NIM_SETFOCUS
|
|
}
|
|
func trayWindowProc(h uintptr, message uint32, wp, lp uintptr) uintptr {
|
|
value, ok := trayWindows.Load(h)
|
|
if !ok {
|
|
r, _, _ := pDefWindowProc.Call(h, uintptr(message), wp, lp)
|
|
return r
|
|
}
|
|
t := value.(*tray)
|
|
if message == t.taskbar {
|
|
if err := t.addIcon(); err != nil {
|
|
t.fail(err)
|
|
}
|
|
return 0
|
|
}
|
|
switch message {
|
|
case trayCallbackMessage:
|
|
event, id, point, anchored := trayEvent(t.version4, wp, lp)
|
|
if id == trayIconID && trayActivation(t.version4, event) {
|
|
t.popup(point, anchored)
|
|
}
|
|
return 0
|
|
case 0x111:
|
|
if uint32(wp)&0xffff == menuExitID {
|
|
t.requestExit()
|
|
}
|
|
return 0 // WM_COMMAND
|
|
case 0x10:
|
|
t.requestExit()
|
|
return 0 // WM_CLOSE requests cleanup, not forceful exit
|
|
case 0x11:
|
|
return 1 // WM_QUERYENDSESSION
|
|
case 0x16: // WM_ENDSESSION: give the controller a bounded chance to restore
|
|
if wp != 0 {
|
|
t.requestExit()
|
|
select {
|
|
case <-t.restored:
|
|
case <-time.After(3 * time.Second):
|
|
}
|
|
}
|
|
return 0
|
|
case 0x1f:
|
|
pEndMenu.Call()
|
|
return 0 // WM_CANCELMODE
|
|
case trayQuitMessage:
|
|
pEndMenu.Call()
|
|
pDestroyWindow.Call(h)
|
|
return 0
|
|
case 0x2:
|
|
t.removeIcon()
|
|
t.hwnd.Store(0)
|
|
t.requestExit()
|
|
pPostQuit.Call(0)
|
|
return 0 // WM_DESTROY
|
|
case 0x82:
|
|
trayWindows.Delete(h) // WM_NCDESTROY
|
|
}
|
|
r, _, _ := pDefWindowProc.Call(h, uintptr(message), wp, lp)
|
|
return r
|
|
}
|
|
func showError(err error) {
|
|
console, _, _ := k32.NewProc("GetConsoleWindow").Call()
|
|
if console != 0 {
|
|
return
|
|
}
|
|
text, _ := syscall.UTF16PtrFromString("autoPriority: " + err.Error())
|
|
title, _ := syscall.UTF16PtrFromString("autoPriority")
|
|
u32.NewProc("MessageBoxW").Call(0, uintptr(unsafe.Pointer(text)), uintptr(unsafe.Pointer(title)), 0x10)
|
|
}
|