VLCStatusBarIcon.m 13.6 KB
Newer Older
Goran Dokic's avatar
Goran Dokic committed
1 2 3
/*****************************************************************************
 * VLCStatusBarIcon.m: Mac OS X module for vlc
 *****************************************************************************
4
 * Copyright (C) 2016 VLC authors and VideoLAN
Goran Dokic's avatar
Goran Dokic committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
 * $Id$
 *
 * Authors: Goran Dokic <vlc at 8hz 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 "VLCStatusBarIcon.h"

David Fuhrmann's avatar
David Fuhrmann committed
26
#import "VLCMainMenu.h"
27
#import "VLCMain.h"
Goran Dokic's avatar
Goran Dokic committed
28 29 30 31

#import <vlc_common.h>
#import <vlc_playlist.h>
#import <vlc_input.h>
32 33

#import "VLCCoreInteraction.h"
34
#import "VLCStringUtility.h"
Goran Dokic's avatar
Goran Dokic committed
35

36 37
#import "VLCApplication.h"

38 39 40 41
@interface VLCStatusBarIcon ()
{
    NSMenuItem *_vlcStatusBarMenuItem;

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
    /* 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 *randButton;

    /* Outlets for menu items */
    IBOutlet NSMenuItem *pathActionItem;
59 60 61
    IBOutlet NSMenuItem *showMainWindowItem;
    IBOutlet NSMenuItem *quitItem;

62 63 64
    BOOL isStopped;
    BOOL showTimeElapsed;
    NSString *_currentPlaybackUrl;
65 66 67
}
@end

Goran Dokic's avatar
Goran Dokic committed
68 69 70
#pragma mark -
#pragma mark Implementation

71
@implementation VLCStatusBarIcon
Goran Dokic's avatar
Goran Dokic committed
72 73 74 75

#pragma mark -
#pragma mark Init

76
- (void)awakeFromNib
Goran Dokic's avatar
Goran Dokic committed
77 78 79 80
{
    [super awakeFromNib];
    [self enableMenuIcon];

81 82 83 84 85 86 87 88 89 90 91 92 93 94
    // Set Accessibility Attributes for Image Buttons
    [backwardsButton.cell accessibilitySetOverrideValue:_NS("Go to previous track")
                                           forAttribute:NSAccessibilityDescriptionAttribute];

    [playPauseButton.cell accessibilitySetOverrideValue:_NS("Play or pause current media")
                                           forAttribute:NSAccessibilityDescriptionAttribute];

    [forwardButton.cell accessibilitySetOverrideValue:_NS("Go to next track")
                                         forAttribute:NSAccessibilityDescriptionAttribute];

    [randButton.cell accessibilitySetOverrideValue:_NS("Toggle random order playback")
                                      forAttribute:NSAccessibilityDescriptionAttribute];
    

95 96
    // Populate menu items with localized strings
    [showMainWindowItem setTitle:_NS("Show Main Window")];
97
    [pathActionItem setTitle:_NS("Path/URL Action")];
98 99
    [quitItem setTitle:_NS("Quit")];

100
    showTimeElapsed = YES;
Goran Dokic's avatar
Goran Dokic committed
101 102 103 104

    // 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];
105 106 107 108 109 110 111
    
    // Register notifications
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateNowPlayingInfo)
                                                 name:VLCInputChangedNotification
                                               object:nil];
}
Goran Dokic's avatar
Goran Dokic committed
112

113 114 115 116 117 118 119 120 121 122
/* Enables the Status Bar Item and initializes it's image
 * and context menu
 */
- (void)enableMenuIcon
{
    // Init the status item
    _statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
    [_statusItem setHighlightMode:YES];
    [_statusItem setEnabled:YES];
    [_statusItem setTarget:self];
Goran Dokic's avatar
Goran Dokic committed
123

124 125 126 127
    // Set the status item image
    NSImage *menuIcon = [NSImage imageNamed:@"VLCStatusBarIcon"];
    [menuIcon setTemplate:YES];
    [_statusItem setImage:menuIcon];
Goran Dokic's avatar
Goran Dokic committed
128

129 130
    // Attach pull-down menu
    [_statusItem setMenu:_vlcStatusBarIconMenu];
Goran Dokic's avatar
Goran Dokic committed
131 132
}

133 134 135 136 137
- (void)dealloc
{
    // Cleanup
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
Goran Dokic's avatar
Goran Dokic committed
138 139

#pragma mark -
140
#pragma mark Event callback functions
Goran Dokic's avatar
Goran Dokic committed
141

142 143 144
/* Menu update delegate
 * Called before menu is opened/displayed
 */
Goran Dokic's avatar
Goran Dokic committed
145 146
- (void)menuNeedsUpdate:(NSMenu *)menu
{
147
    [self updateMetadata];
Goran Dokic's avatar
Goran Dokic committed
148
    [self updateMenuItemRandom];
149
    [self updateDynamicMenuItemText];
Goran Dokic's avatar
Goran Dokic committed
150 151
}

152 153 154 155
/* This is called whenever the playback status for VLC changes and here
 * we can update our information in the menu/view
 */
- (void) updateNowPlayingInfo
Goran Dokic's avatar
Goran Dokic committed
156
{
157 158 159
    [self updateMetadata];
    [self updateProgress];
    [self updateDynamicMenuItemText];
Goran Dokic's avatar
Goran Dokic committed
160 161
}

162 163 164 165
/* Callback to update current playback time
 * Called by InputManager
 */
- (void)updateProgress
Goran Dokic's avatar
Goran Dokic committed
166
{
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
    input_thread_t *input = pl_CurrentInput(getIntf());

    if (input) {
        NSString *elapsedTime;
        NSString *remainingTime;
        NSString *totalTime;

        /* Get elapsed and remaining time */
        elapsedTime = [[VLCStringUtility sharedInstance] getCurrentTimeAsString:input negative:NO];
        remainingTime = [[VLCStringUtility sharedInstance] getCurrentTimeAsString:input negative:YES];

        /* Check item duration */
        mtime_t dur = input_item_GetDuration(input_GetItem(input));

        if (dur == -1) {
            /* Unknown duration, possibly due to buffering */
            [progressField setStringValue:@"--:--"];
            [totalField setStringValue:@"--:--"];
        } else if (dur == 0) {
            /* Infinite duration */
            [progressField setStringValue:elapsedTime];
            [totalField setStringValue:@"∞"];
        } else {
            /* Not unknown, update displayed duration */
            totalTime = [[VLCStringUtility sharedInstance] stringForTime:(dur/1000000)];
            [progressField setStringValue:(showTimeElapsed) ? elapsedTime : remainingTime];
            [totalField setStringValue:totalTime];
        }
        [self setStoppedStatus:NO];
        vlc_object_release(input);
    } else {
        /* Nothing playing */
199 200
        [progressField setStringValue:@"--:--"];
        [totalField setStringValue:@"--:--"];
201 202
        [self setStoppedStatus:YES];
    }
Goran Dokic's avatar
Goran Dokic committed
203 204 205 206
}


#pragma mark -
207
#pragma mark Update functions
Goran Dokic's avatar
Goran Dokic committed
208

209 210 211 212
/* Updates the Metadata for the currently
 * playing item or resets it if nothing is playing
 */
- (void)updateMetadata
Goran Dokic's avatar
Goran Dokic committed
213
{
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
    NSImage         *coverArtImage;
    NSString        *title;
    NSString        *nowPlaying;
    NSString        *artist;
    NSString        *album;
    input_thread_t  *input = pl_CurrentInput(getIntf());
    input_item_t    *item  = NULL;

    // Update play/pause status
    switch ([self getPlaylistPlayStatus]) {
        case PLAYLIST_RUNNING:
            [self setStoppedStatus:NO];
            [self setProgressTimeEnabled:YES];
            [pathActionItem setEnabled:YES];
            _currentPlaybackUrl = [[[VLCCoreInteraction sharedInstance]
                                    URLOfCurrentPlaylistItem] absoluteString];
            break;
        case PLAYLIST_STOPPED:
            [self setStoppedStatus:YES];
            [self setProgressTimeEnabled:NO];
            [pathActionItem setEnabled:NO];
            _currentPlaybackUrl = nil;
            break;
        case PLAYLIST_PAUSED:
            [self setStoppedStatus:NO];
            [self setProgressTimeEnabled:YES];
            [pathActionItem setEnabled:YES];
            _currentPlaybackUrl = [[[VLCCoreInteraction sharedInstance]
                                    URLOfCurrentPlaylistItem] absoluteString];
            [playPauseButton setState:NSOffState];
        default:
            break;
    }
Goran Dokic's avatar
Goran Dokic committed
247

248 249 250
    if (input) {
        item = input_GetItem(input);
    }
Goran Dokic's avatar
Goran Dokic committed
251

252 253 254 255 256 257 258 259 260 261 262 263 264 265
    if (item) {
        /* Something is playing */
        static char *tmp_cstr = NULL;

        // Get Coverart
        tmp_cstr = input_item_GetArtworkURL(item);
        if (tmp_cstr) {
            NSString *tempStr = toNSStr(tmp_cstr);
            if (![tempStr hasPrefix:@"attachment://"]) {
                coverArtImage = [[NSImage alloc]
                                 initWithContentsOfURL:[NSURL URLWithString:tempStr]];
            }
            FREENULL(tmp_cstr);
        }
Goran Dokic's avatar
Goran Dokic committed
266

267 268 269 270 271
        // Get Titel
        tmp_cstr = input_item_GetTitle(item);
        if (tmp_cstr) {
            title = toNSStr(tmp_cstr);
            FREENULL(tmp_cstr);
Goran Dokic's avatar
Goran Dokic committed
272 273
        }

274 275 276 277 278 279
        // Get Now Playing
        tmp_cstr = input_item_GetNowPlaying(item);
        if (tmp_cstr) {
            nowPlaying = toNSStr(tmp_cstr);
            FREENULL(tmp_cstr);
        }
Goran Dokic's avatar
Goran Dokic committed
280

281 282 283 284 285 286
        // Get author
        tmp_cstr = input_item_GetArtist(item);
        if (tmp_cstr) {
            artist = toNSStr(tmp_cstr);
            FREENULL(tmp_cstr);
        }
Goran Dokic's avatar
Goran Dokic committed
287

288 289 290 291 292 293 294 295 296 297
        // Get album
        tmp_cstr = input_item_GetAlbum(item);
        if (tmp_cstr) {
            album = toNSStr(tmp_cstr);
            FREENULL(tmp_cstr);
        }
    } else {
        /* Nothing playing */
        title = _NS("VLC media player");
        artist = _NS("Nothing playing");
Goran Dokic's avatar
Goran Dokic committed
298 299
    }

300 301 302 303
    // Set fallback coverart
    if (!coverArtImage) {
        coverArtImage = [NSImage imageNamed:@"noart.png"];
    }
Goran Dokic's avatar
Goran Dokic committed
304

305 306 307
    // Hack to show now playing for streams (ICY)
    if (nowPlaying && !artist) {
        artist = nowPlaying;
Goran Dokic's avatar
Goran Dokic committed
308 309
    }

310 311 312 313 314 315
    // Set the metadata in the UI
    [self setMetadataTitle:title artist:artist album:album andCover:coverArtImage];

    // Cleanup
    if (input)
        vlc_object_release(input);
Goran Dokic's avatar
Goran Dokic committed
316 317 318 319
}



320
// Update dynamic copy/open menu item status
321
- (void)updateDynamicMenuItemText
Goran Dokic's avatar
Goran Dokic committed
322
{
323 324 325 326 327 328
    if (!_currentPlaybackUrl) {
        [pathActionItem setTitle:_NS("Path/URL Action")];
        return;
    }

    NSURL *itemURI = [NSURL URLWithString:_currentPlaybackUrl];
Goran Dokic's avatar
Goran Dokic committed
329

330 331
    if ([itemURI.scheme isEqualToString:@"file"]) {
        [pathActionItem setTitle:_NS("Select File In Finder")];
Goran Dokic's avatar
Goran Dokic committed
332
    } else {
333
        [pathActionItem setTitle:_NS("Copy URL to clipboard")];
Goran Dokic's avatar
Goran Dokic committed
334 335 336
    }
}

337 338
// Update the random menu item status
- (void)updateMenuItemRandom
Goran Dokic's avatar
Goran Dokic committed
339
{
340 341 342 343 344 345
    // Get current random status
    bool random;
    playlist_t *playlist = pl_Get(getIntf());
    random = var_GetBool(playlist, "random");

    [randButton setState:(random) ? NSOnState : NSOffState];
Goran Dokic's avatar
Goran Dokic committed
346 347 348 349
}



350 351
#pragma mark -
#pragma mark Utility functions
Goran Dokic's avatar
Goran Dokic committed
352

353 354 355 356 357 358 359 360 361 362 363 364 365 366
/* 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];
}
Goran Dokic's avatar
Goran Dokic committed
367

368 369
// Set the play/pause menu item status
- (void)setStoppedStatus:(BOOL)stopped
Goran Dokic's avatar
Goran Dokic committed
370
{
371 372 373
    isStopped = stopped;
    if (stopped) {
        [playPauseButton setState:NSOffState];
Goran Dokic's avatar
Goran Dokic committed
374
    } else {
375
        [playPauseButton setState:NSOnState];
Goran Dokic's avatar
Goran Dokic committed
376 377 378
    }
}

379 380
- (void)setProgressTimeEnabled:(BOOL)enabled
{
381 382 383
    [progressField setEnabled:enabled];
    [separatorField setEnabled:enabled];
    [totalField setEnabled:enabled];
384
}
Goran Dokic's avatar
Goran Dokic committed
385 386


387 388 389 390 391
/* Returns VLC playlist status
 * Check for constants:
 *   PLAYLIST_RUNNING, PLAYLIST_STOPPED, PLAYLIST_PAUSED
 */
- (int)getPlaylistPlayStatus
Goran Dokic's avatar
Goran Dokic committed
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
{
    int res;
    playlist_t *p_playlist = pl_Get(getIntf());

    PL_LOCK;
    res = playlist_Status( p_playlist );
    PL_UNLOCK;

    return res;
}


#pragma mark -
#pragma mark Menu item Actions

407 408 409 410
/* Action: Select the currently playing file in Finder
 *         or in case of a network stream, copy the URL
 */
- (IBAction)copyOrOpenCurrentPlaybackItem:(id)sender
Goran Dokic's avatar
Goran Dokic committed
411
{
412 413 414
    // If nothing playing, there is nothing to do
    if (!_currentPlaybackUrl) {
        return;
Goran Dokic's avatar
Goran Dokic committed
415 416
    }

417 418
    // Check if path or URL
    NSURL *itemURI = [NSURL URLWithString:_currentPlaybackUrl];
Goran Dokic's avatar
Goran Dokic committed
419

420 421 422 423 424 425 426 427 428 429 430
    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];
    }
}
Goran Dokic's avatar
Goran Dokic committed
431

432
// Action: Show VLC main window
Goran Dokic's avatar
Goran Dokic committed
433 434 435 436 437 438
- (IBAction)restoreMainWindow:(id)sender
{
    [[VLCApplication sharedApplication] activateIgnoringOtherApps:YES];
    [[[VLCMain sharedInstance] mainWindow] makeKeyAndOrderFront:sender];
}

439
// Action: Toggle Play / Pause
Goran Dokic's avatar
Goran Dokic committed
440 441 442 443 444
- (IBAction)statusBarIconTogglePlayPause:(id)sender
{
    [[VLCCoreInteraction sharedInstance] playOrPause];
}

445
// Action: Stop playback
Goran Dokic's avatar
Goran Dokic committed
446 447 448 449 450
- (IBAction)statusBarIconStop:(id)sender
{
    [[VLCCoreInteraction sharedInstance] stop];
}

451
// Action: Go to next track
Goran Dokic's avatar
Goran Dokic committed
452 453 454 455 456
- (IBAction)statusBarIconNext:(id)sender
{
    [[VLCCoreInteraction sharedInstance] next];
}

457
// Action: Go to previous track
Goran Dokic's avatar
Goran Dokic committed
458 459 460 461 462
- (IBAction)statusBarIconPrevious:(id)sender
{
    [[VLCCoreInteraction sharedInstance] previous];
}

463
// Action: Toggle random playback (shuffle)
Goran Dokic's avatar
Goran Dokic committed
464 465 466 467 468
- (IBAction)statusBarIconToggleRandom:(id)sender
{
    [[VLCCoreInteraction sharedInstance] shuffle];
}

469 470 471 472 473
// Action: Toggle between elapsed and remaining time
- (IBAction)toggelProgressTime:(id)sender
{
    showTimeElapsed = (!showTimeElapsed);
}
Goran Dokic's avatar
Goran Dokic committed
474

475
// Action: Quit VLC
Goran Dokic's avatar
Goran Dokic committed
476 477 478 479 480 481
- (IBAction)quitAction:(id)sender
{
    [[NSApplication sharedApplication] terminate:nil];
}

@end