macosx: Make fscontroller compatible with <10.10
[vlc.git] / modules / gui / macosx / VLCFSPanelController.m
1 /*****************************************************************************
2  * VLCFSPanelController.m: macOS fullscreen controls window controller
3  *****************************************************************************
4  * Copyright (C) 2006-2016 VLC authors and VideoLAN
5  * $Id$
6  *
7  * Authors: Jérôme Decoodt <djc at videolan dot org>
8  *          Felix Paul Kühne <fkuehne at videolan dot org>
9  *          David Fuhrmann <david dot fuhrmann at googlemail dot com>
10  *          Marvin Scholz <epirat07 at gmail dot com>
11  *
12  * This program is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * (at your option) any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with this program; if not, write to the Free Software
24  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
25  *****************************************************************************/
26
27 #import "VLCFSPanelController.h"
28 #import "VLCCoreInteraction.h"
29 #import "CompatibilityFixes.h"
30 #import "VLCMain.h"
31
32 @interface VLCFSPanelController () {
33     BOOL _isCounting;
34     CGDirectDisplayID _displayID;
35 }
36
37 @end
38
39 @implementation VLCFSPanelController
40
41 #pragma mark -
42 #pragma mark Initialization
43
44 - (id)init
45 {
46     self = [super initWithWindowNibName:@"VLCFullScreenPanel"];
47     return self;
48 }
49
50 - (void)windowDidLoad
51 {
52     [super windowDidLoad];
53
54     /* Do some window setup that is not possible in IB */
55     [self.window setOpaque:NO];
56     [self.window setAlphaValue:0.0f];
57     [self.window setMovableByWindowBackground:YES];
58     [self.window setLevel:NSModalPanelWindowLevel];
59     [self.window setStyleMask:self.window.styleMask | NSResizableWindowMask];
60     [self.window setBackgroundColor:[NSColor clearColor]];
61
62 #ifdef MAC_OS_X_VERSION_10_10
63     /* Inject correct background view depending on OS support */
64     if (OSX_YOSEMITE_OR_HIGHER) {
65         [self injectVisualEffectView];
66     } else {
67         [self injectBackgroundView];
68     }
69 #else
70     /* Compiled with old SDK, always use legacy style */
71     [self injectBackgroundView];
72 #endif
73
74     [(NSButtonCell*)[_playPauseButton cell] setHighlightsBy:NSPushInCellMask];
75     [(NSButtonCell*)[_playPauseButton cell] setShowsStateBy:NSContentsCellMask];
76
77     [_volumeSlider setMaxValue:[[VLCCoreInteraction sharedInstance] maxVolume]];
78     [_volumeSlider setIntValue:AOUT_VOLUME_DEFAULT];
79 }
80
81 #pragma mark -
82 #pragma mark Control Actions
83
84 - (IBAction)togglePlayPause:(id)sender
85 {
86     [[VLCCoreInteraction sharedInstance] playOrPause];
87 }
88
89 - (IBAction)jumpForward:(id)sender
90 {
91     static NSTimeInterval last_event = 0;
92     if (([NSDate timeIntervalSinceReferenceDate] - last_event) > 0.16) {
93         /* We just skipped 4 "continous" events, otherwise we are too fast */
94         [[VLCCoreInteraction sharedInstance] forwardExtraShort];
95         last_event = [NSDate timeIntervalSinceReferenceDate];
96     }
97 }
98
99 - (IBAction)jumpBackward:(id)sender
100 {
101     static NSTimeInterval last_event = 0;
102     if (([NSDate timeIntervalSinceReferenceDate] - last_event) > 0.16) {
103         /* We just skipped 4 "continous" events, otherwise we are too fast */
104         [[VLCCoreInteraction sharedInstance] backwardExtraShort];
105         last_event = [NSDate timeIntervalSinceReferenceDate];
106     }
107 }
108
109 - (IBAction)gotoPrevious:(id)sender
110 {
111     [[VLCCoreInteraction sharedInstance] previous];
112 }
113
114 - (IBAction)gotoNext:(id)sender
115 {
116     [[VLCCoreInteraction sharedInstance] next];
117 }
118
119 - (IBAction)toggleFullscreen:(id)sender
120 {
121     [[VLCCoreInteraction sharedInstance] toggleFullscreen];
122 }
123
124 - (IBAction)timeSliderUpdate:(id)sender
125 {
126     input_thread_t *p_input;
127     p_input = pl_CurrentInput(getIntf());
128
129     if (p_input) {
130         vlc_value_t pos;
131         pos.f_float = [_timeSlider floatValue] / 10000.;
132         var_Set(p_input, "position", pos);
133         vlc_object_release(p_input);
134     }
135     [[[VLCMain sharedInstance] mainWindow] updateTimeSlider];
136 }
137
138 - (IBAction)volumeSliderUpdate:(id)sender
139 {
140     [[VLCCoreInteraction sharedInstance] setVolume:[sender intValue]];
141 }
142
143 #pragma mark -
144 #pragma mark Metadata and state updates
145
146 - (void)setPlay
147 {
148     [_playPauseButton setState:NSOffState];
149 }
150
151 - (void)setPause
152 {
153     [_playPauseButton setState:NSOnState];
154 }
155
156 - (void)setStreamTitle:(NSString *)title
157 {
158     [_mediaTitle setStringValue:title];
159 }
160
161 - (void)updatePositionAndTime
162 {
163     input_thread_t *p_input = pl_CurrentInput(getIntf());
164
165     /* If nothing is playing, reset times and slider */
166     if (!p_input) {
167         [_timeSlider setFloatValue:0.0];
168         [_elapsedTime setStringValue:@""];
169         [_remainingOrTotalTime setHidden:YES];
170         return;
171     }
172
173     vlc_value_t pos;
174     char psz_time[MSTRTIME_MAX_SIZE];
175
176     var_Get(p_input, "position", &pos);
177     float f_updated = 10000. * pos.f_float;
178     [_timeSlider setFloatValue:f_updated];
179
180
181     int64_t t = var_GetInteger(p_input, "time");
182     mtime_t dur = input_item_GetDuration(input_GetItem(p_input));
183
184     /* Update total duration (right field) */
185     if (dur <= 0) {
186         [_remainingOrTotalTime setHidden:YES];
187     } else {
188         [_remainingOrTotalTime setHidden:NO];
189
190         NSString *totalTime;
191
192         if ([_remainingOrTotalTime timeRemaining]) {
193             mtime_t remaining = 0;
194             if (dur > t)
195                 remaining = dur - t;
196             totalTime = [NSString stringWithFormat:@"-%s", secstotimestr(psz_time, (remaining / 1000000))];
197         } else {
198             totalTime = toNSStr(secstotimestr(psz_time, (dur / 1000000)));
199         }
200         [_remainingOrTotalTime setStringValue:totalTime];
201     }
202
203     /* Update current position (left field) */
204     NSString *playbackPosition = toNSStr(secstotimestr(psz_time, t / CLOCK_FREQ));
205
206     [_elapsedTime setStringValue:playbackPosition];
207     vlc_object_release(p_input);
208 }
209
210 - (void)setSeekable:(BOOL)seekable
211 {
212     [_timeSlider setEnabled:seekable];
213     [_forwardButton setEnabled:seekable];
214     [_backwardButton setEnabled:seekable];
215 }
216
217 - (void)setVolumeLevel:(int)value
218 {
219     [_volumeSlider setIntValue:value];
220 }
221
222 #pragma mark -
223 #pragma mark Window interactions
224
225 - (void)fadeIn
226 {
227     [NSAnimationContext beginGrouping];
228     [[NSAnimationContext currentContext] setDuration:0.7f];
229     [[self.window animator] setAlphaValue:1.0f];
230     [NSAnimationContext endGrouping];
231
232     [self startAutohideTimer];
233 }
234
235 - (void)fadeOut
236 {
237     [NSAnimationContext beginGrouping];
238     [[NSAnimationContext currentContext] setDuration:0.7f];
239     [[self.window animator] setAlphaValue:0.0f];
240     [NSAnimationContext endGrouping];
241 }
242
243 - (void)centerWindowOnScreen:(CGDirectDisplayID)screenID
244 {
245     /* Find screen by its ID */
246     NSScreen *screen = [NSScreen screenWithDisplayID:screenID];
247
248     /* Check screen validity, fallback to mainScreen */
249     if (!screen)
250         screen = [NSScreen mainScreen];
251
252     NSRect screenFrame = [screen frame];
253     NSRect windowFrame = [self.window frame];
254
255     /* Calculate coordinates for new NSWindow position */
256     NSPoint coordinates;
257     coordinates.x = (screenFrame.size.width - windowFrame.size.width) / 2 + screenFrame.origin.x;
258     coordinates.y = (screenFrame.size.height / 3) - windowFrame.size.height + screenFrame.origin.y;
259
260     [self.window setFrameTopLeftPoint:coordinates];
261 }
262
263 - (void)center
264 {
265     [self centerWindowOnScreen:_displayID];
266 }
267
268 - (void)setNonActive
269 {
270     [self.window orderOut:self];
271 }
272
273 - (void)setActive
274 {
275     [self.window orderFront:self];
276 }
277
278 #pragma mark -
279 #pragma mark Misc interactions
280
281 - (void)hideMouse
282 {
283     [NSCursor setHiddenUntilMouseMoves:YES];
284 }
285
286 - (void)setVoutWasUpdated:(VLCWindow *)voutWindow
287 {
288     _voutWindow = voutWindow;
289     int newDisplayID = [[self.window screen] displayID];
290
291     if (_displayID != newDisplayID) {
292         _displayID = newDisplayID;
293         [self center];
294     }
295 }
296
297 #pragma mark -
298 #pragma mark Autohide timer management
299
300 - (void)startAutohideTimer
301 {
302     /* Do nothing if timer is already in place */
303     if (_isCounting)
304         return;
305
306     int _timeToKeepVisibleInSec = var_CreateGetInteger(getIntf(), "mouse-hide-timeout") / 1000;
307     _hideTimer = [NSTimer scheduledTimerWithTimeInterval:_timeToKeepVisibleInSec
308                                                   target:self
309                                                 selector:@selector(autohideCallback:)
310                                                 userInfo:nil
311                                                  repeats:NO];
312     _isCounting = YES;
313 }
314
315 - (void)stopAutohideTimer
316 {
317     [_hideTimer invalidate];
318     _isCounting = NO;
319 }
320
321 - (void)autohideCallback:(NSTimer *)timer
322 {
323     if (!NSPointInRect([NSEvent mouseLocation], [self.window frame])) {
324         [self fadeOut];
325         [self hideMouse];
326     }
327     _isCounting = NO;
328 }
329
330 #pragma mark -
331 #pragma mark Helpers
332
333 #ifdef MAC_OS_X_VERSION_10_10
334 /**
335  Create an image mask for the NSVisualEffectView
336  with rounded corners in the given rect
337  
338  This is necessary as clipping the VisualEffectView using the layers
339  rounded corners is not possible when using the NSColor clearColor
340  as background color.
341  
342  \note  The returned image will have the necessary \c capInsets and
343         \c capResizingMode set.
344  
345  \param bounds  The rect for the image size
346  */
347 - (NSImage *)maskImageWithBounds:(NSRect)bounds
348 {
349     static const float radius = 8.0;
350     NSImage *img = [NSImage imageWithSize:bounds.size flipped:YES drawingHandler:^BOOL(NSRect dstRect) {
351         NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:bounds xRadius:radius yRadius:radius];
352         [[NSColor blackColor] setFill];
353         [path fill];
354         return YES;
355     }];
356     [img setCapInsets:NSEdgeInsetsMake(radius, radius, radius, radius)];
357     [img setResizingMode:NSImageResizingModeStretch];
358     return img;
359 }
360
361 /**
362  Injects the visual effect view in the Windows view hierarchy
363  
364  This is necessary as we can't use the NSVisualEffect view on
365  all macOS Versions and therefore need to dynamically insert it.
366  
367  \warning Never call both, \c injectVisualEffectView and \c injectBackgroundView
368  */
369 - (void)injectVisualEffectView
370 {
371     /* Setup the view */
372     NSVisualEffectView *view = [[NSVisualEffectView alloc] initWithFrame:self.window.contentView.frame];
373     [view setMaskImage:[self maskImageWithBounds:self.window.contentView.bounds]];
374     [view setBlendingMode:NSVisualEffectBlendingModeBehindWindow];
375     [view setMaterial:NSVisualEffectMaterialDark];
376     [view setState:NSVisualEffectStateActive];
377     [view setAutoresizesSubviews:YES];
378
379     /* Inject view in view hierarchy */
380     [self.window setContentView:view];
381     [_controllsView setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]];
382     [self.window.contentView addSubview:_controllsView];
383 }
384 #endif
385
386 /**
387  Injects the standard background view in the Windows view hierarchy
388  
389  This is necessary on macOS versions that do not support the
390  NSVisualEffectView that usually is injected.
391  
392  \warning Never call both, \c injectVisualEffectView and \c injectBackgroundView
393  */
394 - (void)injectBackgroundView
395 {
396     /* Setup the view */
397     NSColor *color = [NSColor colorWithCalibratedWhite:0.0 alpha:0.8];
398     NSView *view = [[NSView alloc] initWithFrame:self.window.contentView.frame];
399     [view setWantsLayer:YES];
400     [view.layer setBackgroundColor:[color CGColor]];
401     [view.layer setCornerRadius:8.0];
402     [view setAutoresizesSubviews:YES];
403
404     /* Inject view in view hierarchy */
405     [self.window setContentView:view];
406     [self.window.contentView addSubview:_controllsView];
407 }
408
409 - (void)dealloc
410 {
411     [self stopAutohideTimer];
412 }
413
414
415 @end