Merge b2835f2f7d into 629838b9bd
commit
79c93fb91e
|
|
@ -1,4 +1,5 @@
|
|||
import AppKit
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
import OSLog
|
||||
|
|
@ -39,6 +40,7 @@ class AppDelegate: NSObject,
|
|||
|
||||
@IBOutlet private var menuUndo: NSMenuItem?
|
||||
@IBOutlet private var menuRedo: NSMenuItem?
|
||||
@IBOutlet private var menuCut: NSMenuItem?
|
||||
@IBOutlet private var menuCopy: NSMenuItem?
|
||||
@IBOutlet private var menuPaste: NSMenuItem?
|
||||
@IBOutlet private var menuPasteSelection: NSMenuItem?
|
||||
|
|
@ -154,6 +156,8 @@ class AppDelegate: NSObject,
|
|||
private let appIconUpdater = AppIconUpdater()
|
||||
|
||||
@MainActor private lazy var menuShortcutManager = Ghostty.MenuShortcutManager()
|
||||
/// A observer for resetting menu shortcuts based on firstResponder
|
||||
private var resetMenuObserver: Any?
|
||||
|
||||
override init() {
|
||||
#if DEBUG
|
||||
|
|
@ -209,6 +213,8 @@ class AppDelegate: NSObject,
|
|||
toggleSecureInput(self)
|
||||
}
|
||||
|
||||
setupRestorableMenuItems()
|
||||
|
||||
// Initial config loading
|
||||
ghosttyConfigDidChange(config: ghostty.config)
|
||||
|
||||
|
|
@ -1128,6 +1134,50 @@ extension AppDelegate {
|
|||
self.menuFindParent?.setImageIfDesired(systemSymbolName: "text.page.badge.magnifyingglass")
|
||||
}
|
||||
|
||||
/// 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 setupRestorableMenuItems() {
|
||||
[
|
||||
(menuUndo, "undo"),
|
||||
(menuRedo, "redo"),
|
||||
(menuCut, nil),
|
||||
(menuCopy, "copy_to_clipboard"),
|
||||
(menuPaste, "paste_from_clipboard"),
|
||||
(menuSelectAll, "select_all"),
|
||||
].forEach(menuShortcutManager.saveRestorableMenuItem(_:action:))
|
||||
|
||||
// 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 }
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.
|
||||
@MainActor private func syncMenuShortcuts(_ config: Ghostty.Config) {
|
||||
guard ghostty.readiness == .ready else { return }
|
||||
|
|
@ -1152,6 +1202,7 @@ extension AppDelegate {
|
|||
|
||||
syncMenuShortcut(config, action: "undo", menuItem: self.menuUndo)
|
||||
syncMenuShortcut(config, action: "redo", menuItem: self.menuRedo)
|
||||
syncMenuShortcut(config, action: nil, menuItem: self.menuCut)
|
||||
syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy)
|
||||
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
|
||||
syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection)
|
||||
|
|
@ -1200,7 +1251,7 @@ extension AppDelegate {
|
|||
reloadDockMenu()
|
||||
}
|
||||
|
||||
@MainActor private func syncMenuShortcut(_ config: Ghostty.Config, action: String, menuItem: NSMenuItem?) {
|
||||
@MainActor private func syncMenuShortcut(_ config: Ghostty.Config, action: String?, menuItem: NSMenuItem?) {
|
||||
menuShortcutManager.syncMenuShortcut(config, action: action, menuItem: menuItem)
|
||||
}
|
||||
|
||||
|
|
@ -1285,7 +1336,8 @@ extension AppDelegate: NSMenuItemValidation {
|
|||
item.title = "Redo"
|
||||
}
|
||||
return undoManager.canRedo
|
||||
|
||||
case menuCut?.action:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
<outlet property="menuCloseWindow" destination="W5w-UZ-crk" id="6ff-BT-ENV"/>
|
||||
<outlet property="menuCommandPalette" destination="et6-de-Mh7" id="53t-cu-dm5"/>
|
||||
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
|
||||
<outlet property="menuCut" destination="H0A-Vb-3cx" id="fby-cu-YhH"/>
|
||||
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
|
||||
<outlet property="menuEqualizeSplits" destination="3gH-VD-vL9" id="SiZ-ce-FOF"/>
|
||||
<outlet property="menuFind" destination="nwE-0w-30h" id="idg-Nc-apE"/>
|
||||
|
|
@ -223,27 +224,28 @@
|
|||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="iU4-OB-ccf">
|
||||
<items>
|
||||
<menuItem title="Undo" id="r83-CV-syt">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="r83-CV-syt">
|
||||
<connections>
|
||||
<action selector="undo:" target="-1" id="jrW-j3-OZj"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" id="EX8-lB-4s7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="EX8-lB-4s7">
|
||||
<connections>
|
||||
<action selector="redo:" target="-1" id="7UK-Hj-s4O"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4O9-zO-zB9"/>
|
||||
<menuItem title="Copy" id="Jqf-pv-Zcu">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="H0A-Vb-3cx">
|
||||
<connections>
|
||||
<action selector="cut:" target="-1" id="iOR-ZB-8ZI"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="Jqf-pv-Zcu">
|
||||
<connections>
|
||||
<action selector="copy:" target="-1" id="B4F-hg-R4T"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" id="i27-pK-umN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="i27-pK-umN">
|
||||
<connections>
|
||||
<action selector="paste:" target="-1" id="ZKe-2B-mel"/>
|
||||
</connections>
|
||||
|
|
@ -254,8 +256,7 @@
|
|||
<action selector="pasteSelection:" target="-1" id="vo3-Rf-Udb"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" id="q2h-lq-e4r">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="q2h-lq-e4r">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="-1" id="0CH-Tp-7Ud"/>
|
||||
</connections>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,46 @@ extension Ghostty {
|
|||
/// If multiple items map to the same shortcut, the most recent one wins.
|
||||
private var menuItemsByShortcut: [MenuShortcutKey: Weak<NSMenuItem>] = [:]
|
||||
|
||||
/// Original shortcut configured in xib indexed by their action
|
||||
private var restorableMenuItemsByOriginalShortcut: [MenuShortcutKey: (Weak<NSMenuItem>, String?)] = [:]
|
||||
|
||||
private var menuItemsRestored = [(Weak<NSMenuItem>, String?)]()
|
||||
|
||||
func saveRestorableMenuItem(_ item: NSMenuItem?, action ghosttyAction: String?) {
|
||||
guard
|
||||
let item,
|
||||
let key = MenuShortcutKey(item)
|
||||
else {
|
||||
return
|
||||
}
|
||||
// Later registrations intentionally override earlier ones for the same key.
|
||||
restorableMenuItemsByOriginalShortcut[key] = (.init(item), ghosttyAction)
|
||||
}
|
||||
|
||||
/// Restore shortcuts for the items that are registered
|
||||
///
|
||||
/// - Important: the item is only restored when the current shortcut is empty
|
||||
func restoreMenuShortcuts() {
|
||||
menuItemsRestored.removeAll()
|
||||
for (key, item) in restorableMenuItemsByOriginalShortcut {
|
||||
if let menuItem = item.0.value, menuItem.keyEquivalent.isEmpty {
|
||||
menuItem.keyEquivalent = key.keyEquivalent
|
||||
menuItem.keyEquivalentModifierMask = key.modifierFlags
|
||||
menuItemsRestored.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-sync shortcuts for the items that are restored
|
||||
func reSyncRestoredMenuShortcuts(config: Ghostty.Config) {
|
||||
for item in menuItemsRestored {
|
||||
if let menuItem = item.0.value {
|
||||
syncMenuShortcut(config, action: item.1, menuItem: menuItem)
|
||||
}
|
||||
}
|
||||
menuItemsRestored.removeAll()
|
||||
}
|
||||
|
||||
/// Reset our shortcut index since we're about to rebuild all menu bindings.
|
||||
func reset() {
|
||||
menuItemsByShortcut.removeAll(keepingCapacity: true)
|
||||
|
|
|
|||
|
|
@ -791,11 +791,9 @@ extension Ghostty {
|
|||
|
||||
override func resignFirstResponder() -> Bool {
|
||||
let result = super.resignFirstResponder()
|
||||
|
||||
// We sometimes call this manually (see SplitView) as a way to force us to
|
||||
// yield our focus state.
|
||||
if result { focusDidChange(false) }
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue