fix: restore Windows tray lifecycle

This commit is contained in:
2026-07-15 18:56:22 +08:00
parent 3c1f2c7177
commit 6eeebd39ad
25 changed files with 711 additions and 203 deletions
+22 -5
View File
@@ -11,6 +11,7 @@ import (
"path/filepath"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
@@ -100,6 +101,8 @@ type App struct {
browserInboxEvents map[string]bool
browserInboxEnabled atomic.Bool
allowQuit atomic.Bool
trayReady atomic.Bool
quitOnce sync.Once
}
// SetNotificationService attaches the core-owned plugin notification scheduler.
@@ -221,11 +224,23 @@ func (a *App) Shutdown(ctx context.Context) {
cleanupNativeNotifications(ctx)
}
// BeforeClose hides the primary window until the user explicitly quits from the tray.
// SetTrayReady reports whether the native tray can safely return a hidden window.
func (a *App) SetTrayReady(ready bool) {
if a != nil {
a.trayReady.Store(ready)
}
}
// BeforeClose hides the primary window only after the native tray has confirmed
// that it can return the window. Otherwise the normal Wails close path exits.
func (a *App) BeforeClose(ctx context.Context) bool {
if a.allowQuit.Load() {
return false
}
if !a.trayReady.Load() {
log.Printf("[app] tray is unavailable; allowing normal window close")
return false
}
hideNativeWindow(ctx)
return true
}
@@ -243,10 +258,12 @@ func (a *App) Quit() {
if a == nil {
return
}
a.allowQuit.Store(true)
if a.ctx != nil {
quitNativeApplication(a.ctx)
}
a.quitOnce.Do(func() {
a.allowQuit.Store(true)
if a.ctx != nil {
quitNativeApplication(a.ctx)
}
})
}
// NativeNotificationSender delivers scheduler items through the Wails runtime.
+35 -2
View File
@@ -319,7 +319,7 @@ func TestNativeNotificationSenderUsesStablePluginScopedID(t *testing.T) {
}
}
func TestBeforeCloseHidesWindowUntilUserChoosesQuit(t *testing.T) {
func TestBeforeCloseAllowsExitUntilTrayIsReady(t *testing.T) {
oldHide := hideNativeWindow
defer func() { hideNativeWindow = oldHide }()
@@ -327,8 +327,25 @@ func TestBeforeCloseHidesWindowUntilUserChoosesQuit(t *testing.T) {
hideNativeWindow = func(context.Context) { hideCalls++ }
app := &App{}
if prevent := app.BeforeClose(context.Background()); prevent {
t.Fatal("BeforeClose() = true, want false while tray is unavailable")
}
if hideCalls != 0 {
t.Fatalf("hide calls = %d, want 0", hideCalls)
}
}
func TestBeforeCloseHidesWindowAfterTrayIsReady(t *testing.T) {
oldHide := hideNativeWindow
defer func() { hideNativeWindow = oldHide }()
hideCalls := 0
hideNativeWindow = func(context.Context) { hideCalls++ }
app := &App{}
app.SetTrayReady(true)
if prevent := app.BeforeClose(context.Background()); !prevent {
t.Fatal("BeforeClose() = false, want true while tray mode is active")
t.Fatal("BeforeClose() = false, want true while tray is ready")
}
if hideCalls != 1 {
t.Fatalf("hide calls = %d, want 1", hideCalls)
@@ -353,6 +370,22 @@ func TestTrayQuitAllowsWindowCloseAndQuitsApplication(t *testing.T) {
}
}
func TestTrayQuitRequestsNativeShutdownOnlyOnce(t *testing.T) {
oldQuit := quitNativeApplication
defer func() { quitNativeApplication = oldQuit }()
quitCalls := 0
quitNativeApplication = func(context.Context) { quitCalls++ }
app := &App{ctx: context.Background()}
app.Quit()
app.Quit()
if quitCalls != 1 {
t.Fatalf("native quit calls = %d, want 1", quitCalls)
}
}
func TestShowWindowUsesWailsContext(t *testing.T) {
oldShow := showNativeWindow
defer func() { showNativeWindow = oldShow }()