Merged [21073]: When encoding an icon to JPEG, start off at 100% quality. If that...
[adiumx.git] / Source / AIPreferenceController.m
blob5a4d234cfd8b26725457ad310b272bf22c3f57b3
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "AIPreferenceController.h"
19 #import <Adium/AIContactControllerProtocol.h>
20 #import <Adium/AILoginControllerProtocol.h>
21 #import <Adium/AIToolbarControllerProtocol.h>
23 #import "AIPreferenceWindowController.h"
24 #import <AIUtilities/AIDictionaryAdditions.h>
25 #import <AIUtilities/AIFileManagerAdditions.h>
26 #import <AIUtilities/AIStringAdditions.h>
27 #import <AIUtilities/AIToolbarUtilities.h>
28 #import <AIUtilities/AIImageAdditions.h>
29 #import <Adium/AIListObject.h>
30 #import "AIPreferencePane.h"
31 #import "AIAdvancedPreferencePane.h"
33 #define PREFS_DEFAULT_PREFS     @"PrefsPrefs.plist"
34 #define TITLE_OPEN_PREFERENCES  AILocalizedString(@"Open Preferences",nil)
36 @interface AIPreferenceController (PRIVATE)
37 - (NSDictionary *)cachedDefaultsForGroup:(NSString *)group object:(AIListObject *)object;
38 - (NSDictionary *)cachedPreferencesWithDefaultsForGroup:(NSString *)group object:(AIListObject *)object;
39 - (NSMutableDictionary *)cachedPreferencesForGroup:(NSString *)group object:(AIListObject *)object;
41 - (void)updatePreferences:(NSMutableDictionary *)prefDict forKey:(NSString *)key group:(NSString *)group object:(AIListObject *)object;
42 @end
44 /*!
45  * @class AIPreferenceController
46  * @brief Preference Controller
47  *
48  * Handles loading and saving preferences, default preferences, and preference changed notifications
49  */
50 @implementation AIPreferenceController
52 /*!
53  * @brief Initialize
54  */
55 - (id)init
57         if ((self = [super init])) {
58                 //
59                 paneArray = [[NSMutableArray alloc] init];
60                 advancedPaneArray = [[NSMutableArray alloc] init];
62                 defaults = [[NSMutableDictionary alloc] init];
63                 prefCache = [[NSMutableDictionary alloc] init];
64                 prefWithDefaultsCache = [[NSMutableDictionary alloc] init];
65                 
66                 objectDefaults = [[NSMutableDictionary alloc] init];
67                 objectPrefCache = [[NSMutableDictionary alloc] init];
68                 objectPrefWithDefaultsCache = [[NSMutableDictionary alloc] init];
69                 
70                 observers = [[NSMutableDictionary alloc] init];
71                 delayedNotificationGroups = [[NSMutableSet alloc] init];
72                 preferenceChangeDelays = 0;
73         }
74         
75         return self;
78 /*!
79  * @brief Finish initialization
80  *
81  * Sets up the toolbar items.
82  * We can't do these in initing, since the toolbar controller hasn't loaded yet at that point.
83  */
84 - (void)controllerDidLoad
86         //
87         userDirectory = [[[adium loginController] userDirectory] retain];
88         
89     //Create the 'ByObject' and 'Accounts' object specific preference directory
90         [[NSFileManager defaultManager] createDirectoriesForPath:[userDirectory stringByAppendingPathComponent:OBJECT_PREFS_PATH]];
91         [[NSFileManager defaultManager] createDirectoriesForPath:[userDirectory stringByAppendingPathComponent:ACCOUNT_PREFS_PATH]];
92         
93         //Register our default preferences
94     [self registerDefaults:[NSDictionary dictionaryNamed:PREFS_DEFAULT_PREFS forClass:[self class]] forGroup:PREF_GROUP_GENERAL];
97 /*!
98  * @brief Close
99  */
100 - (void)controllerWillClose
102     //Preferences are (always) saved as they're modified, so there's no need to save them here.
106  * @brief Deallocate
107  */
108 - (void)dealloc
110     [delayedNotificationGroups release]; delayedNotificationGroups = nil;
111     [paneArray release]; paneArray = nil;
112     [prefCache release]; prefCache = nil;
113         [objectPrefCache release]; objectPrefCache = nil;
114     [super dealloc];
119 //Preference Window ----------------------------------------------------------------------------------------------------
120 #pragma mark Preference Window
122  * @brief Show the preference window
123  */
124 - (IBAction)showPreferenceWindow:(id)sender
126         [AIPreferenceWindowController openPreferenceWindow];
129 - (IBAction)closePreferenceWindow:(id)sender
131         [AIPreferenceWindowController closePreferenceWindow];
135  * @brief Show a specific category of the preference window
137  * Opens the preference window if necessary
139  * @param category The category to show
140  */
141 - (void)openPreferencesToCategoryWithIdentifier:(NSString *)identifier
143         [AIPreferenceWindowController openPreferenceWindowToCategoryWithIdentifier:identifier];
147  * @brief Add a view to the preferences
148  */
149 - (void)addPreferencePane:(AIPreferencePane *)inPane
151     [paneArray addObject:inPane];
155  * @brief Returns all currently available preference panes
156  */
157 - (NSArray *)paneArray
159     return paneArray;
163 * @brief Add a view to the preferences
164  */
165 - (void)addAdvancedPreferencePane:(AIAdvancedPreferencePane *)inPane
167     [advancedPaneArray addObject:inPane];
170 - (NSArray *)advancedPaneArray
172         return advancedPaneArray;
175 //Observing ------------------------------------------------------------------------------------------------------------
176 #pragma mark Observing
178  * @brief Register a preference observer
180  * The preference observer will be notified when preferences in group change and passed the preference dictionary for that group
181  * The observer must implement:
182  *              - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
184  */
185 - (void)registerPreferenceObserver:(id)observer forGroup:(NSString *)group
187         NSMutableSet    *groupObservers;
188         
189         NSParameterAssert([observer respondsToSelector:@selector(preferencesChangedForGroup:key:object:preferenceDict:firstTime:)]);
190         
191         //Fetch the observers for this group
192         if (!(groupObservers = [observers objectForKey:group])) {
193                 groupObservers = [[NSMutableSet alloc] init];
194                 [observers setObject:groupObservers forKey:group];
195                 [groupObservers release];
196         }
198         //Add our new observer
199         [groupObservers addObject:[NSValue valueWithNonretainedObject:observer]];
200         
201         //Blanket change notification for initialization
202         [observer preferencesChangedForGroup:group
203                                                                          key:nil
204                                                                   object:nil
205                                                   preferenceDict:[self cachedPreferencesWithDefaultsForGroup:group object:nil]
206                                                            firstTime:YES];
210  * @brief Unregister a preference observer
211  */
212 - (void)unregisterPreferenceObserver:(id)observer
214         NSEnumerator    *enumerator = [observers objectEnumerator];
215         NSMutableArray  *observerArray;
216         NSValue                 *observerValue = [NSValue valueWithNonretainedObject:observer];
218         while ((observerArray = [enumerator nextObject])) {
219                 [observerArray removeObject:observerValue];
220         }
224  * @brief Broadcast a key changed notification.  
226  * Broadcasts a group changed notification if key is nil.
228  * If notifications are delayed, remember the group that changed and broadcast this notification when the delay is
229  * lifted instead of immediately. Currently, our delayed notification system isn't setup to handle object-specific 
230  * preferences, so always notify if there is an object present for now.
232  * @param key The key
233  * @param group The group
234  * @param object The object, or nil if global
235  */
236 - (void)informObserversOfChangedKey:(NSString *)key inGroup:(NSString *)group object:(AIListObject *)object
238         if (!object && preferenceChangeDelays > 0) {
239         [delayedNotificationGroups addObject:group];
240     } else {
241                 NSDictionary    *preferenceDict = [self cachedPreferencesWithDefaultsForGroup:group object:object];
242                 NSEnumerator    *enumerator = [[observers objectForKey:group] objectEnumerator];
243                 NSValue                 *observerValue;
245                 while ((observerValue = [enumerator nextObject])) {
246                         id observer = [observerValue nonretainedObjectValue];
248                         [observer preferencesChangedForGroup:group
249                                                                                          key:key
250                                                                                   object:object
251                                                                   preferenceDict:preferenceDict
252                                                                            firstTime:NO];
253                 }
254     }
258  * @brief Set if preference changed notifications should be delayed
260  * Changing large amounts of preferences at once causes a lot of notification overhead. This should be used like
261  * [lockFocus] / [unlockFocus] around groups of preference changes to improve performance.
262  */
263 - (void)delayPreferenceChangedNotifications:(BOOL)inDelay
265         if (inDelay) {
266                 preferenceChangeDelays++;
267         } else {
268                 preferenceChangeDelays--;
269         }
270         
271         //If changes are no longer delayed, save and notify of all preferences modified while delayed
272     if (!preferenceChangeDelays) {
273                 NSEnumerator    *enumerator = [delayedNotificationGroups objectEnumerator];
274                 NSString        *group;
275                 
276                 [[adium contactController] delayListObjectNotifications];
278                 while ((group = [enumerator nextObject])) {
279                         [self informObserversOfChangedKey:nil inGroup:group object:nil];
280                 }
282                 [[adium contactController] endListObjectNotificationsDelay];
283                 
284                 [delayedNotificationGroups removeAllObjects];
285     }
288     
289 //Setting Preferences -------------------------------------------------------------------
290 #pragma mark Setting Preferences
292  * @brief Set a global preference
294  * Set and save a preference at the global level.
296  * @param value The preference, which must be plist-encodable
297  * @param key An arbitrary NSString key
298  * @param group An arbitrary NSString group
299  */
300 - (void)setPreference:(id)value forKey:(NSString *)key group:(NSString *)group{
301         [self setPreference:value forKey:key group:group object:nil];
305 * @brief Set multiple preferences at once
307  * @param inPrefDict An NSDictionary whose keys are preference keys and objects are the preferences for those keys. All must be plist-encodable.
308  * @param group An arbitrary NSString group
309  */
310 - (void)setPreferences:(NSDictionary *)inPrefDict inGroup:(NSString *)group object:(AIListObject *)object
312         NSMutableDictionary     *prefDict = [self cachedPreferencesForGroup:group object:object];
313         
314         [prefDict addEntriesFromDictionary:inPrefDict];
315         [self updatePreferences:prefDict forKey:nil group:group object:object];
319  * @brief Set multiple global preferences at once
321  * @param inPrefDict An NSDictionary whose keys are preference keys and objects are the preferences for those keys. All must be plist-encodable.
322  * @param group An arbitrary NSString group
323  */
324 - (void)setPreferences:(NSDictionary *)inPrefDict inGroup:(NSString *)group
326         [self setPreferences:inPrefDict inGroup:group object:nil];
330  * @brief Set a global or object-specific preference
332  * Set and save a preference.  This should not be called directly from plugins or components.  To set an object-specific
333  * preference, use the appropriate method on the object. To set a global preference, use setPreference:forKey:group:
334  */
335 - (void)setPreference:(id)value
336                            forKey:(NSString *)key
337                                 group:(NSString *)group
338                            object:(AIListObject *)object
340         NSMutableDictionary     *prefDict = [self cachedPreferencesForGroup:group object:object];
341         BOOL                            changed = YES;
343     //Set the new value
344     if (value != nil) {
345         [prefDict setObject:value forKey:key];
346     } else {
347                 if ([prefDict objectForKey:key]) {
348                         [prefDict removeObjectForKey:key];
349                 } else {
350                         changed = NO;
351                 }
352     }
354         //Update the preference cache with our changes
355         if (changed) {
356                 [self updatePreferences:prefDict forKey:key group:group object:object];
357         }
361 //Retrieving Preferences ----------------------------------------------------------------
362 #pragma mark Retrieving Preferences
364  * @brief Retrieve a preference
365  */
366 - (id)preferenceForKey:(NSString *)key group:(NSString *)group
368         return [self preferenceForKey:key group:group objectIgnoringInheritance:nil];
372  * @brief Retrieve an object specific preference with inheritance, ignoring defaults
374  * Should only be used within AIPreferenceController. See preferenceForKey:group:object: for details.
375  */
376 - (id)_noDefaultsPreferenceForKey:(NSString *)key group:(NSString *)group object:(AIListObject *)object
378         id      result = [[self cachedPreferencesForGroup:group object:object] objectForKey:key];
379         
380         //If there is no object specific preference, inherit the value from the object containing this one
381         if (!result && object) {
382                 return [self _noDefaultsPreferenceForKey:key group:group object:[object containingObject]];
383         } else {
384                 //If we have no object (either were passed no object initially or got here recursively) use defaults if necessary
385                 return result;
386         }
390  * @brief Retrieve an object specific default preference with inheritance
391  */
392 - (id)defaultPreferenceForKey:(NSString *)key group:(NSString *)group object:(AIListObject *)object
394         //Don't use the defaults initially
395         id      result = [[self cachedDefaultsForGroup:group object:object] objectForKey:key];
396         
397         //If there is no object specific preference, inherit the value from the object containing this one
398         if (!result && object) {
399                 return [self defaultPreferenceForKey:key group:group object:[object containingObject]];
400         } else {
401                 //If we have no object (either were passed no object initially or got here recursively) use defaults if necessary
402                 return result;
403         }       
407  * @brief Retrieve an object specific preference with inheritance.
409  * Objects inherit from their containing objects, up to the global preference.  If this entire tree has no preference
410  * defaults are searched starting with the object and continuing up to global.
411  */
412 - (id)preferenceForKey:(NSString *)key group:(NSString *)group object:(AIListObject *)object
414         //Don't use the defaults initially
415         id result = [self _noDefaultsPreferenceForKey:key group:group object:object];
416         
417         //If no result, try defaults
418         if (!result) result = [self defaultPreferenceForKey:key group:group object:object];
419         
420         return result;
424  * @brief Retrieve an object specific preference ignoring inheritance.
426  * If object is nil, this returns the global preference.  Uses defaults only for the specified preference level,
427  * not inherited defaults, as expected.
428  */
429 - (id)preferenceForKey:(NSString *)key group:(NSString *)group objectIgnoringInheritance:(AIListObject *)object
431         //We are ignoring inheritance, so we can ignore inherited defaults, too, and use the cachedPreferencesWithDefaultsForGroup:object: dict
432         id result = [[self cachedPreferencesWithDefaultsForGroup:group object:object] objectForKey:key];
433         
434         return result;
438  * @brief Retrieve all the preferences in a group
440  * @result A dictionary of preferences for the group, including default values as appropriate
441  */
442 - (NSDictionary *)preferencesForGroup:(NSString *)group
444     return [self cachedPreferencesWithDefaultsForGroup:group object:nil];
447 //Defaults -------------------------------------------------------------------------------------------------------------
448 #pragma mark Defaults
450  * @brief Register a dictionary of defaults.
451  */
452 - (void)registerDefaults:(NSDictionary *)defaultDict forGroup:(NSString *)group{
453         [self registerDefaults:defaultDict forGroup:group object:nil];
457  * @brief Register a dictionary of object-specific defaults.
458  */
459 - (void)registerDefaults:(NSDictionary *)defaultDict forGroup:(NSString *)group object:(AIListObject *)object
461         NSMutableDictionary     *targetDefaultsDict;
462         NSMutableDictionary     *activeDefaultsCache;
463         NSMutableDictionary *actualDefaultsDict;
464         NSString                        *cacheKey;      
465         
466         //Object specific preferences are stored by path and objectID, while regular preferences are stored by group.
467         if (object) {
468                 cacheKey = [object preferencesCacheKey];
469                 activeDefaultsCache = objectPrefWithDefaultsCache;
470                 targetDefaultsDict = objectDefaults;
471                 
472         } else {
473                 cacheKey = group;
474                 activeDefaultsCache = prefWithDefaultsCache;
475                 targetDefaultsDict = defaults;
476                 
477         }
478         
479         actualDefaultsDict = [targetDefaultsDict objectForKey:cacheKey];
480         if (!actualDefaultsDict) actualDefaultsDict = [NSMutableDictionary dictionary];
481         
482         [actualDefaultsDict addEntriesFromDictionary:defaultDict];
483         [targetDefaultsDict setObject:actualDefaultsDict
484                                                    forKey:cacheKey];
486         //Now clear our current prefWithDefaults cache so it will be regenerated with these entries included on next call
487         [activeDefaultsCache removeObjectForKey:cacheKey];
491 //Preference Cache -----------------------------------------------------------------------------------------------------
492 //We cache the preferences locally to avoid loading them each time we need a value
493 #pragma mark Preference Cache
495  * @brief Fetch cached preferences
497  * @param group The group
498  * @param object The object, or nil for global
499  */
500 - (NSMutableDictionary *)cachedPreferencesForGroup:(NSString *)group object:(AIListObject *)object
502         NSMutableDictionary     *prefDict;
503         
504         //Object specific preferences are stored by path and objectID, while regular preferences are stored by group.
505         if (object) {
506                 NSString        *cacheKey = [object preferencesCacheKey];
507                 
508                 if (!(prefDict = [objectPrefCache objectForKey:cacheKey])) {
509                         prefDict = [NSMutableDictionary dictionaryAtPath:[userDirectory stringByAppendingPathComponent:[object pathToPreferences]]
510                                                                                                         withName:[[object internalObjectID] safeFilenameString]
511                                                                                                           create:YES];
512                         [objectPrefCache setObject:prefDict forKey:cacheKey];
513                 }
515         } else {
516                 if (!(prefDict = [prefCache objectForKey:group])) {
517                         prefDict = [NSMutableDictionary dictionaryAtPath:userDirectory
518                                                                                                         withName:group
519                                                                                                           create:YES];
520                         [prefCache setObject:prefDict forKey:group];
521                 }
522         }
523         
524         return prefDict;
528  * @brief Return just the defaults for a specified group and object
530  * @param group The group
531  * @param object The object, or nil for global defaults
532  */
533 - (NSDictionary *)cachedDefaultsForGroup:(NSString *)group object:(AIListObject *)object
535         NSDictionary            *sourceDefaultsDict;
536         NSString                        *cacheKey;
538         //Object specific preferences are stored by path and objectID, while regular preferences are stored by group.
539         if (object) {
540                 cacheKey = [object preferencesCacheKey];
541                 sourceDefaultsDict = objectDefaults;
542                 
543         } else {
544                 cacheKey = group;
545                 sourceDefaultsDict = defaults;
546         }
547         
548         return [sourceDefaultsDict objectForKey:cacheKey];
552  * @brief Locally update our cached prefrences, including defaults
554  * Must be called before preferences are accessed after preferences change for changes to be visible to the rest of Adium
555  */
556 - (NSDictionary *)updateCachedPreferencesWithDefaultsForGroup:(NSString *)group object:(AIListObject *)object
558         NSDictionary            *prefWithDefaultsDict;
559         NSMutableDictionary     *activeDefaultsCache;
560         NSDictionary            *sourceDefaultsDict;
561         NSString                        *cacheKey;
563         //Object specific preferences are stored by path and objectID, while regular preferences are stored by group.
564         if (object) {
565                 cacheKey = [object preferencesCacheKey];
566                 activeDefaultsCache = objectPrefWithDefaultsCache;
567                 sourceDefaultsDict = objectDefaults;
568                 
569         } else {
570                 cacheKey = group;
571                 activeDefaultsCache = prefWithDefaultsCache;
572                 sourceDefaultsDict = defaults;
573         }
575         NSDictionary    *userPrefs = [self cachedPreferencesForGroup:group object:object];
576         NSDictionary    *defaultPrefs = [sourceDefaultsDict objectForKey:cacheKey];
577         if (defaultPrefs) {
578                 //Add the object's own preferences to the defaults dictionary to get a dict with the object's keys
579                 //overriding the default keys
580                 prefWithDefaultsDict = [[defaultPrefs mutableCopy] autorelease];
581                 [(NSMutableDictionary *)prefWithDefaultsDict addEntriesFromDictionary:userPrefs];
582         } else {
583                 //With no defaults, just use the userPrefs
584                 prefWithDefaultsDict = userPrefs;
585         }
587         NSMutableDictionary     *existingDict;
588         
589         if (!(existingDict = [activeDefaultsCache objectForKey:cacheKey])) {
590                 existingDict = [NSMutableDictionary dictionary];
591                 [activeDefaultsCache setObject:existingDict forKey:cacheKey];
592         }
593         
594         [existingDict setDictionary:prefWithDefaultsDict];
595         return existingDict;
599  * @brief Return the result of taking the defaults and superceding them with any set preferences
601  * @param group The group
602  * @param object The object, or nil for global
603  */
604 - (NSDictionary *)cachedPreferencesWithDefaultsForGroup:(NSString *)group object:(AIListObject *)object
606         NSDictionary            *prefWithDefaultsDict;
607         NSMutableDictionary     *activeDefaultsCache;
608         NSDictionary            *sourceDefaultsDict;
609         NSString                        *cacheKey;
610         
611         //Object specific preferences are stored by path and objectID, while regular preferences are stored by group.
612         if (object) {
613                 cacheKey = [object preferencesCacheKey];
614                 activeDefaultsCache = objectPrefWithDefaultsCache;
615                 sourceDefaultsDict = objectDefaults;
617         } else {
618                 cacheKey = group;
619                 activeDefaultsCache = prefWithDefaultsCache;
620                 sourceDefaultsDict = defaults;
621         }
622         
623         if (!(prefWithDefaultsDict = [activeDefaultsCache objectForKey:cacheKey])) {
624                 prefWithDefaultsDict = [self updateCachedPreferencesWithDefaultsForGroup:group object:object];
625         }
627         return prefWithDefaultsDict;
631  * @brief Write preference changes back to the cache and to disk
633  * @param prefDict The user-specified preferences (not including defaults)
634  * @param key The key that changed, or nil if multiple keys changed
635  * @param group The group
636  * @param object The object, or nil for global
637  */
638 - (void)updatePreferences:(NSMutableDictionary *)prefDict forKey:(NSString *)key group:(NSString *)group object:(AIListObject *)object
640         //Upgrade code; message context isn't needed now that we draw from logs. This should be removed eventually.
641         [prefDict removeObjectForKey:@"Message Context"];
642         
643         NSString        *path = (object ? [userDirectory stringByAppendingPathComponent:[object pathToPreferences]] : userDirectory);
644         NSString        *name = (object ? [[object internalObjectID] safeFilenameString] : group);
646         //Update our cache
647         [self updateCachedPreferencesWithDefaultsForGroup:group object:object];
648         
649         //Now inform observers
650         [self informObserversOfChangedKey:key inGroup:group object:object];
652         //Save the preference change immediately (Probably not the best idea?)
653         [prefDict writeToPath:path
654                                  withName:name];
657 //Default download locaiton --------------------------------------------------------------------------------------------
658 #pragma mark Default download location
660  * @brief Get the default download location
662  * This will use an Adium-specific preference if set, or the systemwide download location if not
664  * @result A full path to the download location
665  */
666 - (NSString *)userPreferredDownloadFolder
668         NSString        *userPreferredDownloadFolder;
669         
670         userPreferredDownloadFolder = [self preferenceForKey:@"UserPreferredDownloadFolder"
671                                                                                                    group:PREF_GROUP_GENERAL];
672         
673         if (!userPreferredDownloadFolder) {
674                 OSStatus                err = noErr;
675                 ICInstance              inst = NULL;
676                 ICFileSpec              folder;
677                 long                    length = kICFileSpecHeaderSize;
678                 FSRef                   ref;
679                 char                    path[1024];
680                 
681                 memset( path, 0, 1024 ); //clear path's memory range
682                 
683                 if ((err = ICStart(&inst, 'AdiM')) == noErr) {
684                         ICGetPref( inst, kICDownloadFolder, NULL, &folder, &length );
685                         ICStop( inst );
686                         
687                         if (((err = FSpMakeFSRef(&folder.fss, &ref)) == noErr) &&
688                            ((err = FSRefMakePath(&ref, (unsigned char *)path, 1024)) == noErr) &&
689                            ((path != NULL) && (strlen(path) > 0))) {
690                                 userPreferredDownloadFolder = [NSString stringWithUTF8String:path];
691                         }
692                 }
693         }
694         
695         if (!userPreferredDownloadFolder) {
696                 userPreferredDownloadFolder = @"~/Desktop";
697         }
699         userPreferredDownloadFolder = [userPreferredDownloadFolder stringByExpandingTildeInPath];
700         
701         /* If we can't write to the specified folder, fall back to the desktop and then to the home directory;
702          * if neither are writable the user has worse problems then an IM download to worry about.
703          */
704         if (![[NSFileManager defaultManager] isWritableFileAtPath:userPreferredDownloadFolder]) {
705                 NSString *originalFolder = userPreferredDownloadFolder;
707                 userPreferredDownloadFolder = [@"~/Desktop" stringByExpandingTildeInPath];
709                 if (![[NSFileManager defaultManager] isWritableFileAtPath:userPreferredDownloadFolder]) {
710                         userPreferredDownloadFolder = NSHomeDirectory();
711                 }
713                 NSLog(@"Could not obtain write access for %@; defaulting to %@",
714                           originalFolder,
715                           userPreferredDownloadFolder);
716         }
718         return userPreferredDownloadFolder;
722  * @brief Set the location Adium should use for saving files
724  * @param A path to an existing folder
725  */
726 - (void)setUserPreferredDownloadFolder:(NSString *)path
728         [self setPreference:[path stringByAbbreviatingWithTildeInPath]
729                                  forKey:@"UserPreferredDownloadFolder"
730                                   group:PREF_GROUP_GENERAL];
733 #pragma mark KVC
735 + (BOOL) accessInstanceVariablesDirectly {
736         return NO;
739 - (id) valueForKey:(NSString *)key {
740         return [self cachedPreferencesWithDefaultsForGroup:key object:nil];
744  * @brief Set a dictionary of preferences for a group
746  * Note that while setPreferences:inGroup: adds the passed dictionary to the current one, this method replaces the dictionary entirely
748  * @param value An NSDictionary which reprsents an entire group of preferences (without defaults)
749  * @param key The group name
750  */
751 - (void) setValue:(id)value forKey:(NSString *)key {
752         NSRange prefixRange = [key rangeOfString:@"Group:" options:NSLiteralSearch | NSAnchoredSearch];
753         if(prefixRange.location == 0) {
754                 key = [key substringFromIndex:prefixRange.length + 1];
755         } else {
756                 prefixRange = [key rangeOfString:@"ByObject:" options:NSLiteralSearch | NSAnchoredSearch];
757                  if(prefixRange.location == 0) {
758                         NSAssert(NO, @"ByObject is not yet supported in AIPreferenceController KVC methods.");
759 //                      key = [key substringFromIndex:prefixRange.length + 1];
760 #warning XXX ByObject NOT REALLY SUPPORTED YET
761                 }
762         }
764         NSMutableDictionary     *prefDict = [self cachedPreferencesForGroup:key object:nil];
766         //Handy feature: This asserts for us that [value isKindOfClass:[NSDictionary class]].
767         [prefDict setDictionary:value];
769         [self updatePreferences:prefDict forKey:nil group:key object:nil];
772 //- (id) valueForKeyPath:(NSString *)keyPath
773 //We don't need this method. NSObject's version works by calling -valueForKey: successively, which works for us.
775 /* 
776  * Key paths:
777  *              No prefix: Group
778  *              "Group:": Group
779  *              "ByObject" (futar): by-object (objectXyz instead of xyz ivars)
781  * For example, General.MyKey would refer to the MyKey value of the General group, as would Group:General.MyKey
782  */
783 - (void) setValue:(id)value forKeyPath:(NSString *)keyPath {
784         //NSLog(@"AIPC setting value %@ for key path %@", value, keyPath);
785         unsigned periodIdx = [keyPath rangeOfString:@"." options:NSLiteralSearch].location;
786         NSString *key = [keyPath substringToIndex:periodIdx];
787         if(periodIdx == NSNotFound) {
788                 [self setValue:value forKey:key];
789         } else {
790                 NSRange prefixRange = [key rangeOfString:@"Group:" options:NSLiteralSearch | NSAnchoredSearch];
791                 if(prefixRange.location == 0) {
792                         key = [key substringFromIndex:prefixRange.length + 1];
793                 } else {
794                         prefixRange = [key rangeOfString:@"ByObject:" options:NSLiteralSearch | NSAnchoredSearch];
795                         if(prefixRange.location == 0) {
796                                 NSAssert(NO, @"ByObject is not yet supported in AIPreferenceController KVC methods.");
797 //                              key = [key substringFromIndex:prefixRange.length + 1];
798 #warning XXX ByObject NOT REALLY SUPPORTED YET
799                         }
800                 }
801                 keyPath = [keyPath substringFromIndex:periodIdx + 1];
802                 
803                 //We need the key to do AIPC change notifications.
804                 NSString *keyInGroup;
805                 periodIdx = [keyPath rangeOfString:@"." options:NSLiteralSearch].location;
806                 if (periodIdx == NSNotFound) {
807                         keyInGroup = keyPath;
808                 } else {
809                         keyInGroup = [keyPath substringToIndex:periodIdx];
810                 }
812                 //NSLog(@"key path: %@; first key: %@; second key: %@", keyPath, key, keyInGroup);
813                 //Change the value.
814                 NSMutableDictionary *prefDict = [self cachedPreferencesForGroup:key object:nil];
816                 [self willChangeValueForKey:key];
817                 [prefDict setValue:value forKeyPath:keyPath];
818                 [self updatePreferences:prefDict forKey:keyInGroup group:key object:nil];
819                 [self didChangeValueForKey:key];
820         }
823 @end