feat: persist main window state
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
@@ -17,7 +18,8 @@ type AppService struct {
|
||||
SecondWindow *application.WebviewWindow
|
||||
MainWindow *application.WebviewWindow
|
||||
|
||||
Config *AppConfigService
|
||||
Config *AppConfigService
|
||||
windowStateMu sync.Mutex
|
||||
|
||||
Tray *application.SystemTray
|
||||
I18nFS embed.FS
|
||||
@@ -45,15 +47,16 @@ func ConfigureAppService(a *AppService, app *application.App, i18nFS embed.FS, s
|
||||
a.Config = NewAppConfigService(suidb)
|
||||
|
||||
isWindows := runtime.GOOS == "windows"
|
||||
mainWindowState := a.loadMainWindowStateOptions()
|
||||
mainWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
Title: "main",
|
||||
Name: "main",
|
||||
Width: mainWindowWidth,
|
||||
Height: mainWindowHeight,
|
||||
Width: mainWindowState.Width,
|
||||
Height: mainWindowState.Height,
|
||||
MinWidth: mainWindowMinWidth,
|
||||
MinHeight: mainWindowMinHeight,
|
||||
InitialPosition: application.WindowCentered,
|
||||
StartState: application.WindowStateNormal,
|
||||
StartState: mainWindowState.StartState,
|
||||
DisableResize: false,
|
||||
Frameless: isWindows,
|
||||
Mac: application.MacWindow{
|
||||
@@ -83,6 +86,7 @@ func ConfigureAppService(a *AppService, app *application.App, i18nFS embed.FS, s
|
||||
secondWindow.Hide()
|
||||
a.SecondWindow = secondWindow
|
||||
a.MainWindow = mainWindow
|
||||
a.registerMainWindowStatePersistence()
|
||||
|
||||
RegisterWindowAndCallback(HotkeyShowSecondWindow, a.SecondWindow, func() {
|
||||
if a.SecondWindow == nil {
|
||||
@@ -93,6 +97,7 @@ func ConfigureAppService(a *AppService, app *application.App, i18nFS embed.FS, s
|
||||
})
|
||||
|
||||
a.MainWindow.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
|
||||
a.persistMainWindowState()
|
||||
a.MainWindow.Hide()
|
||||
e.Cancel()
|
||||
})
|
||||
|
||||
@@ -58,7 +58,7 @@ func settingsDefinitions() []models.AppSettingDefinition {
|
||||
Label: "启动窗口状态",
|
||||
Type: settingTypeSelect,
|
||||
DefaultValue: "normal",
|
||||
Description: "主窗口启动时默认状态;尺寸恢复由 T-015 继续落地。",
|
||||
Description: "主窗口启动时恢复的窗口状态。",
|
||||
Options: []models.AppSettingOption{
|
||||
{Value: "normal", Label: "普通窗口"},
|
||||
{Value: "maximized", Label: "最大化"},
|
||||
@@ -70,7 +70,7 @@ func settingsDefinitions() []models.AppSettingDefinition {
|
||||
Label: "默认宽度",
|
||||
Type: settingTypeNumber,
|
||||
DefaultValue: "1280",
|
||||
Description: "主窗口默认宽度,当前窗口创建逻辑使用固定默认值。",
|
||||
Description: "主窗口普通状态下保存的宽度。",
|
||||
},
|
||||
{
|
||||
Key: "window.height",
|
||||
@@ -78,15 +78,15 @@ func settingsDefinitions() []models.AppSettingDefinition {
|
||||
Label: "默认高度",
|
||||
Type: settingTypeNumber,
|
||||
DefaultValue: "800",
|
||||
Description: "主窗口默认高度,当前窗口创建逻辑使用固定默认值。",
|
||||
Description: "主窗口普通状态下保存的高度。",
|
||||
},
|
||||
{
|
||||
Key: "window.remember_state",
|
||||
Category: "window",
|
||||
Label: "记住窗口状态",
|
||||
Type: settingTypeBoolean,
|
||||
DefaultValue: "false",
|
||||
Description: "保存主窗口尺寸和最大化状态,T-015 使用该配置。",
|
||||
DefaultValue: "true",
|
||||
Description: "保存并恢复主窗口尺寸和最大化状态。",
|
||||
},
|
||||
{
|
||||
Key: "shortcut.open_second_window",
|
||||
@@ -221,6 +221,17 @@ func defaultAppConfigSQL() string {
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func appConfigDataMigrationSQL() string {
|
||||
return `
|
||||
UPDATE appconfig
|
||||
SET value = 'true',
|
||||
description = '保存并恢复主窗口尺寸和最大化状态。'
|
||||
WHERE key = 'window.remember_state'
|
||||
AND value = 'false'
|
||||
AND description = '保存主窗口尺寸和最大化状态,T-015 使用该配置。';
|
||||
`
|
||||
}
|
||||
|
||||
func sqlString(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
|
||||
}
|
||||
|
||||
@@ -113,6 +113,9 @@ func initDefaultData(db *sql.DB) error {
|
||||
if _, err := db.Exec(defaultAppConfigSQL()); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(appConfigDataMigrationSQL()); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(defaultHotkeysSQL()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestAppConfigServiceSettingsSchema(t *testing.T) {
|
||||
"window.start_state": "normal",
|
||||
"window.width": "1280",
|
||||
"window.height": "800",
|
||||
"window.remember_state": "false",
|
||||
"window.remember_state": "true",
|
||||
"shortcut.open_second_window": "Alt+P",
|
||||
"shortcut.open_main_window": "Alt+M",
|
||||
"data.root_mode": "app_config_dir",
|
||||
@@ -114,6 +114,31 @@ func TestAppConfigServiceSettingsSchema(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateUpgradesWindowRememberStateOldDefault(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
|
||||
if _, err := store.DB.Exec(`
|
||||
UPDATE appconfig
|
||||
SET value = 'false',
|
||||
description = '保存主窗口尺寸和最大化状态,T-015 使用该配置。'
|
||||
WHERE key = 'window.remember_state'
|
||||
`); err != nil {
|
||||
t.Fatalf("set old window remember default: %v", err)
|
||||
}
|
||||
|
||||
if err := Migrate(store.DB); err != nil {
|
||||
t.Fatalf("migrate old window remember default: %v", err)
|
||||
}
|
||||
|
||||
var value string
|
||||
if err := store.DB.QueryRow("SELECT value FROM appconfig WHERE key = 'window.remember_state'").Scan(&value); err != nil {
|
||||
t.Fatalf("query window remember state: %v", err)
|
||||
}
|
||||
if value != "true" {
|
||||
t.Fatalf("window.remember_state = %q, want true", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppConfigServiceValidatesSchemaValues(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
service := NewAppConfigService(store)
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
const (
|
||||
windowStartStateNormal = "normal"
|
||||
windowStartStateMaximized = "maximized"
|
||||
|
||||
settingKeyWindowStartState = "window.start_state"
|
||||
settingKeyWindowWidth = "window.width"
|
||||
settingKeyWindowHeight = "window.height"
|
||||
settingKeyWindowRememberState = "window.remember_state"
|
||||
)
|
||||
|
||||
type mainWindowStateOptions struct {
|
||||
Width int
|
||||
Height int
|
||||
StartState application.WindowState
|
||||
Remember bool
|
||||
}
|
||||
|
||||
func (a *AppService) loadMainWindowStateOptions() mainWindowStateOptions {
|
||||
options := mainWindowStateOptions{
|
||||
Width: mainWindowWidth,
|
||||
Height: mainWindowHeight,
|
||||
StartState: application.WindowStateNormal,
|
||||
Remember: true,
|
||||
}
|
||||
if a == nil || a.Config == nil {
|
||||
return options
|
||||
}
|
||||
|
||||
options.Remember = configBool(a.Config, settingKeyWindowRememberState, true)
|
||||
if !options.Remember {
|
||||
return options
|
||||
}
|
||||
|
||||
options.Width = max(configInt(a.Config, settingKeyWindowWidth, mainWindowWidth), mainWindowMinWidth)
|
||||
options.Height = max(configInt(a.Config, settingKeyWindowHeight, mainWindowHeight), mainWindowMinHeight)
|
||||
if configString(a.Config, settingKeyWindowStartState, windowStartStateNormal) == windowStartStateMaximized {
|
||||
options.StartState = application.WindowStateMaximised
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func (a *AppService) registerMainWindowStatePersistence() {
|
||||
if a == nil || a.MainWindow == nil {
|
||||
return
|
||||
}
|
||||
|
||||
a.MainWindow.OnWindowEvent(events.Common.WindowDidResize, func(_ *application.WindowEvent) {
|
||||
a.persistMainWindowNormalSize()
|
||||
})
|
||||
a.MainWindow.OnWindowEvent(events.Common.WindowMaximise, func(_ *application.WindowEvent) {
|
||||
a.persistMainWindowStartState(windowStartStateMaximized)
|
||||
})
|
||||
a.MainWindow.OnWindowEvent(events.Common.WindowRestore, func(_ *application.WindowEvent) {
|
||||
a.persistMainWindowState()
|
||||
})
|
||||
a.MainWindow.OnWindowEvent(events.Common.WindowUnMaximise, func(_ *application.WindowEvent) {
|
||||
a.persistMainWindowState()
|
||||
})
|
||||
}
|
||||
|
||||
func (a *AppService) shouldRememberMainWindowState() bool {
|
||||
if a == nil || a.Config == nil {
|
||||
return false
|
||||
}
|
||||
return configBool(a.Config, settingKeyWindowRememberState, true)
|
||||
}
|
||||
|
||||
func (a *AppService) persistMainWindowState() {
|
||||
if !a.shouldRememberMainWindowState() || a.MainWindow == nil {
|
||||
return
|
||||
}
|
||||
if a.MainWindow.IsMaximised() {
|
||||
a.persistMainWindowStartState(windowStartStateMaximized)
|
||||
return
|
||||
}
|
||||
if a.MainWindow.IsFullscreen() || a.MainWindow.IsMinimised() {
|
||||
return
|
||||
}
|
||||
a.persistMainWindowNormalSize()
|
||||
}
|
||||
|
||||
func (a *AppService) persistMainWindowStartState(state string) {
|
||||
if !a.shouldRememberMainWindowState() || a.Config == nil {
|
||||
return
|
||||
}
|
||||
|
||||
a.windowStateMu.Lock()
|
||||
defer a.windowStateMu.Unlock()
|
||||
|
||||
if err := a.Config.SetAppConfig(settingKeyWindowStartState, state); err != nil {
|
||||
log.Printf("persist main window state failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AppService) persistMainWindowNormalSize() {
|
||||
if !a.shouldRememberMainWindowState() || a.MainWindow == nil || a.Config == nil {
|
||||
return
|
||||
}
|
||||
if a.MainWindow.IsMaximised() || a.MainWindow.IsFullscreen() || a.MainWindow.IsMinimised() {
|
||||
return
|
||||
}
|
||||
|
||||
width, height := a.MainWindow.Size()
|
||||
if width < mainWindowMinWidth || height < mainWindowMinHeight {
|
||||
return
|
||||
}
|
||||
|
||||
a.windowStateMu.Lock()
|
||||
defer a.windowStateMu.Unlock()
|
||||
|
||||
if err := a.Config.SetAppConfig(settingKeyWindowStartState, windowStartStateNormal); err != nil {
|
||||
log.Printf("persist main window start state failed: %v", err)
|
||||
}
|
||||
if err := a.Config.SetAppConfig(settingKeyWindowWidth, strconv.Itoa(width)); err != nil {
|
||||
log.Printf("persist main window width failed: %v", err)
|
||||
}
|
||||
if err := a.Config.SetAppConfig(settingKeyWindowHeight, strconv.Itoa(height)); err != nil {
|
||||
log.Printf("persist main window height failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func configString(config *AppConfigService, key string, fallback string) string {
|
||||
if config == nil {
|
||||
return fallback
|
||||
}
|
||||
value, err := config.GetAppConfig(key)
|
||||
if err != nil || value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func configBool(config *AppConfigService, key string, fallback bool) bool {
|
||||
value := configString(config, key, "")
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func configInt(config *AppConfigService, key string, fallback int) int {
|
||||
value := configString(config, key, "")
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
func TestLoadMainWindowStateOptionsDefaults(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
service := &AppService{Config: NewAppConfigService(store)}
|
||||
|
||||
options := service.loadMainWindowStateOptions()
|
||||
if options.Width != mainWindowWidth {
|
||||
t.Fatalf("width = %d, want %d", options.Width, mainWindowWidth)
|
||||
}
|
||||
if options.Height != mainWindowHeight {
|
||||
t.Fatalf("height = %d, want %d", options.Height, mainWindowHeight)
|
||||
}
|
||||
if options.StartState != application.WindowStateNormal {
|
||||
t.Fatalf("start state = %v, want normal", options.StartState)
|
||||
}
|
||||
if !options.Remember {
|
||||
t.Fatal("remember = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMainWindowStateOptionsUsesSavedValues(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
config := NewAppConfigService(store)
|
||||
service := &AppService{Config: config}
|
||||
|
||||
if err := config.SetAppConfig(settingKeyWindowWidth, "1440"); err != nil {
|
||||
t.Fatalf("set width: %v", err)
|
||||
}
|
||||
if err := config.SetAppConfig(settingKeyWindowHeight, "900"); err != nil {
|
||||
t.Fatalf("set height: %v", err)
|
||||
}
|
||||
if err := config.SetAppConfig(settingKeyWindowStartState, windowStartStateMaximized); err != nil {
|
||||
t.Fatalf("set start state: %v", err)
|
||||
}
|
||||
|
||||
options := service.loadMainWindowStateOptions()
|
||||
if options.Width != 1440 {
|
||||
t.Fatalf("width = %d, want 1440", options.Width)
|
||||
}
|
||||
if options.Height != 900 {
|
||||
t.Fatalf("height = %d, want 900", options.Height)
|
||||
}
|
||||
if options.StartState != application.WindowStateMaximised {
|
||||
t.Fatalf("start state = %v, want maximised", options.StartState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMainWindowStateOptionsIgnoresSavedValuesWhenDisabled(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
config := NewAppConfigService(store)
|
||||
service := &AppService{Config: config}
|
||||
|
||||
if err := config.SetAppConfig(settingKeyWindowRememberState, "false"); err != nil {
|
||||
t.Fatalf("set remember state: %v", err)
|
||||
}
|
||||
if err := config.SetAppConfig(settingKeyWindowWidth, "1440"); err != nil {
|
||||
t.Fatalf("set width: %v", err)
|
||||
}
|
||||
if err := config.SetAppConfig(settingKeyWindowStartState, windowStartStateMaximized); err != nil {
|
||||
t.Fatalf("set start state: %v", err)
|
||||
}
|
||||
|
||||
options := service.loadMainWindowStateOptions()
|
||||
if options.Width != mainWindowWidth {
|
||||
t.Fatalf("width = %d, want %d", options.Width, mainWindowWidth)
|
||||
}
|
||||
if options.StartState != application.WindowStateNormal {
|
||||
t.Fatalf("start state = %v, want normal", options.StartState)
|
||||
}
|
||||
if options.Remember {
|
||||
t.Fatal("remember = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMainWindowStateOptionsClampsSmallSavedSize(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
config := NewAppConfigService(store)
|
||||
service := &AppService{Config: config}
|
||||
|
||||
if _, err := store.DB.Exec("UPDATE appconfig SET value = '900' WHERE key = ?", settingKeyWindowWidth); err != nil {
|
||||
t.Fatalf("set raw width: %v", err)
|
||||
}
|
||||
if _, err := store.DB.Exec("UPDATE appconfig SET value = '600' WHERE key = ?", settingKeyWindowHeight); err != nil {
|
||||
t.Fatalf("set raw height: %v", err)
|
||||
}
|
||||
|
||||
options := service.loadMainWindowStateOptions()
|
||||
if options.Width != mainWindowMinWidth {
|
||||
t.Fatalf("width = %d, want %d", options.Width, mainWindowMinWidth)
|
||||
}
|
||||
if options.Height != mainWindowMinHeight {
|
||||
t.Fatalf("height = %d, want %d", options.Height, mainWindowMinHeight)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user