⚡ Optimize multi-window services
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export * from "./models.js";
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import {Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ServiceOptions provides optional parameters for calls to [NewService].
|
||||||
|
*/
|
||||||
|
export class ServiceOptions {
|
||||||
|
/**
|
||||||
|
* Name can be set to override the name of the service
|
||||||
|
* for logging and debugging purposes.
|
||||||
|
*
|
||||||
|
* If empty, it will default
|
||||||
|
* either to the value obtained through the [ServiceName] interface,
|
||||||
|
* or to the type name.
|
||||||
|
*/
|
||||||
|
"Name": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the service instance implements [http.Handler],
|
||||||
|
* it will be mounted on the internal asset server
|
||||||
|
* at the prefix specified by Route.
|
||||||
|
*/
|
||||||
|
"Route": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MarshalError will be called if non-nil
|
||||||
|
* to marshal to JSON the error values returned by this service's methods.
|
||||||
|
*
|
||||||
|
* MarshalError is not allowed to fail,
|
||||||
|
* but it may return a nil slice to fall back
|
||||||
|
* to the globally configured error handler.
|
||||||
|
*
|
||||||
|
* If the returned slice is not nil, it must contain valid JSON.
|
||||||
|
*/
|
||||||
|
"MarshalError": any;
|
||||||
|
|
||||||
|
/** Creates a new ServiceOptions instance. */
|
||||||
|
constructor($$source: Partial<ServiceOptions> = {}) {
|
||||||
|
if (!("Name" in $$source)) {
|
||||||
|
this["Name"] = "";
|
||||||
|
}
|
||||||
|
if (!("Route" in $$source)) {
|
||||||
|
this["Route"] = "";
|
||||||
|
}
|
||||||
|
if (!("MarshalError" in $$source)) {
|
||||||
|
this["MarshalError"] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ServiceOptions instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): ServiceOptions {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new ServiceOptions($$parsedSource as Partial<ServiceOptions>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Window = any;
|
||||||
@@ -19,7 +19,6 @@ import * as ThemeService from "./themeservice.js";
|
|||||||
import * as TranslationService from "./translationservice.js";
|
import * as TranslationService from "./translationservice.js";
|
||||||
import * as TrayService from "./trayservice.js";
|
import * as TrayService from "./trayservice.js";
|
||||||
import * as WindowService from "./windowservice.js";
|
import * as WindowService from "./windowservice.js";
|
||||||
import * as WindowSnapService from "./windowsnapservice.js";
|
|
||||||
export {
|
export {
|
||||||
BackupService,
|
BackupService,
|
||||||
ConfigService,
|
ConfigService,
|
||||||
@@ -38,8 +37,7 @@ export {
|
|||||||
ThemeService,
|
ThemeService,
|
||||||
TranslationService,
|
TranslationService,
|
||||||
TrayService,
|
TrayService,
|
||||||
WindowService,
|
WindowService
|
||||||
WindowSnapService
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export * from "./models.js";
|
export * from "./models.js";
|
||||||
|
|||||||
@@ -391,26 +391,6 @@ export class SystemInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* WindowSnapService 窗口吸附服务
|
|
||||||
*/
|
|
||||||
export class WindowSnapService {
|
|
||||||
|
|
||||||
/** Creates a new WindowSnapService instance. */
|
|
||||||
constructor($$source: Partial<WindowSnapService> = {}) {
|
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new WindowSnapService instance from a string or object.
|
|
||||||
*/
|
|
||||||
static createFrom($$source: any = {}): WindowSnapService {
|
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
|
||||||
return new WindowSnapService($$parsedSource as Partial<WindowSnapService>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = $Create.Map($Create.Any, $Create.Any);
|
const $$createType0 = $Create.Map($Create.Any, $Create.Any);
|
||||||
var $$createType1 = (function $$initCreateType1(...args): any {
|
var $$createType1 = (function $$initCreateType1(...args): any {
|
||||||
|
|||||||
@@ -14,22 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore: Unused imports
|
|
||||||
import * as $models from "./models.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GetOpenDocumentWindows 获取所有文档窗口
|
|
||||||
*/
|
|
||||||
export function GetOpenDocumentWindows(): Promise<application$0.Window[]> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(3057936408) as any;
|
|
||||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
|
||||||
return $$createType0($result);
|
|
||||||
}) as any;
|
|
||||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
|
||||||
return $typingPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GetOpenWindows 获取所有打开的文档窗口
|
* GetOpenWindows 获取所有打开的文档窗口
|
||||||
*/
|
*/
|
||||||
@@ -67,10 +51,10 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SetWindowSnapService 设置窗口吸附服务引用
|
* ServiceStartup 服务启动时初始化
|
||||||
*/
|
*/
|
||||||
export function SetWindowSnapService(snapService: $models.WindowSnapService | null): Promise<void> & { cancel(): void } {
|
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(1105193745, snapService) as any;
|
let $resultPromise = $Call.ByID(2432987694, options) as any;
|
||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
|
||||||
// This file is automatically generated. DO NOT EDIT
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WindowSnapService 窗口吸附服务
|
|
||||||
* @module
|
|
||||||
*/
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore: Unused imports
|
|
||||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore: Unused imports
|
|
||||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup 清理资源
|
|
||||||
*/
|
|
||||||
export function Cleanup(): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(2155505498) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GetCurrentThreshold 获取当前自适应阈值(用于调试或显示)
|
|
||||||
*/
|
|
||||||
export function GetCurrentThreshold(): Promise<number> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(3176419026) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GetDiagnosticInfo 获取诊断信息(用于调试)
|
|
||||||
*/
|
|
||||||
export function GetDiagnosticInfo(): Promise<{ [_: string]: any }> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(39381769) as any;
|
|
||||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
|
||||||
return $$createType0($result);
|
|
||||||
}) as any;
|
|
||||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
|
||||||
return $typingPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OnWindowSnapConfigChanged 处理窗口吸附配置变更
|
|
||||||
*/
|
|
||||||
export function OnWindowSnapConfigChanged(enabled: boolean): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(3794787039, enabled) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegisterWindow 注册需要吸附管理的窗口
|
|
||||||
*/
|
|
||||||
export function RegisterWindow(documentID: number, window: application$0.WebviewWindow | null): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(1000222723, documentID, window) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ServiceShutdown 实现服务关闭接口
|
|
||||||
*/
|
|
||||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(1172710495) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ServiceStartup 服务启动时初始化
|
|
||||||
*/
|
|
||||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(2456823262, options) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SetSnapEnabled 设置是否启用窗口吸附
|
|
||||||
*/
|
|
||||||
export function SetSnapEnabled(enabled: boolean): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(2280126835, enabled) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UnregisterWindow 取消注册窗口
|
|
||||||
*/
|
|
||||||
export function UnregisterWindow(documentID: number): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(2844230768, documentID) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private type creation functions
|
|
||||||
const $$createType0 = $Create.Map($Create.Any, $Create.Any);
|
|
||||||
@@ -39,11 +39,11 @@ type SelfUpdateService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewSelfUpdateService 创建自我更新服务实例
|
// NewSelfUpdateService 创建自我更新服务实例
|
||||||
func NewSelfUpdateService(configService *ConfigService, badgeService *dock.DockService, notificationService *notifications.NotificationService, logger *log.LogService) (*SelfUpdateService, error) {
|
func NewSelfUpdateService(configService *ConfigService, badgeService *dock.DockService, notificationService *notifications.NotificationService, logger *log.LogService) *SelfUpdateService {
|
||||||
// 获取配置
|
// 获取配置
|
||||||
appConfig, err := configService.GetConfig()
|
appConfig, err := configService.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get config: %w", err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
service := &SelfUpdateService{
|
service := &SelfUpdateService{
|
||||||
@@ -55,7 +55,7 @@ func NewSelfUpdateService(configService *ConfigService, badgeService *dock.DockS
|
|||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
return service, nil
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckForUpdates 检查更新
|
// CheckForUpdates 检查更新
|
||||||
|
|||||||
@@ -58,14 +58,11 @@ func NewServiceManager() *ServiceManager {
|
|||||||
// 初始化文档服务
|
// 初始化文档服务
|
||||||
documentService := NewDocumentService(databaseService, logger)
|
documentService := NewDocumentService(databaseService, logger)
|
||||||
|
|
||||||
// 初始化窗口服务
|
|
||||||
windowService := NewWindowService(logger, documentService)
|
|
||||||
|
|
||||||
// 初始化窗口吸附服务
|
// 初始化窗口吸附服务
|
||||||
windowSnapService := NewWindowSnapService(logger, configService)
|
windowSnapService := NewWindowSnapService(logger, configService)
|
||||||
|
|
||||||
// 将吸附服务与窗口服务关联
|
// 初始化窗口服务
|
||||||
windowService.SetWindowSnapService(windowSnapService)
|
windowService := NewWindowService(logger, documentService, windowSnapService)
|
||||||
|
|
||||||
// 初始化系统服务
|
// 初始化系统服务
|
||||||
systemService := NewSystemService(logger)
|
systemService := NewSystemService(logger)
|
||||||
@@ -89,10 +86,7 @@ func NewServiceManager() *ServiceManager {
|
|||||||
startupService := NewStartupService(configService, logger)
|
startupService := NewStartupService(configService, logger)
|
||||||
|
|
||||||
// 初始化自我更新服务
|
// 初始化自我更新服务
|
||||||
selfUpdateService, err := NewSelfUpdateService(configService, badgeService, notificationService, logger)
|
selfUpdateService := NewSelfUpdateService(configService, badgeService, notificationService, logger)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化翻译服务
|
// 初始化翻译服务
|
||||||
translationService := NewTranslationService(logger)
|
translationService := NewTranslationService(logger)
|
||||||
@@ -110,7 +104,7 @@ func NewServiceManager() *ServiceManager {
|
|||||||
testService := NewTestService(badgeService, notificationService, logger)
|
testService := NewTestService(badgeService, notificationService, logger)
|
||||||
|
|
||||||
// 使用新的配置通知系统设置热键配置变更监听
|
// 使用新的配置通知系统设置热键配置变更监听
|
||||||
err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
err := configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
||||||
return hotkeyService.UpdateHotkey(enable, hotkey)
|
return hotkeyService.UpdateHotkey(enable, hotkey)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -190,7 +184,6 @@ func (sm *ServiceManager) GetServices() []application.Service {
|
|||||||
application.NewService(sm.testService),
|
application.NewService(sm.testService),
|
||||||
application.NewService(sm.BackupService),
|
application.NewService(sm.BackupService),
|
||||||
application.NewService(sm.httpClientService),
|
application.NewService(sm.httpClientService),
|
||||||
application.NewService(sm.windowSnapService),
|
|
||||||
}
|
}
|
||||||
return services
|
return services
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"voidraft/internal/common/constant"
|
"voidraft/internal/common/constant"
|
||||||
@@ -19,20 +20,22 @@ type WindowService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewWindowService 创建新的窗口服务实例
|
// NewWindowService 创建新的窗口服务实例
|
||||||
func NewWindowService(logger *log.LogService, documentService *DocumentService) *WindowService {
|
func NewWindowService(logger *log.LogService, documentService *DocumentService, windowSnapService *WindowSnapService) *WindowService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &WindowService{
|
return &WindowService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
documentService: documentService,
|
documentService: documentService,
|
||||||
|
windowSnapService: windowSnapService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWindowSnapService 设置窗口吸附服务引用
|
// ServiceStartup 服务启动时初始化
|
||||||
func (ws *WindowService) SetWindowSnapService(snapService *WindowSnapService) {
|
func (ws *WindowService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||||
ws.windowSnapService = snapService
|
ws.windowSnapService.UpdateMainWindowCache()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenDocumentWindow 为指定文档ID打开新窗口
|
// OpenDocumentWindow 为指定文档ID打开新窗口
|
||||||
@@ -115,20 +118,6 @@ func (ws *WindowService) GetOpenWindows() []application.Window {
|
|||||||
return app.Window.GetAll()
|
return app.Window.GetAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOpenDocumentWindows 获取所有文档窗口
|
|
||||||
func (ws *WindowService) GetOpenDocumentWindows() []application.Window {
|
|
||||||
app := application.Get()
|
|
||||||
allWindows := app.Window.GetAll()
|
|
||||||
|
|
||||||
var docWindows []application.Window
|
|
||||||
for _, window := range allWindows {
|
|
||||||
if window.Name() != constant.VOIDRAFT_MAIN_WINDOW_NAME {
|
|
||||||
docWindows = append(docWindows, window)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return docWindows
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDocumentWindowOpen 检查指定文档的窗口是否已打开
|
// IsDocumentWindowOpen 检查指定文档的窗口是否已打开
|
||||||
func (ws *WindowService) IsDocumentWindowOpen(documentID int64) bool {
|
func (ws *WindowService) IsDocumentWindowOpen(documentID int64) bool {
|
||||||
app := application.Get()
|
app := application.Get()
|
||||||
@@ -141,7 +130,7 @@ func (ws *WindowService) IsDocumentWindowOpen(documentID int64) bool {
|
|||||||
func (ws *WindowService) ServiceShutdown() error {
|
func (ws *WindowService) ServiceShutdown() error {
|
||||||
// 从吸附服务中取消注册所有窗口
|
// 从吸附服务中取消注册所有窗口
|
||||||
if ws.windowSnapService != nil {
|
if ws.windowSnapService != nil {
|
||||||
windows := ws.GetOpenDocumentWindows()
|
windows := ws.GetOpenWindows()
|
||||||
for _, window := range windows {
|
for _, window := range windows {
|
||||||
if documentID, err := strconv.ParseInt(window.Name(), 10, 64); err == nil {
|
if documentID, err := strconv.ParseInt(window.Name(), 10, 64); err == nil {
|
||||||
ws.windowSnapService.UnregisterWindow(documentID)
|
ws.windowSnapService.UnregisterWindow(documentID)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"math"
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -12,6 +11,16 @@ import (
|
|||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 防抖和检测常量
|
||||||
|
const (
|
||||||
|
// 移动事件防抖阈值:连续移动事件间隔小于此值时忽略
|
||||||
|
debounceThreshold = 30 * time.Millisecond
|
||||||
|
|
||||||
|
// 用户拖拽检测阈值:快速移动被认为是用户主动拖拽
|
||||||
|
// 设置为稍大于防抖阈值,确保逻辑一致
|
||||||
|
dragDetectionThreshold = 40 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
// WindowSnapService 窗口吸附服务
|
// WindowSnapService 窗口吸附服务
|
||||||
type WindowSnapService struct {
|
type WindowSnapService struct {
|
||||||
logger *log.LogService
|
logger *log.LogService
|
||||||
@@ -34,6 +43,16 @@ type WindowSnapService struct {
|
|||||||
// 管理的窗口
|
// 管理的窗口
|
||||||
managedWindows map[int64]*models.WindowInfo // documentID -> WindowInfo
|
managedWindows map[int64]*models.WindowInfo // documentID -> WindowInfo
|
||||||
windowRefs map[int64]*application.WebviewWindow // documentID -> Window引用
|
windowRefs map[int64]*application.WebviewWindow // documentID -> Window引用
|
||||||
|
|
||||||
|
// 窗口尺寸缓存
|
||||||
|
windowSizeCache map[int64][2]int // documentID -> [width, height]
|
||||||
|
|
||||||
|
// 事件循环保护
|
||||||
|
isUpdatingPosition map[int64]bool // documentID -> 是否正在更新位置
|
||||||
|
|
||||||
|
// 事件监听器清理函数
|
||||||
|
mainMoveUnhook func() // 主窗口移动监听清理函数
|
||||||
|
windowMoveUnhooks map[int64]func() // documentID -> 子窗口移动监听清理函数
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWindowSnapService 创建新的窗口吸附服务实例
|
// NewWindowSnapService 创建新的窗口吸附服务实例
|
||||||
@@ -60,29 +79,19 @@ func NewWindowSnapService(logger *log.LogService, configService *ConfigService)
|
|||||||
maxThreshold: 40, // 最大40像素(大屏幕上限)
|
maxThreshold: 40, // 最大40像素(大屏幕上限)
|
||||||
managedWindows: make(map[int64]*models.WindowInfo),
|
managedWindows: make(map[int64]*models.WindowInfo),
|
||||||
windowRefs: make(map[int64]*application.WebviewWindow),
|
windowRefs: make(map[int64]*application.WebviewWindow),
|
||||||
|
windowSizeCache: make(map[int64][2]int),
|
||||||
|
isUpdatingPosition: make(map[int64]bool),
|
||||||
|
windowMoveUnhooks: make(map[int64]func()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceStartup 服务启动时初始化
|
|
||||||
func (wss *WindowSnapService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
|
||||||
// 初始化主窗口位置缓存
|
|
||||||
wss.updateMainWindowCache()
|
|
||||||
|
|
||||||
wss.setupMainWindowEvents()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterWindow 注册需要吸附管理的窗口
|
// RegisterWindow 注册需要吸附管理的窗口
|
||||||
func (wss *WindowSnapService) RegisterWindow(documentID int64, window *application.WebviewWindow) {
|
func (wss *WindowSnapService) RegisterWindow(documentID int64, window *application.WebviewWindow) {
|
||||||
wss.mu.Lock()
|
wss.mu.Lock()
|
||||||
defer wss.mu.Unlock()
|
defer wss.mu.Unlock()
|
||||||
|
|
||||||
wss.logger.Info("[WindowSnap] RegisterWindow - DocumentID: %d, SnapEnabled: %v", documentID, wss.snapEnabled)
|
|
||||||
|
|
||||||
// 获取初始位置
|
// 获取初始位置
|
||||||
x, y := window.Position()
|
x, y := window.Position()
|
||||||
wss.logger.Info("[WindowSnap] Initial position - X: %d, Y: %d", x, y)
|
|
||||||
|
|
||||||
windowInfo := &models.WindowInfo{
|
windowInfo := &models.WindowInfo{
|
||||||
DocumentID: documentID,
|
DocumentID: documentID,
|
||||||
@@ -96,7 +105,13 @@ func (wss *WindowSnapService) RegisterWindow(documentID int64, window *applicati
|
|||||||
wss.managedWindows[documentID] = windowInfo
|
wss.managedWindows[documentID] = windowInfo
|
||||||
wss.windowRefs[documentID] = window
|
wss.windowRefs[documentID] = window
|
||||||
|
|
||||||
wss.logger.Info("[WindowSnap] Managed windows count: %d", len(wss.managedWindows))
|
// 初始化窗口尺寸缓存
|
||||||
|
wss.updateWindowSizeCacheLocked(documentID, window)
|
||||||
|
|
||||||
|
// 如果这是第一个注册的窗口,启动主窗口事件监听
|
||||||
|
if len(wss.managedWindows) == 1 {
|
||||||
|
wss.setupMainWindowEvents()
|
||||||
|
}
|
||||||
|
|
||||||
// 为窗口设置移动事件监听
|
// 为窗口设置移动事件监听
|
||||||
wss.setupWindowEvents(window, windowInfo)
|
wss.setupWindowEvents(window, windowInfo)
|
||||||
@@ -107,8 +122,21 @@ func (wss *WindowSnapService) UnregisterWindow(documentID int64) {
|
|||||||
wss.mu.Lock()
|
wss.mu.Lock()
|
||||||
defer wss.mu.Unlock()
|
defer wss.mu.Unlock()
|
||||||
|
|
||||||
|
// 清理子窗口事件监听
|
||||||
|
if unhook, exists := wss.windowMoveUnhooks[documentID]; exists {
|
||||||
|
unhook()
|
||||||
|
delete(wss.windowMoveUnhooks, documentID)
|
||||||
|
}
|
||||||
|
|
||||||
delete(wss.managedWindows, documentID)
|
delete(wss.managedWindows, documentID)
|
||||||
delete(wss.windowRefs, documentID)
|
delete(wss.windowRefs, documentID)
|
||||||
|
delete(wss.windowSizeCache, documentID)
|
||||||
|
delete(wss.isUpdatingPosition, documentID)
|
||||||
|
|
||||||
|
// 如果没有管理的窗口了,取消主窗口事件监听
|
||||||
|
if len(wss.managedWindows) == 0 {
|
||||||
|
wss.cleanupMainWindowEvents()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSnapEnabled 设置是否启用窗口吸附
|
// SetSnapEnabled 设置是否启用窗口吸附
|
||||||
@@ -155,7 +183,7 @@ func (wss *WindowSnapService) calculateAdaptiveThreshold() int {
|
|||||||
return adaptiveThreshold
|
return adaptiveThreshold
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentThreshold 获取当前自适应阈值(用于调试或显示)
|
// GetCurrentThreshold 获取当前自适应阈值
|
||||||
func (wss *WindowSnapService) GetCurrentThreshold() int {
|
func (wss *WindowSnapService) GetCurrentThreshold() int {
|
||||||
wss.mu.RLock()
|
wss.mu.RLock()
|
||||||
defer wss.mu.RUnlock()
|
defer wss.mu.RUnlock()
|
||||||
@@ -170,53 +198,125 @@ func (wss *WindowSnapService) OnWindowSnapConfigChanged(enabled bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupMainWindowEvents 设置主窗口事件监听
|
// setupMainWindowEventsLocked 设置主窗口事件监听
|
||||||
func (wss *WindowSnapService) setupMainWindowEvents() {
|
func (wss *WindowSnapService) setupMainWindowEvents() {
|
||||||
// 获取主窗口
|
// 如果已经设置过,不重复设置
|
||||||
|
if wss.mainMoveUnhook != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在锁外获取主窗口
|
||||||
|
wss.mu.Unlock()
|
||||||
mainWindow, ok := wss.windowHelper.GetMainWindow()
|
mainWindow, ok := wss.windowHelper.GetMainWindow()
|
||||||
|
wss.mu.Lock()
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听主窗口移动事件
|
// 监听主窗口移动事件
|
||||||
mainWindow.RegisterHook(events.Common.WindowDidMove, func(event *application.WindowEvent) {
|
wss.mainMoveUnhook = mainWindow.RegisterHook(events.Common.WindowDidMove, func(event *application.WindowEvent) {
|
||||||
wss.onMainWindowMoved()
|
wss.onMainWindowMoved()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupMainWindowEventsLocked 清理主窗口事件监听
|
||||||
|
func (wss *WindowSnapService) cleanupMainWindowEvents() {
|
||||||
|
// 调用清理函数取消监听
|
||||||
|
if wss.mainMoveUnhook != nil {
|
||||||
|
wss.mainMoveUnhook()
|
||||||
|
wss.mainMoveUnhook = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupWindowEvents 为子窗口设置事件监听
|
// setupWindowEvents 为子窗口设置事件监听
|
||||||
func (wss *WindowSnapService) setupWindowEvents(window *application.WebviewWindow, windowInfo *models.WindowInfo) {
|
func (wss *WindowSnapService) setupWindowEvents(window *application.WebviewWindow, windowInfo *models.WindowInfo) {
|
||||||
// 监听子窗口移动事件
|
// 监听子窗口移动事件,保存清理函数
|
||||||
window.RegisterHook(events.Common.WindowDidMove, func(event *application.WindowEvent) {
|
unhook := window.RegisterHook(events.Common.WindowDidMove, func(event *application.WindowEvent) {
|
||||||
wss.onChildWindowMoved(window, windowInfo)
|
wss.onChildWindowMoved(window, windowInfo)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 保存清理函数以便后续取消监听
|
||||||
|
wss.windowMoveUnhooks[windowInfo.DocumentID] = unhook
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateMainWindowCache 更新主窗口缓存
|
// updateMainWindowCache 更新主窗口缓存
|
||||||
func (wss *WindowSnapService) updateMainWindowCache() {
|
func (wss *WindowSnapService) updateMainWindowCacheLocked() {
|
||||||
mainWindow := wss.windowHelper.MustGetMainWindow()
|
mainWindow := wss.windowHelper.MustGetMainWindow()
|
||||||
if mainWindow == nil {
|
if mainWindow == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在锁外获取窗口信息,避免死锁
|
||||||
|
wss.mu.Unlock()
|
||||||
x, y := mainWindow.Position()
|
x, y := mainWindow.Position()
|
||||||
w, h := mainWindow.Size()
|
w, h := mainWindow.Size()
|
||||||
|
wss.mu.Lock()
|
||||||
|
|
||||||
wss.lastMainWindowPos = models.WindowPosition{X: x, Y: y}
|
wss.lastMainWindowPos = models.WindowPosition{X: x, Y: y}
|
||||||
wss.lastMainWindowSize = [2]int{w, h}
|
wss.lastMainWindowSize = [2]int{w, h}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateMainWindowCache 更新主窗口缓存
|
||||||
|
func (wss *WindowSnapService) UpdateMainWindowCache() {
|
||||||
|
wss.mu.Lock()
|
||||||
|
defer wss.mu.Unlock()
|
||||||
|
wss.updateMainWindowCacheLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateWindowSizeCacheLocked 更新窗口尺寸缓存
|
||||||
|
func (wss *WindowSnapService) updateWindowSizeCacheLocked(documentID int64, window *application.WebviewWindow) {
|
||||||
|
// 在锁外获取窗口尺寸,避免死锁
|
||||||
|
wss.mu.Unlock()
|
||||||
|
w, h := window.Size()
|
||||||
|
wss.mu.Lock()
|
||||||
|
|
||||||
|
wss.windowSizeCache[documentID] = [2]int{w, h}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getWindowSizeCached 获取缓存的窗口尺寸,如果不存在则实时获取并缓存
|
||||||
|
func (wss *WindowSnapService) getWindowSizeCached(documentID int64, window *application.WebviewWindow) (int, int) {
|
||||||
|
// 先检查缓存
|
||||||
|
if size, exists := wss.windowSizeCache[documentID]; exists {
|
||||||
|
return size[0], size[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存不存在,实时获取并缓存
|
||||||
|
wss.updateWindowSizeCacheLocked(documentID, window)
|
||||||
|
|
||||||
|
if size, exists := wss.windowSizeCache[documentID]; exists {
|
||||||
|
return size[0], size[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回实时尺寸
|
||||||
|
wss.mu.Unlock()
|
||||||
|
w, h := window.Size()
|
||||||
|
wss.mu.Lock()
|
||||||
|
return w, h
|
||||||
|
}
|
||||||
|
|
||||||
// onMainWindowMoved 主窗口移动事件处理
|
// onMainWindowMoved 主窗口移动事件处理
|
||||||
func (wss *WindowSnapService) onMainWindowMoved() {
|
func (wss *WindowSnapService) onMainWindowMoved() {
|
||||||
if !wss.snapEnabled {
|
if !wss.snapEnabled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 先在锁外获取主窗口的位置和尺寸
|
||||||
|
mainWindow := wss.windowHelper.MustGetMainWindow()
|
||||||
|
if mainWindow == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
x, y := mainWindow.Position()
|
||||||
|
w, h := mainWindow.Size()
|
||||||
|
|
||||||
wss.mu.Lock()
|
wss.mu.Lock()
|
||||||
defer wss.mu.Unlock()
|
defer wss.mu.Unlock()
|
||||||
|
|
||||||
// 更新主窗口缓存
|
// 更新主窗口位置和尺寸缓存
|
||||||
wss.updateMainWindowCache()
|
wss.lastMainWindowPos = models.WindowPosition{X: x, Y: y}
|
||||||
|
wss.lastMainWindowSize = [2]int{w, h}
|
||||||
|
|
||||||
// 只更新已吸附窗口的位置,无需重新检测所有窗口
|
// 只更新已吸附窗口的位置,无需重新检测所有窗口
|
||||||
for _, windowInfo := range wss.managedWindows {
|
for _, windowInfo := range wss.managedWindows {
|
||||||
@@ -232,13 +332,20 @@ func (wss *WindowSnapService) onChildWindowMoved(window *application.WebviewWind
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 事件循环保护:如果正在更新位置,忽略此次事件
|
||||||
wss.mu.Lock()
|
wss.mu.Lock()
|
||||||
defer wss.mu.Unlock()
|
if wss.isUpdatingPosition[windowInfo.DocumentID] {
|
||||||
|
wss.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wss.mu.Unlock()
|
||||||
|
|
||||||
// 获取当前位置
|
|
||||||
x, y := window.Position()
|
x, y := window.Position()
|
||||||
currentPos := models.WindowPosition{X: x, Y: y}
|
currentPos := models.WindowPosition{X: x, Y: y}
|
||||||
|
|
||||||
|
wss.mu.Lock()
|
||||||
|
defer wss.mu.Unlock()
|
||||||
|
|
||||||
// 检查是否真的移动了(避免无效触发)
|
// 检查是否真的移动了(避免无效触发)
|
||||||
if currentPos.X == windowInfo.LastPos.X && currentPos.Y == windowInfo.LastPos.Y {
|
if currentPos.X == windowInfo.LastPos.X && currentPos.Y == windowInfo.LastPos.Y {
|
||||||
return
|
return
|
||||||
@@ -270,11 +377,22 @@ func (wss *WindowSnapService) updateSnappedWindowPosition(windowInfo *models.Win
|
|||||||
expectedX := wss.lastMainWindowPos.X + windowInfo.SnapOffset.X
|
expectedX := wss.lastMainWindowPos.X + windowInfo.SnapOffset.X
|
||||||
expectedY := wss.lastMainWindowPos.Y + windowInfo.SnapOffset.Y
|
expectedY := wss.lastMainWindowPos.Y + windowInfo.SnapOffset.Y
|
||||||
|
|
||||||
// 查找对应的window对象并移动
|
// 查找对应的window对象
|
||||||
if window, exists := wss.windowRefs[windowInfo.DocumentID]; exists {
|
window, exists := wss.windowRefs[windowInfo.DocumentID]
|
||||||
window.SetPosition(expectedX, expectedY)
|
if !exists {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置更新标志,防止事件循环
|
||||||
|
wss.isUpdatingPosition[windowInfo.DocumentID] = true
|
||||||
|
|
||||||
|
wss.mu.Unlock()
|
||||||
|
window.SetPosition(expectedX, expectedY)
|
||||||
|
wss.mu.Lock()
|
||||||
|
|
||||||
|
// 清除更新标志
|
||||||
|
wss.isUpdatingPosition[windowInfo.DocumentID] = false
|
||||||
|
|
||||||
windowInfo.LastPos = models.WindowPosition{X: expectedX, Y: expectedY}
|
windowInfo.LastPos = models.WindowPosition{X: expectedX, Y: expectedY}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,9 +407,9 @@ func (wss *WindowSnapService) handleSnappedWindow(window *application.WebviewWin
|
|||||||
distanceY := math.Abs(float64(currentPos.Y - expectedY))
|
distanceY := math.Abs(float64(currentPos.Y - expectedY))
|
||||||
maxDistance := math.Max(distanceX, distanceY)
|
maxDistance := math.Max(distanceX, distanceY)
|
||||||
|
|
||||||
// 用户拖拽检测:距离超过阈值且移动很快
|
// 用户拖拽检测:距离超过阈值且移动很快(使用统一的拖拽检测阈值)
|
||||||
userDragThreshold := float64(wss.calculateAdaptiveThreshold())
|
userDragThreshold := float64(wss.calculateAdaptiveThreshold())
|
||||||
isUserDrag := maxDistance > userDragThreshold && time.Since(windowInfo.MoveTime) < 50*time.Millisecond
|
isUserDrag := maxDistance > userDragThreshold && time.Since(windowInfo.MoveTime) < dragDetectionThreshold
|
||||||
|
|
||||||
if isUserDrag {
|
if isUserDrag {
|
||||||
// 用户主动拖拽,解除吸附
|
// 用户主动拖拽,解除吸附
|
||||||
@@ -310,8 +428,17 @@ func (wss *WindowSnapService) handleUnsnappedWindow(window *application.WebviewW
|
|||||||
windowInfo.SnapEdge = snapEdge
|
windowInfo.SnapEdge = snapEdge
|
||||||
|
|
||||||
// 执行吸附移动
|
// 执行吸附移动
|
||||||
targetPos := wss.calculateSnapPosition(snapEdge, currentPos, window)
|
targetPos := wss.calculateSnapPosition(snapEdge, currentPos, windowInfo.DocumentID, window)
|
||||||
|
|
||||||
|
// 设置更新标志,防止事件循环
|
||||||
|
wss.isUpdatingPosition[windowInfo.DocumentID] = true
|
||||||
|
|
||||||
|
wss.mu.Unlock()
|
||||||
window.SetPosition(targetPos.X, targetPos.Y)
|
window.SetPosition(targetPos.X, targetPos.Y)
|
||||||
|
wss.mu.Lock()
|
||||||
|
|
||||||
|
// 清除更新标志
|
||||||
|
wss.isUpdatingPosition[windowInfo.DocumentID] = false
|
||||||
|
|
||||||
// 计算并保存偏移量
|
// 计算并保存偏移量
|
||||||
windowInfo.SnapOffset.X = targetPos.X - wss.lastMainWindowPos.X
|
windowInfo.SnapOffset.X = targetPos.X - wss.lastMainWindowPos.X
|
||||||
@@ -326,32 +453,26 @@ func (wss *WindowSnapService) handleUnsnappedWindow(window *application.WebviewW
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// getWindowPosition 获取窗口的位置
|
// shouldSnapToMainWindow 吸附检测
|
||||||
func (wss *WindowSnapService) getWindowPosition(window *application.WebviewWindow) (models.WindowPosition, bool) {
|
|
||||||
x, y := window.Position()
|
|
||||||
return models.WindowPosition{X: x, Y: y}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// shouldSnapToMainWindow 优化版吸附检测
|
|
||||||
func (wss *WindowSnapService) shouldSnapToMainWindow(window *application.WebviewWindow, windowInfo *models.WindowInfo, currentPos models.WindowPosition, lastMoveTime time.Time) (bool, models.SnapEdge) {
|
func (wss *WindowSnapService) shouldSnapToMainWindow(window *application.WebviewWindow, windowInfo *models.WindowInfo, currentPos models.WindowPosition, lastMoveTime time.Time) (bool, models.SnapEdge) {
|
||||||
// 防抖:移动太快时不检测,
|
// 防抖:移动太快时不检测(使用统一的防抖阈值)
|
||||||
timeSinceLastMove := time.Since(lastMoveTime)
|
timeSinceLastMove := time.Since(lastMoveTime)
|
||||||
if timeSinceLastMove < 30*time.Millisecond && timeSinceLastMove > 0 {
|
if timeSinceLastMove < debounceThreshold {
|
||||||
return false, models.SnapEdgeNone
|
return false, models.SnapEdgeNone
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用缓存的主窗口位置和尺寸
|
// 使用缓存的主窗口位置和尺寸
|
||||||
if wss.lastMainWindowSize[0] == 0 || wss.lastMainWindowSize[1] == 0 {
|
if wss.lastMainWindowSize[0] == 0 || wss.lastMainWindowSize[1] == 0 {
|
||||||
// 主窗口缓存未初始化,立即更新
|
// 主窗口缓存未初始化,立即更新
|
||||||
wss.updateMainWindowCache()
|
wss.updateMainWindowCacheLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
mainPos := wss.lastMainWindowPos
|
mainPos := wss.lastMainWindowPos
|
||||||
mainWidth := wss.lastMainWindowSize[0]
|
mainWidth := wss.lastMainWindowSize[0]
|
||||||
mainHeight := wss.lastMainWindowSize[1]
|
mainHeight := wss.lastMainWindowSize[1]
|
||||||
|
|
||||||
// 获取子窗口尺寸
|
// 使用缓存的子窗口尺寸,减少系统调用
|
||||||
windowWidth, windowHeight := window.Size()
|
windowWidth, windowHeight := wss.getWindowSizeCached(windowInfo.DocumentID, window)
|
||||||
|
|
||||||
// 自适应阈值计算
|
// 自适应阈值计算
|
||||||
threshold := float64(wss.calculateAdaptiveThreshold())
|
threshold := float64(wss.calculateAdaptiveThreshold())
|
||||||
@@ -423,14 +544,14 @@ func (wss *WindowSnapService) shouldSnapToMainWindow(window *application.Webview
|
|||||||
}
|
}
|
||||||
|
|
||||||
// calculateSnapPosition 计算吸附目标位置
|
// calculateSnapPosition 计算吸附目标位置
|
||||||
func (wss *WindowSnapService) calculateSnapPosition(snapEdge models.SnapEdge, currentPos models.WindowPosition, window *application.WebviewWindow) models.WindowPosition {
|
func (wss *WindowSnapService) calculateSnapPosition(snapEdge models.SnapEdge, currentPos models.WindowPosition, documentID int64, window *application.WebviewWindow) models.WindowPosition {
|
||||||
// 使用缓存的主窗口信息
|
// 使用缓存的主窗口信息
|
||||||
mainPos := wss.lastMainWindowPos
|
mainPos := wss.lastMainWindowPos
|
||||||
mainWidth := wss.lastMainWindowSize[0]
|
mainWidth := wss.lastMainWindowSize[0]
|
||||||
mainHeight := wss.lastMainWindowSize[1]
|
mainHeight := wss.lastMainWindowSize[1]
|
||||||
|
|
||||||
// 获取子窗口尺寸
|
// 使用缓存的子窗口尺寸,减少系统调用
|
||||||
windowWidth, windowHeight := window.Size()
|
windowWidth, windowHeight := wss.getWindowSizeCached(documentID, window)
|
||||||
|
|
||||||
switch snapEdge {
|
switch snapEdge {
|
||||||
case models.SnapEdgeRight:
|
case models.SnapEdgeRight:
|
||||||
@@ -483,9 +604,23 @@ func (wss *WindowSnapService) Cleanup() {
|
|||||||
wss.mu.Lock()
|
wss.mu.Lock()
|
||||||
defer wss.mu.Unlock()
|
defer wss.mu.Unlock()
|
||||||
|
|
||||||
|
// 清理主窗口事件监听
|
||||||
|
wss.cleanupMainWindowEvents()
|
||||||
|
|
||||||
|
// 清理所有子窗口事件监听
|
||||||
|
for documentID, unhook := range wss.windowMoveUnhooks {
|
||||||
|
if unhook != nil {
|
||||||
|
unhook()
|
||||||
|
}
|
||||||
|
delete(wss.windowMoveUnhooks, documentID)
|
||||||
|
}
|
||||||
|
|
||||||
// 清空管理的窗口
|
// 清空管理的窗口
|
||||||
wss.managedWindows = make(map[int64]*models.WindowInfo)
|
wss.managedWindows = make(map[int64]*models.WindowInfo)
|
||||||
wss.windowRefs = make(map[int64]*application.WebviewWindow)
|
wss.windowRefs = make(map[int64]*application.WebviewWindow)
|
||||||
|
wss.windowSizeCache = make(map[int64][2]int)
|
||||||
|
wss.isUpdatingPosition = make(map[int64]bool)
|
||||||
|
wss.windowMoveUnhooks = make(map[int64]func())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceShutdown 实现服务关闭接口
|
// ServiceShutdown 实现服务关闭接口
|
||||||
|
|||||||
831
internal/services/window_snap_service_test.go
Normal file
831
internal/services/window_snap_service_test.go
Normal file
@@ -0,0 +1,831 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
"voidraft/internal/models"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockWindow 模拟窗口对象
|
||||||
|
type MockWindow struct {
|
||||||
|
x, y int
|
||||||
|
w, h int
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *MockWindow) Position() (int, int) {
|
||||||
|
mw.mu.Lock()
|
||||||
|
defer mw.mu.Unlock()
|
||||||
|
return mw.x, mw.y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *MockWindow) Size() (int, int) {
|
||||||
|
mw.mu.Lock()
|
||||||
|
defer mw.mu.Unlock()
|
||||||
|
return mw.w, mw.h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *MockWindow) SetPosition(x, y int) {
|
||||||
|
mw.mu.Lock()
|
||||||
|
defer mw.mu.Unlock()
|
||||||
|
mw.x, mw.y = x, y
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建测试用的服务实例
|
||||||
|
func createTestService() *WindowSnapService {
|
||||||
|
logger := log.New()
|
||||||
|
|
||||||
|
service := &WindowSnapService{
|
||||||
|
logger: logger,
|
||||||
|
configService: nil, // 测试中不需要实际的配置服务
|
||||||
|
windowHelper: NewWindowHelper(),
|
||||||
|
snapEnabled: true,
|
||||||
|
baseThresholdRatio: 0.025,
|
||||||
|
minThreshold: 8,
|
||||||
|
maxThreshold: 40,
|
||||||
|
managedWindows: make(map[int64]*models.WindowInfo),
|
||||||
|
windowRefs: make(map[int64]*application.WebviewWindow),
|
||||||
|
windowSizeCache: make(map[int64][2]int),
|
||||||
|
isUpdatingPosition: make(map[int64]bool),
|
||||||
|
windowMoveUnhooks: make(map[int64]func()),
|
||||||
|
lastMainWindowPos: models.WindowPosition{X: 100, Y: 100},
|
||||||
|
lastMainWindowSize: [2]int{800, 600},
|
||||||
|
}
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDebounceThreshold 测试防抖阈值常量
|
||||||
|
func TestDebounceThreshold(t *testing.T) {
|
||||||
|
if debounceThreshold != 30*time.Millisecond {
|
||||||
|
t.Errorf("debounceThreshold = %v, want %v", debounceThreshold, 30*time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dragDetectionThreshold != 40*time.Millisecond {
|
||||||
|
t.Errorf("dragDetectionThreshold = %v, want %v", dragDetectionThreshold, 40*time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保拖拽阈值大于防抖阈值
|
||||||
|
if dragDetectionThreshold <= debounceThreshold {
|
||||||
|
t.Error("dragDetectionThreshold should be greater than debounceThreshold")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCalculateAdaptiveThreshold 测试自适应阈值计算
|
||||||
|
func TestCalculateAdaptiveThreshold(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mainWidth int
|
||||||
|
wantMin int
|
||||||
|
wantMax int
|
||||||
|
wantResult int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "小窗口",
|
||||||
|
mainWidth: 400,
|
||||||
|
wantMin: 8,
|
||||||
|
wantMax: 40,
|
||||||
|
wantResult: 10, // 400 * 0.025 = 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "中等窗口",
|
||||||
|
mainWidth: 1920,
|
||||||
|
wantMin: 8,
|
||||||
|
wantMax: 40,
|
||||||
|
wantResult: 40, // 1920 * 0.025 = 48, 但限制在 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "超小窗口",
|
||||||
|
mainWidth: 200,
|
||||||
|
wantMin: 8,
|
||||||
|
wantMax: 40,
|
||||||
|
wantResult: 8, // 200 * 0.025 = 5, 但最小是 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "零宽度",
|
||||||
|
mainWidth: 0,
|
||||||
|
wantMin: 8,
|
||||||
|
wantMax: 40,
|
||||||
|
wantResult: 8, // 应返回最小值
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
service.lastMainWindowSize = [2]int{tt.mainWidth, 600}
|
||||||
|
|
||||||
|
result := service.calculateAdaptiveThreshold()
|
||||||
|
|
||||||
|
if result != tt.wantResult {
|
||||||
|
t.Errorf("calculateAdaptiveThreshold() = %v, want %v", result, tt.wantResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result < tt.wantMin {
|
||||||
|
t.Errorf("result %v is less than minimum %v", result, tt.wantMin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result > tt.wantMax {
|
||||||
|
t.Errorf("result %v is greater than maximum %v", result, tt.wantMax)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWindowSizeCache 测试窗口尺寸缓存
|
||||||
|
func TestWindowSizeCache(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
|
||||||
|
// 初始化缓存
|
||||||
|
service.windowSizeCache[1] = [2]int{640, 480}
|
||||||
|
|
||||||
|
// 测试缓存读取
|
||||||
|
service.mu.Lock()
|
||||||
|
if size, exists := service.windowSizeCache[1]; !exists {
|
||||||
|
t.Error("Cache should exist for documentID 1")
|
||||||
|
} else if size[0] != 640 || size[1] != 480 {
|
||||||
|
t.Errorf("Cache size = [%d, %d], want [640, 480]", size[0], size[1])
|
||||||
|
}
|
||||||
|
service.mu.Unlock()
|
||||||
|
|
||||||
|
// 测试缓存不存在的情况
|
||||||
|
service.mu.Lock()
|
||||||
|
if _, exists := service.windowSizeCache[999]; exists {
|
||||||
|
t.Error("Cache should not exist for documentID 999")
|
||||||
|
}
|
||||||
|
service.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetSnapEnabled 测试启用/禁用吸附
|
||||||
|
func TestSetSnapEnabled(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
|
||||||
|
// 初始状态
|
||||||
|
if !service.snapEnabled {
|
||||||
|
t.Error("Initial snapEnabled should be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用吸附
|
||||||
|
service.SetSnapEnabled(false)
|
||||||
|
if service.snapEnabled {
|
||||||
|
t.Error("snapEnabled should be false after SetSnapEnabled(false)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用吸附
|
||||||
|
service.SetSnapEnabled(true)
|
||||||
|
if !service.snapEnabled {
|
||||||
|
t.Error("snapEnabled should be true after SetSnapEnabled(true)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnregisterWindow 测试窗口注销
|
||||||
|
func TestUnregisterWindow(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
|
||||||
|
// 模拟注册窗口
|
||||||
|
documentID := int64(1)
|
||||||
|
service.managedWindows[documentID] = &models.WindowInfo{DocumentID: documentID}
|
||||||
|
service.windowSizeCache[documentID] = [2]int{640, 480}
|
||||||
|
service.isUpdatingPosition[documentID] = false
|
||||||
|
|
||||||
|
// 模拟事件清理函数
|
||||||
|
cleanupCalled := false
|
||||||
|
service.windowMoveUnhooks[documentID] = func() {
|
||||||
|
cleanupCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证已注册
|
||||||
|
service.mu.RLock()
|
||||||
|
if _, exists := service.managedWindows[documentID]; !exists {
|
||||||
|
t.Error("Window should be registered")
|
||||||
|
}
|
||||||
|
service.mu.RUnlock()
|
||||||
|
|
||||||
|
// 注销窗口
|
||||||
|
service.UnregisterWindow(documentID)
|
||||||
|
|
||||||
|
// 验证已清理
|
||||||
|
service.mu.RLock()
|
||||||
|
if _, exists := service.managedWindows[documentID]; exists {
|
||||||
|
t.Error("managedWindows should be cleaned up")
|
||||||
|
}
|
||||||
|
if _, exists := service.windowSizeCache[documentID]; exists {
|
||||||
|
t.Error("windowSizeCache should be cleaned up")
|
||||||
|
}
|
||||||
|
if _, exists := service.isUpdatingPosition[documentID]; exists {
|
||||||
|
t.Error("isUpdatingPosition should be cleaned up")
|
||||||
|
}
|
||||||
|
if _, exists := service.windowMoveUnhooks[documentID]; exists {
|
||||||
|
t.Error("windowMoveUnhooks should be cleaned up")
|
||||||
|
}
|
||||||
|
service.mu.RUnlock()
|
||||||
|
|
||||||
|
// 验证清理函数被调用
|
||||||
|
if !cleanupCalled {
|
||||||
|
t.Error("Cleanup function should have been called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCleanup 测试资源清理
|
||||||
|
func TestCleanup(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
|
||||||
|
// 添加一些数据
|
||||||
|
service.managedWindows[1] = &models.WindowInfo{DocumentID: 1}
|
||||||
|
service.managedWindows[2] = &models.WindowInfo{DocumentID: 2}
|
||||||
|
service.windowSizeCache[1] = [2]int{640, 480}
|
||||||
|
service.windowSizeCache[2] = [2]int{800, 600}
|
||||||
|
service.isUpdatingPosition[1] = false
|
||||||
|
service.isUpdatingPosition[2] = true
|
||||||
|
|
||||||
|
// 添加事件清理函数
|
||||||
|
cleanup1Called := false
|
||||||
|
cleanup2Called := false
|
||||||
|
service.windowMoveUnhooks[1] = func() {
|
||||||
|
cleanup1Called = true
|
||||||
|
}
|
||||||
|
service.windowMoveUnhooks[2] = func() {
|
||||||
|
cleanup2Called = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行清理
|
||||||
|
service.Cleanup()
|
||||||
|
|
||||||
|
// 验证所有map都被重置
|
||||||
|
service.mu.RLock()
|
||||||
|
defer service.mu.RUnlock()
|
||||||
|
|
||||||
|
if len(service.managedWindows) != 0 {
|
||||||
|
t.Errorf("managedWindows length = %d, want 0", len(service.managedWindows))
|
||||||
|
}
|
||||||
|
if len(service.windowSizeCache) != 0 {
|
||||||
|
t.Errorf("windowSizeCache length = %d, want 0", len(service.windowSizeCache))
|
||||||
|
}
|
||||||
|
if len(service.isUpdatingPosition) != 0 {
|
||||||
|
t.Errorf("isUpdatingPosition length = %d, want 0", len(service.isUpdatingPosition))
|
||||||
|
}
|
||||||
|
if len(service.windowMoveUnhooks) != 0 {
|
||||||
|
t.Errorf("windowMoveUnhooks length = %d, want 0", len(service.windowMoveUnhooks))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证清理函数都被调用
|
||||||
|
if !cleanup1Called {
|
||||||
|
t.Error("Cleanup function for window 1 should have been called")
|
||||||
|
}
|
||||||
|
if !cleanup2Called {
|
||||||
|
t.Error("Cleanup function for window 2 should have been called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEventCleanupOnUnregister 测试事件清理功能
|
||||||
|
func TestEventCleanupOnUnregister(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
|
||||||
|
// 模拟多个窗口注册
|
||||||
|
documentIDs := []int64{1, 2, 3, 4, 5}
|
||||||
|
cleanupCounters := make(map[int64]int)
|
||||||
|
|
||||||
|
for _, id := range documentIDs {
|
||||||
|
service.managedWindows[id] = &models.WindowInfo{DocumentID: id}
|
||||||
|
service.windowSizeCache[id] = [2]int{640, 480}
|
||||||
|
|
||||||
|
// 为每个窗口添加清理函数
|
||||||
|
localID := id // 捕获循环变量
|
||||||
|
service.windowMoveUnhooks[id] = func() {
|
||||||
|
cleanupCounters[localID]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证初始状态
|
||||||
|
if len(service.windowMoveUnhooks) != len(documentIDs) {
|
||||||
|
t.Errorf("Expected %d cleanup hooks, got %d", len(documentIDs), len(service.windowMoveUnhooks))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逐个注销窗口
|
||||||
|
for _, id := range documentIDs[:3] { // 注销前3个
|
||||||
|
service.UnregisterWindow(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证部分清理
|
||||||
|
service.mu.RLock()
|
||||||
|
remainingHooks := len(service.windowMoveUnhooks)
|
||||||
|
service.mu.RUnlock()
|
||||||
|
|
||||||
|
if remainingHooks != 2 {
|
||||||
|
t.Errorf("Expected 2 remaining hooks, got %d", remainingHooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证清理函数被调用
|
||||||
|
for _, id := range documentIDs[:3] {
|
||||||
|
if cleanupCounters[id] != 1 {
|
||||||
|
t.Errorf("Cleanup for window %d should have been called once, got %d", id, cleanupCounters[id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证未注销的窗口清理函数未被调用
|
||||||
|
for _, id := range documentIDs[3:] {
|
||||||
|
if cleanupCounters[id] != 0 {
|
||||||
|
t.Errorf("Cleanup for window %d should not have been called, got %d", id, cleanupCounters[id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Event cleanup test passed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEventCleanupNilSafety 测试空清理函数的安全性
|
||||||
|
func TestEventCleanupNilSafety(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
|
||||||
|
documentID := int64(1)
|
||||||
|
service.managedWindows[documentID] = &models.WindowInfo{DocumentID: documentID}
|
||||||
|
// 故意不设置清理函数
|
||||||
|
|
||||||
|
// 注销窗口不应该panic
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("UnregisterWindow panicked with nil cleanup function: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
service.UnregisterWindow(documentID)
|
||||||
|
|
||||||
|
t.Log("Nil cleanup function handled safely")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 并发测试 ==========
|
||||||
|
|
||||||
|
// TestConcurrentRegisterUnregister 测试并发注册和注销窗口
|
||||||
|
func TestConcurrentRegisterUnregister(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
const goroutines = 100
|
||||||
|
const iterations = 50
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(goroutines * 2)
|
||||||
|
|
||||||
|
// 并发注册
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < iterations; j++ {
|
||||||
|
documentID := int64(id*iterations + j)
|
||||||
|
windowInfo := &models.WindowInfo{
|
||||||
|
DocumentID: documentID,
|
||||||
|
IsSnapped: false,
|
||||||
|
LastPos: models.WindowPosition{X: 0, Y: 0},
|
||||||
|
MoveTime: time.Now(),
|
||||||
|
}
|
||||||
|
service.mu.Lock()
|
||||||
|
service.managedWindows[documentID] = windowInfo
|
||||||
|
service.windowSizeCache[documentID] = [2]int{640, 480}
|
||||||
|
service.mu.Unlock()
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并发注销
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
time.Sleep(10 * time.Millisecond) // 让注册先执行一点
|
||||||
|
for j := 0; j < iterations; j++ {
|
||||||
|
documentID := int64(id*iterations + j)
|
||||||
|
service.UnregisterWindow(documentID)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// 验证数据一致性
|
||||||
|
service.mu.RLock()
|
||||||
|
defer service.mu.RUnlock()
|
||||||
|
|
||||||
|
if len(service.managedWindows) != len(service.windowSizeCache) {
|
||||||
|
t.Errorf("managedWindows length (%d) != windowSizeCache length (%d)",
|
||||||
|
len(service.managedWindows), len(service.windowSizeCache))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConcurrentSetSnapEnabled 测试并发启用/禁用吸附
|
||||||
|
func TestConcurrentSetSnapEnabled(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
const goroutines = 50
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(goroutines * 2)
|
||||||
|
|
||||||
|
// 并发启用
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < 100; j++ {
|
||||||
|
service.SetSnapEnabled(true)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并发禁用
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < 100; j++ {
|
||||||
|
service.SetSnapEnabled(false)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// 只要不panic就算成功
|
||||||
|
t.Log("Concurrent SetSnapEnabled completed without panic")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConcurrentCacheAccess 测试并发缓存访问
|
||||||
|
func TestConcurrentCacheAccess(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
const goroutines = 50
|
||||||
|
const operations = 1000
|
||||||
|
|
||||||
|
// 初始化一些缓存数据
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
service.windowSizeCache[int64(i)] = [2]int{640 + i*10, 480 + i*10}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(goroutines * 3)
|
||||||
|
|
||||||
|
// 并发读取
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < operations; j++ {
|
||||||
|
service.mu.RLock()
|
||||||
|
_ = service.windowSizeCache[int64(j%10)]
|
||||||
|
service.mu.RUnlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并发写入
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < operations; j++ {
|
||||||
|
documentID := int64(id*operations + j)
|
||||||
|
service.mu.Lock()
|
||||||
|
service.windowSizeCache[documentID] = [2]int{640, 480}
|
||||||
|
service.mu.Unlock()
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并发删除
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < operations; j++ {
|
||||||
|
documentID := int64(id*operations + j)
|
||||||
|
service.mu.Lock()
|
||||||
|
delete(service.windowSizeCache, documentID)
|
||||||
|
service.mu.Unlock()
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
t.Log("Concurrent cache access completed without race conditions")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConcurrentUpdateMainWindowCache 测试并发更新主窗口缓存
|
||||||
|
func TestConcurrentUpdateMainWindowCache(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
const goroutines = 50
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(goroutines)
|
||||||
|
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < 100; j++ {
|
||||||
|
service.mu.Lock()
|
||||||
|
service.lastMainWindowPos = models.WindowPosition{
|
||||||
|
X: id*100 + j,
|
||||||
|
Y: id*100 + j,
|
||||||
|
}
|
||||||
|
service.lastMainWindowSize = [2]int{800 + id, 600 + id}
|
||||||
|
service.mu.Unlock()
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// 验证数据一致性
|
||||||
|
service.mu.RLock()
|
||||||
|
defer service.mu.RUnlock()
|
||||||
|
|
||||||
|
if service.lastMainWindowSize[0] < 800 || service.lastMainWindowSize[0] > 800+goroutines {
|
||||||
|
t.Errorf("Unexpected main window width: %d", service.lastMainWindowSize[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 性能测试 ==========
|
||||||
|
|
||||||
|
// BenchmarkCalculateAdaptiveThreshold 基准测试:阈值计算
|
||||||
|
func BenchmarkCalculateAdaptiveThreshold(b *testing.B) {
|
||||||
|
service := createTestService()
|
||||||
|
service.lastMainWindowSize = [2]int{1920, 1080}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = service.calculateAdaptiveThreshold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkWindowSizeCacheHit 基准测试:缓存命中
|
||||||
|
func BenchmarkWindowSizeCacheHit(b *testing.B) {
|
||||||
|
service := createTestService()
|
||||||
|
documentID := int64(1)
|
||||||
|
service.windowSizeCache[documentID] = [2]int{640, 480}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
service.mu.RLock()
|
||||||
|
_, _ = service.windowSizeCache[documentID][0], service.windowSizeCache[documentID][1]
|
||||||
|
service.mu.RUnlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkRegisterUnregister 基准测试:注册注销窗口
|
||||||
|
func BenchmarkRegisterUnregister(b *testing.B) {
|
||||||
|
service := createTestService()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
documentID := int64(i)
|
||||||
|
windowInfo := &models.WindowInfo{
|
||||||
|
DocumentID: documentID,
|
||||||
|
IsSnapped: false,
|
||||||
|
LastPos: models.WindowPosition{X: 0, Y: 0},
|
||||||
|
MoveTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
service.mu.Lock()
|
||||||
|
service.managedWindows[documentID] = windowInfo
|
||||||
|
service.windowSizeCache[documentID] = [2]int{640, 480}
|
||||||
|
service.mu.Unlock()
|
||||||
|
|
||||||
|
service.UnregisterWindow(documentID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkConcurrentCacheRead 基准测试:并发缓存读取
|
||||||
|
func BenchmarkConcurrentCacheRead(b *testing.B) {
|
||||||
|
service := createTestService()
|
||||||
|
|
||||||
|
// 初始化缓存
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
service.windowSizeCache[int64(i)] = [2]int{640, 480}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
i := 0
|
||||||
|
for pb.Next() {
|
||||||
|
documentID := int64(i % 100)
|
||||||
|
service.mu.RLock()
|
||||||
|
_, _ = service.windowSizeCache[documentID][0], service.windowSizeCache[documentID][1]
|
||||||
|
service.mu.RUnlock()
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSetSnapEnabled 基准测试:启用禁用吸附
|
||||||
|
func BenchmarkSetSnapEnabled(b *testing.B) {
|
||||||
|
service := createTestService()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
service.SetSnapEnabled(i%2 == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 压力测试 ==========
|
||||||
|
|
||||||
|
// TestHighFrequencyUpdates 测试高频率更新
|
||||||
|
func TestHighFrequencyUpdates(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
const updates = 10000
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
for i := 0; i < updates; i++ {
|
||||||
|
service.mu.Lock()
|
||||||
|
service.lastMainWindowPos = models.WindowPosition{X: i, Y: i}
|
||||||
|
service.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
avgTime := elapsed / updates
|
||||||
|
|
||||||
|
t.Logf("High frequency updates: %d updates in %v", updates, elapsed)
|
||||||
|
t.Logf("Average time per update: %v", avgTime)
|
||||||
|
|
||||||
|
if avgTime > 100*time.Microsecond {
|
||||||
|
t.Logf("Warning: Average update time (%v) is higher than expected", avgTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMemoryUsage 测试内存使用
|
||||||
|
func TestMemoryUsage(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
const windows = 1000
|
||||||
|
|
||||||
|
// 添加大量窗口
|
||||||
|
for i := 0; i < windows; i++ {
|
||||||
|
documentID := int64(i)
|
||||||
|
service.mu.Lock()
|
||||||
|
service.managedWindows[documentID] = &models.WindowInfo{
|
||||||
|
DocumentID: documentID,
|
||||||
|
IsSnapped: false,
|
||||||
|
LastPos: models.WindowPosition{X: i, Y: i},
|
||||||
|
MoveTime: time.Now(),
|
||||||
|
}
|
||||||
|
service.windowSizeCache[documentID] = [2]int{640, 480}
|
||||||
|
service.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证数据
|
||||||
|
service.mu.RLock()
|
||||||
|
managedCount := len(service.managedWindows)
|
||||||
|
cacheCount := len(service.windowSizeCache)
|
||||||
|
service.mu.RUnlock()
|
||||||
|
|
||||||
|
if managedCount != windows {
|
||||||
|
t.Errorf("managedWindows count = %d, want %d", managedCount, windows)
|
||||||
|
}
|
||||||
|
if cacheCount != windows {
|
||||||
|
t.Errorf("windowSizeCache count = %d, want %d", cacheCount, windows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
service.Cleanup()
|
||||||
|
|
||||||
|
service.mu.RLock()
|
||||||
|
afterCleanup := len(service.managedWindows) + len(service.windowSizeCache)
|
||||||
|
service.mu.RUnlock()
|
||||||
|
|
||||||
|
if afterCleanup != 0 {
|
||||||
|
t.Errorf("After cleanup, total items = %d, want 0", afterCleanup)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Memory test: Successfully managed %d windows", windows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWindowResizeCache 测试窗口尺寸变化时缓存更新
|
||||||
|
func TestWindowResizeCache(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
|
||||||
|
// 初始化缓存
|
||||||
|
documentID := int64(1)
|
||||||
|
service.windowSizeCache[documentID] = [2]int{640, 480}
|
||||||
|
|
||||||
|
// 验证初始缓存
|
||||||
|
service.mu.RLock()
|
||||||
|
size := service.windowSizeCache[documentID]
|
||||||
|
service.mu.RUnlock()
|
||||||
|
|
||||||
|
if size[0] != 640 || size[1] != 480 {
|
||||||
|
t.Errorf("Initial cache = [%d, %d], want [640, 480]", size[0], size[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟窗口尺寸变化
|
||||||
|
service.mu.Lock()
|
||||||
|
service.windowSizeCache[documentID] = [2]int{800, 600}
|
||||||
|
service.mu.Unlock()
|
||||||
|
|
||||||
|
// 验证缓存更新
|
||||||
|
service.mu.RLock()
|
||||||
|
newSize := service.windowSizeCache[documentID]
|
||||||
|
service.mu.RUnlock()
|
||||||
|
|
||||||
|
if newSize[0] != 800 || newSize[1] != 600 {
|
||||||
|
t.Errorf("Updated cache = [%d, %d], want [800, 600]", newSize[0], newSize[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Window resize cache test passed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMainWindowResizeEffect 测试主窗口尺寸变化对吸附窗口的影响
|
||||||
|
func TestMainWindowResizeEffect(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
|
||||||
|
// 设置初始主窗口尺寸
|
||||||
|
service.lastMainWindowPos = models.WindowPosition{X: 100, Y: 100}
|
||||||
|
service.lastMainWindowSize = [2]int{800, 600}
|
||||||
|
|
||||||
|
// 添加一个已吸附的窗口
|
||||||
|
documentID := int64(1)
|
||||||
|
service.managedWindows[documentID] = &models.WindowInfo{
|
||||||
|
DocumentID: documentID,
|
||||||
|
IsSnapped: true,
|
||||||
|
SnapEdge: models.SnapEdgeRight,
|
||||||
|
SnapOffset: models.SnapPosition{X: 800, Y: 0}, // 吸附在右侧
|
||||||
|
LastPos: models.WindowPosition{X: 900, Y: 100},
|
||||||
|
MoveTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟主窗口尺寸变化
|
||||||
|
service.mu.Lock()
|
||||||
|
service.lastMainWindowSize = [2]int{1000, 600} // 宽度从800变为1000
|
||||||
|
service.mu.Unlock()
|
||||||
|
|
||||||
|
// 验证吸附偏移量应该基于新的尺寸重新计算
|
||||||
|
// 注意:这里只是验证数据结构,实际的位置更新由事件触发
|
||||||
|
|
||||||
|
service.mu.RLock()
|
||||||
|
windowInfo := service.managedWindows[documentID]
|
||||||
|
service.mu.RUnlock()
|
||||||
|
|
||||||
|
if !windowInfo.IsSnapped {
|
||||||
|
t.Error("Window should still be snapped after main window resize")
|
||||||
|
}
|
||||||
|
|
||||||
|
if windowInfo.SnapEdge != models.SnapEdgeRight {
|
||||||
|
t.Errorf("SnapEdge = %v, want %v", windowInfo.SnapEdge, models.SnapEdgeRight)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Main window resize effect test passed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConcurrentResizeAndMove 测试并发的尺寸变化和移动操作
|
||||||
|
func TestConcurrentResizeAndMove(t *testing.T) {
|
||||||
|
service := createTestService()
|
||||||
|
const goroutines = 20
|
||||||
|
const operations = 100
|
||||||
|
|
||||||
|
// 初始化一些窗口
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
documentID := int64(i)
|
||||||
|
service.managedWindows[documentID] = &models.WindowInfo{
|
||||||
|
DocumentID: documentID,
|
||||||
|
IsSnapped: false,
|
||||||
|
LastPos: models.WindowPosition{X: i * 100, Y: i * 100},
|
||||||
|
MoveTime: time.Now(),
|
||||||
|
}
|
||||||
|
service.windowSizeCache[documentID] = [2]int{640, 480}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(goroutines * 3)
|
||||||
|
|
||||||
|
// 并发更新窗口尺寸缓存
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < operations; j++ {
|
||||||
|
documentID := int64(id % 10)
|
||||||
|
service.mu.Lock()
|
||||||
|
service.windowSizeCache[documentID] = [2]int{640 + j, 480 + j}
|
||||||
|
service.mu.Unlock()
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并发更新主窗口尺寸
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < operations; j++ {
|
||||||
|
service.mu.Lock()
|
||||||
|
service.lastMainWindowSize = [2]int{800 + j, 600 + j}
|
||||||
|
service.mu.Unlock()
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并发读取缓存
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < operations; j++ {
|
||||||
|
documentID := int64(j % 10)
|
||||||
|
service.mu.RLock()
|
||||||
|
_ = service.windowSizeCache[documentID]
|
||||||
|
service.mu.RUnlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
t.Log("Concurrent resize and move test completed without race conditions")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user