Files
steipete--codexbar/Sources/CodexBar/StatusItemMenu.swift
T
2026-07-13 12:22:33 +08:00

108 lines
3.6 KiB
Swift

import AppKit
enum StatusItemMenuProviderNavigationDirection {
case previous
case next
}
protocol StatusItemMenuPersistentActionDelegate: AnyObject {
func performPersistentRefreshAction(
in menuID: ObjectIdentifier,
menuInteractionGeneration: Int)
func performPersistentSettingsAction()
func performPersistentQuitAction()
func performProviderNavigation(_ direction: StatusItemMenuProviderNavigationDirection)
}
final class StatusItemMenu: NSMenu {
weak var persistentActionDelegate: StatusItemMenuPersistentActionDelegate?
var menuInteractionGeneration: Int?
func requestPersistentRefreshAction() {
guard let menuInteractionGeneration else { return }
self.persistentActionDelegate?.performPersistentRefreshAction(
in: ObjectIdentifier(self),
menuInteractionGeneration: menuInteractionGeneration)
}
override func performKeyEquivalent(with event: NSEvent) -> Bool {
if let action = Self.persistentAction(for: event) {
switch action {
case .refresh:
self.requestPersistentRefreshAction()
case .settings:
self.persistentActionDelegate?.performPersistentSettingsAction()
case .quit:
self.persistentActionDelegate?.performPersistentQuitAction()
}
return true
}
if let direction = Self.providerNavigationDirection(for: event),
self.items.first?.view is ProviderSwitcherView
{
self.persistentActionDelegate?.performProviderNavigation(direction)
return true
}
return super.performKeyEquivalent(with: event)
}
private enum PersistentAction {
case refresh
case settings
case quit
}
nonisolated static func isPersistentRefreshShortcut(for event: NSEvent) -> Bool {
self.persistentAction(for: event) == .refresh
}
private nonisolated static func persistentAction(for event: NSEvent) -> PersistentAction? {
guard event.type == .keyDown else { return nil }
let relevantModifiers = event.modifierFlags.intersection([.command, .option, .control, .shift])
guard relevantModifiers == .command else { return nil }
switch event.charactersIgnoringModifiers?.lowercased() {
case "r":
return .refresh
case ",":
return .settings
case "q":
return .quit
default:
return nil
}
}
nonisolated static func providerNavigationDirection(
for event: NSEvent) -> StatusItemMenuProviderNavigationDirection?
{
guard event.type == .keyDown else { return nil }
let relevantModifiers = event.modifierFlags.intersection([.command, .option, .control, .shift])
guard relevantModifiers.isEmpty else { return nil }
switch event.keyCode {
case 123:
return .previous
case 124:
return .next
default:
return nil
}
}
nonisolated static func providerSelectionIndex(for event: NSEvent) -> Int? {
guard event.type == .keyDown else { return nil }
let relevantModifiers = event.modifierFlags.intersection([.command, .option, .control, .shift])
guard relevantModifiers == .command,
let characters = event.charactersIgnoringModifiers,
characters.count == 1,
let number = Int(characters),
(1...9).contains(number)
else {
return nil
}
return number - 1
}
}