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.
 
 
 
 
 
 

503 lines
16 KiB

/*****************************************************************************
* VLCStatusBarIcon.m: Status bar icon controller/delegate
*****************************************************************************
* Copyright (C) 2016-2019 VLC authors and VideoLAN
*
* Authors: Goran Dokic <vlc at 8hz dot com>
* Felix Paul Kühne <fkuehne # videolan.org>
*
* 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 "VLCStatusBarIcon.h"
#import "extensions/NSString+Helpers.h"
#import "main/VLCMain.h"
#import "playqueue/VLCPlayQueueController.h"
#import "playqueue/VLCPlayerController.h"
#import "library/VLCInputItem.h"
#import "windows/VLCDetachedAudioWindow.h"
#import <vlc_configuration.h>
@interface VLCStatusBarIcon ()
{
intf_thread_t *_intf;
NSMenuItem *_vlcStatusBarMenuItem;
/* Outlets for Now Playing labels */
IBOutlet NSTextField *titleField;
IBOutlet NSTextField *artistField;
IBOutlet NSTextField *albumField;
IBOutlet NSTextField *progressField;
IBOutlet NSTextField *separatorField;
IBOutlet NSTextField *totalField;
IBOutlet NSImageView *coverImageView;
/* Outlets for player controls */
IBOutlet NSButton *backwardsButton;
IBOutlet NSButton *playPauseButton;
IBOutlet NSButton *forwardButton;
IBOutlet NSButton *randomButton;
/* Outlets for menu items */
IBOutlet NSMenuItem *pathActionItem;
IBOutlet NSMenuItem *showMainWindowItem;
IBOutlet NSMenuItem *quitItem;
BOOL _showTimeElapsed;
NSString *_currentPlaybackUrl;
VLCDetachedAudioWindow *_detachedAudioWindow;
}
@end
#pragma mark -
#pragma mark Implementation
@implementation VLCStatusBarIcon
#pragma mark -
#pragma mark Init
- (instancetype)init:(intf_thread_t *)intf;
{
self = [super init];
if (self) {
_intf = intf;
[[NSBundle mainBundle] loadNibNamed:@"VLCStatusBarIconMainMenu" owner:self topLevelObjects:nil];
}
return self;
}
- (void)awakeFromNib
{
[super awakeFromNib];
[_controlsView setAutoresizingMask:NSViewWidthSizable];
[_playbackInfoView setAutoresizingMask:NSViewWidthSizable];
[self configurationChanged:nil];
// Set Accessibility Attributes for Image Buttons
backwardsButton.accessibilityLabel = _NS("Go to previous item");
playPauseButton.accessibilityLabel = _NS("Toggle Play/Pause");
forwardButton.accessibilityLabel = _NS("Go to next item");
randomButton.accessibilityLabel = _NS("Toggle random order playback");
// Populate menu items with localized strings
[showMainWindowItem setTitle:_NS("Show Main Window")];
[pathActionItem setTitle:_NS("Path/URL Action")];
[quitItem setTitle:_NS("Quit")];
_showTimeElapsed = YES;
// Set our selves up as delegate, to receive menuNeedsUpdate messages, so
// we can update our menu as needed/before it's drawn
[_vlcStatusBarIconMenu setDelegate:self];
// Register notifications
NSNotificationCenter *notificationCenter = NSNotificationCenter.defaultCenter;
[notificationCenter addObserver:self
selector:@selector(updateTimeAndPosition:)
name:VLCPlayerTimeAndPositionChanged
object:nil];
[notificationCenter addObserver:self
selector:@selector(inputItemChanged:)
name:VLCPlayerCurrentMediaItemChanged
object:nil];
[notificationCenter addObserver:self
selector:@selector(hasPreviousChanged:)
name:VLCPlaybackHasPreviousChanged
object:nil];
[notificationCenter addObserver:self
selector:@selector(hasNextChanged:)
name:VLCPlaybackHasNextChanged
object:nil];
[notificationCenter addObserver:self
selector:@selector(configurationChanged:)
name:VLCConfigurationChangedNotification
object:nil];
[self inputItemChanged:nil];
[self setMetadataTitle:_NS("VLC media player") artist:_NS("Nothing playing") album:nil andCover:[NSImage imageNamed:@"noart.png"]];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqualToString: NSStringFromSelector(@selector(isVisible))]) {
bool isVisible = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
// Sync status bar visibility with VLC setting
msg_Dbg(getIntf(), "Status bar icon visibility changed to %i", isVisible);
config_PutInt("macosx-statusicon", isVisible ? 1 : 0);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)configurationChanged:(NSNotification *)aNotification
{
if (var_InheritBool(getIntf(), "macosx-statusicon"))
[self enableMenuIcon];
else
[self disableStatusItem];
}
/* Enables the Status Bar Item and initializes it's image
* and context menu
*/
- (void)enableMenuIcon
{
if (!self.statusItem) {
// Init the status item
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
[self.statusItem setHighlightMode:YES];
[self.statusItem setEnabled:YES];
// Set the status item image
NSImage *menuIcon = [NSImage imageNamed:@"VLCStatusBarIcon"];
[menuIcon setTemplate:YES];
[self.statusItem setImage:menuIcon];
// Attach pull-down menu
[self.statusItem setMenu:_vlcStatusBarIconMenu];
if (@available(macOS 10.12, *)) {
[self.statusItem setBehavior:NSStatusItemBehaviorRemovalAllowed];
[self.statusItem setAutosaveName:@"statusBarItem"];
[self.statusItem addObserver:self forKeyPath:NSStringFromSelector(@selector(isVisible))
options:NSKeyValueObservingOptionNew context:NULL];
}
}
if (@available(macOS 10.12, *)) {
// Sync VLC setting with status bar visibility setting (10.12 runtime only)
[self.statusItem setVisible:YES];
}
}
- (void)disableStatusItem
{
if (!self.statusItem)
return;
// Lets keep alive the object in Sierra, and destroy it in older OS versions
if (@available(macOS 10.12, *)) {
self.statusItem.visible = NO;
} else {
[[NSStatusBar systemStatusBar] removeStatusItem:self.statusItem];
self.statusItem = nil;
}
}
- (void)dealloc
{
if (self.statusItem && [self.statusItem respondsToSelector:@selector(isVisible)]) {
[self.statusItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(isVisible)) context:NULL];
}
// Cleanup
[NSNotificationCenter.defaultCenter removeObserver:self];
}
#pragma mark -
#pragma mark Event callback functions
/* Menu update delegate
* Called before menu is opened/displayed
*/
- (void)menuNeedsUpdate:(NSMenu *)menu
{
[self updateMenuItemRandom];
[self updateDynamicMenuItemText];
}
/* Callback to update current playback time
* Called by InputManager
*/
- (void)updateTimeAndPosition:(NSNotification *)aNotification
{
VLCPlayerController *playerController = aNotification.object;
VLCInputItem *inputItem = playerController.currentMedia;
if (inputItem) {
vlc_tick_t duration = inputItem.duration;
vlc_tick_t time = playerController.time;
if (duration == 0) {
/* Infinite duration */
[progressField setStringValue:[NSString stringWithDuration:duration currentTime:time negative:NO]];
[totalField setStringValue:@""];
} else {
/* Not unknown, update displayed duration */
if (_showTimeElapsed) {
[progressField setStringValue:[NSString stringWithDuration:duration currentTime:time negative:NO]];
} else {
[progressField setStringValue:[NSString stringWithDuration:duration currentTime:time negative:YES]];
}
[totalField setStringValue:[NSString stringWithTimeFromTicks:duration]];
}
[self setStoppedStatus:NO];
} else {
/* Nothing playing */
[progressField setStringValue:@"--:--"];
[totalField setStringValue:@"--:--"];
[self setStoppedStatus:YES];
}
}
#pragma mark -
#pragma mark Update functions
- (void)updateCachedURLOfCurrentMedia:(VLCInputItem *)inputItem
{
if (!inputItem) {
_currentPlaybackUrl = nil;
return;
}
_currentPlaybackUrl = inputItem.decodedMRL;
}
- (void)hasPreviousChanged:(NSNotification *)aNotification
{
backwardsButton.enabled = VLCMain.sharedInstance.playQueueController.hasPreviousPlayQueueItem;
}
- (void)hasNextChanged:(NSNotification *)aNotification
{
forwardButton.enabled = VLCMain.sharedInstance.playQueueController.hasNextPlayQueueItem;
}
/* Updates the Metadata for the currently
* playing item or resets it if nothing is playing
*/
- (void)inputItemChanged:(NSNotification *)aNotification
{
NSImage *coverArtImage;
NSString *title;
NSString *nowPlaying;
NSString *artist;
NSString *album;
VLCPlayerController *playerController = aNotification.object;
enum vlc_player_state playerState = playerController.playerState;
VLCInputItem *inputItem = playerController.currentMedia;
switch (playerState) {
case VLC_PLAYER_STATE_PLAYING:
[self setStoppedStatus:NO];
[self setProgressTimeEnabled:YES];
[pathActionItem setEnabled:YES];
[self updateCachedURLOfCurrentMedia:inputItem];
break;
case VLC_PLAYER_STATE_STOPPED:
[self setStoppedStatus:YES];
[self setProgressTimeEnabled:NO];
[pathActionItem setEnabled:NO];
_currentPlaybackUrl = nil;
break;
case VLC_PLAYER_STATE_PAUSED:
[self setStoppedStatus:NO];
[self setProgressTimeEnabled:YES];
[pathActionItem setEnabled:YES];
[self updateCachedURLOfCurrentMedia:inputItem];
[playPauseButton setState:NSOffState];
default:
break;
}
if (inputItem) {
coverArtImage = [[NSImage alloc] initWithContentsOfURL:inputItem.artworkURL];
title = inputItem.title;
nowPlaying = inputItem.nowPlaying;
artist = inputItem.artist;
album = inputItem.album;
} else {
/* Nothing playing */
title = _NS("VLC media player");
artist = _NS("Nothing playing");
}
// Set fallback coverart
if (!coverArtImage) {
coverArtImage = [NSImage imageNamed:@"noart.png"];
}
// Hack to show now playing for streams (ICY)
if (nowPlaying && !artist) {
artist = nowPlaying;
}
// Set the metadata in the UI
[self setMetadataTitle:title artist:artist album:album andCover:coverArtImage];
}
// Update dynamic copy/open menu item status
- (void)updateDynamicMenuItemText
{
if (!_currentPlaybackUrl) {
pathActionItem.hidden = YES;
return;
}
NSURL *itemURI = [NSURL URLWithString:_currentPlaybackUrl];
pathActionItem.hidden = NO;
if ([itemURI.scheme isEqualToString:@"file"]) {
[pathActionItem setTitle:_NS("Reveal in Finder")];
} else {
[pathActionItem setTitle:_NS("Copy URL to clipboard")];
}
}
// Update the random menu item status
- (void)updateMenuItemRandom
{
// Get current random status
[randomButton setState:VLCMain.sharedInstance.playQueueController.playbackOrder == VLC_PLAYLIST_PLAYBACK_ORDER_RANDOM ? NSOnState : NSOffState];
}
#pragma mark -
#pragma mark Utility functions
/* Update the UI to the specified metadata
* Any of the values can be nil and will be replaced with empty strings
* or no cover Image at all
*/
- (void)setMetadataTitle:(NSString *)title
artist:(NSString *)artist
album:(NSString *)album
andCover:(NSImage *)cover
{
[titleField setStringValue:(title) ? title : @""];
[artistField setStringValue:(artist) ? artist : @""];
[albumField setStringValue:(album) ? album : @""];
[coverImageView setImage:cover];
}
// Set the play/pause menu item status
- (void)setStoppedStatus:(BOOL)stopped
{
if (stopped) {
[playPauseButton setState:NSOffState];
} else {
[playPauseButton setState:NSOnState];
}
}
- (void)setProgressTimeEnabled:(BOOL)enabled
{
[progressField setEnabled:enabled];
[separatorField setEnabled:enabled];
[totalField setEnabled:enabled];
}
#pragma mark -
#pragma mark Menu item Actions
/* Action: Select the currently playing file in Finder
* or in case of a network stream, copy the URL
*/
- (IBAction)copyOrOpenCurrentPlaybackItem:(id)sender
{
// If nothing playing, there is nothing to do
if (!_currentPlaybackUrl) {
return;
}
// Check if path or URL
NSURL *itemURI = [NSURL URLWithString:_currentPlaybackUrl];
if ([itemURI.scheme isEqualToString:@"file"]) {
// Local file, open in Finder
[NSWorkspace.sharedWorkspace selectFile:itemURI.path
inFileViewerRootedAtPath:itemURI.path];
} else {
// URL, copy to pasteboard
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
[pasteboard setString:_currentPlaybackUrl forType:NSPasteboardTypeString];
}
}
// Action: Show VLC main window
- (IBAction)statusBarIconShowMainWindow:(id)sender
{
[NSApplication.sharedApplication activateIgnoringOtherApps:YES];
[(NSWindow *)VLCMain.sharedInstance.libraryWindow makeKeyAndOrderFront:sender];
}
// Action: Toggle Play / Pause
- (IBAction)statusBarIconTogglePlayPause:(id)sender
{
VLCPlayQueueController * const playQueueController = VLCMain.sharedInstance.playQueueController;
VLCPlayerController * const playerController = playQueueController.playerController;
enum vlc_player_state playerState = playerController.playerState;
if (playerState != VLC_PLAYER_STATE_PAUSED) {
[playerController pause];
} else if (playerState == VLC_PLAYER_STATE_PAUSED) {
[playerController resume];
} else {
[playQueueController startPlayQueue];
}
}
// Action: Go to next track
- (IBAction)statusBarIconNext:(id)sender
{
[VLCMain.sharedInstance.playQueueController playNextItem];
}
// Action: Go to previous track
- (IBAction)statusBarIconPrevious:(id)sender
{
[VLCMain.sharedInstance.playQueueController playPreviousItem];
}
// Action: Toggle random playback (shuffle)
- (IBAction)statusBarIconToggleRandom:(id)sender
{
VLCPlayQueueController * const playQueueController = VLCMain.sharedInstance.playQueueController;
playQueueController.playbackOrder = (playQueueController.playbackOrder == VLC_PLAYLIST_PLAYBACK_ORDER_RANDOM) ? VLC_PLAYLIST_PLAYBACK_ORDER_NORMAL : VLC_PLAYLIST_PLAYBACK_ORDER_RANDOM;
}
// Action: Toggle between elapsed and remaining time
- (IBAction)toggelProgressTime:(id)sender
{
_showTimeElapsed = (!_showTimeElapsed);
}
// Action: Quit VLC
- (IBAction)quitAction:(id)sender
{
libvlc_Quit(vlc_object_instance(_intf));
}
- (IBAction)statusBarIconShowMiniAudioPlayer:(id)sender
{
[VLCMain.sharedInstance.detachedAudioWindow makeKeyAndOrderFront:sender];
}
@end