fix: restore window and keyboard interactions with queue
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user