diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index fc516c72f..56946eeaf 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -156,9 +156,7 @@ class AppDelegate: NSObject, private let appIconUpdater = AppIconUpdater() @MainActor private lazy var menuShortcutManager = Ghostty.MenuShortcutManager() - /// A signal to trigger restoration for shortcuts of registered menus, like copy and paste - let restoreShortcutsRequest = PassthroughSubject() - /// A throttle observer for the signal above + /// A observer for resetting menu shortcuts based on firstResponder private var resetMenuObserver: Any? override init() { @@ -215,7 +213,7 @@ class AppDelegate: NSObject, toggleSecureInput(self) } - saveRestorableMenuItems() + setupRestorableMenuItems() // Initial config loading ghosttyConfigDidChange(config: ghostty.config) @@ -626,7 +624,6 @@ class AppDelegate: NSObject, @objc private func windowDidBecomeKey(_ notification: Notification) { let window = notification.object as? NSWindow syncFloatOnTopMenu(window) - restoreRegisteredMenusIfNeeded(for: window) } @objc private func quickTerminalDidChangeVisibility(_ notification: Notification) { @@ -1139,10 +1136,10 @@ extension AppDelegate { self.menuFindParent?.setImageIfDesired(systemSymbolName: "text.page.badge.magnifyingglass") } - /// Save restorable menu items for later + /// Save restorable menu items and restore them whenever the first responder is not a terminal surface /// /// If you plan to add more items here, make sure you add the default shortcut in MainMenu.xib - @MainActor private func saveRestorableMenuItems() { + @MainActor private func setupRestorableMenuItems() { [ (menuUndo, "undo"), (menuRedo, "redo"), @@ -1152,15 +1149,34 @@ extension AppDelegate { (menuSelectAll, "select_all"), ].forEach(menuShortcutManager.saveRestorableMenuItem(_:action:)) - resetMenuObserver = restoreShortcutsRequest - .throttle(for: .seconds(0.5), scheduler: DispatchQueue.main, latest: false) - .sink { [weak self] in + // The reason we're using publisher here instead of `windowDidBecomeKey` + // is that we want reset the observer whenever the key window is changed. + // + // I had a case where help menu's window becomes key with terminal window opened then moving away from help menu doesn't trigger another notification. + resetMenuObserver = NSApp.publisher(for: \.keyWindow) + .flatMap { + $0?.publisher(for: \.firstResponder, options: [.new]) + .map { $0 is Ghostty.SurfaceView } + .eraseToAnyPublisher() ?? + Just(true).eraseToAnyPublisher() + // When the keyWindow is nil, we want to re-sync them + } + .removeDuplicates() + .sink { [weak self] isSurfaceFocused in guard let self else { return } - // We need to check the first responder again, - // because a request could be fired multiple time in a short time. - // It's hard for us to filter them out, - // but firstResponder will be updated correctly - restoreRegisteredMenusIfNeeded(for: nil) + if isSurfaceFocused { + menuShortcutManager.reSyncRestoredMenuShortcuts(config: ghostty.config) + } else { + // Restore for non terminal responders, like: + // 1. About Window + // 2. Alert modal + // 3. ConfigurationErrors + // 4. InlineTitleEditor + // 5. SearchOverlay + // 6. CommandPalette + // 7. Help search + menuShortcutManager.restoreMenuShortcuts() + } } } @@ -1233,9 +1249,6 @@ extension AppDelegate { // // syncMenuShortcut(config, action: "toggle_fullscreen", menuItem: self.menuToggleFullScreen) - // Restore the restorable menu shortcuts if needed - restoreShortcutsRequest.send() - // Dock menu reloadDockMenu() } @@ -1244,34 +1257,6 @@ extension AppDelegate { menuShortcutManager.syncMenuShortcut(config, action: action, menuItem: menuItem) } - @MainActor private func restoreRegisteredMenusIfNeeded(for window: NSWindow?) { - guard let window = window ?? NSApp.keyWindow else { - return - } - guard - window is TerminalWindow || window is QuickTerminalWindow, - window.firstResponder is Ghostty.SurfaceView - else { - // Restore for: - // 1. About Window - // 2. Alert modal - // 3. ConfigurationErrors - // 4. InlineTitleEditor - // 5. SearchOverlay - // 6. CommandPalette - // 7. Help search - menuShortcutManager.restoreMenuShortcuts() - return - } - // If it's a terminal window with surface focused, - // then we re-sync the menu shortcuts - menuShortcutManager.reSyncRestoredMenuShortcuts(config: ghostty.config) - - // For cases like after closing About which is the last window, - // the restore shortcuts will stays there and most of them should be disabled or no-op. - // The next time a terminal window is open, the shortcuts will be updated - } - @MainActor func performGhosttyBindingMenuKeyEquivalent(with event: NSEvent) -> Bool { menuShortcutManager.performGhosttyBindingMenuKeyEquivalent(with: event) } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index adb7334cc..540f31ac3 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -786,9 +786,6 @@ extension Ghostty { override func becomeFirstResponder() -> Bool { let result = super.becomeFirstResponder() if result { focusDidChange(true) } - (NSApp.delegate as? AppDelegate)? - .restoreShortcutsRequest - .send() return result } @@ -797,9 +794,6 @@ extension Ghostty { // We sometimes call this manually (see SplitView) as a way to force us to // yield our focus state. if result { focusDidChange(false) } - (NSApp.delegate as? AppDelegate)? - .restoreShortcutsRequest - .send() return result }