4 // iThink Software, Copyright 2002
10 ¥ Radio mode makes things ugly
11 ¥ Add other options to the menu
14 ¥ Make preferences window pretty
16 - hot keys can't be set when NSBGOnly is on. The window is not key,
17 so the KeyBroadcaster does not pick up key combos. Bad...
18 - the hotkey classes are ugly, I didn't write them
24 #import "MenuTunesView.h"
25 #import "PreferencesController.h"
26 #import "HotKeyCenter.h"
27 #import "StatusWindowController.h"
29 @implementation MenuTunes
31 - (void)applicationDidFinishLaunching:(NSNotification *)note
33 menu = [[NSMenu alloc] initWithTitle:@""];
35 if (![[NSUserDefaults standardUserDefaults] objectForKey:@"menu"])
37 [[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObjects:@"Play/Pause", @"Next Track", @"Previous Track", @"Fast Forward", @"Rewind", @"<separator>", @"Upcoming Songs", @"Playlists", @"<separator>", @"PreferencesÉ", @"Quit", @"<separator>", @"Current Track Info", nil] forKey:@"menu"];
40 iTunesPSN = [self iTunesPSN]; //Get PSN of iTunes if it's running
41 [self rebuildMenu]; //Create the status item menu
43 statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
44 [statusItem setImage:[NSImage imageNamed:@"menu.tiff"]];
45 [statusItem setHighlightMode:YES];
46 [statusItem setMenu:menu];
49 view = [[MenuTunesView alloc] initWithFrame:[[statusItem view] frame]];
50 //[statusItem setView:view];
52 //If iTunes is running, start the timer
53 if (!((iTunesPSN.highLongOfPSN == kNoProcess) && (iTunesPSN.lowLongOfPSN == 0)))
55 refreshTimer = [NSTimer scheduledTimerWithTimeInterval:3.5
56 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
60 NSMenu *menu2 = [[[NSMenu alloc] initWithTitle:@""] autorelease];
62 //Register for the workspace note
63 [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:@selector(iTunesLaunched:) name:NSWorkspaceDidLaunchApplicationNotification object:nil];
66 [[menu2 addItemWithTitle:@"Open iTunes" action:@selector(openiTunes:) keyEquivalent:@""] setTarget:self];
67 [[menu2 addItemWithTitle:@"Preferences" action:@selector(showPreferences:) keyEquivalent:@""] setTarget:self];
68 [[menu2 addItemWithTitle:@"Quit" action:@selector(quitMenuTunes:) keyEquivalent:@""] setTarget:self];
69 [statusItem setMenu:menu2];
73 - (void)applicationWillTerminate:(NSNotification *)note
76 [[NSStatusBar systemStatusBar] removeStatusItem:statusItem];
83 [refreshTimer invalidate];
91 //Recreate the status item menu
94 NSArray *myMenu = [[NSUserDefaults standardUserDefaults] arrayForKey:@"menu"];
98 if (!((iTunesPSN.highLongOfPSN == kNoProcess) && (iTunesPSN.lowLongOfPSN == 0)))
100 didHaveAlbumName = (([[self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn album of current track\nend tell"] length] > 0) ? YES : NO);
104 didHaveAlbumName = NO;
107 while ([menu numberOfItems] > 0)
109 [menu removeItemAtIndex:0];
112 playPauseMenuItem = nil;
113 upcomingSongsItem = nil;
116 for (i = 0; i < [myMenu count]; i++)
118 NSString *item = [myMenu objectAtIndex:i];
119 if ([item isEqualToString:@"Play/Pause"])
121 playPauseMenuItem = [menu addItemWithTitle:@"Play" action:@selector(playPause:) keyEquivalent:@""];
122 [playPauseMenuItem setTarget:self];
124 else if ([item isEqualToString:@"Next Track"])
126 [[menu addItemWithTitle:@"Next Track" action:@selector(nextSong:) keyEquivalent:@""] setTarget:self];
128 else if ([item isEqualToString:@"Previous Track"])
130 [[menu addItemWithTitle:@"Previous Track" action:@selector(prevSong:) keyEquivalent:@""] setTarget:self];
132 else if ([item isEqualToString:@"Fast Forward"])
134 [[menu addItemWithTitle:@"Fast Forward" action:@selector(fastForward:) keyEquivalent:@""] setTarget:self];
136 else if ([item isEqualToString:@"Rewind"])
138 [[menu addItemWithTitle:@"Rewind" action:@selector(rewind:) keyEquivalent:@""] setTarget:self];
140 else if ([item isEqualToString:@"Upcoming Songs"])
142 upcomingSongsItem = [menu addItemWithTitle:@"Upcoming Songs" action:NULL keyEquivalent:@""];
144 else if ([item isEqualToString:@"Playlists"])
146 playlistItem = [menu addItemWithTitle:@"Playlists" action:NULL keyEquivalent:@""];
148 else if ([item isEqualToString:@"PreferencesÉ"])
150 [[menu addItemWithTitle:@"PreferencesÉ" action:@selector(showPreferences:) keyEquivalent:@""] setTarget:self];
152 else if ([item isEqualToString:@"Quit"])
154 [[menu addItemWithTitle:@"Quit" action:@selector(quitMenuTunes:) keyEquivalent:@""] setTarget:self];
156 else if ([item isEqualToString:@"Current Track Info"])
158 trackInfoIndex = [menu numberOfItems];
159 [menu addItemWithTitle:@"No Song" action:NULL keyEquivalent:@""];
161 else if ([item isEqualToString:@"<separator>"])
163 [menu addItem:[NSMenuItem separatorItem]];
166 curTrackIndex = -1; //Force update of everything
167 [self timerUpdate]; //Updates dynamic info in the menu
173 //Updates the menu with current player state, song, and upcoming songs
176 NSString *curSongName, *curAlbumName;
177 NSMenuItem *menuItem;
179 if ((iTunesPSN.highLongOfPSN == kNoProcess) && (iTunesPSN.lowLongOfPSN == 0))
184 //Get the current track name and album.
185 curSongName = [self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn name of current track\nend tell"];
186 curAlbumName = [self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn album of current track\nend tell"];
188 if (upcomingSongsItem)
190 [self rebuildUpcomingSongsMenu];
194 [self rebuildPlaylistMenu];
197 if ([curSongName length] > 0)
199 int index = [menu indexOfItemWithTitle:@"Now Playing"];
203 [menu removeItemAtIndex:index + 1];
204 if (didHaveAlbumName)
206 [menu removeItemAtIndex:index + 1];
210 if ([curAlbumName length] > 0)
212 menuItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@" %@", curAlbumName] action:NULL keyEquivalent:@""];
213 [menu insertItem:menuItem atIndex:trackInfoIndex + 1];
217 menuItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@" %@", curSongName] action:NULL keyEquivalent:@""];
218 [menu insertItem:menuItem atIndex:trackInfoIndex + 1];
223 menuItem = [[NSMenuItem alloc] initWithTitle:@"Now Playing" action:NULL keyEquivalent:@""];
224 [menu removeItemAtIndex:[menu indexOfItemWithTitle:@"No Song"]];
225 [menu insertItem:menuItem atIndex:trackInfoIndex];
229 else if ([menu indexOfItemWithTitle:@"No Song"] == -1)
231 [menu removeItemAtIndex:trackInfoIndex];
232 [menu removeItemAtIndex:trackInfoIndex];
233 if (didHaveAlbumName)
235 [menu removeItemAtIndex:trackInfoIndex];
237 menuItem = [[NSMenuItem alloc] initWithTitle:@"No Song" action:NULL keyEquivalent:@""];
238 [menu insertItem:menuItem atIndex:trackInfoIndex];
242 didHaveAlbumName = (([curAlbumName length] > 0) ? YES : NO);
245 //Rebuild the upcoming songs submenu. Can be improved a lot.
246 - (void)rebuildUpcomingSongsMenu
248 int numSongs = [[self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn number of tracks in current playlist\nend tell"] intValue];
249 int numSongsInAdvance = [[NSUserDefaults standardUserDefaults] integerForKey:@"SongsInAdvance"];
253 int curTrack = [[self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn index of current track\nend tell"] intValue];
256 [upcomingSongsMenu release];
257 upcomingSongsMenu = [[NSMenu alloc] initWithTitle:@""];
259 for (i = curTrack + 1; i <= curTrack + numSongsInAdvance; i++)
263 NSString *curSong = [self runScriptAndReturnResult:[NSString stringWithFormat:@"tell application \"iTunes\"\nreturn name of track %i of current playlist\nend tell", i]];
264 NSMenuItem *songItem;
265 songItem = [[NSMenuItem alloc] initWithTitle:curSong action:@selector(playTrack:) keyEquivalent:@""];
266 [songItem setTarget:self];
267 [songItem setRepresentedObject:[NSNumber numberWithInt:i]];
268 [upcomingSongsMenu addItem:songItem];
273 [upcomingSongsMenu addItemWithTitle:@"End of playlist." action:NULL keyEquivalent:@""];
277 [upcomingSongsItem setSubmenu:upcomingSongsMenu];
281 - (void)rebuildPlaylistMenu
283 int numPlaylists = [[self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn number of playlists\nend tell"] intValue];
286 [playlistMenu release];
287 playlistMenu = [[NSMenu alloc] initWithTitle:@""];
289 for (i = 1; i <= numPlaylists; i++)
291 NSString *playlistName = [self runScriptAndReturnResult:[NSString stringWithFormat:@"tell application \"iTunes\"\nreturn name of playlist %i\nend tell", i]];
292 NSMenuItem *tempItem;
293 tempItem = [[NSMenuItem alloc] initWithTitle:playlistName action:@selector(selectPlaylist:) keyEquivalent:@""];
294 [tempItem setTarget:self];
295 [tempItem setRepresentedObject:[NSNumber numberWithInt:i]];
296 [playlistMenu addItem:tempItem];
299 [playlistItem setSubmenu:playlistMenu];
304 [[HotKeyCenter sharedCenter] removeHotKey:@"PlayPause"];
305 [[HotKeyCenter sharedCenter] removeHotKey:@"NextTrack"];
306 [[HotKeyCenter sharedCenter] removeHotKey:@"PrevTrack"];
307 [[HotKeyCenter sharedCenter] removeHotKey:@"TrackInfo"];
308 [[HotKeyCenter sharedCenter] removeHotKey:@"UpcomingSongs"];
313 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
315 if ([defaults objectForKey:@"PlayPause"] != nil)
317 [[HotKeyCenter sharedCenter] addHotKey:@"PlayPause"
318 combo:[defaults keyComboForKey:@"PlayPause"]
319 target:self action:@selector(playPause:)];
322 if ([defaults objectForKey:@"NextTrack"] != nil)
324 [[HotKeyCenter sharedCenter] addHotKey:@"NextTrack"
325 combo:[defaults keyComboForKey:@"NextTrack"]
326 target:self action:@selector(nextSong:)];
329 if ([defaults objectForKey:@"PrevTrack"] != nil)
331 [[HotKeyCenter sharedCenter] addHotKey:@"PrevTrack"
332 combo:[defaults keyComboForKey:@"PrevTrack"]
333 target:self action:@selector(prevSong:)];
336 if ([defaults objectForKey:@"TrackInfo"] != nil)
338 [[HotKeyCenter sharedCenter] addHotKey:@"TrackInfo"
339 combo:[defaults keyComboForKey:@"TrackInfo"]
340 target:self action:@selector(showCurrentTrackInfo)];
343 if ([defaults objectForKey:@"UpcomingSongs"] != nil)
345 [[HotKeyCenter sharedCenter] addHotKey:@"UpcomingSongs"
346 combo:[defaults keyComboForKey:@"UpcomingSongs"]
347 target:self action:@selector(showUpcomingSongs)];
351 //Runs an AppleScript and returns the result as an NSString after stripping quotes, if needed.
352 - (NSString *)runScriptAndReturnResult:(NSString *)script
354 AEDesc scriptDesc, resultDesc;
359 AECreateDesc(typeChar, [script cString], [script cStringLength],
362 OSADoScript(OpenDefaultComponent(kOSAComponentType, kAppleScriptSubtype), &scriptDesc, kOSANullScript, typeChar, kOSAModeCanInteract, &resultDesc);
364 length = AEGetDescDataSize(&resultDesc);
365 buffer = malloc(length);
367 AEGetDescData(&resultDesc, buffer, length);
368 result = [NSString stringWithCString:buffer length:length];
369 if (![result isEqualToString:@""] &&
370 ([result characterAtIndex:0] == '\"') &&
371 ([result characterAtIndex:[result length] - 1] == '\"'))
373 result = [result substringWithRange:NSMakeRange(1, [result length] - 2)];
380 //Called when the timer fires.
384 if ((GetProcessPID(&iTunesPSN, &pid) == noErr) && (pid > 0))
386 int trackPlayingIndex = [[self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn index of current track\nend tell"] intValue];
387 if (trackPlayingIndex != curTrackIndex)
390 curTrackIndex = trackPlayingIndex;
394 NSString *playlist = [self runScriptAndReturnResult:@"tell application\n\"iTunes\"\nreturn name of current playlist\nend tell"];
396 if (![playlist isEqualToString:curPlaylist])
399 NSLog(@"update due to playlist change");
400 curPlaylist = [NSString stringWithString:playlist];
403 //Update Play/Pause menu item
404 if (playPauseMenuItem)
406 if ([[self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn player state\nend tell"] isEqualToString:@"playing"])
408 [playPauseMenuItem setTitle:@"Pause"];
412 [playPauseMenuItem setTitle:@"Play"];
418 NSMenu *menu2 = [[[NSMenu alloc] initWithTitle:@""] autorelease];
420 [refreshTimer invalidate]; //Stop the timer
422 [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:@selector(iTunesLaunched:) name:NSWorkspaceDidLaunchApplicationNotification object:nil];
424 [[menu2 addItemWithTitle:@"Open iTunes"
425 action:@selector(openiTunes:) keyEquivalent:@""] setTarget:self];
426 [[menu2 addItemWithTitle:@"Preferences"
427 action:@selector(showPreferences:) keyEquivalent:@""] setTarget:self];
428 [[menu2 addItemWithTitle:@"Quit" action:@selector(quitMenuTunes:)
429 keyEquivalent:@""] setTarget:self];
430 [statusItem setMenu:menu2];
434 - (void)iTunesLaunched:(NSNotification *)note
436 NSDictionary *info = [note userInfo];
438 iTunesPSN.highLongOfPSN = [[info objectForKey:@"NSApplicationProcessSerialNumberHigh"] longValue];
439 iTunesPSN.lowLongOfPSN = [[info objectForKey:@"NSApplicationProcessSerialNumberLow"] longValue];
442 refreshTimer = [NSTimer scheduledTimerWithTimeInterval:3.5 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
444 [self rebuildMenu]; //Rebuild the menu since no songs will be playing
445 [statusItem setMenu:menu]; //Set the menu back to the main one
447 [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
450 //Return the PSN of iTunes, if it's running
451 - (ProcessSerialNumber)iTunesPSN
453 ProcessSerialNumber procNum;
454 procNum.highLongOfPSN = kNoProcess;
455 procNum.lowLongOfPSN = 0;
457 while ( (GetNextProcess(&procNum) == noErr) )
459 CFStringRef procName;
461 if ( (CopyProcessName(&procNum, &procName) == noErr) )
463 if ([(NSString *)procName isEqualToString:@"iTunes"])
467 [(NSString *)procName release];
473 //Send an AppleEvent with a given event ID
474 - (void)sendAEWithEventClass:(AEEventClass)eventClass
475 andEventID:(AEEventID)eventID
477 OSType iTunesType = 'hook';
478 AppleEvent event, reply;
480 AEBuildAppleEvent(eventClass, eventID, typeApplSignature, &iTunesType, sizeof(iTunesType), kAutoGenerateReturnID, kAnyTransactionID, &event, NULL, "");
482 AESend(&event, &reply, kAENoReply, kAENormalPriority, kAEDefaultTimeout, nil, nil);
483 AEDisposeDesc(&event);
484 AEDisposeDesc(&reply);
488 // Selectors - called from status item menu
491 - (void)playTrack:(id)sender
493 [self runScriptAndReturnResult:[NSString stringWithFormat:@"tell application \"iTunes\"\nplay track %i of current playlist\nend tell", [[sender representedObject] intValue]]];
497 - (void)selectPlaylist:(id)sender
499 [self runScriptAndReturnResult:[NSString stringWithFormat:@"tell application \"iTunes\"\nplay playlist %i\nend tell", [[sender representedObject] intValue]]];
503 - (void)playPause:(id)sender
505 NSString *state = [self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn player state\nend tell"];
506 if ([state isEqualToString:@"playing"])
508 [self sendAEWithEventClass:'hook' andEventID:'Paus'];
509 [playPauseMenuItem setTitle:@"Play"];
511 else if ([state isEqualToString:@"fast forwarding"] || [state
512 isEqualToString:@"rewinding"])
514 [self sendAEWithEventClass:'hook' andEventID:'Paus'];
515 [self sendAEWithEventClass:'hook' andEventID:'Play'];
519 [self sendAEWithEventClass:'hook' andEventID:'Play'];
520 [playPauseMenuItem setTitle:@"Pause"];
524 - (void)nextSong:(id)sender
526 [self sendAEWithEventClass:'hook' andEventID:'Next'];
529 - (void)prevSong:(id)sender
531 [self sendAEWithEventClass:'hook' andEventID:'Prev'];
534 - (void)fastForward:(id)sender
536 [self sendAEWithEventClass:'hook' andEventID:'Fast'];
539 - (void)rewind:(id)sender
541 [self sendAEWithEventClass:'hook' andEventID:'Rwnd'];
544 - (void)quitMenuTunes:(id)sender
546 [NSApp terminate:self];
549 - (void)openiTunes:(id)sender
551 [[NSWorkspace sharedWorkspace] launchApplication:@"iTunes"];
554 - (void)showPreferences:(id)sender
556 if (!prefsController)
558 prefsController = [[PreferencesController alloc] initWithMenuTunes:self];
564 - (void)closePreferences
567 [prefsController release];
568 prefsController = nil;
573 // Show Current Track Info And Show Upcoming Songs Floaters
577 - (void)showCurrentTrackInfo
579 NSString *trackName = [self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn name of current track\nend tell"];
580 if (!statusController && [trackName length])
582 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
583 NSString *stringToShow = @"";
586 if ([defaults boolForKey:@"showName"])
588 stringToShow = [stringToShow stringByAppendingString:trackName];
589 stringToShow = [stringToShow stringByAppendingString:@"\n"];
593 if ([defaults boolForKey:@"showArtist"])
595 NSString *trackArtist = [self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn artist of current track\nend tell"];
596 stringToShow = [stringToShow stringByAppendingString:trackArtist];
597 stringToShow = [stringToShow stringByAppendingString:@"\n"];
601 if ([defaults boolForKey:@"showAlbum"])
603 NSString *trackAlbum = [self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn album of current track\nend tell"];
604 stringToShow = [stringToShow stringByAppendingString:trackAlbum];
605 stringToShow = [stringToShow stringByAppendingString:@"\n"];
612 if ([defaults boolForKey:@"showTime"])
614 NSString *trackLength = [self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn time of current track\nend tell"];
615 stringToShow = [stringToShow stringByAppendingString:trackLength];
616 stringToShow = [stringToShow stringByAppendingString:@"\n"];
621 int trackTimeLeft = [[self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn (duration of current track) - player position\nend tell"] intValue];
622 int minutes = trackTimeLeft / 60, seconds = trackTimeLeft % 60;
625 stringToShow = [stringToShow stringByAppendingString:
626 [NSString stringWithFormat:@"Time Remaining: %i:0%i", minutes, seconds]];
630 stringToShow = [stringToShow stringByAppendingString:
631 [NSString stringWithFormat:@"Time Remaining: %i:%i", minutes, seconds]];
635 statusController = [[StatusWindowController alloc] init];
636 [statusController setTrackInfo:stringToShow lines:lines];
637 [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(fadeAndCloseStatusWindow) userInfo:nil repeats:NO];
641 - (void)showUpcomingSongs
643 if (!statusController)
645 int numSongs = [[self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn number of tracks in current playlist\nend tell"] intValue];
649 int numSongsInAdvance = [[NSUserDefaults standardUserDefaults] integerForKey:@"SongsInAdvance"];
650 int curTrack = [[self runScriptAndReturnResult:@"tell application \"iTunes\"\nreturn index of current track\nend tell"] intValue];
652 NSString *songs = @"";
654 statusController = [[StatusWindowController alloc] init];
655 for (i = curTrack + 1; i <= curTrack + numSongsInAdvance; i++)
659 NSString *curSong = [self runScriptAndReturnResult:[NSString stringWithFormat:@"tell application \"iTunes\"\nreturn name of track %i of current playlist\nend tell", i]];
660 songs = [songs stringByAppendingString:curSong];
661 songs = [songs stringByAppendingString:@"\n"];
664 [statusController setUpcomingSongs:songs numSongs:numSongsInAdvance];
665 [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(fadeAndCloseStatusWindow) userInfo:nil repeats:NO];
670 - (void)fadeAndCloseStatusWindow
672 [statusController fadeWindowOut];
673 [statusController release];
674 statusController = nil;