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#include <libavutil/common.h> 19 20#include "input/keycodes.h" 21 22#include "osdep/macosx_events.h" 23#include "osdep/macosx_compat.h" 24#include "video/out/cocoa_common.h" 25 26#include "window.h" 27 28@interface MpvVideoWindow() 29@property(nonatomic, retain) NSScreen *targetScreen; 30@property(nonatomic, retain) NSScreen *previousScreen; 31@property(nonatomic, retain) NSScreen *currentScreen; 32@property(nonatomic, retain) NSScreen *unfScreen; 33 34- (NSRect)frameRect:(NSRect)frameRect forCenteredContentSize:(NSSize)newSize; 35- (void)setCenteredContentSize:(NSSize)newSize; 36@end 37 38@implementation MpvVideoWindow { 39 NSSize _queued_video_size; 40 NSRect _unfs_content_frame; 41 int _is_animating; 42} 43 44@synthesize adapter = _adapter; 45@synthesize targetScreen = _target_screen; 46@synthesize previousScreen = _previous_screen; 47@synthesize currentScreen = _current_screen; 48@synthesize unfScreen = _unf_screen; 49 50- (id)initWithContentRect:(NSRect)content_rect 51 styleMask:(NSWindowStyleMask)style_mask 52 backing:(NSBackingStoreType)buffering_type 53 defer:(BOOL)flag 54 screen:(NSScreen *)screen 55{ 56 if (self = [super initWithContentRect:content_rect 57 styleMask:style_mask 58 backing:buffering_type 59 defer:flag 60 screen:screen]) { 61 [self setBackgroundColor:[NSColor whiteColor]]; 62 [self setMinSize:NSMakeSize(50,50)]; 63 [self setCollectionBehavior: NSWindowCollectionBehaviorFullScreenPrimary]; 64 65 self.targetScreen = screen; 66 self.currentScreen = screen; 67 self.unfScreen = screen; 68 _is_animating = 0; 69 _unfs_content_frame = [self convertRectToScreen:[[self contentView] frame]]; 70 } 71 return self; 72} 73 74- (void)setStyleMask:(NSWindowStyleMask)style 75{ 76 NSResponder *nR = [self firstResponder]; 77 [super setStyleMask:style]; 78 [self makeFirstResponder:nR]; 79} 80 81- (void)toggleFullScreen:(id)sender 82{ 83 if (_is_animating) 84 return; 85 86 _is_animating = 1; 87 88 self.targetScreen = [self.adapter getTargetScreen]; 89 if(![self targetScreen] && ![self previousScreen]) { 90 self.targetScreen = [self screen]; 91 } else if (![self targetScreen]) { 92 self.targetScreen = self.previousScreen; 93 self.previousScreen = nil; 94 } else { 95 self.previousScreen = [self screen]; 96 } 97 98 if (![self.adapter isInFullScreenMode]) { 99 _unfs_content_frame = [self convertRectToScreen:[[self contentView] frame]]; 100 self.unfScreen = [self screen]; 101 } 102 103 //move window to target screen when going to fullscreen 104 if (![self.adapter isInFullScreenMode] && ![[self targetScreen] isEqual:[self screen]]) { 105 NSRect frame = [self calculateWindowPositionForScreen:[self targetScreen] 106 withoutBounds:NO]; 107 [self setFrame:frame display:YES]; 108 } 109 110 if ([self.adapter wantsNativeFullscreen]) 111 [super toggleFullScreen:sender]; 112 113 if (![self.adapter isInFullScreenMode]) { 114 [self setToFullScreen]; 115 } else { 116 [self setToWindow]; 117 } 118} 119 120- (void)setToFullScreen 121{ 122 [self setStyleMask:([self styleMask] | NSWindowStyleMaskFullScreen)]; 123 NSRect frame = [[self targetScreen] frame]; 124 125 if ([self.adapter wantsNativeFullscreen]) { 126 [self setFrame:frame display:YES]; 127 } else { 128 [NSApp setPresentationOptions:NSApplicationPresentationAutoHideMenuBar| 129 NSApplicationPresentationAutoHideDock]; 130 [self setFrame:frame display:YES]; 131 _is_animating = 0; 132 [self.adapter windowDidEnterFullScreen]; 133 } 134} 135 136- (void)setToWindow 137{ 138 [self setStyleMask:([self styleMask] & ~NSWindowStyleMaskFullScreen)]; 139 NSRect frame = [self calculateWindowPositionForScreen:[self targetScreen] 140 withoutBounds:[[self targetScreen] isEqual:[self screen]]]; 141 142 if ([self.adapter wantsNativeFullscreen]) { 143 [self setFrame:frame display:YES]; 144 [self setContentAspectRatio:_unfs_content_frame.size]; 145 [self setCenteredContentSize:_unfs_content_frame.size]; 146 } else { 147 [NSApp setPresentationOptions:NSApplicationPresentationDefault]; 148 [self setFrame:frame display:YES]; 149 [self setContentAspectRatio:_unfs_content_frame.size]; 150 [self setCenteredContentSize:_unfs_content_frame.size]; 151 _is_animating = 0; 152 [self.adapter windowDidExitFullScreen]; 153 } 154} 155 156- (NSArray *)customWindowsToEnterFullScreenForWindow:(NSWindow *)window 157{ 158 return [NSArray arrayWithObject:window]; 159} 160 161- (NSArray*)customWindowsToExitFullScreenForWindow:(NSWindow*)window 162{ 163 return [NSArray arrayWithObject:window]; 164} 165 166// we still need to keep those around or it will use the standard animation 167- (void)window:(NSWindow *)window startCustomAnimationToEnterFullScreenWithDuration:(NSTimeInterval)duration {} 168 169- (void)window:(NSWindow *)window startCustomAnimationToExitFullScreenWithDuration:(NSTimeInterval)duration {} 170 171- (void)windowDidEnterFullScreen:(NSNotification *)notification 172{ 173 _is_animating = 0; 174 [self.adapter windowDidEnterFullScreen]; 175} 176 177- (void)windowDidExitFullScreen:(NSNotification *)notification 178{ 179 _is_animating = 0; 180 [self.adapter windowDidExitFullScreen]; 181} 182 183- (void)windowWillEnterFullScreen:(NSNotification *)notification 184{ 185 [self.adapter windowWillEnterFullScreen:notification]; 186} 187 188- (void)windowWillExitFullScreen:(NSNotification *)notification 189{ 190 [self.adapter windowWillExitFullScreen:notification]; 191} 192 193- (void)windowDidFailToEnterFullScreen:(NSWindow *)window 194{ 195 _is_animating = 0; 196 [self setToWindow]; 197 [self.adapter windowDidFailToEnterFullScreen:window]; 198} 199 200- (void)windowDidFailToExitFullScreen:(NSWindow *)window 201{ 202 _is_animating = 0; 203 [self setToFullScreen]; 204 [self.adapter windowDidFailToExitFullScreen:window]; 205} 206 207- (void)windowDidChangeBackingProperties:(NSNotification *)notification 208{ 209 // XXX: we maybe only need expose for this 210 [self.adapter setNeedsResize]; 211} 212 213- (void)windowDidChangeScreen:(NSNotification *)notification 214{ 215 [self.adapter windowDidChangeScreen:notification]; 216 217 if (!_is_animating && ![[self currentScreen] isEqual:[self screen]]) { 218 self.previousScreen = [self screen]; 219 } 220 if (![[self currentScreen] isEqual:[self screen]]) { 221 [self.adapter windowDidChangePhysicalScreen]; 222 } 223 224 self.currentScreen = [self screen]; 225} 226 227- (void)windowDidChangeScreenProfile:(NSNotification *)notification 228{ 229 [self.adapter didChangeWindowedScreenProfile:notification]; 230} 231 232- (void)windowDidResignKey:(NSNotification *)notification 233{ 234 [self.adapter windowDidResignKey:notification]; 235} 236 237- (void)windowDidBecomeKey:(NSNotification *)notification 238{ 239 [self.adapter windowDidBecomeKey:notification]; 240} 241 242- (void)windowWillMove:(NSNotification *)notification 243{ 244 [self.adapter windowWillMove:notification]; 245} 246 247- (BOOL)canBecomeMainWindow { return YES; } 248- (BOOL)canBecomeKeyWindow { return YES; } 249 250- (BOOL)windowShouldClose:(id)sender 251{ 252 cocoa_put_key(MP_KEY_CLOSE_WIN); 253 // We have to wait for MPlayer to handle this, 254 // otherwise we are in trouble if the 255 // MP_KEY_CLOSE_WIN handler is disabled 256 return NO; 257} 258 259- (void)normalSize { [self mulSize:1.0f]; } 260 261- (void)halfSize { [self mulSize:0.5f];} 262 263- (void)doubleSize { [self mulSize:2.0f];} 264 265- (void)mulSize:(float)multiplier 266{ 267 char cmd[50]; 268 snprintf(cmd, sizeof(cmd), "set window-scale %f", multiplier); 269 [self.adapter putCommand:cmd]; 270} 271 272- (void)updateBorder:(int)border 273{ 274 int borderStyle = NSWindowStyleMaskTitled|NSWindowStyleMaskClosable| 275 NSWindowStyleMaskMiniaturizable; 276 if (border) { 277 int window_mask = [self styleMask] & ~NSWindowStyleMaskBorderless; 278 window_mask |= borderStyle; 279 [self setStyleMask:window_mask]; 280 } else { 281 int window_mask = [self styleMask] & ~borderStyle; 282 window_mask |= NSWindowStyleMaskBorderless; 283 [self setStyleMask:window_mask]; 284 } 285 286 if (![self.adapter isInFullScreenMode]) { 287 // XXX: workaround to force redrawing of window decoration 288 if (border) { 289 NSRect frame = [self frame]; 290 frame.size.width += 1; 291 [self setFrame:frame display:YES]; 292 frame.size.width -= 1; 293 [self setFrame:frame display:YES]; 294 } 295 296 [self setContentAspectRatio:_unfs_content_frame.size]; 297 } 298} 299 300- (NSRect)frameRect:(NSRect)f forCenteredContentSize:(NSSize)ns 301{ 302 NSRect cr = [self contentRectForFrameRect:f]; 303 CGFloat dx = (cr.size.width - ns.width) / 2; 304 CGFloat dy = (cr.size.height - ns.height) / 2; 305 return NSInsetRect(f, dx, dy); 306} 307 308- (void)setCenteredContentSize:(NSSize)ns 309{ 310 [self setFrame:[self frameRect:[self frame] forCenteredContentSize:ns] 311 display:NO 312 animate:NO]; 313} 314 315- (NSRect)calculateWindowPositionForScreen:(NSScreen *)screen withoutBounds:(BOOL)withoutBounds 316{ 317 NSRect frame = [self frameRectForContentRect:_unfs_content_frame]; 318 NSRect targetFrame = [screen frame]; 319 NSRect targetVisibleFrame = [screen visibleFrame]; 320 NSRect unfsScreenFrame = [self.unfScreen frame]; 321 NSRect visibleWindow = NSIntersectionRect(unfsScreenFrame, frame); 322 323 // calculate visible area of every side 324 CGFloat left = frame.origin.x - unfsScreenFrame.origin.x; 325 CGFloat right = unfsScreenFrame.size.width - 326 (frame.origin.x - unfsScreenFrame.origin.x + frame.size.width); 327 CGFloat bottom = frame.origin.y - unfsScreenFrame.origin.y; 328 CGFloat top = unfsScreenFrame.size.height - 329 (frame.origin.y - unfsScreenFrame.origin.y + frame.size.height); 330 331 // normalize visible areas, decide which one to take horizontal/vertical 332 CGFloat x_per = (unfsScreenFrame.size.width - visibleWindow.size.width); 333 CGFloat y_per = (unfsScreenFrame.size.height - visibleWindow.size.height); 334 if (x_per != 0) x_per = (left >= 0 || right < 0 ? left : right)/x_per; 335 if (y_per != 0) y_per = (bottom >= 0 || top < 0 ? bottom : top)/y_per; 336 337 // calculate visible area for every side for target screen 338 CGFloat x_new_left = targetFrame.origin.x + 339 (targetFrame.size.width - visibleWindow.size.width)*x_per; 340 CGFloat x_new_right = targetFrame.origin.x + targetFrame.size.width - 341 (targetFrame.size.width - visibleWindow.size.width)*x_per - frame.size.width; 342 CGFloat y_new_bottom = targetFrame.origin.y + 343 (targetFrame.size.height - visibleWindow.size.height)*y_per; 344 CGFloat y_new_top = targetFrame.origin.y + targetFrame.size.height - 345 (targetFrame.size.height - visibleWindow.size.height)*y_per - frame.size.height; 346 347 // calculate new coordinates, decide which one to take horizontal/vertical 348 frame.origin.x = left >= 0 || right < 0 ? x_new_left : x_new_right; 349 frame.origin.y = bottom >= 0 || top < 0 ? y_new_bottom : y_new_top; 350 351 // don't place new window on top of a visible menubar 352 CGFloat top_mar = targetFrame.size.height - 353 (frame.origin.y - targetFrame.origin.y + frame.size.height); 354 CGFloat menuBarHeight = targetFrame.size.height - 355 (targetVisibleFrame.size.height + targetVisibleFrame.origin.y); 356 357 if (top_mar < menuBarHeight) 358 frame.origin.y -= top-menuBarHeight; 359 360 if (withoutBounds) 361 return frame; 362 363 //screen bounds right and left 364 if (frame.origin.x + frame.size.width > targetFrame.origin.x + targetFrame.size.width) 365 frame.origin.x = targetFrame.origin.x + targetFrame.size.width - frame.size.width; 366 if (frame.origin.x < targetFrame.origin.x) 367 frame.origin.x = targetFrame.origin.x; 368 369 //screen bounds top and bottom 370 if (frame.origin.y + frame.size.height > targetFrame.origin.y + targetFrame.size.height) 371 frame.origin.y = targetFrame.origin.y + targetFrame.size.height - frame.size.height; 372 if (frame.origin.y < targetFrame.origin.y) 373 frame.origin.y = targetFrame.origin.y; 374 375 return frame; 376} 377 378- (NSRect)constrainFrameRect:(NSRect)nf toScreen:(NSScreen *)screen 379{ 380 if ((_is_animating && ![self.adapter isInFullScreenMode]) || 381 (!_is_animating && [self.adapter isInFullScreenMode])) 382 { 383 return nf; 384 } 385 386 screen = screen ?: self.screen ?: [NSScreen mainScreen]; 387 NSRect of = [self frame]; 388 NSRect vf = [_is_animating ? [self targetScreen] : screen visibleFrame]; 389 NSRect ncf = [self contentRectForFrameRect:nf]; 390 391 // Prevent the window's titlebar from exiting the screen on the top edge. 392 // This introduces a 'snap to top' behaviour. 393 if (NSMaxY(nf) > NSMaxY(vf)) 394 nf.origin.y = NSMaxY(vf) - NSHeight(nf); 395 396 // Prevent the window's titlebar from exiting the screen on the bottom edge. 397 if (NSMaxY(ncf) < NSMinY(vf)) 398 nf.origin.y = NSMinY(vf) + NSMinY(ncf) - NSMaxY(ncf); 399 400 // Prevent window from exiting the screen on the right edge 401 if (NSMinX(nf) > NSMaxX(vf)) 402 nf.origin.x = NSMaxX(vf) - NSWidth(nf); 403 404 // Prevent window from exiting the screen on the left 405 if (NSMaxX(nf) < NSMinX(vf)) 406 nf.origin.x = NSMinX(vf); 407 408 if (NSHeight(nf) < NSHeight(vf) && NSHeight(of) > NSHeight(vf) && 409 ![self.adapter isInFullScreenMode]) 410 // If the window height is smaller than the visible frame, but it was 411 // bigger previously recenter the smaller window vertically. This is 412 // needed to counter the 'snap to top' behaviour. 413 nf.origin.y = (NSHeight(vf) - NSHeight(nf)) / 2; 414 415 return nf; 416} 417 418- (void)windowWillStartLiveResize:(NSNotification *)notification 419{ 420 [self.adapter windowWillStartLiveResize:notification]; 421} 422 423- (void)windowDidEndLiveResize:(NSNotification *)notification 424{ 425 [self.adapter windowDidEndLiveResize:notification]; 426 [self setFrame:[self constrainFrameRect:self.frame toScreen:self.screen] 427 display:NO]; 428} 429 430- (void)tryDequeueSize 431{ 432 if (_queued_video_size.width <= 0.0 || _queued_video_size.height <= 0.0) 433 return; 434 435 [self setContentAspectRatio:_queued_video_size]; 436 [self setCenteredContentSize:_queued_video_size]; 437 _queued_video_size = NSZeroSize; 438} 439 440- (void)queueNewVideoSize:(NSSize)newSize 441{ 442 _unfs_content_frame = [self frameRect:_unfs_content_frame forCenteredContentSize:newSize]; 443 if (![self.adapter isInFullScreenMode]) { 444 if (NSEqualSizes(_queued_video_size, newSize)) 445 return; 446 _queued_video_size = newSize; 447 [self tryDequeueSize]; 448 } 449} 450 451- (void)windowDidBecomeMain:(NSNotification *)notification 452{ 453 [self tryDequeueSize]; 454} 455@end 456