VLCStatusBarIcon.m 16.3 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 84 85 86 87 88 89

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

    if (self) {
        msg_Dbg(getIntf(), "Loading VLCStatusBarIcon");
        [NSBundle loadNibNamed:@"VLCStatusBarIconMainMenu" owner:self];
    }

    return self;
}

90
- (void)awakeFromNib
91 92
{
    [super awakeFromNib];
93
    [self configurationChanged:nil];
94

95
    // Set Accessibility Attributes for Image Buttons
96
    [backwardsButton.cell accessibilitySetOverrideValue:_NS("Go to previous item")
97 98
                                           forAttribute:NSAccessibilityDescriptionAttribute];

99
    [playPauseButton.cell accessibilitySetOverrideValue:_NS("Toggle Play/Pause")
100 101
                                           forAttribute:NSAccessibilityDescriptionAttribute];

102
    [forwardButton.cell accessibilitySetOverrideValue:_NS("Go to next item")
103 104 105 106 107 108
                                         forAttribute:NSAccessibilityDescriptionAttribute];

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

109 110
    // Populate menu items with localized strings
    [showMainWindowItem setTitle:_NS("Show Main Window")];
111
    [pathActionItem setTitle:_NS("Path/URL Action")];
112 113
    [quitItem setTitle:_NS("Quit")];

114
    showTimeElapsed = YES;
115 116 117 118

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

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

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

148 149 150 151 152 153 154 155
- (void)configurationChanged:(id)obj
{
    if (var_InheritBool(getIntf(), "macosx-statusicon"))
        [self enableMenuIcon];
    else
        [self disableStatusItem];
}

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

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

186
    if (OSX_SIERRA_AND_HIGHER) {
187
        // Sync VLC setting with status bar visibility setting (10.12 runtime only)
188 189
        [self.statusItem setVisible:YES];
    }
190
}
191

192 193 194 195
- (void)disableStatusItem
{
    if (!self.statusItem)
        return;
196

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

207 208
- (void)dealloc
{
209 210 211 212
    if (self.statusItem && [self.statusItem respondsToSelector:@selector(isVisible)]) {
        [self.statusItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(isVisible)) context:NULL];
    }

213 214 215
    // Cleanup
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
216 217

#pragma mark -
218
#pragma mark Event callback functions
219

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

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

240 241 242 243
/* Callback to update current playback time
 * Called by InputManager
 */
- (void)updateProgress
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 273 274 275 276
    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 */
277 278
        [progressField setStringValue:@"--:--"];
        [totalField setStringValue:@"--:--"];
279 280
        [self setStoppedStatus:YES];
    }
281 282 283 284
}


#pragma mark -
285
#pragma mark Update functions
286

287 288 289 290
/* Updates the Metadata for the currently
 * playing item or resets it if nothing is playing
 */
- (void)updateMetadata
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 321 322 323 324
    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;
    }
325

326 327 328
    if (input) {
        item = input_GetItem(input);
    }
329

330 331 332 333 334 335 336 337 338 339 340 341 342 343
    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);
        }
344

345
        // Get Titel
346
        tmp_cstr = input_item_GetTitleFbName(item);
347 348 349
        if (tmp_cstr) {
            title = toNSStr(tmp_cstr);
            FREENULL(tmp_cstr);
350 351
        }

352 353 354 355 356 357
        // Get Now Playing
        tmp_cstr = input_item_GetNowPlaying(item);
        if (tmp_cstr) {
            nowPlaying = toNSStr(tmp_cstr);
            FREENULL(tmp_cstr);
        }
358

359 360 361 362 363 364
        // Get author
        tmp_cstr = input_item_GetArtist(item);
        if (tmp_cstr) {
            artist = toNSStr(tmp_cstr);
            FREENULL(tmp_cstr);
        }
365

366 367 368 369 370 371 372 373 374 375
        // 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");
376 377
    }

378 379 380 381
    // Set fallback coverart
    if (!coverArtImage) {
        coverArtImage = [NSImage imageNamed:@"noart.png"];
    }
382

383 384 385
    // Hack to show now playing for streams (ICY)
    if (nowPlaying && !artist) {
        artist = nowPlaying;
386 387
    }

388 389 390 391 392 393
    // Set the metadata in the UI
    [self setMetadataTitle:title artist:artist album:album andCover:coverArtImage];

    // Cleanup
    if (input)
        vlc_object_release(input);
394 395 396 397
}



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

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

408 409
    if ([itemURI.scheme isEqualToString:@"file"]) {
        [pathActionItem setTitle:_NS("Select File In Finder")];
410
    } else {
411
        [pathActionItem setTitle:_NS("Copy URL to clipboard")];
412 413 414
    }
}

415 416
// Update the random menu item status
- (void)updateMenuItemRandom
417
{
418 419 420 421 422 423
    // Get current random status
    bool random;
    playlist_t *playlist = pl_Get(getIntf());
    random = var_GetBool(playlist, "random");

    [randButton setState:(random) ? NSOnState : NSOffState];
424 425 426 427
}



428 429
#pragma mark -
#pragma mark Utility functions
430

431 432 433 434 435 436 437 438 439 440 441 442 443 444
/* 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];
}
445

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

457 458
- (void)setProgressTimeEnabled:(BOOL)enabled
{
459 460 461
    [progressField setEnabled:enabled];
    [separatorField setEnabled:enabled];
    [totalField setEnabled:enabled];
462
}
463 464


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

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

495 496
    // Check if path or URL
    NSURL *itemURI = [NSURL URLWithString:_currentPlaybackUrl];
497

498 499 500 501 502 503 504 505 506 507 508
    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];
    }
}
509

510
// Action: Show VLC main window
511 512 513 514 515 516
- (IBAction)restoreMainWindow:(id)sender
{
    [[VLCApplication sharedApplication] activateIgnoringOtherApps:YES];
    [[[VLCMain sharedInstance] mainWindow] makeKeyAndOrderFront:sender];
}

517
// Action: Toggle Play / Pause
518 519 520 521 522
- (IBAction)statusBarIconTogglePlayPause:(id)sender
{
    [[VLCCoreInteraction sharedInstance] playOrPause];
}

523
// Action: Stop playback
524 525 526 527 528
- (IBAction)statusBarIconStop:(id)sender
{
    [[VLCCoreInteraction sharedInstance] stop];
}

529
// Action: Go to next track
530 531 532 533 534
- (IBAction)statusBarIconNext:(id)sender
{
    [[VLCCoreInteraction sharedInstance] next];
}

535
// Action: Go to previous track
536 537 538 539 540
- (IBAction)statusBarIconPrevious:(id)sender
{
    [[VLCCoreInteraction sharedInstance] previous];
}

541
// Action: Toggle random playback (shuffle)
542 543 544 545 546
- (IBAction)statusBarIconToggleRandom:(id)sender
{
    [[VLCCoreInteraction sharedInstance] shuffle];
}

547 548 549 550 551
// Action: Toggle between elapsed and remaining time
- (IBAction)toggelProgressTime:(id)sender
{
    showTimeElapsed = (!showTimeElapsed);
}
552

553
// Action: Quit VLC
554 555 556 557 558 559
- (IBAction)quitAction:(id)sender
{
    [[NSApplication sharedApplication] terminate:nil];
}

@end