William Bowling is sharing code with you

Bitbucket is a code hosting site. Unlimited public and private repositories. Free for small teams.

Don't show this again

wbowling / adium (fork of adium / adium)

Fork of Adium for patches/improvements

Clone this repository (size: 338.7 MB): HTTPS / SSH
hg clone https://bitbucket.org/wbowling/adium
hg clone ssh://hg@bitbucket.org/wbowling/adium

adium / Source / AIPreferenceController.m

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
/* 
 * Adium is the legal property of its developers, whose names are listed in the copyright file included
 * with this source distribution.
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation; either version 2 of the License,
 * or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
 * Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program; if not,
 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#import "AIPreferenceController.h"

#import <Adium/AIContactControllerProtocol.h>
#import <Adium/AIContactObserverManager.h>
#import <Adium/AILoginControllerProtocol.h>
#import <Adium/AIToolbarControllerProtocol.h>

#import "AIPreferenceWindowController.h"
#import <AIUtilities/AIApplicationAdditions.h>
#import <AIUtilities/AIDictionaryAdditions.h>
#import <AIUtilities/AIFileManagerAdditions.h>
#import <AIUtilities/AIStringAdditions.h>
#import <AIUtilities/AIToolbarUtilities.h>
#import <AIUtilities/AIImageAdditions.h>
#import <Adium/AIListObject.h>
#import "AIPreferenceContainer.h"
#import "AIPreferencePane.h"
#import "AIAdvancedPreferencePane.h"


#define TITLE_OPEN_PREFERENCES  AILocalizedString(@"Open Preferences",nil)

#define LOADED_OBJECT_PREFS_KEY @"Loaded individual object & account prefs"
#define PREFS_GROUP                             @"Preferences"

@interface AIPreferenceController ()
- (AIPreferenceContainer *)preferenceContainerForGroup:(NSString *)group object:(AIListObject *)object;
- (void)upgradeToSingleObjectPrefsDictIfNeeded;
@end

/*!
 * @class AIPreferenceController
 * @brief Preference Controller
 *
 * Handles loading and saving preferences, default preferences, and preference changed notifications
 */
@implementation AIPreferenceController

/*!
 * @brief Initialize
 */
- (id)init
{
        if ((self = [super init])) {
                //
                paneArray = [[NSMutableArray alloc] init];
                advancedPaneArray = [[NSMutableArray alloc] init];

                prefCache = [[NSMutableDictionary alloc] init];
                objectPrefCache = [[NSMutableDictionary alloc] init];
                
                observers = [[NSMutableDictionary alloc] init];
                delayedNotificationGroups = [[NSMutableSet alloc] init];
                preferenceChangeDelays = 0;
        }
        
        return self;
}

/*!
 * @brief Finish initialization
 */
- (void)controllerDidLoad
{
        [self upgradeToSingleObjectPrefsDictIfNeeded];
}

/*!
 * @brief Upgrade to a single, monolithic prefs dictionary for all objects
 *
 * Adium 1.2 and below used a separate plist file on disk for each object. This is a nice memory optimization but a nasty performance hit.
 * This code moves all those plists into a single file when first run and is a no-op after that.
 */
- (void)upgradeToSingleObjectPrefsDictIfNeeded
{
        if (![[self preferenceForKey:LOADED_OBJECT_PREFS_KEY group:PREF_GROUP_GENERAL] boolValue]) {
                NSString        *userDirectory = [adium.loginController userDirectory];
                NSMutableDictionary *prefsDict;
                NSString *dir;
                
                NSEnumerator *enumerator;
                NSString *file;

                dir = [userDirectory stringByAppendingPathComponent:OBJECT_PREFS_PATH];
                prefsDict = [NSMutableDictionary dictionary];           
                enumerator = [[NSFileManager defaultManager] enumeratorAtPath:dir];
                while ((file = [enumerator nextObject])) {
                        NSString *name = [file stringByDeletingPathExtension];
                        NSMutableDictionary *thisDict = [NSMutableDictionary dictionaryAtPath:dir
                                                                                                                                                 withName:name
                                                                                                                                                   create:NO];
                        if ([thisDict count]) {
                                [thisDict removeObjectForKey:@"Message Context"];

                                //This was previously written out for every single contact. It's only needed for the exceptions
                                [thisDict removeObjectForKey:@"Last Used Spelling Languge"];
                                //This was previously written out for every single contact. It's only needed for the exceptions
                                [thisDict removeObjectForKey:@"Base Writing Direction"];

                                [prefsDict setObject:thisDict
                                                          forKey:name];
                        }
                }

                [prefsDict asyncWriteToPath:userDirectory
                                          withName:@"ByObjectPrefs"];

                dir = [userDirectory stringByAppendingPathComponent:ACCOUNT_PREFS_PATH];
                prefsDict = [NSMutableDictionary dictionary];           
                enumerator = [[NSFileManager defaultManager] enumeratorAtPath:dir];
                while ((file = [enumerator nextObject])) {
                        NSString *name = [file stringByDeletingPathExtension];
                        NSDictionary *thisDict = [NSDictionary dictionaryAtPath:dir
                                                                                                                   withName:name
                                                                                                                         create:NO];
                        if ([thisDict count]) {
                                [prefsDict setObject:thisDict
                                                          forKey:name];
                        }
                }

                [prefsDict asyncWriteToPath:userDirectory
                                          withName:@"AccountPrefs"];

                [self setPreference:[NSNumber numberWithBool:YES]
                                         forKey:LOADED_OBJECT_PREFS_KEY group:PREF_GROUP_GENERAL];
        }
}

/*!
 * @brief Close
 */
- (void)controllerWillClose
{
        [AIPreferenceContainer preferenceControllerWillClose];
} 

/*!
 * @brief Deallocate
 */
- (void)dealloc
{
    [delayedNotificationGroups release]; delayedNotificationGroups = nil;
    [paneArray release]; paneArray = nil;
    [prefCache release]; prefCache = nil;
        [objectPrefCache release]; objectPrefCache = nil;
    [super dealloc];
}



//Preference Window ----------------------------------------------------------------------------------------------------
#pragma mark Preference Window
/*!
 * @brief Show the preference window
 */
- (IBAction)showPreferenceWindow:(id)sender
{
        [AIPreferenceWindowController openPreferenceWindow];
}

- (IBAction)closePreferenceWindow:(id)sender
{
        [AIPreferenceWindowController closePreferenceWindow];
}

/*!
 * @brief Show a specific category of the preference window
 *
 * Opens the preference window if necessary
 *
 * @param category The category to show
 */
- (void)openPreferencesToCategoryWithIdentifier:(NSString *)identifier
{
        [AIPreferenceWindowController openPreferenceWindowToCategoryWithIdentifier:identifier];
}

/*!
 * @brief Add a view to the preferences
 */
- (void)addPreferencePane:(AIPreferencePane *)inPane
{
    [paneArray addObject:inPane];
}

/*!
 * @brief Add a view to the preferences
 */
- (void)removePreferencePane:(AIPreferencePane *)inPane
{
    [paneArray removeObject:inPane];
}

/*!
 * @brief Returns all currently available preference panes
 */
- (NSArray *)paneArray
{
    return paneArray;
}

/*!
* @brief Add a view to the preferences
 */
- (void)addAdvancedPreferencePane:(AIAdvancedPreferencePane *)inPane
{
    [advancedPaneArray addObject:inPane];
}

- (NSArray *)advancedPaneArray
{
        return advancedPaneArray;
}

//Observing ------------------------------------------------------------------------------------------------------------
#pragma mark Observing
/*!
 * @brief Register a preference observer
 *
 * The preference observer will be notified when preferences in group change and passed the preference dictionary for that group
 * The observer must implement:
 *              - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
 *
 */
- (void)registerPreferenceObserver:(id)observer forGroup:(NSString *)group
{
        NSMutableArray  *groupObservers;
        
        NSParameterAssert([observer respondsToSelector:@selector(preferencesChangedForGroup:key:object:preferenceDict:firstTime:)]);
        
        //Fetch the observers for this group
        if (!(groupObservers = [observers objectForKey:group])) {
                groupObservers = [[NSMutableArray alloc] init];
                [observers setObject:groupObservers forKey:group];
                [groupObservers release];
        }

        //Add our new observer
        [groupObservers addObject:[NSValue valueWithNonretainedObject:observer]];

        //Blanket change notification for initialization
        [observer preferencesChangedForGroup:group
                                                                         key:nil
                                                                  object:nil
                                                  preferenceDict:[[self preferenceContainerForGroup:group object:nil] dictionary]
                                                           firstTime:YES];
}

/*!
 * @brief Unregister a preference observer
 */
- (void)unregisterPreferenceObserver:(id)observer
{
        NSEnumerator    *enumerator = [observers objectEnumerator];
        NSMutableArray  *observerArray;
        NSValue                 *observerValue = [NSValue valueWithNonretainedObject:observer];

        while ((observerArray = [enumerator nextObject])) {
                [observerArray removeObject:observerValue];
        }
}

/*!
 * @brief Broadcast a key changed notification.  
 *
 * Broadcasts a group changed notification if key is nil.
 *
 * If notifications are delayed, remember the group that changed and broadcast this notification when the delay is
 * lifted instead of immediately. Currently, our delayed notification system isn't setup to handle object-specific 
 * preferences, so always notify if there is an object present for now.
 *
 * @param key The key
 * @param group The group
 * @param object The object, or nil if global
 */
- (void)informObserversOfChangedKey:(NSString *)key inGroup:(NSString *)group object:(AIListObject *)object
{
        if (!object && preferenceChangeDelays > 0) {
        [delayedNotificationGroups addObject:group];
    } else {
                NSDictionary    *preferenceDict = [[[self preferenceContainerForGroup:group object:object] dictionary] retain];
                for (NSValue *observerValue in [[[observers objectForKey:group] copy] autorelease]) {
                        id observer = observerValue.nonretainedObjectValue;
                        [observer preferencesChangedForGroup:group
                                                                                         key:key
                                                                                  object:object
                                                                  preferenceDict:preferenceDict
                                                                           firstTime:NO];
                }

                [preferenceDict release];
    }
}

/*!
 * @brief Set if preference changed notifications should be delayed
 *
 * Changing large amounts of preferences at once causes a lot of notification overhead. This should be used like
 * [lockFocus] / [unlockFocus] around groups of preference changes to improve performance.
 */
- (void)delayPreferenceChangedNotifications:(BOOL)inDelay
{
        if (inDelay) {
                preferenceChangeDelays++;
        } else {
                preferenceChangeDelays--;
        }
        
        //If changes are no longer delayed, save and notify of all preferences modified while delayed
    if (!preferenceChangeDelays) {
                NSString        *group;
                
                [[AIContactObserverManager sharedManager] delayListObjectNotifications];

                for (group in delayedNotificationGroups) {
                        [self informObserversOfChangedKey:nil inGroup:group object:nil];
                }

                [[AIContactObserverManager sharedManager] endListObjectNotificationsDelay];
                
                [delayedNotificationGroups removeAllObjects];
    }
}

    
//Setting Preferences -------------------------------------------------------------------
#pragma mark Setting Preferences
/*!
 * @brief Set a global preference
 *
 * Set and save a preference at the global level.
 *
 * @param value The preference, which must be plist-encodable
 * @param key An arbitrary NSString key
 * @param group An arbitrary NSString group
 */
- (void)setPreference:(id)value forKey:(NSString *)key group:(NSString *)group{
        [self setPreference:value forKey:key group:group object:nil];
}

/*!
* @brief Set multiple preferences at once
 *
 * @param inPrefDict An NSDictionary whose keys are preference keys and objects are the preferences for those keys. All must be plist-encodable.
 * @param group An arbitrary NSString group
 */
- (void)setPreferences:(NSDictionary *)inPrefDict inGroup:(NSString *)group object:(AIListObject *)object
{
        AIPreferenceContainer   *prefContainer = [self preferenceContainerForGroup:group object:object];

        [prefContainer setPreferenceChangedNotificationsEnabled:NO];
        [prefContainer setValuesForKeysWithDictionary:inPrefDict];
        [prefContainer setPreferenceChangedNotificationsEnabled:YES];
}

/*!
 * @brief Set multiple global preferences at once
 *
 * @param inPrefDict An NSDictionary whose keys are preference keys and objects are the preferences for those keys. All must be plist-encodable.
 * @param group An arbitrary NSString group
 */
- (void)setPreferences:(NSDictionary *)inPrefDict inGroup:(NSString *)group
{
        [self setPreferences:inPrefDict inGroup:group object:nil];
}

/*!
 * @brief Set a global or object-specific preference
 *
 * Set and save a preference.  This should not be called directly from plugins or components.  To set an object-specific
 * preference, use the appropriate method on the object. To set a global preference, use setPreference:forKey:group:
 */
- (void)setPreference:(id)value
                           forKey:(NSString *)key
                                group:(NSString *)group
                           object:(AIListObject *)object
{
        [[self preferenceContainerForGroup:group object:object] setValue:value forKey:key];
}


//Retrieving Preferences ----------------------------------------------------------------
#pragma mark Retrieving Preferences
/*!
 * @brief Retrieve a preference
 */
- (id)preferenceForKey:(NSString *)key group:(NSString *)group
{
        return [self preferenceForKey:key group:group objectIgnoringInheritance:nil];
}

/*!
 * @brief Retrieve an object specific preference with inheritance, ignoring defaults
 *
 * Should only be used within AIPreferenceController. See preferenceForKey:group:object: for details.
 */
- (id)_noDefaultsPreferenceForKey:(NSString *)key group:(NSString *)group object:(AIListObject *)object
{
        return [[self preferenceContainerForGroup:group object:object] valueForKey:key ignoringDefaults:YES];
}

/*!
 * @brief Retrieve an object specific default preference with inheritance
 */
- (id)defaultPreferenceForKey:(NSString *)key group:(NSString *)group object:(AIListObject *)object
{
        return [[self preferenceContainerForGroup:group object:object] defaultValueForKey:key];
}

/*!
 * @brief Retrieve an object specific preference with inheritance.
 *
 * Objects inherit from their containing objects, up to the global preference.  If this entire tree has no set preference,
 * defaults are searched, starting against with the object and proceeding up to the global defaults.
 */
- (id)preferenceForKey:(NSString *)key group:(NSString *)group object:(AIListObject *)object
{
        //Don't use the defaults initially
        id result = [self _noDefaultsPreferenceForKey:key group:group object:object];
        
        //If no result, try defaults
        if (!result) result = [self defaultPreferenceForKey:key group:group object:object];
        
        return result;
}

/*!
 * @brief Retrieve an object specific preference ignoring inheritance.
 *
 * If object is nil, this returns the global preference.  Uses defaults only for the specified preference level,
 * not inherited defaults, as expected.
 */
- (id)preferenceForKey:(NSString *)key group:(NSString *)group objectIgnoringInheritance:(AIListObject *)object
{
        //We are ignoring inheritance, so we can ignore inherited defaults, too, and use the preferenceContainerForGroup:object: dict
        id result = [[self preferenceContainerForGroup:group object:object] valueForKey:key];
        
        return result;
}

/*!
 * @brief Retrieve all the preferences in a group
 *
 * @result A dictionary of preferences for the group, including default values as appropriate
 */
- (NSDictionary *)preferencesForGroup:(NSString *)group
{
    return [[self preferenceContainerForGroup:group object:nil] dictionary];
}

//Defaults -------------------------------------------------------------------------------------------------------------
#pragma mark Defaults
/*!
 * @brief Register a dictionary of defaults.
 */
- (void)registerDefaults:(NSDictionary *)defaultDict forGroup:(NSString *)group{
        [self registerDefaults:defaultDict forGroup:group object:nil];
}

/*!
 * @brief Register a dictionary of object-specific defaults.
 */
- (void)registerDefaults:(NSDictionary *)defaultDict forGroup:(NSString *)group object:(AIListObject *)object
{
        AIPreferenceContainer   *prefContainer = [self preferenceContainerForGroup:group object:object];

        [prefContainer registerDefaults:defaultDict];
        
        [self informObserversOfChangedKey:nil inGroup:group object:object];
}

#pragma mark Preference Container

/*!
 * @brief Retrieve an AIPreferenceContainer
 *
 * @param group The group
 * @param object The object, or nil for global
 */
- (AIPreferenceContainer *)preferenceContainerForGroup:(NSString *)group object:(AIListObject *)object
{
        AIPreferenceContainer   *prefContainer;
        
        if (object) {
                NSString        *cacheKey = [object.internalObjectID stringByAppendingString:group];
                
                if ((prefContainer = [objectPrefCache objectForKey:cacheKey])) {
                        //Until we access this pref container again, it will be associated with the passed group
                        [prefContainer setGroup:group];

                } else {
                        prefContainer = [AIPreferenceContainer preferenceContainerForGroup:group
                                                                                                                                                object:object];
                        [objectPrefCache setObject:prefContainer forKey:cacheKey];
                }
                
        } else {
                if (!(prefContainer = [prefCache objectForKey:group])) {
                        prefContainer = [AIPreferenceContainer preferenceContainerForGroup:group
                                                                                                                                                object:object];
                        [prefCache setObject:prefContainer forKey:group];
                }
        }
        
        return prefContainer;   
}

//Default download locaiton --------------------------------------------------------------------------------------------
#pragma mark Default download location
/*!
 * @brief Get the default download location
 *
 * This will use an Adium-specific preference if set, or the systemwide download location if not
 *
 * @result A full path to the download location
 */
- (NSString *)userPreferredDownloadFolder
{
        NSString        *userPreferredDownloadFolder;
        
        userPreferredDownloadFolder = [[self preferenceForKey:@"UserPreferredDownloadFolder"
                                                                                                        group:PREF_GROUP_GENERAL] stringByExpandingTildeInPath];
        
        if (!userPreferredDownloadFolder) {
                //10.5: ICGetPref() for kICDownloadFolder is useless
                CFURLRef        urlToDefaultBrowser = NULL;
                
                //Use Safari's preference as a default if it's the default browser and it is set
                if (LSGetApplicationForURL((CFURLRef)[NSURL URLWithString:@"http://google.com"],
                                                                   kLSRolesViewer,
                                                                   NULL /*outAppRef*/,
                                                                   &urlToDefaultBrowser) != kLSApplicationNotFoundErr) {
                        NSString        *defaultBrowserName = nil;
                        
                        defaultBrowserName = [[NSFileManager defaultManager] displayNameAtPath:[(NSURL *)urlToDefaultBrowser path]];
                        
                        if ([defaultBrowserName rangeOfString:@"Safari"].location != NSNotFound) {
                                /* ICGetPref() for kICDownloadFolder returns any previously set preference, not the default ~/Downloads or the current
                                 * Safari setting, in 10.5.0, with Safari the default browser
                                 */
                                CFPropertyListRef safariDownloadsPath = CFPreferencesCopyAppValue(CFSTR("DownloadsPath"),CFSTR("com.apple.Safari"));
                                if (safariDownloadsPath) {
                                        //This should return a CFStringRef... we're using another app's prefs, so make sure.
                                        if (CFGetTypeID(safariDownloadsPath) == CFStringGetTypeID()) {
                                                userPreferredDownloadFolder = (NSString *)safariDownloadsPath;
                                        }
                                        
                                        [(NSObject *)safariDownloadsPath autorelease];
                                }                                       
                        }
                }

                NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSUserDomainMask, YES);
                if ([searchPaths count]) {
                        userPreferredDownloadFolder = [searchPaths objectAtIndex:0];
                }
        }

        /* If we can't write to the specified folder, fall back to the desktop and then to the home directory;
         * if neither are writable the user has worse problems then an IM download to worry about.
         */
        if (![[NSFileManager defaultManager] isWritableFileAtPath:userPreferredDownloadFolder]) {
                NSString *originalFolder = userPreferredDownloadFolder;

                userPreferredDownloadFolder = [NSHomeDirectory() stringByAppendingPathComponent:@"Desktop"];

                if (![[NSFileManager defaultManager] isWritableFileAtPath:userPreferredDownloadFolder]) {
                        userPreferredDownloadFolder = NSHomeDirectory();
                }

                NSLog(@"Could not obtain write access for %@; defaulting to %@",
                          originalFolder,
                          userPreferredDownloadFolder);
        }

        return userPreferredDownloadFolder;
}

/*!
 * @brief Set the location Adium should use for saving files
 *
 * @param A path to an existing folder
 */
- (void)setUserPreferredDownloadFolder:(NSString *)path
{
        [self setPreference:[path stringByAbbreviatingWithTildeInPath]
                                 forKey:@"UserPreferredDownloadFolder"
                                  group:PREF_GROUP_GENERAL];
}

#pragma mark KVC

static void parseKeypath(NSString *keyPath, NSString **outGroup, NSString **outKeyPath, NSString **outInternalObjectID)
{
        NSRange prefixRange = [keyPath rangeOfString:@"Group:" options:NSLiteralSearch | NSAnchoredSearch];
        NSString *groupWithKeyPath = keyPath;
        NSString *group = nil, *finalKeyPath = nil;
        NSString *internalObjectID = nil;
        
        if (prefixRange.location == 0) {
                //Allow a Group: prefix, stripping it out if present.
                groupWithKeyPath = [keyPath substringFromIndex:prefixRange.length];
        } else {
                prefixRange = [keyPath rangeOfString:@"ByObject:" options:(NSLiteralSearch | NSAnchoredSearch)];
                if (prefixRange.location == 0) {                         
                        keyPath = [keyPath substringFromIndex:prefixRange.length];
                        
                        NSRange nextPeriod = [keyPath rangeOfString:@"." 
                                                                                                options:NSLiteralSearch
                                                                                                  range:NSMakeRange(0, [keyPath length])];
                        internalObjectID = [keyPath substringToIndex:nextPeriod.location];
                        groupWithKeyPath = [keyPath substringFromIndex:nextPeriod.location + 1];                        
                }
        }
        
        //We need the key to do AIPC change notifications.
        NSInteger periodIdx = [groupWithKeyPath rangeOfString:@"." options:NSLiteralSearch].location;
        if (periodIdx == NSNotFound) {
                group = groupWithKeyPath;
        } else {
                group = [groupWithKeyPath substringToIndex:periodIdx];
                finalKeyPath = [groupWithKeyPath substringFromIndex:periodIdx + 1];
        }
        
        if (outGroup) *outGroup = group;
        if (outKeyPath) *outKeyPath = finalKeyPath;
        if (outInternalObjectID) *outInternalObjectID = internalObjectID;
}

+ (BOOL) accessInstanceVariablesDirectly {
        return NO;
}

- (void)addObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
        NSUInteger periodIdx = [keyPath rangeOfString:@"." options:NSLiteralSearch].location;
        if(periodIdx == NSNotFound) {
                [super addObserver:anObserver forKeyPath:keyPath options:options context:context];
                
        } else {
                NSString *group, *newKeyPath, *internalObjectID;
                parseKeypath(keyPath, &group, &newKeyPath, &internalObjectID);

                AIPreferenceContainer *prefContainer = [self preferenceContainerForGroup:group
                                                                                                                                                  object:(internalObjectID ? [adium.contactController existingListObjectWithUniqueID:internalObjectID] : nil)];
                [prefContainer addObserver:anObserver forKeyPath:newKeyPath options:options context:context];
        }       
}

- (void)addObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath ofObject:(AIListObject *)listObject options:(NSKeyValueObservingOptions)options context:(void *)context
{
        NSString *group, *newKeyPath, *internalObjectID;
        parseKeypath(keyPath, &group, &newKeyPath, &internalObjectID);

        AIPreferenceContainer *prefContainer = [self preferenceContainerForGroup:group object:listObject];              
        [prefContainer addObserver:anObserver forKeyPath:newKeyPath options:options context:context];
}

- (void)removeObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath
{
        NSUInteger periodIdx = [keyPath rangeOfString:@"." options:NSLiteralSearch].location;
        if(periodIdx == NSNotFound) {
                [super removeObserver:anObserver forKeyPath:keyPath];
                
        } else {
                NSString *group, *newKeyPath, *internalObjectID;
                parseKeypath(keyPath, &group, &newKeyPath, &internalObjectID);
                
                AIPreferenceContainer *prefContainer = [self preferenceContainerForGroup:group
                                                                                                                                                  object:(internalObjectID ? [adium.contactController existingListObjectWithUniqueID:internalObjectID] : nil)];
                [prefContainer removeObserver:anObserver forKeyPath:newKeyPath];
        }       
}

- (id) valueForKey:(NSString *)key {
        return [self preferenceContainerForGroup:key object:nil];
}

- (id) valueForKeyPath:(NSString *)keyPath {
        NSUInteger periodIdx = [keyPath rangeOfString:@"." options:NSLiteralSearch].location;
        if(periodIdx == NSNotFound) {
                return [self valueForKey:keyPath];
                
        } else {
                NSString *group, *newKeyPath, *internalObjectID;
                parseKeypath(keyPath, &group, &newKeyPath, &internalObjectID);

                return [[self preferenceContainerForGroup:group 
                                                                                   object:(internalObjectID ? [adium.contactController existingListObjectWithUniqueID:internalObjectID] : nil)]
                                valueForKeyPath:newKeyPath];
        }
}


/*!
 * @brief Set a dictionary of preferences for a group
 *
 * Note that while setPreferences:inGroup: adds the passed dictionary to the current one, this method replaces the dictionary entirely
 *
 * @param value An NSDictionary which reprsents an entire group of preferences (without defaults)
 * @param key The group name
 */
- (void) setValue:(id)value forKey:(NSString *)key {
        NSString *group = nil;
        NSString *internalObjectID = nil;

        parseKeypath(key, &group, NULL, &internalObjectID);

        [[self preferenceContainerForGroup:group
                                                                object:(internalObjectID ?
                                                                                [adium.contactController existingListObjectWithUniqueID:internalObjectID] :
                                                                                nil)] setPreferences:value];
}

/* 
 * Key paths:
 *              No prefix: Group
 *              "Group:": Group
 *              "ByObject" (futar): by-object (objectXyz instead of xyz ivars)
 *
 * For example, General.MyKey would refer to the MyKey value of the General group, as would Group:General.MyKey
 */
- (void) setValue:(id)value forKeyPath:(NSString *)keyPath {
        NSUInteger periodIdx = [keyPath rangeOfString:@"." options:NSLiteralSearch].location;
        if(periodIdx == NSNotFound) {
                NSString *key = [keyPath substringToIndex:periodIdx];

                [self setValue:value forKey:key];
        } else {
                NSString *group, *newKeyPath, *internalObjectID;
                parseKeypath(keyPath, &group, &newKeyPath, &internalObjectID);
                
                //Change the value.
                AIPreferenceContainer *prefContainer = [self preferenceContainerForGroup:group
                                                                                                                                                  object:(internalObjectID ? [adium.contactController existingListObjectWithUniqueID:internalObjectID] : nil)];
                [prefContainer setValue:value forKeyPath:newKeyPath];
        }
}

@end