Update iTunesRemote to match the new API. MainController and
[MenuTunes.git] / MainController.m
1 #import "MainController.h"
2 #import "MenuController.h"
3 #import "PreferencesController.h"
4 #import "HotKeyCenter.h"
5 #import "StatusWindowController.h"
6 #import "StatusItemHack.h"
7
8 @interface MainController(Private)
9 - (ITMTRemote *)loadRemote;
10 - (void)timerUpdate;
11 - (void)setLatestSongIdentifier:(NSString *)newIdentifier;
12 - (void)showCurrentTrackInfo;
13 - (void)applicationLaunched:(NSNotification *)note;
14 - (void)applicationTerminated:(NSNotification *)note;
15 @end
16
17 static MainController *sharedController;
18
19 @implementation MainController
20
21 + (MainController *)sharedController
22 {
23     return sharedController;
24 }
25
26 /*************************************************************************/
27 #pragma mark -
28 #pragma mark INITIALIZATION/DEALLOCATION METHODS
29 /*************************************************************************/
30
31 - (id)init
32 {
33     if ( ( self = [super init] ) ) {
34         sharedController = self;
35         
36         remoteArray = [[NSMutableArray alloc] initWithCapacity:1];
37         statusWindowController = [[StatusWindowController alloc] init];
38         menuController = [[MenuController alloc] init];
39         df = [[NSUserDefaults standardUserDefaults] retain];
40     }
41     return self;
42 }
43
44 - (void)applicationDidFinishLaunching:(NSNotification *)note
45 {
46     currentRemote = [self loadRemote];
47     [currentRemote begin];
48     
49     //Setup for notification of the remote player launching or quitting
50     [[[NSWorkspace sharedWorkspace] notificationCenter]
51             addObserver:self
52             selector:@selector(applicationTerminated:)
53             name:NSWorkspaceDidTerminateApplicationNotification
54             object:nil];
55     
56     [[[NSWorkspace sharedWorkspace] notificationCenter]
57             addObserver:self
58             selector:@selector(applicationLaunched:)
59             name:NSWorkspaceDidLaunchApplicationNotification
60             object:nil];
61     
62     if ( ! [df objectForKey:@"menu"] ) {  // If this is nil, defaults have never been registered.
63         [[PreferencesController sharedPrefs] registerDefaults];
64     }
65     
66     [StatusItemHack install];
67     statusItem = [[ITStatusItem alloc]
68             initWithStatusBar:[NSStatusBar systemStatusBar]
69             withLength:NSSquareStatusItemLength];
70     
71     if ([currentRemote playerRunningState] == ITMTRemotePlayerRunning) {
72         [self applicationLaunched:nil];
73     } else {
74         if ([df boolForKey:@"LaunchPlayerWithMT"])
75         {
76             [self showPlayer];
77         }
78         else
79         {
80             [self applicationTerminated:nil];
81         }
82     }
83     
84     [statusItem setImage:[NSImage imageNamed:@"menu"]];
85     [statusItem setAlternateImage:[NSImage imageNamed:@"selected_image"]];
86 }
87
88 - (ITMTRemote *)loadRemote
89 {
90     NSString *folderPath = [[NSBundle mainBundle] builtInPlugInsPath];
91     
92     if (folderPath) {
93         NSArray      *bundlePathList = [NSBundle pathsForResourcesOfType:@"remote" inDirectory:folderPath];
94         NSEnumerator *enumerator     = [bundlePathList objectEnumerator];
95         NSString     *bundlePath;
96
97         while ( (bundlePath = [enumerator nextObject]) ) {
98             NSBundle* remoteBundle = [NSBundle bundleWithPath:bundlePath];
99
100             if (remoteBundle) {
101                 Class remoteClass = [remoteBundle principalClass];
102
103                 if ([remoteClass conformsToProtocol:@protocol(ITMTRemote)] &&
104                     [remoteClass isKindOfClass:[NSObject class]]) {
105
106                     id remote = [remoteClass remote];
107                     [remoteArray addObject:remote];
108                 }
109             }
110         }
111
112 //      if ( [remoteArray count] > 0 ) {  // UNCOMMENT WHEN WE HAVE > 1 PLUGIN
113 //          if ( [remoteArray count] > 1 ) {
114 //              [remoteArray sortUsingSelector:@selector(sortAlpha:)];
115 //          }
116 //          [self loadModuleAccessUI]; //Comment out this line to disable remote visibility
117 //      }
118     }
119 //  NSLog(@"%@", [remoteArray objectAtIndex:0]);  //DEBUG
120     return [remoteArray objectAtIndex:0];
121 }
122
123 /*************************************************************************/
124 #pragma mark -
125 #pragma mark INSTANCE METHODS
126 /*************************************************************************/
127
128 - (void)startTimerInNewThread
129 {
130     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
131     NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
132     refreshTimer = [[NSTimer scheduledTimerWithTimeInterval:0.5
133                              target:self
134                              selector:@selector(timerUpdate)
135                              userInfo:nil
136                              repeats:YES] retain];
137     [runLoop run];
138     [pool release];
139 }
140
141 - (BOOL)songIsPlaying
142 {
143     return ( ! ([[currentRemote currentSongUniqueIdentifier] isEqualToString:@"0-0"]) );
144 }
145
146 - (BOOL)radioIsPlaying
147 {
148     return ( [currentRemote currentPlaylistClass] == ITMTRemotePlayerRadioPlaylist );
149 }
150
151 - (BOOL)songChanged
152 {
153     return ( ! [[currentRemote currentSongUniqueIdentifier] isEqualToString:_latestSongIdentifier] );
154 }
155
156 - (NSString *)latestSongIdentifier
157 {
158     return _latestSongIdentifier;
159 }
160
161 - (void)setLatestSongIdentifier:(NSString *)newIdentifier
162 {
163     [_latestSongIdentifier autorelease];
164     _latestSongIdentifier = [newIdentifier copy];
165 }
166
167 - (void)timerUpdate
168 {
169     if ([self songChanged]) {
170         [self setLatestSongIdentifier:[currentRemote currentSongUniqueIdentifier]];
171         [menuController rebuildSubmenus];
172         
173         if ( [df boolForKey:@"showSongInfoOnChange"] ) {
174             [self showCurrentTrackInfo];
175         }
176     }
177 }
178
179 - (void)menuClicked
180 {
181     if ([currentRemote playerRunningState] == ITMTRemotePlayerRunning) {
182         [statusItem setMenu:[menuController menu]];
183     } else {
184         [statusItem setMenu:[menuController menuForNoPlayer]];
185     }
186 }
187
188 //
189 //
190 // Menu Selectors
191 //
192 //
193
194 - (void)playPause
195 {
196     ITMTRemotePlayerPlayingState state = [currentRemote playerPlayingState];
197     
198     if (state == ITMTRemotePlayerPlaying) {
199         [currentRemote pause];
200     } else if ((state == ITMTRemotePlayerForwarding) || (state == ITMTRemotePlayerRewinding)) {
201         [currentRemote pause];
202         [currentRemote play];
203     } else {
204         [currentRemote play];
205     }
206 }
207
208 - (void)nextSong
209 {
210     [currentRemote goToNextSong];
211 }
212
213 - (void)prevSong
214 {
215     [currentRemote goToPreviousSong];
216 }
217
218 - (void)fastForward
219 {
220     [currentRemote forward];
221 }
222
223 - (void)rewind
224 {
225     [currentRemote rewind];
226 }
227
228 - (void)selectPlaylistAtIndex:(int)index
229 {
230     [currentRemote switchToPlaylistAtIndex:index];
231 }
232
233 - (void)selectSongAtIndex:(int)index
234 {
235     [currentRemote switchToSongAtIndex:index];
236 }
237
238 - (void)selectSongRating:(int)rating
239 {
240     [currentRemote setCurrentSongRating:(float)rating / 100.0];
241 }
242
243 - (void)selectEQPresetAtIndex:(int)index
244 {
245     [currentRemote switchToEQAtIndex:index];
246 }
247
248 - (void)showPlayer
249 {
250     if ( ( playerRunningState == ITMTRemotePlayerRunning) ) {
251         [currentRemote showPrimaryInterface];
252     } else {
253         if (![[NSWorkspace sharedWorkspace] launchApplication:[currentRemote playerFullName]]) {
254             NSLog(@"MenuTunes: Error Launching Player");
255         }
256     }
257 }
258
259 - (void)showPreferences
260 {
261     [[PreferencesController sharedPrefs] setController:self];
262     [[PreferencesController sharedPrefs] showPrefsWindow:self];
263 }
264
265 - (void)quitMenuTunes
266 {
267     [NSApp terminate:self];
268 }
269
270 //
271 //
272
273 - (void)closePreferences
274 {
275     if ( ( playerRunningState == ITMTRemotePlayerRunning) ) {
276         [self setupHotKeys];
277     }
278 }
279
280 - (ITMTRemote *)currentRemote
281 {
282     return currentRemote;
283 }
284
285 //
286 //
287 // Hot key setup
288 //
289 //
290
291 - (void)clearHotKeys
292 {
293     [[HotKeyCenter sharedCenter] removeHotKey:@"PlayPause"];
294     [[HotKeyCenter sharedCenter] removeHotKey:@"NextTrack"];
295     [[HotKeyCenter sharedCenter] removeHotKey:@"PrevTrack"];
296     [[HotKeyCenter sharedCenter] removeHotKey:@"TrackInfo"];
297     [[HotKeyCenter sharedCenter] removeHotKey:@"ShowPlayer"];
298     [[HotKeyCenter sharedCenter] removeHotKey:@"UpcomingSongs"];
299     [[HotKeyCenter sharedCenter] removeHotKey:@"ToggleLoop"];
300     [[HotKeyCenter sharedCenter] removeHotKey:@"ToggleShuffle"];
301     [[HotKeyCenter sharedCenter] removeHotKey:@"IncrementVolume"];
302     [[HotKeyCenter sharedCenter] removeHotKey:@"DecrementVolume"];
303     [[HotKeyCenter sharedCenter] removeHotKey:@"IncrementRating"];
304     [[HotKeyCenter sharedCenter] removeHotKey:@"DecrementRating"];
305 }
306
307 - (void)setupHotKeys
308 {
309     if ([df objectForKey:@"PlayPause"] != nil) {
310         [[HotKeyCenter sharedCenter] addHotKey:@"PlayPause"
311                 combo:[df keyComboForKey:@"PlayPause"]
312                 target:self action:@selector(playPause)];
313     }
314     
315     if ([df objectForKey:@"NextTrack"] != nil) {
316         [[HotKeyCenter sharedCenter] addHotKey:@"NextTrack"
317                 combo:[df keyComboForKey:@"NextTrack"]
318                 target:self action:@selector(nextSong)];
319     }
320     
321     if ([df objectForKey:@"PrevTrack"] != nil) {
322         [[HotKeyCenter sharedCenter] addHotKey:@"PrevTrack"
323                 combo:[df keyComboForKey:@"PrevTrack"]
324                 target:self action:@selector(prevSong)];
325     }
326     
327     if ([df objectForKey:@"ShowPlayer"] != nil) {
328         [[HotKeyCenter sharedCenter] addHotKey:@"ShowPlayer"
329                 combo:[df keyComboForKey:@"ShowPlayer"]
330                 target:self action:@selector(showPlayer)];
331     }
332     
333     if ([df objectForKey:@"TrackInfo"] != nil) {
334         [[HotKeyCenter sharedCenter] addHotKey:@"TrackInfo"
335                 combo:[df keyComboForKey:@"TrackInfo"]
336                 target:self action:@selector(showCurrentTrackInfo)];
337     }
338     
339     if ([df objectForKey:@"UpcomingSongs"] != nil) {
340         [[HotKeyCenter sharedCenter] addHotKey:@"UpcomingSongs"
341                combo:[df keyComboForKey:@"UpcomingSongs"]
342                target:self action:@selector(showUpcomingSongs)];
343     }
344     
345     if ([df objectForKey:@"ToggleLoop"] != nil) {
346         [[HotKeyCenter sharedCenter] addHotKey:@"ToggleLoop"
347                combo:[df keyComboForKey:@"ToggleLoop"]
348                target:self action:@selector(toggleLoop)];
349     }
350     
351     if ([df objectForKey:@"ToggleShuffle"] != nil) {
352         [[HotKeyCenter sharedCenter] addHotKey:@"ToggleShuffle"
353                combo:[df keyComboForKey:@"ToggleShuffle"]
354                target:self action:@selector(toggleShuffle)];
355     }
356     
357     if ([df objectForKey:@"IncrementVolume"] != nil) {
358         [[HotKeyCenter sharedCenter] addHotKey:@"IncrementVolume"
359                combo:[df keyComboForKey:@"IncrementVolume"]
360                target:self action:@selector(incrementVolume)];
361     }
362     
363     if ([df objectForKey:@"DecrementVolume"] != nil) {
364         [[HotKeyCenter sharedCenter] addHotKey:@"DecrementVolume"
365                combo:[df keyComboForKey:@"DecrementVolume"]
366                target:self action:@selector(decrementVolume)];
367     }
368     
369     if ([df objectForKey:@"IncrementRating"] != nil) {
370         [[HotKeyCenter sharedCenter] addHotKey:@"IncrementRating"
371                combo:[df keyComboForKey:@"IncrementRating"]
372                target:self action:@selector(incrementRating)];
373     }
374     
375     if ([df objectForKey:@"DecrementRating"] != nil) {
376         [[HotKeyCenter sharedCenter] addHotKey:@"DecrementRating"
377                combo:[df keyComboForKey:@"DecrementRating"]
378                target:self action:@selector(decrementRating)];
379     }
380 }
381
382 - (void)showCurrentTrackInfo
383 {
384     NSString *title = [currentRemote currentSongTitle];
385
386     if ( title ) {
387         NSString *album       = nil;
388         NSString *artist      = nil;
389         NSString *time        = nil;
390         int       trackNumber = 0;
391         int       trackTotal  = 0;
392         int       rating      = 0;
393
394         if ( [df boolForKey:@"showAlbum"] ) {
395             album = [currentRemote currentSongAlbum];
396         }
397
398         if ( [df boolForKey:@"showArtist"] ) {
399             artist = [currentRemote currentSongArtist];
400         }
401
402         if ( [df boolForKey:@"showTime"] ) {
403             time = [currentRemote currentSongLength];
404         }
405
406         if ( [df boolForKey:@"showNumber"] ) {
407             trackNumber = [currentRemote currentSongTrack];
408             trackTotal  = [currentRemote currentAlbumTrackCount];
409         }
410
411         if ( [df boolForKey:@"showRating"] ) {
412             rating = ( [currentRemote currentSongRating] * 5 );
413         }
414
415         [statusWindowController showSongWindowWithTitle:title
416                                                   album:album
417                                                  artist:artist
418                                                    time:time
419                                             trackNumber:trackNumber
420                                              trackTotal:trackTotal
421                                                  rating:rating];
422     } else {
423         title = NSLocalizedString(@"noSongPlaying", @"No song is playing.");
424         [statusWindowController showSongWindowWithTitle:title
425                                                   album:nil
426                                                  artist:nil
427                                                    time:nil
428                                             trackNumber:0
429                                              trackTotal:0
430                                                  rating:0];
431     }
432 }
433
434 - (void)showUpcomingSongs
435 {
436     int curPlaylist = [currentRemote currentPlaylistIndex];
437     int numSongs = [currentRemote numberOfSongsInPlaylistAtIndex:curPlaylist];
438
439     if (numSongs > 0) {
440         NSMutableArray *songList = [NSMutableArray arrayWithCapacity:5];
441         int numSongsInAdvance = [df integerForKey:@"SongsInAdvance"];
442         int curTrack = [currentRemote currentSongIndex];
443         int i;
444
445         for (i = curTrack + 1; i <= curTrack + numSongsInAdvance; i++) {
446             if (i <= numSongs) {
447                 [songList addObject:[currentRemote songTitleAtIndex:i]];
448             }
449         }
450         
451         [statusWindowController showUpcomingSongsWithTitles:songList];
452         
453     } else {
454         [statusWindowController showUpcomingSongsWithTitles:[NSArray arrayWithObject:NSLocalizedString(@"noUpcomingSongs", @"No upcoming songs.")]];
455     }
456 }
457
458 - (void)incrementVolume
459 {
460     float volume = [currentRemote volume];
461     volume += 0.2;
462     if (volume > 1.0) {
463         volume = 1.0;
464     }
465     [currentRemote setVolume:volume];
466     
467     //Show volume status window
468     [statusWindowController showVolumeWindowWithLevel:volume];
469 }
470
471 - (void)decrementVolume
472 {
473     float volume = [currentRemote volume];
474     volume -= 0.2;
475     if (volume < 0.0) {
476         volume = 0.0;
477     }
478     [currentRemote setVolume:volume];
479     
480     //Show volume status window
481     [statusWindowController showVolumeWindowWithLevel:volume];
482 }
483
484 - (void)incrementRating
485 {
486     float rating = [currentRemote currentSongRating];
487     rating += 0.2;
488     if (rating > 1.0) {
489         rating = 1.0;
490     }
491     [currentRemote setCurrentSongRating:rating];
492     
493     //Show rating status window
494     [statusWindowController showRatingWindowWithLevel:rating];
495 }
496
497 - (void)decrementRating
498 {
499     float rating = [currentRemote currentSongRating];
500     rating -= 0.2;
501     if (rating < 0.0) {
502         rating = 0.0;
503     }
504     [currentRemote setCurrentSongRating:rating];
505     
506     //Show rating status window
507     [statusWindowController showRatingWindowWithLevel:rating];
508 }
509
510 - (void)toggleLoop
511 {
512     ITMTRemotePlayerRepeatMode repeatMode = [currentRemote repeatMode];
513     
514     switch (repeatMode) {
515         case ITMTRemotePlayerRepeatOff:
516             repeatMode = ITMTRemotePlayerRepeatAll;
517         break;
518         case ITMTRemotePlayerRepeatAll:
519             repeatMode = ITMTRemotePlayerRepeatOne;
520         break;
521         case ITMTRemotePlayerRepeatOne:
522             repeatMode = ITMTRemotePlayerRepeatOff;
523         break;
524     }
525     [currentRemote setRepeatMode:repeatMode];
526     
527     //Show loop status window
528     [statusWindowController showLoopWindowWithMode:repeatMode];
529 }
530
531 - (void)toggleShuffle
532 {
533     bool newShuffleEnabled = ![currentRemote shuffleEnabled];
534     [currentRemote setShuffleEnabled:newShuffleEnabled];
535     //Show shuffle status window
536     [statusWindowController showLoopWindowWithMode:newShuffleEnabled];
537 }
538
539 /*************************************************************************/
540 #pragma mark -
541 #pragma mark WORKSPACE NOTIFICATION HANDLERS
542 /*************************************************************************/
543
544 - (void)applicationLaunched:(NSNotification *)note
545 {
546     if (!note || [[[note userInfo] objectForKey:@"NSApplicationName"] isEqualToString:[currentRemote playerFullName]]) {
547         [currentRemote begin];
548         [self setLatestSongIdentifier:@""];
549         [self timerUpdate];
550         refreshTimer = [[NSTimer scheduledTimerWithTimeInterval:0.5
551                              target:self
552                              selector:@selector(timerUpdate)
553                              userInfo:nil
554                              repeats:YES] retain];
555         //[NSThread detachNewThreadSelector:@selector(startTimerInNewThread) toTarget:self withObject:nil];
556         [self setupHotKeys];
557         playerRunningState = ITMTRemotePlayerRunning;
558     }
559 }
560
561  - (void)applicationTerminated:(NSNotification *)note
562  {
563      if (!note || [[[note userInfo] objectForKey:@"NSApplicationName"] isEqualToString:[currentRemote playerFullName]]) {
564         [currentRemote halt];
565         [refreshTimer invalidate];
566         [refreshTimer release];
567         refreshTimer = nil;
568         [self clearHotKeys];
569         playerRunningState = ITMTRemotePlayerNotRunning;
570      }
571  }
572
573
574 /*************************************************************************/
575 #pragma mark -
576 #pragma mark NSApplication DELEGATE METHODS
577 /*************************************************************************/
578
579 - (void)applicationWillTerminate:(NSNotification *)note
580 {
581     [self clearHotKeys];
582     [[NSStatusBar systemStatusBar] removeStatusItem:statusItem];
583 }
584
585
586 /*************************************************************************/
587 #pragma mark -
588 #pragma mark DEALLOCATION METHOD
589 /*************************************************************************/
590
591 - (void)dealloc
592 {
593     [self applicationTerminated:nil];
594     [statusItem release];
595     [statusWindowController release];
596     [menuController release];
597     [super dealloc];
598 }
599
600
601 @end