/* $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