import AppKit
|
import Darwin
|
import AlignerCore
|
|
@MainActor
|
final class AlignerApplicationDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
private static let quickSwitchConfirmBeforeCloseDefaultsKey = "QuickSwitchConfirmBeforeClose"
|
|
private let permissionChecker = SystemPermissionChecker()
|
private let preferenceStore = UserDefaultsPreferenceStore()
|
private let launchOptions = Round0DebugLaunchOptions.parse(arguments: ProcessInfo.processInfo.arguments)
|
private let round1PoCOptions = Round1PerformancePoCOptions.parse(arguments: ProcessInfo.processInfo.arguments)
|
private let round1TriggerProbeOptions = Round1TriggerProbeOptions.parse(arguments: ProcessInfo.processInfo.arguments)
|
private let round1SnapshotDumpOptions = Round1SnapshotDumpOptions.parse(arguments: ProcessInfo.processInfo.arguments)
|
private let round1ScreenshotProbeOptions = Round1ScreenshotProbeOptions.parse(arguments: ProcessInfo.processInfo.arguments)
|
private let round1QuickSwitchOptions = Round1QuickSwitchLaunchOptions.parse(arguments: ProcessInfo.processInfo.arguments)
|
private var permissionWindowController: PermissionsWindowController?
|
private var debugOverlayController: Round0DebugOverlayController?
|
private var round1PerformancePoCController: Round1PerformancePoCController?
|
private var round1TriggerProbe: CGEventTapTriggerProbe?
|
private var quickSwitchSessionController: QuickSwitchSessionController?
|
private var quickSwitchTriggerService: CGEventTapTriggerService?
|
private var settingsWindowController: SettingsWindowController?
|
private var isRunningQuickSwitchLifecycle = false
|
private var statusItem: NSStatusItem?
|
private var verticalWaterfallViewModeMenuItems: [NSMenuItem] = []
|
private var horizontalWaterfallViewModeMenuItems: [NSMenuItem] = []
|
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
let bundlePathSummary = DevelopmentDiagnostics.pathSummary(Bundle.main.bundlePath)
|
DevelopmentDiagnostics.log("app.lifecycle.didFinishLaunching.start", [
|
"bundlePathKind": bundlePathSummary["locationKind"],
|
"bundlePathBasename": bundlePathSummary["basename"],
|
"bundlePathHash": bundlePathSummary["fingerprint"],
|
"accessibility": permissionStatusString(accessibilityStatus()),
|
"screenRecording": permissionStatusString(screenRecordingStatus()),
|
"skipPermissions": launchOptions.skipPermissions,
|
"openQuickSwitch": round1QuickSwitchOptions.openQuickSwitch,
|
"quickSwitchLifecycle": round1QuickSwitchOptions.lifecycleCycles
|
])
|
|
if runRound1SnapshotDumpIfRequested() {
|
DevelopmentDiagnostics.log("app.lifecycle.didFinishLaunching.exitForSnapshotDump")
|
return
|
}
|
if runRound1ScreenshotProbeIfRequested() {
|
DevelopmentDiagnostics.log("app.lifecycle.didFinishLaunching.exitForScreenshotProbe")
|
return
|
}
|
|
configureMainMenu()
|
configureStatusItem()
|
if !launchOptions.skipPermissions {
|
showPermissionsIfNeeded()
|
}
|
startQuickSwitchTriggerServiceIfPossible()
|
showDebugOverlayIfRequested()
|
showRound1PerformancePoCIfRequested()
|
showQuickSwitchIfRequested()
|
startRound1TriggerProbeIfRequested()
|
|
DevelopmentDiagnostics.log("app.lifecycle.didFinishLaunching.end", [
|
"activationPolicy": NSApp.activationPolicy().rawValue,
|
"quickSwitchTriggerRunning": quickSwitchTriggerService?.isRunning ?? false,
|
"statusItemVisible": statusItem != nil
|
])
|
}
|
|
func applicationWillTerminate(_ notification: Notification) {
|
DevelopmentDiagnostics.logSync("app.lifecycle.willTerminate", [
|
"quickSwitchVisible": quickSwitchSessionController?.isVisible ?? false,
|
"hasPermissionWindow": permissionWindowController?.window != nil,
|
"activationPolicy": NSApp.activationPolicy().rawValue
|
])
|
}
|
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
DevelopmentDiagnostics.log("app.lifecycle.shouldTerminateAfterLastWindowClosed", [
|
"decision": false,
|
"visibleWindowCount": sender.windows.filter(\.isVisible).count
|
])
|
return false
|
}
|
|
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
DevelopmentDiagnostics.log("app.lifecycle.shouldHandleReopen", [
|
"hasVisibleWindows": flag,
|
"activationPolicy": sender.activationPolicy().rawValue
|
])
|
return true
|
}
|
|
private func configureStatusItem() {
|
DevelopmentDiagnostics.log("app.statusItem.configure.start")
|
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
|
item.button?.title = "A"
|
|
let menu = NSMenu()
|
menu.delegate = self
|
menu.addItem(menuItem(title: "About Aligner...", action: #selector(showAbout), keyEquivalent: ""))
|
menu.addItem(menuItem(title: "Settings...", action: #selector(showSettingsFromMenu), keyEquivalent: ""))
|
menu.addItem(.separator())
|
menu.addItem(menuItem(title: "Check Permissions...", action: #selector(showPermissions), keyEquivalent: ""))
|
menu.addItem(.separator())
|
menu.addItem(menuItem(title: "Show Quick Switch", action: #selector(showQuickSwitchFromMenu), keyEquivalent: ""))
|
menu.addItem(menuItem(title: "Hide Quick Switch", action: #selector(hideQuickSwitchFromMenu), keyEquivalent: ""))
|
menu.addItem(quickSwitchViewModeMenuItem())
|
menu.addItem(.separator())
|
menu.addItem(menuItem(title: "Show Round0 Debug Overlay", action: #selector(showRound0DebugOverlay), keyEquivalent: "d"))
|
menu.addItem(menuItem(title: "Hide Round0 Debug Overlay", action: #selector(hideRound0DebugOverlay), keyEquivalent: ""))
|
menu.addItem(.separator())
|
menu.addItem(menuItem(title: "Quit Aligner", action: #selector(quit), keyEquivalent: "q"))
|
item.menu = menu
|
|
statusItem = item
|
updateWaterfallViewModeMenuState()
|
DevelopmentDiagnostics.log("app.statusItem.configure.end", [
|
"hasButton": item.button != nil,
|
"hasAboutItem": true,
|
"hasSettingsItem": true,
|
"hasWaterfallViewModeItems": !verticalWaterfallViewModeMenuItems.isEmpty
|
&& !horizontalWaterfallViewModeMenuItems.isEmpty
|
])
|
}
|
|
private func configureMainMenu() {
|
DevelopmentDiagnostics.log("app.mainMenu.configure.start")
|
|
let mainMenu = NSMenu(title: "Main Menu")
|
let appMenuItem = NSMenuItem(title: "Aligner", action: nil, keyEquivalent: "")
|
let appMenu = NSMenu(title: "Aligner")
|
appMenu.delegate = self
|
appMenuItem.submenu = appMenu
|
appMenu.addItem(menuItem(title: "About Aligner...", action: #selector(showAbout), keyEquivalent: ""))
|
appMenu.addItem(.separator())
|
appMenu.addItem(menuItem(title: "Settings...", action: #selector(showSettingsFromMenu), keyEquivalent: ","))
|
appMenu.addItem(.separator())
|
appMenu.addItem(menuItem(title: "Check Permissions...", action: #selector(showPermissions), keyEquivalent: ""))
|
appMenu.addItem(menuItem(title: "Show Quick Switch", action: #selector(showQuickSwitchFromMenu), keyEquivalent: ""))
|
appMenu.addItem(menuItem(title: "Hide Quick Switch", action: #selector(hideQuickSwitchFromMenu), keyEquivalent: ""))
|
appMenu.addItem(quickSwitchViewModeMenuItem())
|
appMenu.addItem(.separator())
|
appMenu.addItem(menuItem(title: "Quit Aligner", action: #selector(quit), keyEquivalent: "q"))
|
mainMenu.addItem(appMenuItem)
|
|
let editMenuItem = NSMenuItem(title: "Edit", action: nil, keyEquivalent: "")
|
let editMenu = NSMenu(title: "Edit")
|
editMenuItem.submenu = editMenu
|
editMenu.addItem(responderMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z"))
|
editMenu.addItem(responderMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "Z"))
|
editMenu.addItem(.separator())
|
editMenu.addItem(responderMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"))
|
editMenu.addItem(responderMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"))
|
editMenu.addItem(responderMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"))
|
editMenu.addItem(responderMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"))
|
mainMenu.addItem(editMenuItem)
|
|
let viewMenuItem = NSMenuItem(title: "View", action: nil, keyEquivalent: "")
|
let viewMenu = NSMenu(title: "View")
|
viewMenu.delegate = self
|
viewMenuItem.submenu = viewMenu
|
viewMenu.addItem(quickSwitchViewModeMenuItem())
|
mainMenu.addItem(viewMenuItem)
|
|
let windowMenuItem = NSMenuItem(title: "Window", action: nil, keyEquivalent: "")
|
let windowMenu = NSMenu(title: "Window")
|
windowMenuItem.submenu = windowMenu
|
windowMenu.addItem(responderMenuItem(title: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m"))
|
windowMenu.addItem(responderMenuItem(title: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: ""))
|
windowMenu.addItem(.separator())
|
windowMenu.addItem(responderMenuItem(title: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: ""))
|
mainMenu.addItem(windowMenuItem)
|
NSApp.windowsMenu = windowMenu
|
|
let helpMenuItem = NSMenuItem(title: "Help", action: nil, keyEquivalent: "")
|
helpMenuItem.submenu = NSMenu(title: "Help")
|
mainMenu.addItem(helpMenuItem)
|
|
NSApp.mainMenu = mainMenu
|
updateWaterfallViewModeMenuState()
|
DevelopmentDiagnostics.log("app.mainMenu.configure.end", [
|
"hasSettingsItem": appMenu.item(withTitle: "Settings...") != nil,
|
"hasQuickSwitchViewSubmenu": appMenu.item(withTitle: "Quick Switch View")?.submenu != nil
|
])
|
}
|
|
func menuWillOpen(_ menu: NSMenu) {
|
updateWaterfallViewModeMenuState()
|
}
|
|
private func menuItem(title: String, action: Selector?, keyEquivalent: String) -> NSMenuItem {
|
let item = NSMenuItem(title: title, action: action, keyEquivalent: keyEquivalent)
|
item.target = self
|
return item
|
}
|
|
private func responderMenuItem(title: String, action: Selector?, keyEquivalent: String) -> NSMenuItem {
|
NSMenuItem(title: title, action: action, keyEquivalent: keyEquivalent)
|
}
|
|
private func quickSwitchViewModeMenuItem() -> NSMenuItem {
|
let submenu = NSMenu(title: "Quick Switch View")
|
let verticalItem = menuItem(
|
title: "Vertical Columns",
|
action: #selector(useVerticalWaterfallViewMode),
|
keyEquivalent: ""
|
)
|
let horizontalItem = menuItem(
|
title: "Horizontal Waterfall",
|
action: #selector(useHorizontalWaterfallViewMode),
|
keyEquivalent: ""
|
)
|
submenu.addItem(verticalItem)
|
submenu.addItem(horizontalItem)
|
|
verticalWaterfallViewModeMenuItems.append(verticalItem)
|
horizontalWaterfallViewModeMenuItems.append(horizontalItem)
|
|
let rootItem = NSMenuItem(title: "Quick Switch View", action: nil, keyEquivalent: "")
|
rootItem.submenu = submenu
|
return rootItem
|
}
|
|
private func updateWaterfallViewModeMenuState() {
|
let mode = quickSwitchWaterfallViewMode()
|
for item in verticalWaterfallViewModeMenuItems {
|
item.state = mode == .verticalColumns ? .on : .off
|
}
|
for item in horizontalWaterfallViewModeMenuItems {
|
item.state = mode == .horizontalMasonry ? .on : .off
|
}
|
}
|
|
private func showPermissionsIfNeeded() {
|
let accessibility = accessibilityStatus()
|
let screenRecording = screenRecordingStatus()
|
DevelopmentDiagnostics.log("app.permissions.checkOnLaunch", [
|
"accessibility": permissionStatusString(accessibility),
|
"screenRecording": permissionStatusString(screenRecording)
|
])
|
if accessibility != .granted || screenRecording != .granted {
|
showPermissions()
|
}
|
}
|
|
private func runRound1SnapshotDumpIfRequested() -> Bool {
|
guard round1SnapshotDumpOptions.enabled else { return false }
|
DevelopmentDiagnostics.log("app.diagnostic.snapshotDump.start")
|
|
do {
|
try Round1SnapshotDump.run(prettyPrinted: round1SnapshotDumpOptions.prettyPrinted)
|
DevelopmentDiagnostics.logSync("app.diagnostic.snapshotDump.success")
|
NSApp.terminate(nil)
|
} catch {
|
DevelopmentDiagnostics.logSync("app.diagnostic.snapshotDump.failed", DevelopmentDiagnostics.errorSummaryFields(error))
|
fputs("Round01 snapshot dump failed: \(DevelopmentDiagnostics.errorSummaryString(error))\n", stderr)
|
exit(2)
|
}
|
|
return true
|
}
|
|
private func runRound1ScreenshotProbeIfRequested() -> Bool {
|
guard round1ScreenshotProbeOptions.enabled else { return false }
|
DevelopmentDiagnostics.log("app.diagnostic.screenshotProbe.start")
|
|
Task { @MainActor in
|
do {
|
try await Round1ScreenshotProbe.run(prettyPrinted: round1ScreenshotProbeOptions.prettyPrinted)
|
DevelopmentDiagnostics.logSync("app.diagnostic.screenshotProbe.success")
|
NSApp.terminate(nil)
|
} catch {
|
DevelopmentDiagnostics.logSync("app.diagnostic.screenshotProbe.failed", DevelopmentDiagnostics.errorSummaryFields(error))
|
fputs("Round01 screenshot probe failed: \(DevelopmentDiagnostics.errorSummaryString(error))\n", stderr)
|
exit(2)
|
}
|
}
|
|
return true
|
}
|
|
private func showDebugOverlayIfRequested() {
|
guard launchOptions.showOverlay else { return }
|
|
showDebugOverlay(includeSettingsChild: launchOptions.showSettingsChild)
|
|
if let autoHideAfter = launchOptions.autoHideAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + autoHideAfter) { [weak self] in
|
self?.hideDebugOverlay()
|
}
|
}
|
|
if let quitAfter = launchOptions.quitAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + quitAfter) {
|
NSApp.terminate(nil)
|
}
|
}
|
}
|
|
private func showRound1PerformancePoCIfRequested() {
|
guard round1PoCOptions.showPerformancePoC else { return }
|
|
if let lifecycleCycles = round1PoCOptions.lifecycleCycles {
|
runRound1PerformancePoCLifecycle(cycles: lifecycleCycles)
|
return
|
}
|
|
openRound1PerformancePoC(autoCycle: round1PoCOptions.autoCycle)
|
|
if let autoHideAfter = round1PoCOptions.autoHideAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + autoHideAfter) { [weak self] in
|
self?.writeRound1PerformancePoCReportIfNeeded()
|
self?.hideRound1PerformancePoC()
|
}
|
}
|
|
if let quitAfter = round1PoCOptions.quitAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + quitAfter) { [weak self] in
|
self?.writeRound1PerformancePoCReportIfNeeded()
|
NSApp.terminate(nil)
|
}
|
}
|
}
|
|
private func runRound1PerformancePoCLifecycle(cycles: Int) {
|
if round1PerformancePoCController == nil {
|
round1PerformancePoCController = Round1PerformancePoCController()
|
}
|
|
round1PerformancePoCController?.prepareLifecycleRun(targetCycles: cycles)
|
runRound1PerformancePoCLifecycleCycle(currentCycle: 0, totalCycles: max(0, cycles))
|
}
|
|
private func runRound1PerformancePoCLifecycleCycle(currentCycle: Int, totalCycles: Int) {
|
guard currentCycle < totalCycles else {
|
round1PerformancePoCController?.finalizeLifecycleRun()
|
writeRound1PerformancePoCReportIfNeeded()
|
NSApp.terminate(nil)
|
return
|
}
|
|
openRound1PerformancePoC(autoCycle: false)
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + round1PoCOptions.lifecycleVisibleDuration) { [weak self] in
|
guard let self else { return }
|
self.hideRound1PerformancePoC()
|
self.round1PerformancePoCController?.markLifecycleCycleCompleted()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + self.round1PoCOptions.lifecycleInterval) { [weak self] in
|
self?.runRound1PerformancePoCLifecycleCycle(currentCycle: currentCycle + 1, totalCycles: totalCycles)
|
}
|
}
|
}
|
|
private func startRound1TriggerProbeIfRequested() {
|
guard round1TriggerProbeOptions.enabled else { return }
|
DevelopmentDiagnostics.log("app.triggerProbe.startRequested")
|
|
let probe = CGEventTapTriggerProbe()
|
|
do {
|
try probe.start()
|
round1TriggerProbe = probe
|
DevelopmentDiagnostics.log("app.triggerProbe.started")
|
print("Round01 trigger probe started")
|
fflush(stdout)
|
} catch {
|
DevelopmentDiagnostics.logSync("app.triggerProbe.failed", DevelopmentDiagnostics.errorSummaryFields(error))
|
fputs("Round01 trigger probe failed: \(DevelopmentDiagnostics.errorSummaryString(error))\n", stderr)
|
exit(2)
|
}
|
|
if let quitAfter = round1TriggerProbeOptions.quitAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + quitAfter) {
|
NSApp.terminate(nil)
|
}
|
}
|
}
|
|
private func startQuickSwitchTriggerServiceIfPossible() {
|
guard !round1TriggerProbeOptions.enabled else {
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.skip", [
|
"reason": "trigger-probe-enabled"
|
])
|
return
|
}
|
let accessibility = accessibilityStatus()
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.startCheck", [
|
"accessibility": permissionStatusString(accessibility),
|
"alreadyRunning": quickSwitchTriggerService?.isRunning ?? false
|
])
|
guard accessibility == .granted else {
|
stopQuickSwitchTriggerService(reason: "accessibility-not-granted")
|
return
|
}
|
guard quickSwitchTriggerService == nil else {
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.alreadyRunning")
|
return
|
}
|
|
let service = CGEventTapTriggerService(
|
overlayVisibleProvider: { [weak self] in
|
self?.quickSwitchSessionController?.isVisible ?? false
|
},
|
accessibilityTrustedProvider: { [weak self] in
|
self?.accessibilityStatus() == .granted
|
}
|
)
|
service.onQuickSwitch = { [weak self] in
|
self?.openQuickSwitchFromTrigger()
|
}
|
service.onStartFailure = { error in
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.startFailure", DevelopmentDiagnostics.errorSummaryFields(error))
|
fputs("Round01 Quick Switch trigger service failed: \(DevelopmentDiagnostics.errorSummaryString(error))\n", stderr)
|
}
|
service.start()
|
|
if service.isRunning {
|
quickSwitchTriggerService = service
|
}
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.startResult", [
|
"isRunning": service.isRunning
|
])
|
}
|
|
private func stopQuickSwitchTriggerService(reason: String) {
|
guard let quickSwitchTriggerService else {
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.stopSkipped", [
|
"reason": reason,
|
"state": "not-running"
|
])
|
return
|
}
|
|
quickSwitchTriggerService.stop()
|
self.quickSwitchTriggerService = nil
|
DevelopmentDiagnostics.log("app.quickSwitchTrigger.stopped", [
|
"reason": reason
|
])
|
fputs("Round01 Quick Switch trigger service stopped: \(reason)\n", stderr)
|
}
|
|
private func showQuickSwitchIfRequested() {
|
guard round1QuickSwitchOptions.openQuickSwitch || round1QuickSwitchOptions.lifecycleCycles != nil else { return }
|
DevelopmentDiagnostics.log("app.quickSwitch.launchOptionRequested", [
|
"openQuickSwitch": round1QuickSwitchOptions.openQuickSwitch,
|
"lifecycleCycles": round1QuickSwitchOptions.lifecycleCycles
|
])
|
|
if let lifecycleCycles = round1QuickSwitchOptions.lifecycleCycles {
|
runRound1QuickSwitchLifecycle(cycles: lifecycleCycles)
|
return
|
}
|
|
openQuickSwitchFromTrigger()
|
writeQuickSwitchReportIfNeeded()
|
|
if let autoHideAfter = round1QuickSwitchOptions.autoHideAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + autoHideAfter) { [weak self] in
|
self?.hideQuickSwitchSession(reason: .timeout)
|
}
|
}
|
|
if let quitAfter = round1QuickSwitchOptions.quitAfter {
|
DispatchQueue.main.asyncAfter(deadline: .now() + quitAfter) {
|
NSApp.terminate(nil)
|
}
|
}
|
}
|
|
private func runRound1QuickSwitchLifecycle(cycles: Int) {
|
DevelopmentDiagnostics.log("app.quickSwitch.lifecycle.start", [
|
"cycles": cycles
|
])
|
isRunningQuickSwitchLifecycle = true
|
if quickSwitchSessionController == nil {
|
let controller = QuickSwitchSessionController(
|
snapshotLoader: quickSwitchSnapshotLoader(),
|
windowActivationService: quickSwitchWindowActivationService(),
|
spaceActivationService: quickSwitchSpaceActivationService(),
|
windowCloseService: quickSwitchWindowCloseService(),
|
disableScreenshotRefresh: round1QuickSwitchOptions.disableScreenshotRefresh,
|
waterfallViewMode: quickSwitchWaterfallViewMode(),
|
theme: quickSwitchThemePreference(),
|
closeConfirmationRequired: quickSwitchConfirmBeforeClose,
|
onCloseConfirmationDisabled: { [weak self] in
|
self?.setQuickSwitchConfirmBeforeClose(false)
|
},
|
debugHoveredAppGroupIndex: round1QuickSwitchOptions.debugHoveredAppGroupIndex,
|
debugOverlayWidth: round1QuickSwitchOptions.debugOverlayWidth,
|
debugKeySequence: round1QuickSwitchOptions.debugKeySequence,
|
debugMouseSequence: round1QuickSwitchOptions.debugMouseSequence,
|
debugSystemCriticalAfter: round1QuickSwitchOptions.debugSystemCriticalAfter
|
)
|
controller.onSnapshotUpdated = { [weak self] in
|
self?.writeQuickSwitchReportIfNeeded()
|
}
|
controller.onToggleWaterfallViewMode = { [weak self] in
|
self?.toggleQuickSwitchWaterfallViewModeFromQuickSwitch()
|
}
|
controller.onToggleTheme = { [weak self] in
|
self?.toggleQuickSwitchThemeFromQuickSwitch()
|
}
|
quickSwitchSessionController = controller
|
}
|
|
quickSwitchSessionController?.prepareLifecycleRun(targetCycles: cycles)
|
runRound1QuickSwitchLifecycleCycle(currentCycle: 0, totalCycles: max(0, cycles))
|
}
|
|
private func runRound1QuickSwitchLifecycleCycle(currentCycle: Int, totalCycles: Int) {
|
guard currentCycle < totalCycles else {
|
DevelopmentDiagnostics.log("app.quickSwitch.lifecycle.complete", [
|
"totalCycles": totalCycles
|
])
|
quickSwitchSessionController?.finalizeLifecycleRun()
|
isRunningQuickSwitchLifecycle = false
|
writeQuickSwitchReportIfNeeded()
|
NSApp.terminate(nil)
|
return
|
}
|
|
openQuickSwitchSession()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + round1QuickSwitchOptions.lifecycleVisibleDuration) { [weak self] in
|
guard let self else { return }
|
self.hideQuickSwitchSession(reason: .timeout)
|
self.quickSwitchSessionController?.markLifecycleCycleCompleted()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + self.round1QuickSwitchOptions.lifecycleInterval) { [weak self] in
|
self?.runRound1QuickSwitchLifecycleCycle(currentCycle: currentCycle + 1, totalCycles: totalCycles)
|
}
|
}
|
}
|
|
private func openQuickSwitchFromTrigger() {
|
let accessibility = accessibilityStatus()
|
DevelopmentDiagnostics.log("app.quickSwitch.openFromTrigger", [
|
"accessibility": permissionStatusString(accessibility),
|
"screenRecording": permissionStatusString(screenRecordingStatus()),
|
"alreadyVisible": quickSwitchSessionController?.isVisible ?? false
|
])
|
guard accessibility == .granted else {
|
stopQuickSwitchTriggerService(reason: "quick-switch-trigger-without-accessibility")
|
hideQuickSwitchSession(reason: .userClosed)
|
showPermissions()
|
return
|
}
|
|
if quickSwitchSessionController?.isVisible == true {
|
quickSwitchSessionController?.retriggerFromShortcut()
|
} else {
|
openQuickSwitchSession()
|
}
|
}
|
|
private func openQuickSwitchSession() {
|
if quickSwitchSessionController == nil {
|
DevelopmentDiagnostics.log("app.quickSwitch.createSessionController")
|
let controller = QuickSwitchSessionController(
|
snapshotLoader: quickSwitchSnapshotLoader(),
|
windowActivationService: quickSwitchWindowActivationService(),
|
spaceActivationService: quickSwitchSpaceActivationService(),
|
windowCloseService: quickSwitchWindowCloseService(),
|
disableScreenshotRefresh: round1QuickSwitchOptions.disableScreenshotRefresh,
|
waterfallViewMode: quickSwitchWaterfallViewMode(),
|
theme: quickSwitchThemePreference(),
|
closeConfirmationRequired: quickSwitchConfirmBeforeClose,
|
onCloseConfirmationDisabled: { [weak self] in
|
self?.setQuickSwitchConfirmBeforeClose(false)
|
},
|
debugHoveredAppGroupIndex: round1QuickSwitchOptions.debugHoveredAppGroupIndex,
|
debugOverlayWidth: round1QuickSwitchOptions.debugOverlayWidth,
|
debugKeySequence: round1QuickSwitchOptions.debugKeySequence,
|
debugMouseSequence: round1QuickSwitchOptions.debugMouseSequence,
|
debugSystemCriticalAfter: round1QuickSwitchOptions.debugSystemCriticalAfter
|
)
|
controller.onSnapshotUpdated = { [weak self] in
|
self?.writeQuickSwitchReportIfNeeded()
|
}
|
controller.onToggleWaterfallViewMode = { [weak self] in
|
self?.toggleQuickSwitchWaterfallViewModeFromQuickSwitch()
|
}
|
controller.onToggleTheme = { [weak self] in
|
self?.toggleQuickSwitchThemeFromQuickSwitch()
|
}
|
quickSwitchSessionController = controller
|
}
|
|
DevelopmentDiagnostics.log("app.quickSwitch.session.showRequested")
|
quickSwitchSessionController?.show()
|
writeQuickSwitchReportIfNeeded()
|
}
|
|
private func hideQuickSwitchSession(reason: DismissReason = .userClosed) {
|
DevelopmentDiagnostics.log("app.quickSwitch.session.hideRequested", [
|
"reason": Self.dismissReasonString(reason),
|
"wasVisible": quickSwitchSessionController?.isVisible ?? false
|
])
|
quickSwitchSessionController?.hide(reason: reason)
|
}
|
|
private func writeQuickSwitchReportIfNeeded() {
|
guard let reportPath = round1QuickSwitchOptions.reportPath else { return }
|
guard !isRunningQuickSwitchLifecycle else { return }
|
let reportPathSummary = DevelopmentDiagnostics.pathSummary(reportPath)
|
|
var report: [String: Any] = quickSwitchSessionController?.reportDictionary() ?? [
|
"snapshotLoaded": false,
|
"quickSwitchVisible": false,
|
"permissionStatus": "\(accessibilityStatus())",
|
"diagnosticLogLocationKind": DevelopmentDiagnostics.pathSummary(DevelopmentDiagnostics.logPath)["locationKind"] ?? "unknown",
|
"diagnosticLogBasename": DevelopmentDiagnostics.pathSummary(DevelopmentDiagnostics.logPath)["basename"] ?? "aligner-dev.log"
|
]
|
let versionBuild = appVersionBuild()
|
report["preferenceWaterfallViewMode"] = preferenceStore.read().quickSwitchWaterfallViewMode.rawValue
|
report["resolvedWaterfallViewMode"] = quickSwitchWaterfallViewMode().rawValue
|
report["preferenceTheme"] = preferenceStore.read().theme.rawValue
|
report["resolvedQuickSwitchTheme"] = quickSwitchThemePreference().rawValue
|
report["aboutVersion"] = versionBuild.version
|
report["aboutBuild"] = versionBuild.build
|
report["statusMenu"] = statusMenuReportDictionary()
|
report["mainMenu"] = mainMenuReportDictionary()
|
report["settingsWindow"] = settingsWindowReportDictionary()
|
|
do {
|
let data = try JSONSerialization.data(withJSONObject: report, options: [.prettyPrinted, .sortedKeys])
|
try data.write(to: URL(fileURLWithPath: reportPath), options: [.atomic])
|
DevelopmentDiagnostics.log("app.quickSwitch.reportWritten", [
|
"reportPathKind": reportPathSummary["locationKind"],
|
"reportPathBasename": reportPathSummary["basename"],
|
"reportPathHash": reportPathSummary["fingerprint"]
|
])
|
} catch {
|
var fields: [String: CustomStringConvertible?] = [
|
"reportPathKind": reportPathSummary["locationKind"],
|
"reportPathBasename": reportPathSummary["basename"],
|
"reportPathHash": reportPathSummary["fingerprint"]
|
]
|
DevelopmentDiagnostics.errorSummaryFields(error).forEach { fields[$0.key] = $0.value }
|
DevelopmentDiagnostics.log("app.quickSwitch.reportFailed", fields)
|
fputs("Round01 Quick Switch report failed: \(DevelopmentDiagnostics.errorSummaryString(error))\n", stderr)
|
}
|
}
|
|
private func accessibilityStatus() -> PermissionStatus {
|
if round1QuickSwitchOptions.simulateAccessibilityDenied {
|
return .notGranted
|
}
|
|
return permissionChecker.status(for: .accessibility)
|
}
|
|
private func screenRecordingStatus() -> PermissionStatus {
|
if round1QuickSwitchOptions.simulateScreenRecordingDenied {
|
return .notGranted
|
}
|
|
return permissionChecker.status(for: .screenRecording)
|
}
|
|
private func quickSwitchSnapshotLoader() -> any QuickSwitchSnapshotLoading {
|
if round1QuickSwitchOptions.fixtureActivation {
|
return FixtureQuickSwitchSnapshotLoader(appCount: 2, windowsPerApp: 1, mode: .activation)
|
}
|
|
if round1QuickSwitchOptions.fixtureCandidateFiltering {
|
return FixtureQuickSwitchSnapshotLoader(appCount: 1, mode: .candidateFiltering)
|
}
|
|
if round1QuickSwitchOptions.fixtureSpaceFilter {
|
return FixtureQuickSwitchSnapshotLoader(appCount: 1, mode: .spaceFilter)
|
}
|
|
if round1QuickSwitchOptions.fixtureSplitView {
|
return FixtureQuickSwitchSnapshotLoader(appCount: 1, mode: .splitView)
|
}
|
|
if round1QuickSwitchOptions.fixtureMultiPageIdentity {
|
return FixtureQuickSwitchSnapshotLoader(appCount: 1, mode: .multiPageIdentity)
|
}
|
|
if let fixtureAppCount = round1QuickSwitchOptions.fixtureAppCount {
|
return FixtureQuickSwitchSnapshotLoader(
|
appCount: fixtureAppCount,
|
windowsPerApp: round1QuickSwitchOptions.fixtureWindowsPerApp ?? 1,
|
mode: .layout,
|
usesMultipleDisplays: round1QuickSwitchOptions.fixtureMultiDisplay
|
)
|
}
|
|
let liveLoader = LiveQuickSwitchSnapshotLoader()
|
guard let delay = round1QuickSwitchOptions.snapshotLoadDelay else {
|
return liveLoader
|
}
|
|
return DelayedQuickSwitchSnapshotLoader(base: liveLoader, delay: delay)
|
}
|
|
private func quickSwitchWindowActivationService() -> any WindowActivationServiceProtocol {
|
if round1QuickSwitchOptions.debugWindowActivation {
|
return DebugWindowActivationService()
|
}
|
|
return CGWindowAXWindowService(spaceIDsByWindowIDProvider: { _ in [:] })
|
}
|
|
private func quickSwitchWaterfallViewMode() -> QuickSwitchWaterfallViewMode {
|
round1QuickSwitchOptions.waterfallViewMode
|
?? preferenceStore.read().quickSwitchWaterfallViewMode
|
}
|
|
private func quickSwitchThemePreference() -> ThemePreference {
|
let theme = preferenceStore.read().theme
|
return theme == .dark ? .dark : .light
|
}
|
|
private func quickSwitchSpaceActivationService() -> any SpaceActivationServiceProtocol {
|
if round1QuickSwitchOptions.debugWindowActivation {
|
return DebugSpaceActivationService()
|
}
|
|
return PrivateSpaceActivationService()
|
}
|
|
private func quickSwitchWindowCloseService() -> any WindowCloseServiceProtocol {
|
if round1QuickSwitchOptions.debugWindowClose {
|
return DebugWindowCloseService()
|
}
|
|
return CGWindowAXWindowService(spaceIDsByWindowIDProvider: { _ in [:] })
|
}
|
|
private var quickSwitchConfirmBeforeClose: Bool {
|
let value = UserDefaults.standard.object(forKey: Self.quickSwitchConfirmBeforeCloseDefaultsKey)
|
return (value as? Bool) ?? true
|
}
|
|
private func setQuickSwitchConfirmBeforeClose(_ enabled: Bool) {
|
UserDefaults.standard.set(enabled, forKey: Self.quickSwitchConfirmBeforeCloseDefaultsKey)
|
DevelopmentDiagnostics.log("app.quickSwitch.closeConfirmationPreferenceChanged", [
|
"confirmBeforeClose": enabled
|
])
|
}
|
|
private func setQuickSwitchWaterfallViewMode(_ mode: QuickSwitchWaterfallViewMode) {
|
let preferences = preferenceStore.read()
|
let updatedPreferences = AlignerPreferences(
|
showMinimizedWindows: preferences.showMinimizedWindows,
|
showFullscreenWindows: preferences.showFullscreenWindows,
|
quickSwitchWaterfallViewMode: mode,
|
theme: preferences.theme,
|
language: preferences.language,
|
spaceDisplayStrategy: preferences.spaceDisplayStrategy
|
)
|
preferenceStore.write(updatedPreferences)
|
quickSwitchSessionController?.setWaterfallViewMode(mode)
|
updateWaterfallViewModeMenuState()
|
settingsWindowController?.refreshWaterfallViewMode()
|
writeQuickSwitchReportIfNeeded()
|
DevelopmentDiagnostics.log("app.quickSwitch.waterfallViewModePreferenceChanged", [
|
"mode": mode.rawValue,
|
"quickSwitchVisible": quickSwitchSessionController?.isVisible ?? false
|
])
|
}
|
|
private func toggleQuickSwitchWaterfallViewModeFromQuickSwitch() {
|
let currentMode = quickSwitchWaterfallViewMode()
|
let nextMode: QuickSwitchWaterfallViewMode = currentMode == .horizontalMasonry
|
? .verticalColumns
|
: .horizontalMasonry
|
DevelopmentDiagnostics.log("app.quickSwitch.waterfallViewModeToggle", [
|
"currentMode": currentMode.rawValue,
|
"nextMode": nextMode.rawValue,
|
"quickSwitchVisible": quickSwitchSessionController?.isVisible ?? false
|
])
|
setQuickSwitchWaterfallViewMode(nextMode)
|
}
|
|
private func setQuickSwitchThemePreference(_ theme: ThemePreference) {
|
let normalizedTheme: ThemePreference = theme == .dark ? .dark : .light
|
let preferences = preferenceStore.read()
|
let updatedPreferences = AlignerPreferences(
|
showMinimizedWindows: preferences.showMinimizedWindows,
|
showFullscreenWindows: preferences.showFullscreenWindows,
|
quickSwitchWaterfallViewMode: preferences.quickSwitchWaterfallViewMode,
|
theme: normalizedTheme,
|
language: preferences.language,
|
spaceDisplayStrategy: preferences.spaceDisplayStrategy
|
)
|
preferenceStore.write(updatedPreferences)
|
quickSwitchSessionController?.setTheme(normalizedTheme)
|
writeQuickSwitchReportIfNeeded()
|
DevelopmentDiagnostics.log("app.quickSwitch.themePreferenceChanged", [
|
"theme": normalizedTheme.rawValue,
|
"quickSwitchVisible": quickSwitchSessionController?.isVisible ?? false
|
])
|
}
|
|
private func toggleQuickSwitchThemeFromQuickSwitch() {
|
let currentTheme = quickSwitchThemePreference()
|
let nextTheme: ThemePreference = currentTheme == .dark ? .light : .dark
|
DevelopmentDiagnostics.log("app.quickSwitch.themeToggle", [
|
"currentTheme": currentTheme.rawValue,
|
"nextTheme": nextTheme.rawValue,
|
"quickSwitchVisible": quickSwitchSessionController?.isVisible ?? false
|
])
|
setQuickSwitchThemePreference(nextTheme)
|
}
|
|
private func appVersionBuild() -> (version: String, build: String) {
|
let info = Bundle.main.infoDictionary ?? [:]
|
let version = info["CFBundleShortVersionString"] as? String ?? "Unknown"
|
let build = info["CFBundleVersion"] as? String ?? "Unknown"
|
return (version, build)
|
}
|
|
private func statusMenuReportDictionary() -> [String: Any] {
|
updateWaterfallViewModeMenuState()
|
let verticalItem = verticalWaterfallViewModeMenuItems.first
|
let horizontalItem = horizontalWaterfallViewModeMenuItems.first
|
return [
|
"hasAboutItem": statusItem?.menu?.item(withTitle: "About Aligner...") != nil,
|
"hasSettingsItem": statusItem?.menu?.item(withTitle: "Settings...") != nil,
|
"hasQuickSwitchViewSubmenu": statusItem?.menu?.item(withTitle: "Quick Switch View")?.submenu != nil,
|
"hasVerticalColumnsItem": verticalItem != nil,
|
"hasHorizontalWaterfallItem": horizontalItem != nil,
|
"verticalColumnsState": verticalItem?.state == .on ? "on" : "off",
|
"horizontalWaterfallState": horizontalItem?.state == .on ? "on" : "off"
|
]
|
}
|
|
private func mainMenuReportDictionary() -> [String: Any] {
|
updateWaterfallViewModeMenuState()
|
let appMenu = NSApp.mainMenu?.items.first?.submenu
|
let viewMenu = NSApp.mainMenu?.item(withTitle: "View")?.submenu
|
return [
|
"hasMenu": NSApp.mainMenu != nil,
|
"hasAppMenu": appMenu != nil,
|
"hasAboutItem": appMenu?.item(withTitle: "About Aligner...") != nil,
|
"hasSettingsItem": appMenu?.item(withTitle: "Settings...") != nil,
|
"settingsKeyEquivalent": appMenu?.item(withTitle: "Settings...")?.keyEquivalent ?? "",
|
"hasQuickSwitchViewSubmenuInAppMenu": appMenu?.item(withTitle: "Quick Switch View")?.submenu != nil,
|
"hasQuickSwitchViewSubmenuInViewMenu": viewMenu?.item(withTitle: "Quick Switch View")?.submenu != nil
|
]
|
}
|
|
private func settingsWindowReportDictionary() -> [String: Any] {
|
settingsWindowController?.reportDictionary() ?? [
|
"exists": false,
|
"visible": false,
|
"title": "",
|
"selectedTabLabel": "",
|
"selectedTabIdentifier": "",
|
"waterfallViewMode": quickSwitchWaterfallViewMode().rawValue,
|
"hasViewTab": false,
|
"usesSwiftUI": false
|
]
|
}
|
|
@objc private func showAbout() {
|
let versionBuild = appVersionBuild()
|
DevelopmentDiagnostics.log("app.menu.showAbout", [
|
"version": versionBuild.version,
|
"build": versionBuild.build
|
])
|
|
NSApp.setActivationPolicy(.regular)
|
let alert = NSAlert()
|
alert.messageText = "Aligner"
|
alert.informativeText = "Version / Build: \(versionBuild.version) / \(versionBuild.build)"
|
alert.alertStyle = .informational
|
alert.addButton(withTitle: "OK")
|
alert.icon = NSApp.applicationIconImage
|
NSApp.activate(ignoringOtherApps: true)
|
alert.runModal()
|
}
|
|
@objc private func showSettingsFromMenu() {
|
openSettingsWindow(source: "menu")
|
}
|
|
private func showSettingsFromQuickSwitch() {
|
openSettingsWindow(source: "quickSwitch")
|
}
|
|
private func openSettingsWindow(source: String) {
|
DevelopmentDiagnostics.log("app.settings.show", [
|
"source": source,
|
"quickSwitchVisible": quickSwitchSessionController?.isVisible ?? false,
|
"mode": quickSwitchWaterfallViewMode().rawValue
|
])
|
hideQuickSwitchSession(reason: .userClosed)
|
NSApp.setActivationPolicy(.regular)
|
|
if settingsWindowController == nil {
|
settingsWindowController = SettingsWindowController(
|
modeProvider: { [weak self] in
|
self?.quickSwitchWaterfallViewMode() ?? .verticalColumns
|
},
|
onWaterfallViewModeChanged: { [weak self] mode in
|
self?.setQuickSwitchWaterfallViewMode(mode)
|
}
|
)
|
}
|
|
settingsWindowController?.showWindow(nil)
|
NSApp.activate(ignoringOtherApps: true)
|
writeQuickSwitchReportIfNeeded()
|
DevelopmentDiagnostics.log("app.settings.windowShown", [
|
"windowVisible": settingsWindowController?.window?.isVisible ?? false,
|
"activationPolicy": NSApp.activationPolicy().rawValue
|
])
|
}
|
|
@objc private func useVerticalWaterfallViewMode() {
|
setQuickSwitchWaterfallViewMode(.verticalColumns)
|
}
|
|
@objc private func useHorizontalWaterfallViewMode() {
|
setQuickSwitchWaterfallViewMode(.horizontalMasonry)
|
}
|
|
@objc private func showPermissions() {
|
DevelopmentDiagnostics.log("app.permissions.show", [
|
"accessibility": permissionStatusString(accessibilityStatus()),
|
"screenRecording": permissionStatusString(screenRecordingStatus())
|
])
|
NSApp.setActivationPolicy(.regular)
|
stopQuickSwitchTriggerService(reason: "show-permissions")
|
hideQuickSwitchSession(reason: .userClosed)
|
|
if permissionWindowController == nil {
|
let controller = PermissionsWindowController(permissionManager: permissionChecker)
|
controller.onPermissionStatusChanged = { [weak self] kind, status in
|
DevelopmentDiagnostics.log("app.permissions.statusChanged", [
|
"kind": kind.rawValue,
|
"status": self?.permissionStatusString(status) ?? "\(status)"
|
])
|
guard kind == .accessibility else { return }
|
|
switch status {
|
case .granted:
|
self?.startQuickSwitchTriggerServiceIfPossible()
|
case .notGranted:
|
self?.stopQuickSwitchTriggerService(reason: "accessibility-status-not-granted")
|
self?.hideQuickSwitchSession(reason: .userClosed)
|
}
|
}
|
controller.onPermissionRequestStarted = { [weak self] kind in
|
DevelopmentDiagnostics.log("app.permissions.requestStarted", [
|
"kind": kind.rawValue
|
])
|
if kind == .accessibility {
|
self?.stopQuickSwitchTriggerService(reason: "accessibility-request-started")
|
self?.hideQuickSwitchSession(reason: .userClosed)
|
}
|
}
|
permissionWindowController = controller
|
}
|
|
permissionWindowController?.showWindow(nil)
|
permissionWindowController?.window?.makeKeyAndOrderFront(nil)
|
NSApp.activate(ignoringOtherApps: true)
|
DevelopmentDiagnostics.log("app.permissions.windowShown", [
|
"activationPolicy": NSApp.activationPolicy().rawValue,
|
"windowVisible": permissionWindowController?.window?.isVisible ?? false
|
])
|
}
|
|
@objc private func showRound0DebugOverlay() {
|
DevelopmentDiagnostics.log("app.menu.showRound0DebugOverlay")
|
showDebugOverlay(includeSettingsChild: true)
|
}
|
|
@objc private func hideRound0DebugOverlay() {
|
DevelopmentDiagnostics.log("app.menu.hideRound0DebugOverlay")
|
hideDebugOverlay()
|
}
|
|
@objc private func showQuickSwitchFromMenu() {
|
DevelopmentDiagnostics.log("app.menu.showQuickSwitch")
|
openQuickSwitchFromTrigger()
|
}
|
|
@objc private func hideQuickSwitchFromMenu() {
|
DevelopmentDiagnostics.log("app.menu.hideQuickSwitch")
|
hideQuickSwitchSession()
|
}
|
|
private func hideRound1PerformancePoC() {
|
round1PerformancePoCController?.hide()
|
}
|
|
private func writeRound1PerformancePoCReportIfNeeded() {
|
guard let reportPath = round1PoCOptions.reportPath,
|
let report = round1PerformancePoCController?.reportDictionary()
|
else { return }
|
let reportPathSummary = DevelopmentDiagnostics.pathSummary(reportPath)
|
|
do {
|
let data = try JSONSerialization.data(withJSONObject: report, options: [.prettyPrinted, .sortedKeys])
|
try data.write(to: URL(fileURLWithPath: reportPath), options: [.atomic])
|
DevelopmentDiagnostics.log("app.performancePoC.reportWritten", [
|
"reportPathKind": reportPathSummary["locationKind"],
|
"reportPathBasename": reportPathSummary["basename"],
|
"reportPathHash": reportPathSummary["fingerprint"]
|
])
|
} catch {
|
var fields: [String: CustomStringConvertible?] = [
|
"reportPathKind": reportPathSummary["locationKind"],
|
"reportPathBasename": reportPathSummary["basename"],
|
"reportPathHash": reportPathSummary["fingerprint"]
|
]
|
DevelopmentDiagnostics.errorSummaryFields(error).forEach { fields[$0.key] = $0.value }
|
DevelopmentDiagnostics.log("app.performancePoC.reportFailed", fields)
|
fputs("Round01 performance PoC report failed: \(DevelopmentDiagnostics.errorSummaryString(error))\n", stderr)
|
}
|
}
|
|
private func showDebugOverlay(includeSettingsChild: Bool) {
|
DevelopmentDiagnostics.log("app.debugOverlay.show", [
|
"includeSettingsChild": includeSettingsChild
|
])
|
if debugOverlayController == nil {
|
debugOverlayController = Round0DebugOverlayController()
|
}
|
|
debugOverlayController?.show(includeSettingsChild: includeSettingsChild)
|
}
|
|
private func hideDebugOverlay() {
|
DevelopmentDiagnostics.log("app.debugOverlay.hide")
|
debugOverlayController?.hide()
|
}
|
|
private func openRound1PerformancePoC(autoCycle: Bool) {
|
DevelopmentDiagnostics.log("app.performancePoC.show", [
|
"autoCycle": autoCycle
|
])
|
if round1PerformancePoCController == nil {
|
round1PerformancePoCController = Round1PerformancePoCController()
|
}
|
|
round1PerformancePoCController?.show(autoCycle: autoCycle)
|
}
|
|
@objc private func quit() {
|
DevelopmentDiagnostics.logSync("app.menu.quit")
|
NSApp.terminate(nil)
|
}
|
|
private func permissionStatusString(_ status: PermissionStatus) -> String {
|
switch status {
|
case .granted:
|
return "granted"
|
case .notGranted:
|
return "notGranted"
|
}
|
}
|
|
private static func dismissReasonString(_ reason: DismissReason) -> String {
|
switch reason {
|
case .escape:
|
return "escape"
|
case .focusLost:
|
return "focusLost"
|
case .systemCriticalWindow:
|
return "systemCriticalWindow"
|
case .timeout:
|
return "timeout"
|
case .userClosed:
|
return "userClosed"
|
}
|
}
|
}
|