feat: add tray control, priority restoration and session logging
This commit is contained in:
1 parent
4f8f6218ad
commit
4880aa46dc
25 files changed
+3072
-676
No files matched your search
@@ -0,0 +1,145 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type guiThreadInfo struct {
|
||||
Size, Flags uint32
|
||||
Active, Focus, Capture, MenuOwner, MoveSize, Caret uintptr
|
||||
CaretRect winRect
|
||||
}
|
||||
|
||||
func waitNative(t *testing.T, condition func() bool, what string) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if condition() {
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Fatal("timeout: " + what)
|
||||
}
|
||||
func postOwned(t *testing.T, h uintptr, msg uint32, wp, lp uintptr) {
|
||||
t.Helper()
|
||||
ok, _, err := pPostMessage.Call(h, uintptr(msg), wp, lp)
|
||||
if ok == 0 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
func ownIconRect(h uintptr) (winRect, bool) {
|
||||
id := notifyIconIdentifier{Size: uint32(unsafe.Sizeof(notifyIconIdentifier{})), Window: h, ID: trayIconID}
|
||||
var rect winRect
|
||||
hr, _, _ := pNotifyRect.Call(uintptr(unsafe.Pointer(&id)), uintptr(unsafe.Pointer(&rect)))
|
||||
return rect, hr == 0
|
||||
}
|
||||
func ownMenuActive(thread uintptr) bool {
|
||||
info := guiThreadInfo{Size: uint32(unsafe.Sizeof(guiThreadInfo{}))}
|
||||
ok, _, _ := u32.NewProc("GetGUIThreadInfo").Call(thread, uintptr(unsafe.Pointer(&info)))
|
||||
return ok != 0 && info.Flags&(0x4|0x10) != 0
|
||||
}
|
||||
|
||||
func TestNativeTrayMouseKeyboardRestartAndControllerExit(t *testing.T) {
|
||||
requireNativeGUI(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
restored := make(chan struct{})
|
||||
tr, err := newTray(cancel, restored)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tr.Close()
|
||||
h := tr.hwnd.Load()
|
||||
thread, _, _ := u32.NewProc("GetWindowThreadProcessId").Call(h, 0)
|
||||
f := newFake()
|
||||
p := f.add(10, 150, PriorityClassNormal)
|
||||
changed := make(chan struct{})
|
||||
var once sync.Once
|
||||
finished := make(chan error, 1)
|
||||
go func() {
|
||||
err := runController(ctx, options{mem: 100, interval: time.Minute}, f, func(r scanReport) error {
|
||||
if len(r.Rows) > 0 {
|
||||
once.Do(func() { close(changed) })
|
||||
}
|
||||
return nil
|
||||
})
|
||||
close(restored)
|
||||
finished <- err
|
||||
}()
|
||||
select {
|
||||
case <-changed:
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("controller did not start")
|
||||
}
|
||||
waitNative(t, func() bool { _, ok := ownIconRect(h); return ok }, "notification icon registration")
|
||||
// Post only to OUR window. No mouse injection, no interaction with other applications.
|
||||
for _, event := range []uint32{0x400, 0x7b, 0x208, 0x20c} {
|
||||
rect, _ := ownIconRect(h)
|
||||
wp := uintptr(uint16(rect.Left)) | uintptr(uint16(rect.Top))<<16
|
||||
postOwned(t, h, trayCallbackMessage, wp, uintptr(event)|trayIconID<<16)
|
||||
waitNative(t, func() bool { return ownMenuActive(thread) }, "popup for a mouse button")
|
||||
postOwned(t, h, 0x1f, 0, 0)
|
||||
waitNative(t, func() bool { return !tr.menuOpen.Load() }, "menu cancellation")
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("cancelling menu unexpectedly exited application")
|
||||
default:
|
||||
}
|
||||
}
|
||||
// Simulate loss of THIS icon, not an actual restart of the user's Explorer.
|
||||
data := notifyIconData{Size: uint32(unsafe.Sizeof(notifyIconData{})), Window: h, ID: trayIconID}
|
||||
removed, _, _ := pShellNotify.Call(2, uintptr(unsafe.Pointer(&data)))
|
||||
if removed == 0 {
|
||||
t.Fatal("could not remove own test icon")
|
||||
}
|
||||
postOwned(t, h, tr.taskbar, 0, 0)
|
||||
waitNative(t, func() bool { _, ok := ownIconRect(h); return ok }, "icon recreation after TaskbarCreated")
|
||||
// The keyboard notification opens the same real menu; select its only item.
|
||||
postOwned(t, h, trayCallbackMessage, 0, 0x401|trayIconID<<16)
|
||||
waitNative(t, func() bool { return ownMenuActive(thread) }, "keyboard menu")
|
||||
postOwned(t, h, 0x100, 0x28, 0) // VK_DOWN
|
||||
postOwned(t, h, 0x100, 0x0d, 0) // VK_RETURN
|
||||
select {
|
||||
case err = <-finished:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("menu selection did not finish restoration")
|
||||
}
|
||||
assertPriority(t, p, PriorityClassNormal)
|
||||
assertClosed(t, f)
|
||||
if err = tr.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := ownIconRect(h); ok {
|
||||
t.Fatal("notification icon was not removed")
|
||||
}
|
||||
}
|
||||
func TestTrayCoordinatesAndNativeLayout(t *testing.T) {
|
||||
x, y := int16(-1200), int16(1400)
|
||||
wp := uintptr(uint16(x)) | uintptr(uint16(y))<<16
|
||||
event, id, point, anchored := trayEvent(true, wp, 0x400|trayIconID<<16)
|
||||
if event != 0x400 || id != trayIconID || point.X != -1200 || point.Y != 1400 || !anchored {
|
||||
t.Fatal("signed multi-monitor anchor lost")
|
||||
}
|
||||
_, _, _, anchored = trayEvent(true, wp, 0x7b|trayIconID<<16)
|
||||
if anchored {
|
||||
t.Fatal("WM_CONTEXTMENU wParam is undefined in the documented v4 contract")
|
||||
}
|
||||
size := unsafe.Sizeof(uintptr(0))
|
||||
want := map[uintptr]uintptr{4: 956, 8: 976}[size]
|
||||
if unsafe.Sizeof(notifyIconData{}) != want {
|
||||
t.Fatal("NOTIFYICONDATAW layout mismatch")
|
||||
}
|
||||
if unsafe.Sizeof(bitmapHeader{}) != 40 {
|
||||
t.Fatal("BITMAPINFOHEADER layout mismatch")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user