Files
autoPriority/tray_windows.go
T

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)
}