/* $Id: HBPreviewController.m $ This file is part of the HandBrake source code. Homepage: . It may be used under the terms of the GNU General Public License. */ #import "HBPreviewController.h" #import "HBPreviewGenerator.h" #import "HBCroppingController.h" #import "HBController.h" #import "HBPreviewView.h" #import "HBPlayer.h" #import "HBAVPlayer.h" #import "HBPictureHUDController.h" #import "HBEncodingProgressHUDController.h" #import "HBPlayerHUDController.h" #import "NSWindow+HBAdditions.h" #define ANIMATION_DUR 0.15 #define HUD_FADEOUT_TIME 4.0 // Make min width and height of preview window large enough for hud. #define MIN_WIDTH 480.0 #define MIN_HEIGHT 360.0 @interface HBPreviewController () @property (nonatomic, readonly) HBPictureHUDController *pictureHUD; @property (nonatomic, readonly) HBEncodingProgressHUDController *encodingHUD; @property (nonatomic, readonly) HBPlayerHUDController *playerHUD; @property (nonatomic, readwrite) NSViewController *currentHUD; @property (nonatomic) NSTimer *hudTimer; @property (nonatomic) BOOL mouseInWindow; @property (nonatomic) id player; @property (nonatomic) NSPopover *croppingPopover; @property (nonatomic) NSPoint windowCenterPoint; @property (nonatomic, weak) IBOutlet HBPreviewView *previewView; @end @implementation HBPreviewController - (instancetype)init { self = [super initWithWindowNibName:@"PicturePreview"]; return self; } - (void)windowDidLoad { [self.window.contentView setWantsLayer:YES]; // Read the window center position // We need the center and we can't use the // standard NSWindow autosave because we change // the window size at startup. NSString *centerString = [NSUserDefaults.standardUserDefaults stringForKey:@"HBPreviewWindowCenter"]; if (centerString.length) { NSPoint center = NSPointFromString(centerString); self.windowCenterPoint = center; [self.window HB_resizeToBestSizeForViewSize:NSMakeSize(MIN_WIDTH, MIN_HEIGHT) keepInScreenRect:YES centerPoint:center animate:NO]; } else { self.windowCenterPoint = [self.window HB_centerPoint]; } self.window.excludedFromWindowsMenu = YES; self.window.acceptsMouseMovedEvents = YES; _pictureHUD = [[HBPictureHUDController alloc] init]; self.pictureHUD.delegate = self; _encodingHUD = [[HBEncodingProgressHUDController alloc] init]; self.encodingHUD.delegate = self; _playerHUD = [[HBPlayerHUDController alloc] init]; self.playerHUD.delegate = self; [self.window.contentView addSubview:self.pictureHUD.view]; [self.window.contentView addSubview:self.encodingHUD.view]; [self.window.contentView addSubview:self.playerHUD.view]; // Relocate our hud origins. CGPoint origin = CGPointMake(floor((self.window.frame.size.width - _pictureHUD.view.bounds.size.width) / 2), MIN_HEIGHT / 10); [self.pictureHUD.view setFrameOrigin:origin]; [self.encodingHUD.view setFrameOrigin:origin]; [self.playerHUD.view setFrameOrigin:origin]; self.currentHUD = self.pictureHUD; self.currentHUD.view.hidden = YES; NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:self.window.contentView.frame options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingInVisibleRect | NSTrackingActiveAlways owner:self userInfo:nil]; [self.window.contentView addTrackingArea:trackingArea]; } - (void)dealloc { [_hudTimer invalidate]; _generator.delegate = nil; [_generator cancel]; } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL action = menuItem.action; if (action == @selector(selectPresetFromMenu:)) { return [self.documentController validateMenuItem:menuItem]; } return YES; } - (IBAction)selectDefaultPreset:(id)sender { [self.documentController selectDefaultPreset:sender]; } - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window { return self.documentController.window.undoManager; } - (IBAction)selectPresetFromMenu:(id)sender { [self.documentController selectPresetFromMenu:sender]; } - (void)setPicture:(HBPicture *)picture { _picture = picture; [self.croppingPopover close]; self.croppingPopover = nil; } - (void)setGenerator:(HBPreviewGenerator *)generator { if (_generator) { _generator.delegate = nil; [_generator cancel]; } _generator = generator; if (generator) { generator.delegate = self; self.pictureHUD.generator = generator; } else { self.previewView.image = nil; self.window.title = NSLocalizedString(@"Preview", @"Preview -> window title"); self.pictureHUD.generator = nil; } [self switchStateToHUD:self.pictureHUD]; if (generator) { [self resizeToOptimalSize]; } } - (void)reloadPreviews { if (self.generator) { [self.generator cancel]; [self switchStateToHUD:self.pictureHUD]; [self resizeToOptimalSize]; } } - (void)showWindow:(id)sender { [super showWindow:sender]; if (self.currentHUD == self.pictureHUD) { [self reloadPreviews]; } } - (void)windowWillClose:(NSNotification *)aNotification { if (self.currentHUD == self.encodingHUD) { [self cancelEncoding]; } else if (self.currentHUD == self.playerHUD) { [self.player pause]; } [self.generator purgeImageCache]; } #pragma mark - Window sizing - (void)resizeToOptimalSize { if (!(self.window.styleMask & NSWindowStyleMaskFullScreen)) { if (self.previewView.fitToView) { [self.window setFrame:self.window.screen.visibleFrame display:YES animate:YES]; } else { // Get the optimal view size for the image NSSize windowSize = [self.previewView optimalViewSizeForImageSize:self.generator.imageSize minSize:NSMakeSize(MIN_WIDTH, MIN_HEIGHT) scaleFactor:self.window.backingScaleFactor]; // Scale the window to the image size [self.window HB_resizeToBestSizeForViewSize:windowSize keepInScreenRect:YES centerPoint:NSZeroPoint animate:self.window.isVisible]; } } [self updateSizeLabels]; } - (void)windowDidChangeBackingProperties:(NSNotification *)notification { NSWindow *theWindow = (NSWindow *)notification.object; CGFloat newBackingScaleFactor = theWindow.backingScaleFactor; CGFloat oldBackingScaleFactor = [notification.userInfo[NSBackingPropertyOldScaleFactorKey] doubleValue]; if (newBackingScaleFactor != oldBackingScaleFactor) { // Scale factor changed, resize the preview window if (self.generator) { [self resizeToOptimalSize]; } } } #pragma mark - Window sizing - (void)windowDidMove:(NSNotification *)notification { if (self.previewView.fitToView == NO) { self.windowCenterPoint = [self.window HB_centerPoint]; [NSUserDefaults.standardUserDefaults setObject:NSStringFromPoint(self.windowCenterPoint) forKey:@"HBPreviewWindowCenter"]; } } - (void)windowDidResize:(NSNotification *)notification { [self updateSizeLabels]; if (self.currentHUD == self.playerHUD) { [CATransaction begin]; CATransaction.disableActions = YES; self.player.layer.frame = self.previewView.pictureFrame; [CATransaction commit]; } } - (void)updateSizeLabels { if (self.generator) { CGFloat scale = self.previewView.scale; NSMutableString *scaleString = [NSMutableString string]; if (scale * 100.0 != 100) { [scaleString appendFormat:NSLocalizedString(@"(%.0f%% actual size)", @"Preview -> size info label"), floor(scale * 100.0)]; } else { [scaleString appendString:NSLocalizedString(@"(Actual size)", @"Preview -> size info label")]; } if (self.previewView.fitToView == YES) { [scaleString appendString:NSLocalizedString(@" Scaled To Screen", @"Preview -> size info label")]; } // Set the info fields in the hud controller self.pictureHUD.info = self.generator.info; self.pictureHUD.scale = scaleString; // Set the info field in the window title bar self.window.title = [NSString stringWithFormat:NSLocalizedString(@"Preview - %@ %@", @"Preview -> window title format"), self.generator.info, scaleString]; } } - (void)toggleScaleToScreen { self.previewView.fitToView = !self.previewView.fitToView; [self resizeToOptimalSize]; } #pragma mark - Hud State /** * Switch the preview controller to one of his hud mode: * This methods is the only way to change the mode, do not try otherwise. * @param hud NSViewController the hud to show */ - (void)switchStateToHUD:(NSViewController *)hud { if (self.currentHUD == self.playerHUD) { [self exitPlayerState]; } if (hud == self.pictureHUD) { [self enterPictureState]; } else if (hud == self.encodingHUD) { [self enterEncodingState]; } else if (hud == self.playerHUD) { [self enterPlayerState]; } // Show the current hud NSMutableArray *> *huds = [@[self.pictureHUD, self.encodingHUD, self.playerHUD] mutableCopy]; [huds removeObject:hud]; for (NSViewController *controller in huds) { controller.view.hidden = YES; } if (self.generator) { hud.view.hidden = NO; hud.view.layer.opacity = 1.0; } [self.window makeFirstResponder:hud.view]; [self startHudTimer]; self.currentHUD = hud; } #pragma mark - HUD Control Overlay - (void)mouseEntered:(NSEvent *)theEvent { if (self.generator) { NSView *hud = self.currentHUD.view; [self showHudWithAnimation:hud]; [self startHudTimer]; } self.mouseInWindow = YES; } - (void)mouseExited:(NSEvent *)theEvent { [self hudTimerFired:nil]; self.mouseInWindow = NO; } - (void)mouseMoved:(NSEvent *)theEvent { [super mouseMoved:theEvent]; // Test for mouse location to show/hide hud controls if (self.generator && self.mouseInWindow) { NSView *hud = self.currentHUD.view; NSPoint mouseLoc = theEvent.locationInWindow; if (NSPointInRect(mouseLoc, hud.frame)) { [self stopHudTimer]; } else { [self showHudWithAnimation:hud]; [self startHudTimer]; } } } - (void)showHudWithAnimation:(NSView *)hud { // The standard view animator doesn't play // nicely with the Yosemite visual effects yet. // So let's do the fade ourself. if (hud.layer.opacity == 0 || hud.isHidden) { [hud setHidden:NO]; [CATransaction begin]; CABasicAnimation *fadeInAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; fadeInAnimation.fromValue = @([hud.layer.presentationLayer opacity]); fadeInAnimation.toValue = @1.0; fadeInAnimation.beginTime = 0.0; fadeInAnimation.duration = ANIMATION_DUR; [hud.layer addAnimation:fadeInAnimation forKey:nil]; [hud.layer setOpacity:1]; [CATransaction commit]; } } - (void)hideHudWithAnimation:(NSView *)hud { if (hud.layer.opacity != 0) { [CATransaction begin]; CABasicAnimation *fadeOutAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; fadeOutAnimation.fromValue = @([hud.layer.presentationLayer opacity]); fadeOutAnimation.toValue = @0.0; fadeOutAnimation.beginTime = 0.0; fadeOutAnimation.duration = ANIMATION_DUR; [hud.layer addAnimation:fadeOutAnimation forKey:nil]; [hud.layer setOpacity:0]; [CATransaction commit]; } } - (void)startHudTimer { if (self.hudTimer) { [self.hudTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:HUD_FADEOUT_TIME]]; } else { self.hudTimer = [NSTimer scheduledTimerWithTimeInterval:HUD_FADEOUT_TIME target:self selector:@selector(hudTimerFired:) userInfo:nil repeats:YES]; } } - (void)stopHudTimer { [self.hudTimer invalidate]; self.hudTimer = nil; } - (void)hudTimerFired:(NSTimer *)theTimer { if (self.currentHUD.canBeHidden) { [self hideHudWithAnimation:self.currentHUD.view]; } [self stopHudTimer]; } #pragma mark - Still previews mode - (void)enterPictureState { [self displayPreviewAtIndex:self.pictureHUD.selectedIndex]; } - (void)displayPreviewAtIndex:(NSUInteger)idx { if (self.generator && self.window.isVisible) { CGImageRef image = [self.generator copyImageAtIndex:idx shouldCache:YES]; if (image) { self.previewView.image = image; CFRelease(image); } } } - (void)showCroppingSettings:(id)sender { HBCroppingController *croppingController = [[HBCroppingController alloc] initWithPicture:self.picture]; self.croppingPopover = [[NSPopover alloc] init]; self.croppingPopover.behavior = NSPopoverBehaviorTransient; self.croppingPopover.contentViewController = croppingController; self.croppingPopover.appearance = [NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]; [self.croppingPopover showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge]; } #pragma mark - Encoding mode - (void)enterEncodingState { self.encodingHUD.progress = 0; } - (void)cancelEncoding { [self.generator cancel]; } - (void)createMoviePreviewWithPictureIndex:(NSUInteger)index duration:(NSUInteger)duration { if ([self.generator createMovieAsyncWithImageAtIndex:index duration:duration]) { [self switchStateToHUD:self.encodingHUD]; } } - (void)updateProgress:(double)progress info:(NSString *)progressInfo { self.encodingHUD.progress = progress; self.encodingHUD.info = progressInfo; } - (void)didCancelMovieCreation { [self switchStateToHUD:self.pictureHUD]; } - (void)showAlert:(NSURL *)fileURL { NSAlert *alert = [[NSAlert alloc] init]; alert.messageText = NSLocalizedString(@"HandBrake can't open the preview.", @"Preview -> live preview alert message"); 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"); [alert addButtonWithTitle:NSLocalizedString(@"Open in external player", @"Preview -> live preview alert default button")]; [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"Preview -> live preview alert alternate button")]; [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { if (returnCode == NSAlertFirstButtonReturn) { [[NSWorkspace sharedWorkspace] openURL:fileURL]; } }]; } - (void)setUpPlaybackOfURL:(NSURL *)fileURL playerClass:(Class)class { NSArray *availablePlayerClasses = @[[HBAVPlayer class]]; self.player = [[class alloc] initWithURL:fileURL]; if (self.player) { [self.player loadPlayableValueAsynchronouslyWithCompletionHandler:^{ dispatch_async(dispatch_get_main_queue(), ^{ if (self.player.isPlayable && self.currentHUD == self.encodingHUD) { [self switchStateToHUD:self.playerHUD]; } else { // Try to open the preview with the next player class. NSUInteger idx = [availablePlayerClasses indexOfObject:class]; if (idx != NSNotFound && (idx + 1) < availablePlayerClasses.count) { Class nextPlayer = availablePlayerClasses[idx + 1]; [self setUpPlaybackOfURL:fileURL playerClass:nextPlayer]; } else { [self showAlert:fileURL]; [self switchStateToHUD:self.pictureHUD]; } } }); }]; } else { [self showAlert:fileURL]; [self switchStateToHUD:self.pictureHUD]; } } - (void)didCreateMovieAtURL:(NSURL *)fileURL { [self setUpPlaybackOfURL:fileURL playerClass:[HBAVPlayer class]]; } #pragma mark - Player mode - (void)enterPlayerState { // Scale the layer to the picture player size CALayer *playerLayer = self.player.layer; playerLayer.frame = self.previewView.pictureFrame; [self.previewView.layer insertSublayer:playerLayer atIndex:10]; self.playerHUD.player = self.player; } - (void)exitPlayerState { self.playerHUD.player = nil; [self.player pause]; [self.player.layer removeFromSuperlayer]; self.player = nil; } - (void)stopPlayer { [self switchStateToHUD:self.pictureHUD]; } #pragma mark - Scroll - (void)keyDown:(NSEvent *)event { if (self.generator && [self.currentHUD HB_keyDown:event] == NO) { [super keyDown:event]; } } - (void)scrollWheel:(NSEvent *)event { if (self.generator && [self.currentHUD HB_scrollWheel:event] == NO) { [super scrollWheel:event]; } } @end