| | |
| | | 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 quickSwitchTriggerService: CGEventTapTriggerService? |
| | | private var isRunningQuickSwitchLifecycle = false |
| | | private var statusItem: NSStatusItem? |
| | | private var verticalWaterfallViewModeMenuItem: NSMenuItem? |
| | | private var horizontalWaterfallViewModeMenuItem: NSMenuItem? |
| | | |
| | | func applicationDidFinishLaunching(_ notification: Notification) { |
| | | let bundlePathSummary = DevelopmentDiagnostics.pathSummary(Bundle.main.bundlePath) |
| | |
| | | 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(.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, |
| | | "hasWaterfallViewModeItems": verticalWaterfallViewModeMenuItem != nil |
| | | && horizontalWaterfallViewModeMenuItem != 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 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) |
| | | |
| | | verticalWaterfallViewModeMenuItem = verticalItem |
| | | horizontalWaterfallViewModeMenuItem = horizontalItem |
| | | |
| | | let rootItem = NSMenuItem(title: "Quick Switch View", action: nil, keyEquivalent: "") |
| | | rootItem.submenu = submenu |
| | | return rootItem |
| | | } |
| | | |
| | | private func updateWaterfallViewModeMenuState() { |
| | | let mode = quickSwitchWaterfallViewMode() |
| | | verticalWaterfallViewModeMenuItem?.state = mode == .verticalColumns ? .on : .off |
| | | horizontalWaterfallViewModeMenuItem?.state = mode == .horizontalMasonry ? .on : .off |
| | | } |
| | | |
| | | private func showPermissionsIfNeeded() { |
| | |
| | | let controller = QuickSwitchSessionController( |
| | | snapshotLoader: quickSwitchSnapshotLoader(), |
| | | windowActivationService: quickSwitchWindowActivationService(), |
| | | spaceActivationService: quickSwitchSpaceActivationService(), |
| | | windowCloseService: quickSwitchWindowCloseService(), |
| | | disableScreenshotRefresh: round1QuickSwitchOptions.disableScreenshotRefresh, |
| | | waterfallViewMode: quickSwitchWaterfallViewMode(), |
| | | closeConfirmationRequired: quickSwitchConfirmBeforeClose, |
| | | onCloseConfirmationDisabled: { [weak self] in |
| | | self?.setQuickSwitchConfirmBeforeClose(false) |
| | |
| | | let controller = QuickSwitchSessionController( |
| | | snapshotLoader: quickSwitchSnapshotLoader(), |
| | | windowActivationService: quickSwitchWindowActivationService(), |
| | | spaceActivationService: quickSwitchSpaceActivationService(), |
| | | windowCloseService: quickSwitchWindowCloseService(), |
| | | disableScreenshotRefresh: round1QuickSwitchOptions.disableScreenshotRefresh, |
| | | waterfallViewMode: quickSwitchWaterfallViewMode(), |
| | | closeConfirmationRequired: quickSwitchConfirmBeforeClose, |
| | | onCloseConfirmationDisabled: { [weak self] in |
| | | self?.setQuickSwitchConfirmBeforeClose(false) |
| | |
| | | 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() |
| | | |
| | | do { |
| | | let data = try JSONSerialization.data(withJSONObject: report, options: [.prettyPrinted, .sortedKeys]) |
| | |
| | | return CGWindowAXWindowService(spaceIDsByWindowIDProvider: { _ in [:] }) |
| | | } |
| | | |
| | | private func quickSwitchWaterfallViewMode() -> QuickSwitchWaterfallViewMode { |
| | | round1QuickSwitchOptions.waterfallViewMode |
| | | ?? preferenceStore.read().quickSwitchWaterfallViewMode |
| | | } |
| | | |
| | | private func quickSwitchSpaceActivationService() -> any SpaceActivationServiceProtocol { |
| | | if round1QuickSwitchOptions.debugWindowActivation { |
| | | return DebugSpaceActivationService() |
| | | } |
| | | |
| | | return PrivateSpaceActivationService() |
| | | } |
| | | |
| | | private func quickSwitchWindowCloseService() -> any WindowCloseServiceProtocol { |
| | | if round1QuickSwitchOptions.debugWindowClose { |
| | | return DebugWindowCloseService() |
| | |
| | | ]) |
| | | } |
| | | |
| | | 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() |
| | | writeQuickSwitchReportIfNeeded() |
| | | DevelopmentDiagnostics.log("app.quickSwitch.waterfallViewModePreferenceChanged", [ |
| | | "mode": mode.rawValue, |
| | | "quickSwitchVisible": quickSwitchSessionController?.isVisible ?? false |
| | | ]) |
| | | } |
| | | |
| | | 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() |
| | | return [ |
| | | "hasAboutItem": statusItem?.menu?.item(withTitle: "About Aligner...") != nil, |
| | | "hasQuickSwitchViewSubmenu": statusItem?.menu?.item(withTitle: "Quick Switch View")?.submenu != nil, |
| | | "hasVerticalColumnsItem": verticalWaterfallViewModeMenuItem != nil, |
| | | "hasHorizontalWaterfallItem": horizontalWaterfallViewModeMenuItem != nil, |
| | | "verticalColumnsState": verticalWaterfallViewModeMenuItem?.state == .on ? "on" : "off", |
| | | "horizontalWaterfallState": horizontalWaterfallViewModeMenuItem?.state == .on ? "on" : "off" |
| | | ] |
| | | } |
| | | |
| | | @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 useVerticalWaterfallViewMode() { |
| | | setQuickSwitchWaterfallViewMode(.verticalColumns) |
| | | } |
| | | |
| | | @objc private func useHorizontalWaterfallViewMode() { |
| | | setQuickSwitchWaterfallViewMode(.horizontalMasonry) |
| | | } |
| | | |
| | | @objc private func showPermissions() { |
| | | DevelopmentDiagnostics.log("app.permissions.show", [ |
| | | "accessibility": permissionStatusString(accessibilityStatus()), |