/* $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 "HBPictureController.h"
#import "HBController.h"
#import "HBPreviewView.h"
#import "HBPlayer.h"
#import "HBQTKitPlayer.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) HBPictureController *pictureSettingsWindow;
@property (nonatomic) NSPoint windowCenterPoint;
@property (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] objectForKey:@"HBPreviewWindowCenter"];
if (centerString.length)
{
NSPoint center = NSPointFromString(centerString);
self.windowCenterPoint = center;
[self.window HB_resizeToBestSizeForViewSize:NSMakeSize(MIN_WIDTH, MIN_HEIGHT) center:self.windowCenterPoint 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((MIN_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.pictureSettingsWindow.picture = _picture;
}
- (void)setGenerator:(HBPreviewGenerator *)generator
{
if (_generator)
{
_generator.delegate = nil;
[_generator cancel];
}
_generator = generator;
if (generator)
{
generator.delegate = self;
// adjust the preview slider length
self.pictureHUD.pictureCount = generator.imagesCount;
[self switchStateToHUD:self.pictureHUD];
}
else
{
self.previewView.image = nil;
self.currentHUD.view.hidden = YES;
self.window.title = NSLocalizedString(@"Preview", nil);
}
}
- (void)reloadPreviews
{
if (self.generator)
{
[self.generator cancel];
[self switchStateToHUD:self.pictureHUD];
}
}
- (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.pictureSettingsWindow close];
[self.generator purgeImageCache];
}
- (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, update the preview window
// to the new situation
if (self.generator)
{
[self reloadPreviews];
}
}
}
#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];
}
- (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)", nil), scale * 100.0];
}
else
{
[scaleString appendString:NSLocalizedString(@"(Actual size)", nil)];
}
if (self.previewView.fitToView == YES)
{
[scaleString appendString:NSLocalizedString(@" Scaled To Screen", nil)];
}
// 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 - %@ %@", nil),
self.generator.info, scaleString];
}
}
#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;
}
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];
}
/**
* Adjusts the window to draw the current picture (fPicture) adjusting its size as
* necessary to display as much of the picture as possible.
*/
- (void)displayPreviewAtIndex:(NSUInteger)idx
{
if (!self.generator)
{
return;
}
if (self.window.isVisible)
{
CGImageRef fPreviewImage = [self.generator copyImageAtIndex:idx shouldCache:YES];
[self.previewView setImage:fPreviewImage];
CFRelease(fPreviewImage);
}
if (self.previewView.fitToView == NO && !(self.window.styleMask & NSFullScreenWindowMask))
{
// Get the optimal view size for the image
NSSize imageScaledSize = [self.generator imageSize];
// Scale the window to the image size
NSSize windowSize = [self.previewView optimalViewSizeForImageSize:imageScaledSize minSize:NSMakeSize(MIN_WIDTH, MIN_HEIGHT)];
[self.window HB_resizeToBestSizeForViewSize:windowSize center:self.windowCenterPoint animate:self.window.isVisible];
}
[self updateSizeLabels];
}
- (void)toggleScaleToScreen
{
if (self.previewView.fitToView == YES)
{
self.previewView.fitToView = NO;
[self displayPreviewAtIndex:self.pictureHUD.selectedIndex];
}
else
{
self.previewView.fitToView = YES;
if (!(self.window.styleMask & NSFullScreenWindowMask))
{
[self.window setFrame:self.window.screen.visibleFrame display:YES animate:YES];
}
}
}
- (void)showPictureSettings
{
if (self.pictureSettingsWindow == nil)
{
self.pictureSettingsWindow = [[HBPictureController alloc] init];
self.pictureSettingsWindow.previewController = self;
}
self.pictureSettingsWindow.picture = self.picture;
[self.pictureSettingsWindow showWindow:self];
}
#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 alertWithMessageText:NSLocalizedString(@"HandBrake can't open the preview.", nil)
defaultButton:NSLocalizedString(@"Open in external player", nil)
alternateButton:NSLocalizedString(@"Cancel", nil)
otherButton:nil
informativeTextWithFormat:NSLocalizedString(@"HandBrake can't playback this combination of video/audio/container format. Do you want to open it in an external player?", nil)];
[alert beginSheetModalForWindow:self.window modalDelegate:self didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:(void *)CFBridgingRetain(fileURL)];
}
- (void)alertDidEnd:(NSAlert *)alert
returnCode:(NSInteger)returnCode
contextInfo:(void *)contextInfo
{
NSURL *fileURL = CFBridgingRelease(contextInfo);
if (returnCode == NSModalResponseOK)
{
[[NSWorkspace sharedWorkspace] openURL:fileURL];
}
}
- (void)setUpPlaybackOfURL:(NSURL *)fileURL;
{
if (self.player.isPlayable && self.currentHUD == self.encodingHUD)
{
[self switchStateToHUD:self.playerHUD];
}
else
{
[self showAlert:fileURL];
[self switchStateToHUD:self.pictureHUD];
}
}
- (void)didCreateMovieAtURL:(NSURL *)fileURL
{
if (fileURL)
{
self.player = [[HBAVPlayer alloc] initWithURL:fileURL];
if (self.player)
{
[self.player loadPlayableValueAsynchronouslyWithCompletionHandler:^{
dispatch_async(dispatch_get_main_queue(), ^{
[self setUpPlaybackOfURL:fileURL];
});
}];
}
else
{
[self showAlert:fileURL];
[self switchStateToHUD:self.pictureHUD];
}
}
}
#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.window.contentView.layer insertSublayer:playerLayer atIndex:1];
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.currentHUD HB_keyDown:event] == NO)
{
[super keyDown:event];
}
}
- (void)scrollWheel:(NSEvent *)event
{
if ([self.currentHUD HB_scrollWheel:event] == NO)
{
[super scrollWheel:event];
}
}
@end