macosx: Correct description of VLCStatusBarIcon class
[vlc.git] / modules / gui / macosx / VLCStatusBarIcon.m
1 /*****************************************************************************
2  * VLCStatusBarIcon.m: Status bar icon controller/delegate
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 "VLCMainMenu.h"
27 #import "VLCMain.h"
28
29 #import <vlc_common.h>
30 #import <vlc_playlist.h>
31 #import <vlc_input.h>
32
33 #import "CompatibilityFixes.h"
34 #import "VLCCoreInteraction.h"
35 #import "VLCStringUtility.h"
36
37 #import "VLCApplication.h"
38
39 @interface VLCStatusBarIcon ()
40 {
41     NSMenuItem *_vlcStatusBarMenuItem;
42
43     /* Outlets for Now Playing labels */
44     IBOutlet NSTextField *titleField;
45     IBOutlet NSTextField *artistField;
46     IBOutlet NSTextField *albumField;
47     IBOutlet NSTextField *progressField;
48     IBOutlet NSTextField *separatorField;
49     IBOutlet NSTextField *totalField;
50     IBOutlet NSImageView *coverImageView;
51
52     /* Outlets for player controls */
53     IBOutlet NSButton *backwardsButton;
54     IBOutlet NSButton *playPauseButton;
55     IBOutlet NSButton *forwardButton;
56     IBOutlet NSButton *randButton;
57
58     /* Outlets for menu items */
59     IBOutlet NSMenuItem *pathActionItem;
60     IBOutlet NSMenuItem *showMainWindowItem;
61     IBOutlet NSMenuItem *quitItem;
62
63     BOOL isStopped;
64     BOOL showTimeElapsed;
65     NSString *_currentPlaybackUrl;
66 }
67 @end
68
69 #pragma mark -
70 #pragma mark Implementation
71
72 @implementation VLCStatusBarIcon
73
74 #pragma mark -
75 #pragma mark Init
76
77
78 - (instancetype)init
79 {
80     self = [super init];
81
82     if (self) {
83         msg_Dbg(getIntf(), "Loading VLCStatusBarIcon");
84         [NSBundle loadNibNamed:@"VLCStatusBarIconMainMenu" owner:self];
85     }
86
87     return self;
88 }
89
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 track")
97                                            forAttribute:NSAccessibilityDescriptionAttribute];
98
99     [playPauseButton.cell accessibilitySetOverrideValue:_NS("Play or pause current media")
100                                            forAttribute:NSAccessibilityDescriptionAttribute];
101
102     [forwardButton.cell accessibilitySetOverrideValue:_NS("Go to next track")
103                                          forAttribute:NSAccessibilityDescriptionAttribute];
104
105     [randButton.cell accessibilitySetOverrideValue:_NS("Toggle random order playback")
106                                       forAttribute:NSAccessibilityDescriptionAttribute];
107     
108
109     // Populate menu items with localized strings
110     [showMainWindowItem setTitle:_NS("Show Main Window")];
111     [pathActionItem setTitle:_NS("Path/URL Action")];
112     [quitItem setTitle:_NS("Quit")];
113
114     showTimeElapsed = YES;
115
116     // Set our selves up as delegate, to receive menuNeedsUpdate messages, so
117     // we can update our menu as needed/before it's drawn
118     [_vlcStatusBarIconMenu setDelegate:self];
119     
120     // Register notifications
121     [[NSNotificationCenter defaultCenter] addObserver:self
122                                              selector:@selector(updateNowPlayingInfo)
123                                                  name:VLCInputChangedNotification
124                                                object:nil];
125
126     [[NSNotificationCenter defaultCenter] addObserver:self
127                                              selector:@selector(configurationChanged:)
128                                                  name:VLCConfigurationChangedNotification
129                                                object:nil];
130 }
131
132 - (void)observeValueForKeyPath:(NSString *)keyPath
133                       ofObject:(id)object
134                         change:(NSDictionary *)change
135                        context:(void *)context
136 {
137     if ([keyPath isEqualToString: NSStringFromSelector(@selector(isVisible))]) {
138         bool isVisible = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
139
140         // Sync status bar visibility with VLC setting
141         msg_Dbg(getIntf(), "Status bar icon visibility changed to %i", isVisible);
142         config_PutInt(getIntf(), "macosx-statusicon", isVisible ? 1 : 0);
143     } else {
144         [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
145     }
146 }
147
148 - (void)configurationChanged:(id)obj
149 {
150     if (var_InheritBool(getIntf(), "macosx-statusicon"))
151         [self enableMenuIcon];
152     else
153         [self disableStatusItem];
154 }
155
156 /* Enables the Status Bar Item and initializes it's image
157  * and context menu
158  */
159 - (void)enableMenuIcon
160 {
161     if (!self.statusItem) {
162         // Init the status item
163         self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
164         [self.statusItem setHighlightMode:YES];
165         [self.statusItem setEnabled:YES];
166
167         // Set the status item image
168         NSImage *menuIcon = [NSImage imageNamed:@"VLCStatusBarIcon"];
169         [menuIcon setTemplate:YES];
170         [self.statusItem setImage:menuIcon];
171
172         // Attach pull-down menu
173         [self.statusItem setMenu:_vlcStatusBarIconMenu];
174
175         // Visibility is 10.12+
176 #pragma clang diagnostic push
177 #pragma clang diagnostic ignored "-Wpartial-availability"
178         if (OSX_SIERRA_AND_HIGHER) {
179             [self.statusItem setBehavior:NSStatusItemBehaviorRemovalAllowed];
180             [self.statusItem setAutosaveName:@"statusBarItem"];
181             [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         [self.statusItem setVisible:YES];
189     }
190 }
191
192 - (void)disableStatusItem
193 {
194     if (!self.statusItem)
195         return;
196
197     // Lets keep alive the object in Sierra, and destroy it in older OS versions
198     if (OSX_SIERRA_AND_HIGHER) {
199         self.statusItem.visible = NO;
200     } else {
201         [[NSStatusBar systemStatusBar] removeStatusItem:self.statusItem];
202         self.statusItem = nil;
203     }
204 #pragma clang diagnostic pop
205 }
206
207 - (void)dealloc
208 {
209     if (self.statusItem && [self.statusItem respondsToSelector:@selector(isVisible)]) {
210         [self.statusItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(isVisible)) context:NULL];
211     }
212
213     // Cleanup
214     [[NSNotificationCenter defaultCenter] removeObserver:self];
215 }
216
217 #pragma mark -
218 #pragma mark Event callback functions
219
220 /* Menu update delegate
221  * Called before menu is opened/displayed
222  */
223 - (void)menuNeedsUpdate:(NSMenu *)menu
224 {
225     [self updateMetadata];
226     [self updateMenuItemRandom];
227     [self updateDynamicMenuItemText];
228 }
229
230 /* This is called whenever the playback status for VLC changes and here
231  * we can update our information in the menu/view
232  */
233 - (void) updateNowPlayingInfo
234 {
235     [self updateMetadata];
236     [self updateProgress];
237     [self updateDynamicMenuItemText];
238 }
239
240 /* Callback to update current playback time
241  * Called by InputManager
242  */
243 - (void)updateProgress
244 {
245     input_thread_t *input = pl_CurrentInput(getIntf());
246
247     if (input) {
248         NSString *elapsedTime;
249         NSString *remainingTime;
250         NSString *totalTime;
251
252         /* Get elapsed and remaining time */
253         elapsedTime = [[VLCStringUtility sharedInstance] getCurrentTimeAsString:input negative:NO];
254         remainingTime = [[VLCStringUtility sharedInstance] getCurrentTimeAsString:input negative:YES];
255
256         /* Check item duration */
257         mtime_t dur = input_item_GetDuration(input_GetItem(input));
258
259         if (dur == -1) {
260             /* Unknown duration, possibly due to buffering */
261             [progressField setStringValue:@"--:--"];
262             [totalField setStringValue:@"--:--"];
263         } else if (dur == 0) {
264             /* Infinite duration */
265             [progressField setStringValue:elapsedTime];
266             [totalField setStringValue:@"∞"];
267         } else {
268             /* Not unknown, update displayed duration */
269             totalTime = [[VLCStringUtility sharedInstance] stringForTime:(dur/1000000)];
270             [progressField setStringValue:(showTimeElapsed) ? elapsedTime : remainingTime];
271             [totalField setStringValue:totalTime];
272         }
273         [self setStoppedStatus:NO];
274         vlc_object_release(input);
275     } else {
276         /* Nothing playing */
277         [progressField setStringValue:@"--:--"];
278         [totalField setStringValue:@"--:--"];
279         [self setStoppedStatus:YES];
280     }
281 }
282
283
284 #pragma mark -
285 #pragma mark Update functions
286
287 /* Updates the Metadata for the currently
288  * playing item or resets it if nothing is playing
289  */
290 - (void)updateMetadata
291 {
292     NSImage         *coverArtImage;
293     NSString        *title;
294     NSString        *nowPlaying;
295     NSString        *artist;
296     NSString        *album;
297     input_thread_t  *input = pl_CurrentInput(getIntf());
298     input_item_t    *item  = NULL;
299
300     // Update play/pause status
301     switch ([self getPlaylistPlayStatus]) {
302         case PLAYLIST_RUNNING:
303             [self setStoppedStatus:NO];
304             [self setProgressTimeEnabled:YES];
305             [pathActionItem setEnabled:YES];
306             _currentPlaybackUrl = [[[VLCCoreInteraction sharedInstance]
307                                     URLOfCurrentPlaylistItem] absoluteString];
308             break;
309         case PLAYLIST_STOPPED:
310             [self setStoppedStatus:YES];
311             [self setProgressTimeEnabled:NO];
312             [pathActionItem setEnabled:NO];
313             _currentPlaybackUrl = nil;
314             break;
315         case PLAYLIST_PAUSED:
316             [self setStoppedStatus:NO];
317             [self setProgressTimeEnabled:YES];
318             [pathActionItem setEnabled:YES];
319             _currentPlaybackUrl = [[[VLCCoreInteraction sharedInstance]
320                                     URLOfCurrentPlaylistItem] absoluteString];
321             [playPauseButton setState:NSOffState];
322         default:
323             break;
324     }
325
326     if (input) {
327         item = input_GetItem(input);
328     }
329
330     if (item) {
331         /* Something is playing */
332         static char *tmp_cstr = NULL;
333
334         // Get Coverart
335         tmp_cstr = input_item_GetArtworkURL(item);
336         if (tmp_cstr) {
337             NSString *tempStr = toNSStr(tmp_cstr);
338             if (![tempStr hasPrefix:@"attachment://"]) {
339                 coverArtImage = [[NSImage alloc]
340                                  initWithContentsOfURL:[NSURL URLWithString:tempStr]];
341             }
342             FREENULL(tmp_cstr);
343         }
344
345         // Get Titel
346         tmp_cstr = input_item_GetTitleFbName(item);
347         if (tmp_cstr) {
348             title = toNSStr(tmp_cstr);
349             FREENULL(tmp_cstr);
350         }
351
352         // Get Now Playing
353         tmp_cstr = input_item_GetNowPlaying(item);
354         if (tmp_cstr) {
355             nowPlaying = toNSStr(tmp_cstr);
356             FREENULL(tmp_cstr);
357         }
358
359         // Get author
360         tmp_cstr = input_item_GetArtist(item);
361         if (tmp_cstr) {
362             artist = toNSStr(tmp_cstr);
363             FREENULL(tmp_cstr);
364         }
365
366         // Get album
367         tmp_cstr = input_item_GetAlbum(item);
368         if (tmp_cstr) {
369             album = toNSStr(tmp_cstr);
370             FREENULL(tmp_cstr);
371         }
372     } else {
373         /* Nothing playing */
374         title = _NS("VLC media player");
375         artist = _NS("Nothing playing");
376     }
377
378     // Set fallback coverart
379     if (!coverArtImage) {
380         coverArtImage = [NSImage imageNamed:@"noart.png"];
381     }
382
383     // Hack to show now playing for streams (ICY)
384     if (nowPlaying && !artist) {
385         artist = nowPlaying;
386     }
387
388     // Set the metadata in the UI
389     [self setMetadataTitle:title artist:artist album:album andCover:coverArtImage];
390
391     // Cleanup
392     if (input)
393         vlc_object_release(input);
394 }
395
396
397
398 // Update dynamic copy/open menu item status
399 - (void)updateDynamicMenuItemText
400 {
401     if (!_currentPlaybackUrl) {
402         [pathActionItem setTitle:_NS("Path/URL Action")];
403         return;
404     }
405
406     NSURL *itemURI = [NSURL URLWithString:_currentPlaybackUrl];
407
408     if ([itemURI.scheme isEqualToString:@"file"]) {
409         [pathActionItem setTitle:_NS("Select File In Finder")];
410     } else {
411         [pathActionItem setTitle:_NS("Copy URL to clipboard")];
412     }
413 }
414
415 // Update the random menu item status
416 - (void)updateMenuItemRandom
417 {
418     // Get current random status
419     bool random;
420     playlist_t *playlist = pl_Get(getIntf());
421     random = var_GetBool(playlist, "random");
422
423     [randButton setState:(random) ? NSOnState : NSOffState];
424 }
425
426
427
428 #pragma mark -
429 #pragma mark Utility functions
430
431 /* Update the UI to the specified metadata
432  * Any of the values can be nil and will be replaced with empty strings
433  * or no cover Image at all
434  */
435 - (void)setMetadataTitle:(NSString *)title
436                   artist:(NSString *)artist
437                    album:(NSString *)album
438                 andCover:(NSImage *)cover
439 {
440     [titleField setStringValue:(title) ? title : @""];
441     [artistField setStringValue:(artist) ? artist : @""];
442     [albumField setStringValue:(album) ? album : @""];
443     [coverImageView setImage:cover];
444 }
445
446 // Set the play/pause menu item status
447 - (void)setStoppedStatus:(BOOL)stopped
448 {
449     isStopped = stopped;
450     if (stopped) {
451         [playPauseButton setState:NSOffState];
452     } else {
453         [playPauseButton setState:NSOnState];
454     }
455 }
456
457 - (void)setProgressTimeEnabled:(BOOL)enabled
458 {
459     [progressField setEnabled:enabled];
460     [separatorField setEnabled:enabled];
461     [totalField setEnabled:enabled];
462 }
463
464
465 /* Returns VLC playlist status
466  * Check for constants:
467  *   PLAYLIST_RUNNING, PLAYLIST_STOPPED, PLAYLIST_PAUSED
468  */
469 - (int)getPlaylistPlayStatus
470 {
471     int res;
472     playlist_t *p_playlist = pl_Get(getIntf());
473
474     PL_LOCK;
475     res = playlist_Status( p_playlist );
476     PL_UNLOCK;
477
478     return res;
479 }
480
481
482 #pragma mark -
483 #pragma mark Menu item Actions
484
485 /* Action: Select the currently playing file in Finder
486  *         or in case of a network stream, copy the URL
487  */
488 - (IBAction)copyOrOpenCurrentPlaybackItem:(id)sender
489 {
490     // If nothing playing, there is nothing to do
491     if (!_currentPlaybackUrl) {
492         return;
493     }
494
495     // Check if path or URL
496     NSURL *itemURI = [NSURL URLWithString:_currentPlaybackUrl];
497
498     if ([itemURI.scheme isEqualToString:@"file"]) {
499         // Local file, open in Finder
500         [[NSWorkspace sharedWorkspace] selectFile:itemURI.path
501                          inFileViewerRootedAtPath:itemURI.path];
502     } else {
503         // URL, copy to pasteboard
504         NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
505         [pasteboard clearContents];
506         [pasteboard setString:_currentPlaybackUrl forType:NSPasteboardTypeString];
507     }
508 }
509
510 // Action: Show VLC main window
511 - (IBAction)restoreMainWindow:(id)sender
512 {
513     [[VLCApplication sharedApplication] activateIgnoringOtherApps:YES];
514     [[[VLCMain sharedInstance] mainWindow] makeKeyAndOrderFront:sender];
515 }
516
517 // Action: Toggle Play / Pause
518 - (IBAction)statusBarIconTogglePlayPause:(id)sender
519 {
520     [[VLCCoreInteraction sharedInstance] playOrPause];
521 }
522
523 // Action: Stop playback
524 - (IBAction)statusBarIconStop:(id)sender
525 {
526     [[VLCCoreInteraction sharedInstance] stop];
527 }
528
529 // Action: Go to next track
530 - (IBAction)statusBarIconNext:(id)sender
531 {
532     [[VLCCoreInteraction sharedInstance] next];
533 }
534
535 // Action: Go to previous track
536 - (IBAction)statusBarIconPrevious:(id)sender
537 {
538     [[VLCCoreInteraction sharedInstance] previous];
539 }
540
541 // Action: Toggle random playback (shuffle)
542 - (IBAction)statusBarIconToggleRandom:(id)sender
543 {
544     [[VLCCoreInteraction sharedInstance] shuffle];
545 }
546
547 // Action: Toggle between elapsed and remaining time
548 - (IBAction)toggelProgressTime:(id)sender
549 {
550     showTimeElapsed = (!showTimeElapsed);
551 }
552
553 // Action: Quit VLC
554 - (IBAction)quitAction:(id)sender
555 {
556     [[NSApplication sharedApplication] terminate:nil];
557 }
558
559 @end