mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-04 18:43:42 +00:00
fix(ng2): AppKit-owned workspace window opens at launch
三重根因,逐一实测排除:①MenuBarExtra 菜单内容视图首次点开才挂载, launch intent 挂在内容上等于从不执行(此前 legacy 导入自动开窗同样从未 生效),改挂到启动即挂载的标签视图;②SwiftUI openWindow 在 MenuBarExtra 上下文实测不创建场景窗口,且 LSUIElement 应用无场景启动呈现机制——主窗 口改为 AppKit 直控 NSWindow(NSHostingView 承载主 workspace),关闭仅 orderOut 复用,开窗不依赖场景时序;③resyncOnLaunch 的 launchd 往返 await 挡在开窗前,改为先开窗后 resync。装配抽成 AppComposition(init 体量回到 lint 基线)。真机验证:启动即出现主窗口,进程前台形态, SwiftLint/全量测试通过。
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
/// 混合应用形态的 AppKit 生命周期家:LSUIElement 让进程默认纯后台
|
/// 混合应用形态的 AppKit 生命周期家:LSUIElement 让进程默认纯后台
|
||||||
/// (accessory),主 workspace 可见期间由开窗 adapter 切成普通应用
|
/// (accessory),这里负责前台形态的进入与退出——任意路径使主窗口可见
|
||||||
/// (regular);这里负责最后一个窗口关闭后切回纯后台。
|
/// 时切成普通应用(regular),最后一个窗口关闭后切回纯后台。
|
||||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
/// 混合形态的硬性要求:关窗不退进程,代理 GUI 仍驻留菜单栏。
|
/// 混合形态的硬性要求:关窗不退进程,代理 GUI 仍驻留菜单栏。
|
||||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
@@ -10,9 +10,31 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
NotificationCenter.default.addObserver(
|
// 启动即进入前台形态(CONTEXT.md「每次启动呈现主窗口」不变量):LSUIElement
|
||||||
|
// 应用里 SwiftUI 不做启动呈现(defaultLaunchBehavior 只有 suppressed 一档
|
||||||
|
// 增量),而菜单 extra 内容视图首次点开才挂载、launch intent 挂不上;切到
|
||||||
|
// regular 后交给普通应用的默认行为——启动呈现 Window scene。
|
||||||
|
NSApp.setActivationPolicy(.regular)
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
|
||||||
|
let center = NotificationCenter.default
|
||||||
|
center.addObserver(
|
||||||
self, selector: #selector(windowWillClose(_:)),
|
self, selector: #selector(windowWillClose(_:)),
|
||||||
name: NSWindow.willCloseNotification, object: nil)
|
name: NSWindow.willCloseNotification, object: nil)
|
||||||
|
center.addObserver(
|
||||||
|
self, selector: #selector(appOcclusionDidChange(_:)),
|
||||||
|
name: NSApplication.didChangeOcclusionStateNotification, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 关窗回纯后台后再经菜单 ⑦ / Dock reopen 开窗的入场:任意路径让主窗口
|
||||||
|
/// 可见都进前台形态。只进不出:窗口被完全遮挡或 Cmd+H 也会短暂失去
|
||||||
|
/// .visible,但那只意味着「不该退」,退场由 willClose 普查负责;多切一次
|
||||||
|
/// .regular 是幂等的。
|
||||||
|
@objc private func appOcclusionDidChange(_ notification: Notification) {
|
||||||
|
let hasVisibleWindow = NSApp.windows.contains { $0.isVisible && $0.canBecomeMain }
|
||||||
|
guard hasVisibleWindow, NSApp.activationPolicy() != .regular else { return }
|
||||||
|
NSApp.setActivationPolicy(.regular)
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 前台形态下窗口被最小化时,Dock 点击只激活应用不会自动还原(SwiftUI
|
/// 前台形态下窗口被最小化时,Dock 点击只激活应用不会自动还原(SwiftUI
|
||||||
|
|||||||
@@ -12,11 +12,86 @@ struct ShadowsocksXNG2App: App {
|
|||||||
@StateObject private var workspaceRoute: WorkspaceRoute
|
@StateObject private var workspaceRoute: WorkspaceRoute
|
||||||
private let textClipboard: any TextClipboard
|
private let textClipboard: any TextClipboard
|
||||||
private let diagnosticReportExporter: any DiagnosticReportExporter
|
private let diagnosticReportExporter: any DiagnosticReportExporter
|
||||||
|
private let windowOpening: WorkspaceWindowOpeningAdapter
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
let composition = AppComposition.make()
|
||||||
|
_proxyController = StateObject(wrappedValue: composition.controller)
|
||||||
|
_catalogWorkflow = StateObject(wrappedValue: composition.catalogWorkflow)
|
||||||
|
_proxyControl = StateObject(wrappedValue: composition.proxyControl)
|
||||||
|
_loginController = StateObject(wrappedValue: composition.loginController)
|
||||||
|
_settingsWorkflow = StateObject(wrappedValue: composition.settingsWorkflow)
|
||||||
|
_diagnosticsWorkflow = StateObject(wrappedValue: composition.diagnosticsWorkflow)
|
||||||
|
_workspaceRoute = StateObject(wrappedValue: composition.workspaceRoute)
|
||||||
|
textClipboard = composition.textClipboard
|
||||||
|
diagnosticReportExporter = composition.diagnosticReportExporter
|
||||||
|
windowOpening = composition.windowOpening
|
||||||
|
// GUI 事件接入内存环形缓冲(spec #21 D5,issue #34):主窗口日志查看器与
|
||||||
|
// 诊断导出的来源;wrapper 侧不注册,仍走 stderr → agent.log 收敛。
|
||||||
|
RuntimeLog.setSink(RuntimeEventStore.shared)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
MenuBarExtra {
|
||||||
|
ProxyStatusMenu(
|
||||||
|
control: proxyControl,
|
||||||
|
catalogWorkflow: catalogWorkflow,
|
||||||
|
route: workspaceRoute,
|
||||||
|
clipboard: textClipboard,
|
||||||
|
windowOpening: windowOpening)
|
||||||
|
} label: {
|
||||||
|
StatusMenuLabel(
|
||||||
|
control: proxyControl,
|
||||||
|
route: workspaceRoute,
|
||||||
|
windowOpening: windowOpening)
|
||||||
|
}
|
||||||
|
.menuBarExtraStyle(.menu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 状态菜单的标签视图(状态栏图标)。挂载时机与菜单内容视图不同:内容视图
|
||||||
|
/// 首次点开才创建,而标签在启动时即挂载——所以启动策略(resync + launch
|
||||||
|
/// intent,含开窗)只能挂在这里,挂在菜单内容上等于从不执行。
|
||||||
|
private struct StatusMenuLabel: View {
|
||||||
|
@ObservedObject var control: ProxyControlWorkflow
|
||||||
|
@ObservedObject var route: WorkspaceRoute
|
||||||
|
let windowOpening: any WorkspaceWindowOpening
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Image(systemName: "network")
|
||||||
|
.accessibilityLabel("ShadowsocksX-NG 2.0")
|
||||||
|
.task {
|
||||||
|
guard !ApplicationDependencies.isUnitTesting else { return }
|
||||||
|
// 先开窗再 resync:resync 可能 await launchd 往返,不能挡在开窗前面。
|
||||||
|
route.handle(.launch, using: windowOpening)
|
||||||
|
await control.resyncOnLaunch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 组合根装配:目录提交协调器与全部生产运行时适配器只在此接线一次,各 scene
|
||||||
|
/// 不再重复设置提交回调;目录工作流的实现依赖经 CatalogWorkflowDependencies
|
||||||
|
/// 显式装配(workflow 不自行创建生产默认实现);Legacy 导入后的 2.0 运行时
|
||||||
|
/// 边界同为一次性注入。主窗口由 AppKit 直控(见 WorkspaceWindowOpeningAdapter
|
||||||
|
/// 文档);MainWindowView 值廉价且只捕获工作流对象,其 StateObject 状态与
|
||||||
|
/// onAppear 副作用在视图首次进入窗口时才发生。
|
||||||
|
@MainActor
|
||||||
|
private struct AppComposition {
|
||||||
|
let controller: ProxyRuntimeController
|
||||||
|
let catalogWorkflow: CatalogWorkflow
|
||||||
|
let proxyControl: ProxyControlWorkflow
|
||||||
|
let loginController: LaunchAtLoginController
|
||||||
|
let settingsWorkflow: SettingsWorkflow
|
||||||
|
let diagnosticsWorkflow: DiagnosticsWorkflow
|
||||||
|
let workspaceRoute: WorkspaceRoute
|
||||||
|
let textClipboard: any TextClipboard
|
||||||
|
let diagnosticReportExporter: any DiagnosticReportExporter
|
||||||
|
let windowOpening: WorkspaceWindowOpeningAdapter
|
||||||
|
|
||||||
|
static func make() -> AppComposition {
|
||||||
let dependencies = ApplicationDependencies.make()
|
let dependencies = ApplicationDependencies.make()
|
||||||
textClipboard = dependencies.textClipboard
|
let textClipboard = dependencies.textClipboard
|
||||||
diagnosticReportExporter = dependencies.diagnosticReportExporter
|
let diagnosticReportExporter = dependencies.diagnosticReportExporter
|
||||||
// The catalog document is read exactly once at startup. The coordinator
|
// The catalog document is read exactly once at startup. The coordinator
|
||||||
// publishes later committed snapshots; the controller receives read-only
|
// publishes later committed snapshots; the controller receives read-only
|
||||||
// access to this same in-process source.
|
// access to this same in-process source.
|
||||||
@@ -31,18 +106,63 @@ struct ShadowsocksXNG2App: App {
|
|||||||
settingsStore: dependencies.settingsStore,
|
settingsStore: dependencies.settingsStore,
|
||||||
settingsRestore: dependencies.settingsRestore,
|
settingsRestore: dependencies.settingsRestore,
|
||||||
agent: dependencies.launchAgent)
|
agent: dependencies.launchAgent)
|
||||||
_proxyController = StateObject(wrappedValue: controller)
|
|
||||||
let loginController = LaunchAtLoginController(service: dependencies.loginService)
|
let loginController = LaunchAtLoginController(service: dependencies.loginService)
|
||||||
_loginController = StateObject(wrappedValue: loginController)
|
let catalogWorkflow = makeCatalogWorkflow(
|
||||||
// 组合根(issue #40/#41/#49):目录提交协调器与全部生产运行时适配器只在
|
dependencies: dependencies,
|
||||||
// 此接线一次,各 scene 不再重复设置提交回调;目录工作流的实现依赖经
|
controller: controller,
|
||||||
// CatalogWorkflowDependencies 显式装配(workflow 不自行创建生产默认实现);
|
bootstrap: catalogBootstrap)
|
||||||
// Legacy 导入后的 2.0 运行时边界同为一次性注入。
|
// 代理控制工作流 module(issue #47):状态菜单等 UI 表面的唯一代理控制
|
||||||
|
// seam,组合根接线一次。生产 runtime adapter 包装既有控制器(不复制运行
|
||||||
|
// 时语义);目录目标事实经窄缝从目录工作流读取。
|
||||||
|
let proxyControl = ProxyControlWorkflow(
|
||||||
|
runtime: ControllerProxyRuntimeAdapter(controller: controller),
|
||||||
|
targetFacts: catalogWorkflow)
|
||||||
|
// 设置工作流 module(Candidate 02):设置窗口的唯一 seam,组合根接线一次;
|
||||||
|
// 写入侧经窄缝 SettingsCommitting(issue #44),运行时控制器薄扩展即生产实现。
|
||||||
|
let settingsWorkflow = SettingsWorkflow(committing: controller)
|
||||||
|
// 诊断工作流 module(issue #43):诊断区唯一 seam,组合根接线一次;共享
|
||||||
|
// 实例供诊断侧栏与详情共同使用。
|
||||||
|
let diagnosticsWorkflow = DiagnosticsWorkflow(
|
||||||
|
runtimeFacts: controller,
|
||||||
|
catalogFacts: { catalogWorkflow.diagnosticCatalogFacts })
|
||||||
|
let workspaceRoute = WorkspaceRoute()
|
||||||
|
let workspaceContent = MainWindowView(
|
||||||
|
route: workspaceRoute,
|
||||||
|
workflow: catalogWorkflow,
|
||||||
|
control: proxyControl,
|
||||||
|
proxyController: controller,
|
||||||
|
diagnostics: diagnosticsWorkflow,
|
||||||
|
settingsWorkflow: settingsWorkflow,
|
||||||
|
loginController: loginController,
|
||||||
|
clipboard: textClipboard,
|
||||||
|
diagnosticReportExporter: diagnosticReportExporter)
|
||||||
|
let windowOpening = WorkspaceWindowOpeningAdapter(
|
||||||
|
makeContentView: { NSHostingView(rootView: workspaceContent) })
|
||||||
|
return AppComposition(
|
||||||
|
controller: controller,
|
||||||
|
catalogWorkflow: catalogWorkflow,
|
||||||
|
proxyControl: proxyControl,
|
||||||
|
loginController: loginController,
|
||||||
|
settingsWorkflow: settingsWorkflow,
|
||||||
|
diagnosticsWorkflow: diagnosticsWorkflow,
|
||||||
|
workspaceRoute: workspaceRoute,
|
||||||
|
textClipboard: textClipboard,
|
||||||
|
diagnosticReportExporter: diagnosticReportExporter,
|
||||||
|
windowOpening: windowOpening)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 目录工作流接线(issue #40/#41/#49):提交协调器与生产运行时适配器在此
|
||||||
|
/// 一次性装配;Legacy 导入后的 2.0 运行时边界同为一次性注入。
|
||||||
|
private static func makeCatalogWorkflow(
|
||||||
|
dependencies: ApplicationDependencies,
|
||||||
|
controller: ProxyRuntimeController,
|
||||||
|
bootstrap: CatalogCommitBootstrap
|
||||||
|
) -> CatalogWorkflow {
|
||||||
let coordinator = CatalogCommitCoordinator(
|
let coordinator = CatalogCommitCoordinator(
|
||||||
fileStore: dependencies.catalogFileStore,
|
fileStore: dependencies.catalogFileStore,
|
||||||
runtime: ProxyRuntimeSyncAdapter(controller: controller),
|
runtime: ProxyRuntimeSyncAdapter(controller: controller),
|
||||||
bootstrap: catalogBootstrap)
|
bootstrap: bootstrap)
|
||||||
let catalogWorkflow = CatalogWorkflow(
|
return CatalogWorkflow(
|
||||||
dependencies: CatalogWorkflowDependencies(
|
dependencies: CatalogWorkflowDependencies(
|
||||||
coordinator: coordinator,
|
coordinator: coordinator,
|
||||||
credentials: dependencies.credentials,
|
credentials: dependencies.credentials,
|
||||||
@@ -53,51 +173,6 @@ struct ShadowsocksXNG2App: App {
|
|||||||
await controller.legacyImportDidCommit()
|
await controller.legacyImportDidCommit()
|
||||||
},
|
},
|
||||||
activator: controller))
|
activator: controller))
|
||||||
_catalogWorkflow = StateObject(wrappedValue: catalogWorkflow)
|
|
||||||
// 代理控制工作流 module(issue #47):状态菜单等 UI 表面的唯一代理控制
|
|
||||||
// seam,组合根接线一次。生产 runtime adapter 包装既有控制器(不复制运行
|
|
||||||
// 时语义);目录目标事实经窄缝从目录工作流读取。
|
|
||||||
_proxyControl = StateObject(
|
|
||||||
wrappedValue: ProxyControlWorkflow(
|
|
||||||
runtime: ControllerProxyRuntimeAdapter(controller: controller),
|
|
||||||
targetFacts: catalogWorkflow))
|
|
||||||
// 设置工作流 module(Candidate 02):设置窗口的唯一 seam,组合根接线一次;
|
|
||||||
// 写入侧经窄缝 SettingsCommitting(issue #44),运行时控制器薄扩展即生产实现。
|
|
||||||
_settingsWorkflow = StateObject(wrappedValue: SettingsWorkflow(committing: controller))
|
|
||||||
// 诊断工作流 module(issue #43):诊断区唯一 seam,组合根接线一次;共享
|
|
||||||
// 实例供诊断侧栏与详情共同使用。
|
|
||||||
_diagnosticsWorkflow = StateObject(
|
|
||||||
wrappedValue: DiagnosticsWorkflow(
|
|
||||||
runtimeFacts: controller,
|
|
||||||
catalogFacts: { catalogWorkflow.diagnosticCatalogFacts }))
|
|
||||||
_workspaceRoute = StateObject(wrappedValue: WorkspaceRoute())
|
|
||||||
// GUI 事件接入内存环形缓冲(spec #21 D5,issue #34):主窗口日志查看器与
|
|
||||||
// 诊断导出的来源;wrapper 侧不注册,仍走 stderr → agent.log 收敛。
|
|
||||||
RuntimeLog.setSink(RuntimeEventStore.shared)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some Scene {
|
|
||||||
MenuBarExtra("ShadowsocksX-NG 2.0", systemImage: "network") {
|
|
||||||
ProxyStatusMenu(
|
|
||||||
control: proxyControl,
|
|
||||||
catalogWorkflow: catalogWorkflow,
|
|
||||||
route: workspaceRoute,
|
|
||||||
clipboard: textClipboard)
|
|
||||||
}
|
|
||||||
.menuBarExtraStyle(.menu)
|
|
||||||
|
|
||||||
Window("ShadowsocksX-NG 2.0", id: WorkspaceWindowOpeningAdapter.workspaceWindowID) {
|
|
||||||
MainWindowView(
|
|
||||||
route: workspaceRoute,
|
|
||||||
workflow: catalogWorkflow,
|
|
||||||
control: proxyControl,
|
|
||||||
proxyController: proxyController,
|
|
||||||
diagnostics: diagnosticsWorkflow,
|
|
||||||
settingsWorkflow: settingsWorkflow,
|
|
||||||
loginController: loginController,
|
|
||||||
clipboard: textClipboard,
|
|
||||||
diagnosticReportExporter: diagnosticReportExporter)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,23 +10,17 @@ import SwiftUI
|
|||||||
/// 目录树、激活与订阅动作仍走目录工作流,剪贴板写入等 AppKit 副作用留在呈现
|
/// 目录树、激活与订阅动作仍走目录工作流,剪贴板写入等 AppKit 副作用留在呈现
|
||||||
/// 边界。
|
/// 边界。
|
||||||
struct ProxyStatusMenu: View {
|
struct ProxyStatusMenu: View {
|
||||||
@Environment(\.openWindow) private var openWindow
|
|
||||||
/// 代理控制唯一 seam(issue #47):状态摘要与代理命令的唯一来源。
|
/// 代理控制唯一 seam(issue #47):状态摘要与代理命令的唯一来源。
|
||||||
@ObservedObject var control: ProxyControlWorkflow
|
@ObservedObject var control: ProxyControlWorkflow
|
||||||
@ObservedObject var catalogWorkflow: CatalogWorkflow
|
@ObservedObject var catalogWorkflow: CatalogWorkflow
|
||||||
@ObservedObject var route: WorkspaceRoute
|
@ObservedObject var route: WorkspaceRoute
|
||||||
let clipboard: any TextClipboard
|
let clipboard: any TextClipboard
|
||||||
|
/// 开窗 seam 由组合根注入(菜单 extra 上下文的 SwiftUI openWindow 实测不可用)。
|
||||||
|
let windowOpening: any WorkspaceWindowOpening
|
||||||
@StateObject private var errors = ErrorAlertPresenter()
|
@StateObject private var errors = ErrorAlertPresenter()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
menuContent
|
menuContent
|
||||||
.task {
|
|
||||||
guard !isUnitTesting else { return }
|
|
||||||
await control.resyncOnLaunch()
|
|
||||||
route.handle(
|
|
||||||
.launch,
|
|
||||||
using: WorkspaceWindowOpeningAdapter(openWindow: openWindow))
|
|
||||||
}
|
|
||||||
.alert(
|
.alert(
|
||||||
"操作失败",
|
"操作失败",
|
||||||
isPresented: Binding(
|
isPresented: Binding(
|
||||||
@@ -106,9 +100,7 @@ struct ProxyStatusMenu: View {
|
|||||||
|
|
||||||
// ⑦ 打开主窗口
|
// ⑦ 打开主窗口
|
||||||
Button("打开主窗口…") {
|
Button("打开主窗口…") {
|
||||||
route.handle(
|
route.handle(.reopen, using: windowOpening)
|
||||||
.reopen,
|
|
||||||
using: WorkspaceWindowOpeningAdapter(openWindow: openWindow))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
@@ -119,12 +111,6 @@ struct ProxyStatusMenu: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isUnitTesting: Bool {
|
|
||||||
let environment = ProcessInfo.processInfo.environment
|
|
||||||
return environment["XCTestConfigurationFilePath"] != nil
|
|
||||||
|| environment["XCTestSessionIdentifier"] != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func copyHTTPExportLine(_ line: String) {
|
private func copyHTTPExportLine(_ line: String) {
|
||||||
do {
|
do {
|
||||||
try clipboard.write(line)
|
try clipboard.write(line)
|
||||||
|
|||||||
@@ -1,24 +1,42 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// 组合根使用的 workspace 开窗 adapter。scene ID 与 AppKit activation effect
|
/// 组合根持有的 workspace 开窗器:主窗口是 AppKit 拥有的 NSWindow,内容为
|
||||||
/// 只在此 focused adapter 与 MainApp scene declaration 中出现。
|
/// SwiftUI 主 workspace(NSHostingView)。此前经 SwiftUI `Window` scene +
|
||||||
|
/// `openWindow` 开窗,但该环境值在 MenuBarExtra 上下文(无论标签还是内容)
|
||||||
|
/// 实测不产生窗口,SwiftUI 对 LSUIElement 应用也不做场景启动呈现——「每次
|
||||||
|
/// 启动呈现主窗口」必须由 AppKit 直控,不依赖场景挂载时序。窗口关闭时只
|
||||||
|
/// orderOut 不释放(isReleasedWhenClosed = false),重开即复用,workspace
|
||||||
|
/// 内部导航状态得以保留。
|
||||||
@MainActor
|
@MainActor
|
||||||
struct WorkspaceWindowOpeningAdapter: WorkspaceWindowOpening {
|
final class WorkspaceWindowOpeningAdapter: WorkspaceWindowOpening {
|
||||||
static let workspaceWindowID = "main"
|
static let workspaceWindowID = NSUserInterfaceItemIdentifier("main")
|
||||||
|
|
||||||
private let openWindow: OpenWindowAction
|
private var window: NSWindow?
|
||||||
|
private let makeContentView: () -> NSView
|
||||||
|
|
||||||
init(openWindow: OpenWindowAction) {
|
/// 内容视图由组合根惰性构造:主窗口内的 SwiftUI 状态只在首次开窗时创建。
|
||||||
self.openWindow = openWindow
|
init(makeContentView: @escaping () -> NSView) {
|
||||||
|
self.makeContentView = makeContentView
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureWorkspaceVisible() {
|
func ensureWorkspaceVisible() {
|
||||||
// 混合形态契约(CONTEXT.md「Foreground form」不变量):主 workspace 可见
|
|
||||||
// 期间以普通应用呈现,Dock 图标随窗口出现;最后一个窗口关闭后由
|
|
||||||
// AppDelegate 切回纯后台形态。LSUIElement 保证启动即静默。
|
|
||||||
NSApp.setActivationPolicy(.regular)
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
openWindow(id: Self.workspaceWindowID)
|
let window = self.window ?? makeWindow()
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeWindow() -> NSWindow {
|
||||||
|
let window = NSWindow(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: 960, height: 640),
|
||||||
|
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||||
|
backing: .buffered, defer: false)
|
||||||
|
window.title = "ShadowsocksX-NG 2.0"
|
||||||
|
window.identifier = Self.workspaceWindowID
|
||||||
|
window.isReleasedWhenClosed = false
|
||||||
|
window.contentView = makeContentView()
|
||||||
|
window.center()
|
||||||
|
self.window = window
|
||||||
|
return window
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user