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