VLCStatusBarIcon.m 16.4 KB
Newer Older
Goran Dokic's avatar
Goran Dokic committed
1
/*****************************************************************************
2
 * VLCStatusBarIcon.m: Status bar icon controller/delegate
Goran Dokic's avatar
Goran Dokic committed
3
 *****************************************************************************
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's avatar
David 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>
David's avatar
David committed
32

33
#import "CompatibilityFixes.h"
David's avatar
David committed
34
#import "VLCCoreInteraction.h"
David's avatar
David committed
35
#import "VLCStringUtility.h"
Goran Dokic's avatar
Goran Dokic committed
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

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

72
@implementation VLCStatusBarIcon
Goran Dokic's avatar
Goran Dokic committed
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
Goran Dokic's avatar
Goran Dokic committed
91 92
{
    [super awakeFromNib];
93 94 95 96

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

97
    [self configurationChanged:nil];
Goran Dokic's avatar
Goran Dokic committed
98

99
    // Set Accessibility Attributes for Image Buttons
100
    [backwardsButton.cell accessibilitySetOverrideValue:_NS("Go to previous item")
101 102
                                           forAttribute:NSAccessibilityDescriptionAttribute];

103
    [playPauseButton.cell accessibilitySetOverrideValue:_NS("Toggle Play/Pause")
104 105
                                           forAttribute:NSAccessibilityDescriptionAttribute];

106
    [forwardButton.cell accessibilitySetOverrideValue:_NS("Go to next item")
107 108 109 110 111 112
                                         forAttribute:NSAccessibilityDescriptionAttribute];

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

113 114
    // Populate menu items with localized strings
    [showMainWindowItem setTitle:_NS("Show Main Window")];
115
    [pathActionItem setTitle:_NS("Path/URL Action")];
116 117
    [quitItem setTitle:_NS("Quit")];

118
    showTimeElapsed = YES;
Goran Dokic's avatar
Goran Dokic committed
119 120 121 122

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

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(configurationChanged:)
                                                 name:VLCConfigurationChangedNotification
                                               object:nil];
134
}
Goran Dokic's avatar
Goran Dokic committed
135

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
- (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];
    }
}

152 153 154 155 156 157 158 159
- (void)configurationChanged:(id)obj
{
    if (var_InheritBool(getIntf(), "macosx-statusicon"))
        [self enableMenuIcon];
    else
        [self disableStatusItem];
}

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

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

190
    if (OSX_SIERRA_AND_HIGHER) {
191
        // Sync VLC setting with status bar visibility setting (10.12 runtime only)
192 193
        [self.statusItem setVisible:YES];
    }
194
}
Goran Dokic's avatar
Goran Dokic committed
195

196 197 198 199
- (void)disableStatusItem
{
    if (!self.statusItem)
        return;
Goran Dokic's avatar
Goran Dokic committed
200

201
    // Lets keep alive the object in Sierra, and destroy it in older OS versions
202
    if (OSX_SIERRA_AND_HIGHER) {
203 204 205 206 207
        self.statusItem.visible = NO;
    } else {
        [[NSStatusBar systemStatusBar] removeStatusItem:self.statusItem];
        self.statusItem = nil;
    }
208
#pragma clang diagnostic pop
Goran Dokic's avatar
Goran Dokic committed
209 210
}

211 212
- (void)dealloc
{
213 214 215 216
    if (self.statusItem && [self.statusItem respondsToSelector:@selector(isVisible)]) {
        [self.statusItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(isVisible)) context:NULL];
    }

217 218 219
    // Cleanup
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
Goran Dokic's avatar
Goran Dokic committed
220 221

#pragma mark -
222
#pragma mark Event callback functions
Goran Dokic's avatar
Goran Dokic committed
223

224 225 226
/* Menu update delegate
 * Called before menu is opened/displayed
 */
Goran Dokic's avatar
Goran Dokic committed
227 228
- (void)menuNeedsUpdate:(NSMenu *)menu
{
229
    [self updateMetadata];
Goran Dokic's avatar
Goran Dokic committed
230
    [self updateMenuItemRandom];
231
    [self updateDynamicMenuItemText];
Goran Dokic's avatar
Goran Dokic committed
232 233
}

234 235 236 237
/* 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
238
{
239 240 241
    [self updateMetadata];
    [self updateProgress];
    [self updateDynamicMenuItemText];
Goran Dokic's avatar
Goran Dokic committed
242 243
}

244 245 246 247
/* Callback to update current playback time
 * Called by InputManager
 */
- (void)updateProgress
Goran Dokic's avatar
Goran Dokic committed
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 277 278 279 280
    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 */
281 282
        [progressField setStringValue:@"--:--"];
        [totalField setStringValue:@"--:--"];
283 284
        [self setStoppedStatus:YES];
    }
Goran Dokic's avatar
Goran Dokic committed
285 286 287 288
}


#pragma mark -
289
#pragma mark Update functions
Goran Dokic's avatar
Goran Dokic committed
290

291 292 293 294
/* Updates the Metadata for the currently
 * playing item or resets it if nothing is playing
 */
- (void)updateMetadata
Goran Dokic's avatar
Goran Dokic committed
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 325 326 327 328
    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
329

330 331 332
    if (input) {
        item = input_GetItem(input);
    }
Goran Dokic's avatar
Goran Dokic committed
333

334 335 336 337 338 339 340 341 342 343 344 345 346 347
    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
348

349
        // Get Titel
350
        tmp_cstr = input_item_GetTitleFbName(item);
351 352 353
        if (tmp_cstr) {
            title = toNSStr(tmp_cstr);
            FREENULL(tmp_cstr);
Goran Dokic's avatar
Goran Dokic committed
354 355
        }

356 357 358 359 360 361
        // 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
362

363 364 365 366 367 368
        // 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
369

370 371 372 373 374 375 376 377 378 379
        // 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
380 381
    }

382 383 384 385
    // Set fallback coverart
    if (!coverArtImage) {
        coverArtImage = [NSImage imageNamed:@"noart.png"];
    }
Goran Dokic's avatar
Goran Dokic committed
386

387 388 389
    // Hack to show now playing for streams (ICY)
    if (nowPlaying && !artist) {
        artist = nowPlaying;
Goran Dokic's avatar
Goran Dokic committed
390 391
    }

392 393 394 395 396 397
    // 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
398 399 400 401
}



402
// Update dynamic copy/open menu item status
403
- (void)updateDynamicMenuItemText
Goran Dokic's avatar
Goran Dokic committed
404
{
405 406 407 408 409 410
    if (!_currentPlaybackUrl) {
        [pathActionItem setTitle:_NS("Path/URL Action")];
        return;
    }

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

412 413
    if ([itemURI.scheme isEqualToString:@"file"]) {
        [pathActionItem setTitle:_NS("Select File In Finder")];
Goran Dokic's avatar
Goran Dokic committed
414
    } else {
415
        [pathActionItem setTitle:_NS("Copy URL to clipboard")];
Goran Dokic's avatar
Goran Dokic committed
416 417 418
    }
}

419 420
// Update the random menu item status
- (void)updateMenuItemRandom
Goran Dokic's avatar
Goran Dokic committed
421
{
422 423 424 425 426 427
    // 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
428 429 430 431
}



432 433
#pragma mark -
#pragma mark Utility functions
Goran Dokic's avatar
Goran Dokic committed
434

435 436 437 438 439 440 441 442 443 444 445 446 447 448
/* 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
449

450 451
// Set the play/pause menu item status
- (void)setStoppedStatus:(BOOL)stopped
Goran Dokic's avatar
Goran Dokic committed
452
{
453 454 455
    isStopped = stopped;
    if (stopped) {
        [playPauseButton setState:NSOffState];
Goran Dokic's avatar
Goran Dokic committed
456
    } else {
457
        [playPauseButton setState:NSOnState];
Goran Dokic's avatar
Goran Dokic committed
458 459 460
    }
}

461 462
- (void)setProgressTimeEnabled:(BOOL)enabled
{
463 464 465
    [progressField setEnabled:enabled];
    [separatorField setEnabled:enabled];
    [totalField setEnabled:enabled];
466
}
Goran Dokic's avatar
Goran Dokic committed
467 468


469 470 471 472 473
/* Returns VLC playlist status
 * Check for constants:
 *   PLAYLIST_RUNNING, PLAYLIST_STOPPED, PLAYLIST_PAUSED
 */
- (int)getPlaylistPlayStatus
Goran Dokic's avatar
Goran Dokic committed
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
{
    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

489 490 491 492
/* 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
493
{
494 495 496
    // If nothing playing, there is nothing to do
    if (!_currentPlaybackUrl) {
        return;
Goran Dokic's avatar
Goran Dokic committed
497 498
    }

499 500
    // Check if path or URL
    NSURL *itemURI = [NSURL URLWithString:_currentPlaybackUrl];
Goran Dokic's avatar
Goran Dokic committed
501

502 503 504 505 506 507 508 509 510 511 512
    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
513

514
// Action: Show VLC main window
Goran Dokic's avatar
Goran Dokic committed
515 516 517 518 519 520
- (IBAction)restoreMainWindow:(id)sender
{
    [[VLCApplication sharedApplication] activateIgnoringOtherApps:YES];
    [[[VLCMain sharedInstance] mainWindow] makeKeyAndOrderFront:sender];
}

521
// Action: Toggle Play / Pause
Goran Dokic's avatar
Goran Dokic committed
522 523 524 525 526
- (IBAction)statusBarIconTogglePlayPause:(id)sender
{
    [[VLCCoreInteraction sharedInstance] playOrPause];
}

527
// Action: Stop playback
Goran Dokic's avatar
Goran Dokic committed
528 529 530 531 532
- (IBAction)statusBarIconStop:(id)sender
{
    [[VLCCoreInteraction sharedInstance] stop];
}

533
// Action: Go to next track
Goran Dokic's avatar
Goran Dokic committed
534 535 536 537 538
- (IBAction)statusBarIconNext:(id)sender
{
    [[VLCCoreInteraction sharedInstance] next];
}

539
// Action: Go to previous track
Goran Dokic's avatar
Goran Dokic committed
540 541 542 543 544
- (IBAction)statusBarIconPrevious:(id)sender
{
    [[VLCCoreInteraction sharedInstance] previous];
}

545
// Action: Toggle random playback (shuffle)
Goran Dokic's avatar
Goran Dokic committed
546 547 548 549 550
- (IBAction)statusBarIconToggleRandom:(id)sender
{
    [[VLCCoreInteraction sharedInstance] shuffle];
}

551 552 553 554 555
// Action: Toggle between elapsed and remaining time
- (IBAction)toggelProgressTime:(id)sender
{
    showTimeElapsed = (!showTimeElapsed);
}
Goran Dokic's avatar
Goran Dokic committed
556

557
// Action: Quit VLC
Goran Dokic's avatar
Goran Dokic committed
558 559 560 561 562 563
- (IBAction)quitAction:(id)sender
{
    [[NSApplication sharedApplication] terminate:nil];
}

@end