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