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