Added a status field to the audioscrobbler preferences. Using alloc'd NSTimers in...
[MenuTunes.git] / PreferencesController.m
1 #import "PreferencesController.h"
2 #import "MainController.h"
3 #import "MenuController.h"
4 #import "NetworkController.h"
5 #import "NetworkObject.h"
6 #import "StatusWindow.h"
7 #import "StatusWindowController.h"
8 #import "CustomMenuTableView.h"
9 #import "AudioscrobblerController.h"
10
11 #import <Security/Security.h>
12
13 #import <netinet/in.h>
14 #import <arpa/inet.h>
15 #import <openssl/sha.h>
16 #import <sys/types.h>
17 #import <sys/stat.h>
18
19 #import <ITKit/ITLoginItem.h>
20
21 #import <ITKit/ITHotKeyCenter.h>
22 #import <ITKit/ITKeyCombo.h>
23 #import <ITKit/ITKeyComboPanel.h>
24 #import <ITKit/ITWindowPositioning.h>
25 #import <ITKit/ITKeyBroadcaster.h>
26
27 #import <ITKit/ITTSWBackgroundView.h>
28 #import <ITKit/ITWindowEffect.h>
29 #import <ITKit/ITCutWindowEffect.h>
30 #import <ITKit/ITDissolveWindowEffect.h>
31 #import <ITKit/ITSlideHorizontallyWindowEffect.h>
32 #import <ITKit/ITSlideVerticallyWindowEffect.h>
33 #import <ITKit/ITPivotWindowEffect.h>
34
35
36 #define SENDER_STATE (([sender state] == NSOnState) ? YES : NO)
37 #define AUDIOSCROBBLER_KEYCHAIN_SERVICE "MenuTunes: Audioscrobbler"
38 #define AUDIOSCROBBLER_KEYCHAIN_KIND "application password"
39
40 /*************************************************************************/
41 #pragma mark -
42 #pragma mark PRIVATE INTERFACE
43 /*************************************************************************/
44
45 @interface PreferencesController (Private)
46 + (SecKeychainItemRef)keychainItemForUser:(NSString *)user;
47 + (BOOL)keychainItemExistsForUser:(NSString *)user;
48 + (BOOL)createKeychainItemForUser:(NSString *)user andPassword:(NSString *)password;
49 + (BOOL)deleteKeychainItemForUser:(NSString *)user;
50 + (BOOL)setKeychainItemPassword:(NSString *)password forUser:(NSString *)user;
51
52 - (void)setupWindow;
53 - (void)setupCustomizationTables;
54 - (void)setupMenuItems;
55 - (void)setupUI;
56 - (void)setupScreenPopup;
57 - (void)setStatusWindowEntryEffect:(Class)effectClass;
58 - (void)setStatusWindowExitEffect:(Class)effectClass;
59 - (void)setCustomColor:(NSColor *)color updateWell:(BOOL)update;
60 - (void)repopulateEffectPopupsForVerticalPosition:(ITVerticalWindowPosition)vPos horizontalPosition:(ITHorizontalWindowPosition)hPos;
61 - (BOOL)effect:(Class)effectClass supportsVerticalPosition:(ITVerticalWindowPosition)vPos withHorizontalPosition:(ITHorizontalWindowPosition)hPos;
62 - (IBAction)changeMenus:(id)sender;
63 @end
64
65
66 @implementation PreferencesController
67
68
69 /*************************************************************************/
70 #pragma mark -
71 #pragma mark STATIC VARIABLES
72 /*************************************************************************/
73
74 static PreferencesController *prefs = nil;
75
76 /*************************************************************************/
77 #pragma mark -
78 #pragma mark STATIC KEYCHAIN SUPPORT METHODS
79 /*************************************************************************/
80
81 + (SecKeychainItemRef)keychainItemForUser:(NSString *)user
82 {
83         SecKeychainSearchRef search;
84         SecKeychainItemRef item;
85         OSStatus status;
86         SecKeychainAttribute attributes[3];
87         SecKeychainAttributeList list;
88
89         if ((user == nil) || ([user length] == 0)) {
90                 return nil;
91         }
92
93         ITDebugLog(@"Audioscrobbler: Searching for keychain item for %@.", user);
94         attributes[0].tag = kSecAccountItemAttr;
95         attributes[0].data = (char *)[user UTF8String];
96         attributes[0].length = [user length];
97         attributes[1].tag = kSecDescriptionItemAttr;
98         attributes[1].data = AUDIOSCROBBLER_KEYCHAIN_KIND;
99         attributes[1].length = strlen(AUDIOSCROBBLER_KEYCHAIN_KIND);
100         attributes[2].tag = kSecLabelItemAttr;
101         attributes[2].data = AUDIOSCROBBLER_KEYCHAIN_SERVICE;
102         attributes[2].length = strlen(AUDIOSCROBBLER_KEYCHAIN_SERVICE);
103         list.count = 3;
104         list.attr = attributes;
105
106         status = SecKeychainSearchCreateFromAttributes(NULL, kSecGenericPasswordItemClass, &list, &search);
107
108         if (status != noErr) {
109                 ITDebugLog(@"Audioscrobbler: Error searching for existing keychain item: %i", status);
110         }
111         
112         status = SecKeychainSearchCopyNext(search, &item);
113         
114         if (status != noErr) {
115                 ITDebugLog(@"Audioscrobbler: Error searching for existing keychain item: %i", status);
116                 item = nil;
117         }
118         
119     CFRelease(search);
120         return item;
121 }
122
123 + (BOOL)keychainItemExistsForUser:(NSString *)user
124 {
125         SecKeychainItemRef item = [PreferencesController keychainItemForUser:user];
126         BOOL exists = (item != nil);
127         if (item) {
128                 CFRelease(item);
129         }
130         return exists;
131 }
132
133 + (BOOL)createKeychainItemForUser:(NSString *)user andPassword:(NSString *)password
134 {
135         SecKeychainItemRef item;
136         OSStatus status;
137         SecKeychainAttribute attributes[3];
138         SecKeychainAttributeList list;
139
140         ITDebugLog(@"Audioscrobbler: Creating new keychain item for %@.", user);
141         attributes[0].tag = kSecAccountItemAttr;
142         attributes[0].data = (char *)[user UTF8String];
143         attributes[0].length = [user length];
144         attributes[1].tag = kSecDescriptionItemAttr;
145         attributes[1].data = AUDIOSCROBBLER_KEYCHAIN_KIND;
146         attributes[1].length = strlen(AUDIOSCROBBLER_KEYCHAIN_KIND);
147         attributes[2].tag = kSecLabelItemAttr;
148         attributes[2].data = AUDIOSCROBBLER_KEYCHAIN_SERVICE;
149         attributes[2].length = strlen(AUDIOSCROBBLER_KEYCHAIN_SERVICE);
150         list.count = 3;
151         list.attr = attributes;
152
153         status = SecKeychainItemCreateFromContent(kSecGenericPasswordItemClass, &list, [password length], [password UTF8String], NULL, NULL, &item);
154         if (status != noErr) {
155                 ITDebugLog(@"Audioscrobbler: Error creating keychain item: %i", status);
156         }
157         return (status == noErr);
158 }
159
160 + (BOOL)deleteKeychainItemForUser:(NSString *)user
161 {
162         OSStatus status = errSecNotAvailable;
163         SecKeychainItemRef item = [PreferencesController keychainItemForUser:user];
164         if (item != nil) {
165                 status = SecKeychainItemDelete(item);
166                 if (status != noErr) {
167                         ITDebugLog(@"Audioscrobbler: Error deleting keychain item: %i", status);
168                 }
169                 CFRelease(item);
170         }
171         return (status == noErr);
172 }
173
174 + (BOOL)setKeychainItemPassword:(NSString *)password forUser:(NSString *)user
175 {
176         OSStatus status = errSecNotAvailable;
177         SecKeychainItemRef item = [PreferencesController keychainItemForUser:user];
178         if (item != nil) {
179                 status = SecKeychainItemModifyContent(item, NULL, [password length], [password cString]);
180                 if (status != noErr) {
181                         ITDebugLog(@"Audioscrobbler: Error deleting keychain item: %i", status);
182                 }
183                 CFRelease(item);
184         }
185         return (status == noErr);
186 }
187
188 + (NSString *)getKeychainItemPasswordForUser:(NSString *)user
189 {
190         OSStatus status = errSecNotAvailable;
191         SecKeychainItemRef item = [PreferencesController keychainItemForUser:user];
192         NSString *pass = nil;
193         if (item != nil) {
194                 UInt32 length;
195                 char *buffer;
196                 status = SecKeychainItemCopyContent(item, NULL, NULL, &length, (void **)&buffer);
197                 if (status != noErr) {
198                         ITDebugLog(@"Audioscrobbler: Error getting keychain item password: %i", status);
199                 } else {
200                         if ([NSString respondsToSelector:@selector(stringWithCString:encoding:)]) {
201                                 pass = [NSString stringWithCString:buffer encoding:NSASCIIStringEncoding];
202                         } else {
203                                 pass = [NSString stringWithCString:buffer];
204                         }
205                 }
206                 if (status != noErr) {
207                         ITDebugLog(@"Audioscrobbler: Error deleting keychain item: %i", status);
208                 }
209                 CFRelease(item);
210         }
211         return pass;
212 }
213
214 /*************************************************************************/
215 #pragma mark -
216 #pragma mark INITIALIZATION METHODS
217 /*************************************************************************/
218
219 + (PreferencesController *)sharedPrefs;
220 {
221     if (! prefs) {
222         prefs = [[self alloc] init];
223     }
224     return prefs;
225 }
226
227 - (id)init
228 {
229     if ( (self = [super init]) ) {
230         ITDebugLog(@"Preferences initialized.");
231         df = [[NSUserDefaults standardUserDefaults] retain];
232         
233         effectClasses = [[ITWindowEffect effectClasses] retain];
234         
235         hotKeysArray = [[NSArray alloc] initWithObjects:@"PlayPause",
236                                                        @"NextTrack",
237                                                        @"PrevTrack",
238                                                        @"FastForward",
239                                                        @"Rewind",
240                                                        @"ShowPlayer",
241                                                        @"TrackInfo",
242                                                        @"UpcomingSongs",
243                                                        @"IncrementVolume",
244                                                        @"DecrementVolume",
245                                                        @"IncrementRating",
246                                                        @"DecrementRating",
247                                                        @"ToggleShuffle",
248                                                        @"ToggleLoop",
249                                                                                                            @"ToggleShufflability",
250                                                        @"PopupMenu",
251                                                        @"SetRating0",
252                                                        @"SetRating1",
253                                                        @"SetRating2",
254                                                        @"SetRating3",
255                                                        @"SetRating4",
256                                                        @"SetRating5",
257                                                        nil];
258         
259         hotKeyNamesArray = [[NSArray alloc] initWithObjects:@"Play/Pause",
260                                                        @"Next Track",
261                                                        @"Previous Track",
262                                                        @"Fast Forward",
263                                                        @"Rewind",
264                                                        @"Show Player",
265                                                        @"Track Info",
266                                                        @"Upcoming Songs",
267                                                        @"Increment Volume",
268                                                        @"Decrement Volume",
269                                                        @"Increment Rating",
270                                                        @"Decrement Rating",
271                                                        @"Toggle Shuffle",
272                                                        @"Toggle Loop",
273                                                                                                            @"Toggle Song Included In Shuffle",
274                                                        @"Pop-up status menu",
275                                                        [NSString stringWithUTF8String:"Set Rating: â€šÃ²Ãœâ€šÃ²Ãœâ€šÃ²Ãœâ€šÃ²Ãœâ€šÃ²Ãœ"],
276                                                        [NSString stringWithUTF8String:"Set Rating: â€šÃ²Ã–‚òÜ‚òÜ‚òÜ‚òÜ"],
277                                                        [NSString stringWithUTF8String:"Set Rating: â€šÃ²Ã–‚òÖ‚òÜ‚òÜ‚òÜ"],
278                                                        [NSString stringWithUTF8String:"Set Rating: â€šÃ²Ã–‚òÖ‚òÖ‚òÜ‚òÜ"],
279                                                        [NSString stringWithUTF8String:"Set Rating: â€šÃ²Ã–‚òÖ‚òÖ‚òÖ‚òÜ"],
280                                                        [NSString stringWithUTF8String:"Set Rating: â€šÃ²Ã–‚òÖ‚òÖ‚òÖ‚òÖ"],
281                                                        nil];
282         hotKeysDictionary = [[NSMutableDictionary alloc] init];
283         controller = nil;
284         
285         [self setupWindow];  // Load in the nib, and perform any initial setup.
286         [[NSColorPanel sharedColorPanel] setShowsAlpha:YES];
287     }
288     return self;
289 }
290
291
292 /*************************************************************************/
293 #pragma mark -
294 #pragma mark ACCESSOR METHODS
295 /*************************************************************************/
296
297 - (id)controller
298 {
299     return controller;
300 }
301
302 - (void)setController:(id)object
303 {
304     [controller autorelease];
305     controller = [object retain];
306 }
307
308
309 /*************************************************************************/
310 #pragma mark -
311 #pragma mark INSTANCE METHODS
312 /*************************************************************************/
313
314 - (BOOL)showPasswordPanel
315 {
316     [passwordPanel setLevel:NSStatusWindowLevel];
317     [passwordPanelOKButton setTitle:@"Connect"];
318     [passwordPanelTitle setStringValue:@"Password Required"];
319     [passwordPanelMessage setStringValue:[NSString stringWithFormat:@"Please enter a password for access to the MenuTunes player named %@ at %@.", [[[NetworkController sharedController] networkObject] serverName], [[NetworkController sharedController] remoteHost]]];
320     [passwordPanel setLevel:NSStatusWindowLevel];
321     [NSApp activateIgnoringOtherApps:YES];
322     [passwordPanel center];
323     [passwordPanel orderFrontRegardless];
324     [passwordPanel makeKeyWindow];
325     if ([NSApp runModalForWindow:passwordPanel]) {
326         return YES;
327     } else {
328         return NO;
329     }
330 }
331
332 - (BOOL)showInvalidPasswordPanel
333 {
334     [passwordPanel setLevel:NSStatusWindowLevel];
335     [passwordPanelOKButton setTitle:@"Retry"];
336     [passwordPanelTitle setStringValue:@"Invalid Password"];
337     [passwordPanelMessage setStringValue:[NSString stringWithFormat:@"The password entered for access to the MenuTunes player named %@ at %@ is invalid.  Please provide a new password.", [[[NetworkController sharedController] networkObject] serverName], [[NetworkController sharedController] remoteHost]]];
338     [passwordPanel setLevel:NSStatusWindowLevel];
339     [NSApp activateIgnoringOtherApps:YES];
340     [passwordPanel center];
341     [passwordPanel orderFrontRegardless];
342     [passwordPanel makeKeyWindow];
343     if ([NSApp runModalForWindow:passwordPanel]) {
344         return YES;
345     } else {
346         return NO;
347     }
348 }
349
350 - (IBAction)showPrefsWindow:(id)sender
351 {
352     ITDebugLog(@"Showing preferences window.");
353     if (!myItems) {  // If menu array does not exist yet, then the window hasn't been setup.
354         ITDebugLog(@"Window doesn't exist, initial setup.");
355         [self setupCustomizationTables];  // Setup the DnD manu config tables.
356         [self setupMenuItems];  // Setup the arrays of menu items
357         [self setupUI]; // Sets up additional UI
358         [window setDelegate:self];
359         [menuTableView reloadData];
360         [hotKeysTableView reloadData];
361         [hotKeysTableView setDoubleAction:@selector(hotKeysTableViewDoubleClicked:)];
362         
363         //Change the launch player checkbox to the proper name
364         NS_DURING
365             [launchPlayerAtLaunchCheckbox setTitle:[NSString stringWithFormat:@"Launch %@ when MenuTunes launches", [[controller currentRemote] playerSimpleName]]]; //This isn't localized...
366         NS_HANDLER
367             [controller networkError:localException];
368         NS_ENDHANDLER
369     }
370
371     [self resetRemotePlayerTextFields];
372     [launchAtLoginCheckbox becomeFirstResponder];
373     [NSApp activateIgnoringOtherApps:YES];
374         if (![window isVisible]) {
375                 [window center];
376         }
377     [window orderFrontRegardless];
378     [window makeKeyWindow];
379 }
380
381 - (IBAction)changeGeneralSetting:(id)sender
382 {
383     ITDebugLog(@"Changing general setting of tag %i.", [sender tag]);
384     if ( [sender tag] == 1010) {
385         ITSetApplicationLaunchOnLogin([[NSBundle mainBundle] bundlePath], SENDER_STATE);
386     } else if ( [sender tag] == 1020) {
387         [df setBool:SENDER_STATE forKey:@"LaunchPlayerWithMT"];
388     } else if ( [sender tag] == 1030) {
389         [df setInteger:[sender intValue] forKey:@"SongsInAdvance"];
390         if ([[controller currentRemote] playerRunningState] == ITMTRemotePlayerRunning) {
391             [[controller menuController] performSelector:@selector(rebuildSubmenus) withObject:nil afterDelay:0];
392         }
393     } else if ( [sender tag] == 1040) {
394         // This will not be executed.  Song info always shows the title of the song.
395         // [df setBool:SENDER_STATE forKey:@"showName"];
396     } else if ( [sender tag] == 1050) {
397         [df setBool:SENDER_STATE forKey:@"showArtist"];
398     } else if ( [sender tag] == 1055) {
399         [df setBool:SENDER_STATE forKey:@"showComposer"];
400     } else if ( [sender tag] == 1060) {
401         [df setBool:SENDER_STATE forKey:@"showAlbum"];
402     } else if ( [sender tag] == 1070) {
403         [df setBool:SENDER_STATE forKey:@"showTime"];
404     } else if ( [sender tag] == 1080) {
405         [df setBool:SENDER_STATE forKey:@"showTrackNumber"];
406     } else if ( [sender tag] == 1085) {
407         [df setBool:SENDER_STATE forKey:@"showPlayCount"];
408     } else if ( [sender tag] == 1090) {
409         [df setBool:SENDER_STATE forKey:@"showTrackRating"];
410     } else if ( [sender tag] == 1100) {
411         [df setBool:SENDER_STATE forKey:@"showAlbumArtwork"];
412     } else if ( [sender tag] == 1110) {
413         [df setBool:SENDER_STATE forKey:@"runScripts"];
414         if (SENDER_STATE) {
415             [runScriptsCheckbox setState:NSOnState];
416             [showScriptsButton setEnabled:YES];
417         } else {
418             [showScriptsButton setEnabled:NO];
419         }
420     } else if ( [sender tag] == 1120) {
421         mkdir([[NSHomeDirectory() stringByAppendingPathComponent:@"Library/Application Support/MenuTunes/Scripts"] cString], 0744);
422         [[NSWorkspace sharedWorkspace] openFile:[NSHomeDirectory() stringByAppendingPathComponent:@"Library/Application Support/MenuTunes/Scripts"]];
423     } else if ( [sender tag] == 6010) {
424                 //Toggle the other Audioscrobbler options
425                 [df setBool:SENDER_STATE forKey:@"audioscrobblerEnabled"];
426                 [audioscrobblerUseCacheCheckbox setEnabled:SENDER_STATE];
427                 [audioscrobblerUserTextField setEnabled:SENDER_STATE];
428                 [audioscrobblerPasswordTextField setEnabled:SENDER_STATE];
429                 if (SENDER_STATE) {
430                         [[AudioscrobblerController sharedController] attemptHandshake:NO];
431                 }
432         } else if ( [sender tag ] == 6015) {
433                 //Here we create a new keychain item if needed and deletes the keychain item if the field is cleared.
434                 NSString *currentAccount = [df stringForKey:@"audioscrobblerUser"], *newAccount = [sender stringValue];
435                 if ([newAccount length] == 0) {
436                         [PreferencesController deleteKeychainItemForUser:currentAccount];
437                 } else if (![currentAccount isEqualToString:newAccount] && [[audioscrobblerPasswordTextField stringValue] length] > 0) {
438                         [df setObject:newAccount forKey:@"audioscrobblerUser"];
439                         if ([PreferencesController keychainItemExistsForUser:currentAccount]) {
440                                 //Delete the current keychain item if there is one
441                                 [PreferencesController deleteKeychainItemForUser:currentAccount];
442                         }
443                         [PreferencesController createKeychainItemForUser:newAccount andPassword:[audioscrobblerPasswordTextField stringValue]];
444                         [[AudioscrobblerController sharedController] attemptHandshake:YES];
445                 }
446         } else if ( [sender tag ] == 6030) {
447                 //Here we set the password for an existing keychain item or we create a new keychain item.
448                 if ([[audioscrobblerUserTextField stringValue] length] > 0) {
449                         NSString *account = [df stringForKey:@"audioscrobblerUser"];
450                         if ([PreferencesController keychainItemExistsForUser:account]) {
451                                 //Update the current keychain item
452                                 [PreferencesController setKeychainItemPassword:[sender stringValue] forUser:account];
453                         } else if ([[sender stringValue] length] > 0 && [[audioscrobblerUserTextField stringValue] length]) {
454                                 //Create a new keychain item
455                                 [PreferencesController createKeychainItemForUser:account andPassword:[sender stringValue]];
456                         }
457                 }
458         } else if ( [sender tag] == 6045) {
459                 [df setBool:SENDER_STATE forKey:@"audioscrobblerCacheSubmissions"];
460         }
461     [df synchronize];
462 }
463
464 - (IBAction)changeSharingSetting:(id)sender
465 {
466     ITDebugLog(@"Changing sharing setting of tag %i.", [sender tag]);
467     if ( [sender tag] == 5010 ) {
468         BOOL state = SENDER_STATE;
469         [df setBool:state forKey:@"enableSharing"];
470         //Disable/enable the use of shared player options
471         [useSharedMenuTunesCheckbox setEnabled:!state];
472         [passwordTextField setEnabled:state];
473         [nameTextField setEnabled:state];
474         [selectSharedPlayerButton setEnabled:NO];
475         [controller setServerStatus:state]; //Set server status
476     } else if ( [sender tag] == 5015 ) {
477         [df setObject:[sender stringValue] forKey:@"sharedPlayerName"];
478         [[NetworkController sharedController] resetServerName];
479     } else if ( [sender tag] == 5030 ) {
480         //Set the server password
481         const char *instring = [[sender stringValue] UTF8String];
482         const char *password = "p4s5w0rdMT1.2";
483         char *result;
484         NSData *hashedPass, *passwordStringHash;
485         if ([[sender stringValue] length] == 0) {
486             [df setObject:[NSData data] forKey:@"sharedPlayerPassword"];
487             return;
488         }
489         result = (char *)SHA1((unsigned char *)instring, strlen(instring), NULL);
490         hashedPass = [NSData dataWithBytes:result length:strlen(result)];
491         result = (char *)SHA1((unsigned char *)password, strlen(password), NULL);
492         passwordStringHash = [NSData dataWithBytes:result length:strlen(result)];
493         if (![hashedPass isEqualToData:passwordStringHash]) {
494             [df setObject:hashedPass forKey:@"sharedPlayerPassword"];
495             [sender setStringValue:@"p4s5w0rdMT1.2"];
496         }
497     } else if ( [sender tag] == 5040 ) {
498         BOOL state = SENDER_STATE;
499         [df setBool:state forKey:@"useSharedPlayer"];
500         //Disable/enable the use of sharing options
501         [shareMenuTunesCheckbox setEnabled:!state];
502         [passwordTextField setEnabled:NO];
503         [nameTextField setEnabled:NO];
504         [selectSharedPlayerButton setEnabled:state];
505         
506         if (state && ([controller connectToServer] == 1)) {
507             [selectedPlayerTextField setStringValue:[[[NetworkController sharedController] networkObject] serverName]];
508             [locationTextField setStringValue:[[NetworkController sharedController] remoteHost]];
509         } else {
510             [selectedPlayerTextField setStringValue:@"No shared player selected."];
511             [locationTextField setStringValue:@"-"];
512             if ([[NetworkController sharedController] isConnectedToServer]) {
513                 [controller disconnectFromServer];
514             }
515             
516         }
517     } else if ( [sender tag] == 5050 ) {
518         //If no player is selected in the table view, turn off OK button.
519         if ([sender clickedRow] == -1 ) {
520             [sharingPanelOKButton setEnabled:NO];
521         } else {
522             [sharingPanelOKButton setEnabled:YES];
523         }
524     } else if ( [sender tag] == 5051 ) {
525         [df setObject:[sender stringValue] forKey:@"sharedPlayerHost"];
526     } else if ( [sender tag] == 5060 ) {
527         //Set OK button state
528         if (([selectPlayerBox contentView] == zeroConfView && [sharingTableView selectedRow] == -1) ||
529             ([selectPlayerBox contentView] == manualView && [[hostTextField stringValue] length] == 0)) {
530             [sharingPanelOKButton setEnabled:NO];
531         } else {
532             [sharingPanelOKButton setEnabled:YES];
533         }
534         //Show selection sheet
535         [NSApp beginSheet:selectPlayerSheet modalForWindow:window modalDelegate:self didEndSelector:NULL contextInfo:nil];
536     } else if ( [sender tag] == 5100 ) {
537         //Change view
538         if ( ([sender indexOfItem:[sender selectedItem]] == 0) && ([selectPlayerBox contentView] != zeroConfView) ) {
539             NSRect frame = [selectPlayerSheet frame];
540             frame.origin.y -= 58;
541             frame.size.height = 273;
542             if ([sharingTableView selectedRow] == -1) {
543                 [sharingPanelOKButton setEnabled:NO];
544             }
545             [selectPlayerBox setContentView:zeroConfView];
546             [selectPlayerSheet setFrame:frame display:YES animate:YES];
547         } else if ( ([sender indexOfItem:[sender selectedItem]] == 1) && ([selectPlayerBox contentView] != manualView) ){
548             NSRect frame = [selectPlayerSheet frame];
549             frame.origin.y += 58;
550             frame.size.height = 215;
551             if ([[hostTextField stringValue] length] == 0) {
552                 [sharingPanelOKButton setEnabled:NO];
553             } else {
554                 [sharingPanelOKButton setEnabled:YES];
555             }
556             [selectPlayerBox setContentView:manualView];
557             [selectPlayerSheet setFrame:frame display:YES animate:YES];
558             [hostTextField selectText:nil];
559         }
560     } else if ( [sender tag] == 5150 ) {
561         const char *instring = [[sender stringValue] UTF8String];
562         char *result;
563         result = (char *)SHA1((unsigned char *)instring, strlen(instring), NULL);
564         [df setObject:[NSData dataWithBytes:result length:strlen(result)] forKey:@"connectPassword"];
565     } else if ( [sender tag] == 5110 ) {
566         //Cancel
567         [NSApp endSheet:selectPlayerSheet];
568         [selectPlayerSheet orderOut:nil];
569         if ([selectPlayerBox contentView] == manualView) {
570             [hostTextField setStringValue:[df stringForKey:@"sharedPlayerHost"]];
571         } else {
572         }
573     } else if ( [sender tag] == 5120 ) {
574         //OK, try to connect
575         [NSApp endSheet:selectPlayerSheet];
576         [selectPlayerSheet orderOut:nil];
577         
578         [self changeSharingSetting:clientPasswordTextField];
579         
580         if ([selectPlayerBox contentView] == manualView) {
581             [df setObject:[hostTextField stringValue] forKey:@"sharedPlayerHost"];
582         } else {
583             if ([sharingTableView selectedRow] > -1) {
584                 [df setObject:[NSString stringWithCString:inet_ntoa((*(struct sockaddr_in*)[[[[[[NetworkController sharedController] remoteServices] objectAtIndex:[sharingTableView selectedRow]] addresses] objectAtIndex:0] bytes]).sin_addr)] forKey:@"sharedPlayerHost"];
585             }
586         }
587         
588         if ([controller connectToServer] == 1) {
589             [useSharedMenuTunesCheckbox setState:NSOnState];
590             [selectedPlayerTextField setStringValue:[[[NetworkController sharedController] networkObject] serverName]];
591             [locationTextField setStringValue:[[NetworkController sharedController] remoteHost]];
592         } else {
593             NSRunAlertPanel(@"Connection error.", @"The MenuTunes server you attempted to connect to was not responding. MenuTunes will revert back to the local player.", @"OK", nil, nil);
594         }
595     } else if ( [sender tag] == 6010 ) {
596         //Cancel password entry
597         [passwordPanel orderOut:nil];
598         [NSApp stopModalWithCode:0];
599     } else if ( [sender tag] == 6020 ) {
600         //OK password entry, retry connect
601         const char *instring = [[passwordPanelTextField stringValue] UTF8String];
602         char *result;
603         result = (char *)SHA1((unsigned char *)instring, strlen(instring), NULL);
604         [df setObject:[NSData dataWithBytes:result length:strlen(result)] forKey:@"connectPassword"];
605         [passwordPanel orderOut:nil];
606         [NSApp stopModalWithCode:1];
607     }
608     [df synchronize];
609 }
610
611 - (IBAction)changeStatusWindowSetting:(id)sender
612 {
613     StatusWindow *sw = (StatusWindow *)[StatusWindow sharedWindow];
614     ITDebugLog(@"Changing status window setting of tag %i", [sender tag]);
615     
616     if ( [sender tag] == 2010) {
617     
618         BOOL entryEffectValid = YES;
619         BOOL exitEffectValid  = YES;
620                 
621         [df setInteger:[sender selectedRow] forKey:@"statusWindowVerticalPosition"];
622         [df setInteger:[sender selectedColumn] forKey:@"statusWindowHorizontalPosition"];
623         [sw setVerticalPosition:[sender selectedRow]];
624         [sw setHorizontalPosition:[sender selectedColumn]];
625         
626         // Enable/disable the items in the popups.
627         [self repopulateEffectPopupsForVerticalPosition:[sw verticalPosition]
628                                      horizontalPosition:[sw horizontalPosition]];
629
630         // Make sure the effects support the new position.  
631         entryEffectValid = ( [self effect:[[sw entryEffect] class] 
632                  supportsVerticalPosition:[sw verticalPosition]
633                    withHorizontalPosition:[sw horizontalPosition]] );
634         exitEffectValid  = ( [self effect:[[sw exitEffect] class] 
635                  supportsVerticalPosition:[sw verticalPosition]
636                    withHorizontalPosition:[sw horizontalPosition]] );
637         
638         if ( ! entryEffectValid ) {
639             [appearanceEffectPopup selectItemAtIndex:[[appearanceEffectPopup menu] indexOfItemWithRepresentedObject:NSClassFromString(@"ITCutWindowEffect")]];
640             [self setStatusWindowEntryEffect:NSClassFromString(@"ITCutWindowEffect")];
641         } else {
642             [appearanceEffectPopup selectItemAtIndex:[[appearanceEffectPopup menu] indexOfItemWithRepresentedObject:[[sw entryEffect] class]]];
643         }
644         
645         if ( ! exitEffectValid ) {
646             [vanishEffectPopup selectItemAtIndex:[[vanishEffectPopup menu] indexOfItemWithRepresentedObject:NSClassFromString(@"ITDissolveWindowEffect")]];
647             [self setStatusWindowExitEffect:NSClassFromString(@"ITDissolveWindowEffect")];
648         } else {
649             [vanishEffectPopup selectItemAtIndex:[[vanishEffectPopup menu] indexOfItemWithRepresentedObject:[[sw exitEffect] class]]];
650         }
651         
652         [(MainController *)controller showCurrentTrackInfo];
653         
654     } else if ( [sender tag] == 2020) {
655     
656         // Update screen selection.
657                 [[StatusWindow sharedWindow] setScreen:[[NSScreen screens] objectAtIndex:[sender indexOfSelectedItem]]];
658                 [df setInteger:[sender indexOfSelectedItem] forKey:@"statusWindowScreenIndex"];
659         [(MainController *)controller showCurrentTrackInfo];
660                 
661     } else if ( [sender tag] == 2030) {
662     
663         [self setStatusWindowEntryEffect:[[sender selectedItem] representedObject]];
664         [(MainController *)controller showCurrentTrackInfo];
665         
666     } else if ( [sender tag] == 2040) {
667     
668         [self setStatusWindowExitEffect:[[sender selectedItem] representedObject]];
669         [(MainController *)controller showCurrentTrackInfo];
670         
671     } else if ( [sender tag] == 2050) {
672         float newTime = ( -([sender floatValue]) );
673         [df setFloat:newTime forKey:@"statusWindowAppearanceSpeed"];
674         [[sw entryEffect] setEffectTime:newTime];
675     } else if ( [sender tag] == 2060) {
676         float newTime = ( -([sender floatValue]) );
677         [df setFloat:newTime forKey:@"statusWindowVanishSpeed"];
678         [[sw exitEffect] setEffectTime:newTime];
679     } else if ( [sender tag] == 2070) {
680         [df setFloat:[sender floatValue] forKey:@"statusWindowVanishDelay"];
681         [sw setExitDelay:[sender floatValue]];
682     } else if ( [sender tag] == 2080) {
683         [df setBool:SENDER_STATE forKey:@"showSongInfoOnChange"];
684     } else if ( [sender tag] == 2090) {
685         
686         int setting = [sender indexOfSelectedItem];
687         
688         if ( setting == 0 ) {
689             [(ITTSWBackgroundView *)[sw contentView] setBackgroundMode:ITTSWBackgroundApple];
690             [backgroundColorWell  setEnabled:NO];
691             [backgroundColorPopup setEnabled:NO];
692         } else if ( setting == 1 ) {
693             [(ITTSWBackgroundView *)[sw contentView] setBackgroundMode:ITTSWBackgroundReadable];
694             [backgroundColorWell  setEnabled:NO];
695             [backgroundColorPopup setEnabled:NO];
696         } else if ( setting == 2 ) {
697             [(ITTSWBackgroundView *)[sw contentView] setBackgroundMode:ITTSWBackgroundColored];
698             [backgroundColorWell  setEnabled:YES];
699             [backgroundColorPopup setEnabled:YES];
700         }
701
702         [df setInteger:setting forKey:@"statusWindowBackgroundMode"];
703         [(MainController *)controller showCurrentTrackInfo];
704         
705     } else if ( [sender tag] == 2091) {
706         [self setCustomColor:[sender color] updateWell:NO];
707         [(MainController *)controller showCurrentTrackInfo];
708     } else if ( [sender tag] == 2092) {
709         
710         int selectedItem = [sender indexOfSelectedItem];
711         
712         if ( selectedItem == 1 ) { // An NSPopUpButton in PullDown mode uses item 0 as its title.  Its first selectable item is 1.
713             [self setCustomColor:[NSColor colorWithCalibratedRed:0.92549 green:0.686275 blue:0.0 alpha:1.0] updateWell:YES];
714         } else if ( selectedItem == 2 ) {
715             [self setCustomColor:[NSColor colorWithCalibratedRed:0.380392 green:0.670588 blue:0.0 alpha:1.0] updateWell:YES];
716         } else if ( selectedItem == 3 ) {
717             [self setCustomColor:[NSColor colorWithCalibratedRed:0.443137 green:0.231373 blue:0.619608 alpha:1.0] updateWell:YES];
718         } else if ( selectedItem == 4 ) {
719             [self setCustomColor:[NSColor colorWithCalibratedRed:0.831373 green:0.12549 blue:0.509804 alpha:1.0] updateWell:YES];
720         } else if ( selectedItem == 5 ) {
721             [self setCustomColor:[NSColor colorWithCalibratedRed:0.00784314 green:0.611765 blue:0.662745 alpha:1.0] updateWell:YES];
722         } else {
723             [self setCustomColor:[NSColor colorWithCalibratedWhite:0.15 alpha:0.70] updateWell:YES];
724         }
725         [(MainController *)controller showCurrentTrackInfo];
726
727     } else if ( [sender tag] == 2095) {
728         [df setInteger:[sender indexOfSelectedItem] forKey:@"statusWindowSizing"];
729         [(MainController *)controller showCurrentTrackInfo];
730     }
731     
732     [df synchronize];
733 }
734
735 - (void)registerDefaults
736 {
737     ITDebugLog(@"Registering defaults.");
738     [df setObject:[NSArray arrayWithObjects:
739         @"trackInfo",
740         @"separator",
741         @"playPause",
742         @"prevTrack",
743         @"nextTrack",
744         @"separator",
745         @"playlists",
746         @"upcomingSongs",
747         @"separator",
748         @"preferences",
749                 @"about",
750         @"quit",
751         nil] forKey:@"menu"];
752
753     [df setInteger:MT_CURRENT_VERSION forKey:@"appVersion"];
754     [df setInteger:5 forKey:@"SongsInAdvance"];
755 //  [df setBool:YES forKey:@"showName"];  // Song info will always show song title.
756     [df setBool:YES forKey:@"showArtist"];
757     [df setBool:YES forKey:@"showAlbumArtwork"];
758     [df setBool:NO forKey:@"showAlbum"];
759     [df setBool:NO forKey:@"showComposer"];
760     [df setBool:NO forKey:@"showTime"];
761     [df setBool:NO forKey:@"showToolTip"];
762
763     [df setObject:@"ITCutWindowEffect" forKey:@"statusWindowAppearanceEffect"];
764     [df setObject:@"ITDissolveWindowEffect" forKey:@"statusWindowVanishEffect"];
765     [df setFloat:0.8 forKey:@"statusWindowAppearanceSpeed"];
766     [df setFloat:0.8 forKey:@"statusWindowVanishSpeed"];
767     [df setFloat:4.0 forKey:@"statusWindowVanishDelay"];
768     [df setInteger:(int)ITWindowPositionBottom forKey:@"statusWindowVerticalPosition"];
769     [df setInteger:(int)ITWindowPositionLeft forKey:@"statusWindowHorizontalPosition"];
770         [df setInteger:0 forKey:@"statusWindowScreenIndex"];
771     [[StatusWindow sharedWindow] setVerticalPosition:(int)ITWindowPositionBottom];
772     [[StatusWindow sharedWindow] setHorizontalPosition:(int)ITWindowPositionLeft];
773     [df setBool:YES forKey:@"showSongInfoOnChange"];
774     
775     [df setObject:[NSArchiver archivedDataWithRootObject:[NSColor blueColor]] forKey:@"statusWindowBackgroundColor"];
776     
777     [df synchronize];
778     
779     if (ITDoesApplicationLaunchOnLogin([[NSBundle mainBundle] bundlePath])) {
780         [[StatusWindowController sharedController] showSetupQueryWindow];
781     }
782 }
783
784 - (void)autoLaunchOK
785 {
786     [(StatusWindow *)[StatusWindow sharedWindow] setLocked:NO];
787     [[StatusWindow sharedWindow] vanish:self];
788     [[StatusWindow sharedWindow] setIgnoresMouseEvents:YES];
789     
790     ITSetApplicationLaunchOnLogin([[NSBundle mainBundle] bundlePath], YES);
791 }
792
793 - (void)autoLaunchCancel
794 {
795     [(StatusWindow *)[StatusWindow sharedWindow] setLocked:NO];
796     [[StatusWindow sharedWindow] vanish:self];
797     [[StatusWindow sharedWindow] setIgnoresMouseEvents:YES];
798 }
799
800 - (void)deletePressedInTableView:(NSTableView *)tableView
801 {
802     if (tableView == menuTableView) {
803         int selRow = [tableView selectedRow];
804         ITDebugLog(@"Delete pressed in menu table view.");
805         if (selRow != - 1) {
806             NSString *object = [myItems objectAtIndex:selRow];
807             
808             if ([object isEqualToString:@"preferences"]) {
809                 NSBeep();
810                 return;
811             }
812             
813             if (![object isEqualToString:@"separator"])
814                 [availableItems addObject:object];
815             ITDebugLog(@"Removing object named %@", object);
816             [myItems removeObjectAtIndex:selRow];
817             [menuTableView reloadData];
818             [allTableView reloadData];
819         }
820         [self changeMenus:self];
821     }
822 }
823
824 - (void)resetRemotePlayerTextFields
825 {
826     if ([[NetworkController sharedController] isConnectedToServer]) {
827         [selectedPlayerTextField setStringValue:[[[NetworkController sharedController] networkObject] serverName]];
828         [locationTextField setStringValue:[[NetworkController sharedController] remoteHost]];
829     } else {
830         [selectedPlayerTextField setStringValue:@"No shared player selected."];
831         [locationTextField setStringValue:@"-"];
832     }
833 }
834
835 /*************************************************************************/
836 #pragma mark -
837 #pragma mark HOTKEY SUPPORT METHODS
838 /*************************************************************************/
839
840 - (IBAction)clearHotKey:(id)sender
841 {
842     [hotKeysDictionary setObject:[ITKeyCombo clearKeyCombo] forKey:[hotKeysArray objectAtIndex:[hotKeysTableView selectedRow]]];
843     [df setObject:[[ITKeyCombo clearKeyCombo] plistRepresentation] forKey:[hotKeysArray objectAtIndex:[hotKeysTableView selectedRow]]];
844     [controller setupHotKeys];
845     [hotKeysTableView reloadData];
846 }
847
848 - (IBAction)editHotKey:(id)sender
849 {
850     ITKeyComboPanel *panel = [ITKeyComboPanel sharedPanel];
851     NSString *keyComboKey = [hotKeysArray objectAtIndex:[hotKeysTableView selectedRow]];
852     ITKeyCombo *keyCombo;
853     
854     ITDebugLog(@"Setting key combo on hot key %@.", keyComboKey);
855     [controller clearHotKeys];
856     [panel setKeyCombo:[hotKeysDictionary objectForKey:[hotKeysArray objectAtIndex:[hotKeysTableView selectedRow]]]];
857     [panel setKeyBindingName:[hotKeyNamesArray objectAtIndex:[hotKeysTableView selectedRow]]];
858     if ([panel runModal] == NSOKButton) {
859         NSEnumerator *keyEnumerator = [[hotKeysDictionary allKeys] objectEnumerator];
860         NSString *nextKey;
861         keyCombo = [panel keyCombo];
862         
863         //Check for duplicate key combo
864         while ( (nextKey = [keyEnumerator nextObject]) ) {
865             if ([[hotKeysDictionary objectForKey:nextKey] isEqual:keyCombo] &&
866                 ![keyCombo isEqual:[ITKeyCombo clearKeyCombo]]) {
867                 [hotKeysDictionary setObject:[ITKeyCombo clearKeyCombo]
868                                    forKey:nextKey];
869                 [df setObject:[[ITKeyCombo clearKeyCombo] plistRepresentation]
870                     forKey:nextKey];
871             }
872         }
873         
874         [hotKeysDictionary setObject:keyCombo forKey:keyComboKey];
875         [df setObject:[keyCombo plistRepresentation] forKey:keyComboKey];
876         [controller setupHotKeys];
877         [hotKeysTableView reloadData];
878         ITDebugLog(@"Set combo %@ on hot key %@.", keyCombo, keyComboKey);
879     } else {
880         ITDebugLog(@"Hot key setting on hot key %@ cancelled.", keyComboKey);
881     }
882 }
883
884 - (void)hotKeysTableViewDoubleClicked:(id)sender
885 {
886     if ([sender clickedRow] > -1) {
887         [self editHotKey:sender];
888     }
889 }
890
891 /*************************************************************************/
892 #pragma mark -
893 #pragma mark PRIVATE METHOD IMPLEMENTATIONS
894 /*************************************************************************/
895
896 - (void)audioscrobblerStatusChanged:(NSNotification *)note
897 {
898         [audioscrobblerStatusTextField setStringValue:[[note userInfo] objectForKey:@"StatusString"]];
899 }
900
901 - (void)setupWindow
902 {
903     ITDebugLog(@"Loading Preferences.nib.");
904     if (![NSBundle loadNibNamed:@"Preferences" owner:self]) {
905         ITDebugLog(@"Failed to load Preferences.nib.");
906         NSBeep();
907         return;
908     }
909 }
910
911 - (void)setupCustomizationTables
912 {
913     NSImageCell *imgCell = [[[NSImageCell alloc] initImageCell:nil] autorelease];
914     ITDebugLog(@"Setting up table views.");
915     // Set the table view cells up
916     [imgCell setImageScaling:NSScaleNone];
917     [[menuTableView tableColumnWithIdentifier:@"submenu"] setDataCell:imgCell];
918     [[allTableView tableColumnWithIdentifier:@"submenu"] setDataCell:imgCell];
919
920     // Register for drag and drop
921     [menuTableView registerForDraggedTypes:[NSArray arrayWithObjects:
922         @"MenuTableViewPboardType",
923         @"AllTableViewPboardType",
924         nil]];
925     [allTableView registerForDraggedTypes:[NSArray arrayWithObjects:
926         @"MenuTableViewPboardType",
927         @"AllTableViewPboardType",
928         nil]];
929 }
930
931 - (void)setupMenuItems
932 {
933     NSEnumerator *itemEnum;
934     id            anItem;
935     ITDebugLog(@"Setting up table view arrays.");
936     // Set the list of items you can have.
937     availableItems = [[NSMutableArray alloc] initWithObjects:
938         @"separator",
939         @"trackInfo",
940         @"upcomingSongs",
941         @"playlists",
942         @"artists",
943         @"albums",
944         @"eqPresets",
945         @"songRating",
946         @"playPause",
947         @"nextTrack",
948         @"prevTrack",
949         @"fastForward",
950         @"rewind",
951         @"showPlayer",
952                 @"about",
953         @"quit",
954         nil];
955     
956     // Get our preferred menu
957     myItems = [[df arrayForKey:@"menu"] mutableCopy];
958     
959     // Delete items in the availableItems array that are already part of the menu
960     itemEnum = [myItems objectEnumerator];
961     while ( (anItem = [itemEnum nextObject]) ) {
962         if (![anItem isEqualToString:@"separator"]) {
963             [availableItems removeObject:anItem];
964         }
965     }
966     
967     // Items that show should a submenu image
968     submenuItems = [[NSArray alloc] initWithObjects:
969         @"upcomingSongs",
970         @"playlists",
971         @"artists",
972         @"albums",
973         @"eqPresets",
974         @"songRating",
975         nil];
976 }
977
978 - (void)setupUI
979 {
980     NSEnumerator   *keyArrayEnum;
981     NSString       *serverName;
982     NSData         *colorData;
983     int selectedBGStyle;
984     id anItem;
985         
986         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioscrobblerStatusChanged:) name:@"AudioscrobblerStatusChanged" object:nil];
987         if ([df boolForKey:@"audioscrobblerEnabled"]) {
988                 NSString *status = [[AudioscrobblerController sharedController] lastStatus];
989                 [audioscrobblerStatusTextField setStringValue:(status == nil) ? @"Idle" : status];
990         }
991         
992     [df setInteger:MT_CURRENT_VERSION forKey:@"appVersion"];
993     
994     ITDebugLog(@"Setting up preferences UI.");
995     // Fill in the number of songs in advance to show field
996     [songsInAdvance setIntValue:[df integerForKey:@"SongsInAdvance"]];
997     
998     // Fill hot key array
999     keyArrayEnum = [hotKeysArray objectEnumerator];
1000     
1001     while ( (anItem = [keyArrayEnum nextObject]) ) {
1002         if ([df objectForKey:anItem]) {
1003             ITDebugLog(@"Setting up \"%@\" hot key.", anItem);
1004             [hotKeysDictionary setObject:[ITKeyCombo keyComboWithPlistRepresentation:[df objectForKey:anItem]] forKey:anItem];
1005         } else {
1006             [hotKeysDictionary setObject:[ITKeyCombo clearKeyCombo] forKey:anItem];
1007         }
1008     }
1009     
1010         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setupScreenPopup) name:NSApplicationDidChangeScreenParametersNotification object:nil];
1011         [self setupScreenPopup];
1012         
1013     ITDebugLog(@"Setting up track info checkboxes.");
1014     // Check current track info buttons
1015     [albumCheckbox setState:[df boolForKey:@"showAlbum"] ? NSOnState : NSOffState];
1016     [nameCheckbox setState:NSOnState];  // Song info will ALWAYS show song title.
1017     [nameCheckbox setEnabled:NO];  // Song info will ALWAYS show song title.
1018     [artistCheckbox setState:[df boolForKey:@"showArtist"] ? NSOnState : NSOffState];
1019     [composerCheckbox setState:[df boolForKey:@"showComposer"] ? NSOnState : NSOffState];
1020     [trackTimeCheckbox setState:[df boolForKey:@"showTime"] ? NSOnState : NSOffState];
1021     [trackNumberCheckbox setState:[df boolForKey:@"showTrackNumber"] ? NSOnState : NSOffState];
1022     [playCountCheckbox setState:[df boolForKey:@"showPlayCount"] ? NSOnState : NSOffState];
1023     [ratingCheckbox setState:[df boolForKey:@"showTrackRating"] ? NSOnState : NSOffState];
1024     [albumArtworkCheckbox setState:[df boolForKey:@"showAlbumArtwork"] ? NSOnState : NSOffState];
1025     
1026     if ([df boolForKey:@"runScripts"]) {
1027         [runScriptsCheckbox setState:NSOnState];
1028         [showScriptsButton setEnabled:YES];
1029     } else {
1030         [showScriptsButton setEnabled:NO];
1031     }
1032     
1033     // Set the launch at login checkbox state
1034     ITDebugLog(@"Setting launch at login state.");
1035     if (ITDoesApplicationLaunchOnLogin([[NSBundle mainBundle] bundlePath])) {
1036         [launchAtLoginCheckbox setState:NSOnState];
1037     }
1038     
1039     // Set the launch player checkbox state
1040     ITDebugLog(@"Setting launch player with MenuTunes state.");
1041     [launchPlayerAtLaunchCheckbox setState:[df boolForKey:@"LaunchPlayerWithMT"] ? NSOnState : NSOffState];
1042     
1043     // Setup the positioning controls
1044     [positionMatrix selectCellAtRow:[df integerForKey:@"statusWindowVerticalPosition"]
1045                              column:[df integerForKey:@"statusWindowHorizontalPosition"]];
1046     
1047     // Setup effects controls
1048     // Populate the effects popups
1049     [appearanceEffectPopup setAutoenablesItems:NO];
1050     [vanishEffectPopup     setAutoenablesItems:NO];
1051     [self repopulateEffectPopupsForVerticalPosition:[df integerForKey:@"statusWindowVerticalPosition"] 
1052                                  horizontalPosition:[df integerForKey:@"statusWindowHorizontalPosition"]];
1053     
1054     // Attempt to find the pref'd effect in the list.
1055     // If it's not there, use cut/dissolve.
1056     if ( [effectClasses containsObject:NSClassFromString([df stringForKey:@"statusWindowAppearanceEffect"])] ) {
1057         [appearanceEffectPopup selectItemAtIndex:[effectClasses indexOfObject:NSClassFromString([df stringForKey:@"statusWindowAppearanceEffect"])]];
1058     } else {
1059         [appearanceEffectPopup selectItemAtIndex:[effectClasses indexOfObject:NSClassFromString(@"ITCutWindowEffect")]];
1060     }
1061     
1062     if ( [effectClasses containsObject:NSClassFromString([df stringForKey:@"statusWindowVanishEffect"])] ) {
1063         [vanishEffectPopup selectItemAtIndex:[effectClasses indexOfObject:NSClassFromString([df stringForKey:@"statusWindowVanishEffect"])]];
1064     } else {
1065         [vanishEffectPopup selectItemAtIndex:[effectClasses indexOfObject:NSClassFromString(@"ITCutWindowEffect")]];
1066     }
1067     
1068     [appearanceSpeedSlider setFloatValue:( -([df floatForKey:@"statusWindowAppearanceSpeed"]) )];
1069     [vanishSpeedSlider     setFloatValue:( -([df floatForKey:@"statusWindowVanishSpeed"]) )];
1070     [vanishDelaySlider     setFloatValue:[df floatForKey:@"statusWindowVanishDelay"]];
1071
1072     // Setup General Controls
1073     selectedBGStyle = [df integerForKey:@"statusWindowBackgroundMode"];
1074     [backgroundStylePopup selectItem:[backgroundStylePopup itemAtIndex:[backgroundStylePopup indexOfItemWithTag:selectedBGStyle]]];
1075
1076     if ( selectedBGStyle == ITTSWBackgroundColored ) {
1077         [backgroundColorWell  setEnabled:YES];
1078         [backgroundColorPopup setEnabled:YES];
1079     } else {
1080         [backgroundColorWell  setEnabled:NO];
1081         [backgroundColorPopup setEnabled:NO];
1082     }
1083
1084     colorData = [df dataForKey:@"statusWindowBackgroundColor"];
1085
1086     if ( colorData ) {
1087         [backgroundColorWell setColor:(NSColor *)[NSUnarchiver unarchiveObjectWithData:colorData]];
1088     } else {
1089         [backgroundColorWell setColor:[NSColor blueColor]];
1090     }
1091     
1092     [showOnChangeCheckbox setState:([df boolForKey:@"showSongInfoOnChange"] ? NSOnState : NSOffState)];
1093     
1094     [windowSizingPopup selectItem:[windowSizingPopup itemAtIndex:[windowSizingPopup indexOfItemWithTag:[df integerForKey:@"statusWindowSizing"]]]];
1095
1096     // Setup the sharing controls
1097     if ([df boolForKey:@"enableSharing"]) {
1098         [shareMenuTunesCheckbox setState:NSOnState];
1099         [useSharedMenuTunesCheckbox setEnabled:NO];
1100         [selectSharedPlayerButton setEnabled:NO];
1101         [passwordTextField setEnabled:YES];
1102         [nameTextField setEnabled:YES];
1103     } else if ([df boolForKey:@"useSharedPlayer"]) {
1104         [useSharedMenuTunesCheckbox setState:NSOnState];
1105         [shareMenuTunesCheckbox setEnabled:NO];
1106         [selectSharedPlayerButton setEnabled:YES];
1107     }
1108     
1109         //Setup the Audioscrobbler controls
1110         if ([df boolForKey:@"audioscrobblerEnabled"]) {
1111                 [audioscrobblerEnabledCheckbox setState:NSOnState];
1112                 [audioscrobblerUserTextField setEnabled:YES];
1113                 [audioscrobblerPasswordTextField setEnabled:YES];
1114                 [audioscrobblerUseCacheCheckbox setEnabled:YES];
1115         } else {
1116                 [audioscrobblerUserTextField setEnabled:NO];
1117                 [audioscrobblerPasswordTextField setEnabled:NO];
1118                 [audioscrobblerUseCacheCheckbox setEnabled:NO];
1119         }
1120         NSString *audioscrobblerUser = [df stringForKey:@"audioscrobblerUser"];
1121         if (audioscrobblerUser != nil && [audioscrobblerUser length] > 0 && [PreferencesController keychainItemExistsForUser:audioscrobblerUser]) {
1122                 NSString *password = [PreferencesController getKeychainItemPasswordForUser:audioscrobblerUser];
1123                 [audioscrobblerUserTextField setStringValue:audioscrobblerUser];
1124                 if (password != nil) {
1125                         [audioscrobblerPasswordTextField setStringValue:password];
1126                 }
1127         }
1128         [audioscrobblerUseCacheCheckbox setState:[df boolForKey:@"audioscrobblerCacheSubmissions"]];
1129         
1130     [[NSNotificationCenter defaultCenter] addObserver:sharingTableView selector:@selector(reloadData) name:@"ITMTFoundNetService" object:nil];
1131     
1132     serverName = [df stringForKey:@"sharedPlayerName"];
1133     if (!serverName || [serverName length] == 0) {
1134         serverName = @"MenuTunes Shared Player";
1135     }
1136     [nameTextField setStringValue:serverName];
1137     
1138     [selectPlayerBox setContentView:zeroConfView];
1139     if ([[df dataForKey:@"sharedPlayerPassword"] length] > 0) {
1140         [passwordTextField setStringValue:@"p4s5w0rdMT1.2"];
1141     } else {
1142         [passwordTextField setStringValue:@""];
1143     }
1144     if ([df stringForKey:@"sharedPlayerHost"]) {
1145         [hostTextField setStringValue:[df stringForKey:@"sharedPlayerHost"]];
1146     }
1147     
1148     if ([[NetworkController sharedController] isConnectedToServer]) {
1149         [selectedPlayerTextField setStringValue:[[[NetworkController sharedController] networkObject] serverName]];
1150         [locationTextField setStringValue:[[NetworkController sharedController] remoteHost]];
1151     } else {
1152         [selectedPlayerTextField setStringValue:@"No shared player selected."];
1153         [locationTextField setStringValue:@"-"];
1154     }
1155 }
1156
1157 - (void)setupScreenPopup
1158 {
1159         ITDebugLog(@"Setting up screen popup");
1160         NSArray *screens = [NSScreen screens];
1161         if ([screens count] > 1) {
1162                 int i, index = [df integerForKey:@"statusWindowScreenIndex"];
1163                 [screenPopup setEnabled:YES];
1164                 for (i = 0; i < [screens count]; i++) {
1165                         NSScreen *screen = [screens objectAtIndex:i];
1166                         if (![screen isEqual:[NSScreen mainScreen]]) {
1167                                 [screenPopup addItemWithTitle:[NSString stringWithFormat:@"Screen %i", i + 1]];
1168                         }
1169                 }
1170                 [screenPopup selectItemAtIndex:index];
1171                 [[StatusWindow sharedWindow] setScreen:[[NSScreen screens] objectAtIndex:index]];
1172         } else {
1173                 while ([screenPopup numberOfItems] > 1) {
1174                         [screenPopup removeItemAtIndex:1];
1175                 }
1176                 [screenPopup setEnabled:NO];
1177                 [[StatusWindow sharedWindow] setScreen:[NSScreen mainScreen]];
1178         }
1179 }
1180
1181 - (void)setStatusWindowEntryEffect:(Class)effectClass
1182 {
1183     StatusWindow *sw = (StatusWindow *)[StatusWindow sharedWindow];
1184     
1185     float time = ([df floatForKey:@"statusWindowAppearanceSpeed"] ? [df floatForKey:@"statusWindowAppearanceSpeed"] : 0.8);
1186     [df setObject:NSStringFromClass(effectClass) forKey:@"statusWindowAppearanceEffect"];
1187     
1188     [sw setEntryEffect:[[[effectClass alloc] initWithWindow:sw] autorelease]];
1189     [[sw entryEffect] setEffectTime:time];
1190 }
1191
1192 - (void)setStatusWindowExitEffect:(Class)effectClass
1193 {
1194     StatusWindow *sw = (StatusWindow *)[StatusWindow sharedWindow];
1195     
1196     float time = ([df floatForKey:@"statusWindowVanishSpeed"] ? [df floatForKey:@"statusWindowVanishSpeed"] : 0.8);
1197     [df setObject:NSStringFromClass(effectClass) forKey:@"statusWindowVanishEffect"];
1198     
1199     [sw setExitEffect:[[[effectClass alloc] initWithWindow:sw] autorelease]];
1200     [[sw exitEffect] setEffectTime:time];
1201 }
1202
1203 - (void)setCustomColor:(NSColor *)color updateWell:(BOOL)update
1204 {
1205     [(ITTSWBackgroundView *)[[StatusWindow sharedWindow] contentView] setBackgroundColor:color];
1206     [df setObject:[NSArchiver archivedDataWithRootObject:color] forKey:@"statusWindowBackgroundColor"];
1207     
1208     if ( update ) {
1209         [backgroundColorWell setColor:color];
1210     }
1211 }
1212
1213 - (void)repopulateEffectPopupsForVerticalPosition:(ITVerticalWindowPosition)vPos horizontalPosition:(ITHorizontalWindowPosition)hPos
1214 {
1215     NSEnumerator *effectEnum = [effectClasses objectEnumerator];
1216     id anItem;
1217     
1218     [appearanceEffectPopup removeAllItems];
1219     [vanishEffectPopup     removeAllItems];
1220     
1221     while ( (anItem = [effectEnum nextObject]) ) {
1222         [appearanceEffectPopup addItemWithTitle:[anItem effectName]];
1223         [vanishEffectPopup     addItemWithTitle:[anItem effectName]];
1224         
1225         [[appearanceEffectPopup lastItem] setRepresentedObject:anItem];
1226         [[vanishEffectPopup     lastItem] setRepresentedObject:anItem];
1227         
1228         if ( [self effect:anItem supportsVerticalPosition:vPos withHorizontalPosition:hPos] ) {
1229             [[appearanceEffectPopup lastItem] setEnabled:YES];
1230             [[vanishEffectPopup     lastItem] setEnabled:YES];
1231         } else {
1232             [[appearanceEffectPopup lastItem] setEnabled:NO];
1233             [[vanishEffectPopup     lastItem] setEnabled:NO];
1234         }
1235     }
1236     
1237 }
1238
1239 - (BOOL)effect:(Class)effectClass supportsVerticalPosition:(ITVerticalWindowPosition)vPos withHorizontalPosition:(ITHorizontalWindowPosition)hPos
1240 {
1241     BOOL valid = NO;
1242     
1243     if ( vPos == ITWindowPositionTop ) {
1244         if ( hPos == ITWindowPositionLeft ) {
1245             valid = ( [[[[effectClass supportedPositions] objectForKey:@"Top"] objectForKey:@"Left"] boolValue] ) ;
1246         } else if ( hPos == ITWindowPositionCenter ) {
1247             valid = ( [[[[effectClass supportedPositions] objectForKey:@"Top"] objectForKey:@"Center"] boolValue] );
1248         } else if ( hPos == ITWindowPositionRight ) {
1249             valid = ( [[[[effectClass supportedPositions] objectForKey:@"Top"] objectForKey:@"Right"] boolValue] );
1250         }
1251     } else if ( vPos == ITWindowPositionMiddle ) {
1252         if ( hPos == ITWindowPositionLeft ) {
1253             valid = ( [[[[effectClass supportedPositions] objectForKey:@"Middle"] objectForKey:@"Left"] boolValue] );
1254         } else if ( hPos == ITWindowPositionCenter ) {
1255             valid = ( [[[[effectClass supportedPositions] objectForKey:@"Middle"] objectForKey:@"Center"] boolValue] );
1256         } else if ( hPos == ITWindowPositionRight ) {
1257             valid = ( [[[[effectClass supportedPositions] objectForKey:@"Middle"] objectForKey:@"Right"] boolValue] );
1258         }
1259     } else if ( vPos == ITWindowPositionBottom ) {
1260         if ( hPos == ITWindowPositionLeft ) {
1261             valid = ( [[[[effectClass supportedPositions] objectForKey:@"Bottom"] objectForKey:@"Left"] boolValue] );
1262         } else if ( hPos == ITWindowPositionCenter ) {
1263             valid = ( [[[[effectClass supportedPositions] objectForKey:@"Bottom"] objectForKey:@"Center"] boolValue] );
1264         } else if ( hPos == ITWindowPositionRight ) {
1265             valid = ( [[[[effectClass supportedPositions] objectForKey:@"Bottom"] objectForKey:@"Right"] boolValue] );
1266         }
1267     }
1268     
1269     return valid;
1270 }
1271
1272 - (IBAction)changeMenus:(id)sender
1273 {
1274     ITDebugLog(@"Synchronizing menus");
1275     [df setObject:myItems forKey:@"menu"];
1276     [df synchronize];
1277     
1278     [[controller menuController] performSelector:@selector(rebuildSubmenus) withObject:nil afterDelay:0.0];
1279     
1280     //If we're connected over a network, refresh the menu immediately
1281     if ([[NetworkController sharedController] isConnectedToServer]) {
1282         [controller timerUpdate];
1283     }
1284 }
1285
1286
1287 /*************************************************************************/
1288 #pragma mark -
1289 #pragma mark NSWindow DELEGATE METHODS
1290 /*************************************************************************/
1291
1292 - (void)windowWillClose:(NSNotification *)note
1293 {
1294     [(MainController *)controller closePreferences]; 
1295 }
1296
1297 /*************************************************************************/
1298 #pragma mark -
1299 #pragma mark NSTextField DELEGATE METHODS
1300 /*************************************************************************/
1301
1302 - (void)controlTextDidChange:(NSNotification*)note
1303 {
1304     if ([note object] == hostTextField) {
1305         if ([[hostTextField stringValue] length] == 0) {
1306             [sharingPanelOKButton setEnabled:NO];
1307         } else {
1308             [sharingPanelOKButton setEnabled:YES];
1309         }
1310     }
1311 }
1312
1313 /*************************************************************************/
1314 #pragma mark -
1315 #pragma mark NSTableView DATASOURCE METHODS
1316 /*************************************************************************/
1317
1318 - (int)numberOfRowsInTableView:(NSTableView *)aTableView
1319 {
1320     if (aTableView == menuTableView) {
1321         return [myItems count];
1322     } else if (aTableView == allTableView) {
1323         return [availableItems count];
1324     } else if (aTableView == hotKeysTableView) {
1325         return [hotKeysArray count];
1326     } else {
1327         return [[[NetworkController sharedController] remoteServices] count];
1328     }
1329 }
1330
1331 - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
1332 {
1333     if (aTableView == menuTableView) {
1334         NSString *object = [myItems objectAtIndex:rowIndex];
1335         if ([[aTableColumn identifier] isEqualToString:@"name"]) {
1336             if ([object isEqualToString:@"showPlayer"]) {
1337                 NSString *string = nil;
1338                 NS_DURING
1339                     string = [NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"show", @"Show"), [[controller currentRemote] playerSimpleName]];
1340                 NS_HANDLER
1341                     [controller networkError:localException];
1342                 NS_ENDHANDLER
1343                 return string;
1344             }
1345             return NSLocalizedString(object, @"ERROR");
1346         } else {
1347             if ([submenuItems containsObject:object])
1348             {
1349                 return [NSImage imageNamed:@"submenu"];
1350             } else {
1351                 return nil;
1352             }
1353         }
1354     } else if (aTableView == allTableView) {
1355         NSString *object = [availableItems objectAtIndex:rowIndex];
1356         if ([[aTableColumn identifier] isEqualToString:@"name"]) {
1357             if ([object isEqualToString:@"showPlayer"]) {
1358                 NSString *string = nil;
1359                 NS_DURING
1360                     string = [NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"show", @"Show"), [[controller currentRemote] playerSimpleName]];
1361                 NS_HANDLER
1362                     [controller networkError:localException];
1363                 NS_ENDHANDLER
1364                 return string;
1365             }
1366             return NSLocalizedString(object, @"ERROR");
1367         } else {
1368             if ([submenuItems containsObject:object]) {
1369                 return [NSImage imageNamed:@"submenu"];
1370             } else {
1371                 return nil;
1372             }
1373         }
1374     } else if (aTableView == hotKeysTableView) {
1375         if ([[aTableColumn identifier] isEqualToString:@"name"]) {
1376             return [hotKeyNamesArray objectAtIndex:rowIndex];
1377         } else {
1378             return [[hotKeysDictionary objectForKey:[hotKeysArray objectAtIndex:rowIndex]] description];
1379         }
1380     } else {
1381         return [[[[NetworkController sharedController] remoteServices] objectAtIndex:rowIndex] name];
1382     }
1383 }
1384
1385 - (BOOL)tableView:(NSTableView *)tableView writeRows:(NSArray*)rows toPasteboard:(NSPasteboard*)pboard
1386 {
1387     if (tableView == menuTableView) {
1388         [pboard declareTypes:[NSArray arrayWithObjects:@"MenuTableViewPboardType", nil] owner:self];
1389         [pboard setString:[[rows objectAtIndex:0] stringValue] forType:@"MenuTableViewPboardType"];
1390         return YES;
1391     }
1392     
1393     if (tableView == allTableView) {
1394         [pboard declareTypes:[NSArray arrayWithObjects:@"AllTableViewPboardType", nil] owner:self];
1395         [pboard setString:[[rows objectAtIndex:0] stringValue] forType:@"AllTableViewPboardType"];
1396         return YES;
1397     }
1398     return NO;
1399 }
1400
1401 - (BOOL)tableView:(NSTableView*)tableView acceptDrop:(id <NSDraggingInfo>)info row:(int)row dropOperation:(NSTableViewDropOperation)operation
1402 {
1403     NSPasteboard *pb;
1404     int dragRow;
1405     NSString *dragData, *temp;
1406     
1407     pb = [info draggingPasteboard];
1408     
1409     if ([[pb types] containsObject:@"MenuTableViewPboardType"]) {
1410         dragData = [pb stringForType:@"MenuTableViewPboardType"];
1411         dragRow = [dragData intValue];
1412         temp = [myItems objectAtIndex:dragRow];
1413         
1414         if (tableView == menuTableView) {
1415             [myItems insertObject:temp atIndex:row];
1416             if (row > dragRow) {
1417                 [myItems removeObjectAtIndex:dragRow];
1418             } else {
1419                 [myItems removeObjectAtIndex:dragRow + 1];
1420             }
1421         } else if (tableView == allTableView) {
1422             if (![temp isEqualToString:@"separator"]) {
1423                 [availableItems addObject:temp];
1424             }
1425             [myItems removeObjectAtIndex:dragRow];
1426         }
1427     } else if ([[pb types] containsObject:@"AllTableViewPboardType"]) {
1428         dragData = [pb stringForType:@"AllTableViewPboardType"];
1429         dragRow = [dragData intValue];
1430         temp = [availableItems objectAtIndex:dragRow];
1431         
1432         [myItems insertObject:temp atIndex:row];
1433         
1434         if (![temp isEqualToString:@"separator"]) {
1435             [availableItems removeObjectAtIndex:dragRow];
1436         }
1437     }
1438     
1439     [menuTableView reloadData];
1440     [allTableView reloadData];
1441     [self changeMenus:self];
1442     return YES;
1443 }
1444
1445 - (NSDragOperation)tableView:(NSTableView*)tableView validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row proposedDropOperation:(NSTableViewDropOperation)operation
1446 {
1447     if (tableView == allTableView) {
1448         if ([[[info draggingPasteboard] types] containsObject:@"AllTableViewPboardType"]) {
1449             return NSDragOperationNone;
1450         }
1451         
1452         if ([[[info draggingPasteboard] types] containsObject:@"MenuTableViewPboardType"]) {
1453             NSString *item = [myItems objectAtIndex:[[[info draggingPasteboard] stringForType:@"MenuTableViewPboardType"] intValue]];
1454             if ([item isEqualToString:@"preferences"] || [item isEqualToString:@"quit"]) {
1455                 return NSDragOperationNone;
1456             }
1457         }
1458         
1459         [tableView setDropRow:-1 dropOperation:NSTableViewDropOn];
1460         return NSDragOperationGeneric;
1461     }
1462     
1463     if (operation == NSTableViewDropOn || row == -1)
1464     {
1465         return NSDragOperationNone;
1466     }
1467     return NSDragOperationGeneric;
1468 }
1469
1470
1471 /*************************************************************************/
1472 #pragma mark -
1473 #pragma mark DEALLOCATION METHODS
1474 /*************************************************************************/
1475
1476 - (void)dealloc
1477 {
1478         [[NSNotificationCenter defaultCenter] removeObserver:self];
1479     [hotKeysArray release];
1480     [hotKeysDictionary release];
1481     [effectClasses release];
1482     [menuTableView setDataSource:nil];
1483     [allTableView setDataSource:nil];
1484     [controller release];
1485     [availableItems release];
1486     [submenuItems release];
1487     [myItems release];
1488     [df release];
1489         [super dealloc];
1490 }
1491
1492 @end