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