1 /*
2  * This file is part of mpv.
3  *
4  * mpv is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * mpv is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 import Cocoa
19 
20 class Window: NSWindow, NSWindowDelegate {
21     weak var common: Common! = nil
22     var mpv: MPVHelper? { get { return common.mpv } }
23 
24     var targetScreen: NSScreen?
25     var previousScreen: NSScreen?
26     var currentScreen: NSScreen?
27     var unfScreen: NSScreen?
28 
29     var unfsContentFrame: NSRect?
30     var isInFullscreen: Bool = false
31     var isAnimating: Bool = false
32     var isMoving: Bool = false
33     var previousStyleMask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable]
34 
35     var unfsContentFramePixel: NSRect { get { return convertToBacking(unfsContentFrame ?? NSRect(x: 0, y: 0, width: 160, height: 90)) } }
36     var framePixel: NSRect { get { return convertToBacking(frame) } }
37 
38     var keepAspect: Bool = true {
39         didSet {
40             if let contentViewFrame = contentView?.frame, !isInFullscreen {
41                 unfsContentFrame = convertToScreen(contentViewFrame)
42             }
43 
44             if keepAspect {
45                 contentAspectRatio = unfsContentFrame?.size ?? contentAspectRatio
46             } else {
47                 resizeIncrements = NSSize(width: 1.0, height: 1.0)
48             }
49         }
50     }
51 
52     var border: Bool = true {
53         didSet { if !border { common.titleBar?.hide() } }
54     }
55 
56     override var canBecomeKey: Bool { return true }
57     override var canBecomeMain: Bool { return true }
58 
59     override var styleMask: NSWindow.StyleMask {
60         get { return super.styleMask }
61         set {
62             let responder = firstResponder
63             let windowTitle = title
64             previousStyleMask = super.styleMask
65             super.styleMask = newValue
66             makeFirstResponder(responder)
67             title = windowTitle
68         }
69     }
70 
71     convenience init(contentRect: NSRect, screen: NSScreen?, view: NSView, common com: Common) {
72         self.init(contentRect: contentRect,
73                   styleMask: [.titled, .closable, .miniaturizable, .resizable],
74                   backing: .buffered, defer: false, screen: screen)
75 
76         // workaround for an AppKit bug where the NSWindow can't be placed on a
77         // none Main screen NSScreen outside the Main screen's frame bounds
78         if let wantedScreen = screen, screen != NSScreen.main {
79             var absoluteWantedOrigin = contentRect.origin
80             absoluteWantedOrigin.x += wantedScreen.frame.origin.x
81             absoluteWantedOrigin.y += wantedScreen.frame.origin.y
82 
83             if !NSEqualPoints(absoluteWantedOrigin, self.frame.origin) {
84                 self.setFrameOrigin(absoluteWantedOrigin)
85             }
86         }
87 
88         common = com
89         title = com.title
90         minSize = NSMakeSize(160, 90)
91         collectionBehavior = .fullScreenPrimary
92         delegate = self
93 
94         if let cView = contentView {
95             cView.addSubview(view)
96             view.frame = cView.frame
97             unfsContentFrame = convertToScreen(cView.frame)
98         }
99 
100         targetScreen = screen
101         currentScreen = screen
102         unfScreen = screen
103 
104         if let app = NSApp as? Application {
105             app.menuBar.register(#selector(setHalfWindowSize), for: MPM_H_SIZE)
106             app.menuBar.register(#selector(setNormalWindowSize), for: MPM_N_SIZE)
107             app.menuBar.register(#selector(setDoubleWindowSize), for: MPM_D_SIZE)
108             app.menuBar.register(#selector(performMiniaturize(_:)), for: MPM_MINIMIZE)
109             app.menuBar.register(#selector(performZoom(_:)), for: MPM_ZOOM)
110         }
111     }
112 
toggleFullScreennull113     override func toggleFullScreen(_ sender: Any?) {
114         if isAnimating {
115             return
116         }
117 
118         isAnimating = true
119 
120         targetScreen = common.getTargetScreen(forFullscreen: !isInFullscreen)
121         if targetScreen == nil && previousScreen == nil {
122             targetScreen = screen
123         } else if targetScreen == nil {
124             targetScreen = previousScreen
125             previousScreen = nil
126         } else {
127             previousScreen = screen
128         }
129 
130         if let contentViewFrame = contentView?.frame, !isInFullscreen {
131             unfsContentFrame = convertToScreen(contentViewFrame)
132             unfScreen = screen
133         }
134         // move window to target screen when going to fullscreen
135         if let tScreen = targetScreen, !isInFullscreen && (tScreen != screen) {
136             let frame = calculateWindowPosition(for: tScreen, withoutBounds: false)
137             setFrame(frame, display: true)
138         }
139 
140         if Bool(mpv?.opts.native_fs ?? 1) {
141             super.toggleFullScreen(sender)
142         } else {
143             if !isInFullscreen {
144                 setToFullScreen()
145             }
146             else {
147                 setToWindow()
148             }
149         }
150     }
151 
customWindowsToEnterFullScreennull152     func customWindowsToEnterFullScreen(for window: NSWindow) -> [NSWindow]? {
153         return [window]
154     }
155 
customWindowsToExitFullScreennull156     func customWindowsToExitFullScreen(for window: NSWindow) -> [NSWindow]? {
157         return [window]
158     }
159 
windownull160     func window(_ window: NSWindow, startCustomAnimationToEnterFullScreenWithDuration duration: TimeInterval) {
161         guard let tScreen = targetScreen else { return }
162         common.view?.layerContentsPlacement = .scaleProportionallyToFit
163         common.titleBar?.hide()
164         NSAnimationContext.runAnimationGroup({ (context) -> Void in
165             context.duration = getFsAnimationDuration(duration - 0.05)
166             window.animator().setFrame(tScreen.frame, display: true)
167         }, completionHandler: nil)
168     }
169 
windownull170     func window(_ window: NSWindow, startCustomAnimationToExitFullScreenWithDuration duration: TimeInterval) {
171         guard let tScreen = targetScreen, let currentScreen = screen else { return }
172         let newFrame = calculateWindowPosition(for: tScreen, withoutBounds: tScreen == screen)
173         let intermediateFrame = aspectFit(rect: newFrame, in: currentScreen.frame)
174         common.titleBar?.hide(0.0)
175 
176         NSAnimationContext.runAnimationGroup({ (context) -> Void in
177             context.duration = 0.0
178             common.view?.layerContentsPlacement = .scaleProportionallyToFill
179             window.animator().setFrame(intermediateFrame, display: true)
180         }, completionHandler: {
181             NSAnimationContext.runAnimationGroup({ (context) -> Void in
182                 context.duration = self.getFsAnimationDuration(duration - 0.05)
183                 self.styleMask.remove(.fullScreen)
184                 window.animator().setFrame(newFrame, display: true)
185             }, completionHandler: nil)
186         })
187     }
188 
windowDidEnterFullScreennull189     func windowDidEnterFullScreen(_ notification: Notification) {
190         isInFullscreen = true
191         mpv?.setOption(fullscreen: isInFullscreen)
192         common.updateCursorVisibility()
193         endAnimation(frame)
194         common.titleBar?.show()
195     }
196 
windowDidExitFullScreennull197     func windowDidExitFullScreen(_ notification: Notification) {
198         guard let tScreen = targetScreen else { return }
199         isInFullscreen = false
200         mpv?.setOption(fullscreen: isInFullscreen)
201         endAnimation(calculateWindowPosition(for: tScreen, withoutBounds: targetScreen == screen))
202         common.view?.layerContentsPlacement = .scaleProportionallyToFit
203     }
204 
windowDidFailToEnterFullScreennull205     func windowDidFailToEnterFullScreen(_ window: NSWindow) {
206         guard let tScreen = targetScreen else { return }
207         let newFrame = calculateWindowPosition(for: tScreen, withoutBounds: targetScreen == screen)
208         setFrame(newFrame, display: true)
209         endAnimation()
210     }
211 
windowDidFailToExitFullScreennull212     func windowDidFailToExitFullScreen(_ window: NSWindow) {
213         guard let targetFrame = targetScreen?.frame else { return }
214         setFrame(targetFrame, display: true)
215         endAnimation()
216         common.view?.layerContentsPlacement = .scaleProportionallyToFit
217     }
218 
endAnimationnull219     func endAnimation(_ newFrame: NSRect = NSZeroRect) {
220         if !NSEqualRects(newFrame, NSZeroRect) && isAnimating {
221             NSAnimationContext.runAnimationGroup({ (context) -> Void in
222                 context.duration = 0.01
223                 self.animator().setFrame(newFrame, display: true)
224             }, completionHandler: nil )
225         }
226 
227         isAnimating = false
228         common.windowDidEndAnimation()
229     }
230 
setToFullScreennull231     func setToFullScreen() {
232         guard let targetFrame = targetScreen?.frame else { return }
233 
234         if #available(macOS 11.0, *) {
235             styleMask = .borderless
236             common.titleBar?.hide(0.0)
237         } else {
238             styleMask.insert(.fullScreen)
239         }
240 
241         NSApp.presentationOptions = [.autoHideMenuBar, .autoHideDock]
242         setFrame(targetFrame, display: true)
243         endAnimation()
244         isInFullscreen = true
245         mpv?.setOption(fullscreen: isInFullscreen)
246         common.windowSetToFullScreen()
247     }
248 
setToWindownull249     func setToWindow() {
250         guard let tScreen = targetScreen else { return }
251 
252         if #available(macOS 11.0, *) {
253             styleMask = previousStyleMask
254             common.titleBar?.hide(0.0)
255         } else {
256             styleMask.remove(.fullScreen)
257         }
258 
259         let newFrame = calculateWindowPosition(for: tScreen, withoutBounds: targetScreen == screen)
260         NSApp.presentationOptions = []
261         setFrame(newFrame, display: true)
262         endAnimation()
263         isInFullscreen = false
264         mpv?.setOption(fullscreen: isInFullscreen)
265         common.windowSetToWindow()
266     }
267 
getFsAnimationDurationnull268     func getFsAnimationDuration(_ def: Double) -> Double {
269         let duration = mpv?.macOpts.macos_fs_animation_duration ?? -1
270         if duration < 0 {
271             return def
272         } else {
273             return Double(duration)/1000
274         }
275     }
276 
setOnTopnull277     func setOnTop(_ state: Bool, _ ontopLevel: Int) {
278         if state {
279             switch ontopLevel {
280             case -1:
281                 level = .floating
282             case -2:
283                 level = .statusBar + 1
284             case -3:
285                 level = NSWindow.Level(Int(CGWindowLevelForKey(.desktopWindow)))
286             default:
287                 level = NSWindow.Level(ontopLevel)
288             }
289             collectionBehavior.remove(.transient)
290             collectionBehavior.insert(.managed)
291         } else {
292             level = .normal
293         }
294     }
295 
setOnAllWorkspacesnull296     func setOnAllWorkspaces(_ state: Bool) {
297         if state {
298             collectionBehavior.insert(.canJoinAllSpaces)
299         } else {
300             collectionBehavior.remove(.canJoinAllSpaces)
301         }
302     }
303 
setMinimizednull304     func setMinimized(_ stateWanted: Bool) {
305         if isMiniaturized == stateWanted { return }
306 
307         if stateWanted {
308             performMiniaturize(self)
309         } else {
310             deminiaturize(self)
311         }
312     }
313 
setMaximizednull314     func setMaximized(_ stateWanted: Bool) {
315         if isZoomed == stateWanted { return }
316 
317         zoom(self)
318     }
319 
updateMovableBackgroundnull320     func updateMovableBackground(_ pos: NSPoint) {
321         if !isInFullscreen {
322             isMovableByWindowBackground = mpv?.canBeDraggedAt(pos) ?? true
323         } else {
324             isMovableByWindowBackground = false
325         }
326     }
327 
updateFramenull328     func updateFrame(_ rect: NSRect) {
329         if rect != frame {
330             let cRect = frameRect(forContentRect: rect)
331             unfsContentFrame = rect
332             setFrame(cRect, display: true)
333             common.windowDidUpdateFrame()
334         }
335     }
336 
updateSizenull337     func updateSize(_ size: NSSize) {
338         if let currentSize = contentView?.frame.size, size != currentSize {
339             let newContentFrame = centeredContentSize(for: frame, size: size)
340             if !isInFullscreen {
341                 updateFrame(newContentFrame)
342             } else {
343                 unfsContentFrame = newContentFrame
344             }
345         }
346     }
347 
setFramenull348     override func setFrame(_ frameRect: NSRect, display flag: Bool) {
349         if frameRect.width < minSize.width || frameRect.height < minSize.height {
350             common.log.sendVerbose("tried to set too small window size: \(frameRect.size)")
351             return
352         }
353 
354         super.setFrame(frameRect, display: flag)
355 
356         if let size = unfsContentFrame?.size, keepAspect {
357             contentAspectRatio = size
358         }
359     }
360 
centeredContentSizenull361     func centeredContentSize(for rect: NSRect, size sz: NSSize) -> NSRect {
362         let cRect = contentRect(forFrameRect: rect)
363         let dx = (cRect.size.width  - sz.width)  / 2
364         let dy = (cRect.size.height - sz.height) / 2
365         return NSInsetRect(cRect, dx, dy)
366     }
367 
aspectFitnull368     func aspectFit(rect r: NSRect, in rTarget: NSRect) -> NSRect {
369         var s = rTarget.width / r.width
370         if r.height*s > rTarget.height {
371             s = rTarget.height / r.height
372         }
373         let w = r.width * s
374         let h = r.height * s
375         return NSRect(x: rTarget.midX - w/2, y: rTarget.midY - h/2, width: w, height: h)
376     }
377 
calculateWindowPositionnull378     func calculateWindowPosition(for tScreen: NSScreen, withoutBounds: Bool) -> NSRect {
379         guard let contentFrame = unfsContentFrame, let screen = unfScreen else {
380             return frame
381         }
382         var newFrame = frameRect(forContentRect: contentFrame)
383         let targetFrame = tScreen.frame
384         let targetVisibleFrame = tScreen.visibleFrame
385         let unfsScreenFrame = screen.frame
386         let visibleWindow = NSIntersectionRect(unfsScreenFrame, newFrame)
387 
388         // calculate visible area of every side
389         let left = newFrame.origin.x - unfsScreenFrame.origin.x
390         let right = unfsScreenFrame.size.width -
391             (newFrame.origin.x - unfsScreenFrame.origin.x + newFrame.size.width)
392         let bottom = newFrame.origin.y - unfsScreenFrame.origin.y
393         let top = unfsScreenFrame.size.height -
394             (newFrame.origin.y - unfsScreenFrame.origin.y + newFrame.size.height)
395 
396         // normalize visible areas, decide which one to take horizontal/vertical
397         var xPer = (unfsScreenFrame.size.width - visibleWindow.size.width)
398         var yPer = (unfsScreenFrame.size.height - visibleWindow.size.height)
399         if xPer != 0 { xPer = (left >= 0 || right < 0 ? left : right) / xPer }
400         if yPer != 0 { yPer = (bottom >= 0 || top < 0 ? bottom : top) / yPer }
401 
402         // calculate visible area for every side for target screen
403         let xNewLeft = targetFrame.origin.x +
404             (targetFrame.size.width - visibleWindow.size.width) * xPer
405         let xNewRight = targetFrame.origin.x + targetFrame.size.width -
406             (targetFrame.size.width - visibleWindow.size.width) * xPer - newFrame.size.width
407         let yNewBottom = targetFrame.origin.y +
408             (targetFrame.size.height - visibleWindow.size.height) * yPer
409         let yNewTop = targetFrame.origin.y + targetFrame.size.height -
410             (targetFrame.size.height - visibleWindow.size.height) * yPer - newFrame.size.height
411 
412         // calculate new coordinates, decide which one to take horizontal/vertical
413         newFrame.origin.x = left >= 0 || right < 0 ? xNewLeft : xNewRight
414         newFrame.origin.y = bottom >= 0 || top < 0 ? yNewBottom : yNewTop
415 
416         // don't place new window on top of a visible menubar
417         let topMar = targetFrame.size.height -
418             (newFrame.origin.y - targetFrame.origin.y + newFrame.size.height)
419         let menuBarHeight = targetFrame.size.height -
420             (targetVisibleFrame.size.height + targetVisibleFrame.origin.y)
421         if topMar < menuBarHeight {
422             newFrame.origin.y -= top - menuBarHeight
423         }
424 
425         if withoutBounds {
426             return newFrame
427         }
428 
429         // screen bounds right and left
430         if newFrame.origin.x + newFrame.size.width > targetFrame.origin.x + targetFrame.size.width {
431             newFrame.origin.x = targetFrame.origin.x + targetFrame.size.width - newFrame.size.width
432         }
433         if newFrame.origin.x < targetFrame.origin.x {
434             newFrame.origin.x = targetFrame.origin.x
435         }
436 
437         // screen bounds top and bottom
438         if newFrame.origin.y + newFrame.size.height > targetFrame.origin.y + targetFrame.size.height {
439             newFrame.origin.y = targetFrame.origin.y + targetFrame.size.height - newFrame.size.height
440         }
441         if newFrame.origin.y < targetFrame.origin.y {
442             newFrame.origin.y = targetFrame.origin.y
443         }
444         return newFrame
445     }
446 
constrainFrameRectnull447     override func constrainFrameRect(_ frameRect: NSRect, to tScreen: NSScreen?) -> NSRect {
448         if (isAnimating && !isInFullscreen) || (!isAnimating && isInFullscreen ||
449             level == NSWindow.Level(Int(CGWindowLevelForKey(.desktopWindow))))
450         {
451             return frameRect
452         }
453 
454         guard let ts: NSScreen = tScreen ?? screen ?? NSScreen.main else {
455             return frameRect
456         }
457         var nf: NSRect = frameRect
458         let of: NSRect = frame
459         let vf: NSRect = (isAnimating ? (targetScreen ?? ts) : ts).visibleFrame
460         let ncf: NSRect = contentRect(forFrameRect: nf)
461 
462         // screen bounds top and bottom
463         if NSMaxY(nf) > NSMaxY(vf) {
464             nf.origin.y = NSMaxY(vf) - NSHeight(nf)
465         }
466         if NSMaxY(ncf) < NSMinY(vf) {
467             nf.origin.y = NSMinY(vf) + NSMinY(ncf) - NSMaxY(ncf)
468         }
469 
470         // screen bounds right and left
471         if NSMinX(nf) > NSMaxX(vf) {
472             nf.origin.x = NSMaxX(vf) - NSWidth(nf)
473         }
474         if NSMaxX(nf) < NSMinX(vf) {
475             nf.origin.x = NSMinX(vf)
476         }
477 
478         if NSHeight(nf) < NSHeight(vf) && NSHeight(of) > NSHeight(vf) && !isInFullscreen {
479             // If the window height is smaller than the visible frame, but it was
480             // bigger previously recenter the smaller window vertically. This is
481             // needed to counter the 'snap to top' behaviour.
482             nf.origin.y = (NSHeight(vf) - NSHeight(nf)) / 2
483         }
484         return nf
485     }
486 
setNormalWindowSizenull487     @objc func setNormalWindowSize() { setWindowScale(1.0) }
setHalfWindowSizenull488     @objc func setHalfWindowSize()   { setWindowScale(0.5) }
setDoubleWindowSizenull489     @objc func setDoubleWindowSize() { setWindowScale(2.0) }
490 
setWindowScalenull491     func setWindowScale(_ scale: Double) {
492         mpv?.command("set window-scale \(scale)")
493     }
494 
addWindowScalenull495     func addWindowScale(_ scale: Double) {
496         if !isInFullscreen {
497             mpv?.command("add window-scale \(scale)")
498         }
499     }
500 
windowDidChangeScreennull501     func windowDidChangeScreen(_ notification: Notification) {
502         if screen == nil {
503             return
504         }
505         if !isAnimating && (currentScreen != screen) {
506             previousScreen = screen
507         }
508         if currentScreen != screen {
509             common.updateDisplaylink()
510             common.windowDidChangeScreen()
511         }
512         currentScreen = screen
513     }
514 
windowDidChangeScreenProfilenull515     func windowDidChangeScreenProfile(_ notification: Notification) {
516         common.windowDidChangeScreenProfile()
517     }
518 
windowDidChangeBackingPropertiesnull519     func windowDidChangeBackingProperties(_ notification: Notification) {
520         common.windowDidChangeBackingProperties()
521         common.flagEvents(VO_EVENT_DPI)
522     }
523 
windowWillStartLiveResizenull524     func windowWillStartLiveResize(_ notification: Notification) {
525         common.windowWillStartLiveResize()
526     }
527 
windowDidEndLiveResizenull528     func windowDidEndLiveResize(_ notification: Notification) {
529         common.windowDidEndLiveResize()
530         mpv?.setOption(maximized: isZoomed)
531 
532         if let contentViewFrame = contentView?.frame,
533                !isAnimating && !isInFullscreen
534         {
535             unfsContentFrame = convertToScreen(contentViewFrame)
536         }
537     }
538 
windowDidResizenull539     func windowDidResize(_ notification: Notification) {
540         common.windowDidResize()
541     }
542 
windowShouldClosenull543     func windowShouldClose(_ sender: NSWindow) -> Bool {
544         cocoa_put_key(MP_KEY_CLOSE_WIN)
545         return false
546     }
547 
windowDidMiniaturizenull548     func windowDidMiniaturize(_ notification: Notification) {
549         mpv?.setOption(minimized: true)
550     }
551 
windowDidDeminiaturizenull552     func windowDidDeminiaturize(_ notification: Notification) {
553         mpv?.setOption(minimized: false)
554     }
555 
windowDidResignKeynull556     func windowDidResignKey(_ notification: Notification) {
557         common.setCursorVisiblility(true)
558     }
559 
windowDidBecomeKeynull560     func windowDidBecomeKey(_ notification: Notification) {
561         common.updateCursorVisibility()
562     }
563 
windowDidChangeOcclusionStatenull564     func windowDidChangeOcclusionState(_ notification: Notification) {
565         if occlusionState.contains(.visible) {
566             common.windowDidChangeOcclusionState()
567             common.updateCursorVisibility()
568         }
569     }
570 
windowWillMovenull571     func windowWillMove(_ notification: Notification) {
572         isMoving = true
573     }
574 
windowDidMovenull575     func windowDidMove(_ notification: Notification) {
576         mpv?.setOption(maximized: isZoomed)
577     }
578 }
579