Files
Jarvis/App/MenuBar/MenuBarContextMenu.swift
T
james.maes f8da4b7c39 feat(01-03): add MenuBarIconController + MenuBarContextMenu + JarvisAppTests target
- MenuBarIconController (@MainActor) owns NSStatusItem, loads Icon-MenuBar-Template asset with isTemplate=true, re-anchors layer.frame after anchorPoint change (Q7 gotcha #2), transitions across 5 HudStates via CAAnimationFactory with 150ms crossfade + rate-limited VoiceOver announcements (3s limiter per UI-SPEC Surface 3 line 421)
- MenuBarContextMenu builds Setup… / Settings… (disabled) / Show Dev Overlay / Copy State Dump / Quit Jarvis (⌘Q), with a ClosureTarget trampoline for closure-based actions
- Left-click vs right-click disambiguation via NSApp.currentEvent.type inspection (D-16 / UI-SPEC Surface 3 lines 406-409)
- JarvisAppTests Xcode unit-test bundle target added via project.yml; scheme wires test action against JarvisAppTests
- MenuBarIconControllerTests covers state transitions, no-op-on-same-state, context-menu items, and HudState voiceOverLabels (4 tests pass)
- PBXCopyFilesBuildPhase for Contents/Helpers re-applied post-xcodegen (Plan 01 invariant — MCP-05 acceptance-criteria grep)
2026-04-22 13:14:09 -05:00

64 lines
2.2 KiB
Swift

import AppKit
/// Builds the menu-bar right-click / Ctrl-click menu per UI-SPEC Surface 4 lines 428-436.
/// Action targets are wired by `AppDelegate` (currently in this plan as stubs; Plan 04
/// wires them to the wizard / dev-overlay / state-dump).
@MainActor
public enum MenuBarContextMenu {
public static func build(
setupAction: @escaping () -> Void,
devOverlayToggleAction: @escaping () -> Void,
stateDumpAction: @escaping () -> Void
) -> NSMenu {
let menu = NSMenu()
let setup = NSMenuItem(title: "Setup…", action: nil, keyEquivalent: "")
setup.target = ClosureTarget(action: setupAction)
setup.action = #selector(ClosureTarget.run)
menu.addItem(setup)
let settings = NSMenuItem(title: "Settings…", action: nil, keyEquivalent: "")
settings.isEnabled = false
settings.toolTip = "Coming soon"
menu.addItem(settings)
menu.addItem(NSMenuItem.separator())
let devOverlay = NSMenuItem(title: "Show Dev Overlay", action: nil, keyEquivalent: "")
devOverlay.target = ClosureTarget(action: devOverlayToggleAction)
devOverlay.action = #selector(ClosureTarget.run)
menu.addItem(devOverlay)
let stateDump = NSMenuItem(title: "Copy State Dump", action: nil, keyEquivalent: "")
stateDump.target = ClosureTarget(action: stateDumpAction)
stateDump.action = #selector(ClosureTarget.run)
menu.addItem(stateDump)
menu.addItem(NSMenuItem.separator())
let quit = NSMenuItem(
title: "Quit Jarvis",
action: #selector(NSApplication.terminate(_:)),
keyEquivalent: "q"
)
quit.keyEquivalentModifierMask = [.command]
menu.addItem(quit)
return menu
}
}
/// Objective-C-visible trampoline so `NSMenuItem` can call closure-based actions.
/// Strong-ref on the menu item is deliberate: the closure survives the menu's lifetime.
@MainActor
private final class ClosureTarget: NSObject {
let action: () -> Void
init(action: @escaping () -> Void) {
self.action = action
super.init()
}
@objc func run() {
action()
}
}