diff --git a/Sources/BrowserWindowPortal.swift b/Sources/BrowserWindowPortal.swift index 868d004b..597efdb4 100644 --- a/Sources/BrowserWindowPortal.swift +++ b/Sources/BrowserWindowPortal.swift @@ -70,6 +70,13 @@ final class WindowBrowserHostView: NSView { private var activeDividerCursorKind: DividerCursorKind? private var hostedInspectorDividerDrag: HostedInspectorDividerDragState? + deinit { + if let trackingArea { + removeTrackingArea(trackingArea) + } + clearActiveDividerCursor(restoreArrow: false) + } + #if DEBUG private static func shouldLogPointerEvent(_ event: NSEvent?) -> Bool { switch event?.type { diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 2e086282..f41aea13 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -2209,7 +2209,7 @@ struct ContentView: View { ) } .buttonStyle(.plain) - .help(String(localized: "workspace.page.new.tooltip", defaultValue: "New Page")) + .safeHelp(String(localized: "workspace.page.new.tooltip", defaultValue: "New Page")) .accessibilityIdentifier("titlebarPageNewButton") .transition(.opacity) } @@ -2282,7 +2282,7 @@ struct ContentView: View { ) } .buttonStyle(.plain) - .help(page.title) + .safeHelp(page.title) .accessibilityIdentifier(titlebarPageButtonAccessibilityIdentifier(pageId: page.id, isActive: isActive)) HStack(spacing: 4) { @@ -9091,7 +9091,7 @@ private struct SidebarHelpMenuButton: View { helpPopover } .accessibilityElement(children: .ignore) - .help(helpTitle) + .safeHelp(helpTitle) .accessibilityLabel(helpTitle) .accessibilityIdentifier("SidebarHelpMenuButton") } @@ -9717,7 +9717,7 @@ private struct TabItemView: View { .foregroundColor(activeSecondaryColor(0.7)) } .buttonStyle(.plain) - .help(KeyboardShortcutSettings.Action.closeWorkspace.tooltip(closeWorkspaceTooltip)) + .safeHelp(KeyboardShortcutSettings.Action.closeWorkspace.tooltip(closeWorkspaceTooltip)) .frame(width: 16, height: 16, alignment: .center) .opacity(showCloseButton && !showsWorkspaceShortcutHint ? 1 : 0) .allowsHitTesting(showCloseButton && !showsWorkspaceShortcutHint) @@ -9890,7 +9890,7 @@ private struct TabItemView: View { .foregroundColor(pullRequestForegroundColor) } .buttonStyle(.plain) - .help(String(localized: "sidebar.pullRequest.openTooltip", defaultValue: "Open \(pullRequest.label) #\(pullRequest.number)")) + .safeHelp(String(localized: "sidebar.pullRequest.openTooltip", defaultValue: "Open \(pullRequest.label) #\(pullRequest.number)")) } } } @@ -10757,7 +10757,7 @@ private struct SidebarMetadataRows: View { .frame(maxWidth: .infinity, alignment: .leading) } } - .help(helpText) + .safeHelp(helpText) } private var activeSecondaryTextColor: Color { @@ -10797,7 +10797,7 @@ private struct SidebarMetadataEntryRow: View { rowContent(underlined: true) } .buttonStyle(.plain) - .help(url.absoluteString) + .safeHelp(url.absoluteString) } else { rowContent(underlined: false) .contentShape(Rectangle()) @@ -11972,7 +11972,7 @@ private struct DraggableFolderIcon: View { var body: some View { DraggableFolderIconRepresentable(directory: directory) .frame(width: 16, height: 16) - .help(String(localized: "sidebar.folderIcon.dragHint", defaultValue: "Drag to open in Finder or another app")) + .safeHelp(String(localized: "sidebar.folderIcon.dragHint", defaultValue: "Drag to open in Finder or another app")) .onTapGesture(count: 2) { NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: directory) } diff --git a/Sources/Find/BrowserSearchOverlay.swift b/Sources/Find/BrowserSearchOverlay.swift index 9a022e5f..b7f874ea 100644 --- a/Sources/Find/BrowserSearchOverlay.swift +++ b/Sources/Find/BrowserSearchOverlay.swift @@ -73,7 +73,7 @@ struct BrowserSearchOverlay: View { Image(systemName: "chevron.up") } .buttonStyle(SearchButtonStyle()) - .help("Next match (Return)") + .safeHelp("Next match (Return)") Button(action: { #if DEBUG @@ -84,7 +84,7 @@ struct BrowserSearchOverlay: View { Image(systemName: "chevron.down") } .buttonStyle(SearchButtonStyle()) - .help("Previous match (Shift+Return)") + .safeHelp("Previous match (Shift+Return)") Button(action: { #if DEBUG @@ -95,7 +95,7 @@ struct BrowserSearchOverlay: View { Image(systemName: "xmark") } .buttonStyle(SearchButtonStyle()) - .help("Close (Esc)") + .safeHelp("Close (Esc)") } .padding(8) .background(.background) diff --git a/Sources/Find/SurfaceSearchOverlay.swift b/Sources/Find/SurfaceSearchOverlay.swift index 0efc3d50..aee272e9 100644 --- a/Sources/Find/SurfaceSearchOverlay.swift +++ b/Sources/Find/SurfaceSearchOverlay.swift @@ -88,7 +88,7 @@ struct SurfaceSearchOverlay: View { Image(systemName: "chevron.up") } .buttonStyle(SearchButtonStyle()) - .help(String(localized: "search.nextMatch.help", defaultValue: "Next match (Return)")) + .safeHelp(String(localized: "search.nextMatch.help", defaultValue: "Next match (Return)")) Button(action: { #if DEBUG @@ -99,7 +99,7 @@ struct SurfaceSearchOverlay: View { Image(systemName: "chevron.down") } .buttonStyle(SearchButtonStyle()) - .help(String(localized: "search.previousMatch.help", defaultValue: "Previous match (Shift+Return)")) + .safeHelp(String(localized: "search.previousMatch.help", defaultValue: "Previous match (Shift+Return)")) Button(action: { #if DEBUG @@ -110,7 +110,7 @@ struct SurfaceSearchOverlay: View { Image(systemName: "xmark") } .buttonStyle(SearchButtonStyle()) - .help(String(localized: "search.close.help", defaultValue: "Close (Esc)")) + .safeHelp(String(localized: "search.close.help", defaultValue: "Close (Esc)")) } .padding(8) .background(.background) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 278a9111..1e654241 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -4688,6 +4688,9 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { if let windowObserver { NotificationCenter.default.removeObserver(windowObserver) } + if let trackingArea { + removeTrackingArea(trackingArea) + } terminalSurface = nil } diff --git a/Sources/NotificationsPage.swift b/Sources/NotificationsPage.swift index 370477f0..096dce20 100644 --- a/Sources/NotificationsPage.swift +++ b/Sources/NotificationsPage.swift @@ -1,3 +1,4 @@ +import Bonsplit import SwiftUI struct NotificationsPage: View { @@ -113,7 +114,7 @@ struct NotificationsPage: View { } .buttonStyle(.bordered) .keyboardShortcut(key, modifiers: jumpToUnreadShortcut.eventModifiers) - .help(KeyboardShortcutSettings.Action.jumpToUnread.tooltip(String(localized: "notifications.jumpToLatestUnread", defaultValue: "Jump to Latest Unread"))) + .safeHelp(KeyboardShortcutSettings.Action.jumpToUnread.tooltip(String(localized: "notifications.jumpToLatestUnread", defaultValue: "Jump to Latest Unread"))) .disabled(!hasUnreadNotifications) } else { Button(action: { @@ -125,7 +126,7 @@ struct NotificationsPage: View { } } .buttonStyle(.bordered) - .help(KeyboardShortcutSettings.Action.jumpToUnread.tooltip(String(localized: "notifications.jumpToLatestUnread", defaultValue: "Jump to Latest Unread"))) + .safeHelp(KeyboardShortcutSettings.Action.jumpToUnread.tooltip(String(localized: "notifications.jumpToLatestUnread", defaultValue: "Jump to Latest Unread"))) .disabled(!hasUnreadNotifications) } } diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index cbb7aaff..371b239a 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -538,7 +538,7 @@ struct BrowserPanelView: View { .buttonStyle(OmnibarAddressButtonStyle()) .disabled(!panel.canGoBack) .opacity(panel.canGoBack ? 1.0 : 0.4) - .help(String(localized: "browser.goBack", defaultValue: "Go Back")) + .safeHelp(String(localized: "browser.goBack", defaultValue: "Go Back")) Button(action: { #if DEBUG @@ -554,7 +554,7 @@ struct BrowserPanelView: View { .buttonStyle(OmnibarAddressButtonStyle()) .disabled(!panel.canGoForward) .opacity(panel.canGoForward ? 1.0 : 0.4) - .help(String(localized: "browser.goForward", defaultValue: "Go Forward")) + .safeHelp(String(localized: "browser.goForward", defaultValue: "Go Forward")) Button(action: { if panel.isLoading { @@ -575,7 +575,7 @@ struct BrowserPanelView: View { .contentShape(Rectangle()) } .buttonStyle(OmnibarAddressButtonStyle()) - .help(panel.isLoading ? String(localized: "browser.stop", defaultValue: "Stop") : String(localized: "browser.reload", defaultValue: "Reload")) + .safeHelp(panel.isLoading ? String(localized: "browser.stop", defaultValue: "Stop") : String(localized: "browser.reload", defaultValue: "Reload")) if panel.isDownloading { HStack(spacing: 4) { @@ -586,7 +586,7 @@ struct BrowserPanelView: View { .foregroundStyle(.secondary) } .padding(.leading, 6) - .help(String(localized: "browser.downloadInProgress", defaultValue: "Download in progress")) + .safeHelp(String(localized: "browser.downloadInProgress", defaultValue: "Download in progress")) } } } @@ -604,7 +604,7 @@ struct BrowserPanelView: View { } .buttonStyle(OmnibarAddressButtonStyle()) .frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center) - .help(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.tooltip(String(localized: "browser.toggleDevTools", defaultValue: "Toggle Developer Tools"))) + .safeHelp(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.tooltip(String(localized: "browser.toggleDevTools", defaultValue: "Toggle Developer Tools"))) .accessibilityIdentifier("BrowserToggleDevToolsButton") } @@ -624,7 +624,7 @@ struct BrowserPanelView: View { .popover(isPresented: $isBrowserThemeMenuPresented, arrowEdge: .bottom) { browserThemeModePopover } - .help("Browser Theme: \(browserThemeMode.displayName)") + .safeHelp("Browser Theme: \(browserThemeMode.displayName)") .accessibilityIdentifier("BrowserThemeModeButton") } @@ -3139,6 +3139,13 @@ struct WebViewRepresentable: NSViewRepresentable { private var hasLoggedMissingHostedInspectorCandidate = false #endif + deinit { + if let trackingArea { + removeTrackingArea(trackingArea) + } + clearActiveDividerCursor(restoreArrow: false) + } + #if DEBUG private static func shouldLogPointerEvent(_ event: NSEvent?) -> Bool { switch event?.type { diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index 901aefae..3657eefc 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -10238,81 +10238,91 @@ class TerminalController { return "OK" } - private func simulateShortcut(_ args: String) -> String { - let combo = args.trimmingCharacters(in: .whitespacesAndNewlines) - guard !combo.isEmpty else { - return "ERROR: Usage: simulate_shortcut " - } - guard let parsed = parseShortcutCombo(combo) else { - return "ERROR: Invalid combo. Example: cmd+ctrl+h" - } + private func prepareWindowForSyntheticInput(_ window: NSWindow?) { + guard let window else { return } - // Stamp at socket-handler arrival so event.timestamp includes any wait - // before the main-thread event dispatch. - let requestTimestamp = ProcessInfo.processInfo.systemUptime - - var result = "ERROR: Failed to create event" - DispatchQueue.main.sync { - // Prefer the current active-tab-manager window so shortcut simulation stays - // scoped to the intended window even when NSApp.keyWindow is stale. - let targetWindow: NSWindow? = { - if let activeTabManager = self.tabManager, - let windowId = AppDelegate.shared?.windowId(for: activeTabManager), - let window = AppDelegate.shared?.mainWindow(for: windowId) { - return window - } - return NSApp.keyWindow - ?? NSApp.mainWindow - ?? NSApp.windows.first(where: { $0.isVisible }) - ?? NSApp.windows.first - }() - if let targetWindow { - NSApp.activate(ignoringOtherApps: true) - targetWindow.makeKeyAndOrderFront(nil) - } - let windowNumber = targetWindow?.windowNumber ?? 0 - guard let keyDownEvent = NSEvent.keyEvent( - with: .keyDown, - location: .zero, - modifierFlags: parsed.modifierFlags, - timestamp: requestTimestamp, - windowNumber: windowNumber, - context: nil, - characters: parsed.characters, - charactersIgnoringModifiers: parsed.charactersIgnoringModifiers, - isARepeat: false, - keyCode: parsed.keyCode - ) else { - result = "ERROR: NSEvent.keyEvent returned nil" - return - } - let keyUpEvent = NSEvent.keyEvent( - with: .keyUp, - location: .zero, - modifierFlags: parsed.modifierFlags, - timestamp: requestTimestamp + 0.0001, - windowNumber: windowNumber, - context: nil, - characters: parsed.characters, - charactersIgnoringModifiers: parsed.charactersIgnoringModifiers, - isARepeat: false, - keyCode: parsed.keyCode - ) - // Socket-driven shortcut simulation should reuse the exact same matching logic as the - // app-level shortcut monitor (so tests are hermetic), while still falling back to the - // normal responder chain for plain typing. - if let delegate = AppDelegate.shared, delegate.debugHandleCustomShortcut(event: keyDownEvent) { - result = "OK" - return - } - NSApp.sendEvent(keyDownEvent) - if let keyUpEvent { - NSApp.sendEvent(keyUpEvent) - } - result = "OK" - } - return result - } + // Keep socket-driven input simulation focused on the intended window without + // paying repeated activation/order-front costs for every synthetic key event. + if !NSApp.isActive { + NSApp.activate(ignoringOtherApps: true) + } + if !window.isKeyWindow || !window.isVisible { + window.makeKeyAndOrderFront(nil) + } + } + + private func simulateShortcut(_ args: String) -> String { + let combo = args.trimmingCharacters(in: .whitespacesAndNewlines) + guard !combo.isEmpty else { + return "ERROR: Usage: simulate_shortcut " + } + guard let parsed = parseShortcutCombo(combo) else { + return "ERROR: Invalid combo. Example: cmd+ctrl+h" + } + + // Stamp at socket-handler arrival so event.timestamp includes any wait + // before the main-thread event dispatch. + let requestTimestamp = ProcessInfo.processInfo.systemUptime + + var result = "ERROR: Failed to create event" + DispatchQueue.main.sync { + // Prefer the current active-tab-manager window so shortcut simulation stays + // scoped to the intended window even when NSApp.keyWindow is stale. + let targetWindow: NSWindow? = { + if let activeTabManager = self.tabManager, + let windowId = AppDelegate.shared?.windowId(for: activeTabManager), + let window = AppDelegate.shared?.mainWindow(for: windowId) { + return window + } + return NSApp.keyWindow + ?? NSApp.mainWindow + ?? NSApp.windows.first(where: { $0.isVisible }) + ?? NSApp.windows.first + }() + prepareWindowForSyntheticInput(targetWindow) + let windowNumber = targetWindow?.windowNumber ?? 0 + guard let keyDownEvent = NSEvent.keyEvent( + with: .keyDown, + location: .zero, + modifierFlags: parsed.modifierFlags, + timestamp: requestTimestamp, + windowNumber: windowNumber, + context: nil, + characters: parsed.characters, + charactersIgnoringModifiers: parsed.charactersIgnoringModifiers, + isARepeat: false, + keyCode: parsed.keyCode + ) else { + result = "ERROR: NSEvent.keyEvent returned nil" + return + } + let keyUpEvent = NSEvent.keyEvent( + with: .keyUp, + location: .zero, + modifierFlags: parsed.modifierFlags, + timestamp: requestTimestamp + 0.0001, + windowNumber: windowNumber, + context: nil, + characters: parsed.characters, + charactersIgnoringModifiers: parsed.charactersIgnoringModifiers, + isARepeat: false, + keyCode: parsed.keyCode + ) + // Socket-driven shortcut simulation should reuse the exact same matching logic as the + // app-level shortcut monitor (so tests are hermetic), while still falling back to the + // normal responder chain for plain typing. + if let delegate = AppDelegate.shared, delegate.debugHandleCustomShortcut(event: keyDownEvent) { + result = "OK" + return + } + NSApp.sendEvent(keyDownEvent) + if let keyUpEvent { + NSApp.sendEvent(keyUpEvent) + } + result = "OK" + } + return result + } private func activateApp() -> String { DispatchQueue.main.sync { @@ -10357,8 +10367,7 @@ class TerminalController { ?? NSApp.mainWindow ?? NSApp.windows.first(where: { $0.isVisible }) ?? NSApp.windows.first else { return } - NSApp.activate(ignoringOtherApps: true) - window.makeKeyAndOrderFront(nil) + prepareWindowForSyntheticInput(window) guard let fr = window.firstResponder else { result = "ERROR: No first responder" return diff --git a/Sources/TerminalWindowPortal.swift b/Sources/TerminalWindowPortal.swift index c6758791..80c0d2ba 100644 --- a/Sources/TerminalWindowPortal.swift +++ b/Sources/TerminalWindowPortal.swift @@ -54,6 +54,13 @@ final class WindowTerminalHostView: NSView { private var lastDragRouteSignature: String? #endif + deinit { + if let trackingArea { + removeTrackingArea(trackingArea) + } + clearActiveDividerCursor(restoreArrow: false) + } + override func viewDidMoveToWindow() { super.viewDidMoveToWindow() if window == nil { diff --git a/Sources/Update/UpdatePill.swift b/Sources/Update/UpdatePill.swift index f8d69aff..ed43c192 100644 --- a/Sources/Update/UpdatePill.swift +++ b/Sources/Update/UpdatePill.swift @@ -1,4 +1,5 @@ import AppKit +import Bonsplit import Foundation import SwiftUI @@ -54,7 +55,7 @@ struct UpdatePill: View { .contentShape(Capsule()) } .buttonStyle(.plain) - .help(model.text) + .safeHelp(model.text) .accessibilityLabel(model.text) .accessibilityIdentifier("UpdatePill") } diff --git a/Sources/Update/UpdateTitlebarAccessory.swift b/Sources/Update/UpdateTitlebarAccessory.swift index 3ec1362d..1e4795ac 100644 --- a/Sources/Update/UpdateTitlebarAccessory.swift +++ b/Sources/Update/UpdateTitlebarAccessory.swift @@ -273,7 +273,7 @@ struct TitlebarControlsView: View { } var body: some View { - // Force the `.help(...)` tooltips to re-evaluate when shortcuts are changed in settings. + // Force the `.safeHelp(...)` tooltips to re-evaluate when shortcuts are changed in settings. // (The titlebar controls don't otherwise re-render on UserDefaults changes.) let _ = shortcutRefreshTick let style = TitlebarControlsStyle(rawValue: styleRawValue) ?? .classic @@ -321,7 +321,7 @@ struct TitlebarControlsView: View { } .accessibilityIdentifier("titlebarControl.toggleSidebar") .accessibilityLabel(String(localized: "titlebar.sidebar.accessibilityLabel", defaultValue: "Toggle Sidebar")) - .help(KeyboardShortcutSettings.Action.toggleSidebar.tooltip(String(localized: "titlebar.sidebar.tooltip", defaultValue: "Show or hide the sidebar"))) + .safeHelp(KeyboardShortcutSettings.Action.toggleSidebar.tooltip(String(localized: "titlebar.sidebar.tooltip", defaultValue: "Show or hide the sidebar"))) TitlebarControlButton(config: config, action: { #if DEBUG @@ -348,7 +348,7 @@ struct TitlebarControlsView: View { .accessibilityIdentifier("titlebarControl.showNotifications") .background(NotificationsAnchorView { viewModel.notificationsAnchorView = $0 }) .accessibilityLabel(String(localized: "titlebar.notifications.accessibilityLabel", defaultValue: "Notifications")) - .help(KeyboardShortcutSettings.Action.showNotifications.tooltip(String(localized: "titlebar.notifications.tooltip", defaultValue: "Show notifications"))) + .safeHelp(KeyboardShortcutSettings.Action.showNotifications.tooltip(String(localized: "titlebar.notifications.tooltip", defaultValue: "Show notifications"))) TitlebarControlButton(config: config, action: { #if DEBUG @@ -360,7 +360,7 @@ struct TitlebarControlsView: View { } .accessibilityIdentifier("titlebarControl.newTab") .accessibilityLabel(String(localized: "titlebar.newWorkspace.accessibilityLabel", defaultValue: "New Workspace")) - .help(KeyboardShortcutSettings.Action.newTab.tooltip(String(localized: "titlebar.newWorkspace.tooltip", defaultValue: "New workspace"))) + .safeHelp(KeyboardShortcutSettings.Action.newTab.tooltip(String(localized: "titlebar.newWorkspace.tooltip", defaultValue: "New workspace"))) } let paddedContent = content.padding(config.groupPadding) diff --git a/vendor/bonsplit b/vendor/bonsplit index 89a4fd12..c5b3dd4c 160000 --- a/vendor/bonsplit +++ b/vendor/bonsplit @@ -1 +1 @@ -Subproject commit 89a4fd1288a706ae4b766f323191d6570b7123aa +Subproject commit c5b3dd4cd314f7452bd27ffacd00ebeb19d96d17