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