STATUS WINDOWS NOW EXIST IN MENUTUNES. Some work left to do on them, then positioni...
[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
216 - (void)nextSong
217 {
218     [currentRemote goToNextSong];
219 }
220
221 - (void)prevSong
222 {
223     [currentRemote goToPreviousSong];
224 }
225
226 - (void)fastForward
227 {
228     [currentRemote forward];
229 }
230
231 - (void)rewind
232 {
233     [currentRemote rewind];
234 }
235
236 - (void)selectPlaylistAtIndex:(int)index
237 {
238     [currentRemote switchToPlaylistAtIndex:index];
239 }
240
241 - (void)selectSongAtIndex:(int)index
242 {
243     [currentRemote switchToSongAtIndex:index];
244 }
245
246 - (void)selectSongRating:(int)rating
247 {
248     [currentRemote setCurrentSongRating:(float)rating / 100.0];
249 }
250
251 - (void)selectEQPresetAtIndex:(int)index
252 {
253     [currentRemote switchToEQAtIndex:index];
254 }
255
256 - (void)showPlayer
257 {
258     if ( ( playerRunningState == ITMTRemotePlayerRunning) ) {
259         [currentRemote showPrimaryInterface];
260     } else {
261         if (![[NSWorkspace sharedWorkspace] launchApplication:[currentRemote playerFullName]]) {
262             NSLog(@"MenuTunes: Error Launching Player");
263         }
264     }
265 }
266
267 - (void)showPreferences
268 {
269     [[PreferencesController sharedPrefs] setController:self];
270     [[PreferencesController sharedPrefs] showPrefsWindow:self];
271 }
272
273 - (void)quitMenuTunes
274 {
275     [NSApp terminate:self];
276 }
277
278 //
279 //
280
281 - (void)closePreferences
282 {
283     if ( ( playerRunningState == ITMTRemotePlayerRunning) ) {
284         [self setupHotKeys];
285     }
286 }
287
288 - (ITMTRemote *)currentRemote
289 {
290     return currentRemote;
291 }
292
293 //
294 //
295 // Hot key setup
296 //
297 //
298
299 - (void)clearHotKeys
300 {
301     NSEnumerator *hotKeyEnumerator = [[[ITHotKeyCenter sharedCenter] allHotKeys] objectEnumerator];
302     ITHotKey *nextHotKey;
303     
304     while ( (nextHotKey = [hotKeyEnumerator nextObject]) ) {
305         [[ITHotKeyCenter sharedCenter] unregisterHotKey:nextHotKey];
306     }
307 }
308
309 - (void)setupHotKeys
310 {
311     ITHotKey *hotKey;
312     
313     if ([df objectForKey:@"PlayPause"] != nil) {
314         hotKey = [[ITHotKey alloc] init];
315         [hotKey setName:@"PlayPause"];
316         [hotKey setKeyCombo:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:@"PlayPause"]]];
317         [hotKey setTarget:self];
318         [hotKey setAction:@selector(playPause)];
319         [[ITHotKeyCenter sharedCenter] registerHotKey:[hotKey autorelease]];
320     }
321     
322     if ([df objectForKey:@"NextTrack"] != nil) {
323         hotKey = [[ITHotKey alloc] init];
324         [hotKey setName:@"NextTrack"];
325         [hotKey setKeyCombo:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:@"NextTrack"]]];
326         [hotKey setTarget:self];
327         [hotKey setAction:@selector(nextSong)];
328         [[ITHotKeyCenter sharedCenter] registerHotKey:[hotKey autorelease]];
329     }
330     
331     if ([df objectForKey:@"PrevTrack"] != nil) {
332         hotKey = [[ITHotKey alloc] init];
333         [hotKey setName:@"PrevTrack"];
334         [hotKey setKeyCombo:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:@"PrevTrack"]]];
335         [hotKey setTarget:self];
336         [hotKey setAction:@selector(prevSong)];
337         [[ITHotKeyCenter sharedCenter] registerHotKey:[hotKey autorelease]];
338     }
339     
340     if ([df objectForKey:@"ShowPlayer"] != nil) {
341         hotKey = [[ITHotKey alloc] init];
342         [hotKey setName:@"ShowPlayer"];
343         [hotKey setKeyCombo:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:@"ShowPlayer"]]];
344         [hotKey setTarget:self];
345         [hotKey setAction:@selector(showPlayer)];
346         [[ITHotKeyCenter sharedCenter] registerHotKey:[hotKey autorelease]];
347     }
348     
349     if ([df objectForKey:@"TrackInfo"] != nil) {
350         hotKey = [[ITHotKey alloc] init];
351         [hotKey setName:@"TrackInfo"];
352         [hotKey setKeyCombo:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:@"TrackInfo"]]];
353         [hotKey setTarget:self];
354         [hotKey setAction:@selector(showCurrentTrackInfo)];
355         [[ITHotKeyCenter sharedCenter] registerHotKey:[hotKey autorelease]];
356     }
357     
358     if ([df objectForKey:@"UpcomingSongs"] != nil) {
359         hotKey = [[ITHotKey alloc] init];
360         [hotKey setName:@"UpcomingSongs"];
361         [hotKey setKeyCombo:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:@"UpcomingSongs"]]];
362         [hotKey setTarget:self];
363         [hotKey setAction:@selector(showUpcomingSongs)];
364         [[ITHotKeyCenter sharedCenter] registerHotKey:[hotKey autorelease]];
365     }
366     
367     if ([df objectForKey:@"ToggleLoop"] != nil) {
368         hotKey = [[ITHotKey alloc] init];
369         [hotKey setName:@"ToggleLoop"];
370         [hotKey setKeyCombo:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:@"ToggleLoop"]]];
371         [hotKey setTarget:self];
372         [hotKey setAction:@selector(toggleLoop)];
373         [[ITHotKeyCenter sharedCenter] registerHotKey:[hotKey autorelease]];
374     }
375     
376     if ([df objectForKey:@"ToggleShuffle"] != nil) {
377         hotKey = [[ITHotKey alloc] init];
378         [hotKey setName:@"ToggleShuffle"];
379         [hotKey setKeyCombo:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:@"ToggleShuffle"]]];
380         [hotKey setTarget:self];
381         [hotKey setAction:@selector(toggleShuffle)];
382         [[ITHotKeyCenter sharedCenter] registerHotKey:[hotKey autorelease]];
383     }
384     
385     if ([df objectForKey:@"IncrementVolume"] != nil) {
386         hotKey = [[ITHotKey alloc] init];
387         [hotKey setName:@"IncrementVolume"];
388         [hotKey setKeyCombo:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:@"IncrementVolume"]]];
389         [hotKey setTarget:self];
390         [hotKey setAction:@selector(incrementVolume)];
391         [[ITHotKeyCenter sharedCenter] registerHotKey:[hotKey autorelease]];
392     }
393     
394     if ([df objectForKey:@"DecrementVolume"] != nil) {
395         hotKey = [[ITHotKey alloc] init];
396         [hotKey setName:@"DecrementVolume"];
397         [hotKey setKeyCombo:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:@"DecrementVolume"]]];
398         [hotKey setTarget:self];
399         [hotKey setAction:@selector(decrementVolume)];
400         [[ITHotKeyCenter sharedCenter] registerHotKey:[hotKey autorelease]];
401     }
402     
403     if ([df objectForKey:@"IncrementRating"] != nil) {
404         hotKey = [[ITHotKey alloc] init];
405         [hotKey setName:@"IncrementRating"];
406         [hotKey setKeyCombo:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:@"IncrementRating"]]];
407         [hotKey setTarget:self];
408         [hotKey setAction:@selector(incrementRating)];
409         [[ITHotKeyCenter sharedCenter] registerHotKey:[hotKey autorelease]];
410     }
411     
412     if ([df objectForKey:@"DecrementRating"] != nil) {
413         hotKey = [[ITHotKey alloc] init];
414         [hotKey setName:@"DecrementRating"];
415         [hotKey setKeyCombo:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:@"DecrementRating"]]];
416         [hotKey setTarget:self];
417         [hotKey setAction:@selector(decrementRating)];
418         [[ITHotKeyCenter sharedCenter] registerHotKey:[hotKey autorelease]];
419     }
420 }
421
422 - (void)showCurrentTrackInfo
423 {
424     NSString *title = [currentRemote currentSongTitle];
425
426     if ( title ) {
427         NSString              *album       = nil;
428         NSString              *artist      = nil;
429         NSString              *time        = nil;
430         int                    trackNumber = 0;
431         int                    trackTotal  = 0;
432         int                    rating      = 0;
433         ITMTRemotePlayerSource source      = [currentRemote currentSource];
434
435         if ( [df boolForKey:@"showAlbum"] ) {
436             album = [currentRemote currentSongAlbum];
437         }
438
439         if ( [df boolForKey:@"showArtist"] ) {
440             artist = [currentRemote currentSongArtist];
441         }
442
443         if ( [df boolForKey:@"showTime"] ) {
444             time = [currentRemote currentSongLength];
445         }
446
447         if ( [df boolForKey:@"showNumber"] ) {
448             trackNumber = [currentRemote currentSongTrack];
449             trackTotal  = [currentRemote currentAlbumTrackCount];
450         }
451
452         if ( [df boolForKey:@"showRating"] ) {
453             rating = ( [currentRemote currentSongRating] * 5 );
454         }
455
456         [statusWindowController showSongWindowWithTitle:title
457                                                   album:album
458                                                  artist:artist
459                                                    time:time
460                                             trackNumber:trackNumber
461                                              trackTotal:trackTotal
462                                                  rating:rating
463                                                  source:source];
464     } else {
465         title = NSLocalizedString(@"noSongPlaying", @"No song is playing.");
466         [statusWindowController showSongWindowWithTitle:title
467                                                   album:nil
468                                                  artist:nil
469                                                    time:nil
470                                             trackNumber:0
471                                              trackTotal:0
472                                                  rating:0
473                                                  source:[currentRemote currentSource]];
474     }
475 }
476
477 - (void)showUpcomingSongs
478 {
479     int curPlaylist = [currentRemote currentPlaylistIndex];
480     int numSongs = [currentRemote numberOfSongsInPlaylistAtIndex:curPlaylist];
481
482     if (numSongs > 0) {
483         NSMutableArray *songList = [NSMutableArray arrayWithCapacity:5];
484         int numSongsInAdvance = [df integerForKey:@"SongsInAdvance"];
485         int curTrack = [currentRemote currentSongIndex];
486         int i;
487
488         for (i = curTrack + 1; i <= curTrack + numSongsInAdvance; i++) {
489             if (i <= numSongs) {
490                 [songList addObject:[currentRemote songTitleAtIndex:i]];
491             }
492         }
493         
494         [statusWindowController showUpcomingSongsWithTitles:songList];
495         
496     } else {
497         [statusWindowController showUpcomingSongsWithTitles:[NSArray arrayWithObject:NSLocalizedString(@"noUpcomingSongs", @"No upcoming songs.")]];
498     }
499 }
500
501 - (void)incrementVolume
502 {
503     float volume = [currentRemote volume];
504     volume += 0.2;
505     if (volume > 1.0) {
506         volume = 1.0;
507     }
508     [currentRemote setVolume:volume];
509     
510     //Show volume status window
511     [statusWindowController showVolumeWindowWithLevel:volume];
512 }
513
514 - (void)decrementVolume
515 {
516     float volume = [currentRemote volume];
517     volume -= 0.2;
518     if (volume < 0.0) {
519         volume = 0.0;
520     }
521     [currentRemote setVolume:volume];
522     
523     //Show volume status window
524     [statusWindowController showVolumeWindowWithLevel:volume];
525 }
526
527 - (void)incrementRating
528 {
529     float rating = [currentRemote currentSongRating];
530     rating += 0.2;
531     if (rating > 1.0) {
532         rating = 1.0;
533     }
534     [currentRemote setCurrentSongRating:rating];
535     
536     //Show rating status window
537     [statusWindowController showRatingWindowWithLevel:rating];
538 }
539
540 - (void)decrementRating
541 {
542     float rating = [currentRemote currentSongRating];
543     rating -= 0.2;
544     if (rating < 0.0) {
545         rating = 0.0;
546     }
547     [currentRemote setCurrentSongRating:rating];
548     
549     //Show rating status window
550     [statusWindowController showRatingWindowWithLevel:rating];
551 }
552
553 - (void)toggleLoop
554 {
555     ITMTRemotePlayerRepeatMode repeatMode = [currentRemote repeatMode];
556     
557     switch (repeatMode) {
558         case ITMTRemotePlayerRepeatOff:
559             repeatMode = ITMTRemotePlayerRepeatAll;
560         break;
561         case ITMTRemotePlayerRepeatAll:
562             repeatMode = ITMTRemotePlayerRepeatOne;
563         break;
564         case ITMTRemotePlayerRepeatOne:
565             repeatMode = ITMTRemotePlayerRepeatOff;
566         break;
567     }
568     [currentRemote setRepeatMode:repeatMode];
569     
570     //Show loop status window
571     [statusWindowController showLoopWindowWithMode:repeatMode];
572 }
573
574 - (void)toggleShuffle
575 {
576     bool newShuffleEnabled = ![currentRemote shuffleEnabled];
577     [currentRemote setShuffleEnabled:newShuffleEnabled];
578     //Show shuffle status window
579     [statusWindowController showLoopWindowWithMode:newShuffleEnabled];
580 }
581
582 /*************************************************************************/
583 #pragma mark -
584 #pragma mark WORKSPACE NOTIFICATION HANDLERS
585 /*************************************************************************/
586
587 - (void)applicationLaunched:(NSNotification *)note
588 {
589     if (!note || [[[note userInfo] objectForKey:@"NSApplicationName"] isEqualToString:[currentRemote playerFullName]]) {
590         [currentRemote begin];
591         [self setLatestSongIdentifier:@""];
592         [self timerUpdate];
593         refreshTimer = [[NSTimer scheduledTimerWithTimeInterval:0.5
594                              target:self
595                              selector:@selector(timerUpdate)
596                              userInfo:nil
597                              repeats:YES] retain];
598         //[NSThread detachNewThreadSelector:@selector(startTimerInNewThread) toTarget:self withObject:nil];
599         [self setupHotKeys];
600         playerRunningState = ITMTRemotePlayerRunning;
601     }
602 }
603
604  - (void)applicationTerminated:(NSNotification *)note
605  {
606      if (!note || [[[note userInfo] objectForKey:@"NSApplicationName"] isEqualToString:[currentRemote playerFullName]]) {
607         [currentRemote halt];
608         [refreshTimer invalidate];
609         [refreshTimer release];
610         refreshTimer = nil;
611         [self clearHotKeys];
612         playerRunningState = ITMTRemotePlayerNotRunning;
613      }
614  }
615
616
617 /*************************************************************************/
618 #pragma mark -
619 #pragma mark NSApplication DELEGATE METHODS
620 /*************************************************************************/
621
622 - (void)applicationWillTerminate:(NSNotification *)note
623 {
624     [self clearHotKeys];
625     [[NSStatusBar systemStatusBar] removeStatusItem:statusItem];
626 }
627
628
629 /*************************************************************************/
630 #pragma mark -
631 #pragma mark DEALLOCATION METHOD
632 /*************************************************************************/
633
634 - (void)dealloc
635 {
636     [self applicationTerminated:nil];
637     [statusItem release];
638     [statusWindowController release];
639     [menuController release];
640     [super dealloc];
641 }
642
643
644 @end