1/* $Id: HBPreviewController.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 "HBPreviewController.h" 8#import "HBPreviewGenerator.h" 9#import "HBCroppingController.h" 10 11#import "HBController.h" 12#import "HBPreviewView.h" 13 14#import "HBPlayer.h" 15#import "HBAVPlayer.h" 16 17#import "HBPictureHUDController.h" 18#import "HBEncodingProgressHUDController.h" 19#import "HBPlayerHUDController.h" 20 21#import "NSWindow+HBAdditions.h" 22 23#define ANIMATION_DUR 0.15 24#define HUD_FADEOUT_TIME 4.0 25 26// Make min width and height of preview window large enough for hud. 27#define MIN_WIDTH 480.0 28#define MIN_HEIGHT 360.0 29 30@interface HBPreviewController () <NSMenuItemValidation, HBPreviewGeneratorDelegate, HBPictureHUDControllerDelegate, HBEncodingProgressHUDControllerDelegate, HBPlayerHUDControllerDelegate> 31 32@property (nonatomic, readonly) HBPictureHUDController *pictureHUD; 33@property (nonatomic, readonly) HBEncodingProgressHUDController *encodingHUD; 34@property (nonatomic, readonly) HBPlayerHUDController *playerHUD; 35 36@property (nonatomic, readwrite) NSViewController<HBHUD> *currentHUD; 37 38@property (nonatomic) NSTimer *hudTimer; 39@property (nonatomic) BOOL mouseInWindow; 40 41@property (nonatomic) id<HBPlayer> player; 42 43@property (nonatomic) NSPopover *croppingPopover; 44 45@property (nonatomic) NSPoint windowCenterPoint; 46@property (nonatomic, weak) IBOutlet HBPreviewView *previewView; 47 48@end 49 50@implementation HBPreviewController 51 52- (instancetype)init 53{ 54 self = [super initWithWindowNibName:@"PicturePreview"]; 55 return self; 56} 57 58- (void)windowDidLoad 59{ 60 [self.window.contentView setWantsLayer:YES]; 61 62 // Read the window center position 63 // We need the center and we can't use the 64 // standard NSWindow autosave because we change 65 // the window size at startup. 66 NSString *centerString = [NSUserDefaults.standardUserDefaults stringForKey:@"HBPreviewWindowCenter"]; 67 if (centerString.length) 68 { 69 NSPoint center = NSPointFromString(centerString); 70 self.windowCenterPoint = center; 71 [self.window HB_resizeToBestSizeForViewSize:NSMakeSize(MIN_WIDTH, MIN_HEIGHT) keepInScreenRect:YES centerPoint:center animate:NO]; 72 } 73 else 74 { 75 self.windowCenterPoint = [self.window HB_centerPoint]; 76 } 77 78 self.window.excludedFromWindowsMenu = YES; 79 self.window.acceptsMouseMovedEvents = YES; 80 81 _pictureHUD = [[HBPictureHUDController alloc] init]; 82 self.pictureHUD.delegate = self; 83 _encodingHUD = [[HBEncodingProgressHUDController alloc] init]; 84 self.encodingHUD.delegate = self; 85 _playerHUD = [[HBPlayerHUDController alloc] init]; 86 self.playerHUD.delegate = self; 87 88 [self.window.contentView addSubview:self.pictureHUD.view]; 89 [self.window.contentView addSubview:self.encodingHUD.view]; 90 [self.window.contentView addSubview:self.playerHUD.view]; 91 92 // Relocate our hud origins. 93 CGPoint origin = CGPointMake(floor((self.window.frame.size.width - _pictureHUD.view.bounds.size.width) / 2), MIN_HEIGHT / 10); 94 95 [self.pictureHUD.view setFrameOrigin:origin]; 96 [self.encodingHUD.view setFrameOrigin:origin]; 97 [self.playerHUD.view setFrameOrigin:origin]; 98 99 self.currentHUD = self.pictureHUD; 100 self.currentHUD.view.hidden = YES; 101 102 NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:self.window.contentView.frame 103 options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingInVisibleRect | NSTrackingActiveAlways 104 owner:self 105 userInfo:nil]; 106 [self.window.contentView addTrackingArea:trackingArea]; 107} 108 109- (void)dealloc 110{ 111 [_hudTimer invalidate]; 112 _generator.delegate = nil; 113 [_generator cancel]; 114} 115 116- (BOOL)validateMenuItem:(NSMenuItem *)menuItem 117{ 118 SEL action = menuItem.action; 119 120 if (action == @selector(selectPresetFromMenu:)) 121 { 122 return [self.documentController validateMenuItem:menuItem]; 123 } 124 125 return YES; 126} 127 128- (IBAction)selectDefaultPreset:(id)sender 129{ 130 [self.documentController selectDefaultPreset:sender]; 131} 132 133- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window 134{ 135 return self.documentController.window.undoManager; 136} 137 138- (IBAction)selectPresetFromMenu:(id)sender 139{ 140 [self.documentController selectPresetFromMenu:sender]; 141} 142 143- (void)setPicture:(HBPicture *)picture 144{ 145 _picture = picture; 146 [self.croppingPopover close]; 147 self.croppingPopover = nil; 148} 149 150- (void)setGenerator:(HBPreviewGenerator *)generator 151{ 152 if (_generator) 153 { 154 _generator.delegate = nil; 155 [_generator cancel]; 156 } 157 158 _generator = generator; 159 160 if (generator) 161 { 162 generator.delegate = self; 163 self.pictureHUD.generator = generator; 164 } 165 else 166 { 167 self.previewView.image = nil; 168 self.window.title = NSLocalizedString(@"Preview", @"Preview -> window title"); 169 self.pictureHUD.generator = nil; 170 } 171 172 [self switchStateToHUD:self.pictureHUD]; 173 174 if (generator) 175 { 176 [self resizeToOptimalSize]; 177 } 178} 179 180- (void)reloadPreviews 181{ 182 if (self.generator) 183 { 184 [self.generator cancel]; 185 [self switchStateToHUD:self.pictureHUD]; 186 [self resizeToOptimalSize]; 187 } 188} 189 190- (void)showWindow:(id)sender 191{ 192 [super showWindow:sender]; 193 194 if (self.currentHUD == self.pictureHUD) 195 { 196 [self reloadPreviews]; 197 } 198} 199 200- (void)windowWillClose:(NSNotification *)aNotification 201{ 202 if (self.currentHUD == self.encodingHUD) 203 { 204 [self cancelEncoding]; 205 } 206 else if (self.currentHUD == self.playerHUD) 207 { 208 [self.player pause]; 209 } 210 211 [self.generator purgeImageCache]; 212} 213 214#pragma mark - Window sizing 215 216- (void)resizeToOptimalSize 217{ 218 if (!(self.window.styleMask & NSWindowStyleMaskFullScreen)) 219 { 220 if (self.previewView.fitToView) 221 { 222 [self.window setFrame:self.window.screen.visibleFrame display:YES animate:YES]; 223 } 224 else 225 { 226 // Get the optimal view size for the image 227 NSSize windowSize = [self.previewView optimalViewSizeForImageSize:self.generator.imageSize 228 minSize:NSMakeSize(MIN_WIDTH, MIN_HEIGHT) 229 scaleFactor:self.window.backingScaleFactor]; 230 // Scale the window to the image size 231 [self.window HB_resizeToBestSizeForViewSize:windowSize keepInScreenRect:YES centerPoint:NSZeroPoint animate:self.window.isVisible]; 232 } 233 } 234 235 [self updateSizeLabels]; 236} 237 238- (void)windowDidChangeBackingProperties:(NSNotification *)notification 239{ 240 NSWindow *theWindow = (NSWindow *)notification.object; 241 242 CGFloat newBackingScaleFactor = theWindow.backingScaleFactor; 243 CGFloat oldBackingScaleFactor = [notification.userInfo[NSBackingPropertyOldScaleFactorKey] doubleValue]; 244 245 if (newBackingScaleFactor != oldBackingScaleFactor) 246 { 247 // Scale factor changed, resize the preview window 248 if (self.generator) 249 { 250 [self resizeToOptimalSize]; 251 } 252 } 253} 254 255#pragma mark - Window sizing 256 257- (void)windowDidMove:(NSNotification *)notification 258{ 259 if (self.previewView.fitToView == NO) 260 { 261 self.windowCenterPoint = [self.window HB_centerPoint]; 262 [NSUserDefaults.standardUserDefaults setObject:NSStringFromPoint(self.windowCenterPoint) forKey:@"HBPreviewWindowCenter"]; 263 } 264} 265 266- (void)windowDidResize:(NSNotification *)notification 267{ 268 [self updateSizeLabels]; 269 if (self.currentHUD == self.playerHUD) 270 { 271 [CATransaction begin]; 272 CATransaction.disableActions = YES; 273 self.player.layer.frame = self.previewView.pictureFrame; 274 [CATransaction commit]; 275 } 276} 277 278- (void)updateSizeLabels 279{ 280 if (self.generator) 281 { 282 CGFloat scale = self.previewView.scale; 283 284 NSMutableString *scaleString = [NSMutableString string]; 285 if (scale * 100.0 != 100) 286 { 287 [scaleString appendFormat:NSLocalizedString(@"(%.0f%% actual size)", @"Preview -> size info label"), floor(scale * 100.0)]; 288 } 289 else 290 { 291 [scaleString appendString:NSLocalizedString(@"(Actual size)", @"Preview -> size info label")]; 292 } 293 294 if (self.previewView.fitToView == YES) 295 { 296 [scaleString appendString:NSLocalizedString(@" Scaled To Screen", @"Preview -> size info label")]; 297 } 298 299 // Set the info fields in the hud controller 300 self.pictureHUD.info = self.generator.info; 301 self.pictureHUD.scale = scaleString; 302 303 // Set the info field in the window title bar 304 self.window.title = [NSString stringWithFormat:NSLocalizedString(@"Preview - %@ %@", @"Preview -> window title format"), 305 self.generator.info, scaleString]; 306 } 307} 308 309- (void)toggleScaleToScreen 310{ 311 self.previewView.fitToView = !self.previewView.fitToView; 312 [self resizeToOptimalSize]; 313} 314 315#pragma mark - Hud State 316 317/** 318 * Switch the preview controller to one of his hud mode: 319 * This methods is the only way to change the mode, do not try otherwise. 320 * @param hud NSViewController<HBHUD> the hud to show 321 */ 322- (void)switchStateToHUD:(NSViewController<HBHUD> *)hud 323{ 324 if (self.currentHUD == self.playerHUD) 325 { 326 [self exitPlayerState]; 327 } 328 329 if (hud == self.pictureHUD) 330 { 331 [self enterPictureState]; 332 } 333 else if (hud == self.encodingHUD) 334 { 335 [self enterEncodingState]; 336 } 337 else if (hud == self.playerHUD) 338 { 339 [self enterPlayerState]; 340 } 341 342 // Show the current hud 343 NSMutableArray<NSViewController<HBHUD> *> *huds = [@[self.pictureHUD, self.encodingHUD, self.playerHUD] mutableCopy]; 344 [huds removeObject:hud]; 345 for (NSViewController *controller in huds) 346 { 347 controller.view.hidden = YES; 348 } 349 350 if (self.generator) 351 { 352 hud.view.hidden = NO; 353 hud.view.layer.opacity = 1.0; 354 } 355 356 [self.window makeFirstResponder:hud.view]; 357 [self startHudTimer]; 358 self.currentHUD = hud; 359} 360 361#pragma mark - HUD Control Overlay 362 363- (void)mouseEntered:(NSEvent *)theEvent 364{ 365 if (self.generator) 366 { 367 NSView *hud = self.currentHUD.view; 368 369 [self showHudWithAnimation:hud]; 370 [self startHudTimer]; 371 } 372 self.mouseInWindow = YES; 373} 374 375- (void)mouseExited:(NSEvent *)theEvent 376{ 377 [self hudTimerFired:nil]; 378 self.mouseInWindow = NO; 379} 380 381- (void)mouseMoved:(NSEvent *)theEvent 382{ 383 [super mouseMoved:theEvent]; 384 385 // Test for mouse location to show/hide hud controls 386 if (self.generator && self.mouseInWindow) 387 { 388 NSView *hud = self.currentHUD.view; 389 NSPoint mouseLoc = theEvent.locationInWindow; 390 391 if (NSPointInRect(mouseLoc, hud.frame)) 392 { 393 [self stopHudTimer]; 394 } 395 else 396 { 397 [self showHudWithAnimation:hud]; 398 [self startHudTimer]; 399 } 400 } 401} 402 403- (void)showHudWithAnimation:(NSView *)hud 404{ 405 // The standard view animator doesn't play 406 // nicely with the Yosemite visual effects yet. 407 // So let's do the fade ourself. 408 if (hud.layer.opacity == 0 || hud.isHidden) 409 { 410 [hud setHidden:NO]; 411 412 [CATransaction begin]; 413 CABasicAnimation *fadeInAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; 414 fadeInAnimation.fromValue = @([hud.layer.presentationLayer opacity]); 415 fadeInAnimation.toValue = @1.0; 416 fadeInAnimation.beginTime = 0.0; 417 fadeInAnimation.duration = ANIMATION_DUR; 418 419 [hud.layer addAnimation:fadeInAnimation forKey:nil]; 420 [hud.layer setOpacity:1]; 421 422 [CATransaction commit]; 423 } 424} 425 426- (void)hideHudWithAnimation:(NSView *)hud 427{ 428 if (hud.layer.opacity != 0) 429 { 430 [CATransaction begin]; 431 CABasicAnimation *fadeOutAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; 432 fadeOutAnimation.fromValue = @([hud.layer.presentationLayer opacity]); 433 fadeOutAnimation.toValue = @0.0; 434 fadeOutAnimation.beginTime = 0.0; 435 fadeOutAnimation.duration = ANIMATION_DUR; 436 437 [hud.layer addAnimation:fadeOutAnimation forKey:nil]; 438 [hud.layer setOpacity:0]; 439 440 [CATransaction commit]; 441 } 442} 443 444- (void)startHudTimer 445{ 446 if (self.hudTimer) 447 { 448 [self.hudTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:HUD_FADEOUT_TIME]]; 449 } 450 else 451 { 452 self.hudTimer = [NSTimer scheduledTimerWithTimeInterval:HUD_FADEOUT_TIME target:self selector:@selector(hudTimerFired:) 453 userInfo:nil repeats:YES]; 454 } 455} 456 457- (void)stopHudTimer 458{ 459 [self.hudTimer invalidate]; 460 self.hudTimer = nil; 461} 462 463- (void)hudTimerFired:(NSTimer *)theTimer 464{ 465 if (self.currentHUD.canBeHidden) 466 { 467 [self hideHudWithAnimation:self.currentHUD.view]; 468 } 469 [self stopHudTimer]; 470} 471 472#pragma mark - Still previews mode 473 474- (void)enterPictureState 475{ 476 [self displayPreviewAtIndex:self.pictureHUD.selectedIndex]; 477} 478 479- (void)displayPreviewAtIndex:(NSUInteger)idx 480{ 481 if (self.generator && self.window.isVisible) 482 { 483 CGImageRef image = [self.generator copyImageAtIndex:idx shouldCache:YES]; 484 if (image) 485 { 486 self.previewView.image = image; 487 CFRelease(image); 488 } 489 } 490} 491 492- (void)showCroppingSettings:(id)sender 493{ 494 HBCroppingController *croppingController = [[HBCroppingController alloc] initWithPicture:self.picture]; 495 self.croppingPopover = [[NSPopover alloc] init]; 496 self.croppingPopover.behavior = NSPopoverBehaviorTransient; 497 self.croppingPopover.contentViewController = croppingController; 498 self.croppingPopover.appearance = [NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]; 499 [self.croppingPopover showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge]; 500} 501 502#pragma mark - Encoding mode 503 504- (void)enterEncodingState 505{ 506 self.encodingHUD.progress = 0; 507} 508 509- (void)cancelEncoding 510{ 511 [self.generator cancel]; 512} 513 514- (void)createMoviePreviewWithPictureIndex:(NSUInteger)index duration:(NSUInteger)duration 515{ 516 if ([self.generator createMovieAsyncWithImageAtIndex:index duration:duration]) 517 { 518 [self switchStateToHUD:self.encodingHUD]; 519 } 520} 521 522- (void)updateProgress:(double)progress info:(NSString *)progressInfo 523{ 524 self.encodingHUD.progress = progress; 525 self.encodingHUD.info = progressInfo; 526} 527 528- (void)didCancelMovieCreation 529{ 530 [self switchStateToHUD:self.pictureHUD]; 531} 532 533- (void)showAlert:(NSURL *)fileURL 534{ 535 NSAlert *alert = [[NSAlert alloc] init]; 536 alert.messageText = NSLocalizedString(@"HandBrake can't open the preview.", @"Preview -> live preview alert message"); 537 alert.informativeText = NSLocalizedString(@"HandBrake can't playback this combination of video/audio/container format. Do you want to open it in an external player?", @"Preview -> live preview alert informative text"); 538 [alert addButtonWithTitle:NSLocalizedString(@"Open in external player", @"Preview -> live preview alert default button")]; 539 [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"Preview -> live preview alert alternate button")]; 540 541 [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) 542 { 543 if (returnCode == NSAlertFirstButtonReturn) 544 { 545 [[NSWorkspace sharedWorkspace] openURL:fileURL]; 546 } 547 }]; 548} 549 550- (void)setUpPlaybackOfURL:(NSURL *)fileURL playerClass:(Class)class 551{ 552 NSArray<Class> *availablePlayerClasses = @[[HBAVPlayer class]]; 553 554 self.player = [[class alloc] initWithURL:fileURL]; 555 556 if (self.player) 557 { 558 [self.player loadPlayableValueAsynchronouslyWithCompletionHandler:^{ 559 560 dispatch_async(dispatch_get_main_queue(), ^{ 561 if (self.player.isPlayable && self.currentHUD == self.encodingHUD) 562 { 563 [self switchStateToHUD:self.playerHUD]; 564 } 565 else 566 { 567 // Try to open the preview with the next player class. 568 NSUInteger idx = [availablePlayerClasses indexOfObject:class]; 569 if (idx != NSNotFound && (idx + 1) < availablePlayerClasses.count) 570 { 571 Class nextPlayer = availablePlayerClasses[idx + 1]; 572 [self setUpPlaybackOfURL:fileURL playerClass:nextPlayer]; 573 } 574 else 575 { 576 [self showAlert:fileURL]; 577 [self switchStateToHUD:self.pictureHUD]; 578 } 579 } 580 }); 581 582 }]; 583 } 584 else 585 { 586 [self showAlert:fileURL]; 587 [self switchStateToHUD:self.pictureHUD]; 588 } 589} 590 591- (void)didCreateMovieAtURL:(NSURL *)fileURL 592{ 593 [self setUpPlaybackOfURL:fileURL playerClass:[HBAVPlayer class]]; 594} 595 596#pragma mark - Player mode 597 598- (void)enterPlayerState 599{ 600 // Scale the layer to the picture player size 601 CALayer *playerLayer = self.player.layer; 602 playerLayer.frame = self.previewView.pictureFrame; 603 604 [self.previewView.layer insertSublayer:playerLayer atIndex:10]; 605 self.playerHUD.player = self.player; 606} 607 608- (void)exitPlayerState 609{ 610 self.playerHUD.player = nil; 611 [self.player pause]; 612 [self.player.layer removeFromSuperlayer]; 613 self.player = nil; 614} 615 616- (void)stopPlayer 617{ 618 [self switchStateToHUD:self.pictureHUD]; 619} 620 621#pragma mark - Scroll 622 623- (void)keyDown:(NSEvent *)event 624{ 625 if (self.generator && [self.currentHUD HB_keyDown:event] == NO) 626 { 627 [super keyDown:event]; 628 } 629} 630 631- (void)scrollWheel:(NSEvent *)event 632{ 633 if (self.generator && [self.currentHUD HB_scrollWheel:event] == NO) 634 { 635 [super scrollWheel:event]; 636 } 637} 638 639@end 640