fix: restore window and keyboard interactions with queue

This commit is contained in:
2026-10-18 01:51:51 +02:00
parent afb0c30e09
commit 07243f0ed7
6 changed files with 279 additions and 39 deletions
+157
View File
@@ -2,6 +2,20 @@ import AppKit
import MediaPlayer
import TerebiConfiguration
// Native focus eligibility normally depends on the user's Full Keyboard Access setting.
// Force eligibility only; keep the controls' real AppKit responder/key behavior.
private final class KeyboardCheckButton: NSButton {
override var acceptsFirstResponder: Bool { true }
}
private final class KeyboardCheckSlider: NSSlider {
override var acceptsFirstResponder: Bool { true }
}
private final class KeyboardCheckLabel: NSTextField {
override var acceptsFirstResponder: Bool { true }
}
@main
@MainActor
struct SystemPlaybackChecks {
@@ -17,6 +31,50 @@ struct SystemPlaybackChecks {
require(condition(), message)
}
private final class DragCheckWindow: NSWindow {
var drags = 0
var fullscreenToggles = 0
override func performDrag(with event: NSEvent) { drags += 1 }
override func toggleFullScreen(_ sender: Any?) { fullscreenToggles += 1 }
}
static func checkWindowSurface(defaults: UserDefaults) {
let model = PlayerModel(defaults: defaults)
defer { model.shutdown() }
let window = DragCheckWindow(contentRect: NSRect(x: -10_000, y: -10_000, width: 640, height: 360),
styleMask: [.titled, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
defer { window.close() }
let surface = PlaybackSurface.DragView(model: model)
surface.frame = window.contentView!.bounds
window.contentView!.addSubview(surface)
model.attachWindow(window)
@MainActor func event(_ type: NSEvent.EventType, clicks: Int = 1) -> NSEvent {
NSEvent.mouseEvent(with: type, location: NSPoint(x: 100, y: 100), modifierFlags: [],
timestamp: 0, windowNumber: window.windowNumber, context: nil,
eventNumber: 0, clickCount: clicks, pressure: 0)!
}
require(surface.hitTest(NSPoint(x: 100, y: 100)) === surface,
"video renderer and unused background route mouse input to their owning surface")
require(!surface.mouseDownCanMoveWindow, "native background movement cannot turn clicks or resize into drags")
for point in [NSPoint(x: 1, y: 100), NSPoint(x: 639, y: 100),
NSPoint(x: 100, y: 1), NSPoint(x: 100, y: surface.bounds.maxY - 1)] {
require(surface.hitTest(point) == nil, "playback drag surface leaves all native resize edges untouched")
}
surface.mouseDown(with: event(.leftMouseDown))
require(window.drags == 0, "mouse-down alone never begins window dragging")
surface.mouseUp(with: event(.leftMouseUp))
require(window.drags == 0 && window.fullscreenToggles == 0, "single video click never drags or enters fullscreen")
surface.mouseDown(with: event(.leftMouseDown))
surface.mouseDragged(with: event(.leftMouseDragged))
surface.mouseUp(with: event(.leftMouseUp))
require(window.drags == 1 && window.fullscreenToggles == 0, "real mouse-drag callback invokes native window dragging once")
surface.mouseDown(with: event(.leftMouseDown, clicks: 2))
surface.mouseUp(with: event(.leftMouseUp, clicks: 2))
require(window.drags == 1 && window.fullscreenToggles == 1, "double video click retains fullscreen instead of dragging")
}
static func checkCompactProgress() {
let times: [(Double, Double, String)] = [
(12.9, 45.9, "12 / 45 s"),
@@ -99,6 +157,103 @@ struct SystemPlaybackChecks {
"playback failure stops on failed queue entry rather than skipping")
}
static func checkKeyboardRouting(defaults: UserDefaults) {
let bindings = try! KeyBindings(overrides: [
"play": [], "pause": [], "seek_backward_5s": [],
"show_buffer_info": ["space", "return", "left", "shift+left", "tab", "shift+tab",
"cmd+b", "f12", "ctrl+alt+b"]
])
let model = PlayerModel(defaults: defaults, configuration: PlayerConfiguration(keys: bindings))
defer { model.shutdown() }
let window = NSWindow(contentRect: NSRect(x: -10_000, y: -10_000, width: 640, height: 360),
styleMask: [.titled, .resizable], backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
defer { window.close() }
func event(_ code: UInt16, _ text: String, _ flags: NSEvent.ModifierFlags = []) -> NSEvent {
NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: flags, timestamp: 0,
windowNumber: window.windowNumber, context: nil, characters: text,
charactersIgnoringModifiers: text, isARepeat: false, keyCode: code)!
}
let shortcuts = [event(49, " "), event(36, "\r"), event(123, "\u{F702}"),
event(123, "\u{F702}", .shift), event(11, "b", .command),
event(111, "\u{F70F}", .function)]
func focus(_ responder: NSView) {
window.makeFirstResponder(nil)
window.contentView?.subviews.forEach { $0.removeFromSuperview() }
responder.frame = NSRect(x: 0, y: 0, width: 300, height: 200)
window.contentView?.addSubview(responder)
require(window.makeFirstResponder(responder), "keyboard regression fixture accepts actual window focus")
let focusedResponder = (responder as? NSTextField)?.currentEditor() ?? responder
require(window.firstResponder === focusedResponder,
"keyboard regression fixture owns actual control or field-editor focus")
}
let lists = [NSTableView(), NSOutlineView()]
for list in lists {
list.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("title")))
focus(list)
for shortcut in shortcuts {
model.setShowsBufferInfo(false)
require(model.handleKeyEvent(shortcut, in: window) && model.showsBufferInfo,
"focused queue list dispatches configured \(KeyChord.from(shortcut)!) binding")
require(window.firstResponder === list, "playback shortcuts do not steal queue focus")
}
let traversalModifiers: [NSEvent.ModifierFlags] = [[], .shift]
for flags in traversalModifiers {
model.setShowsBufferInfo(false)
require(!model.handleKeyEvent(event(48, "\t", flags), in: window) && !model.showsBufferInfo,
"Tab and Shift-Tab remain native traversal even when configured")
}
}
let label = KeyboardCheckLabel(labelWithString: "Queue item")
focus(label)
model.setShowsBufferInfo(false)
require(model.handleKeyEvent(shortcuts[0], in: window) && model.showsBufferInfo,
"noneditable nonselectable queue label does not block playback")
let controls: [NSControl] = [KeyboardCheckButton(), KeyboardCheckSlider()]
for control in controls {
focus(control)
for shortcut in shortcuts.prefix(4) {
model.setShowsBufferInfo(false)
require(!model.handleKeyEvent(shortcut, in: window) && !model.showsBufferInfo,
"deliberately focused native button/slider retains activation and navigation")
require(window.firstResponder === control, "native control keyboard focus is preserved")
}
require(model.handleKeyEvent(shortcuts[4], in: window) && model.showsBufferInfo,
"modified configured bindings still work with native control focus")
}
let editor = NSTextView()
focus(editor)
for shortcut in shortcuts {
model.setShowsBufferInfo(false)
require(!model.handleKeyEvent(shortcut, in: window) && !model.showsBufferInfo,
"text editing retains configured keys instead of dispatching playback")
}
editor.keyDown(with: shortcuts[0])
require(editor.string == " ", "unconsumed Space reaches actual text entry")
label.isSelectable = true
focus(label)
require(!model.handleKeyEvent(shortcuts[0], in: window),
"selectable text retains native text interaction")
label.isSelectable = false
label.isEditable = true
require(!model.handleKeyEvent(shortcuts[0], in: window),
"editable text field retains native text entry")
focus(lists[0])
let voiceOverChord = event(11, "b", [.control, .option])
require(model.handleKeyEvent(voiceOverChord, in: window) != NSWorkspace.shared.isVoiceOverEnabled,
"VoiceOver retains its Control-Option chords when enabled")
let sheet = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 200, height: 100),
styleMask: .titled, backing: .buffered, defer: false)
sheet.isReleasedWhenClosed = false
window.beginSheet(sheet)
require(!model.handleKeyEvent(shortcuts[0], in: window), "attached sheet blocks player shortcuts")
window.endSheet(sheet)
sheet.close()
let modal = NSApp.beginModalSession(for: window)
require(!model.handleKeyEvent(shortcuts[0], in: window), "modal session blocks player shortcuts")
NSApp.endModalSession(modal)
}
static func main() {
checkCompactProgress()
_ = NSApplication.shared
@@ -106,7 +261,9 @@ struct SystemPlaybackChecks {
let suite = "terebi.system-playback-check.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
checkKeyboardRouting(defaults: defaults)
checkQueue(directory: URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true), defaults: defaults)
checkWindowSurface(defaults: defaults)
defaults.removePersistentDomain(forName: suite)
let initialModel = PlayerModel(defaults: defaults)
require(!initialModel.showsBufferInfo, "buffer readout defaults off before playback")