mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-07 04:12:48 +08:00
Merge 803c33b306 into 0009c98c7e
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { Layout, Button, ConfigProvider, theme, message, Modal, Spin, Slider, Progress, Switch, Input, InputNumber, Select, Segmented, Tooltip } from 'antd';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import { PlusOutlined, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined, BgColorsOutlined, AppstoreOutlined, RobotOutlined } from '@ant-design/icons';
|
||||
import { BrowserOpenURL, Environment, EventsOn, Quit, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowMaximise, WindowMinimise, WindowSetPosition, WindowSetSize, WindowToggleMaximise, WindowUnfullscreen } from '../wailsjs/runtime';
|
||||
import { BrowserOpenURL, Environment, EventsOn, Quit, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowIsMinimised, WindowIsNormal, WindowMaximise, WindowMinimise, WindowSetPosition, WindowSetSize, WindowToggleMaximise, WindowUnfullscreen } from '../wailsjs/runtime';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import TabManager from './components/TabManager';
|
||||
import ConnectionModal from './components/ConnectionModal';
|
||||
@@ -130,6 +130,9 @@ function App() {
|
||||
const toggleAIPanel = useStore(state => state.toggleAIPanel);
|
||||
const setAIPanelVisible = useStore(state => state.setAIPanelVisible);
|
||||
const globalProxyInvalidHintShownRef = React.useRef(false);
|
||||
const windowDiagSequenceRef = React.useRef(0);
|
||||
const windowDiagLastSignatureRef = React.useRef('');
|
||||
const windowDiagLastAtRef = React.useRef(0);
|
||||
const connectionWorkbenchState = getConnectionWorkbenchState(isStoreHydrated, hasLoadedSecureConfig);
|
||||
|
||||
const windowCornerRadius = 14;
|
||||
@@ -481,6 +484,10 @@ function App() {
|
||||
const store = useStore.getState();
|
||||
const newState = isFs ? 'fullscreen' : (isMax ? 'maximized' : 'normal');
|
||||
if (store.windowState !== newState) {
|
||||
void emitWindowDiagnostic('transition:windowState', {
|
||||
from: store.windowState,
|
||||
to: newState,
|
||||
});
|
||||
store.setWindowState(newState);
|
||||
}
|
||||
|
||||
@@ -496,15 +503,18 @@ function App() {
|
||||
const h = Math.trunc(Number(size.h || 0));
|
||||
const x = Math.trunc(Number(pos.x || 0));
|
||||
const y = Math.trunc(Number(pos.y || 0));
|
||||
if (w < 400 || h < 300) return;
|
||||
if (w < 400 || h < 300) return;
|
||||
|
||||
const key = `${w},${h},${x},${y}`;
|
||||
if (key === lastSaved) return;
|
||||
lastSaved = key;
|
||||
store.setWindowBounds({ width: w, height: h, x, y });
|
||||
} catch (e) {
|
||||
// 静默忽略
|
||||
}
|
||||
const key = `${w},${h},${x},${y}`;
|
||||
if (key === lastSaved) return;
|
||||
lastSaved = key;
|
||||
if (Math.abs(x) > 5000 || Math.abs(y) > 5000) {
|
||||
void emitWindowDiagnostic('anomaly:windowBounds', { width: w, height: h, x, y });
|
||||
}
|
||||
store.setWindowBounds({ width: w, height: h, x, y });
|
||||
} catch (e) {
|
||||
// 静默忽略
|
||||
}
|
||||
};
|
||||
|
||||
const timer = window.setInterval(saveWindowState, SAVE_INTERVAL_MS);
|
||||
@@ -854,6 +864,63 @@ function App() {
|
||||
|| (runtimePlatform === '' && isWindowsPlatform());
|
||||
const useNativeMacWindowControls = isMacRuntime && appearance.useNativeMacWindowControls === true;
|
||||
|
||||
const emitWindowDiagnostic = useCallback(async (stage: string, extra: Record<string, unknown> = {}) => {
|
||||
if (!isMacRuntime) {
|
||||
return;
|
||||
}
|
||||
const backendApp = (window as any).go?.app?.App;
|
||||
if (typeof backendApp?.LogWindowDiagnostic !== 'function') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const [isFullscreen, isMaximised, isMinimised, isNormal, size, position] = await Promise.all([
|
||||
WindowIsFullscreen().catch(() => false),
|
||||
WindowIsMaximised().catch(() => false),
|
||||
WindowIsMinimised().catch(() => false),
|
||||
WindowIsNormal().catch(() => false),
|
||||
WindowGetSize().catch(() => null),
|
||||
WindowGetPosition().catch(() => null),
|
||||
]);
|
||||
const payload = {
|
||||
seq: ++windowDiagSequenceRef.current,
|
||||
ts: new Date().toISOString(),
|
||||
stage,
|
||||
nativeControls: useNativeMacWindowControls,
|
||||
documentVisible: document.visibilityState,
|
||||
documentHasFocus: document.hasFocus(),
|
||||
devicePixelRatio: Number(window.devicePixelRatio) || 1,
|
||||
windowState: {
|
||||
isFullscreen,
|
||||
isMaximised,
|
||||
isMinimised,
|
||||
isNormal,
|
||||
},
|
||||
size: size ? { w: Math.trunc(Number(size.w || 0)), h: Math.trunc(Number(size.h || 0)) } : null,
|
||||
position: position ? { x: Math.trunc(Number(position.x || 0)), y: Math.trunc(Number(position.y || 0)) } : null,
|
||||
extra,
|
||||
};
|
||||
const signature = JSON.stringify({
|
||||
stage,
|
||||
nativeControls: payload.nativeControls,
|
||||
visible: payload.documentVisible,
|
||||
focus: payload.documentHasFocus,
|
||||
state: payload.windowState,
|
||||
size: payload.size,
|
||||
position: payload.position,
|
||||
extra,
|
||||
});
|
||||
const now = Date.now();
|
||||
if (signature === windowDiagLastSignatureRef.current && now-windowDiagLastAtRef.current < 250) {
|
||||
return;
|
||||
}
|
||||
windowDiagLastSignatureRef.current = signature;
|
||||
windowDiagLastAtRef.current = now;
|
||||
await backendApp.LogWindowDiagnostic(stage, JSON.stringify(payload));
|
||||
} catch (error) {
|
||||
console.warn('Failed to emit window diagnostic', error);
|
||||
}
|
||||
}, [isMacRuntime, useNativeMacWindowControls]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isStoreHydrated || !isMacRuntime) {
|
||||
return;
|
||||
@@ -866,6 +933,104 @@ function App() {
|
||||
}
|
||||
}, [isMacRuntime, isStoreHydrated, useNativeMacWindowControls]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMacRuntime) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
let pollTimer: number | null = null;
|
||||
let burstTimer: number | null = null;
|
||||
|
||||
const stopBurst = () => {
|
||||
if (pollTimer !== null) {
|
||||
window.clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
if (burstTimer !== null) {
|
||||
window.clearTimeout(burstTimer);
|
||||
burstTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startBurst = (reason: string, extra: Record<string, unknown> = {}) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
void emitWindowDiagnostic(`burst:start:${reason}`, extra);
|
||||
if (pollTimer === null) {
|
||||
pollTimer = window.setInterval(() => {
|
||||
void emitWindowDiagnostic(`burst:tick:${reason}`);
|
||||
}, 250);
|
||||
}
|
||||
if (burstTimer !== null) {
|
||||
window.clearTimeout(burstTimer);
|
||||
}
|
||||
burstTimer = window.setTimeout(() => {
|
||||
stopBurst();
|
||||
void emitWindowDiagnostic(`burst:stop:${reason}`);
|
||||
}, 6000);
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
void emitWindowDiagnostic('event:focus');
|
||||
};
|
||||
const handleBlur = () => {
|
||||
void emitWindowDiagnostic('event:blur');
|
||||
};
|
||||
const handleResize = () => {
|
||||
void emitWindowDiagnostic('event:resize');
|
||||
};
|
||||
const handleVisibilityChange = () => {
|
||||
void emitWindowDiagnostic('event:visibilitychange', { visibility: document.visibilityState });
|
||||
};
|
||||
const handleEditableKeydown = (event: KeyboardEvent) => {
|
||||
if (!isEditableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
const key = String(event.key || '');
|
||||
const maybeFullscreenKey = key === 'Escape' || key.toLowerCase() === 'f' || key === 'Process';
|
||||
const hasModifier = event.ctrlKey || event.metaKey || event.altKey;
|
||||
startBurst('editable-keydown', {
|
||||
key,
|
||||
code: String(event.code || ''),
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
altKey: event.altKey,
|
||||
shiftKey: event.shiftKey,
|
||||
maybeFullscreenKey,
|
||||
hasModifier,
|
||||
});
|
||||
};
|
||||
const handleCompositionStart = () => {
|
||||
startBurst('compositionstart');
|
||||
};
|
||||
const handleCompositionEnd = () => {
|
||||
startBurst('compositionend');
|
||||
};
|
||||
|
||||
void emitWindowDiagnostic('session:start');
|
||||
window.addEventListener('focus', handleFocus);
|
||||
window.addEventListener('blur', handleBlur);
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('keydown', handleEditableKeydown, true);
|
||||
window.addEventListener('compositionstart', handleCompositionStart, true);
|
||||
window.addEventListener('compositionend', handleCompositionEnd, true);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
stopBurst();
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
window.removeEventListener('blur', handleBlur);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('keydown', handleEditableKeydown, true);
|
||||
window.removeEventListener('compositionstart', handleCompositionStart, true);
|
||||
window.removeEventListener('compositionend', handleCompositionEnd, true);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [emitWindowDiagnostic, isMacRuntime]);
|
||||
|
||||
const formatBytes = (bytes?: number) => {
|
||||
if (!bytes || bytes <= 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
@@ -1373,15 +1538,19 @@ function App() {
|
||||
|
||||
const handleTitleBarWindowToggle = async () => {
|
||||
try {
|
||||
void emitWindowDiagnostic('action:titlebar-toggle:before');
|
||||
if (await WindowIsFullscreen()) {
|
||||
await WindowUnfullscreen();
|
||||
void emitWindowDiagnostic('action:titlebar-toggle:after-unfullscreen');
|
||||
return;
|
||||
}
|
||||
if (useNativeMacWindowControls && isMacRuntime) {
|
||||
await WindowFullscreen();
|
||||
void emitWindowDiagnostic('action:titlebar-toggle:after-fullscreen');
|
||||
return;
|
||||
}
|
||||
await WindowToggleMaximise();
|
||||
void emitWindowDiagnostic('action:titlebar-toggle:after-toggle-maximise');
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
2
frontend/wailsjs/go/app/App.d.ts
vendored
2
frontend/wailsjs/go/app/App.d.ts
vendored
@@ -110,6 +110,8 @@ export function InstallLocalDriverPackage(arg1:string,arg2:string,arg3:string,ar
|
||||
|
||||
export function InstallUpdateAndRestart():Promise<connection.QueryResult>;
|
||||
|
||||
export function LogWindowDiagnostic(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function MongoDiscoverMembers(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
|
||||
|
||||
export function MySQLConnect(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
|
||||
|
||||
@@ -214,6 +214,10 @@ export function InstallUpdateAndRestart() {
|
||||
return window['go']['app']['App']['InstallUpdateAndRestart']();
|
||||
}
|
||||
|
||||
export function LogWindowDiagnostic(arg1, arg2) {
|
||||
return window['go']['app']['App']['LogWindowDiagnostic'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function MongoDiscoverMembers(arg1) {
|
||||
return window['go']['app']['App']['MongoDiscoverMembers'](arg1);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ func (a *App) startup(ctx context.Context) {
|
||||
}
|
||||
logger.Init()
|
||||
a.loadPersistedGlobalProxy()
|
||||
installMacNativeWindowDiagnostics(logger.Path())
|
||||
applyMacWindowTranslucencyFix()
|
||||
logger.Infof("应用启动完成(首次连接保护窗口=%s,最多重试=%d 次)", startupConnectRetryWindow, startupConnectRetryAttempts)
|
||||
}
|
||||
@@ -108,6 +109,16 @@ func (a *App) SetMacNativeWindowControls(enabled bool) {
|
||||
setMacNativeWindowControls(enabled)
|
||||
}
|
||||
|
||||
// LogWindowDiagnostic 记录前端采集到的窗口诊断信息,便于排查 macOS 原生全屏异常。
|
||||
func (a *App) LogWindowDiagnostic(stage string, payload string) {
|
||||
stage = strings.TrimSpace(stage)
|
||||
payload = strings.TrimSpace(payload)
|
||||
if stage == "" {
|
||||
stage = "unknown"
|
||||
}
|
||||
logger.Warnf("窗口诊断:stage=%s payload=%s", stage, payload)
|
||||
}
|
||||
|
||||
// Shutdown is called when the app terminates
|
||||
func (a *App) Shutdown(ctx context.Context) {
|
||||
logger.Infof("应用开始关闭,准备释放资源")
|
||||
|
||||
@@ -5,12 +5,143 @@ package app
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c -fblocks
|
||||
#cgo LDFLAGS: -framework Cocoa
|
||||
#include <stdlib.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <dispatch/dispatch.h>
|
||||
|
||||
static inline BOOL gonaviBoolYES() { return YES; }
|
||||
static inline BOOL gonaviBoolNO() { return NO; }
|
||||
|
||||
static char *gonaviNativeLogPath = NULL;
|
||||
static BOOL gonaviNativeObserverInstalled = NO;
|
||||
|
||||
static void gonaviWriteNativeWindowLogLine(NSString *line) {
|
||||
if (line == nil || gonaviNativeLogPath == NULL) {
|
||||
return;
|
||||
}
|
||||
NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:[NSString stringWithUTF8String:gonaviNativeLogPath]];
|
||||
if (handle == nil) {
|
||||
return;
|
||||
}
|
||||
@try {
|
||||
[handle seekToEndOfFile];
|
||||
NSData *data = [line dataUsingEncoding:NSUTF8StringEncoding];
|
||||
[handle writeData:data];
|
||||
} @catch (__unused NSException *exception) {
|
||||
} @finally {
|
||||
[handle closeFile];
|
||||
}
|
||||
}
|
||||
|
||||
static NSString *gonaviWindowDiagnosticLine(NSString *eventName, NSWindow *window) {
|
||||
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
||||
[formatter setDateFormat:@"yyyy/MM/dd HH:mm:ss.SSSSSS"];
|
||||
NSString *timestamp = [formatter stringFromDate:[NSDate date]];
|
||||
[formatter release];
|
||||
if (window == nil) {
|
||||
return [NSString stringWithFormat:@"%@ [WARN] 原生窗口诊断:event=%@ window=nil\n", timestamp, eventName ?: @"unknown"];
|
||||
}
|
||||
NSUInteger occlusionState = 0;
|
||||
NSInteger windowNumber = [window windowNumber];
|
||||
NSInteger level = [window level];
|
||||
NSUInteger collectionBehavior = [window collectionBehavior];
|
||||
NSString *className = NSStringFromClass([window class]);
|
||||
NSString *delegateClassName = [window delegate] ? NSStringFromClass([[window delegate] class]) : @"nil";
|
||||
if (@available(macOS 10.9, *)) {
|
||||
occlusionState = [window occlusionState];
|
||||
}
|
||||
return [NSString stringWithFormat:@"%@ [WARN] 原生窗口诊断:event=%@ ptr=%p number=%ld class=%@ delegate=%@ visible=%@ miniaturized=%@ key=%@ main=%@ canHide=%@ level=%ld occlusion=%lu styleMask=%lu collectionBehavior=%lu frame=%@ screen=%@ title=%@\n",
|
||||
timestamp,
|
||||
eventName ?: @"unknown",
|
||||
window,
|
||||
(long)windowNumber,
|
||||
className ?: @"nil",
|
||||
delegateClassName,
|
||||
[window isVisible] ? @"true" : @"false",
|
||||
[window isMiniaturized] ? @"true" : @"false",
|
||||
[window isKeyWindow] ? @"true" : @"false",
|
||||
[window isMainWindow] ? @"true" : @"false",
|
||||
[window canHide] ? @"true" : @"false",
|
||||
(long)level,
|
||||
(unsigned long)occlusionState,
|
||||
(unsigned long)[window styleMask],
|
||||
(unsigned long)collectionBehavior,
|
||||
NSStringFromRect([window frame]),
|
||||
[window screen] ? NSStringFromRect([[window screen] frame]) : @"nil",
|
||||
[window title] ?: @""];
|
||||
}
|
||||
|
||||
@interface GoNaviNativeWindowObserver : NSObject
|
||||
@end
|
||||
|
||||
@implementation GoNaviNativeWindowObserver
|
||||
|
||||
- (void)logNotification:(NSNotification *)notification {
|
||||
NSString *name = [notification name] ?: @"unknown";
|
||||
NSWindow *window = nil;
|
||||
if ([[notification object] isKindOfClass:[NSWindow class]]) {
|
||||
window = (NSWindow *)[notification object];
|
||||
} else {
|
||||
window = [NSApp keyWindow] ?: [NSApp mainWindow];
|
||||
}
|
||||
gonaviWriteNativeWindowLogLine(gonaviWindowDiagnosticLine(name, window));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static GoNaviNativeWindowObserver *gonaviNativeWindowObserver = nil;
|
||||
|
||||
static void gonaviInstallNativeWindowObserver(void) {
|
||||
if (gonaviNativeObserverInstalled) {
|
||||
return;
|
||||
}
|
||||
gonaviNativeObserverInstalled = YES;
|
||||
gonaviNativeWindowObserver = [[GoNaviNativeWindowObserver alloc] init];
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
NSArray<NSString *> *windowNotifications = @[
|
||||
NSWindowDidBecomeKeyNotification,
|
||||
NSWindowDidResignKeyNotification,
|
||||
NSWindowDidBecomeMainNotification,
|
||||
NSWindowDidResignMainNotification,
|
||||
NSWindowDidMiniaturizeNotification,
|
||||
NSWindowDidDeminiaturizeNotification,
|
||||
NSWindowDidEnterFullScreenNotification,
|
||||
NSWindowDidExitFullScreenNotification,
|
||||
NSWindowDidMoveNotification,
|
||||
NSWindowDidResizeNotification,
|
||||
NSWindowDidChangeOcclusionStateNotification,
|
||||
];
|
||||
for (NSString *notificationName in windowNotifications) {
|
||||
[center addObserver:gonaviNativeWindowObserver selector:@selector(logNotification:) name:notificationName object:nil];
|
||||
}
|
||||
NSArray<NSString *> *appNotifications = @[
|
||||
NSApplicationDidHideNotification,
|
||||
NSApplicationDidUnhideNotification,
|
||||
NSApplicationDidBecomeActiveNotification,
|
||||
NSApplicationDidResignActiveNotification,
|
||||
];
|
||||
for (NSString *notificationName in appNotifications) {
|
||||
[center addObserver:gonaviNativeWindowObserver selector:@selector(logNotification:) name:notificationName object:nil];
|
||||
}
|
||||
for (NSWindow *window in [NSApp windows]) {
|
||||
gonaviWriteNativeWindowLogLine(gonaviWindowDiagnosticLine(@"observer:snapshot", window));
|
||||
}
|
||||
}
|
||||
|
||||
static void gonaviConfigureNativeWindowDiagnostics(const char *logPath) {
|
||||
if (logPath == NULL || logPath[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
if (gonaviNativeLogPath != NULL) {
|
||||
free(gonaviNativeLogPath);
|
||||
gonaviNativeLogPath = NULL;
|
||||
}
|
||||
gonaviNativeLogPath = strdup(logPath);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
gonaviInstallNativeWindowObserver();
|
||||
});
|
||||
}
|
||||
|
||||
static void gonaviSetWindowButtonsVisible(NSWindow *window, BOOL visible) {
|
||||
if (window == nil) {
|
||||
return;
|
||||
@@ -24,12 +155,31 @@ static void gonaviSetWindowButtonsVisible(NSWindow *window, BOOL visible) {
|
||||
}
|
||||
}
|
||||
|
||||
static BOOL gonaviShouldApplyMacWindowStyle(NSWindow *window) {
|
||||
if (window == nil) {
|
||||
return NO;
|
||||
}
|
||||
NSString *className = NSStringFromClass([window class]) ?: @"";
|
||||
NSString *delegateClassName = [window delegate] ? NSStringFromClass([[window delegate] class]) : @"";
|
||||
NSString *title = [window title] ?: @"";
|
||||
|
||||
// 仅对主 WailsWindow 套用原生标题栏/全屏样式,避免误伤输入法候选窗、全屏过渡窗。
|
||||
if ([className isEqualToString:@"WailsWindow"] || [delegateClassName isEqualToString:@"WindowDelegate"]) {
|
||||
return YES;
|
||||
}
|
||||
return [title isEqualToString:@"GoNavi"];
|
||||
}
|
||||
|
||||
static void gonaviApplyMacWindowStyle(BOOL enabled) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
for (NSWindow *window in [NSApp windows]) {
|
||||
if (window == nil) {
|
||||
continue;
|
||||
}
|
||||
if (!gonaviShouldApplyMacWindowStyle(window)) {
|
||||
gonaviWriteNativeWindowLogLine(gonaviWindowDiagnosticLine(@"style:skip-non-app-window", window));
|
||||
continue;
|
||||
}
|
||||
|
||||
NSUInteger styleMask = [window styleMask];
|
||||
styleMask |= NSWindowStyleMaskClosable;
|
||||
@@ -57,12 +207,24 @@ static void gonaviApplyMacWindowStyle(BOOL enabled) {
|
||||
|
||||
[[window contentView] setNeedsDisplay:YES];
|
||||
[window invalidateShadow];
|
||||
gonaviWriteNativeWindowLogLine(gonaviWindowDiagnosticLine(enabled ? @"style:enable-native-controls" : @"style:disable-native-controls", window));
|
||||
}
|
||||
});
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import "unsafe"
|
||||
|
||||
func installMacNativeWindowDiagnostics(logPath string) {
|
||||
if logPath == "" {
|
||||
return
|
||||
}
|
||||
cLogPath := C.CString(logPath)
|
||||
defer C.free(unsafe.Pointer(cLogPath))
|
||||
C.gonaviConfigureNativeWindowDiagnostics(cLogPath)
|
||||
}
|
||||
|
||||
func setMacNativeWindowControls(enabled bool) {
|
||||
state := resolveMacNativeWindowControlState(enabled)
|
||||
if state.ShowNativeButtons {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package app
|
||||
|
||||
import "strings"
|
||||
|
||||
type macNativeWindowControlState struct {
|
||||
ShowNativeButtons bool
|
||||
UseTitledWindow bool
|
||||
@@ -30,3 +32,24 @@ func resolveMacNativeWindowControlState(enabled bool) macNativeWindowControlStat
|
||||
AllowNativeFullscreen: false,
|
||||
}
|
||||
}
|
||||
|
||||
type macWindowIdentity struct {
|
||||
ClassName string
|
||||
DelegateClassName string
|
||||
Title string
|
||||
}
|
||||
|
||||
// shouldApplyMacNativeWindowStyle 只允许对主 WailsWindow 应用原生标题栏/全屏能力,
|
||||
// 避免误伤输入法候选窗、全屏过渡窗等系统辅助窗口。
|
||||
func shouldApplyMacNativeWindowStyle(identity macWindowIdentity) bool {
|
||||
className := strings.TrimSpace(identity.ClassName)
|
||||
delegateClassName := strings.TrimSpace(identity.DelegateClassName)
|
||||
title := strings.TrimSpace(identity.Title)
|
||||
|
||||
if className == "WailsWindow" || delegateClassName == "WindowDelegate" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 兜底只接受明确命名的主应用窗口,避免把无标题系统辅助窗口纳入样式改写范围。
|
||||
return title == "GoNavi"
|
||||
}
|
||||
|
||||
@@ -35,3 +35,33 @@ func TestResolveMacNativeWindowControlStateDisabled(t *testing.T) {
|
||||
t.Fatal("expected disabled state to avoid native fullscreen behavior")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldApplyMacNativeWindowStyleAcceptsMainWailsWindow(t *testing.T) {
|
||||
tests := []macWindowIdentity{
|
||||
{ClassName: "WailsWindow", DelegateClassName: "WindowDelegate", Title: "GoNavi"},
|
||||
{ClassName: "WailsWindow", DelegateClassName: "", Title: ""},
|
||||
{ClassName: "", DelegateClassName: "WindowDelegate", Title: ""},
|
||||
{ClassName: "", DelegateClassName: "", Title: "GoNavi"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if !shouldApplyMacNativeWindowStyle(tt) {
|
||||
t.Fatalf("expected window identity %+v to be treated as main app window", tt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldApplyMacNativeWindowStyleRejectsSystemAuxiliaryWindows(t *testing.T) {
|
||||
tests := []macWindowIdentity{
|
||||
{ClassName: "TUINSWindow", DelegateClassName: "TUINSWindow", Title: ""},
|
||||
{ClassName: "NSToolbarFullScreenWindow", DelegateClassName: "", Title: ""},
|
||||
{ClassName: "_NSFullScreenTransitionOverlayWindow", DelegateClassName: "", Title: ""},
|
||||
{ClassName: "NSPanel", DelegateClassName: "", Title: ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if shouldApplyMacNativeWindowStyle(tt) {
|
||||
t.Fatalf("expected window identity %+v to be rejected as auxiliary/system window", tt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user