fix: restore window and keyboard interactions with queue
This commit is contained in:
@@ -285,6 +285,25 @@ def check_cli_instances(root, app, fixtures):
|
||||
and state["position"] > 0 and state["hasVideo"] for state in states.values()),
|
||||
"cold default CLI opens one actual player window")
|
||||
first_pid = next(iter(first))
|
||||
opened = input_request(first_pid, 1, lambda state: state["showsQueue"]
|
||||
and state["window"].get("videoOwnsMouse") is True
|
||||
and abs(state["window"]["videoWidth"] - first[first_pid]["window"]["videoWidth"]) < 1,
|
||||
"queue preserves video size and mouse target", action="queue", visible=True)
|
||||
window = opened[first_pid]["window"]
|
||||
if (not window["resizable"] or window["aspectWidth"] != 0 or window["aspectHeight"] != 0
|
||||
or window["minWidth"] > 660 or window["minHeight"] > 300
|
||||
or window["maxWidth"] <= window["width"] + 80
|
||||
or window["maxHeight"] <= window["height"] + 80
|
||||
or not window["controlsOwnMouse"] or not window["sidebarOwnsMouse"]
|
||||
or not window["edgesAvoidDragSurface"] or window["surfaceMovesOnMouseDown"]):
|
||||
raise RuntimeError(f"queue must preserve native resize limits and independent mouse targets: {window}")
|
||||
input_request(first_pid, 1, lambda state: not state["showsQueue"]
|
||||
and abs(state["window"]["videoWidth"] - first[first_pid]["window"]["videoWidth"]) < 1,
|
||||
"queue closes without changing video size", action="queue", visible=False)
|
||||
print(f"PASS native queue window: independent video/controls/sidebar/edge targets, "
|
||||
f"resizable min {window['minWidth']}x{window['minHeight']}, unconstrained maxima and cleared aspect")
|
||||
if os.environ.get("TEREBI_CHECK_WINDOW_ONLY") == "1":
|
||||
return
|
||||
if settings(first[first_pid]) != (False, False, True) or config.parent.exists():
|
||||
raise RuntimeError(f"missing config must use defaults without creating a file: {first}")
|
||||
invoke([])
|
||||
|
||||
@@ -98,6 +98,46 @@ private enum CLIInstanceCheck {
|
||||
return nil
|
||||
}
|
||||
|
||||
static func windowSnapshot(_ window: NSWindow?) -> [String: Any] {
|
||||
guard let window, let content = window.contentView else { return [:] }
|
||||
func surface(in view: NSView) -> NSView? {
|
||||
if view.identifier?.rawValue == "PlaybackSurface" { return view }
|
||||
return view.subviews.lazy.compactMap { surface(in: $0) }.first
|
||||
}
|
||||
var result: [String: Any] = [
|
||||
"width": window.frame.width, "height": window.frame.height,
|
||||
"minWidth": window.minSize.width, "minHeight": window.minSize.height,
|
||||
"maxWidth": window.maxSize.width, "maxHeight": window.maxSize.height,
|
||||
"contentMinWidth": window.contentMinSize.width, "contentMinHeight": window.contentMinSize.height,
|
||||
"contentMaxWidth": window.contentMaxSize.width, "contentMaxHeight": window.contentMaxSize.height,
|
||||
"aspectWidth": window.aspectRatio.width, "aspectHeight": window.aspectRatio.height,
|
||||
"resizable": window.styleMask.contains(.resizable),
|
||||
]
|
||||
if let surface = surface(in: content) {
|
||||
func hit(_ point: NSPoint) -> NSView? {
|
||||
content.hitTest(surface.convert(point, to: content.superview))
|
||||
}
|
||||
result["videoOwnsMouse"] = hit(NSPoint(x: surface.bounds.midX, y: surface.bounds.midY)) === surface
|
||||
result["controlsOwnMouse"] = hit(NSPoint(x: surface.bounds.midX, y: 46)) !== surface
|
||||
result["sidebarOwnsMouse"] = hit(NSPoint(x: surface.bounds.maxX + 130, y: surface.bounds.midY)) !== surface
|
||||
result["videoWidth"] = surface.bounds.width
|
||||
result["videoHeight"] = surface.bounds.height
|
||||
result["surfaceMovesOnMouseDown"] = surface.mouseDownCanMoveWindow
|
||||
let edges = [NSPoint(x: 1, y: content.bounds.midY),
|
||||
NSPoint(x: content.bounds.maxX - 1, y: content.bounds.midY),
|
||||
NSPoint(x: content.bounds.midX, y: 1),
|
||||
NSPoint(x: content.bounds.midX, y: content.bounds.maxY - 1)]
|
||||
let targets = edges.map { point in
|
||||
content.hitTest(content.convert(point, to: content.superview))
|
||||
}
|
||||
result["edgeTargets"] = targets.map { $0.map { String(describing: type(of: $0)) } ?? "native window" }
|
||||
result["edgesAvoidDragSurface"] = targets.allSatisfy { target in
|
||||
target !== surface && !(target.map { String(describing: type(of: $0)).contains("ChromeView") } ?? false)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static func run(_ configuration: URL) {
|
||||
let settings = try! JSONSerialization.jsonObject(with: Data(contentsOf: configuration)) as! [String: String]
|
||||
let directory = URL(fileURLWithPath: settings["directory"]!, isDirectory: true)
|
||||
@@ -147,6 +187,9 @@ private enum CLIInstanceCheck {
|
||||
switch action {
|
||||
case "youtube":
|
||||
model.openYouTube(command["url"] as! String)
|
||||
case "queue":
|
||||
model.showsQueue = command["visible"] as! Bool
|
||||
model.revealControls()
|
||||
case "caption":
|
||||
guard let view = window.contentView, let selector = captionSelector(in: view),
|
||||
let cell = selector.cell as? NSPopUpButtonCell, let menu = selector.menu else {
|
||||
@@ -252,6 +295,8 @@ private enum CLIInstanceCheck {
|
||||
"mainWindow": windows.first?.isMainWindow ?? false,
|
||||
"editorText": editorText,
|
||||
"compact": model.isCompact,
|
||||
"showsQueue": model.showsQueue,
|
||||
"window": windowSnapshot(windows.first),
|
||||
"onTop": model.isOnTop,
|
||||
]
|
||||
do {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -49,7 +49,7 @@ Open one or more local files with **⌘O**, drag them onto the video, or use Fin
|
||||
|
||||
Compact mode keeps playback in the same floating window; drag to move it and double-click to restore. The titlebar pin keeps the normal window on top. macOS media keys also control playback; previous/next seek five seconds rather than changing tracks.
|
||||
|
||||
Playback controls stay anchored at the bottom center of the video and cannot be dragged.
|
||||
Drag the video or unused playback background to move the window, including with the queue open. A single video click hides the controls; double-click toggles fullscreen. Playback controls stay anchored at the bottom center of the video and cannot be dragged. Native window edges remain available for resizing; the open queue allows free resizing with letterboxed video.
|
||||
|
||||
Active playback prevents idle system sleep. Local video also keeps the display awake; audio-only playback allows display sleep. Pause, end, close, playback failure and quit release sleep prevention.
|
||||
|
||||
@@ -59,7 +59,7 @@ Use the titlebar's **Queue** icon or **View → Show Queue** to show the editabl
|
||||
|
||||
In a normal window, showing the queue expands the window outward while preserving the video's size; hiding it removes that extra width. The sidebar meets the video directly, without a separate divider gap. The window shifts left when necessary near the screen's right edge. Fullscreen keeps its fixed bounds.
|
||||
|
||||
While the sidebar is open, its persistent top header holds the queue count, queue toggle, pin, Cast and settings controls; these do not fade with the video controls. Closing the sidebar returns those controls to the auto-hiding video titlebar.
|
||||
While the sidebar is open, its persistent top header holds the queue count, queue toggle, pin, Cast and settings controls; these do not fade with the video controls. Closing the sidebar returns those controls to the auto-hiding video titlebar. Space and configured playback shortcuts continue working with queue-list focus; Tab navigation, deliberately focused buttons/sliders and text entry keep their native keyboard behavior.
|
||||
|
||||
Playback advances automatically and stops at the end, without wrapping. **Previous Queue Item** and **Next Queue Item** are available in the sidebar and Playback menu and are disabled at their respective ends. Reordering or removing another item does not interrupt playback; removing the current item plays its next neighbor, or stops if there is none. A playback error stays on that item rather than skipping it.
|
||||
|
||||
|
||||
@@ -79,11 +79,14 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
private func handleKeyEvent(_ event: NSEvent, in window: NSWindow, chord: KeyChord) -> Bool {
|
||||
guard event.window === window, NSApp.modalWindow == nil, window.attachedSheet == nil,
|
||||
!(window.firstResponder is NSTextView), !(window.firstResponder is NSTextField) else { return false }
|
||||
// Preserve focus traversal, control activation/navigation and VoiceOver's own chords.
|
||||
guard event.window === window, NSApp.modalWindow == nil, window.attachedSheet == nil else { return false }
|
||||
let responder = window.firstResponder
|
||||
if let text = responder as? NSTextView, text.isEditable || text.isSelectable { return false }
|
||||
if let text = responder as? NSTextField, text.isEditable || text.isSelectable { return false }
|
||||
// Lists and inert labels are playback surfaces, not text entry or activation controls.
|
||||
// Keep deliberate button/slider focus, traversal and VoiceOver's own chords native.
|
||||
if chord.key == "tab" && chord.modifiers.subtracting(.shift).isEmpty { return false }
|
||||
if window.firstResponder is NSControl,
|
||||
if responder is NSControl, !(responder is NSTableView), !(responder is NSTextField),
|
||||
chord.modifiers.subtracting(.shift).isEmpty,
|
||||
Self.nativeNavigationKeys.contains(chord.key) {
|
||||
return false
|
||||
|
||||
+48
-32
@@ -1809,14 +1809,6 @@ private struct PlaybackSlider: NSViewRepresentable {
|
||||
func castVolumeSliderFeedbackCheck() -> Bool { PlaybackSlider.Slider.feedbackCheck() }
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
private struct VideoSurface: NSViewRepresentable {
|
||||
let view: NSView
|
||||
|
||||
func makeNSView(context: Context) -> NSView { view }
|
||||
func updateNSView(_ nsView: NSView, context: Context) {}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private struct CaptionSelector: NSViewRepresentable {
|
||||
let model: PlayerModel
|
||||
@@ -1888,22 +1880,46 @@ private struct CaptionSelector: NSViewRepresentable {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private struct CompactDragSurface: NSViewRepresentable {
|
||||
struct PlaybackSurface: NSViewRepresentable {
|
||||
let model: PlayerModel
|
||||
|
||||
func makeNSView(context: Context) -> DragView { DragView(model: model) }
|
||||
func updateNSView(_ view: DragView, context: Context) {}
|
||||
func updateNSView(_ view: DragView, context: Context) { view.updateVideo() }
|
||||
|
||||
final class DragView: NSView {
|
||||
private let model: PlayerModel
|
||||
private var hoverTracking: NSTrackingArea?
|
||||
private var mouseDownEvent: NSEvent?
|
||||
|
||||
init(model: PlayerModel) {
|
||||
self.model = model
|
||||
super.init(frame: .zero)
|
||||
identifier = NSUserInterfaceItemIdentifier("PlaybackSurface")
|
||||
if let video = model.videoView {
|
||||
video.frame = bounds
|
||||
video.autoresizingMask = [.width, .height]
|
||||
addSubview(video)
|
||||
}
|
||||
updateVideo()
|
||||
}
|
||||
required init?(coder: NSCoder) { nil }
|
||||
|
||||
func updateVideo() {
|
||||
model.videoView?.isHidden = model.fileName == nil || model.castDevice != nil
|
||||
}
|
||||
|
||||
override var mouseDownCanMoveWindow: Bool { false }
|
||||
|
||||
override func hitTest(_ point: NSPoint) -> NSView? {
|
||||
guard super.hitTest(point) != nil else { return nil }
|
||||
// Leave the outer native resize border alone, not the internal queue boundary.
|
||||
if let content = window?.contentView,
|
||||
!content.bounds.insetBy(dx: 5, dy: 5).contains(content.convert(point, from: superview)) {
|
||||
return nil
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
override func updateTrackingAreas() {
|
||||
super.updateTrackingAreas()
|
||||
if let hoverTracking { removeTrackingArea(hoverTracking) }
|
||||
@@ -1916,18 +1932,33 @@ private struct CompactDragSurface: NSViewRepresentable {
|
||||
|
||||
override func mouseEntered(with event: NSEvent) { model.revealControls() }
|
||||
override func mouseMoved(with event: NSEvent) { model.revealControls() }
|
||||
override func mouseExited(with event: NSEvent) { model.hideControls(hideCursor: false) }
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
if model.isCompact { model.hideControls(hideCursor: false) }
|
||||
}
|
||||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
mouseDownEvent = nil
|
||||
if event.clickCount == 2 {
|
||||
model.toggleCompact()
|
||||
return
|
||||
if model.isCompact { model.toggleCompact() } else { model.toggleFullScreen() }
|
||||
} else {
|
||||
mouseDownEvent = event
|
||||
}
|
||||
}
|
||||
|
||||
override func mouseDragged(with event: NSEvent) {
|
||||
guard let down = mouseDownEvent, let window else { return }
|
||||
mouseDownEvent = nil
|
||||
model.setControlsDragging(true)
|
||||
window?.performDrag(with: event)
|
||||
model.compactWindow.snapAfterDrag()
|
||||
window.performDrag(with: down)
|
||||
if model.isCompact { model.compactWindow.snapAfterDrag() }
|
||||
model.setControlsDragging(false)
|
||||
}
|
||||
|
||||
override func mouseUp(with event: NSEvent) {
|
||||
guard mouseDownEvent != nil else { return }
|
||||
mouseDownEvent = nil
|
||||
if !model.isCompact { model.hideControls() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2350,18 +2381,8 @@ private struct PlayerContent: View {
|
||||
} else {
|
||||
Color.black
|
||||
}
|
||||
if let view = model.videoView {
|
||||
VideoSurface(view: view)
|
||||
PlaybackSurface(model: model)
|
||||
.accessibilityLabel("Video")
|
||||
.opacity(model.fileName == nil || model.castDevice != nil ? 0 : 1)
|
||||
.gesture(
|
||||
TapGesture(count: 2).onEnded { model.toggleFullScreen() }
|
||||
.exclusively(before: TapGesture().onEnded { model.hideControls() })
|
||||
)
|
||||
}
|
||||
if model.isCompact {
|
||||
CompactDragSurface(model: model).accessibilityHidden(true)
|
||||
}
|
||||
if !model.subtitleText.isEmpty {
|
||||
Text(model.subtitleText)
|
||||
.font(.system(size: max(16, min(28, geometry.size.width / 32)), weight: .semibold))
|
||||
@@ -2703,12 +2724,7 @@ private struct WindowChrome: NSViewRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
model.setControlsDragging(true)
|
||||
window?.performDrag(with: event)
|
||||
model.compactWindow.snapAfterDrag()
|
||||
model.setControlsDragging(false)
|
||||
}
|
||||
override func hitTest(_ point: NSPoint) -> NSView? { nil }
|
||||
|
||||
func detach() {
|
||||
if let monitor { NSEvent.removeMonitor(monitor) }
|
||||
|
||||
Reference in New Issue
Block a user