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