296 lines
8.8 KiB
Go
296 lines
8.8 KiB
Go
//go:build windows
|
|
|
|
package platform
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image/png"
|
|
"strings"
|
|
"unsafe"
|
|
|
|
"github.com/stevec/fancywin/internal/assets"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
const (
|
|
wmAppTray = 0x8001
|
|
wmRButtonUp = 0x0205
|
|
wmContextMenu = 0x007B
|
|
wmNull = 0x0000
|
|
nimAdd = 0x00000000
|
|
nimModify = 0x00000001
|
|
nimDelete = 0x00000002
|
|
nifMessage = 0x00000001
|
|
nifIcon = 0x00000002
|
|
nifTip = 0x00000004
|
|
mfString = 0x00000000
|
|
mfGray = 0x00000001
|
|
mfChecked = 0x00000008
|
|
mfSeparator = 0x00000800
|
|
tpmRightButton = 0x00000002
|
|
tpmReturnCmd = 0x00000100
|
|
tpmNonotify = 0x00000080
|
|
trayCommandExit = 1001
|
|
trayLayoutBase = 2000
|
|
idiApplication = 32512
|
|
biRGB = 0
|
|
dibRGBColors = 0
|
|
)
|
|
|
|
var (
|
|
shell32 = windows.NewLazySystemDLL("shell32.dll")
|
|
procShellNotifyIcon = shell32.NewProc("Shell_NotifyIconW")
|
|
procLoadIcon = user32.NewProc("LoadIconW")
|
|
procCreatePopupMenu = user32.NewProc("CreatePopupMenu")
|
|
procAppendMenu = user32.NewProc("AppendMenuW")
|
|
procTrackPopupMenu = user32.NewProc("TrackPopupMenu")
|
|
procDestroyMenu = user32.NewProc("DestroyMenu")
|
|
procSetForegroundWindow = user32.NewProc("SetForegroundWindow")
|
|
procPostMessage = user32.NewProc("PostMessageW")
|
|
procPostQuitMessage = user32.NewProc("PostQuitMessage")
|
|
procRegisterWindowMessage = user32.NewProc("RegisterWindowMessageW")
|
|
procCreateIconIndirect = user32.NewProc("CreateIconIndirect")
|
|
procDestroyIcon = user32.NewProc("DestroyIcon")
|
|
procCreateDIBSection = gdi32.NewProc("CreateDIBSection")
|
|
procCreateBitmap = gdi32.NewProc("CreateBitmap")
|
|
trayClassName, _ = windows.UTF16PtrFromString("FancyWinTrayWindow")
|
|
trayWindowProc = windows.NewCallback(trayWndProc)
|
|
activeTray *trayIcon
|
|
)
|
|
|
|
type notifyIconData struct {
|
|
Size uint32
|
|
Window windows.Handle
|
|
ID uint32
|
|
Flags uint32
|
|
CallbackMessage uint32
|
|
Icon windows.Handle
|
|
Tip [128]uint16
|
|
State uint32
|
|
StateMask uint32
|
|
Info [256]uint16
|
|
TimeoutOrVersion uint32
|
|
InfoTitle [64]uint16
|
|
InfoFlags uint32
|
|
GUID [16]byte
|
|
BalloonIcon windows.Handle
|
|
}
|
|
|
|
type trayIcon struct {
|
|
hwnd windows.Handle
|
|
icon windows.Handle
|
|
activeLayout string
|
|
layouts []string
|
|
taskbarMessage uint32
|
|
ownsIcon bool
|
|
}
|
|
|
|
type bitmapInfoHeader struct {
|
|
Size uint32
|
|
Width int32
|
|
Height int32
|
|
Planes uint16
|
|
BitCount uint16
|
|
Compression uint32
|
|
SizeImage uint32
|
|
XPelsPerMeter int32
|
|
YPelsPerMeter int32
|
|
ClrUsed uint32
|
|
ClrImportant uint32
|
|
}
|
|
|
|
type iconInfo struct {
|
|
Icon int32
|
|
XHotspot uint32
|
|
YHotspot uint32
|
|
Mask windows.Handle
|
|
Color windows.Handle
|
|
}
|
|
|
|
func newTrayIcon(activeLayout string, layouts []string) (*trayIcon, error) {
|
|
instance, _, instanceErr := procGetModuleHandle.Call(0)
|
|
if instance == 0 {
|
|
return nil, fmt.Errorf("tray GetModuleHandleW: %v", instanceErr)
|
|
}
|
|
wc := windowClassEx{
|
|
Size: uint32(unsafe.Sizeof(windowClassEx{})),
|
|
WndProc: trayWindowProc,
|
|
Instance: windows.Handle(instance),
|
|
ClassName: trayClassName,
|
|
}
|
|
if atom, _, err := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc))); atom == 0 {
|
|
return nil, fmt.Errorf("register tray window class: %v", err)
|
|
}
|
|
|
|
raw, _, createErr := procCreateWindowEx.Call(
|
|
0,
|
|
uintptr(unsafe.Pointer(trayClassName)),
|
|
uintptr(unsafe.Pointer(trayClassName)),
|
|
0, 0, 0, 0, 0, 0, 0, instance, 0,
|
|
)
|
|
if raw == 0 {
|
|
return nil, fmt.Errorf("create tray message window: %v", createErr)
|
|
}
|
|
icon, iconErr := loadFancyIcon()
|
|
ownsIcon := icon != 0
|
|
if icon == 0 {
|
|
fallback, _, fallbackErr := procLoadIcon.Call(0, idiApplication)
|
|
if fallback == 0 {
|
|
procDestroyWindow.Call(raw)
|
|
return nil, fmt.Errorf("load tray icon: generated icon: %v; fallback: %v", iconErr, fallbackErr)
|
|
}
|
|
icon = windows.Handle(fallback)
|
|
}
|
|
taskbarName, _ := windows.UTF16PtrFromString("TaskbarCreated")
|
|
taskbarMessage, _, _ := procRegisterWindowMessage.Call(uintptr(unsafe.Pointer(taskbarName)))
|
|
|
|
t := &trayIcon{
|
|
hwnd: windows.Handle(raw), icon: windows.Handle(icon),
|
|
activeLayout: activeLayout, layouts: append([]string(nil), layouts...),
|
|
taskbarMessage: uint32(taskbarMessage), ownsIcon: ownsIcon,
|
|
}
|
|
activeTray = t
|
|
if err := t.notify(nimAdd); err != nil {
|
|
activeTray = nil
|
|
procDestroyWindow.Call(raw)
|
|
return nil, err
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
func (t *trayIcon) close() {
|
|
if t == nil || t.hwnd == 0 {
|
|
return
|
|
}
|
|
_ = t.notify(nimDelete)
|
|
procDestroyWindow.Call(uintptr(t.hwnd))
|
|
if t.ownsIcon && t.icon != 0 {
|
|
procDestroyIcon.Call(uintptr(t.icon))
|
|
}
|
|
if activeTray == t {
|
|
activeTray = nil
|
|
}
|
|
t.hwnd = 0
|
|
}
|
|
|
|
func (t *trayIcon) setLayouts(active string, layouts []string) {
|
|
if t == nil {
|
|
return
|
|
}
|
|
t.activeLayout = active
|
|
t.layouts = append(t.layouts[:0], layouts...)
|
|
_ = t.notify(nimModify)
|
|
}
|
|
|
|
func (t *trayIcon) notify(operation uintptr) error {
|
|
nid := notifyIconData{
|
|
Size: uint32(unsafe.Sizeof(notifyIconData{})), Window: t.hwnd, ID: 1,
|
|
Flags: nifMessage | nifIcon | nifTip, CallbackMessage: wmAppTray, Icon: t.icon,
|
|
}
|
|
tip, _ := windows.UTF16FromString("FancyWin — " + t.activeLayout)
|
|
copy(nid.Tip[:len(nid.Tip)-1], tip)
|
|
if ok, _, err := procShellNotifyIcon.Call(operation, uintptr(unsafe.Pointer(&nid))); ok == 0 {
|
|
return fmt.Errorf("update notification-area icon: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func trayWndProc(rawHWND, message, wParam, lParam uintptr) uintptr {
|
|
t := activeTray
|
|
if t != nil && t.taskbarMessage != 0 && uint32(message) == t.taskbarMessage {
|
|
_ = t.notify(nimAdd) // Explorer was restarted; restore the icon.
|
|
return 0
|
|
}
|
|
if message == wmAppTray && (uint32(lParam) == wmRButtonUp || uint32(lParam) == wmContextMenu) {
|
|
if t != nil {
|
|
t.showMenu()
|
|
}
|
|
return 0
|
|
}
|
|
r, _, _ := procDefWindowProc.Call(rawHWND, message, wParam, lParam)
|
|
return r
|
|
}
|
|
|
|
func (t *trayIcon) showMenu() {
|
|
menu, _, _ := procCreatePopupMenu.Call()
|
|
if menu == 0 {
|
|
return
|
|
}
|
|
defer procDestroyMenu.Call(menu)
|
|
heading, _ := windows.UTF16PtrFromString("Layouts")
|
|
exit, _ := windows.UTF16PtrFromString("Exit")
|
|
procAppendMenu.Call(menu, mfString|mfGray, 0, uintptr(unsafe.Pointer(heading)))
|
|
for i, name := range t.layouts {
|
|
label, _ := windows.UTF16PtrFromString(name)
|
|
flags := uintptr(mfString)
|
|
if strings.EqualFold(name, t.activeLayout) {
|
|
flags |= mfChecked
|
|
}
|
|
procAppendMenu.Call(menu, flags, uintptr(trayLayoutBase+i), uintptr(unsafe.Pointer(label)))
|
|
}
|
|
procAppendMenu.Call(menu, mfSeparator, 0, 0)
|
|
procAppendMenu.Call(menu, mfString, trayCommandExit, uintptr(unsafe.Pointer(exit)))
|
|
|
|
var p point
|
|
if ok, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&p))); ok == 0 {
|
|
return
|
|
}
|
|
procSetForegroundWindow.Call(uintptr(t.hwnd))
|
|
command, _, _ := procTrackPopupMenu.Call(menu, tpmRightButton|tpmReturnCmd|tpmNonotify, uintptr(p.X), uintptr(p.Y), 0, uintptr(t.hwnd), 0)
|
|
procPostMessage.Call(uintptr(t.hwnd), wmNull, 0, 0)
|
|
if command == trayCommandExit {
|
|
procPostQuitMessage.Call(0)
|
|
} else if command >= trayLayoutBase && command < uintptr(trayLayoutBase+len(t.layouts)) {
|
|
if e := activeEngine; e != nil {
|
|
e.selectLayout(t.layouts[int(command)-trayLayoutBase])
|
|
}
|
|
}
|
|
}
|
|
|
|
func loadFancyIcon() (windows.Handle, error) {
|
|
img, err := png.Decode(bytes.NewReader(assets.TrayIconPNG))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
bounds := img.Bounds()
|
|
w, h := bounds.Dx(), bounds.Dy()
|
|
if w < 1 || h < 1 {
|
|
return 0, fmt.Errorf("embedded icon is empty")
|
|
}
|
|
header := bitmapInfoHeader{
|
|
Size: uint32(unsafe.Sizeof(bitmapInfoHeader{})), Width: int32(w), Height: -int32(h),
|
|
Planes: 1, BitCount: 32, Compression: biRGB, SizeImage: uint32(w * h * 4),
|
|
}
|
|
var bits unsafe.Pointer
|
|
color, _, dibErr := procCreateDIBSection.Call(0, uintptr(unsafe.Pointer(&header)), dibRGBColors, uintptr(unsafe.Pointer(&bits)), 0, 0)
|
|
if color == 0 || bits == nil {
|
|
return 0, fmt.Errorf("CreateDIBSection: %v", dibErr)
|
|
}
|
|
defer procDeleteObject.Call(color)
|
|
pixels := unsafe.Slice((*byte)(bits), w*h*4)
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
r, g, b, a := img.At(bounds.Min.X+x, bounds.Min.Y+y).RGBA()
|
|
i := (y*w + x) * 4
|
|
pixels[i+0] = byte(b >> 8)
|
|
pixels[i+1] = byte(g >> 8)
|
|
pixels[i+2] = byte(r >> 8)
|
|
pixels[i+3] = byte(a >> 8)
|
|
}
|
|
}
|
|
maskStride := ((w + 15) / 16) * 2
|
|
maskBits := make([]byte, maskStride*h)
|
|
mask, _, maskErr := procCreateBitmap.Call(uintptr(w), uintptr(h), 1, 1, uintptr(unsafe.Pointer(&maskBits[0])))
|
|
if mask == 0 {
|
|
return 0, fmt.Errorf("CreateBitmap icon mask: %v", maskErr)
|
|
}
|
|
defer procDeleteObject.Call(mask)
|
|
info := iconInfo{Icon: 1, Mask: windows.Handle(mask), Color: windows.Handle(color)}
|
|
icon, _, iconErr := procCreateIconIndirect.Call(uintptr(unsafe.Pointer(&info)))
|
|
if icon == 0 {
|
|
return 0, fmt.Errorf("CreateIconIndirect: %v", iconErr)
|
|
}
|
|
return windows.Handle(icon), nil
|
|
}
|