feat: configure explicit state-dependent player keybindings
This commit is contained in:
@@ -65,6 +65,29 @@ private enum AppStartupCheck {
|
||||
@MainActor
|
||||
private enum CLIInstanceCheck {
|
||||
static var timer: Timer?
|
||||
static var requestID = ""
|
||||
static var editorText = ""
|
||||
static func selectMenuItem(_ title: String, in menu: NSMenu) -> Bool {
|
||||
for (index, item) in menu.items.enumerated() {
|
||||
if item.title == title, item.action != nil {
|
||||
precondition(item.isEnabled, "Native menu action \(title) must be available")
|
||||
menu.performActionForItem(at: index)
|
||||
return true
|
||||
}
|
||||
if let submenu = item.submenu, selectMenuItem(title, in: submenu) { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func prepareMenu(_ menu: NSMenu) {
|
||||
// Direct key-equivalent dispatch does not open/track the native menu first.
|
||||
// Ask AppKit to prepare and validate its real items just as normal menu use does.
|
||||
menu.delegate?.menuNeedsUpdate?(menu)
|
||||
menu.update()
|
||||
for item in menu.items {
|
||||
if let submenu = item.submenu { prepareMenu(submenu) }
|
||||
}
|
||||
}
|
||||
|
||||
static func run(_ configuration: URL) {
|
||||
let settings = try! JSONSerialization.jsonObject(with: Data(contentsOf: configuration)) as! [String: String]
|
||||
@@ -77,14 +100,66 @@ private enum CLIInstanceCheck {
|
||||
}
|
||||
// Exercise the real SwiftUI scene and delegate without displaying test windows.
|
||||
_ = NotificationCenter.default.addObserver(forName: NSWindow.didUpdateNotification, object: nil, queue: .main) { _ in
|
||||
MainActor.assumeIsolated { for window in NSApp.windows { window.alphaValue = 0 } }
|
||||
MainActor.assumeIsolated {
|
||||
for window in NSApp.windows {
|
||||
window.alphaValue = 0
|
||||
window.ignoresMouseEvents = true
|
||||
}
|
||||
}
|
||||
}
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
|
||||
MainActor.assumeIsolated {
|
||||
let model = PlayerModel.shared
|
||||
let pid = ProcessInfo.processInfo.processIdentifier
|
||||
let windows = NSApp.windows.filter { !($0 is NSPanel) && $0.contentView != nil }
|
||||
// Text input can create non-main auxiliary windows; count actual player windows.
|
||||
let windows = NSApp.windows.filter { $0.canBecomeMain && !($0 is NSPanel) && $0.contentView != nil }
|
||||
for window in windows { window.alphaValue = 0 }
|
||||
let request = directory.appendingPathComponent("keys-\(pid).json")
|
||||
if let data = try? Data(contentsOf: request),
|
||||
let command = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let id = command["id"] as? String, id != requestID, let window = windows.first {
|
||||
if !NSApp.isActive || !window.isKeyWindow || !window.isMainWindow {
|
||||
NSApp.activate()
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
window.makeMain()
|
||||
return
|
||||
}
|
||||
requestID = id
|
||||
if let media = command["media"] as? String { model.open(URL(fileURLWithPath: media)) }
|
||||
if let events = command["events"] as? [[String: Any]] {
|
||||
window.makeKey()
|
||||
for input in events {
|
||||
if let title = input["select"] as? String {
|
||||
guard let menu = NSApp.mainMenu else { fatalError("Native application menu is missing") }
|
||||
prepareMenu(menu)
|
||||
precondition(selectMenuItem(title, in: menu), "Native menu action \(title) must exist")
|
||||
continue
|
||||
}
|
||||
let editor = NSTextView(frame: .zero)
|
||||
if input["editing"] as? Bool == true {
|
||||
window.contentView?.addSubview(editor)
|
||||
window.makeFirstResponder(editor)
|
||||
} else { window.makeFirstResponder(nil) }
|
||||
let text = input["key"] as! String
|
||||
let event = NSEvent.keyEvent(with: .keyDown, location: .zero,
|
||||
modifierFlags: NSEvent.ModifierFlags(rawValue: UInt(input["flags"] as? Int ?? 0)),
|
||||
timestamp: 0, windowNumber: window.windowNumber, context: nil,
|
||||
characters: text, charactersIgnoringModifiers: text,
|
||||
isARepeat: input["repeat"] as? Bool ?? false,
|
||||
keyCode: UInt16(input["code"] as? Int ?? 7))!
|
||||
if input["menu"] as? Bool == true {
|
||||
if let menu = NSApp.mainMenu { prepareMenu(menu) }
|
||||
precondition(NSApp.mainMenu?.performKeyEquivalent(with: event) == true,
|
||||
"Configured shortcut must be accepted by the native menu")
|
||||
} else { NSApp.sendEvent(event) }
|
||||
if input["editing"] as? Bool == true {
|
||||
editorText = editor.string
|
||||
window.makeFirstResponder(nil)
|
||||
editor.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let applications = NSWorkspace.shared.runningApplications.filter {
|
||||
!$0.isTerminated && $0.bundleURL == Bundle.main.bundleURL
|
||||
}
|
||||
@@ -101,6 +176,10 @@ private enum CLIInstanceCheck {
|
||||
"showBufferInfo": model.showsBufferInfo,
|
||||
"automaticQuality": model.automaticYouTubeQuality,
|
||||
"snapToEdges": model.compactWindow.snappingEnabled,
|
||||
"requestID": requestID,
|
||||
"editorText": editorText,
|
||||
"compact": model.isCompact,
|
||||
"onTop": model.isOnTop,
|
||||
]
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: snapshot)
|
||||
|
||||
Reference in New Issue
Block a user