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