Skip to content

Bring tooltips back on macOS 26 beta + show accept key as a keycap#350

Merged
FuJacob merged 1 commit into
mainfrom
fix/visible-tooltips-and-keycap-label
May 28, 2026
Merged

Bring tooltips back on macOS 26 beta + show accept key as a keycap#350
FuJacob merged 1 commit into
mainfrom
fix/visible-tooltips-and-keycap-label

Conversation

@FuJacob
Copy link
Copy Markdown
Owner

@FuJacob FuJacob commented May 28, 2026

Summary

Two fixes to the Settings + menu-bar surface, both reported in one ticket:

  1. The tooltip PR (Add tooltips across Settings and the menu-bar quick controls #313) sprinkled 40+ `.help(:)` modifiers across Settings, the menu-bar panel, and the rule/language editors — but on macOS 26 beta SwiftUI's tooltip bridge silently stopped firing for LSUIElement (menu-bar) apps, so none of those tooltips were ever visible. AppKit's own tooltip subsystem still works fine, so this PR adds a thin `.cotabbyHelp(:)` modifier that calls `.help(:)` (for accessibility) and overlays a click-through `NSViewRepresentable` that sets `NSView.toolTip` directly. All 44 `.help(` call sites were updated. When SwiftUI's bridge is fixed in a later macOS update, the underlying `.help(:)` will already be wired up and the overlay becomes a harmless no-op.

  2. The 'Show Accept Hint' toggle's label was vague — it didn't say what the hint shows. Replaced with a live label that renders the user's actual accept key as an inline keycap pill: 'Show [Tab] Key Hint' (or '[⌥Tab]', '[`]', etc., depending on what the user bound). Picks up rebinds automatically because it reads `suggestionSettings.acceptanceKeyLabel` directly.

Validation

  • `swiftlint lint --quiet` → exit 0
  • `xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build` → ** BUILD SUCCEEDED **
  • `xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build-for-testing` → ** TEST BUILD SUCCEEDED **
  • `xcodegen generate` regenerated the project to pick up the new `TooltipSupport.swift`.

UI verification recommended: open Settings on macOS 26 beta and hover any control with a tooltip — pill should appear after ~1.5 s. Repeat in the menu-bar panel. Rebind the accept key from Settings and confirm the new key glyph appears in the 'Show ... Key Hint' label without restarting.

Linked issues

Refs #313.

Risk / rollout notes

  • The click-through `NSView` overlay returns `nil` from `hitTest`, so it must not intercept clicks meant for the wrapped control. Worth a quick smoke on the toggles, the engine picker, and the keybind recorder to confirm interaction still feels normal.
  • If we ever drop macOS 26 beta support and SwiftUI tooltips return, `.cotabbyHelp(:)` can be reduced back to just `.help(:)` with a one-line edit in `TooltipSupport.swift`.

Greptile Summary

This PR fixes two issues on macOS 26 beta: it restores tooltips in the Settings window and menu-bar panel (where SwiftUI's tooltip bridge silently stopped working for LSUIElement apps) by introducing a thin cotabbyHelp(_:) modifier that layers an AppKit NSView.toolTip overlay over every existing .help() call, and it replaces the static "Show Accept Hint" toggle label with a live keycap pill that displays the user's currently bound acceptance key.

  • TooltipSupport.swift (new): Defines CotabbyTooltipModifier, which chains .help() for accessibility and overlays a ClickThroughTooltipView (hitTest → nil) whose toolTip property drives AppKit's NSToolTipManager directly. All 44 existing call sites are updated to .cotabbyHelp().
  • SettingsView.swift: The "Show Accept Hint" toggle label is rebuilt as an HStack with an inline rounded-rectangle keycap pill reading suggestionSettings.acceptanceKeyLabel; rebinds are reflected immediately without restart.

Confidence Score: 4/5

Safe to merge; the AppKit overlay is click-through by design and the tooltip workaround is well-scoped to the UI layer.

The ClickThroughTooltipView + toolTip approach is sound — AppKit tooltip tracking is independent of hit testing, so hitTest → nil correctly lets clicks pass through while still triggering tooltip popovers. The only non-trivial concerns are both non-blocking: when the acceptance key is cleared, the keycap pill reads "None" (producing "Show [None] Key Hint"), and once a future macOS ships with a fixed SwiftUI tooltip bridge, both .help() and the AppKit tracking area will be active simultaneously, risking duplicate tooltips until the workaround is removed.

Cotabby/UI/TooltipSupport.swift warrants a second look for the dual-tooltip scenario on future macOS; Cotabby/UI/SettingsView.swift's keycap pill is worth smoke-testing with the key unbound.

Important Files Changed

Filename Overview
Cotabby/UI/TooltipSupport.swift New file introducing cotabbyHelp(_:) — a thin AppKit-backed tooltip workaround for macOS 26 beta. Implementation is clean; minor concern that both SwiftUI and AppKit tooltip systems will fire simultaneously on a future OS where the bridge is fixed.
Cotabby/UI/SettingsView.swift All .help() calls replaced with .cotabbyHelp(); "Show Accept Hint" toggle updated to show a live keycap pill. When the acceptance key is cleared, the pill displays "None", producing a slightly odd "Show [None] Key Hint" label.
Cotabby/UI/MenuBarView.swift Mechanical .help().cotabbyHelp() replacements throughout; no logic changes.
Cotabby/UI/CustomRulesEditor.swift Two .help().cotabbyHelp() replacements; no other changes.
Cotabby/UI/LanguageTagsEditor.swift Two .help().cotabbyHelp() replacements; no other changes.
Cotabby.xcodeproj/project.pbxproj Adds TooltipSupport.swift to the Xcode project sources; standard xcodegen output.

Sequence Diagram

sequenceDiagram
    participant SW as SwiftUI View
    participant CM as CotabbyTooltipModifier
    participant OV as TooltipOverlayView (NSViewRepresentable)
    participant AK as ClickThroughTooltipView (NSView)
    participant AM as AppKit NSToolTipManager
    participant ACC as Accessibility (.help)

    SW->>CM: .cotabbyHelp("text")
    CM->>ACC: .help("text") — wires up VoiceOver
    CM->>OV: .overlay(TooltipOverlayView)
    OV->>AK: "makeNSView → toolTip = "text""
    AK->>AM: registers tooltip tracking area
    Note over AK: hitTest → nil (click-through)
    AM-->>SW: tooltip popover on hover (AppKit path)

    Note over SW,ACC: On future macOS where SwiftUI bridge is fixed,<br/>both .help() and AppKit tracking area are active
Loading

Fix All in Codex Fix All in Claude Code

Reviews (1): Last reviewed commit: "Bring tooltips back on macOS 26 beta + s..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Two changes on the same Settings/menu-bar surface:

1. SwiftUI's .help(_:) silently stopped rendering tooltips for LSUIElement
   apps on the macOS 26 beta, so every tooltip we added in #313 was
   invisible. Wrap a click-through NSViewRepresentable that sets
   NSView.toolTip directly (AppKit's tooltip subsystem still works there)
   and expose it as .cotabbyHelp(_:). Replace all 44 .help( call sites
   in SettingsView, MenuBarView, CustomRulesEditor, and LanguageTagsEditor.
   Still calls .help(_:) underneath so accessibility help stays wired up
   and the SwiftUI path 'just starts working' again on a future macOS
   update.

2. Rename 'Show Accept Hint' to a live label that renders the user's
   actual accept key as an inline keycap pill — 'Show [Tab] Key Hint',
   'Show [⌥Tab] Key Hint', etc. The static name was vague about what
   the hint actually shows.
@FuJacob FuJacob merged commit a7616df into main May 28, 2026
4 checks passed
@FuJacob FuJacob deleted the fix/visible-tooltips-and-keycap-label branch May 28, 2026 09:20
Comment on lines +151 to +165
Toggle(isOn: showAcceptanceHintBinding) {
HStack(spacing: 4) {
Text("Show")
Text(suggestionSettings.acceptanceKeyLabel)
.font(.system(.body, design: .rounded).weight(.semibold))
.padding(.horizontal, 5)
.padding(.vertical, 1)
.background(
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(.quaternary)
)
Text("Key Hint")
}
}
.cotabbyHelp("Show a small label near the ghost text reminding you which key accepts it.")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Keycap pill shows "None" when acceptance key is disabled

When the user clears the word-by-word acceptance key, acceptanceKeyLabel becomes "None" (the disabledKeyLabel constant), so the toggle label renders as "Show [None] Key Hint". This is accurate but may read oddly — a user who has unbound the key sees a hint referring to a non-existent binding. Consider falling back to a plain "Show Key Hint" label (no pill) when acceptanceKeyCode == SuggestionSettingsModel.disabledKeyCode.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

Comment on lines +25 to +29
func body(content: Content) -> some View {
content
.help(text)
.overlay(TooltipOverlayView(text: text))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate tooltip risk on future macOS where SwiftUI's bridge is repaired

Both .help(text) and the NSView.toolTip tracking area will be active simultaneously. On a macOS version that fixes the bridge, two separate tooltip managers may each fire for the same hover, producing a double tooltip. The PR notes this will "become a harmless no-op", but that assumes only one system fires — which isn't guaranteed. Adding a version gate (e.g., if #unavailable(macOS X.Y)) around the overlay would prevent the double-fire once a fixed OS ships and makes the intended cleanup path explicit.

Suggested change
func body(content: Content) -> some View {
content
.help(text)
.overlay(TooltipOverlayView(text: text))
}
func body(content: Content) -> some View {
// On macOS 26 beta SwiftUI's tooltip bridge is broken for LSUIElement apps (issue #313).
// Overlay an AppKit NSView so the tooltip is still shown via AppKit's own subsystem.
// When SwiftUI's bridge is fixed, gate the overlay out with `if #unavailable(macOS X.Y)`
// to avoid both systems firing simultaneously.
content
.help(text)
.overlay(TooltipOverlayView(text: text))
}

Fix in Codex Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant