Fix tooltip tracking lifetime and shortcut lag

This commit is contained in:
austinpower1258 2026-03-07 01:46:02 -08:00
parent 4de975e6a4
commit 9e8a401e3c
12 changed files with 139 additions and 104 deletions

View file

@ -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 {

View file

@ -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)
}

View file

@ -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)

View file

@ -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)

View file

@ -4688,6 +4688,9 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
if let windowObserver {
NotificationCenter.default.removeObserver(windowObserver)
}
if let trackingArea {
removeTrackingArea(trackingArea)
}
terminalSurface = nil
}

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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 <combo>"
}
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 <combo>"
}
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

View file

@ -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 {

View file

@ -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")
}

View file

@ -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)

2
vendor/bonsplit vendored

@ -1 +1 @@
Subproject commit 89a4fd1288a706ae4b766f323191d6570b7123aa
Subproject commit c5b3dd4cd314f7452bd27ffacd00ebeb19d96d17