| | |
| | | import AlignerCore |
| | | |
| | | @MainActor |
| | | final class AlignerApplicationDelegate: NSObject, NSApplicationDelegate { |
| | | 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 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) |
| | |
| | | return |
| | | } |
| | | |
| | | configureMainMenu() |
| | | configureStatusItem() |
| | | if !launchOptions.skipPermissions { |
| | | showPermissionsIfNeeded() |
| | |
| | | item.button?.title = "A" |
| | | |
| | | let menu = NSMenu() |
| | | menu.addItem(NSMenuItem(title: "Check Permissions...", action: #selector(showPermissions), keyEquivalent: ",")) |
| | | 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(NSMenuItem(title: "Show Quick Switch", action: #selector(showQuickSwitchFromMenu), keyEquivalent: "")) |
| | | menu.addItem(NSMenuItem(title: "Hide Quick Switch", action: #selector(hideQuickSwitchFromMenu), keyEquivalent: "")) |
| | | menu.addItem(menuItem(title: "Check Permissions...", action: #selector(showPermissions), keyEquivalent: "")) |
| | | menu.addItem(.separator()) |
| | | menu.addItem(NSMenuItem(title: "Show Round0 Debug Overlay", action: #selector(showRound0DebugOverlay), keyEquivalent: "d")) |
| | | menu.addItem(NSMenuItem(title: "Hide Round0 Debug Overlay", action: #selector(hideRound0DebugOverlay), keyEquivalent: "")) |
| | | 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(NSMenuItem(title: "Quit Aligner", action: #selector(quit), keyEquivalent: "q")) |
| | | 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 |
| | | "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() { |
| | |
| | | spaceActivationService: quickSwitchSpaceActivationService(), |
| | | windowCloseService: quickSwitchWindowCloseService(), |
| | | disableScreenshotRefresh: round1QuickSwitchOptions.disableScreenshotRefresh, |
| | | waterfallViewMode: quickSwitchWaterfallViewMode(), |
| | | closeConfirmationRequired: quickSwitchConfirmBeforeClose, |
| | | onCloseConfirmationDisabled: { [weak self] in |
| | | self?.setQuickSwitchConfirmBeforeClose(false) |
| | |
| | | ) |
| | | controller.onSnapshotUpdated = { [weak self] in |
| | | self?.writeQuickSwitchReportIfNeeded() |
| | | } |
| | | controller.onToggleWaterfallViewMode = { [weak self] in |
| | | self?.toggleQuickSwitchWaterfallViewModeFromQuickSwitch() |
| | | } |
| | | quickSwitchSessionController = controller |
| | | } |
| | |
| | | spaceActivationService: quickSwitchSpaceActivationService(), |
| | | windowCloseService: quickSwitchWindowCloseService(), |
| | | disableScreenshotRefresh: round1QuickSwitchOptions.disableScreenshotRefresh, |
| | | waterfallViewMode: quickSwitchWaterfallViewMode(), |
| | | closeConfirmationRequired: quickSwitchConfirmBeforeClose, |
| | | onCloseConfirmationDisabled: { [weak self] in |
| | | self?.setQuickSwitchConfirmBeforeClose(false) |
| | |
| | | ) |
| | | controller.onSnapshotUpdated = { [weak self] in |
| | | self?.writeQuickSwitchReportIfNeeded() |
| | | } |
| | | controller.onToggleWaterfallViewMode = { [weak self] in |
| | | self?.toggleQuickSwitchWaterfallViewModeFromQuickSwitch() |
| | | } |
| | | quickSwitchSessionController = controller |
| | | } |
| | |
| | | guard !isRunningQuickSwitchLifecycle else { return } |
| | | let reportPathSummary = DevelopmentDiagnostics.pathSummary(reportPath) |
| | | |
| | | let report = quickSwitchSessionController?.reportDictionary() ?? [ |
| | | 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["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]) |
| | |
| | | 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, |
| | |
| | | } |
| | | |
| | | return CGWindowAXWindowService(spaceIDsByWindowIDProvider: { _ in [:] }) |
| | | } |
| | | |
| | | private func quickSwitchWaterfallViewMode() -> QuickSwitchWaterfallViewMode { |
| | | round1QuickSwitchOptions.waterfallViewMode |
| | | ?? preferenceStore.read().quickSwitchWaterfallViewMode |
| | | } |
| | | |
| | | private func quickSwitchSpaceActivationService() -> any SpaceActivationServiceProtocol { |
| | |
| | | ]) |
| | | } |
| | | |
| | | 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 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()), |