1/*  HBPlayerHUDController.m $
2
3 This file is part of the HandBrake source code.
4 Homepage: <http://handbrake.fr/>.
5 It may be used under the terms of the GNU General Public License. */
6
7#import "HBPlayerHUDController.h"
8#import "HBAttributedStringAdditions.h"
9
10@interface HBPlayerHUDController ()
11
12@property (nonatomic, weak) IBOutlet NSButton *playButton;
13@property (nonatomic, weak) IBOutlet NSSlider *slider;
14
15@property (nonatomic, weak) IBOutlet NSSlider *volumeSlider;
16
17@property (nonatomic, weak) IBOutlet NSTextField *currentTimeLabel;
18@property (nonatomic, weak) IBOutlet NSTextField *remainingTimeLabel;
19
20@property (nonatomic, weak) IBOutlet NSPopUpButton *tracksSelection;
21
22@property (nonatomic, readwrite) id rateObserver;
23@property (nonatomic, readwrite) id periodicObserver;
24
25@end
26
27@interface HBPlayerHUDController (TouchBar) <NSTouchBarProvider, NSTouchBarDelegate>
28- (void)_touchBar_updatePlayState:(BOOL)playing;
29- (void)_touchBar_updateMaxDuration:(NSTimeInterval)duration;
30- (void)_touchBar_updateTime:(NSTimeInterval)currentTime duration:(NSTimeInterval)duration;
31@end
32
33@implementation HBPlayerHUDController
34
35- (NSString *)nibName
36{
37    return @"HBPlayerHUDController";
38}
39
40- (BOOL)canBeHidden
41{
42    return YES;
43}
44
45- (void)setPlayer:(id<HBPlayer>)player
46{
47    if (_player)
48    {
49        [self.player removeRateObserver:self.rateObserver];
50        [self.player removeTimeObserver:self.periodicObserver];
51        self.rateObserver = nil;
52        self.periodicObserver = nil;
53        [self _clearTracksMenu];
54    }
55
56    _player = player;
57
58    if (player)
59    {
60        [self _buildTracksMenu];
61
62        __weak HBPlayerHUDController *weakSelf = self;
63
64        self.periodicObserver = [self.player addPeriodicTimeObserverUsingBlock:^(NSTimeInterval time) {
65            [weakSelf _refreshUI];
66        }];
67
68        self.rateObserver = [self.player addRateObserverUsingBlock:^{
69            [weakSelf _refreshPlayButtonState];
70        }];
71
72        NSTimeInterval duration = self.player.duration;
73        [self.slider setMinValue:0.0];
74        [self.slider setMaxValue:duration];
75        [self.slider setDoubleValue:0.0];
76
77        if (@available(macOS 10.12.2, *))
78        {
79            [self _touchBar_updateMaxDuration:duration];
80        }
81
82        self.player.volume = self.volumeSlider.floatValue;
83
84        [self.player play];
85    }
86}
87
88- (void)dealloc
89{
90    if (_rateObserver)
91    {
92        [_player removeRateObserver:_rateObserver];
93        _rateObserver = nil;
94    }
95    if (_periodicObserver)
96    {
97        [_player removeTimeObserver:_periodicObserver];
98        _periodicObserver = nil;
99    }
100}
101
102#pragma mark - Audio and subtitles selection menu
103
104- (void)_buildTracksMenu
105{
106    [self _clearTracksMenu];
107
108    NSArray<HBPlayerTrack *> *audioTracks = self.player.audioTracks;
109    if (audioTracks.count)
110    {
111        [self _addSectionTitle:NSLocalizedString(@"Audio", @"Player HUD -> audio menu")];
112        [self _addTracksItemFromArray:audioTracks selector:@selector(enableAudioTrack:)];
113    }
114
115    NSArray<HBPlayerTrack *>  *subtitlesTracks = self.player.subtitlesTracks;
116    if (subtitlesTracks.count)
117    {
118        if (audioTracks.count)
119        {
120            [self.tracksSelection.menu addItem:[NSMenuItem separatorItem]];
121        }
122        [self _addSectionTitle:NSLocalizedString(@"Subtitles", @"Player HUD -> subtitles menu")];
123        [self _addTracksItemFromArray:subtitlesTracks selector:@selector(enableSubtitlesTrack:)];
124    }
125}
126
127- (void)_clearTracksMenu
128{
129    for (NSMenuItem *item in [self.tracksSelection.menu.itemArray copy])
130    {
131        if (item.tag != 1)
132        {
133            [self.tracksSelection.menu removeItem:item];
134        }
135    }
136}
137
138- (void)_addSectionTitle:(NSString *)title
139{
140    NSMenuItem *sectionTitle = [[NSMenuItem alloc] init];
141    sectionTitle.title = title;
142    sectionTitle.enabled = NO;
143    sectionTitle.indentationLevel = 0;
144    [self.tracksSelection.menu addItem:sectionTitle];
145}
146
147- (void)_addTracksItemFromArray:(NSArray<HBPlayerTrack *> *)tracks selector:(SEL)selector
148{
149    for (HBPlayerTrack *track in tracks)
150    {
151        NSMenuItem *item = [[NSMenuItem alloc] init];
152        item.title = track.name;
153        item.enabled = YES;
154        item.indentationLevel = 1;
155        item.action = selector;
156        item.target = self;
157        item.state = track.enabled;
158        item.representedObject = track;
159        [self.tracksSelection.menu addItem:item];
160    }
161}
162
163- (void)_updateTracksMenuState
164{
165    for (NSMenuItem *item in self.tracksSelection.menu.itemArray)
166    {
167        if (item.representedObject)
168        {
169            HBPlayerTrack *track = (HBPlayerTrack *)item.representedObject;
170            item.state = track.enabled;
171        }
172    }
173}
174
175- (IBAction)enableAudioTrack:(NSMenuItem *)sender
176{
177    [self.player enableAudioTrack:sender.representedObject];
178    [self _updateTracksMenuState];
179}
180
181- (IBAction)enableSubtitlesTrack:(NSMenuItem *)sender
182{
183    [self.player enableSubtitlesTrack:sender.representedObject];
184    [self _updateTracksMenuState];
185}
186
187- (NSString *)_timeToTimecode:(NSTimeInterval)timeInSeconds
188{
189    UInt16 seconds = (UInt16)fmod(timeInSeconds, 60.0);
190    UInt16 minutes = (UInt16)fmod(timeInSeconds / 60.0, 60.0);
191    UInt16 milliseconds = (UInt16)((timeInSeconds - (int) timeInSeconds) * 1000);
192
193    return [NSString stringWithFormat:@"%02d:%02d.%03d", minutes, seconds, milliseconds];
194}
195
196- (void)_refreshUI
197{
198    if (self.player)
199    {
200        NSTimeInterval currentTime = self.player.currentTime;
201        NSTimeInterval duration = self.player.duration;
202
203        self.slider.doubleValue = currentTime;
204        self.currentTimeLabel.attributedStringValue = [self _timeToTimecode:currentTime].HB_smallMonospacedString;
205        self.remainingTimeLabel.attributedStringValue = [self _timeToTimecode:duration - currentTime].HB_smallMonospacedString;
206
207        if (@available(macOS 10.12.2, *))
208        {
209            [self _touchBar_updateTime:currentTime duration:duration];
210        }
211    }
212}
213
214- (void)_refreshPlayButtonState
215{
216    BOOL playing = self.player.rate != 0.0;
217    if (playing)
218    {
219        self.playButton.image = [NSImage imageNamed:@"PauseTemplate"];
220    }
221    else
222    {
223        self.playButton.image = [NSImage imageNamed:@"PlayTemplate"];
224    }
225
226    if (@available(macOS 10.12.2, *))
227    {
228        [self _touchBar_updatePlayState:playing];
229    }
230}
231
232- (IBAction)playPauseToggle:(id)sender
233{
234    if (self.player.rate != 0.0)
235    {
236        [self.player pause];
237    }
238    else
239    {
240        [self.player play];
241    }
242}
243
244- (IBAction)goToBeginning:(id)sender
245{
246    [self.player gotoBeginning];
247}
248
249- (IBAction)goToEnd:(id)sender
250{
251    [self.player gotoEnd];
252}
253
254- (IBAction)showPicturesPreview:(id)sender
255{
256    [self.delegate stopPlayer];
257}
258
259- (IBAction)sliderChanged:(NSSlider *)sender
260{
261    self.player.currentTime = sender.doubleValue;
262}
263
264- (IBAction)maxVolume:(id)sender
265{
266    self.volumeSlider.doubleValue = 1;
267    self.player.volume = 1;
268}
269
270- (IBAction)mute:(id)sender
271{
272    self.volumeSlider.doubleValue = 0;
273    self.player.volume = 0;
274}
275
276- (IBAction)volumeSliderChanged:(NSSlider *)sender
277{
278    self.player.volume = sender.floatValue;
279}
280
281#pragma mark - Keyboard and mouse wheel control
282
283- (BOOL)HB_keyDown:(NSEvent *)event
284{
285    unichar key = [event.charactersIgnoringModifiers characterAtIndex:0];
286
287    if (self.player)
288    {
289        if (key == 32)
290        {
291            if (self.player.rate != 0.0)
292                [self.player pause];
293            else
294                [self.player play];
295        }
296        else if (key == 'k')
297            [self.player pause];
298        else if (key == 'l')
299        {
300            float rate = self.player.rate;
301            rate += 1.0f;
302            [self.player play];
303            self.player.rate = rate;
304        }
305        else if (key == 'j')
306        {
307            float rate = self.player.rate;
308            rate -= 1.0f;
309            [self.player play];
310            self.player.rate = rate;
311        }
312        else if (event.modifierFlags & NSEventModifierFlagOption && key == NSLeftArrowFunctionKey)
313        {
314            [self.player gotoBeginning];
315        }
316        else if (event.modifierFlags & NSEventModifierFlagOption && key == NSRightArrowFunctionKey)
317        {
318            [self.player gotoEnd];
319        }
320        else if (key == NSLeftArrowFunctionKey)
321        {
322            [self.player stepBackward];
323        }
324        else if (key == NSRightArrowFunctionKey)
325        {
326            [self.player stepForward];
327        }
328        else
329        {
330            return NO;
331        }
332    }
333    else
334    {
335        return NO;
336    }
337
338    return YES;
339}
340
341- (BOOL)HB_scrollWheel:(NSEvent *)theEvent
342{
343    if (theEvent.deltaY < 0)
344    {
345        self.player.currentTime = self.player.currentTime + 0.5;
346    }
347    else if (theEvent.deltaY > 0)
348    {
349        self.player.currentTime = self.player.currentTime - 0.5;
350    }
351    return YES;
352}
353
354@end
355
356@implementation HBPlayerHUDController (TouchBar)
357
358static NSTouchBarItemIdentifier HBTouchBar = @"fr.handbrake.playerHUDTouchBar";
359
360static NSTouchBarItemIdentifier HBTouchBarDone = @"fr.handbrake.done";
361static NSTouchBarItemIdentifier HBTouchBarPlayPause = @"fr.handbrake.playPause";
362static NSTouchBarItemIdentifier HBTouchBarCurrentTime = @"fr.handbrake.currentTime";
363static NSTouchBarItemIdentifier HBTouchBarRemainingTime = @"fr.handbrake.remainingTime";
364static NSTouchBarItemIdentifier HBTouchBarTimeSlider = @"fr.handbrake.timeSlider";
365
366@dynamic touchBar;
367
368- (NSTouchBar *)makeTouchBar
369{
370    NSTouchBar *bar = [[NSTouchBar alloc] init];
371    bar.delegate = self;
372
373    bar.escapeKeyReplacementItemIdentifier = HBTouchBarDone;
374
375    bar.defaultItemIdentifiers = @[HBTouchBarPlayPause, NSTouchBarItemIdentifierFixedSpaceSmall, HBTouchBarCurrentTime, NSTouchBarItemIdentifierFixedSpaceSmall, HBTouchBarTimeSlider, NSTouchBarItemIdentifierFixedSpaceSmall, HBTouchBarRemainingTime];
376
377    bar.customizationIdentifier = HBTouchBar;
378    bar.customizationAllowedItemIdentifiers = @[HBTouchBarPlayPause, HBTouchBarCurrentTime, HBTouchBarTimeSlider, HBTouchBarRemainingTime, NSTouchBarItemIdentifierFlexibleSpace];
379
380    return bar;
381}
382
383- (NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier
384{
385    if ([identifier isEqualTo:HBTouchBarDone])
386    {
387        NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier];
388        item.customizationLabel = NSLocalizedString(@"Done", @"Touch bar");
389
390        NSButton *button = [NSButton buttonWithTitle:NSLocalizedString(@"Done", @"Touch bar") target:self action:@selector(showPicturesPreview:)];
391        button.bezelColor = NSColor.systemYellowColor;
392
393        item.view = button;
394        return item;
395    }
396    else if ([identifier isEqualTo:HBTouchBarPlayPause])
397    {
398        NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier];
399        item.customizationLabel = NSLocalizedString(@"Play/Pause", @"Touch bar");
400
401        NSButton *button = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameTouchBarPlayTemplate]
402                                              target:self action:@selector(playPauseToggle:)];
403
404        item.view = button;
405        return item;
406    }
407    else if ([identifier isEqualTo:HBTouchBarCurrentTime])
408    {
409        NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier];
410        item.customizationLabel = NSLocalizedString(@"Current Time", @"Touch bar");
411
412        NSTextField *label = [NSTextField labelWithString:NSLocalizedString(@"--:--", @"")];
413
414        item.view = label;
415        return item;
416    }
417    else if ([identifier isEqualTo:HBTouchBarRemainingTime])
418    {
419        NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier];
420        item.customizationLabel = NSLocalizedString(@"Remaining Time", @"Touch bar");
421
422        NSTextField *label = [NSTextField labelWithString:NSLocalizedString(@"- --:--", @"")];
423
424        item.view = label;
425        return item;
426    }
427    else if ([identifier isEqualTo:HBTouchBarTimeSlider])
428    {
429        NSSliderTouchBarItem *item = [[NSSliderTouchBarItem alloc] initWithIdentifier:identifier];
430        item.customizationLabel = NSLocalizedString(@"Slider", @"Touch bar");
431
432        item.slider.minValue = 0.0f;
433        item.slider.maxValue = 100.0f;
434        item.slider.doubleValue = 0.0f;
435        item.slider.continuous = YES;
436        item.target = self;
437        item.action = @selector(touchBarSliderChanged:);
438
439        return item;
440    }
441    return nil;
442}
443
444- (void)touchBarSliderChanged:(NSSliderTouchBarItem *)sender
445{
446    [self sliderChanged:sender.slider];
447}
448
449- (void)_touchBar_updatePlayState:(BOOL)playing
450{
451    NSButton *playButton = (NSButton *)[[self.touchBar itemForIdentifier:HBTouchBarPlayPause] view];
452
453    if (playing)
454    {
455        playButton.image = [NSImage imageNamed:NSImageNameTouchBarPauseTemplate];
456    }
457    else
458    {
459        playButton.image = [NSImage imageNamed:NSImageNameTouchBarPlayTemplate];
460    }
461}
462
463- (void)_touchBar_updateMaxDuration:(NSTimeInterval)duration
464{
465    NSSlider *slider = (NSSlider *)[[self.touchBar itemForIdentifier:HBTouchBarTimeSlider] slider];
466    slider.maxValue = duration;
467}
468
469- (NSString *)_timeToString:(NSTimeInterval)timeInSeconds negative:(BOOL)negative
470{
471    UInt16 seconds = (UInt16)fmod(timeInSeconds, 60.0);
472    UInt16 minutes = (UInt16)fmod(timeInSeconds / 60.0, 60.0);
473
474    if (negative)
475    {
476        return [NSString stringWithFormat:@"-%02d:%02d", minutes, seconds];
477    }
478    else
479    {
480        return [NSString stringWithFormat:@"%02d:%02d", minutes, seconds];
481    }
482}
483
484- (void)_touchBar_updateTime:(NSTimeInterval)currentTime duration:(NSTimeInterval)duration
485{
486    NSSlider *slider = (NSSlider *)[[self.touchBar itemForIdentifier:HBTouchBarTimeSlider] slider];
487    NSTextField *currentTimeLabel = (NSTextField *)[[self.touchBar itemForIdentifier:HBTouchBarCurrentTime] view];
488    NSTextField *remainingTimeLabel = (NSTextField *)[[self.touchBar itemForIdentifier:HBTouchBarRemainingTime] view];
489
490    slider.doubleValue = currentTime;
491    currentTimeLabel.attributedStringValue = [self _timeToString:currentTime negative:NO].HB_monospacedString;
492    remainingTimeLabel.attributedStringValue = [self _timeToString:duration - currentTime negative:YES].HB_monospacedString;
493}
494
495@end
496