macOS: compact context menu context rows
This commit is contained in:
@@ -6,8 +6,10 @@ final class MenuContextCardInjector: NSObject, NSMenuDelegate {
|
||||
static let shared = MenuContextCardInjector()
|
||||
|
||||
private let tag = 9_415_227
|
||||
private let cardWidth: CGFloat = 320
|
||||
private let fallbackCardWidth: CGFloat = 320
|
||||
private weak var originalDelegate: NSMenuDelegate?
|
||||
private var loadTask: Task<Void, Never>?
|
||||
private let activeWindowSeconds: TimeInterval = 24 * 60 * 60
|
||||
|
||||
func install(into statusItem: NSStatusItem) {
|
||||
// SwiftUI owns the menu, but we can inject a custom NSMenuItem.view right before display.
|
||||
@@ -29,10 +31,15 @@ final class MenuContextCardInjector: NSObject, NSMenuDelegate {
|
||||
|
||||
guard let insertIndex = self.findInsertIndex(in: menu) else { return }
|
||||
|
||||
let cardView = ContextMenuCardView(width: self.cardWidth)
|
||||
let hosting = NSHostingView(rootView: cardView)
|
||||
self.loadTask?.cancel()
|
||||
|
||||
let placeholder = AnyView(ContextMenuCardView(
|
||||
rows: [],
|
||||
statusText: "Loading sessions…"))
|
||||
|
||||
let hosting = NSHostingView(rootView: placeholder)
|
||||
let size = hosting.fittingSize
|
||||
hosting.frame = NSRect(origin: .zero, size: NSSize(width: self.cardWidth, height: size.height))
|
||||
hosting.frame = NSRect(origin: .zero, size: NSSize(width: self.initialCardWidth(for: menu), height: size.height))
|
||||
|
||||
let item = NSMenuItem()
|
||||
item.tag = self.tag
|
||||
@@ -40,10 +47,30 @@ final class MenuContextCardInjector: NSObject, NSMenuDelegate {
|
||||
item.isEnabled = false
|
||||
|
||||
menu.insertItem(item, at: insertIndex)
|
||||
|
||||
// After the menu attaches the view to its window, adopt the menu's computed width.
|
||||
DispatchQueue.main.async { [weak self, weak hosting] in
|
||||
guard let self, let hosting else { return }
|
||||
self.adoptMenuWidthIfAvailable(for: menu, hosting: hosting)
|
||||
}
|
||||
|
||||
self.loadTask = Task { [weak hosting] in
|
||||
let view = await self.makeCardView()
|
||||
await MainActor.run {
|
||||
hosting?.rootView = view
|
||||
hosting?.invalidateIntrinsicContentSize()
|
||||
if let hosting {
|
||||
self.adoptMenuWidthIfAvailable(for: menu, hosting: hosting)
|
||||
let size = hosting.fittingSize
|
||||
hosting.frame.size.height = size.height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func menuDidClose(_ menu: NSMenu) {
|
||||
self.originalDelegate?.menuDidClose?(menu)
|
||||
self.loadTask?.cancel()
|
||||
}
|
||||
|
||||
func menuNeedsUpdate(_ menu: NSMenu) {
|
||||
@@ -57,6 +84,34 @@ final class MenuContextCardInjector: NSObject, NSMenuDelegate {
|
||||
return NSRect.zero
|
||||
}
|
||||
|
||||
private func makeCardView() async -> AnyView {
|
||||
let hints = SessionLoader.configHints()
|
||||
let store = SessionLoader.resolveStorePath(override: hints.storePath)
|
||||
let defaults = SessionDefaults(
|
||||
model: hints.model ?? SessionLoader.fallbackModel,
|
||||
contextTokens: hints.contextTokens ?? SessionLoader.fallbackContextTokens)
|
||||
|
||||
do {
|
||||
let loaded = try await SessionLoader.loadRows(at: store, defaults: defaults)
|
||||
let now = Date()
|
||||
let current = loaded.filter { row in
|
||||
if row.key == "main" { return true }
|
||||
guard let updatedAt = row.updatedAt else { return false }
|
||||
return now.timeIntervalSince(updatedAt) <= self.activeWindowSeconds
|
||||
}
|
||||
|
||||
let sorted = current.sorted { lhs, rhs in
|
||||
if lhs.key == "main" { return true }
|
||||
if rhs.key == "main" { return false }
|
||||
return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)
|
||||
}
|
||||
|
||||
return AnyView(ContextMenuCardView(rows: sorted))
|
||||
} catch {
|
||||
return AnyView(ContextMenuCardView(rows: [], statusText: "Could not load sessions"))
|
||||
}
|
||||
}
|
||||
|
||||
private func findInsertIndex(in menu: NSMenu) -> Int? {
|
||||
// Prefer inserting before the "Send Heartbeats" toggle item.
|
||||
if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) {
|
||||
@@ -66,4 +121,31 @@ final class MenuContextCardInjector: NSObject, NSMenuDelegate {
|
||||
if menu.items.count >= 2 { return 2 }
|
||||
return menu.items.count
|
||||
}
|
||||
|
||||
private func initialCardWidth(for menu: NSMenu) -> CGFloat {
|
||||
let width = menu.minimumWidth
|
||||
if width > 0 { return max(300, width) }
|
||||
return 300
|
||||
}
|
||||
|
||||
private func adoptMenuWidthIfAvailable(for menu: NSMenu, hosting: NSHostingView<AnyView>) {
|
||||
let targetWidth: CGFloat? = {
|
||||
if let contentWidth = hosting.window?.contentView?.bounds.width, contentWidth > 0 { return contentWidth }
|
||||
if let superWidth = hosting.superview?.bounds.width, superWidth > 0 { return superWidth }
|
||||
let minimumWidth = menu.minimumWidth
|
||||
if minimumWidth > 0 { return minimumWidth }
|
||||
return nil
|
||||
}()
|
||||
|
||||
guard let targetWidth else {
|
||||
if hosting.frame.width <= 0 {
|
||||
hosting.frame.size.width = self.fallbackCardWidth
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let clamped = max(300, targetWidth)
|
||||
if abs(hosting.frame.width - clamped) < 1 { return }
|
||||
hosting.frame.size.width = clamped
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user