You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
520 lines
16 KiB
520 lines
16 KiB
/*****************************************************************************
|
|
* VLCFSPanelController.m: macOS fullscreen controls window controller
|
|
*****************************************************************************
|
|
* Copyright (C) 2006-2016 VLC authors and VideoLAN
|
|
* $Id$
|
|
*
|
|
* Authors: Jérôme Decoodt <djc at videolan dot org>
|
|
* Felix Paul Kühne <fkuehne at videolan dot org>
|
|
* David Fuhrmann <david dot fuhrmann at googlemail dot com>
|
|
* Marvin Scholz <epirat07 at gmail dot com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
|
*****************************************************************************/
|
|
|
|
#import "VLCFSPanelController.h"
|
|
#import "VLCCoreInteraction.h"
|
|
#import "CompatibilityFixes.h"
|
|
#import "VLCMain.h"
|
|
|
|
@interface VLCFSPanelController () {
|
|
BOOL _isCounting;
|
|
|
|
// Only used to track changes and trigger centering of FS panel
|
|
NSRect _associatedVoutFrame;
|
|
// Used to ask for current constraining rect on movement
|
|
NSWindow *_associatedVoutWindow;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation VLCFSPanelController
|
|
|
|
static NSString *kAssociatedFullscreenRect = @"VLCFullscreenAssociatedWindowRect";
|
|
|
|
+ (void)initialize
|
|
{
|
|
NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys: NSStringFromRect(NSZeroRect), kAssociatedFullscreenRect, nil];
|
|
|
|
[[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults];
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Initialization
|
|
|
|
- (id)init
|
|
{
|
|
self = [super initWithWindowNibName:@"VLCFullScreenPanel"];
|
|
if (self) {
|
|
NSString *rectStr = [[NSUserDefaults standardUserDefaults] stringForKey:kAssociatedFullscreenRect];
|
|
_associatedVoutFrame = NSRectFromString(rectStr);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)windowDidLoad
|
|
{
|
|
[super windowDidLoad];
|
|
|
|
/* Do some window setup that is not possible in IB */
|
|
[self.window setOpaque:NO];
|
|
[self.window setAlphaValue:0.0f];
|
|
[self.window setMovableByWindowBackground:NO];
|
|
[self.window setLevel:NSModalPanelWindowLevel];
|
|
[self.window setStyleMask:self.window.styleMask | NSResizableWindowMask];
|
|
[self.window setBackgroundColor:[NSColor clearColor]];
|
|
|
|
/* Set autosave name after we changed window mask to resizable */
|
|
[self.window setFrameAutosaveName:@"VLCFullscreenControls"];
|
|
|
|
#ifdef MAC_OS_X_VERSION_10_10
|
|
/* Inject correct background view depending on OS support */
|
|
if (OSX_YOSEMITE_AND_HIGHER) {
|
|
[self injectVisualEffectView];
|
|
} else {
|
|
[self injectBackgroundView];
|
|
}
|
|
#else
|
|
/* Compiled with old SDK, always use legacy style */
|
|
[self injectBackgroundView];
|
|
#endif
|
|
|
|
[self setupControls];
|
|
}
|
|
|
|
#define setupButton(target, title, desc) \
|
|
[target accessibilitySetOverrideValue:title \
|
|
forAttribute:NSAccessibilityTitleAttribute]; \
|
|
[target accessibilitySetOverrideValue:desc \
|
|
forAttribute:NSAccessibilityDescriptionAttribute]; \
|
|
[target setToolTip:desc];
|
|
|
|
- (void)setupControls
|
|
{
|
|
/* Setup translations for buttons */
|
|
setupButton(_playPauseButton,
|
|
_NS("Play/Pause"),
|
|
_NS("Play/Pause the current media"));
|
|
setupButton(_nextButton,
|
|
_NS("Next"),
|
|
_NS("Go to next item"));
|
|
setupButton(_previousButton,
|
|
_NS("Previous"),
|
|
_NS("Go to the previous item"));
|
|
setupButton(_forwardButton,
|
|
_NS("Forward"),
|
|
_NS("Seek forward"));
|
|
setupButton(_backwardButton,
|
|
_NS("Backward"),
|
|
_NS("Seek backward"));
|
|
setupButton(_fullscreenButton,
|
|
_NS("Toggle Fullscreen mode"),
|
|
_NS("Leave fullscreen mode"));
|
|
setupButton(_volumeSlider,
|
|
_NS("Volume"),
|
|
_NS("Adjust the volume"));
|
|
setupButton(_timeSlider,
|
|
_NS("Position"),
|
|
_NS("Adjust the current playback position"));
|
|
|
|
/* Setup other controls */
|
|
[_volumeSlider setMaxValue:[[VLCCoreInteraction sharedInstance] maxVolume]];
|
|
[_volumeSlider setIntValue:AOUT_VOLUME_DEFAULT];
|
|
[_volumeSlider setDefaultValue:AOUT_VOLUME_DEFAULT];
|
|
}
|
|
|
|
#undef setupButton
|
|
|
|
#pragma mark -
|
|
#pragma mark Control Actions
|
|
|
|
- (IBAction)togglePlayPause:(id)sender
|
|
{
|
|
[[VLCCoreInteraction sharedInstance] playOrPause];
|
|
}
|
|
|
|
- (IBAction)jumpForward:(id)sender
|
|
{
|
|
static NSTimeInterval last_event = 0;
|
|
if (([NSDate timeIntervalSinceReferenceDate] - last_event) > 0.16) {
|
|
/* We just skipped 4 "continuous" events, otherwise we are too fast */
|
|
[[VLCCoreInteraction sharedInstance] forwardExtraShort];
|
|
last_event = [NSDate timeIntervalSinceReferenceDate];
|
|
}
|
|
}
|
|
|
|
- (IBAction)jumpBackward:(id)sender
|
|
{
|
|
static NSTimeInterval last_event = 0;
|
|
if (([NSDate timeIntervalSinceReferenceDate] - last_event) > 0.16) {
|
|
/* We just skipped 4 "continuous" events, otherwise we are too fast */
|
|
[[VLCCoreInteraction sharedInstance] backwardExtraShort];
|
|
last_event = [NSDate timeIntervalSinceReferenceDate];
|
|
}
|
|
}
|
|
|
|
- (IBAction)gotoPrevious:(id)sender
|
|
{
|
|
[[VLCCoreInteraction sharedInstance] previous];
|
|
}
|
|
|
|
- (IBAction)gotoNext:(id)sender
|
|
{
|
|
[[VLCCoreInteraction sharedInstance] next];
|
|
}
|
|
|
|
- (IBAction)toggleFullscreen:(id)sender
|
|
{
|
|
[[VLCCoreInteraction sharedInstance] toggleFullscreen];
|
|
}
|
|
|
|
- (IBAction)timeSliderUpdate:(id)sender
|
|
{
|
|
switch([[NSApp currentEvent] type]) {
|
|
case NSLeftMouseUp:
|
|
/* Ignore mouse up, as this is a continuous slider and
|
|
* when the user does a single click to a position on the slider,
|
|
* the action is called twice, once for the mouse down and once
|
|
* for the mouse up event. This results in two short seeks one
|
|
* after another to the same position, which results in weird
|
|
* audio quirks.
|
|
*/
|
|
return;
|
|
case NSLeftMouseDown:
|
|
case NSLeftMouseDragged:
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
input_thread_t *p_input;
|
|
p_input = pl_CurrentInput(getIntf());
|
|
|
|
if (p_input) {
|
|
vlc_value_t pos;
|
|
pos.f_float = [_timeSlider floatValue] / 10000.;
|
|
var_Set(p_input, "position", pos);
|
|
vlc_object_release(p_input);
|
|
}
|
|
[[[VLCMain sharedInstance] mainWindow] updateTimeSlider];
|
|
}
|
|
|
|
- (IBAction)volumeSliderUpdate:(id)sender
|
|
{
|
|
[[VLCCoreInteraction sharedInstance] setVolume:[sender intValue]];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Metadata and state updates
|
|
|
|
- (void)setPlay
|
|
{
|
|
[_playPauseButton setState:NSOffState];
|
|
}
|
|
|
|
- (void)setPause
|
|
{
|
|
[_playPauseButton setState:NSOnState];
|
|
}
|
|
|
|
- (void)setStreamTitle:(NSString *)title
|
|
{
|
|
[_mediaTitle setStringValue:title];
|
|
}
|
|
|
|
- (void)updatePositionAndTime
|
|
{
|
|
input_thread_t *p_input = pl_CurrentInput(getIntf());
|
|
|
|
/* If nothing is playing, reset times and slider */
|
|
if (!p_input) {
|
|
[_timeSlider setFloatValue:0.0];
|
|
[_elapsedTime setStringValue:@""];
|
|
[_remainingOrTotalTime setHidden:YES];
|
|
return;
|
|
}
|
|
|
|
vlc_value_t pos;
|
|
char psz_time[MSTRTIME_MAX_SIZE];
|
|
|
|
var_Get(p_input, "position", &pos);
|
|
float f_updated = 10000. * pos.f_float;
|
|
[_timeSlider setFloatValue:f_updated];
|
|
|
|
|
|
int64_t t = var_GetInteger(p_input, "time");
|
|
mtime_t dur = input_item_GetDuration(input_GetItem(p_input));
|
|
|
|
/* Update total duration (right field) */
|
|
if (dur <= 0) {
|
|
[_remainingOrTotalTime setHidden:YES];
|
|
} else {
|
|
[_remainingOrTotalTime setHidden:NO];
|
|
|
|
NSString *totalTime;
|
|
|
|
if ([_remainingOrTotalTime timeRemaining]) {
|
|
mtime_t remaining = 0;
|
|
if (dur > t)
|
|
remaining = dur - t;
|
|
totalTime = [NSString stringWithFormat:@"-%s", secstotimestr(psz_time, (remaining / 1000000))];
|
|
} else {
|
|
totalTime = toNSStr(secstotimestr(psz_time, (dur / 1000000)));
|
|
}
|
|
[_remainingOrTotalTime setStringValue:totalTime];
|
|
}
|
|
|
|
/* Update current position (left field) */
|
|
NSString *playbackPosition = toNSStr(secstotimestr(psz_time, t / CLOCK_FREQ));
|
|
|
|
[_elapsedTime setStringValue:playbackPosition];
|
|
vlc_object_release(p_input);
|
|
}
|
|
|
|
- (void)setSeekable:(BOOL)seekable
|
|
{
|
|
[_timeSlider setEnabled:seekable];
|
|
[_forwardButton setEnabled:seekable];
|
|
[_backwardButton setEnabled:seekable];
|
|
}
|
|
|
|
- (void)setVolumeLevel:(int)value
|
|
{
|
|
[_volumeSlider setIntValue:value];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Window interactions
|
|
|
|
- (void)fadeIn
|
|
{
|
|
[NSAnimationContext beginGrouping];
|
|
[[NSAnimationContext currentContext] setDuration:0.4f];
|
|
[[self.window animator] setAlphaValue:1.0f];
|
|
[NSAnimationContext endGrouping];
|
|
|
|
[self startAutohideTimer];
|
|
}
|
|
|
|
- (void)fadeOut
|
|
{
|
|
[NSAnimationContext beginGrouping];
|
|
[[NSAnimationContext currentContext] setDuration:0.4f];
|
|
[[self.window animator] setAlphaValue:0.0f];
|
|
[NSAnimationContext endGrouping];
|
|
}
|
|
|
|
- (void)centerPanel
|
|
{
|
|
NSRect windowFrame = [self.window frame];
|
|
windowFrame = [self contrainFrameToAssociatedVoutWindow:windowFrame];
|
|
|
|
/* Calculate coordinates for centered position */
|
|
NSRect limitFrame = _associatedVoutWindow.frame;
|
|
windowFrame.origin.x = (limitFrame.size.width - windowFrame.size.width) / 2 + limitFrame.origin.x;
|
|
windowFrame.origin.y = (limitFrame.size.height / 5) - windowFrame.size.height + limitFrame.origin.y;
|
|
|
|
[self.window setFrame:windowFrame display:YES animate:NO];
|
|
}
|
|
|
|
- (NSRect)contrainFrameToAssociatedVoutWindow:(NSRect)frame
|
|
{
|
|
NSRect limitFrame = _associatedVoutWindow.frame;
|
|
|
|
// Limit rect to limitation view
|
|
if (frame.origin.x < limitFrame.origin.x)
|
|
frame.origin.x = limitFrame.origin.x;
|
|
if (frame.origin.y < limitFrame.origin.y)
|
|
frame.origin.y = limitFrame.origin.y;
|
|
|
|
// Limit size (could be needed after resolution changes)
|
|
if (frame.size.height > limitFrame.size.height)
|
|
frame.size.height = limitFrame.size.height;
|
|
if (frame.size.width > limitFrame.size.width)
|
|
frame.size.width = limitFrame.size.width;
|
|
|
|
if (frame.origin.x + frame.size.width > limitFrame.origin.x + limitFrame.size.width)
|
|
frame.origin.x = limitFrame.origin.x + limitFrame.size.width - frame.size.width;
|
|
if (frame.origin.y + frame.size.height > limitFrame.origin.y + limitFrame.size.height)
|
|
frame.origin.y = limitFrame.origin.y + limitFrame.size.height - frame.size.height;
|
|
|
|
return frame;
|
|
}
|
|
|
|
- (void)setNonActive
|
|
{
|
|
[self.window orderOut:self];
|
|
}
|
|
|
|
- (void)setActive
|
|
{
|
|
[self.window orderFront:self];
|
|
[self fadeIn];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Misc interactions
|
|
|
|
- (void)hideMouse
|
|
{
|
|
[NSCursor setHiddenUntilMouseMoves:YES];
|
|
}
|
|
|
|
- (void)setVoutWasUpdated:(VLCWindow *)voutWindow
|
|
{
|
|
_associatedVoutWindow = voutWindow;
|
|
|
|
NSRect voutRect = voutWindow.frame;
|
|
if (!NSEqualRects(_associatedVoutFrame, voutRect)) {
|
|
_associatedVoutFrame = voutRect;
|
|
[[NSUserDefaults standardUserDefaults] setObject:NSStringFromRect(_associatedVoutFrame) forKey:kAssociatedFullscreenRect];
|
|
|
|
[self centerPanel];
|
|
}
|
|
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Autohide timer management
|
|
|
|
- (void)startAutohideTimer
|
|
{
|
|
/* Do nothing if timer is already in place */
|
|
if (_isCounting)
|
|
return;
|
|
|
|
/* Get timeout and make sure it is not lower than 1 second */
|
|
int _timeToKeepVisibleInSec = MAX(var_CreateGetInteger(getIntf(), "mouse-hide-timeout") / 1000, 1);
|
|
|
|
_hideTimer = [NSTimer scheduledTimerWithTimeInterval:_timeToKeepVisibleInSec
|
|
target:self
|
|
selector:@selector(autohideCallback:)
|
|
userInfo:nil
|
|
repeats:NO];
|
|
_isCounting = YES;
|
|
}
|
|
|
|
- (void)stopAutohideTimer
|
|
{
|
|
[_hideTimer invalidate];
|
|
_isCounting = NO;
|
|
}
|
|
|
|
- (void)autohideCallback:(NSTimer *)timer
|
|
{
|
|
if (!NSMouseInRect([NSEvent mouseLocation], [self.window frame], NO)) {
|
|
[self fadeOut];
|
|
[self hideMouse];
|
|
}
|
|
_isCounting = NO;
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Helpers
|
|
|
|
#ifdef MAC_OS_X_VERSION_10_10
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wpartial-availability"
|
|
|
|
/**
|
|
Create an image mask for the NSVisualEffectView
|
|
with rounded corners in the given rect
|
|
|
|
This is necessary as clipping the VisualEffectView using the layers
|
|
rounded corners is not possible when using the NSColor clearColor
|
|
as background color.
|
|
|
|
\note The returned image will have the necessary \c capInsets and
|
|
\c capResizingMode set.
|
|
|
|
\param bounds The rect for the image size
|
|
*/
|
|
- (NSImage *)maskImageWithBounds:(NSRect)bounds
|
|
{
|
|
static const float radius = 8.0;
|
|
NSImage *img = [NSImage imageWithSize:bounds.size flipped:YES drawingHandler:^BOOL(NSRect dstRect) {
|
|
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:bounds xRadius:radius yRadius:radius];
|
|
[[NSColor blackColor] setFill];
|
|
[path fill];
|
|
return YES;
|
|
}];
|
|
[img setCapInsets:NSEdgeInsetsMake(radius, radius, radius, radius)];
|
|
[img setResizingMode:NSImageResizingModeStretch];
|
|
return img;
|
|
}
|
|
|
|
/**
|
|
Injects the visual effect view in the Windows view hierarchy
|
|
|
|
This is necessary as we can't use the NSVisualEffect view on
|
|
all macOS Versions and therefore need to dynamically insert it.
|
|
|
|
\warning Never call both, \c injectVisualEffectView and \c injectBackgroundView
|
|
*/
|
|
- (void)injectVisualEffectView
|
|
{
|
|
/* Setup the view */
|
|
NSVisualEffectView *view = [[NSVisualEffectView alloc] initWithFrame:self.window.contentView.frame];
|
|
[view setMaskImage:[self maskImageWithBounds:self.window.contentView.bounds]];
|
|
[view setBlendingMode:NSVisualEffectBlendingModeBehindWindow];
|
|
[view setMaterial:NSVisualEffectMaterialDark];
|
|
[view setState:NSVisualEffectStateActive];
|
|
[view setAutoresizesSubviews:YES];
|
|
|
|
/* Inject view in view hierarchy */
|
|
[self.window setContentView:view];
|
|
[_controlsView setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]];
|
|
[self.window.contentView addSubview:_controlsView];
|
|
}
|
|
#pragma clang diagnostic pop
|
|
#endif
|
|
|
|
/**
|
|
Injects the standard background view in the Windows view hierarchy
|
|
|
|
This is necessary on macOS versions that do not support the
|
|
NSVisualEffectView that usually is injected.
|
|
|
|
\warning Never call both, \c injectVisualEffectView and \c injectBackgroundView
|
|
*/
|
|
- (void)injectBackgroundView
|
|
{
|
|
/* Setup the view */
|
|
CGColorRef color = CGColorCreateGenericGray(0.0, 0.8);
|
|
NSView *view = [[NSView alloc] initWithFrame:self.window.contentView.frame];
|
|
[view setWantsLayer:YES];
|
|
[view.layer setBackgroundColor:color];
|
|
[view.layer setCornerRadius:8.0];
|
|
[view setAutoresizesSubviews:YES];
|
|
CGColorRelease(color);
|
|
|
|
/* Inject view in view hierarchy */
|
|
[self.window setContentView:view];
|
|
[self.window.contentView addSubview:_controlsView];
|
|
|
|
/* Disable adjusting height to workaround autolayout problems */
|
|
[_heightMaxConstraint setConstant:42.0];
|
|
[self.window setMaxSize:NSMakeSize(4068, 80)];
|
|
[self.window setMinSize:NSMakeSize(480, 80)];
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[self stopAutohideTimer];
|
|
}
|
|
|
|
|
|
@end
|
|
|