macosx: Rework VLC Status bar icon for OS X
[vlc.git] / modules / gui / macosx / VLCStatusBarIcon.m
1 /*****************************************************************************
2  * VLCStatusBarIcon.m: Mac OS X module for vlc
3  *****************************************************************************
4  * Copyright (C) 2016 VLC authors and VideoLAN
5  * $Id$
6  *
7  * Authors: Goran Dokic <vlc at 8hz dot com>
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
22  *****************************************************************************/
23
24 #import "VLCStatusBarIcon.h"
25
26 #import "MainMenu.h"
27 #import "intf.h"
28
29 #import <vlc_common.h>
30 #import <vlc_playlist.h>
31 #import <vlc_input.h>
32 #import <CoreInteraction.h>
33 #import <StringUtility.h>
34
35 @interface VLCStatusBarIcon ()
36 {
37     NSMenuItem *_vlcStatusBarMenuItem;
38
39     /* Outlets for Now Playing labels */
40     IBOutlet NSTextField *titleField;
41     IBOutlet NSTextField *artistField;
42     IBOutlet NSTextField *albumField;
43     IBOutlet NSTextField *progressField;
44     IBOutlet NSTextField *separatorField;
45     IBOutlet NSTextField *totalField;
46     IBOutlet NSImageView *coverImageView;
47
48     /* Outlets for player controls */
49     IBOutlet NSButton *backwardsButton;
50     IBOutlet NSButton *playPauseButton;
51     IBOutlet NSButton *forwardButton;
52     IBOutlet NSButton *randButton;
53
54     /* Outlets for menu items */
55     IBOutlet NSMenuItem *pathActionItem;
56     IBOutlet NSMenuItem *showMainWindowItem;
57     IBOutlet NSMenuItem *quitItem;
58
59     BOOL isStopped;
60     BOOL showTimeElapsed;
61     NSString *_currentPlaybackUrl;
62 }
63 @end
64
65 #pragma mark -
66 #pragma mark Implementation
67
68 @implementation VLCStatusBarIcon
69
70 #pragma mark -
71 #pragma mark Init
72
73 - (void)awakeFromNib
74 {
75     [super awakeFromNib];
76     [self enableMenuIcon];
77
78     // Set Accessibility Attributes for Image Buttons
79     [backwardsButton.cell accessibilitySetOverrideValue:_NS("Go to previous track")
80                                            forAttribute:NSAccessibilityDescriptionAttribute];
81
82     [playPauseButton.cell accessibilitySetOverrideValue:_NS("Play or pause current media")
83                                            forAttribute:NSAccessibilityDescriptionAttribute];
84
85     [forwardButton.cell accessibilitySetOverrideValue:_NS("Go to next track")
86                                          forAttribute:NSAccessibilityDescriptionAttribute];
87
88     [randButton.cell accessibilitySetOverrideValue:_NS("Toggle random order playback")
89                                       forAttribute:NSAccessibilityDescriptionAttribute];
90     
91
92     // Populate menu items with localized strings
93     [showMainWindowItem setTitle:_NS("Show Main Window")];
94     [pathActionItem setTitle:_NS("Path/URL Action")];
95     [quitItem setTitle:_NS("Quit")];
96
97     showTimeElapsed = YES;
98
99     // Set our selves up as delegate, to receive menuNeedsUpdate messages, so
100     // we can update our menu as needed/before it's drawn
101     [_vlcStatusBarIconMenu setDelegate:self];
102     
103     // Register notifications
104     [[NSNotificationCenter defaultCenter] addObserver:self
105                                              selector:@selector(updateNowPlayingInfo)
106                                                  name:VLCInputChangedNotification
107                                                object:nil];
108 }
109
110 /* Enables the Status Bar Item and initializes it's image
111  * and context menu
112  */
113 - (void)enableMenuIcon
114 {
115     // Init the status item
116     _statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
117     [_statusItem setHighlightMode:YES];
118     [_statusItem setEnabled:YES];
119     [_statusItem setTarget:self];
120
121     // Set the status item image
122     NSImage *menuIcon = [NSImage imageNamed:@"VLCStatusBarIcon"];
123     [menuIcon setTemplate:YES];
124     [_statusItem setImage:menuIcon];
125
126     // Attach pull-down menu
127     [_statusItem setMenu:_vlcStatusBarIconMenu];
128 }
129
130 - (void)dealloc
131 {
132     // Cleanup
133     [[NSNotificationCenter defaultCenter] removeObserver:self];
134 }
135
136 #pragma mark -
137 #pragma mark Event callback functions
138
139 /* Menu update delegate
140  * Called before menu is opened/displayed
141  */
142 - (void)menuNeedsUpdate:(NSMenu *)menu
143 {
144     [self updateMetadata];
145     [self updateMenuItemRandom];
146     [self updateDynamicMenuItemText];
147 }
148
149 /* This is called whenever the playback status for VLC changes and here
150  * we can update our information in the menu/view
151  */
152 - (void) updateNowPlayingInfo
153 {
154     [self updateMetadata];
155     [self updateProgress];
156     [self updateDynamicMenuItemText];
157 }
158
159 /* Callback to update current playback time
160  * Called by InputManager
161  */
162 - (void)updateProgress
163 {
164     input_thread_t *input = pl_CurrentInput(getIntf());
165
166     if (input) {
167         NSString *elapsedTime;
168         NSString *remainingTime;
169         NSString *totalTime;
170
171         /* Get elapsed and remaining time */
172         elapsedTime = [[VLCStringUtility sharedInstance] getCurrentTimeAsString:input negative:NO];
173         remainingTime = [[VLCStringUtility sharedInstance] getCurrentTimeAsString:input negative:YES];
174
175         /* Check item duration */
176         mtime_t dur = input_item_GetDuration(input_GetItem(input));
177
178         if (dur == -1) {
179             /* Unknown duration, possibly due to buffering */
180             [progressField setStringValue:@"--:--"];
181             [totalField setStringValue:@"--:--"];
182         } else if (dur == 0) {
183             /* Infinite duration */
184             [progressField setStringValue:elapsedTime];
185             [totalField setStringValue:@"∞"];
186         } else {
187             /* Not unknown, update displayed duration */
188             totalTime = [[VLCStringUtility sharedInstance] stringForTime:(dur/1000000)];
189             [progressField setStringValue:(showTimeElapsed) ? elapsedTime : remainingTime];
190             [totalField setStringValue:totalTime];
191         }
192         [self setStoppedStatus:NO];
193         vlc_object_release(input);
194     } else {
195         /* Nothing playing */
196         [self setStoppedStatus:YES];
197     }
198 }
199
200
201 #pragma mark -
202 #pragma mark Update functions
203
204 /* Updates the Metadata for the currently
205  * playing item or resets it if nothing is playing
206  */
207 - (void)updateMetadata
208 {
209     NSImage         *coverArtImage;
210     NSString        *title;
211     NSString        *nowPlaying;
212     NSString        *artist;
213     NSString        *album;
214     input_thread_t  *input = pl_CurrentInput(getIntf());
215     input_item_t    *item  = NULL;
216
217     // Update play/pause status
218     switch ([self getPlaylistPlayStatus]) {
219         case PLAYLIST_RUNNING:
220             [self setStoppedStatus:NO];
221             [self setProgressTimeEnabled:YES];
222             [pathActionItem setEnabled:YES];
223             _currentPlaybackUrl = [[[VLCCoreInteraction sharedInstance]
224                                     URLOfCurrentPlaylistItem] absoluteString];
225             break;
226         case PLAYLIST_STOPPED:
227             [self setStoppedStatus:YES];
228             [self setProgressTimeEnabled:NO];
229             [pathActionItem setEnabled:NO];
230             _currentPlaybackUrl = nil;
231             break;
232         case PLAYLIST_PAUSED:
233             [self setStoppedStatus:NO];
234             [self setProgressTimeEnabled:YES];
235             [pathActionItem setEnabled:YES];
236             _currentPlaybackUrl = [[[VLCCoreInteraction sharedInstance]
237                                     URLOfCurrentPlaylistItem] absoluteString];
238             [playPauseButton setState:NSOffState];
239         default:
240             break;
241     }
242
243     if (input) {
244         item = input_GetItem(input);
245     }
246
247     if (item) {
248         /* Something is playing */
249         static char *tmp_cstr = NULL;
250
251         // Get Coverart
252         tmp_cstr = input_item_GetArtworkURL(item);
253         if (tmp_cstr) {
254             NSString *tempStr = toNSStr(tmp_cstr);
255             if (![tempStr hasPrefix:@"attachment://"]) {
256                 coverArtImage = [[NSImage alloc]
257                                  initWithContentsOfURL:[NSURL URLWithString:tempStr]];
258             }
259             FREENULL(tmp_cstr);
260         }
261
262         // Get Titel
263         tmp_cstr = input_item_GetTitle(item);
264         if (tmp_cstr) {
265             title = toNSStr(tmp_cstr);
266             FREENULL(tmp_cstr);
267         }
268
269         // Get Now Playing
270         tmp_cstr = input_item_GetNowPlaying(item);
271         if (tmp_cstr) {
272             nowPlaying = toNSStr(tmp_cstr);
273             FREENULL(tmp_cstr);
274         }
275
276         // Get author
277         tmp_cstr = input_item_GetArtist(item);
278         if (tmp_cstr) {
279             artist = toNSStr(tmp_cstr);
280             FREENULL(tmp_cstr);
281         }
282
283         // Get album
284         tmp_cstr = input_item_GetAlbum(item);
285         if (tmp_cstr) {
286             album = toNSStr(tmp_cstr);
287             FREENULL(tmp_cstr);
288         }
289     } else {
290         /* Nothing playing */
291         title = _NS("VLC media player");
292         artist = _NS("Nothing playing");
293     }
294
295     // Set fallback coverart
296     if (!coverArtImage) {
297         coverArtImage = [NSImage imageNamed:@"noart.png"];
298     }
299
300     // Hack to show now playing for streams (ICY)
301     if (nowPlaying && !artist) {
302         artist = nowPlaying;
303     }
304
305     // Set the metadata in the UI
306     [self setMetadataTitle:title artist:artist album:album andCover:coverArtImage];
307
308     // Cleanup
309     if (input)
310         vlc_object_release(input);
311 }
312
313
314
315 // Update dynamic copy/open menu item status
316 - (void)updateDynamicMenuItemText
317 {
318     if (!_currentPlaybackUrl) {
319         [pathActionItem setTitle:_NS("Path/URL Action")];
320         return;
321     }
322
323     NSURL *itemURI = [NSURL URLWithString:_currentPlaybackUrl];
324
325     if ([itemURI.scheme isEqualToString:@"file"]) {
326         [pathActionItem setTitle:_NS("Select File In Finder")];
327     } else {
328         [pathActionItem setTitle:_NS("Copy URL to clipboard")];
329     }
330 }
331
332 // Update the random menu item status
333 - (void)updateMenuItemRandom
334 {
335     // Get current random status
336     bool random;
337     playlist_t *playlist = pl_Get(getIntf());
338     random = var_GetBool(playlist, "random");
339
340     [randButton setState:(random) ? NSOnState : NSOffState];
341 }
342
343
344
345 #pragma mark -
346 #pragma mark Utility functions
347
348 /* Update the UI to the specified metadata
349  * Any of the values can be nil and will be replaced with empty strings
350  * or no cover Image at all
351  */
352 - (void)setMetadataTitle:(NSString *)title
353                   artist:(NSString *)artist
354                    album:(NSString *)album
355                 andCover:(NSImage *)cover
356 {
357     [titleField setStringValue:(title) ? title : @""];
358     [artistField setStringValue:(artist) ? artist : @""];
359     [albumField setStringValue:(album) ? album : @""];
360     [coverImageView setImage:cover];
361 }
362
363 // Set the play/pause menu item status
364 - (void)setStoppedStatus:(BOOL)stopped
365 {
366     isStopped = stopped;
367     if (stopped) {
368         [playPauseButton setState:NSOffState];
369     } else {
370         [playPauseButton setState:NSOnState];
371     }
372 }
373
374 - (void)setProgressTimeEnabled:(BOOL)enabled
375 {
376     NSColor *color;
377     color = (!enabled) ? [NSColor secondaryLabelColor] : [NSColor blackColor];
378     // Set color to the labels
379     [progressField setTextColor:color];
380     [separatorField setTextColor:color];
381     [totalField setTextColor:color];
382 }
383
384
385 /* Returns VLC playlist status
386  * Check for constants:
387  *   PLAYLIST_RUNNING, PLAYLIST_STOPPED, PLAYLIST_PAUSED
388  */
389 - (int)getPlaylistPlayStatus
390 {
391     int res;
392     playlist_t *p_playlist = pl_Get(getIntf());
393
394     PL_LOCK;
395     res = playlist_Status( p_playlist );
396     PL_UNLOCK;
397
398     return res;
399 }
400
401
402 #pragma mark -
403 #pragma mark Menu item Actions
404
405 /* Action: Select the currently playing file in Finder
406  *         or in case of a network stream, copy the URL
407  */
408 - (IBAction)copyOrOpenCurrentPlaybackItem:(id)sender
409 {
410     // If nothing playing, there is nothing to do
411     if (!_currentPlaybackUrl) {
412         return;
413     }
414
415     // Check if path or URL
416     NSURL *itemURI = [NSURL URLWithString:_currentPlaybackUrl];
417
418     if ([itemURI.scheme isEqualToString:@"file"]) {
419         // Local file, open in Finder
420         [[NSWorkspace sharedWorkspace] selectFile:itemURI.path
421                          inFileViewerRootedAtPath:itemURI.path];
422     } else {
423         // URL, copy to pasteboard
424         NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
425         [pasteboard clearContents];
426         [pasteboard setString:_currentPlaybackUrl forType:NSPasteboardTypeString];
427     }
428 }
429
430 // Action: Show VLC main window
431 - (IBAction)restoreMainWindow:(id)sender
432 {
433     [[VLCApplication sharedApplication] activateIgnoringOtherApps:YES];
434     [[[VLCMain sharedInstance] mainWindow] makeKeyAndOrderFront:sender];
435 }
436
437 // Action: Toggle Play / Pause
438 - (IBAction)statusBarIconTogglePlayPause:(id)sender
439 {
440     [[VLCCoreInteraction sharedInstance] playOrPause];
441 }
442
443 // Action: Stop playback
444 - (IBAction)statusBarIconStop:(id)sender
445 {
446     [[VLCCoreInteraction sharedInstance] stop];
447 }
448
449 // Action: Go to next track
450 - (IBAction)statusBarIconNext:(id)sender
451 {
452     [[VLCCoreInteraction sharedInstance] next];
453 }
454
455 // Action: Go to previous track
456 - (IBAction)statusBarIconPrevious:(id)sender
457 {
458     [[VLCCoreInteraction sharedInstance] previous];
459 }
460
461 // Action: Toggle random playback (shuffle)
462 - (IBAction)statusBarIconToggleRandom:(id)sender
463 {
464     [[VLCCoreInteraction sharedInstance] shuffle];
465 }
466
467 // Action: Toggle between elapsed and remaining time
468 - (IBAction)toggelProgressTime:(id)sender
469 {
470     showTimeElapsed = (!showTimeElapsed);
471 }
472
473 // Action: Quit VLC
474 - (IBAction)quitAction:(id)sender
475 {
476     [[NSApplication sharedApplication] terminate:nil];
477 }
478
479 @end