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