Files
mpv/video/out/cocoa-cb/window.swift
der richter 3ad9c32a5f cocoa-cb: simplify cursor hiding and make it less greedy
for reasons unknown to me the NSCursor (un)hide functions can be
completely unreliable and the cursor can have an unknown state. this
only happens on some system and wasn't able to reproduce this. it's
probably some dumb race condition that might be possible to work around,
though because of the lack of reproducibility on my end it's hard to
test.

i decided to rework the cursor hiding code yet again and make it a lot
less greedy. the cursor will now always unhide when moved and there
will never be a situation again the cursor can't be unhidden again.
on the other hand there might be edge cases now where the cursor won't
hide immediately and you have to move it slightly to make it disappear
again. this should be an acceptable tradeoff.

Fixes #6886
2020-02-08 10:56:11 +01:00

538 lines
19 KiB
Swift

/*
* This file is part of mpv.
*
* mpv is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* mpv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
*/
import Cocoa
class Window: NSWindow, NSWindowDelegate {
weak var cocoaCB: CocoaCB! = nil
var mpv: MPVHelper? { get { return cocoaCB.mpv } }
var libmpv: LibmpvHelper { get { return cocoaCB.libmpv } }
var targetScreen: NSScreen?
var previousScreen: NSScreen?
var currentScreen: NSScreen?
var unfScreen: NSScreen?
var unfsContentFrame: NSRect?
var isInFullscreen: Bool = false
var isAnimating: Bool = false
var isMoving: Bool = false
var forceTargetScreen: Bool = false
var keepAspect: Bool = true {
didSet {
if let contentViewFrame = contentView?.frame, !isInFullscreen {
unfsContentFrame = convertToScreen(contentViewFrame)
}
if keepAspect {
contentAspectRatio = unfsContentFrame?.size ?? contentAspectRatio
} else {
resizeIncrements = NSSize(width: 1.0, height: 1.0)
}
}
}
var border: Bool = true {
didSet { if !border { cocoaCB.titleBar?.hide() } }
}
override var canBecomeKey: Bool { return true }
override var canBecomeMain: Bool { return true }
override var styleMask: NSWindow.StyleMask {
get { return super.styleMask }
set {
let responder = firstResponder
let windowTitle = title
super.styleMask = newValue
makeFirstResponder(responder)
title = windowTitle
}
}
convenience init(contentRect: NSRect, screen: NSScreen?, view: NSView, cocoaCB ccb: CocoaCB) {
self.init(contentRect: contentRect,
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered, defer: false, screen: screen)
// workaround for an AppKit bug where the NSWindow can't be placed on a
// none Main screen NSScreen outside the Main screen's frame bounds
if let wantedScreen = screen, screen != NSScreen.main {
var absoluteWantedOrigin = contentRect.origin
absoluteWantedOrigin.x += wantedScreen.frame.origin.x
absoluteWantedOrigin.y += wantedScreen.frame.origin.y
if !NSEqualPoints(absoluteWantedOrigin, self.frame.origin) {
self.setFrameOrigin(absoluteWantedOrigin)
}
}
cocoaCB = ccb
title = cocoaCB.title
minSize = NSMakeSize(160, 90)
collectionBehavior = .fullScreenPrimary
delegate = self
if let cView = contentView {
cView.addSubview(view)
view.frame = cView.frame
unfsContentFrame = convertToScreen(cView.frame)
}
targetScreen = screen
currentScreen = screen
unfScreen = screen
if let app = NSApp as? Application {
app.menuBar.register(#selector(setHalfWindowSize), for: MPM_H_SIZE)
app.menuBar.register(#selector(setNormalWindowSize), for: MPM_N_SIZE)
app.menuBar.register(#selector(setDoubleWindowSize), for: MPM_D_SIZE)
app.menuBar.register(#selector(performMiniaturize(_:)), for: MPM_MINIMIZE)
app.menuBar.register(#selector(performZoom(_:)), for: MPM_ZOOM)
}
}
override func toggleFullScreen(_ sender: Any?) {
if isAnimating {
return
}
isAnimating = true
targetScreen = cocoaCB.getTargetScreen(forFullscreen: !isInFullscreen)
if targetScreen == nil && previousScreen == nil {
targetScreen = screen
} else if targetScreen == nil {
targetScreen = previousScreen
previousScreen = nil
} else {
previousScreen = screen
}
if let contentViewFrame = contentView?.frame, !isInFullscreen {
unfsContentFrame = convertToScreen(contentViewFrame)
unfScreen = screen
}
// move window to target screen when going to fullscreen
if let tScreen = targetScreen, !isInFullscreen && (tScreen != screen) {
let frame = calculateWindowPosition(for: tScreen, withoutBounds: false)
setFrame(frame, display: true)
}
if Bool(mpv?.opts.native_fs ?? 1) {
super.toggleFullScreen(sender)
} else {
if !isInFullscreen {
setToFullScreen()
}
else {
setToWindow()
}
}
}
func customWindowsToEnterFullScreen(for window: NSWindow) -> [NSWindow]? {
return [window]
}
func customWindowsToExitFullScreen(for window: NSWindow) -> [NSWindow]? {
return [window]
}
func window(_ window: NSWindow, startCustomAnimationToEnterFullScreenWithDuration duration: TimeInterval) {
guard let tScreen = targetScreen else { return }
cocoaCB.view?.layerContentsPlacement = .scaleProportionallyToFit
cocoaCB.titleBar?.hide()
NSAnimationContext.runAnimationGroup({ (context) -> Void in
context.duration = getFsAnimationDuration(duration - 0.05)
window.animator().setFrame(tScreen.frame, display: true)
}, completionHandler: { })
}
func window(_ window: NSWindow, startCustomAnimationToExitFullScreenWithDuration duration: TimeInterval) {
guard let tScreen = targetScreen, let currentScreen = screen else { return }
let newFrame = calculateWindowPosition(for: tScreen, withoutBounds: tScreen == screen)
let intermediateFrame = aspectFit(rect: newFrame, in: currentScreen.frame)
cocoaCB.view?.layerContentsPlacement = .scaleProportionallyToFill
cocoaCB.titleBar?.hide()
styleMask.remove(.fullScreen)
setFrame(intermediateFrame, display: true)
NSAnimationContext.runAnimationGroup({ (context) -> Void in
context.duration = getFsAnimationDuration(duration - 0.05)
window.animator().setFrame(newFrame, display: true)
}, completionHandler: { })
}
func windowDidEnterFullScreen(_ notification: Notification) {
isInFullscreen = true
cocoaCB.mpv?.setConfigProperty(fullscreen: isInFullscreen)
cocoaCB.updateCursorVisibility()
endAnimation(frame)
cocoaCB.titleBar?.show()
}
func windowDidExitFullScreen(_ notification: Notification) {
guard let tScreen = targetScreen else { return }
isInFullscreen = false
cocoaCB.mpv?.setConfigProperty(fullscreen: isInFullscreen)
endAnimation(calculateWindowPosition(for: tScreen, withoutBounds: targetScreen == screen))
cocoaCB.view?.layerContentsPlacement = .scaleProportionallyToFit
}
func windowDidFailToEnterFullScreen(_ window: NSWindow) {
guard let tScreen = targetScreen else { return }
let newFrame = calculateWindowPosition(for: tScreen, withoutBounds: targetScreen == screen)
setFrame(newFrame, display: true)
endAnimation()
}
func windowDidFailToExitFullScreen(_ window: NSWindow) {
guard let targetFrame = targetScreen?.frame else { return }
setFrame(targetFrame, display: true)
endAnimation()
cocoaCB.view?.layerContentsPlacement = .scaleProportionallyToFit
}
func endAnimation(_ newFrame: NSRect = NSZeroRect) {
if !NSEqualRects(newFrame, NSZeroRect) && isAnimating {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
context.duration = 0.01
self.animator().setFrame(newFrame, display: true)
}, completionHandler: nil )
}
isAnimating = false
cocoaCB.layer?.update()
cocoaCB.checkShutdown()
}
func setToFullScreen() {
guard let targetFrame = targetScreen?.frame else { return }
styleMask.insert(.fullScreen)
NSApp.presentationOptions = [.autoHideMenuBar, .autoHideDock]
setFrame(targetFrame, display: true)
endAnimation()
isInFullscreen = true
cocoaCB.mpv?.setConfigProperty(fullscreen: isInFullscreen)
cocoaCB.layer?.update()
}
func setToWindow() {
guard let tScreen = targetScreen else { return }
let newFrame = calculateWindowPosition(for: tScreen, withoutBounds: targetScreen == screen)
NSApp.presentationOptions = []
setFrame(newFrame, display: true)
styleMask.remove(.fullScreen)
endAnimation()
isInFullscreen = false
cocoaCB.mpv?.setConfigProperty(fullscreen: isInFullscreen)
cocoaCB.layer?.update()
}
func getFsAnimationDuration(_ def: Double) -> Double {
let duration = libmpv.macOpts.macos_fs_animation_duration
if duration < 0 {
return def
} else {
return Double(duration)/1000
}
}
func setOnTop(_ state: Bool, _ ontopLevel: Int) {
if state {
switch ontopLevel {
case -1:
level = .floating
case -2:
level = .statusBar + 1
default:
level = NSWindow.Level(ontopLevel)
}
collectionBehavior.remove(.transient)
collectionBehavior.insert(.managed)
} else {
level = .normal
}
}
func setMinimized(_ stateWanted: Bool) {
if isMiniaturized == stateWanted { return }
if stateWanted {
performMiniaturize(self)
} else {
deminiaturize(self)
}
}
func setMaximized(_ stateWanted: Bool) {
if isZoomed == stateWanted { return }
zoom(self)
}
func updateMovableBackground(_ pos: NSPoint) {
if !isInFullscreen {
isMovableByWindowBackground = mpv?.canBeDraggedAt(pos) ?? true
} else {
isMovableByWindowBackground = false
}
}
func updateFrame(_ rect: NSRect) {
if rect != frame {
let cRect = frameRect(forContentRect: rect)
unfsContentFrame = rect
setFrame(cRect, display: true)
cocoaCB.layer?.update(force: true)
}
}
func updateSize(_ size: NSSize) {
if let currentSize = contentView?.frame.size, size != currentSize {
let newContentFrame = centeredContentSize(for: frame, size: size)
if !isInFullscreen {
updateFrame(newContentFrame)
} else {
unfsContentFrame = newContentFrame
}
}
}
override func setFrame(_ frameRect: NSRect, display flag: Bool) {
if frameRect.width < minSize.width || frameRect.height < minSize.height {
mpv?.sendVerbose("tried to set too small window size: \(frameRect.size)")
return
}
super.setFrame(frameRect, display: flag)
if let size = unfsContentFrame?.size, keepAspect {
contentAspectRatio = size
}
}
func centeredContentSize(for rect: NSRect, size sz: NSSize) -> NSRect {
let cRect = contentRect(forFrameRect: rect)
let dx = (cRect.size.width - sz.width) / 2
let dy = (cRect.size.height - sz.height) / 2
return NSInsetRect(cRect, dx, dy)
}
func aspectFit(rect r: NSRect, in rTarget: NSRect) -> NSRect {
var s = rTarget.width / r.width;
if r.height*s > rTarget.height {
s = rTarget.height / r.height
}
let w = r.width * s
let h = r.height * s
return NSRect(x: rTarget.midX - w/2, y: rTarget.midY - h/2, width: w, height: h)
}
func calculateWindowPosition(for tScreen: NSScreen, withoutBounds: Bool) -> NSRect {
guard let contentFrame = unfsContentFrame, let screen = unfScreen else {
return frame
}
var newFrame = frameRect(forContentRect: contentFrame)
let targetFrame = tScreen.frame
let targetVisibleFrame = tScreen.visibleFrame
let unfsScreenFrame = screen.frame
let visibleWindow = NSIntersectionRect(unfsScreenFrame, newFrame)
// calculate visible area of every side
let left = newFrame.origin.x - unfsScreenFrame.origin.x
let right = unfsScreenFrame.size.width -
(newFrame.origin.x - unfsScreenFrame.origin.x + newFrame.size.width)
let bottom = newFrame.origin.y - unfsScreenFrame.origin.y
let top = unfsScreenFrame.size.height -
(newFrame.origin.y - unfsScreenFrame.origin.y + newFrame.size.height)
// normalize visible areas, decide which one to take horizontal/vertical
var xPer = (unfsScreenFrame.size.width - visibleWindow.size.width)
var yPer = (unfsScreenFrame.size.height - visibleWindow.size.height)
if xPer != 0 { xPer = (left >= 0 || right < 0 ? left : right) / xPer }
if yPer != 0 { yPer = (bottom >= 0 || top < 0 ? bottom : top) / yPer }
// calculate visible area for every side for target screen
let xNewLeft = targetFrame.origin.x +
(targetFrame.size.width - visibleWindow.size.width) * xPer
let xNewRight = targetFrame.origin.x + targetFrame.size.width -
(targetFrame.size.width - visibleWindow.size.width) * xPer - newFrame.size.width
let yNewBottom = targetFrame.origin.y +
(targetFrame.size.height - visibleWindow.size.height) * yPer
let yNewTop = targetFrame.origin.y + targetFrame.size.height -
(targetFrame.size.height - visibleWindow.size.height) * yPer - newFrame.size.height
// calculate new coordinates, decide which one to take horizontal/vertical
newFrame.origin.x = left >= 0 || right < 0 ? xNewLeft : xNewRight
newFrame.origin.y = bottom >= 0 || top < 0 ? yNewBottom : yNewTop
// don't place new window on top of a visible menubar
let topMar = targetFrame.size.height -
(newFrame.origin.y - targetFrame.origin.y + newFrame.size.height)
let menuBarHeight = targetFrame.size.height -
(targetVisibleFrame.size.height + targetVisibleFrame.origin.y)
if topMar < menuBarHeight {
newFrame.origin.y -= top - menuBarHeight
}
if withoutBounds {
return newFrame
}
// screen bounds right and left
if newFrame.origin.x + newFrame.size.width > targetFrame.origin.x + targetFrame.size.width {
newFrame.origin.x = targetFrame.origin.x + targetFrame.size.width - newFrame.size.width
}
if newFrame.origin.x < targetFrame.origin.x {
newFrame.origin.x = targetFrame.origin.x
}
// screen bounds top and bottom
if newFrame.origin.y + newFrame.size.height > targetFrame.origin.y + targetFrame.size.height {
newFrame.origin.y = targetFrame.origin.y + targetFrame.size.height - newFrame.size.height
}
if newFrame.origin.y < targetFrame.origin.y {
newFrame.origin.y = targetFrame.origin.y
}
return newFrame
}
override func constrainFrameRect(_ frameRect: NSRect, to tScreen: NSScreen?) -> NSRect {
if (isAnimating && !isInFullscreen) || (!isAnimating && isInFullscreen) {
return frameRect
}
guard let ts: NSScreen = tScreen ?? screen ?? NSScreen.main else {
return frameRect
}
var nf: NSRect = frameRect
let of: NSRect = frame
let vf: NSRect = (isAnimating ? (targetScreen ?? ts) : ts).visibleFrame
let ncf: NSRect = contentRect(forFrameRect: nf)
// screen bounds top and bottom
if NSMaxY(nf) > NSMaxY(vf) {
nf.origin.y = NSMaxY(vf) - NSHeight(nf)
}
if NSMaxY(ncf) < NSMinY(vf) {
nf.origin.y = NSMinY(vf) + NSMinY(ncf) - NSMaxY(ncf)
}
// screen bounds right and left
if NSMinX(nf) > NSMaxX(vf) {
nf.origin.x = NSMaxX(vf) - NSWidth(nf)
}
if NSMaxX(nf) < NSMinX(vf) {
nf.origin.x = NSMinX(vf)
}
if NSHeight(nf) < NSHeight(vf) && NSHeight(of) > NSHeight(vf) && !isInFullscreen {
// If the window height is smaller than the visible frame, but it was
// bigger previously recenter the smaller window vertically. This is
// needed to counter the 'snap to top' behaviour.
nf.origin.y = (NSHeight(vf) - NSHeight(nf)) / 2
}
return nf
}
@objc func setNormalWindowSize() { setWindowScale(1.0) }
@objc func setHalfWindowSize() { setWindowScale(0.5) }
@objc func setDoubleWindowSize() { setWindowScale(2.0) }
func setWindowScale(_ scale: Double) {
mpv?.command("set window-scale \(scale)")
}
func addWindowScale(_ scale: Double) {
if !isInFullscreen {
mpv?.command("add window-scale \(scale)")
}
}
func windowDidChangeScreen(_ notification: Notification) {
if screen == nil {
return
}
if !isAnimating && (currentScreen != screen) {
previousScreen = screen
}
if currentScreen != screen {
cocoaCB.updateDisplaylink()
cocoaCB.layer?.update(force: true)
}
currentScreen = screen
}
func windowDidChangeScreenProfile(_ notification: Notification) {
cocoaCB.layer?.needsICCUpdate = true
}
func windowDidChangeBackingProperties(_ notification: Notification) {
cocoaCB.layer?.contentsScale = backingScaleFactor
cocoaCB.flagEvents(VO_EVENT_DPI)
}
func windowWillStartLiveResize(_ notification: Notification) {
cocoaCB.layer?.inLiveResize = true
}
func windowDidEndLiveResize(_ notification: Notification) {
cocoaCB.layer?.inLiveResize = false
cocoaCB.mpv?.setConfigProperty(maximized: isZoomed)
}
func windowShouldClose(_ sender: NSWindow) -> Bool {
cocoa_put_key(MP_KEY_CLOSE_WIN)
return false
}
func windowDidMiniaturize(_ notification: Notification) {
cocoaCB.mpv?.setConfigProperty(minimized: true)
}
func windowDidDeminiaturize(_ notification: Notification) {
cocoaCB.mpv?.setConfigProperty(minimized: false)
}
func windowDidResignKey(_ notification: Notification) {
cocoaCB.setCursorVisiblility(true)
}
func windowDidBecomeKey(_ notification: Notification) {
cocoaCB.updateCursorVisibility()
}
func windowDidChangeOcclusionState(_ notification: Notification) {
if occlusionState.contains(.visible) {
cocoaCB.layer?.update(force: true)
cocoaCB.updateCursorVisibility()
}
}
func windowWillMove(_ notification: Notification) {
isMoving = true
}
func windowDidMove(_ notification: Notification) {
cocoaCB.mpv?.setConfigProperty(maximized: isZoomed)
}
}