diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index af1c9572073a..8a0035293ca4 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -9416,16 +9416,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent writeGotoSplitTestData(updates) } - private func recordGotoSplitZoomIfNeeded() { + private func recordGotoSplitZoomIfNeeded(tabManager: TabManager? = nil) { guard isGotoSplitUITestRecordingEnabled() else { return } - guard let workspace = tabManager?.selectedWorkspace else { return } + guard let workspace = (tabManager ?? self.tabManager)?.selectedWorkspace else { return } func snapshot(for workspace: Workspace) -> ([String: String], Bool) { let browserPanel = workspace.panels.values.compactMap { $0 as? BrowserPanel }.first let otherTerminal = workspace.panels.values.compactMap { $0 as? TerminalPanel }.first - let browserSnapshot = browserPanel.flatMap { - BrowserWindowPortalRegistry.debugSnapshot(for: $0.webView) - } + let browserSnapshot = browserPanel.flatMap { BrowserWindowPortalRegistry.debugSnapshot(for: $0.webView) } var updates = self.gotoSplitFindStateSnapshot(for: workspace) updates["splitZoomedAfterToggle"] = workspace.bonsplitController.isSplitZoomed ? "true" : "false" @@ -9497,7 +9495,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent @MainActor func evaluate() { - guard !resolved, let currentWorkspace = self.tabManager?.selectedWorkspace else { return } + guard !resolved, let currentWorkspace = (tabManager ?? self.tabManager)?.selectedWorkspace else { return } let (updates, settled) = snapshot(for: currentWorkspace) guard settled else { return } finish(with: updates) @@ -9532,7 +9530,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in guard let self else { return } Task { @MainActor in - guard !resolved, let currentWorkspace = self.tabManager?.selectedWorkspace else { return } + guard !resolved, let currentWorkspace = (tabManager ?? self.tabManager)?.selectedWorkspace else { return } finish(with: snapshot(for: currentWorkspace).0) } } @@ -11243,9 +11241,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } if matchConfiguredShortcut(event: event, action: .toggleSplitZoom) { - _ = tabManager?.toggleFocusedSplitZoom() + let routedManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager + _ = routedManager?.toggleFocusedSplitZoom() #if DEBUG - recordGotoSplitZoomIfNeeded() + recordGotoSplitZoomIfNeeded(tabManager: routedManager) #endif return true } diff --git a/Sources/KeyboardShortcutSettings.swift b/Sources/KeyboardShortcutSettings.swift index 519a29d91a20..fad5c6a9d1d9 100644 --- a/Sources/KeyboardShortcutSettings.swift +++ b/Sources/KeyboardShortcutSettings.swift @@ -1914,7 +1914,7 @@ struct StoredShortcut: Codable, Equatable, Hashable { } var isUnbound: Bool { - key.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + key.isEmpty } var firstStroke: ShortcutStroke { diff --git a/Sources/Panels/CmuxWebView.swift b/Sources/Panels/CmuxWebView.swift index 0b0151012aa2..77f2f45cd0b6 100644 --- a/Sources/Panels/CmuxWebView.swift +++ b/Sources/Panels/CmuxWebView.swift @@ -591,22 +591,22 @@ final class CmuxWebView: WKWebView { extra: "handled=\(handled ? 1 : 0)" ) } + func finish(_ result: Bool) -> Bool { + handled = result + return result + } +#else + func finish(_ result: Bool) -> Bool { result } #endif if event.keyCode == 36 || event.keyCode == 76 { - // Always bypass app/menu key-equivalent routing for Return/Enter so WebKit - // receives the keyDown path used by form submission handlers. - return false + return finish(AppDelegate.shared?.handleBrowserSurfaceKeyEquivalent(event) == true) } let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) // Menu/app shortcut routing is only needed for Command equivalents // (New Tab, Close Tab, tab switching, split commands, etc). guard flags.contains(.command) else { - let result = super.performKeyEquivalent(with: event) -#if DEBUG - handled = result -#endif - return result + return finish(super.performKeyEquivalent(with: event)) } if Self.isPasteAsPlainTextCommandEquivalent(event) { @@ -619,10 +619,7 @@ final class CmuxWebView: WKWebView { if result { lastPasteAsPlainTextPerformKeyEventTimestamp = nil } -#if DEBUG - handled = result -#endif - return result + return finish(result) } var replayedBrowserFindShortcutIntoWebContent = false @@ -633,34 +630,21 @@ final class CmuxWebView: WKWebView { ) { replayedBrowserFindShortcutIntoWebContent = true let result = super.performKeyEquivalent(with: event) -#if DEBUG - handled = result -#endif if result { - return true + return finish(true) } } if !shouldRouteCommandEquivalentDirectlyToMainMenu(event) { - let result = super.performKeyEquivalent(with: event) -#if DEBUG - handled = result -#endif - return result + return finish(super.performKeyEquivalent(with: event)) } if AppDelegate.shared?.handleBrowserSurfaceKeyEquivalentBeforeMainMenu(event) == true { -#if DEBUG - handled = true -#endif - return true + return finish(true) } if let menu = NSApp.mainMenu, menu.performKeyEquivalent(with: event) { -#if DEBUG - handled = true -#endif - return true + return finish(true) } let result: Bool @@ -671,10 +655,7 @@ final class CmuxWebView: WKWebView { } else { result = super.performKeyEquivalent(with: event) } -#if DEBUG - handled = result -#endif - return result + return finish(result) } override func keyDown(with event: NSEvent) { diff --git a/cmuxTests/AppDelegateEqualizeSplitsShortcutTests.swift b/cmuxTests/AppDelegateEqualizeSplitsShortcutTests.swift index 000743530329..302525c1da99 100644 --- a/cmuxTests/AppDelegateEqualizeSplitsShortcutTests.swift +++ b/cmuxTests/AppDelegateEqualizeSplitsShortcutTests.swift @@ -10,6 +10,57 @@ import XCTest @MainActor final class AppDelegateEqualizeSplitsShortcutTests: XCTestCase { + func testCmdShiftReturnFocusedBrowserTogglesSplitZoom() { + withTemporaryShortcut(action: .toggleSplitZoom) { + guard let appDelegate = AppDelegate.shared else { + XCTFail("Expected AppDelegate.shared") + return + } + + let windowId = appDelegate.createMainWindow() + defer { closeWindow(withId: windowId) } + + guard let window = window(withId: windowId), + let manager = appDelegate.tabManagerFor(windowId: windowId), + let workspace = manager.selectedWorkspace, + let browserPanelId = manager.openBrowser(inWorkspace: workspace.id, preferSplitRight: true), + let browserPanel = workspace.browserPanel(for: browserPanelId), + let event = makeKeyDownEvent(key: "\r", modifiers: [.command, .shift], keyCode: 36, windowNumber: window.windowNumber) else { + XCTFail("Expected focused browser panel and Cmd+Shift+Return event") + return + } + + workspace.focusPanel(browserPanel.id) + XCTAssertEqual(workspace.focusedPanelId, browserPanel.id) + XCTAssertFalse(workspace.bonsplitController.isSplitZoomed) + + var didAttachForTest = false + if browserPanel.webView.superview == nil { + browserPanel.webView.frame = window.contentView?.bounds ?? .zero + window.contentView?.addSubview(browserPanel.webView) + didAttachForTest = true + } + defer { + if didAttachForTest { browserPanel.webView.removeFromSuperview() } + } + + window.makeKeyAndOrderFront(nil) + XCTAssertTrue(window.makeFirstResponder(browserPanel.webView)) + XCTAssertTrue(KeyboardShortcutSettings.shortcut(for: .toggleSplitZoom).matches(event: event)) + +#if DEBUG + XCTAssertTrue(appDelegate.debugHandleShortcutMonitorEvent(event: event)) + XCTAssertTrue(workspace.bonsplitController.isSplitZoomed) + XCTAssertTrue(workspace.clearSplitZoom()) +#else + XCTFail("debugHandleShortcutMonitorEvent is only available in DEBUG") +#endif + + XCTAssertTrue(browserPanel.webView.performKeyEquivalent(with: event)) + XCTAssertTrue(workspace.bonsplitController.isSplitZoomed) + } + } + func testConfiguredEqualizeSplitsShortcutBalancesWorkspaceDividers() { guard let appDelegate = AppDelegate.shared else { XCTFail("Expected AppDelegate.shared") @@ -149,6 +200,24 @@ final class AppDelegateEqualizeSplitsShortcutTests: XCTestCase { ) } + private func withTemporaryShortcut( + action: KeyboardShortcutSettings.Action, + shortcut: StoredShortcut? = nil, + _ body: () -> Void + ) { + let hadPersistedShortcut = UserDefaults.standard.object(forKey: action.defaultsKey) != nil + let originalShortcut = KeyboardShortcutSettings.shortcut(for: action) + defer { + if hadPersistedShortcut { + KeyboardShortcutSettings.setShortcut(originalShortcut, for: action) + } else { + KeyboardShortcutSettings.resetShortcut(for: action) + } + } + KeyboardShortcutSettings.setShortcut(shortcut ?? action.defaultShortcut, for: action) + body() + } + private func window(withId windowId: UUID) -> NSWindow? { let identifier = "cmux.main.\(windowId.uuidString)" return NSApp.windows.first(where: { $0.identifier?.rawValue == identifier })