VLCStatusBarIcon.m 16 KB
Newer Older
1
/*****************************************************************************
2
 * VLCStatusBarIcon.m: Status bar icon controller/delegate
3
 *****************************************************************************
4
 * Copyright (C) 2016 VLC authors and VideoLAN
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"

26
#import "VLCMainMenu.h"
27
#import "VLCMain.h"
28 29 30 31

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

33
#import "CompatibilityFixes.h"
34
#import "VLCCoreInteraction.h"
35
#import "VLCStringUtility.h"
36

37 38
#import "VLCApplication.h"

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

43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
    /* 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;
60 61 62
    IBOutlet NSMenuItem *showMainWindowItem;
    IBOutlet NSMenuItem *quitItem;

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

69 70 71
#pragma mark -
#pragma mark Implementation

72
@implementation VLCStatusBarIcon
73 74 75 76

#pragma mark -
#pragma mark Init

77 78 79 80 81 82 83

- (instancetype)init
{
    self = [super init];

    if (self) {
        msg_Dbg(getIntf(), "Loading VLCStatusBarIcon");
84
        [[NSBundle mainBundle] loadNibNamed:@"VLCStatusBarIconMainMenu" owner:self topLevelObjects:nil];
85 86 87 88 89
    }

    return self;
}

90
- (void)awakeFromNib
91 92
{
    [super awakeFromNib];
93 94 95 96

    [_controlsView setAutoresizingMask:NSViewWidthSizable];
    [_playbackInfoView setAutoresizingMask:NSViewWidthSizable];

97
    [self configurationChanged:nil];
98

99
    // Set Accessibility Attributes for Image Buttons
100 101 102 103
    backwardsButton.accessibilityLabel = _NS("Go to previous item");
    playPauseButton.accessibilityLabel = _NS("Toggle Play/Pause");
    forwardButton.accessibilityLabel = _NS("Go to next item");
    randButton.accessibilityLabel = _NS("Toggle random order playback");
104

105 106
    // Populate menu items with localized strings
    [showMainWindowItem setTitle:_NS("Show Main Window")];
107
    [pathActionItem setTitle:_NS("Path/URL Action")];
108 109
    [quitItem setTitle:_NS("Quit")];

110
    showTimeElapsed = YES;
111 112 113 114

    // 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];
115 116 117 118 119 120
    
    // Register notifications
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateNowPlayingInfo)
                                                 name:VLCInputChangedNotification
                                               object:nil];
121 122 123 124 125

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(configurationChanged:)
                                                 name:VLCConfigurationChangedNotification
                                               object:nil];
126
}
127

128 129 130 131 132 133 134 135 136 137
- (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);
138
        config_PutInt("macosx-statusicon", isVisible ? 1 : 0);
139 140 141 142 143
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

144 145 146 147 148 149 150 151
- (void)configurationChanged:(id)obj
{
    if (var_InheritBool(getIntf(), "macosx-statusicon"))
        [self enableMenuIcon];
    else
        [self disableStatusItem];
}

152 153 154 155 156
/* Enables the Status Bar Item and initializes it's image
 * and context menu
 */
- (void)enableMenuIcon
{
157 158 159 160 161 162 163 164 165 166 167 168 169 170
    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];

171 172 173
        // Visibility is 10.12+
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
174
        if (OSX_SIERRA_AND_HIGHER) {
175 176 177
            [self.statusItem setBehavior:NSStatusItemBehaviorRemovalAllowed];
            [self.statusItem setAutosaveName:@"statusBarItem"];
            [self.statusItem addObserver:self forKeyPath:NSStringFromSelector(@selector(isVisible))
178
                                 options:NSKeyValueObservingOptionNew context:NULL];
179 180
        }
    }
181

182
    if (OSX_SIERRA_AND_HIGHER) {
183
        // Sync VLC setting with status bar visibility setting (10.12 runtime only)
184 185
        [self.statusItem setVisible:YES];
    }
186
}
187

188 189 190 191
- (void)disableStatusItem
{
    if (!self.statusItem)
        return;
192

193
    // Lets keep alive the object in Sierra, and destroy it in older OS versions
194
    if (OSX_SIERRA_AND_HIGHER) {
195 196 197 198 199
        self.statusItem.visible = NO;
    } else {
        [[NSStatusBar systemStatusBar] removeStatusItem:self.statusItem];
        self.statusItem = nil;
    }
200
#pragma clang diagnostic pop
201 202
}

203 204
- (void)dealloc
{
205 206 207 208
    if (self.statusItem && [self.statusItem respondsToSelector:@selector(isVisible)]) {
        [self.statusItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(isVisible)) context:NULL];
    }

209 210 211
    // Cleanup
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
212 213

#pragma mark -
214
#pragma mark Event callback functions
215

216 217 218
/* Menu update delegate
 * Called before menu is opened/displayed
 */
219 220
- (void)menuNeedsUpdate:(NSMenu *)menu
{
221
    [self updateMetadata];
222
    [self updateMenuItemRandom];
223
    [self updateDynamicMenuItemText];
224 225
}

226 227 228 229
/* This is called whenever the playback status for VLC changes and here
 * we can update our information in the menu/view
 */
- (void) updateNowPlayingInfo
230
{
231 232 233
    [self updateMetadata];
    [self updateProgress];
    [self updateDynamicMenuItemText];
234 235
}

236 237 238 239
/* Callback to update current playback time
 * Called by InputManager
 */
- (void)updateProgress
240
{
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
    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 */
273 274
        [progressField setStringValue:@"--:--"];
        [totalField setStringValue:@"--:--"];
275 276
        [self setStoppedStatus:YES];
    }
277 278 279 280
}


#pragma mark -
281
#pragma mark Update functions
282

283 284 285 286
/* Updates the Metadata for the currently
 * playing item or resets it if nothing is playing
 */
- (void)updateMetadata
287
{
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
    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;
    }
321

322 323 324
    if (input) {
        item = input_GetItem(input);
    }
325

326 327 328 329 330 331 332 333 334 335 336 337 338 339
    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);
        }
340

341
        // Get Titel
342
        tmp_cstr = input_item_GetTitleFbName(item);
343 344 345
        if (tmp_cstr) {
            title = toNSStr(tmp_cstr);
            FREENULL(tmp_cstr);
346 347
        }

348 349 350 351 352 353
        // Get Now Playing
        tmp_cstr = input_item_GetNowPlaying(item);
        if (tmp_cstr) {
            nowPlaying = toNSStr(tmp_cstr);
            FREENULL(tmp_cstr);
        }
354

355 356 357 358 359 360
        // Get author
        tmp_cstr = input_item_GetArtist(item);
        if (tmp_cstr) {
            artist = toNSStr(tmp_cstr);
            FREENULL(tmp_cstr);
        }
361

362 363 364 365 366 367 368 369 370 371
        // 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");
372 373
    }

374 375 376 377
    // Set fallback coverart
    if (!coverArtImage) {
        coverArtImage = [NSImage imageNamed:@"noart.png"];
    }
378

379 380 381
    // Hack to show now playing for streams (ICY)
    if (nowPlaying && !artist) {
        artist = nowPlaying;
382 383
    }

384 385 386 387 388 389
    // Set the metadata in the UI
    [self setMetadataTitle:title artist:artist album:album andCover:coverArtImage];

    // Cleanup
    if (input)
        vlc_object_release(input);
390 391 392 393
}



394
// Update dynamic copy/open menu item status
395
- (void)updateDynamicMenuItemText
396
{
397 398 399 400 401 402
    if (!_currentPlaybackUrl) {
        [pathActionItem setTitle:_NS("Path/URL Action")];
        return;
    }

    NSURL *itemURI = [NSURL URLWithString:_currentPlaybackUrl];
403

404 405
    if ([itemURI.scheme isEqualToString:@"file"]) {
        [pathActionItem setTitle:_NS("Select File In Finder")];
406
    } else {
407
        [pathActionItem setTitle:_NS("Copy URL to clipboard")];
408 409 410
    }
}

411 412
// Update the random menu item status
- (void)updateMenuItemRandom
413
{
414 415 416 417 418 419
    // Get current random status
    bool random;
    playlist_t *playlist = pl_Get(getIntf());
    random = var_GetBool(playlist, "random");

    [randButton setState:(random) ? NSOnState : NSOffState];
420 421 422 423
}



424 425
#pragma mark -
#pragma mark Utility functions
426

427 428 429 430 431 432 433 434 435 436 437 438 439 440
/* 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];
}
441

442 443
// Set the play/pause menu item status
- (void)setStoppedStatus:(BOOL)stopped
444
{
445 446 447
    isStopped = stopped;
    if (stopped) {
        [playPauseButton setState:NSOffState];
448
    } else {
449
        [playPauseButton setState:NSOnState];
450 451 452
    }
}

453 454
- (void)setProgressTimeEnabled:(BOOL)enabled
{
455 456 457
    [progressField setEnabled:enabled];
    [separatorField setEnabled:enabled];
    [totalField setEnabled:enabled];
458
}
459 460


461 462 463 464 465
/* Returns VLC playlist status
 * Check for constants:
 *   PLAYLIST_RUNNING, PLAYLIST_STOPPED, PLAYLIST_PAUSED
 */
- (int)getPlaylistPlayStatus
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
{
    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

481 482 483 484
/* Action: Select the currently playing file in Finder
 *         or in case of a network stream, copy the URL
 */
- (IBAction)copyOrOpenCurrentPlaybackItem:(id)sender
485
{
486 487 488
    // If nothing playing, there is nothing to do
    if (!_currentPlaybackUrl) {
        return;
489 490
    }

491 492
    // Check if path or URL
    NSURL *itemURI = [NSURL URLWithString:_currentPlaybackUrl];
493

494 495 496 497 498 499 500 501 502 503 504
    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];
    }
}
505

506
// Action: Show VLC main window
507 508 509 510 511 512
- (IBAction)restoreMainWindow:(id)sender
{
    [[VLCApplication sharedApplication] activateIgnoringOtherApps:YES];
    [[[VLCMain sharedInstance] mainWindow] makeKeyAndOrderFront:sender];
}

513
// Action: Toggle Play / Pause
514 515 516 517 518
- (IBAction)statusBarIconTogglePlayPause:(id)sender
{
    [[VLCCoreInteraction sharedInstance] playOrPause];
}

519
// Action: Stop playback
520 521 522 523 524
- (IBAction)statusBarIconStop:(id)sender
{
    [[VLCCoreInteraction sharedInstance] stop];
}

525
// Action: Go to next track
526 527 528 529 530
- (IBAction)statusBarIconNext:(id)sender
{
    [[VLCCoreInteraction sharedInstance] next];
}

531
// Action: Go to previous track
532 533 534 535 536
- (IBAction)statusBarIconPrevious:(id)sender
{
    [[VLCCoreInteraction sharedInstance] previous];
}

537
// Action: Toggle random playback (shuffle)
538 539 540 541 542
- (IBAction)statusBarIconToggleRandom:(id)sender
{
    [[VLCCoreInteraction sharedInstance] shuffle];
}

543 544 545 546 547
// Action: Toggle between elapsed and remaining time
- (IBAction)toggelProgressTime:(id)sender
{
    showTimeElapsed = (!showTimeElapsed);
}
548

549
// Action: Quit VLC
550 551 552 553 554 555
- (IBAction)quitAction:(id)sender
{
    [[NSApplication sharedApplication] terminate:nil];
}

@end