feat(ui): 系统托盘 + 关闭最小化到托盘
- 原生 win32 实现的 TrayManager:Shell_NotifyIcon + 私有 message-only 窗口承载 popup 菜单,专用 OS 线程跑 GetMessage 泵,避免引入 cgo 依赖。 - WindowSubclass 通过 SetWindowLongPtrW 子类化 Gio 主窗口,拦截 WM_CLOSE:minimize_to_tray=true 时吞掉消息只隐藏窗口;exit 路径下 放行让 Gio 正常关闭事件循环。 - Preferences 新增"最小化到托盘"复选框,切换后立即重装/卸载钩子, 不需要重启程序。 - 托盘菜单提供 Show / Hide / Exit;左键单击图标等同 Show;右键弹菜单。 - 非 Windows 平台所有相关 API 退化为 ErrUnsupported / no-op stub。
This commit is contained in:
+27
-4
@@ -119,6 +119,11 @@ type appState struct {
|
||||
lastActivity time.Time
|
||||
locked bool
|
||||
lockDialog *passwordDialog
|
||||
|
||||
// tray owns the system-tray icon and (when minimize_to_tray is on)
|
||||
// the WM_CLOSE subclass. nil on platforms or installs where it
|
||||
// could not be set up.
|
||||
tray *trayRuntime
|
||||
}
|
||||
|
||||
// snapshotEntries returns a freshly serialized slice of config entries.
|
||||
@@ -174,6 +179,17 @@ func loop(w *app.Window, configPath string) error {
|
||||
state.registerAllHotkeys()
|
||||
go state.runHotkeyLoop(w)
|
||||
|
||||
// Install the system tray icon + close-hook (Windows only). HWND
|
||||
// discovery races the very first FrameEvent, so kick it from a
|
||||
// goroutine that polls FindWindow for a few hundred ms.
|
||||
_, _, _, minToTray := state.store.Preferences()
|
||||
go func() {
|
||||
tr := installTrayRuntime(i18n.T("app_title"), minToTray, w.Invalidate)
|
||||
state.mu.Lock()
|
||||
state.tray = tr
|
||||
state.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Tick once per second to refresh TOTP codes.
|
||||
go func() {
|
||||
t := time.NewTicker(time.Second)
|
||||
@@ -188,6 +204,7 @@ func loop(w *app.Window, configPath string) error {
|
||||
switch e := w.Event().(type) {
|
||||
case app.DestroyEvent:
|
||||
global.Log.WithField("func", fn).Info("window closed")
|
||||
state.tray.Stop()
|
||||
return e.Err
|
||||
case app.FrameEvent:
|
||||
gtx := app.NewContext(&ops, e)
|
||||
@@ -436,8 +453,8 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind
|
||||
case settingsActionImportLegacy:
|
||||
st.importDialog = newImportLegacyDialog()
|
||||
case settingsActionPreferences:
|
||||
lang, theme, autoLock, _ := st.store.Preferences()
|
||||
st.prefsDialog = newPreferencesDialog(lang, normalizeThemeMode(theme), autoLock)
|
||||
lang, theme, autoLock, minToTray := st.store.Preferences()
|
||||
st.prefsDialog = newPreferencesDialog(lang, normalizeThemeMode(theme), autoLock, minToTray)
|
||||
case settingsActionAbout:
|
||||
st.aboutDialog = newAboutDialog()
|
||||
}
|
||||
@@ -450,11 +467,17 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind
|
||||
if st.prefsDialog != nil {
|
||||
return st.prefsDialog.Layout(gtx, th, func(r preferencesResult) {
|
||||
if !r.cancel {
|
||||
_, _, _, minToTray := st.store.Preferences()
|
||||
st.store.SetPreferences(r.language, string(r.theme), r.autoLockMinutes, minToTray)
|
||||
_, _, _, prevMinToTray := st.store.Preferences()
|
||||
st.store.SetPreferences(r.language, string(r.theme), r.autoLockMinutes, r.minimizeToTray)
|
||||
i18n.SetLanguage(r.language)
|
||||
st.themeMode = r.theme
|
||||
st.theme = nil // force rebuild on next frame
|
||||
// Re-arm or release the WM_CLOSE hook when the user
|
||||
// flips the minimize-to-tray preference. Hot-reloading
|
||||
// avoids forcing a restart for a setting change.
|
||||
if prevMinToTray != r.minimizeToTray && st.tray != nil {
|
||||
st.tray.setMinimizeToTray(r.minimizeToTray)
|
||||
}
|
||||
}
|
||||
st.prefsDialog = nil
|
||||
w.Invalidate()
|
||||
|
||||
Reference in New Issue
Block a user