diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 337ad9f3..57166431 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -3258,9 +3258,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ) } + @MainActor + static func presentPreferencesWindow( + sendShowSettingsAction: @MainActor () -> Bool = { + NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) + }, + showFallbackSettingsWindow: @MainActor () -> Void = { + SettingsWindowController.shared.show() + }, + activateApplication: @MainActor () -> Void = { + NSApp.activate(ignoringOtherApps: true) + } + ) { + let handledByResponderChain = sendShowSettingsAction() + if !handledByResponderChain { + showFallbackSettingsWindow() + } + activateApplication() + } + @objc func openPreferencesWindow() { - NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) - NSApp.activate(ignoringOtherApps: true) + Self.presentPreferencesWindow() } func refreshMenuBarExtraForDebug() { diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 04a6433a..60b38650 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -3923,7 +3923,7 @@ struct ContentView: View { AppDelegate.shared?.jumpToLatestUnread() } registry.register(commandId: "palette.openSettings") { - NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) + AppDelegate.presentPreferencesWindow() } registry.register(commandId: "palette.checkForUpdates") { AppDelegate.shared?.checkForUpdates(nil) diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 004406e7..e8a0c634 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -1701,7 +1701,7 @@ private struct AcknowledgmentsView: View { } } -private final class SettingsWindowController: NSWindowController, NSWindowDelegate { +final class SettingsWindowController: NSWindowController, NSWindowDelegate { static let shared = SettingsWindowController() private init() { diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index 59796d6d..3ef18f94 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -417,6 +417,52 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { XCTAssertTrue(appDelegate.tabManager === firstManager, "Unresolved event window should not retarget active manager") } + func testPresentPreferencesWindowUsesFallbackWhenResponderChainDoesNotHandleSettingsAction() { + var sendShowSettingsActionCallCount = 0 + var showFallbackSettingsWindowCallCount = 0 + var activateApplicationCallCount = 0 + + AppDelegate.presentPreferencesWindow( + sendShowSettingsAction: { + sendShowSettingsActionCallCount += 1 + return false + }, + showFallbackSettingsWindow: { + showFallbackSettingsWindowCallCount += 1 + }, + activateApplication: { + activateApplicationCallCount += 1 + } + ) + + XCTAssertEqual(sendShowSettingsActionCallCount, 1) + XCTAssertEqual(showFallbackSettingsWindowCallCount, 1) + XCTAssertEqual(activateApplicationCallCount, 1) + } + + func testPresentPreferencesWindowSkipsFallbackWhenResponderChainHandlesSettingsAction() { + var sendShowSettingsActionCallCount = 0 + var showFallbackSettingsWindowCallCount = 0 + var activateApplicationCallCount = 0 + + AppDelegate.presentPreferencesWindow( + sendShowSettingsAction: { + sendShowSettingsActionCallCount += 1 + return true + }, + showFallbackSettingsWindow: { + showFallbackSettingsWindowCallCount += 1 + }, + activateApplication: { + activateApplicationCallCount += 1 + } + ) + + XCTAssertEqual(sendShowSettingsActionCallCount, 1) + XCTAssertEqual(showFallbackSettingsWindowCallCount, 0) + XCTAssertEqual(activateApplicationCallCount, 1) + } + private func makeKeyDownEvent( key: String, modifiers: NSEvent.ModifierFlags,