]>
Commit | Line | Data |
---|---|---|
224d48e3 | 1 | /* WinterBoard - Theme Manager for the iPhone |
43fd3a0b | 2 | * Copyright (C) 2009-2010 Jay Freeman (saurik) |
224d48e3 JF |
3 | */ |
4 | ||
5 | /* | |
6 | * Redistribution and use in source and binary | |
7 | * forms, with or without modification, are permitted | |
8 | * provided that the following conditions are met: | |
9 | * | |
10 | * 1. Redistributions of source code must retain the | |
11 | * above copyright notice, this list of conditions | |
12 | * and the following disclaimer. | |
13 | * 2. Redistributions in binary form must reproduce the | |
14 | * above copyright notice, this list of conditions | |
15 | * and the following disclaimer in the documentation | |
16 | * and/or other materials provided with the | |
17 | * distribution. | |
18 | * 3. The name of the author may not be used to endorse | |
19 | * or promote products derived from this software | |
20 | * without specific prior written permission. | |
21 | * | |
22 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' | |
23 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, | |
24 | * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF | |
25 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
26 | * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE | |
27 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
28 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT | |
29 | * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
31 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF | |
32 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR | |
33 | * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN | |
34 | * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF | |
35 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
36 | */ | |
37 | ||
38 | #import <Foundation/Foundation.h> | |
39 | #import <UIKit/UIKit.h> | |
40 | #import <Preferences/PSListController.h> | |
41 | #import <Preferences/PSSpecifier.h> | |
42 | #import <Preferences/PSTableCell.h> | |
43 | #import <UIKit/UINavigationButton.h> | |
44 | ||
7c88a3f1 JF |
45 | #include <dlfcn.h> |
46 | ||
47 | static BOOL (*IsIconHiddenDisplayId)(NSString *); | |
48 | static BOOL (*HideIconViaDisplayId)(NSString *); | |
49 | static BOOL (*UnHideIconViaDisplayId)(NSString *); | |
50 | ||
51 | static const NSString *WinterBoardDisplayID = @"com.saurik.WinterBoard"; | |
52 | ||
224d48e3 JF |
53 | extern NSString *PSTableCellKey; |
54 | extern "C" UIImage *_UIImageWithName(NSString *); | |
55 | ||
56 | static UIImage *checkImage; | |
57 | static UIImage *uncheckedImage; | |
58 | ||
59 | static BOOL settingsChanged; | |
60 | static NSMutableDictionary *_settings; | |
61 | static NSString *_plist; | |
62 | ||
265e19b2 JF |
63 | /* [NSObject yieldToSelector:(withObject:)] {{{*/ |
64 | @interface NSObject (wb$yieldToSelector) | |
65 | - (id) wb$yieldToSelector:(SEL)selector withObject:(id)object; | |
66 | - (id) wb$yieldToSelector:(SEL)selector; | |
67 | @end | |
68 | ||
69 | @implementation NSObject (Cydia) | |
70 | ||
71 | - (void) wb$doNothing { | |
72 | } | |
73 | ||
74 | - (void) wb$_yieldToContext:(NSMutableArray *)context { | |
75 | NSAutoreleasePool *pool([[NSAutoreleasePool alloc] init]); | |
76 | ||
77 | SEL selector(reinterpret_cast<SEL>([[context objectAtIndex:0] pointerValue])); | |
78 | id object([[context objectAtIndex:1] nonretainedObjectValue]); | |
79 | volatile bool &stopped(*reinterpret_cast<bool *>([[context objectAtIndex:2] pointerValue])); | |
80 | ||
81 | /* XXX: deal with exceptions */ | |
82 | id value([self performSelector:selector withObject:object]); | |
83 | ||
84 | NSMethodSignature *signature([self methodSignatureForSelector:selector]); | |
85 | [context removeAllObjects]; | |
86 | if ([signature methodReturnLength] != 0 && value != nil) | |
87 | [context addObject:value]; | |
88 | ||
89 | stopped = true; | |
90 | ||
91 | [self | |
92 | performSelectorOnMainThread:@selector(wb$doNothing) | |
93 | withObject:nil | |
94 | waitUntilDone:NO | |
95 | ]; | |
96 | ||
97 | [pool release]; | |
98 | } | |
99 | ||
100 | - (id) wb$yieldToSelector:(SEL)selector withObject:(id)object { | |
101 | /*return [self performSelector:selector withObject:object];*/ | |
102 | ||
103 | volatile bool stopped(false); | |
104 | ||
105 | NSMutableArray *context([NSMutableArray arrayWithObjects: | |
106 | [NSValue valueWithPointer:selector], | |
107 | [NSValue valueWithNonretainedObject:object], | |
108 | [NSValue valueWithPointer:const_cast<bool *>(&stopped)], | |
109 | nil]); | |
110 | ||
111 | NSThread *thread([[[NSThread alloc] | |
112 | initWithTarget:self | |
113 | selector:@selector(wb$_yieldToContext:) | |
114 | object:context | |
115 | ] autorelease]); | |
116 | ||
117 | [thread start]; | |
118 | ||
119 | NSRunLoop *loop([NSRunLoop currentRunLoop]); | |
120 | NSDate *future([NSDate distantFuture]); | |
121 | ||
122 | while (!stopped && [loop runMode:NSDefaultRunLoopMode beforeDate:future]); | |
123 | ||
124 | return [context count] == 0 ? nil : [context objectAtIndex:0]; | |
125 | } | |
126 | ||
127 | - (id) wb$yieldToSelector:(SEL)selector { | |
128 | return [self wb$yieldToSelector:selector withObject:nil]; | |
129 | } | |
130 | ||
131 | @end | |
132 | /* }}} */ | |
133 | ||
224d48e3 JF |
134 | /* Theme Settings Controller {{{ */ |
135 | @interface WBSThemesController: PSViewController <UITableViewDelegate, UITableViewDataSource> { | |
136 | UITableView *_tableView; | |
137 | NSMutableArray *_themes; | |
138 | } | |
139 | ||
140 | @property (nonatomic, retain) NSMutableArray *themes; | |
141 | ||
142 | + (void) load; | |
143 | ||
144 | - (id) initForContentSize:(CGSize)size; | |
145 | - (id) view; | |
146 | - (id) navigationTitle; | |
147 | - (void) themesChanged; | |
148 | ||
149 | - (int) numberOfSectionsInTableView:(UITableView *)tableView; | |
150 | - (id) tableView:(UITableView *)tableView titleForHeaderInSection:(int)section; | |
151 | - (int) tableView:(UITableView *)tableView numberOfRowsInSection:(int)section; | |
152 | - (id) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; | |
153 | - (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath; | |
154 | - (void) tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; | |
155 | - (UITableViewCellEditingStyle) tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath; | |
156 | - (BOOL) tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath; | |
157 | - (BOOL) tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath; | |
7c88a3f1 | 158 | |
224d48e3 JF |
159 | @end |
160 | ||
161 | @implementation WBSThemesController | |
162 | ||
163 | @synthesize themes = _themes; | |
164 | ||
165 | + (void) load { | |
166 | NSAutoreleasePool *pool([[NSAutoreleasePool alloc] init]); | |
167 | checkImage = [_UIImageWithName(@"UIPreferencesBlueCheck.png") retain]; | |
168 | uncheckedImage = [[UIImage imageWithContentsOfFile:@"/System/Library/PreferenceBundles/WinterBoardSettings.bundle/SearchResultsCheckmarkClear.png"] retain]; | |
169 | [pool release]; | |
170 | } | |
171 | ||
172 | - (id) initForContentSize:(CGSize)size { | |
173 | if ((self = [super initForContentSize:size]) != nil) { | |
174 | self.themes = [_settings objectForKey:@"Themes"]; | |
175 | if (!_themes) { | |
176 | if (NSString *theme = [_settings objectForKey:@"Theme"]) { | |
177 | self.themes = [NSMutableArray arrayWithObject: | |
178 | [NSMutableDictionary dictionaryWithObjectsAndKeys: | |
179 | theme, @"Name", | |
180 | [NSNumber numberWithBool:YES], @"Active", nil]]; | |
181 | [_settings removeObjectForKey:@"Theme"]; | |
182 | } | |
183 | if (!_themes) | |
184 | self.themes = [NSMutableArray array]; | |
185 | [_settings setObject:_themes forKey:@"Themes"]; | |
186 | } | |
187 | ||
188 | NSMutableArray *themesOnDisk([NSMutableArray array]); | |
189 | ||
190 | [themesOnDisk | |
191 | addObjectsFromArray:[[NSFileManager defaultManager] | |
192 | contentsOfDirectoryAtPath:@"/Library/Themes" error:NULL] | |
193 | ]; | |
194 | ||
195 | [themesOnDisk addObjectsFromArray:[[NSFileManager defaultManager] | |
196 | contentsOfDirectoryAtPath:[NSString stringWithFormat:@"%@/Library/SummerBoard/Themes", NSHomeDirectory()] | |
197 | error:NULL | |
198 | ]]; | |
199 | ||
200 | for (int i = 0, count = [themesOnDisk count]; i < count; i++) { | |
201 | NSString *theme = [themesOnDisk objectAtIndex:i]; | |
202 | if ([theme hasSuffix:@".theme"]) | |
203 | [themesOnDisk replaceObjectAtIndex:i withObject:[theme stringByDeletingPathExtension]]; | |
204 | } | |
205 | ||
206 | NSMutableSet *themesSet([NSMutableSet set]); | |
207 | ||
208 | for (int i = 0, count = [_themes count]; i < count; i++) { | |
209 | NSDictionary *theme([_themes objectAtIndex:i]); | |
210 | NSString *name([theme objectForKey:@"Name"]); | |
211 | ||
212 | if (!name || ![themesOnDisk containsObject:name]) { | |
213 | [_themes removeObjectAtIndex:i]; | |
214 | i--; | |
215 | count--; | |
216 | } else { | |
217 | [themesSet addObject:name]; | |
218 | } | |
219 | } | |
220 | ||
221 | for (NSString *theme in themesOnDisk) { | |
222 | if ([themesSet containsObject:theme]) | |
223 | continue; | |
224 | [themesSet addObject:theme]; | |
225 | ||
226 | [_themes insertObject:[NSMutableDictionary dictionaryWithObjectsAndKeys: | |
227 | theme, @"Name", | |
228 | [NSNumber numberWithBool:NO], @"Active", | |
229 | nil] atIndex:0]; | |
230 | } | |
231 | ||
232 | _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 480-64) style:UITableViewStyleGrouped]; | |
233 | [_tableView setDataSource:self]; | |
234 | [_tableView setDelegate:self]; | |
235 | [_tableView setEditing:YES]; | |
236 | [_tableView setAllowsSelectionDuringEditing:YES]; | |
237 | [self showLeftButton:@"WinterBoard" withStyle:1 rightButton:nil withStyle:0]; | |
238 | } | |
239 | return self; | |
240 | } | |
241 | ||
242 | - (void) dealloc { | |
243 | [_tableView release]; | |
244 | [_themes release]; | |
245 | [super dealloc]; | |
246 | } | |
247 | ||
248 | - (id) navigationTitle { | |
249 | return @"Themes"; | |
250 | } | |
251 | ||
252 | - (id) view { | |
253 | return _tableView; | |
254 | } | |
255 | ||
256 | - (void) themesChanged { | |
257 | settingsChanged = YES; | |
258 | } | |
259 | ||
260 | /* UITableViewDelegate / UITableViewDataSource Methods {{{ */ | |
261 | - (int) numberOfSectionsInTableView:(UITableView *)tableView { | |
262 | return 1; | |
263 | } | |
264 | ||
265 | - (id) tableView:(UITableView *)tableView titleForHeaderInSection:(int)section { | |
266 | return nil; | |
267 | } | |
268 | ||
269 | - (int) tableView:(UITableView *)tableView numberOfRowsInSection:(int)section { | |
270 | return _themes.count; | |
271 | } | |
272 | ||
273 | - (id) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | |
274 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ThemeCell"]; | |
275 | if (!cell) { | |
276 | cell = [[[UITableViewCell alloc] initWithFrame:CGRectMake(0, 0, 100, 100) reuseIdentifier:@"ThemeCell"] autorelease]; | |
277 | //[cell setTableViewStyle:UITableViewCellStyleDefault]; | |
278 | } | |
279 | ||
280 | NSDictionary *theme([_themes objectAtIndex:indexPath.row]); | |
281 | cell.text = [theme objectForKey:@"Name"]; | |
282 | cell.hidesAccessoryWhenEditing = NO; | |
283 | NSNumber *active([theme objectForKey:@"Active"]); | |
284 | BOOL inactive(active == nil || ![active boolValue]); | |
285 | [cell setImage:(inactive ? uncheckedImage : checkImage)]; | |
286 | return cell; | |
287 | } | |
288 | ||
289 | - (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | |
290 | UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; | |
291 | NSMutableDictionary *theme = [_themes objectAtIndex:indexPath.row]; | |
292 | NSNumber *active = [theme objectForKey:@"Active"]; | |
293 | BOOL inactive = active == nil || ![active boolValue]; | |
294 | [theme setObject:[NSNumber numberWithBool:inactive] forKey:@"Active"]; | |
295 | [cell setImage:(!inactive ? uncheckedImage : checkImage)]; | |
296 | [tableView deselectRowAtIndexPath:(NSIndexPath *)indexPath animated:YES]; | |
297 | [self themesChanged]; | |
298 | } | |
299 | ||
300 | - (void) tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { | |
301 | NSUInteger fromIndex = [fromIndexPath row]; | |
302 | NSUInteger toIndex = [toIndexPath row]; | |
303 | if (fromIndex == toIndex) | |
304 | return; | |
305 | NSMutableDictionary *theme = [[[_themes objectAtIndex:fromIndex] retain] autorelease]; | |
306 | [_themes removeObjectAtIndex:fromIndex]; | |
307 | [_themes insertObject:theme atIndex:toIndex]; | |
308 | [self themesChanged]; | |
309 | } | |
310 | ||
311 | - (UITableViewCellEditingStyle) tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { | |
312 | return UITableViewCellEditingStyleNone; | |
313 | } | |
314 | ||
315 | - (BOOL) tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath { | |
316 | return NO; | |
317 | } | |
318 | ||
319 | - (BOOL) tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { | |
320 | return YES; | |
321 | } | |
322 | /* }}} */ | |
323 | @end | |
324 | /* }}} */ | |
325 | ||
326 | @interface WBSettingsController: PSListController { | |
327 | } | |
328 | ||
329 | - (id) initForContentSize:(CGSize)size; | |
330 | - (void) dealloc; | |
331 | - (void) suspend; | |
332 | - (void) navigationBarButtonClicked:(int)buttonIndex; | |
333 | - (void) viewWillRedisplay; | |
334 | - (void) pushController:(id)controller; | |
335 | - (id) specifiers; | |
336 | - (void) settingsChanged; | |
337 | - (NSString *) title; | |
338 | - (void) setPreferenceValue:(id)value specifier:(PSSpecifier *)spec; | |
339 | - (id) readPreferenceValue:(PSSpecifier *)spec; | |
340 | ||
341 | @end | |
342 | ||
343 | @implementation WBSettingsController | |
344 | ||
7c88a3f1 | 345 | + (void) load { |
7c88a3f1 | 346 | void *libhide(dlopen("/usr/lib/hide.dylib", RTLD_LAZY)); |
7c88a3f1 | 347 | IsIconHiddenDisplayId = reinterpret_cast<BOOL (*)(NSString *)>(dlsym(libhide, "IsIconHiddenDisplayId")); |
7c88a3f1 | 348 | HideIconViaDisplayId = reinterpret_cast<BOOL (*)(NSString *)>(dlsym(libhide, "HideIconViaDisplayId")); |
7c88a3f1 | 349 | UnHideIconViaDisplayId = reinterpret_cast<BOOL (*)(NSString *)>(dlsym(libhide, "UnHideIconViaDisplayId")); |
7c88a3f1 JF |
350 | } |
351 | ||
224d48e3 JF |
352 | - (id) initForContentSize:(CGSize)size { |
353 | if ((self = [super initForContentSize:size]) != nil) { | |
354 | _plist = [[NSString stringWithFormat:@"%@/Library/Preferences/com.saurik.WinterBoard.plist", NSHomeDirectory()] retain]; | |
355 | _settings = [([NSMutableDictionary dictionaryWithContentsOfFile:_plist] ?: [NSMutableDictionary dictionary]) retain]; | |
7c88a3f1 JF |
356 | |
357 | [_settings setObject:[NSNumber numberWithBool:IsIconHiddenDisplayId(WinterBoardDisplayID)] forKey:@"IconHidden"]; | |
224d48e3 JF |
358 | } return self; |
359 | } | |
360 | ||
361 | - (void) dealloc { | |
362 | [_settings release]; | |
363 | [_plist release]; | |
364 | [super dealloc]; | |
365 | } | |
366 | ||
43fd3a0b | 367 | - (void) __optimizeThemes { |
265e19b2 JF |
368 | system("/usr/libexec/winterboard/Optimize"); |
369 | } | |
370 | ||
371 | - (void) optimizeThemes { | |
43fd3a0b JF |
372 | UIActionSheet *sheet([[[UIActionSheet alloc] |
373 | initWithTitle:@"Optimize Themes" | |
374 | buttons:[NSArray arrayWithObjects:@"Optimize", @"Cancel", nil] | |
375 | defaultButtonIndex:1 | |
376 | delegate:self | |
377 | context:@"optimize" | |
378 | ] autorelease]); | |
379 | ||
380 | [sheet setBodyText:@"Please note that this setting /replaces/ the PNG files that came with the theme. PNG files that have been iPhone-optimized cannot be viewed on a normal computer unless they are first deoptimized. You can use Cydia to reinstall themes that have been optimized in order to revert to the original PNG files."]; | |
381 | [sheet setNumberOfRows:1]; | |
382 | [sheet setDestructiveButtonIndex:0]; | |
383 | [sheet popupAlertAnimated:YES]; | |
384 | } | |
385 | ||
386 | - (void) _optimizeThemes { | |
265e19b2 JF |
387 | UIView *view([self view]); |
388 | UIWindow *window([view window]); | |
389 | ||
390 | UIProgressHUD *hud([[[UIProgressHUD alloc] initWithWindow:window] autorelease]); | |
391 | [hud setText:@"Reticulating Splines\nPlease Wait (Minutes)"]; | |
392 | ||
393 | [window setUserInteractionEnabled:NO]; | |
394 | ||
395 | [window addSubview:hud]; | |
396 | [hud show:YES]; | |
43fd3a0b | 397 | [self wb$yieldToSelector:@selector(__optimizeThemes)]; |
265e19b2 JF |
398 | [hud removeFromSuperview]; |
399 | ||
400 | [window setUserInteractionEnabled:YES]; | |
401 | ||
402 | [self settingsChanged]; | |
403 | } | |
404 | ||
43fd3a0b JF |
405 | - (void) alertSheet:(UIActionSheet *)sheet buttonClicked:(int)button { |
406 | NSString *context([sheet context]); | |
407 | ||
408 | if ([context isEqualToString:@"optimize"]) { | |
409 | switch (button) { | |
410 | case 1: | |
411 | [self performSelector:@selector(_optimizeThemes) withObject:nil afterDelay:0]; | |
412 | break; | |
413 | } | |
414 | ||
415 | [sheet dismiss]; | |
416 | } else | |
417 | [super alertSheet:sheet buttonClicked:button]; | |
418 | ||
419 | } | |
420 | ||
224d48e3 JF |
421 | - (void) suspend { |
422 | if (!settingsChanged) | |
423 | return; | |
424 | ||
425 | NSData *data([NSPropertyListSerialization dataFromPropertyList:_settings format:NSPropertyListBinaryFormat_v1_0 errorDescription:NULL]); | |
426 | if (!data) | |
427 | return; | |
428 | if (![data writeToFile:_plist options:NSAtomicWrite error:NULL]) | |
429 | return; | |
430 | ||
7c88a3f1 JF |
431 | ([[_settings objectForKey:@"IconHidden"] boolValue] ? HideIconViaDisplayId : UnHideIconViaDisplayId)(WinterBoardDisplayID); |
432 | ||
224d48e3 JF |
433 | unlink("/User/Library/Caches/com.apple.springboard-imagecache-icons"); |
434 | unlink("/User/Library/Caches/com.apple.springboard-imagecache-icons.plist"); | |
435 | unlink("/User/Library/Caches/com.apple.springboard-imagecache-smallicons"); | |
436 | unlink("/User/Library/Caches/com.apple.springboard-imagecache-smallicons.plist"); | |
7c88a3f1 JF |
437 | |
438 | system("rm -rf /User/Library/Caches/SpringBoardIconCache"); | |
439 | system("rm -rf /User/Library/Caches/SpringBoardIconCache-small"); | |
440 | ||
224d48e3 JF |
441 | system("killall SpringBoard"); |
442 | } | |
443 | ||
444 | - (void) navigationBarButtonClicked:(int)buttonIndex { | |
445 | if (!settingsChanged) { | |
446 | [super navigationBarButtonClicked:buttonIndex]; | |
447 | return; | |
448 | } | |
449 | ||
450 | if (buttonIndex == 0) | |
451 | settingsChanged = NO; | |
452 | ||
453 | [self suspend]; | |
454 | [self.rootController popController]; | |
455 | } | |
456 | ||
457 | - (void) viewWillRedisplay { | |
458 | if (settingsChanged) | |
459 | [self settingsChanged]; | |
460 | [super viewWillRedisplay]; | |
461 | } | |
462 | ||
463 | - (void) pushController:(id)controller { | |
464 | [self hideNavigationBarButtons]; | |
465 | [super pushController:controller]; | |
466 | } | |
467 | ||
468 | - (id) specifiers { | |
469 | if (!_specifiers) | |
470 | _specifiers = [[self loadSpecifiersFromPlistName:@"WinterBoard" target:self] retain]; | |
471 | return _specifiers; | |
472 | } | |
473 | ||
474 | - (void) settingsChanged { | |
475 | [self showLeftButton:@"Respring" withStyle:2 rightButton:@"Cancel" withStyle:0]; | |
476 | settingsChanged = YES; | |
477 | } | |
478 | ||
479 | - (NSString *) title { | |
480 | return @"WinterBoard"; | |
481 | } | |
482 | ||
483 | - (void) setPreferenceValue:(id)value specifier:(PSSpecifier *)spec { | |
7c88a3f1 | 484 | NSString *key([spec propertyForKey:@"key"]); |
224d48e3 JF |
485 | if ([[spec propertyForKey:@"negate"] boolValue]) |
486 | value = [NSNumber numberWithBool:(![value boolValue])]; | |
7c88a3f1 | 487 | [_settings setValue:value forKey:key]; |
224d48e3 JF |
488 | [self settingsChanged]; |
489 | } | |
490 | ||
491 | - (id) readPreferenceValue:(PSSpecifier *)spec { | |
492 | NSString *key([spec propertyForKey:@"key"]); | |
493 | id defaultValue([spec propertyForKey:@"default"]); | |
494 | id plistValue([_settings objectForKey:key]); | |
495 | if (!plistValue) | |
496 | return defaultValue; | |
497 | if ([[spec propertyForKey:@"negate"] boolValue]) | |
498 | plistValue = [NSNumber numberWithBool:(![plistValue boolValue])]; | |
499 | return plistValue; | |
500 | } | |
501 | ||
502 | @end |