1/* xscreensaver, Copyright (c) 2006-2019 Jamie Zawinski <jwz@jwz.org> 2 * 3 * Permission to use, copy, modify, distribute, and sell this software and its 4 * documentation for any purpose is hereby granted without fee, provided that 5 * the above copyright notice appear in all copies and that both that 6 * copyright notice and this permission notice appear in supporting 7 * documentation. No representations are made about the suitability of this 8 * software for any purpose. It is provided "as is" without express or 9 * implied warranty. 10 */ 11 12/* XScreenSaver uses XML files to describe the user interface for configuring 13 the various screen savers. These files live in .../hacks/config/ and 14 say relatively high level things like: "there should be a checkbox 15 labelled "Leave Trails", and when it is checked, add the option '-trails' 16 to the command line when launching the program." 17 18 This code reads that XML and constructs a Cocoa interface from it. 19 The Cocoa controls are hooked up to NSUserDefaultsController to save 20 those settings into the MacOS preferences system. The Cocoa preferences 21 names are the same as the resource names specified in the screenhack's 22 'options' array (we use that array to map the command line switches 23 specified in the XML to the resource names to use). 24 */ 25 26#import "XScreenSaverConfigSheet.h" 27#import "Updater.h" 28 29#import "jwxyz.h" 30#import "InvertedSlider.h" 31 32#ifdef USE_IPHONE 33# define NSView UIView 34# define NSRect CGRect 35# define NSSize CGSize 36# define NSTextField UITextField 37# define NSButton UIButton 38# define NSFont UIFont 39# define NSStepper UIStepper 40# define NSMenuItem UIMenuItem 41# define NSText UILabel 42# define minValue minimumValue 43# define maxValue maximumValue 44# define setMinValue setMinimumValue 45# define setMaxValue setMaximumValue 46# define LABEL UILabel 47#else 48# define LABEL NSTextField 49#endif // USE_IPHONE 50 51#undef LABEL_ABOVE_SLIDER 52#define USE_HTML_LABELS 53 54 55#pragma mark XML Parser 56 57/* I used to use the "NSXMLDocument" XML parser, but that doesn't exist 58 on iOS. The "NSXMLParser" parser exists on both OSX and iOS, so I 59 converted to use that. However, to avoid having to re-write all of 60 the old code, I faked out a halfassed implementation of the 61 "NSXMLNode" class that "NSXMLDocument" used to return. 62 */ 63 64#define NSXMLNode SimpleXMLNode 65#define NSXMLElement SimpleXMLNode 66#define NSXMLCommentKind SimpleXMLCommentKind 67#define NSXMLElementKind SimpleXMLElementKind 68#define NSXMLAttributeKind SimpleXMLAttributeKind 69#define NSXMLTextKind SimpleXMLTextKind 70 71typedef enum { SimpleXMLCommentKind, 72 SimpleXMLElementKind, 73 SimpleXMLAttributeKind, 74 SimpleXMLTextKind, 75} SimpleXMLKind; 76 77@interface SimpleXMLNode : NSObject 78{ 79 SimpleXMLKind kind; 80 NSString *name; 81 SimpleXMLNode *parent; 82 NSMutableArray *children; 83 NSMutableArray *attributes; 84 id object; 85} 86 87@property(nonatomic) SimpleXMLKind kind; 88@property(nonatomic, retain) NSString *name; 89@property(nonatomic, retain) SimpleXMLNode *parent; 90@property(nonatomic, retain) NSMutableArray *children; 91@property(nonatomic, retain) NSMutableArray *attributes; 92@property(nonatomic, retain, getter=objectValue, setter=setObjectValue:) 93 id object; 94 95@end 96 97@implementation SimpleXMLNode 98 99@synthesize kind; 100@synthesize name; 101//@synthesize parent; 102@synthesize children; 103@synthesize attributes; 104@synthesize object; 105 106- (void) dealloc 107{ 108 [name release]; 109 [children release]; 110 //[attributes release]; 111 [object release]; 112 [super dealloc]; 113} 114 115- (id) init 116{ 117 self = [super init]; 118 attributes = [[NSMutableArray alloc] initWithCapacity:10]; 119 return self; 120} 121 122 123- (id) initWithName:(NSString *)n 124{ 125 self = [self init]; 126 [self setKind:NSXMLElementKind]; 127 [self setName:n]; 128 return self; 129} 130 131 132- (void) setAttributesAsDictionary:(NSDictionary *)dict 133{ 134 for (NSString *key in dict) { 135 NSObject *val = [dict objectForKey:key]; 136 SimpleXMLNode *n = [[SimpleXMLNode alloc] init]; 137 [n setKind:SimpleXMLAttributeKind]; 138 [n setName:key]; 139 [n setObjectValue:val]; 140 [attributes addObject:n]; 141 [n release]; 142 } 143} 144 145- (SimpleXMLNode *) parent { return parent; } 146 147- (void) setParent:(SimpleXMLNode *)p 148{ 149 NSAssert (!parent, @"parent already set"); 150 if (!p) return; 151 parent = p; 152 NSMutableArray *kids = [p children]; 153 if (!kids) { 154 kids = [NSMutableArray arrayWithCapacity:10]; 155 [p setChildren:kids]; 156 } 157 [kids addObject:self]; 158} 159@end 160 161 162#pragma mark textMode value transformer 163 164// A value transformer for mapping "url" to "3" and vice versa in the 165// "textMode" preference, since NSMatrix uses NSInteger selectedIndex. 166 167#ifndef USE_IPHONE 168@interface TextModeTransformer: NSValueTransformer {} 169@end 170@implementation TextModeTransformer 171+ (Class)transformedValueClass { return [NSString class]; } 172+ (BOOL)allowsReverseTransformation { return YES; } 173 174- (id)transformedValue:(id)value { 175 if ([value isKindOfClass:[NSString class]]) { 176 int i = -1; 177 if ([value isEqualToString:@"date"]) { i = 0; } 178 else if ([value isEqualToString:@"literal"]) { i = 1; } 179 else if ([value isEqualToString:@"file"]) { i = 2; } 180 else if ([value isEqualToString:@"url"]) { i = 3; } 181 else if ([value isEqualToString:@"program"]) { i = 4; } 182 if (i != -1) 183 value = [NSNumber numberWithInt: i]; 184 } 185 return value; 186} 187 188- (id)reverseTransformedValue:(id)value { 189 if ([value isKindOfClass:[NSNumber class]]) { 190 switch ((int) [value doubleValue]) { 191 case 0: value = @"date"; break; 192 case 1: value = @"literal"; break; 193 case 2: value = @"file"; break; 194 case 3: value = @"url"; break; 195 case 4: value = @"program"; break; 196 } 197 } 198 return value; 199} 200@end 201 202/* Current theory is that the @"value" KVO binding on NSTextFields are 203 sometimes returning nil when they're empty, but meanwhile 204 [NSUserDefaults setObject:forKey:] needs non-nil objects. 205 */ 206@interface NonNilStringTransformer: NSValueTransformer {} 207@end 208@implementation NonNilStringTransformer 209+ (Class)transformedValueClass { return [NSString class]; } 210+ (BOOL)allowsReverseTransformation { return YES; } 211 212- (id)transformedValue:(id)value { 213 return value ? value : @""; 214} 215 216- (id)reverseTransformedValue:(id)value { 217 return value ? value : @""; 218} 219@end 220#endif // !USE_IPHONE 221 222 223#pragma mark Implementing radio buttons 224 225/* The UIPickerView is a hideous and uncustomizable piece of shit. 226 I can't believe Apple actually released that thing on the world. 227 Let's fake up some radio buttons instead. 228 */ 229 230#if defined(USE_IPHONE) && !defined(USE_PICKER_VIEW) 231 232@interface RadioButton : UILabel 233{ 234 int index; 235 NSArray *items; 236} 237 238@property(nonatomic) int index; 239@property(nonatomic, retain) NSArray *items; 240 241@end 242 243@implementation RadioButton 244 245@synthesize index; 246@synthesize items; 247 248- (void)dealloc 249{ 250 [items release]; 251 [super dealloc]; 252} 253 254- (id) initWithIndex:(int)_index items:_items 255{ 256 self = [super initWithFrame:CGRectZero]; 257 index = _index; 258 items = [_items retain]; 259 260 [self setText: [[items objectAtIndex:index] objectAtIndex:0]]; 261 [self setBackgroundColor:[UIColor clearColor]]; 262 [self sizeToFit]; 263 264 return self; 265} 266 267@end 268 269 270# endif // !USE_PICKER_VIEW 271 272 273# pragma mark Implementing labels with clickable links 274 275#if defined(USE_IPHONE) && defined(USE_HTML_LABELS) 276 277@interface HTMLLabel : UIView <UIWebViewDelegate> 278{ 279 NSString *html; 280 UIFont *font; 281 UIWebView *webView; 282} 283 284@property(nonatomic, retain) NSString *html; 285@property(nonatomic, retain) UIWebView *webView; 286 287- (id) initWithHTML:(NSString *)h font:(UIFont *)f; 288- (id) initWithText:(NSString *)t font:(UIFont *)f; 289- (void) setHTML:(NSString *)h; 290- (void) setText:(NSString *)t; 291- (void) sizeToFit; 292 293@end 294 295@implementation HTMLLabel 296 297@synthesize html; 298@synthesize webView; 299 300- (id) initWithHTML:(NSString *)h font:(UIFont *)f 301{ 302 self = [super init]; 303 if (! self) return 0; 304 font = [f retain]; 305 webView = [[UIWebView alloc] init]; 306 webView.delegate = self; 307 webView.dataDetectorTypes = UIDataDetectorTypeNone; 308 self. autoresizingMask = UIViewAutoresizingNone; // we do it manually 309 webView.autoresizingMask = UIViewAutoresizingNone; 310 webView.scrollView.scrollEnabled = NO; 311 webView.scrollView.bounces = NO; 312 webView.opaque = NO; 313 [webView setBackgroundColor:[UIColor clearColor]]; 314 315 [self addSubview: webView]; 316 [self setHTML: h]; 317 return self; 318} 319 320- (id) initWithText:(NSString *)t font:(UIFont *)f 321{ 322 self = [self initWithHTML:@"" font:f]; 323 if (! self) return 0; 324 [self setText: t]; 325 return self; 326} 327 328 329- (void) setHTML: (NSString *)h 330{ 331 if (! h) return; 332 [h retain]; 333 if (html) [html release]; 334 html = h; 335 336 BOOL dark_mode_p = FALSE; 337 { 338 UITraitCollection *t = [self traitCollection]; 339# pragma clang diagnostic push // "only available on iOS 12.0 or newer" 340# pragma clang diagnostic ignored "-Wunguarded-availability-new" 341 if (t && [t respondsToSelector:@selector(userInterfaceStyle)] && 342 [t userInterfaceStyle] == UIUserInterfaceStyleDark) 343 dark_mode_p = TRUE; 344# pragma clang diagnostic pop 345 } 346 347 NSString *h2 = 348 [NSString stringWithFormat: 349 @"<!DOCTYPE HTML PUBLIC " 350 "\"-//W3C//DTD HTML 4.01 Transitional//EN\"" 351 " \"http://www.w3.org/TR/html4/loose.dtd\">" 352 "<HTML>" 353 "<HEAD>" 354// "<META NAME=\"viewport\" CONTENT=\"" 355// "width=device-width" 356// "initial-scale=1.0;" 357// "maximum-scale=1.0;\">" 358 "<STYLE>" 359 "<!--\n" 360 "body {" 361 " margin: 0; padding: 0; border: 0;" 362 " font-family: \"%@\";" 363 " font-size: %.4fpx;" // Must be "px", not "pt"! 364 " line-height: %.4fpx;" // And no spaces before it. 365 " -webkit-text-size-adjust: none;" 366 " color: %@;" 367 "}" 368 " a { color: %@ !important; }" 369 "\n//-->\n" 370 "</STYLE>" 371 "</HEAD>" 372 "<BODY>" 373 "%@" 374 "</BODY>" 375 "</HTML>", 376 // [font fontName], // Returns ".SFUI-Regular", doesn't work. 377 @"Helvetica", // "SanFranciscoDisplay-Regular" also doesn't work. 378 [font pointSize], 379 [font lineHeight], 380 (dark_mode_p ? @"#FFF" : @"#000"), 381 (dark_mode_p ? @"#0DF" : @"#00E"), 382 h]; 383 [webView stopLoading]; 384 [webView loadHTMLString:h2 baseURL:[NSURL URLWithString:@""]]; 385} 386 387 388static char *anchorize (const char *url); 389 390- (void) setText: (NSString *)t 391{ 392 t = [t stringByTrimmingCharactersInSet:[NSCharacterSet 393 whitespaceCharacterSet]]; 394 t = [t stringByReplacingOccurrencesOfString:@"&" withString:@"&"]; 395 t = [t stringByReplacingOccurrencesOfString:@"<" withString:@"<"]; 396 t = [t stringByReplacingOccurrencesOfString:@">" withString:@">"]; 397 t = [t stringByReplacingOccurrencesOfString:@"\n\n" withString:@" <P> "]; 398 t = [t stringByReplacingOccurrencesOfString:@"<P> " 399 withString:@"<P> "]; 400 t = [t stringByReplacingOccurrencesOfString:@"\n " 401 withString:@"<BR> "]; 402 403 NSString *h = @""; 404 for (NSString *s in 405 [t componentsSeparatedByCharactersInSet: 406 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) { 407 if ([s hasPrefix:@"http://"] || 408 [s hasPrefix:@"https://"]) { 409 char *anchor = anchorize ([s cStringUsingEncoding:NSUTF8StringEncoding]); 410 NSString *a2 = [NSString stringWithCString: anchor 411 encoding: NSUTF8StringEncoding]; 412 s = [NSString stringWithFormat: @"<A HREF=\"%@\">%@</A><BR>", s, a2]; 413 free (anchor); 414 } 415 h = [NSString stringWithFormat: @"%@ %@", h, s]; 416 } 417 418 h = [h stringByReplacingOccurrencesOfString:@" <P> " withString:@"<P>"]; 419 h = [h stringByReplacingOccurrencesOfString:@"<BR><P>" withString:@"<P>"]; 420 h = [h stringByTrimmingCharactersInSet:[NSCharacterSet 421 whitespaceAndNewlineCharacterSet]]; 422 423 [self setHTML: h]; 424} 425 426 427-(BOOL) webView:(UIWebView *)wv 428 shouldStartLoadWithRequest:(NSURLRequest *)req 429 navigationType:(UIWebViewNavigationType)type 430{ 431 // Force clicked links to open in Safari, not in this window. 432 if (type == UIWebViewNavigationTypeLinkClicked) { 433 [[UIApplication sharedApplication] openURL:[req URL]]; 434 return NO; 435 } 436 return YES; 437} 438 439 440- (void) setFrame: (CGRect)r 441{ 442 [super setFrame: r]; 443 r.origin.x = 0; 444 r.origin.y = 0; 445 [webView setFrame: r]; 446} 447 448 449- (NSString *) stripTags:(NSString *)str 450{ 451 NSString *result = @""; 452 453 // Add newlines. 454 str = [str stringByReplacingOccurrencesOfString:@"<P>" 455 withString:@"<BR><BR>" 456 options:NSCaseInsensitiveSearch 457 range:NSMakeRange(0, [str length])]; 458 str = [str stringByReplacingOccurrencesOfString:@"<BR>" 459 withString:@"\n" 460 options:NSCaseInsensitiveSearch 461 range:NSMakeRange(0, [str length])]; 462 463 // Remove HREFs. 464 for (NSString *s in [str componentsSeparatedByString: @"<"]) { 465 NSRange r = [s rangeOfString:@">"]; 466 if (r.length > 0) 467 s = [s substringFromIndex: r.location + r.length]; 468 result = [result stringByAppendingString: s]; 469 } 470 471 // Compress internal horizontal whitespace. 472 str = result; 473 result = @""; 474 for (NSString *s in [str componentsSeparatedByCharactersInSet: 475 [NSCharacterSet whitespaceCharacterSet]]) { 476 if ([result length] == 0) 477 result = s; 478 else if ([s length] > 0) 479 result = [NSString stringWithFormat: @"%@ %@", result, s]; 480 } 481 482 return result; 483} 484 485 486- (void) sizeToFit 487{ 488 CGRect r = [self frame]; 489 490 /* It would be sensible to just ask the UIWebView how tall the page is, 491 instead of hoping that NSString and UIWebView measure fonts and do 492 wrapping in exactly the same way, but since UIWebView is asynchronous, 493 we'd have to wait for the document to load first, e.g.: 494 495 - Start the document loading; 496 - return a default height to use for the UITableViewCell; 497 - wait for the webViewDidFinishLoad delegate method to fire; 498 - then force the UITableView to reload, to pick up the new height. 499 500 But I couldn't make that work. 501 */ 502# if 0 503 r.size.height = [[webView 504 stringByEvaluatingJavaScriptFromString: 505 @"document.body.offsetHeight"] 506 doubleValue]; 507# else 508 NSString *text = [self stripTags: html]; 509 CGSize s = r.size; 510 s.height = 999999; 511 s = [text boundingRectWithSize:s 512 options:NSStringDrawingUsesLineFragmentOrigin 513 attributes:@{NSFontAttributeName: font} 514 context:nil].size; 515 r.size.height = s.height; 516# endif 517 518 [self setFrame: r]; 519} 520 521 522- (void) dealloc 523{ 524 [html release]; 525 [font release]; 526 [webView release]; 527 [super dealloc]; 528} 529 530@end 531 532#endif // USE_IPHONE && USE_HTML_LABELS 533 534 535@interface XScreenSaverConfigSheet (Private) 536 537- (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent; 538 539# ifndef USE_IPHONE 540- (void) placeChild: (NSView *)c on:(NSView *)p right:(BOOL)r; 541- (void) placeChild: (NSView *)c on:(NSView *)p; 542static NSView *last_child (NSView *parent); 543static void layout_group (NSView *group, BOOL horiz_p); 544# else // USE_IPHONE 545- (void) placeChild: (NSObject *)c on:(NSView *)p right:(BOOL)r; 546- (void) placeChild: (NSObject *)c on:(NSView *)p; 547- (void) placeSeparator; 548- (void) bindResource:(NSObject *)ctl key:(NSString *)k reload:(BOOL)r; 549- (void) refreshTableView; 550# endif // USE_IPHONE 551 552@end 553 554 555@implementation XScreenSaverConfigSheet 556 557# define LEFT_MARGIN 20 // left edge of window 558# define COLUMN_SPACING 10 // gap between e.g. labels and text fields 559# define LEFT_LABEL_WIDTH 70 // width of all left labels 560# define LINE_SPACING 10 // leading between each line 561 562# define FONT_SIZE 17 // Magic hardcoded UITableView font size. 563 564#pragma mark Talking to the resource database 565 566 567/* Normally we read resources by looking up "KEY" in the database 568 "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone 569 app, everything is stored in the database "org.jwz.xscreensaver" 570 instead, so transform keys to "SAVERNAME.KEY". 571 572 NOTE: This is duplicated in PrefsReader.m, cause I suck. 573 */ 574- (NSString *) makeKey:(NSString *)key 575{ 576# ifdef USE_IPHONE 577 NSString *prefix = [saver_name stringByAppendingString:@"."]; 578 if (! [key hasPrefix:prefix]) // Don't double up! 579 key = [prefix stringByAppendingString:key]; 580# endif 581 return key; 582} 583 584 585- (NSString *) makeCKey:(const char *)key 586{ 587 return [self makeKey:[NSString stringWithCString:key 588 encoding:NSUTF8StringEncoding]]; 589} 590 591 592/* Given a command-line option, returns the corresponding resource name. 593 Any arguments in the switch string are ignored (e.g., "-foo x"). 594 */ 595- (NSString *) switchToResource:(NSString *)cmdline_switch 596 opts:(const XrmOptionDescRec *)opts_array 597 valRet:(NSString **)val_ret 598{ 599 char buf[1280]; 600 char *tail = 0; 601 NSAssert(cmdline_switch, @"cmdline switch is null"); 602 if (! [cmdline_switch getCString:buf maxLength:sizeof(buf) 603 encoding:NSUTF8StringEncoding]) { 604 NSAssert1(0, @"unable to convert %@", cmdline_switch); 605 return 0; 606 } 607 char *s = strpbrk(buf, " \t\r\n"); 608 if (s && *s) { 609 *s = 0; 610 tail = s+1; 611 while (*tail && (*tail == ' ' || *tail == '\t')) 612 tail++; 613 } 614 615 while (opts_array[0].option) { 616 if (!strcmp (opts_array[0].option, buf)) { 617 const char *ret = 0; 618 619 if (opts_array[0].argKind == XrmoptionNoArg) { 620 if (tail && *tail) 621 NSAssert1 (0, @"expected no args to switch: \"%@\"", 622 cmdline_switch); 623 ret = opts_array[0].value; 624 } else { 625 if (!tail || !*tail) 626 NSAssert1 (0, @"expected args to switch: \"%@\"", 627 cmdline_switch); 628 ret = tail; 629 } 630 631 if (val_ret) 632 *val_ret = (ret 633 ? [NSString stringWithCString:ret 634 encoding:NSUTF8StringEncoding] 635 : 0); 636 637 const char *res = opts_array[0].specifier; 638 while (*res && (*res == '.' || *res == '*')) 639 res++; 640 return [self makeCKey:res]; 641 } 642 opts_array++; 643 } 644 645 NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch); 646 return 0; 647} 648 649 650- (NSUserDefaultsController *)controllerForKey:(NSString *)key 651{ 652 static NSDictionary *a = 0; 653 if (! a) { 654 a = UPDATER_DEFAULTS; 655 [a retain]; 656 } 657 if ([a objectForKey:key]) 658 // These preferences are global to all xscreensavers. 659 return globalDefaultsController; 660 else 661 // All other preferences are per-saver. 662 return userDefaultsController; 663} 664 665 666#ifdef USE_IPHONE 667 668// Called when a slider is bonked. 669// 670- (void)sliderAction:(UISlider*)sender 671{ 672 if ([active_text_field canResignFirstResponder]) 673 [active_text_field resignFirstResponder]; 674 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]]; 675 676 // Hacky API. See comment in InvertedSlider.m. 677 double v = ([sender isKindOfClass: [InvertedSlider class]] 678 ? [(InvertedSlider *) sender transformedValue] 679 : [sender value]); 680 681 [[self controllerForKey:pref_key] 682 setObject:((v == (int) v) 683 ? [NSNumber numberWithInt:(int) v] 684 : [NSNumber numberWithDouble: v]) 685 forKey:pref_key]; 686} 687 688// Called when a checkbox/switch is bonked. 689// 690- (void)switchAction:(UISwitch*)sender 691{ 692 if ([active_text_field canResignFirstResponder]) 693 [active_text_field resignFirstResponder]; 694 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]]; 695 NSString *v = ([sender isOn] ? @"true" : @"false"); 696 [[self controllerForKey:pref_key] setObject:v forKey:pref_key]; 697} 698 699# ifdef USE_PICKER_VIEW 700// Called when a picker is bonked. 701// 702- (void)pickerView:(UIPickerView *)pv 703 didSelectRow:(NSInteger)row 704 inComponent:(NSInteger)column 705{ 706 if ([active_text_field canResignFirstResponder]) 707 [active_text_field resignFirstResponder]; 708 709 NSAssert (column == 0, @"internal error"); 710 NSArray *a = [picker_values objectAtIndex: [pv tag]]; 711 if (! a) return; // Too early? 712 a = [a objectAtIndex:row]; 713 NSAssert (a, @"missing row"); 714 715//NSString *label = [a objectAtIndex:0]; 716 NSString *pref_key = [a objectAtIndex:1]; 717 NSObject *pref_val = [a objectAtIndex:2]; 718 [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key]; 719} 720# else // !USE_PICKER_VIEW 721 722// Called when a RadioButton is bonked. 723// 724- (void)radioAction:(RadioButton*)sender 725{ 726 if ([active_text_field canResignFirstResponder]) 727 [active_text_field resignFirstResponder]; 728 729 NSArray *item = [[sender items] objectAtIndex: [sender index]]; 730 NSString *pref_key = [item objectAtIndex:1]; 731 NSObject *pref_val = [item objectAtIndex:2]; 732 [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key]; 733} 734 735- (BOOL)textFieldShouldBeginEditing:(UITextField *)tf 736{ 737 active_text_field = tf; 738 return YES; 739} 740 741- (void)textFieldDidEndEditing:(UITextField *)tf 742{ 743 NSString *pref_key = [pref_keys objectAtIndex: [tf tag]]; 744 NSString *txt = [tf text]; 745 [[self controllerForKey:pref_key] setObject:txt forKey:pref_key]; 746} 747 748- (BOOL)textFieldShouldReturn:(UITextField *)tf 749{ 750 active_text_field = nil; 751 [tf resignFirstResponder]; 752 return YES; 753} 754 755# endif // !USE_PICKER_VIEW 756 757#endif // USE_IPHONE 758 759 760# ifndef USE_IPHONE 761 762- (void) okAction:(NSObject *)arg 763{ 764 // Without this, edits to text fields only happen if the user hits RET. 765 // Clicking OK should also commit those edits. 766 [self makeFirstResponder:nil]; 767 768 // Without the setAppliesImmediately:, when the saver restarts, it's still 769 // got the old settings. -[XScreenSaverConfigSheet traverseTree] sets this 770 // to NO; default is YES. 771 772 [userDefaultsController setAppliesImmediately:YES]; 773 [globalDefaultsController setAppliesImmediately:YES]; 774 [userDefaultsController commitEditing]; 775 [globalDefaultsController commitEditing]; 776 [userDefaultsController save:self]; 777 [globalDefaultsController save:self]; 778 [NSApp endSheet:self returnCode:NSOKButton]; 779 [self close]; 780} 781 782- (void) cancelAction:(NSObject *)arg 783{ 784 [userDefaultsController revert:self]; 785 [globalDefaultsController revert:self]; 786 [NSApp endSheet:self returnCode:NSCancelButton]; 787 [self close]; 788} 789# endif // !USE_IPHONE 790 791 792- (void) resetAction:(NSObject *)arg 793{ 794# ifndef USE_IPHONE 795 [userDefaultsController revertToInitialValues:self]; 796 [globalDefaultsController revertToInitialValues:self]; 797# else // USE_IPHONE 798 799 for (NSString *key in defaultOptions) { 800 NSObject *val = [defaultOptions objectForKey:key]; 801 [[self controllerForKey:key] setObject:val forKey:key]; 802 } 803 804 for (UIControl *ctl in pref_ctls) { 805 NSString *pref_key = [pref_keys objectAtIndex: ctl.tag]; 806 [self bindResource:ctl key:pref_key reload:YES]; 807 } 808 809 [self refreshTableView]; 810# endif // USE_IPHONE 811} 812 813 814/* Connects a control (checkbox, etc) to the corresponding preferences key. 815 */ 816- (void) bindResource:(NSObject *)control key:(NSString *)pref_key 817 reload:(BOOL)reload_p 818{ 819 NSUserDefaultsController *prefs = [self controllerForKey:pref_key]; 820# ifndef USE_IPHONE 821 NSDictionary *opts_dict = nil; 822 NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]] 823 ? @"selectedObject" 824 : ([control isKindOfClass:[NSMatrix class]] 825 ? @"selectedIndex" 826 : @"value")); 827 828 if ([control isKindOfClass:[NSMatrix class]]) { 829 opts_dict = @{ NSValueTransformerNameBindingOption: 830 @"TextModeTransformer" }; 831 } else if ([control isKindOfClass:[NSTextField class]]) { 832 opts_dict = @{ NSValueTransformerNameBindingOption: 833 @"NonNilStringTransformer" }; 834 } 835 836 [control bind:bindto 837 toObject:prefs 838 withKeyPath:[@"values." stringByAppendingString: pref_key] 839 options:opts_dict]; 840 841# else // USE_IPHONE 842 SEL sel; 843 NSObject *val = [prefs objectForKey:pref_key]; 844 NSString *sval = 0; 845 double dval = 0; 846 847 if ([val isKindOfClass:[NSString class]]) { 848 sval = (NSString *) val; 849 if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] || 850 NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] || 851 NSOrderedSame == [sval caseInsensitiveCompare:@"1"]) 852 dval = 1; 853 else 854 dval = [sval doubleValue]; 855 } else if ([val isKindOfClass:[NSNumber class]]) { 856 // NSBoolean (__NSCFBoolean) is really NSNumber. 857 dval = [(NSNumber *) val doubleValue]; 858 sval = [(NSNumber *) val stringValue]; 859 } 860 861 if ([control isKindOfClass:[UISlider class]]) { 862 sel = @selector(sliderAction:); 863 // Hacky API. See comment in InvertedSlider.m. 864 if ([control isKindOfClass:[InvertedSlider class]]) 865 [(InvertedSlider *) control setTransformedValue: dval]; 866 else 867 [(UISlider *) control setValue: dval]; 868 } else if ([control isKindOfClass:[UISwitch class]]) { 869 sel = @selector(switchAction:); 870 [(UISwitch *) control setOn: ((int) dval != 0)]; 871# ifdef USE_PICKER_VIEW 872 } else if ([control isKindOfClass:[UIPickerView class]]) { 873 sel = 0; 874 [(UIPickerView *) control selectRow:((int)dval) inComponent:0 875 animated:NO]; 876# else // !USE_PICKER_VIEW 877 } else if ([control isKindOfClass:[RadioButton class]]) { 878 sel = 0; // radioAction: sent from didSelectRowAtIndexPath. 879 } else if ([control isKindOfClass:[UITextField class]]) { 880 sel = 0; // #### 881 [(UITextField *) control setText: sval]; 882# endif // !USE_PICKER_VIEW 883 } else { 884 NSAssert (0, @"unknown class"); 885 } 886 887 // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval); 888 889 if (!reload_p) { 890 if (! pref_keys) { 891 pref_keys = [[NSMutableArray arrayWithCapacity:10] retain]; 892 pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain]; 893 } 894 895 [pref_keys addObject: [self makeKey:pref_key]]; 896 [pref_ctls addObject: control]; 897 ((UIControl *) control).tag = [pref_keys count] - 1; 898 899 if (sel) { 900 [(UIControl *) control addTarget:self action:sel 901 forControlEvents:UIControlEventValueChanged]; 902 } 903 } 904 905# endif // USE_IPHONE 906 907# if 0 908 NSObject *def = [[prefs defaults] objectForKey:pref_key]; 909 NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key]; 910 s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0]; 911 s = [NSString stringWithFormat:@"%@ = %@", s, 912 ([def isKindOfClass:[NSString class]] 913 ? [NSString stringWithFormat:@"\"%@\"", def] 914 : def)]; 915 s = [s stringByPaddingToLength:30 withString:@" " startingAtIndex:0]; 916 s = [NSString stringWithFormat:@"%@ %@ / %@", s, 917 [def class], [control class]]; 918# ifndef USE_IPHONE 919 s = [NSString stringWithFormat:@"%@ / %@", s, bindto]; 920# endif 921 NSLog (@"%@", s); 922# endif 923} 924 925 926- (void) bindResource:(NSObject *)control key:(NSString *)pref_key 927{ 928 [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO]; 929} 930 931 932 933- (void) bindSwitch:(NSObject *)control 934 cmdline:(NSString *)cmd 935{ 936 [self bindResource:control 937 key:[self switchToResource:cmd opts:opts valRet:0]]; 938} 939 940 941#pragma mark Text-manipulating utilities 942 943 944static NSString * 945unwrap (NSString *text) 946{ 947 // Unwrap lines: delete \n but do not delete \n\n. 948 // 949 NSArray *lines = [text componentsSeparatedByString:@"\n"]; 950 NSUInteger i, nlines = [lines count]; 951 BOOL eolp = YES; 952 953 text = @"\n"; // start with one blank line 954 955 // skip trailing blank lines in file 956 for (i = nlines-1; i > 0; i--) { 957 NSString *s = (NSString *) [lines objectAtIndex:i]; 958 if ([s length] > 0) 959 break; 960 nlines--; 961 } 962 963 // skip leading blank lines in file 964 for (i = 0; i < nlines; i++) { 965 NSString *s = (NSString *) [lines objectAtIndex:i]; 966 if ([s length] > 0) 967 break; 968 } 969 970 // unwrap 971 Bool any = NO; 972 for (; i < nlines; i++) { 973 NSString *s = (NSString *) [lines objectAtIndex:i]; 974 if ([s length] == 0) { 975 text = [text stringByAppendingString:@"\n\n"]; 976 eolp = YES; 977 } else if ([s characterAtIndex:0] == ' ' || 978 [s hasPrefix:@"Copyright "] || 979 [s hasPrefix:@"https://"] || 980 [s hasPrefix:@"http://"]) { 981 // don't unwrap if the following line begins with whitespace, 982 // or with the word "Copyright", or if it begins with a URL. 983 if (any && !eolp) 984 text = [text stringByAppendingString:@"\n"]; 985 text = [text stringByAppendingString:s]; 986 any = YES; 987 eolp = NO; 988 } else { 989 if (!eolp) 990 text = [text stringByAppendingString:@" "]; 991 text = [text stringByAppendingString:s]; 992 eolp = NO; 993 any = YES; 994 } 995 } 996 997 return text; 998} 999 1000 1001# ifndef USE_IPHONE 1002/* Makes the text up to the first comma be bold. 1003 */ 1004static void 1005boldify (NSText *nstext) 1006{ 1007 NSString *text = [nstext string]; 1008 NSRange r = [text rangeOfString:@"," options:0]; 1009 r.length = r.location+1; 1010 1011 r.location = 0; 1012 1013 NSFont *font = [nstext font]; 1014 font = [NSFont boldSystemFontOfSize:[font pointSize]]; 1015 [nstext setFont:font range:r]; 1016} 1017# endif // !USE_IPHONE 1018 1019 1020/* Creates a human-readable anchor to put on a URL. 1021 */ 1022static char * 1023anchorize (const char *url) 1024{ 1025 const char *wiki1 = "http://en.wikipedia.org/wiki/"; 1026 const char *wiki2 = "https://en.wikipedia.org/wiki/"; 1027 const char *math1 = "http://mathworld.wolfram.com/"; 1028 const char *math2 = "https://mathworld.wolfram.com/"; 1029 if (!strncmp (wiki1, url, strlen(wiki1)) || 1030 !strncmp (wiki2, url, strlen(wiki2))) { 1031 char *anchor = (char *) malloc (strlen(url) * 3 + 10); 1032 strcpy (anchor, "Wikipedia: \""); 1033 const char *in = url + strlen(!strncmp (wiki1, url, strlen(wiki1)) 1034 ? wiki1 : wiki2); 1035 char *out = anchor + strlen(anchor); 1036 while (*in) { 1037 if (*in == '_') { 1038 *out++ = ' '; 1039 } else if (*in == '#') { 1040 *out++ = ':'; 1041 *out++ = ' '; 1042 } else if (*in == '%') { 1043 char hex[3]; 1044 hex[0] = in[1]; 1045 hex[1] = in[2]; 1046 hex[2] = 0; 1047 int n = 0; 1048 sscanf (hex, "%x", &n); 1049 *out++ = (char) n; 1050 in += 2; 1051 } else { 1052 *out++ = *in; 1053 } 1054 in++; 1055 } 1056 *out++ = '"'; 1057 *out = 0; 1058 return anchor; 1059 1060 } else if (!strncmp (math1, url, strlen(math1)) || 1061 !strncmp (math2, url, strlen(math2))) { 1062 char *anchor = (char *) malloc (strlen(url) * 3 + 10); 1063 strcpy (anchor, "MathWorld: \""); 1064 const char *start = url + strlen(!strncmp (math1, url, strlen(math1)) 1065 ? math1 : math2); 1066 const char *in = start; 1067 char *out = anchor + strlen(anchor); 1068 while (*in) { 1069 if (*in == '_') { 1070 *out++ = ' '; 1071 } else if (in != start && *in >= 'A' && *in <= 'Z') { 1072 *out++ = ' '; 1073 *out++ = *in; 1074 } else if (!strncmp (in, ".htm", 4)) { 1075 break; 1076 } else { 1077 *out++ = *in; 1078 } 1079 in++; 1080 } 1081 *out++ = '"'; 1082 *out = 0; 1083 return anchor; 1084 1085 } else { 1086 return strdup (url); 1087 } 1088} 1089 1090 1091#if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS) 1092 1093/* Converts any http: URLs in the given text field to clickable links. 1094 */ 1095static void 1096hreffify (NSText *nstext) 1097{ 1098# ifndef USE_IPHONE 1099 NSString *text = [nstext string]; 1100 [nstext setRichText:YES]; 1101# else 1102 NSString *text = [nstext text]; 1103# endif 1104 1105 NSUInteger L = [text length]; 1106 NSRange start; // range is start-of-search to end-of-string 1107 start.location = 0; 1108 start.length = L; 1109 while (start.location < L) { 1110 1111 // Find the beginning of a URL... 1112 // 1113 NSRange r2 = [text rangeOfString: @"http://" options:0 range:start]; 1114 NSRange r3 = [text rangeOfString:@"https://" options:0 range:start]; 1115 if ((r2.location == NSNotFound && 1116 r3.location != NSNotFound) || 1117 (r2.location != NSNotFound && 1118 r3.location != NSNotFound && 1119 r3.location < r2.location)) 1120 r2 = r3; 1121 if (r2.location == NSNotFound) 1122 break; 1123 1124 // Next time around, start searching after this. 1125 start.location = r2.location + r2.length; 1126 start.length = L - start.location; 1127 1128 // Find the end of a URL (whitespace or EOF)... 1129 // 1130 r3 = [text rangeOfCharacterFromSet: 1131 [NSCharacterSet whitespaceAndNewlineCharacterSet] 1132 options:0 range:start]; 1133 if (r3.location == NSNotFound) // EOF 1134 r3.location = L, r3.length = 0; 1135 1136 // Next time around, start searching after this. 1137 start.location = r3.location; 1138 start.length = L - start.location; 1139 1140 // Set r2 to the start/length of this URL. 1141 r2.length = start.location - r2.location; 1142 1143 // Extract the URL. 1144 NSString *nsurl = [text substringWithRange:r2]; 1145 const char *url = [nsurl UTF8String]; 1146 1147 // If this is a Wikipedia URL, make the linked text be prettier. 1148 // 1149 char *anchor = anchorize(url); 1150 1151# ifndef USE_IPHONE 1152 1153 // Construct the RTF corresponding to <A HREF="url">anchor</A> 1154 // 1155 const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}"; 1156 char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10); 1157 sprintf (rtf, fmt, url, anchor); 1158 1159 NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)]; 1160 [nstext replaceCharactersInRange:r2 withRTF:rtfdata]; 1161 1162# else // !USE_IPHONE 1163 // *anchor = 0; // Omit Wikipedia anchor 1164 text = [text stringByReplacingCharactersInRange:r2 1165 withString:[NSString stringWithCString:anchor 1166 encoding:NSUTF8StringEncoding]]; 1167 // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n" 1168 // withString:@"\n\n"]; 1169# endif // !USE_IPHONE 1170 1171 free (anchor); 1172 1173 NSUInteger L2 = [text length]; // might have changed 1174 start.location -= (L - L2); 1175 L = L2; 1176 } 1177 1178# ifdef USE_IPHONE 1179 [nstext setText:text]; 1180 [nstext sizeToFit]; 1181# endif 1182} 1183 1184#endif /* !USE_IPHONE || !USE_HTML_LABELS */ 1185 1186 1187 1188#pragma mark Creating controls from XML 1189 1190 1191/* Parse the attributes of an XML tag into a dictionary. 1192 For input, the dictionary should have as attributes the keys, each 1193 with @"" as their value. 1194 On output, the dictionary will set the keys to the values specified, 1195 and keys that were not specified will not be present in the dictionary. 1196 Warnings are printed if there are duplicate or unknown attributes. 1197 */ 1198- (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node 1199{ 1200 NSArray *attrs = [(NSXMLElement *) node attributes]; 1201 NSUInteger n = [attrs count]; 1202 int i; 1203 1204 // For each key in the dictionary, fill in the dict with the corresponding 1205 // value. The value @"" is assumed to mean "un-set". Issue a warning if 1206 // an attribute is specified twice. 1207 // 1208 for (i = 0; i < n; i++) { 1209 NSXMLNode *attr = [attrs objectAtIndex:i]; 1210 NSString *key = [attr name]; 1211 NSString *val = [attr objectValue]; 1212 NSString *old = [dict objectForKey:key]; 1213 1214 if (! old) { 1215 NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]); 1216 } else if ([old length] != 0) { 1217 NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val); 1218 } else { 1219 [dict setValue:val forKey:key]; 1220 } 1221 } 1222 1223 // Remove from the dictionary any keys whose value is still @"", 1224 // meaning there was no such attribute specified. 1225 // 1226 NSArray *keys = [dict allKeys]; 1227 n = [keys count]; 1228 for (i = 0; i < n; i++) { 1229 NSString *key = [keys objectAtIndex:i]; 1230 NSString *val = [dict objectForKey:key]; 1231 if ([val length] == 0) 1232 [dict removeObjectForKey:key]; 1233 } 1234 1235# ifdef USE_IPHONE 1236 // Kludge for starwars.xml: 1237 // If there is a "_low-label" and no "_label", but "_low-label" contains 1238 // spaces, divide them. 1239 NSString *lab = [dict objectForKey:@"_label"]; 1240 NSString *low = [dict objectForKey:@"_low-label"]; 1241 if (low && !lab) { 1242 NSArray *split = 1243 [[[low stringByTrimmingCharactersInSet: 1244 [NSCharacterSet whitespaceAndNewlineCharacterSet]] 1245 componentsSeparatedByString: @" "] 1246 filteredArrayUsingPredicate: 1247 [NSPredicate predicateWithFormat:@"length > 0"]]; 1248 if (split && [split count] == 2) { 1249 [dict setValue:[split objectAtIndex:0] forKey:@"_label"]; 1250 [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"]; 1251 } 1252 } 1253# endif // USE_IPHONE 1254} 1255 1256 1257/* Handle the options on the top level <xscreensaver> tag. 1258 */ 1259- (NSString *) parseXScreenSaverTag:(NSXMLNode *)node 1260{ 1261 NSMutableDictionary *dict = [@{ @"name": @"", 1262 @"_label": @"", 1263 @"gl": @"" } 1264 mutableCopy]; 1265 [self parseAttrs:dict node:node]; 1266 NSString *name = [dict objectForKey:@"name"]; 1267 NSString *label = [dict objectForKey:@"_label"]; 1268 [dict release]; 1269 dict = 0; 1270 1271 NSAssert1 (label, @"no _label in %@", [node name]); 1272 NSAssert1 (name, @"no name in \"%@\"", label); 1273 return label; 1274} 1275 1276 1277/* Creates a label: an un-editable NSTextField displaying the given text. 1278 */ 1279- (LABEL *) makeLabel:(NSString *)text 1280{ 1281 NSRect rect; 1282 rect.origin.x = rect.origin.y = 0; 1283 rect.size.width = rect.size.height = 10; 1284# ifndef USE_IPHONE 1285 NSTextField *lab = [[NSTextField alloc] initWithFrame:rect]; 1286 [lab setSelectable:NO]; 1287 [lab setEditable:NO]; 1288 [lab setBezeled:NO]; 1289 [lab setDrawsBackground:NO]; 1290 [lab setStringValue:NSLocalizedString(text, @"")]; 1291 [lab sizeToFit]; 1292# else // USE_IPHONE 1293 UILabel *lab = [[UILabel alloc] initWithFrame:rect]; 1294 [lab setText: [text stringByTrimmingCharactersInSet: 1295 [NSCharacterSet whitespaceAndNewlineCharacterSet]]]; 1296 [lab setBackgroundColor:[UIColor clearColor]]; 1297 [lab setNumberOfLines:0]; // unlimited 1298 // [lab setLineBreakMode:UILineBreakModeWordWrap]; 1299 [lab setLineBreakMode:NSLineBreakByTruncatingHead]; 1300 [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth | 1301 UIViewAutoresizingFlexibleHeight)]; 1302# endif // USE_IPHONE 1303 [lab autorelease]; 1304 return lab; 1305} 1306 1307 1308/* Creates the checkbox (NSButton) described by the given XML node. 1309 */ 1310- (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent 1311{ 1312 NSMutableDictionary *dict = [@{ @"id": @"", 1313 @"_label": @"", 1314 @"arg-set": @"", 1315 @"arg-unset": @"", 1316 @"disabled": @"" } 1317 mutableCopy]; 1318 [self parseAttrs:dict node:node]; 1319 NSString *label = [dict objectForKey:@"_label"]; 1320 NSString *arg_set = [dict objectForKey:@"arg-set"]; 1321 NSString *arg_unset = [dict objectForKey:@"arg-unset"]; 1322 1323 NSString *dd = [dict objectForKey:@"disabled"]; 1324 BOOL disabledp = (dd && 1325 (NSOrderedSame == [dd caseInsensitiveCompare:@"true"] || 1326 NSOrderedSame == [dd caseInsensitiveCompare:@"yes"])); 1327 [dict release]; 1328 dict = 0; 1329 1330 if (!label) { 1331 NSAssert1 (0, @"no _label in %@", [node name]); 1332 return; 1333 } 1334 if (!arg_set && !arg_unset) { 1335 NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"", 1336 label); 1337 } 1338 if (arg_set && arg_unset) { 1339 NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"", 1340 label); 1341 } 1342 1343 // sanity-check the choice of argument names. 1344 // 1345 if (arg_set && ([arg_set hasPrefix:@"-no-"] || 1346 [arg_set hasPrefix:@"--no-"])) 1347 NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@", 1348 label, arg_set); 1349 if (arg_unset && (![arg_unset hasPrefix:@"-no-"] && 1350 ![arg_unset hasPrefix:@"--no-"])) 1351 NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@", 1352 label, arg_unset); 1353 1354 NSRect rect; 1355 rect.origin.x = rect.origin.y = 0; 1356 rect.size.width = rect.size.height = 10; 1357 1358# ifndef USE_IPHONE 1359 1360 NSButton *button = [[NSButton alloc] initWithFrame:rect]; 1361 [button setButtonType:NSSwitchButton]; 1362 [button setTitle:label]; 1363 [button sizeToFit]; 1364 [self placeChild:button on:parent]; 1365 1366# else // USE_IPHONE 1367 1368 LABEL *lab = [self makeLabel:label]; 1369 [self placeChild:lab on:parent]; 1370 UISwitch *button = [[UISwitch alloc] initWithFrame:rect]; 1371 [self placeChild:button on:parent right:YES]; 1372 1373# endif // USE_IPHONE 1374 1375 if (disabledp) 1376 [button setEnabled:NO]; 1377 else 1378 [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)]; 1379 1380 [button release]; 1381} 1382 1383 1384/* Creates the number selection control described by the given XML node. 1385 If "type=slider", it's an NSSlider. 1386 If "type=spinbutton", it's a text field with up/down arrows next to it. 1387 */ 1388- (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent 1389{ 1390 NSMutableDictionary *dict = [@{ @"id": @"", 1391 @"_label": @"", 1392 @"_low-label": @"", 1393 @"_high-label": @"", 1394 @"type": @"", 1395 @"arg": @"", 1396 @"low": @"", 1397 @"high": @"", 1398 @"default": @"", 1399 @"convert": @"" } 1400 mutableCopy]; 1401 [self parseAttrs:dict node:node]; 1402 NSString *label = [dict objectForKey:@"_label"]; 1403 NSString *low_label = [dict objectForKey:@"_low-label"]; 1404 NSString *high_label = [dict objectForKey:@"_high-label"]; 1405 NSString *type = [dict objectForKey:@"type"]; 1406 NSString *arg = [dict objectForKey:@"arg"]; 1407 NSString *low = [dict objectForKey:@"low"]; 1408 NSString *high = [dict objectForKey:@"high"]; 1409 NSString *def = [dict objectForKey:@"default"]; 1410 NSString *cvt = [dict objectForKey:@"convert"]; 1411 [dict release]; 1412 dict = 0; 1413 1414 NSAssert1 (arg, @"no arg in %@", label); 1415 NSAssert1 (type, @"no type in %@", label); 1416 1417 if (! low) { 1418 NSAssert1 (0, @"no low in %@", [node name]); 1419 return; 1420 } 1421 if (! high) { 1422 NSAssert1 (0, @"no high in %@", [node name]); 1423 return; 1424 } 1425 if (! def) { 1426 NSAssert1 (0, @"no default in %@", [node name]); 1427 return; 1428 } 1429 if (cvt && ![cvt isEqualToString:@"invert"]) { 1430 NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@", 1431 label); 1432 } 1433 1434 // If either the min or max field contains a decimal point, then this 1435 // option may have a floating point value; otherwise, it is constrained 1436 // to be an integer. 1437 // 1438 NSCharacterSet *dot = 1439 [NSCharacterSet characterSetWithCharactersInString:@"."]; 1440 BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound || 1441 [high rangeOfCharacterFromSet:dot].location != NSNotFound); 1442 1443 if ([type isEqualToString:@"slider"] 1444# ifdef USE_IPHONE // On iPhone, we use sliders for all numeric values. 1445 || [type isEqualToString:@"spinbutton"] 1446# endif 1447 ) { 1448 1449 NSRect rect; 1450 rect.origin.x = rect.origin.y = 0; 1451 rect.size.width = 150; 1452 rect.size.height = 23; // apparent min height for slider with ticks... 1453 NSSlider *slider; 1454 slider = [[InvertedSlider alloc] initWithFrame:rect 1455 inverted: !!cvt 1456 integers: !float_p]; 1457 [slider setMaxValue:[high doubleValue]]; 1458 [slider setMinValue:[low doubleValue]]; 1459 1460 int range = [slider maxValue] - [slider minValue] + 1; 1461 int range2 = range; 1462 int max_ticks = 21; 1463 while (range2 > max_ticks) 1464 range2 /= 10; 1465 1466# ifndef USE_IPHONE 1467 // If we have elided ticks, leave it at the max number of ticks. 1468 if (range != range2 && range2 < max_ticks) 1469 range2 = max_ticks; 1470 1471 // If it's a float, always display the max number of ticks. 1472 if (float_p && range2 < max_ticks) 1473 range2 = max_ticks; 1474 1475 [slider setNumberOfTickMarks:range2]; 1476 1477 [slider setAllowsTickMarkValuesOnly: 1478 (range == range2 && // we are showing the actual number of ticks 1479 !float_p)]; // and we want integer results 1480# endif // !USE_IPHONE 1481 1482 // #### Note: when the slider's range is large enough that we aren't 1483 // showing all possible ticks, the slider's value is not constrained 1484 // to be an integer, even though it should be... 1485 // Maybe we need to use a value converter or something? 1486 1487 LABEL *lab; 1488 if (label) { 1489 lab = [self makeLabel:label]; 1490 [self placeChild:lab on:parent]; 1491# ifdef USE_IPHONE 1492 if (low_label) { 1493 CGFloat s = [NSFont systemFontSize] + 4; 1494 [lab setFont:[NSFont boldSystemFontOfSize:s]]; 1495 } 1496# endif 1497 } 1498 1499 if (low_label) { 1500 lab = [self makeLabel:low_label]; 1501 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; 1502# ifndef USE_IPHONE 1503 [lab setAlignment:1]; // right aligned 1504 rect = [lab frame]; 1505 if (rect.size.width < LEFT_LABEL_WIDTH) 1506 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size 1507 rect.size.height = [slider frame].size.height; 1508 [lab setFrame:rect]; 1509 [self placeChild:lab on:parent]; 1510# else // USE_IPHONE 1511 [lab setTextAlignment: NSTextAlignmentRight]; 1512 // Sometimes rotation screws up truncation. 1513 [lab setLineBreakMode:NSLineBreakByClipping]; 1514 [self placeChild:lab on:parent right:(label ? YES : NO)]; 1515# endif // USE_IPHONE 1516 } 1517 1518# ifndef USE_IPHONE 1519 [self placeChild:slider on:parent right:(low_label ? YES : NO)]; 1520# else // USE_IPHONE 1521 [self placeChild:slider on:parent right:(label || low_label ? YES : NO)]; 1522# endif // USE_IPHONE 1523 1524 if (low_label) { 1525 // Make left label be same height as slider. 1526 rect = [lab frame]; 1527 rect.size.height = [slider frame].size.height; 1528 [lab setFrame:rect]; 1529 } 1530 1531 if (! low_label) { 1532 rect = [slider frame]; 1533 if (rect.origin.x < LEFT_LABEL_WIDTH) 1534 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too 1535 [slider setFrame:rect]; 1536 } 1537 1538 if (high_label) { 1539 lab = [self makeLabel:high_label]; 1540 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; 1541 rect = [lab frame]; 1542 1543 // Make right label be same height as slider. 1544 rect.size.height = [slider frame].size.height; 1545 [lab setFrame:rect]; 1546# ifdef USE_IPHONE 1547 // Sometimes rotation screws up truncation. 1548 [lab setLineBreakMode:NSLineBreakByClipping]; 1549# endif 1550 [self placeChild:lab on:parent right:YES]; 1551 } 1552 1553 [self bindSwitch:slider cmdline:arg]; 1554 [slider release]; 1555 1556#ifndef USE_IPHONE // On iPhone, we use sliders for all numeric values. 1557 1558 } else if ([type isEqualToString:@"spinbutton"]) { 1559 1560 if (! label) { 1561 NSAssert1 (0, @"no _label in spinbutton %@", [node name]); 1562 return; 1563 } 1564 NSAssert1 (!low_label, 1565 @"low-label not allowed in spinbutton \"%@\"", [node name]); 1566 NSAssert1 (!high_label, 1567 @"high-label not allowed in spinbutton \"%@\"", [node name]); 1568 NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"", 1569 [node name]); 1570 1571 NSRect rect; 1572 rect.origin.x = rect.origin.y = 0; 1573 rect.size.width = rect.size.height = 10; 1574 1575 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect]; 1576 [txt setStringValue:NSLocalizedString(@"0000.0", @"")]; 1577 [txt sizeToFit]; 1578 [txt setStringValue:@""]; 1579 1580 if (label) { 1581 LABEL *lab = [self makeLabel:label]; 1582 //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; 1583 [lab setAlignment:1]; // right aligned 1584 rect = [lab frame]; 1585 if (rect.size.width < LEFT_LABEL_WIDTH) 1586 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size 1587 rect.size.height = [txt frame].size.height; 1588 [lab setFrame:rect]; 1589 [self placeChild:lab on:parent]; 1590 } 1591 1592 [self placeChild:txt on:parent right:(label ? YES : NO)]; 1593 1594 if (! label) { 1595 rect = [txt frame]; 1596 if (rect.origin.x < LEFT_LABEL_WIDTH) 1597 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up 1598 [txt setFrame:rect]; 1599 } 1600 1601 rect.size.width = rect.size.height = 10; 1602 NSStepper *step = [[NSStepper alloc] initWithFrame:rect]; 1603 [step sizeToFit]; 1604 [self placeChild:step on:parent right:YES]; 1605 rect = [step frame]; 1606 rect.origin.x -= COLUMN_SPACING; // this one goes close 1607 rect.origin.y += ([txt frame].size.height - rect.size.height) / 2; 1608 [step setFrame:rect]; 1609 1610 [step setMinValue:[low doubleValue]]; 1611 [step setMaxValue:[high doubleValue]]; 1612 [step setAutorepeat:YES]; 1613 [step setValueWraps:NO]; 1614 1615 double range = [high doubleValue] - [low doubleValue]; 1616 if (range < 1.0) 1617 [step setIncrement:range / 10.0]; 1618 else if (range >= 500) 1619 [step setIncrement:range / 100.0]; 1620 else 1621 [step setIncrement:1.0]; 1622 1623 NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease]; 1624 [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4]; 1625 [fmt setNumberStyle:NSNumberFormatterDecimalStyle]; 1626 [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]]; 1627 [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]]; 1628 [fmt setMinimumFractionDigits: (float_p ? 1 : 0)]; 1629 [fmt setMaximumFractionDigits: (float_p ? 2 : 0)]; 1630 1631 [fmt setGeneratesDecimalNumbers:float_p]; 1632 [[txt cell] setFormatter:fmt]; 1633 1634 [self bindSwitch:step cmdline:arg]; 1635 [self bindSwitch:txt cmdline:arg]; 1636 1637 [step release]; 1638 [txt release]; 1639 1640# endif // USE_IPHONE 1641 1642 } else { 1643 NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label); 1644 } 1645} 1646 1647 1648# ifndef USE_IPHONE 1649static void 1650set_menu_item_object (NSMenuItem *item, NSObject *obj) 1651{ 1652 /* If the object associated with this menu item looks like a boolean, 1653 store an NSNumber instead of an NSString, since that's what 1654 will be in the preferences (due to similar logic in PrefsReader). 1655 */ 1656 if ([obj isKindOfClass:[NSString class]]) { 1657 NSString *string = (NSString *) obj; 1658 if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] || 1659 NSOrderedSame == [string caseInsensitiveCompare:@"yes"]) 1660 obj = [NSNumber numberWithBool:YES]; 1661 else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] || 1662 NSOrderedSame == [string caseInsensitiveCompare:@"no"]) 1663 obj = [NSNumber numberWithBool:NO]; 1664 else 1665 obj = string; 1666 } 1667 1668 [item setRepresentedObject:obj]; 1669 //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]); 1670} 1671# endif // !USE_IPHONE 1672 1673 1674/* Creates the popup menu described by the given XML node (and its children). 1675 */ 1676- (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent 1677 disabled:(BOOL)disabled 1678{ 1679 NSArray *children = [node children]; 1680 NSUInteger i, count = [children count]; 1681 1682 if (count <= 0) { 1683 NSAssert1 (0, @"no menu items in \"%@\"", [node name]); 1684 return; 1685 } 1686 1687 // get the "id" attribute off the <select> tag. 1688 // 1689 NSMutableDictionary *dict = [@{ @"id": @"", } mutableCopy]; 1690 [self parseAttrs:dict node:node]; 1691 [dict release]; 1692 dict = 0; 1693 1694 NSRect rect; 1695 rect.origin.x = rect.origin.y = 0; 1696 rect.size.width = 10; 1697 rect.size.height = 10; 1698 1699 NSString *menu_key = nil; // the resource key used by items in this menu 1700 1701# ifndef USE_IPHONE 1702 // #### "Build and Analyze" says that all of our widgets leak, because it 1703 // seems to not realize that placeChild -> addSubview retains them. 1704 // Not sure what to do to make these warnings go away. 1705 1706 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect 1707 pullsDown:NO]; 1708 NSMenuItem *def_item = nil; 1709 float max_width = 0; 1710 1711# else // USE_IPHONE 1712 1713 NSString *def_item = nil; 1714 1715 rect.size.width = 0; 1716 rect.size.height = 0; 1717# ifdef USE_PICKER_VIEW 1718 UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain]; 1719 popup.delegate = self; 1720 popup.dataSource = self; 1721# endif // !USE_PICKER_VIEW 1722 NSMutableArray *items = [NSMutableArray arrayWithCapacity:10]; 1723 1724# endif // USE_IPHONE 1725 1726 for (i = 0; i < count; i++) { 1727 NSXMLNode *child = [children objectAtIndex:i]; 1728 1729 if ([child kind] == NSXMLCommentKind) 1730 continue; 1731 if ([child kind] != NSXMLElementKind) { 1732// NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node); 1733 continue; 1734 } 1735 1736 // get the "id", "_label", and "arg-set" attrs off of the <option> tags. 1737 // 1738 NSMutableDictionary *dict2 = [@{ @"id": @"", 1739 @"_label": @"", 1740 @"arg-set": @"" } 1741 mutableCopy]; 1742 [self parseAttrs:dict2 node:child]; 1743 NSString *label = [dict2 objectForKey:@"_label"]; 1744 NSString *arg_set = [dict2 objectForKey:@"arg-set"]; 1745 [dict2 release]; 1746 dict2 = 0; 1747 1748 if (!label) { 1749 NSAssert1 (0, @"no _label in %@", [child name]); 1750 continue; 1751 } 1752 1753# ifndef USE_IPHONE 1754 // create the menu item (and then get a pointer to it) 1755 [popup addItemWithTitle:label]; 1756 NSMenuItem *item = [popup itemWithTitle:label]; 1757# endif // USE_IPHONE 1758 1759 if (arg_set) { 1760 NSString *this_val = NULL; 1761 NSString *this_key = [self switchToResource: arg_set 1762 opts: opts 1763 valRet: &this_val]; 1764 NSAssert1 (this_val, @"this_val null for %@", arg_set); 1765 if (menu_key && ![menu_key isEqualToString:this_key]) 1766 NSAssert3 (0, 1767 @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"", 1768 menu_key, this_key, this_val); 1769 if (this_key) 1770 menu_key = this_key; 1771 1772 /* If this menu has the cmd line "-mode foo" then set this item's 1773 value to "foo" (the menu itself will be bound to e.g. "modeString") 1774 */ 1775# ifndef USE_IPHONE 1776 set_menu_item_object (item, this_val); 1777# else 1778 // Array holds ["Label", "resource-key", "resource-val"]. 1779 [items addObject:[NSMutableArray arrayWithObjects: 1780 label, @"", this_val, nil]]; 1781# endif 1782 1783 } else { 1784 // no arg-set -- only one menu item can be missing that. 1785 NSAssert1 (!def_item, @"no arg-set in \"%@\"", label); 1786# ifndef USE_IPHONE 1787 def_item = item; 1788# else 1789 def_item = label; 1790 // Array holds ["Label", "resource-key", "resource-val"]. 1791 [items addObject:[NSMutableArray arrayWithObjects: 1792 label, @"", @"", nil]]; 1793# endif 1794 } 1795 1796 /* make sure the menu button has room for the text of this item, 1797 and remember the greatest width it has reached. 1798 */ 1799# ifndef USE_IPHONE 1800 [popup setTitle:label]; 1801 [popup sizeToFit]; 1802 NSRect r = [popup frame]; 1803 if (r.size.width > max_width) max_width = r.size.width; 1804# endif // USE_IPHONE 1805 } 1806 1807 if (!menu_key) { 1808 NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]); 1809 return; 1810 } 1811 1812 /* We've added all of the menu items. If there was an item with no 1813 command-line switch, then it's the item that represents the default 1814 value. Now we must bind to that item as well... (We have to bind 1815 this one late, because if it was the first item, then we didn't 1816 yet know what resource was associated with this menu.) 1817 */ 1818 if (def_item) { 1819 NSObject *def_obj = [defaultOptions objectForKey:menu_key]; 1820 NSAssert2 (def_obj, 1821 @"no default value for resource \"%@\" in menu item \"%@\"", 1822 menu_key, 1823# ifndef USE_IPHONE 1824 [def_item title] 1825# else 1826 def_item 1827# endif 1828 ); 1829 1830# ifndef USE_IPHONE 1831 set_menu_item_object (def_item, def_obj); 1832# else // !USE_IPHONE 1833 for (NSMutableArray *a in items) { 1834 // Make sure each array contains the resource key. 1835 [a replaceObjectAtIndex:1 withObject:menu_key]; 1836 // Make sure the default item contains the default resource value. 1837 if (def_obj && def_item && 1838 [def_item isEqualToString:[a objectAtIndex:0]]) 1839 [a replaceObjectAtIndex:2 withObject:def_obj]; 1840 } 1841# endif // !USE_IPHONE 1842 } 1843 1844# ifndef USE_IPHONE 1845# ifdef USE_PICKER_VIEW 1846 /* Finish tweaking the menu button itself. 1847 */ 1848 if (def_item) 1849 [popup setTitle:[def_item title]]; 1850 NSRect r = [popup frame]; 1851 r.size.width = max_width; 1852 [popup setFrame:r]; 1853# endif // USE_PICKER_VIEW 1854# endif 1855 1856# if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW) 1857 [self placeChild:popup on:parent]; 1858 if (disabled) 1859 [popup setEnabled:NO]; 1860 else 1861 [self bindResource:popup key:menu_key]; 1862 [popup release]; 1863# endif 1864 1865# ifdef USE_IPHONE 1866# ifdef USE_PICKER_VIEW 1867 // Store the items for this picker in the picker_values array. 1868 // This is so fucking stupid. 1869 1870 unsigned long menu_number = [pref_keys count] - 1; 1871 if (! picker_values) 1872 picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain]; 1873 while ([picker_values count] <= menu_number) 1874 [picker_values addObject:[NSArray arrayWithObjects: nil]]; 1875 [picker_values replaceObjectAtIndex:menu_number withObject:items]; 1876 [popup reloadAllComponents]; 1877 1878# else // !USE_PICKER_VIEW 1879 1880 [self placeSeparator]; 1881 1882 i = 0; 1883 for (__attribute__((unused)) NSArray *item in items) { 1884 RadioButton *b = [[RadioButton alloc] initWithIndex: (int)i 1885 items:items]; 1886 [b setLineBreakMode:NSLineBreakByTruncatingHead]; 1887 [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]]; 1888 [self placeChild:b on:parent]; 1889 [b release]; 1890 i++; 1891 } 1892 1893 [self placeSeparator]; 1894 1895# endif // !USE_PICKER_VIEW 1896# endif // !USE_IPHONE 1897 1898} 1899 1900- (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent 1901{ 1902 [self makeOptionMenu:node on:parent disabled:NO]; 1903} 1904 1905 1906/* Creates an uneditable, wrapping NSTextField to display the given 1907 text enclosed by <description> ... </description> in the XML. 1908 */ 1909- (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent 1910{ 1911 NSString *text = nil; 1912 NSArray *children = [node children]; 1913 NSUInteger i, count = [children count]; 1914 1915 for (i = 0; i < count; i++) { 1916 NSXMLNode *child = [children objectAtIndex:i]; 1917 NSString *s = [child objectValue]; 1918 if (text) 1919 text = [text stringByAppendingString:s]; 1920 else 1921 text = s; 1922 } 1923 1924 text = unwrap (text); 1925 1926 NSRect rect = [parent frame]; 1927 rect.origin.x = rect.origin.y = 0; 1928 rect.size.width = 200; 1929 rect.size.height = 50; // sized later 1930# ifndef USE_IPHONE 1931 NSText *lab = [[NSText alloc] initWithFrame:rect]; 1932 [lab autorelease]; 1933 [lab setEditable:NO]; 1934 [lab setDrawsBackground:NO]; 1935 [lab setHorizontallyResizable:YES]; 1936 [lab setVerticallyResizable:YES]; 1937 [lab setString:text]; 1938 hreffify (lab); 1939 boldify (lab); 1940 [lab sizeToFit]; 1941 1942# else // USE_IPHONE 1943 1944# ifndef USE_HTML_LABELS 1945 1946 UILabel *lab = [self makeLabel:text]; 1947 [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]]; 1948 hreffify (lab); 1949 1950# else // USE_HTML_LABELS 1951 HTMLLabel *lab = [[HTMLLabel alloc] 1952 initWithText:text 1953 font:[NSFont systemFontOfSize: [NSFont systemFontSize]]]; 1954 [lab autorelease]; 1955 [lab setFrame:rect]; 1956 [lab sizeToFit]; 1957# endif // USE_HTML_LABELS 1958 1959 [self placeSeparator]; 1960 1961# endif // USE_IPHONE 1962 1963 [self placeChild:lab on:parent]; 1964} 1965 1966 1967/* Creates the NSTextField described by the given XML node. 1968 */ 1969- (void) makeTextField: (NSXMLNode *)node 1970 on: (NSView *)parent 1971 withLabel: (BOOL) label_p 1972 horizontal: (BOOL) horiz_p 1973{ 1974 NSMutableDictionary *dict = [@{ @"id": @"", 1975 @"_label": @"", 1976 @"arg": @"" } 1977 mutableCopy]; 1978 [self parseAttrs:dict node:node]; 1979 NSString *label = [dict objectForKey:@"_label"]; 1980 NSString *arg = [dict objectForKey:@"arg"]; 1981 [dict release]; 1982 dict = 0; 1983 1984 if (!label && label_p) { 1985 NSAssert1 (0, @"no _label in %@", [node name]); 1986 return; 1987 } 1988 1989 NSAssert1 (arg, @"no arg in %@", label); 1990 1991 NSRect rect; 1992 rect.origin.x = rect.origin.y = 0; 1993 rect.size.width = rect.size.height = 10; 1994 1995 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect]; 1996 1997# ifndef USE_IPHONE 1998 1999 // make the default size be around 30 columns; a typical value for 2000 // these text fields is "xscreensaver-text --cols 40". 2001 // 2002 [txt setStringValue: 2003 NSLocalizedString(@"123456789 123456789 123456789 ", @"")]; 2004 [txt sizeToFit]; 2005 [[txt cell] setWraps:NO]; 2006 [[txt cell] setScrollable:YES]; 2007 [txt setStringValue:@""]; 2008 2009# else // USE_IPHONE 2010 2011 txt.adjustsFontSizeToFitWidth = YES; 2012 // Why did I do this? Messes up dark mode. 2013 // txt.textColor = [UIColor blackColor]; 2014 txt.font = [UIFont systemFontOfSize: FONT_SIZE]; 2015 txt.placeholder = @""; 2016 txt.borderStyle = UITextBorderStyleRoundedRect; 2017 txt.textAlignment = NSTextAlignmentRight; 2018 txt.keyboardType = UIKeyboardTypeDefault; // Full kbd 2019 txt.autocorrectionType = UITextAutocorrectionTypeNo; 2020 txt.autocapitalizationType = UITextAutocapitalizationTypeNone; 2021 txt.clearButtonMode = UITextFieldViewModeAlways; 2022 txt.returnKeyType = UIReturnKeyDone; 2023 txt.delegate = self; 2024 txt.text = @""; 2025 [txt setEnabled: YES]; 2026 2027 rect.size.height = [txt.font lineHeight] * 1.2; 2028 [txt setFrame:rect]; 2029 2030# endif // USE_IPHONE 2031 2032 if (label) { 2033 LABEL *lab = [self makeLabel:label]; 2034 [self placeChild:lab on:parent]; 2035 } 2036 2037 [self placeChild:txt on:parent right:(label ? YES : NO)]; 2038 2039 [self bindSwitch:txt cmdline:arg]; 2040 [txt release]; 2041} 2042 2043 2044/* Creates the NSTextField described by the given XML node, 2045 and hooks it up to a Choose button and a file selector widget. 2046 */ 2047- (void) makeFileSelector: (NSXMLNode *)node 2048 on: (NSView *)parent 2049 dirsOnly: (BOOL) dirsOnly 2050 withLabel: (BOOL) label_p 2051 editable: (BOOL) editable_p 2052{ 2053# ifndef USE_IPHONE // No files. No selectors. 2054 NSMutableDictionary *dict = [@{ @"id": @"", 2055 @"_label": @"", 2056 @"arg": @"" } 2057 mutableCopy]; 2058 [self parseAttrs:dict node:node]; 2059 NSString *label = [dict objectForKey:@"_label"]; 2060 NSString *arg = [dict objectForKey:@"arg"]; 2061 [dict release]; 2062 dict = 0; 2063 2064 if (!label && label_p) { 2065 NSAssert1 (0, @"no _label in %@", [node name]); 2066 return; 2067 } 2068 2069 NSAssert1 (arg, @"no arg in %@", label); 2070 2071 NSRect rect; 2072 rect.origin.x = rect.origin.y = 0; 2073 rect.size.width = rect.size.height = 10; 2074 2075 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect]; 2076 2077 // make the default size be around 20 columns. 2078 // 2079 [txt setStringValue:NSLocalizedString(@"123456789 123456789 ", @"")]; 2080 [txt sizeToFit]; 2081 [txt setSelectable:YES]; 2082 [txt setEditable:editable_p]; 2083 [txt setBezeled:editable_p]; 2084 [txt setDrawsBackground:editable_p]; 2085 [[txt cell] setWraps:NO]; 2086 [[txt cell] setScrollable:YES]; 2087 [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead]; 2088 [txt setStringValue:@""]; 2089 2090 LABEL *lab = 0; 2091 if (label) { 2092 lab = [self makeLabel:label]; 2093 [self placeChild:lab on:parent]; 2094 } 2095 2096 [self placeChild:txt on:parent right:(label ? YES : NO)]; 2097 2098 [self bindSwitch:txt cmdline:arg]; 2099 [txt release]; 2100 2101 // Make the text field and label be the same height, whichever is taller. 2102 if (lab) { 2103 rect = [txt frame]; 2104 rect.size.height = ([lab frame].size.height > [txt frame].size.height 2105 ? [lab frame].size.height 2106 : [txt frame].size.height); 2107 [txt setFrame:rect]; 2108 } 2109 2110 // Now put a "Choose" button next to it. 2111 // 2112 rect.origin.x = rect.origin.y = 0; 2113 rect.size.width = rect.size.height = 10; 2114 NSButton *choose = [[NSButton alloc] initWithFrame:rect]; 2115 [choose setTitle:NSLocalizedString(@"Choose...", @"")]; 2116 [choose setBezelStyle:NSRoundedBezelStyle]; 2117 [choose sizeToFit]; 2118 2119 [self placeChild:choose on:parent right:YES]; 2120 2121 // center the Choose button around the midpoint of the text field. 2122 rect = [choose frame]; 2123 rect.origin.y = ([txt frame].origin.y + 2124 (([txt frame].size.height - rect.size.height) / 2)); 2125 [choose setFrameOrigin:rect.origin]; 2126 2127 [choose setTarget:[parent window]]; 2128 if (dirsOnly) 2129 [choose setAction:@selector(fileSelectorChooseDirsAction:)]; 2130 else 2131 [choose setAction:@selector(fileSelectorChooseAction:)]; 2132 2133 [choose release]; 2134# endif // !USE_IPHONE 2135} 2136 2137 2138# ifndef USE_IPHONE 2139 2140/* Runs a modal file selector and sets the text field's value to the 2141 selected file or directory. 2142 */ 2143static void 2144do_file_selector (NSTextField *txt, BOOL dirs_p) 2145{ 2146 NSOpenPanel *panel = [NSOpenPanel openPanel]; 2147 [panel setAllowsMultipleSelection:NO]; 2148 [panel setCanChooseFiles:!dirs_p]; 2149 [panel setCanChooseDirectories:dirs_p]; 2150 [panel setCanCreateDirectories:NO]; 2151 2152 NSString *def = [[txt stringValue] stringByExpandingTildeInPath]; 2153 if (dirs_p) { 2154 // Open in the previously-selected directory. 2155 [panel setDirectoryURL: 2156 [NSURL fileURLWithPath:def isDirectory:YES]]; 2157 [panel setNameFieldStringValue:[def lastPathComponent]]; 2158 } else { 2159 // Open in the directory of the previously-selected file. 2160 [panel setDirectoryURL: 2161 [NSURL fileURLWithPath:[def stringByDeletingLastPathComponent] 2162 isDirectory:YES]]; 2163 // I hoped that this would select that file by default, but it does not. 2164 [panel setNameFieldStringValue:[def lastPathComponent]]; 2165 } 2166 2167 NSInteger result = [panel runModal]; 2168 if (result == NSOKButton) { 2169 NSArray *files = [panel URLs]; 2170 NSString *file = ([files count] > 0 ? [[files objectAtIndex:0] path] : @""); 2171 file = [file stringByAbbreviatingWithTildeInPath]; 2172 [txt setStringValue:file]; 2173 2174 // Fuck me! Just setting the value of the NSTextField does not cause 2175 // that to end up in the preferences! 2176 // 2177 [[txt window] makeFirstResponder:nil]; // And this doesn't fix it. 2178 2179 // So set the value manually. 2180 NSDictionary *dict = [txt infoForBinding:@"value"]; 2181 NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"]; 2182 NSString *path = [dict objectForKey:@"NSObservedKeyPath"]; 2183 if ([path hasPrefix:@"values."]) // WTF. 2184 path = [path substringFromIndex:7]; 2185 [[prefs values] setValue:file forKey:path]; 2186 } 2187} 2188 2189 2190/* Returns the NSTextField that is to the left of or above the NSButton. 2191 */ 2192static NSTextField * 2193find_text_field_of_button (NSButton *button) 2194{ 2195 NSView *parent = [button superview]; 2196 NSArray *kids = [parent subviews]; 2197 NSUInteger nkids = [kids count]; 2198 int i; 2199 NSTextField *f = 0; 2200 for (i = 0; i < nkids; i++) { 2201 NSObject *kid = [kids objectAtIndex:i]; 2202 if ([kid isKindOfClass:[NSTextField class]]) { 2203 f = (NSTextField *) kid; 2204 } else if (kid == button) { 2205 if (! f) abort(); 2206 return f; 2207 } 2208 } 2209 abort(); 2210} 2211 2212 2213- (void) fileSelectorChooseAction:(NSObject *)arg 2214{ 2215 NSButton *choose = (NSButton *) arg; 2216 NSTextField *txt = find_text_field_of_button (choose); 2217 do_file_selector (txt, NO); 2218} 2219 2220- (void) fileSelectorChooseDirsAction:(NSObject *)arg 2221{ 2222 NSButton *choose = (NSButton *) arg; 2223 NSTextField *txt = find_text_field_of_button (choose); 2224 do_file_selector (txt, YES); 2225} 2226 2227#endif // !USE_IPHONE 2228 2229 2230- (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent 2231{ 2232# ifndef USE_IPHONE 2233 /* 2234 Display Text: 2235 (x) Computer name and time 2236 ( ) Text [__________________________] 2237 ( ) Text file [_________________] [Choose] 2238 ( ) URL [__________________________] 2239 ( ) Shell Cmd [__________________________] 2240 2241 textMode -text-mode date 2242 textMode -text-mode literal textLiteral -text-literal % 2243 textMode -text-mode file textFile -text-file % 2244 textMode -text-mode url textURL -text-url % 2245 textMode -text-mode program textProgram -text-program % 2246 */ 2247 NSRect rect; 2248 rect.size.width = rect.size.height = 1; 2249 rect.origin.x = rect.origin.y = 0; 2250 NSView *group = [[NSView alloc] initWithFrame:rect]; 2251 NSView *rgroup = [[NSView alloc] initWithFrame:rect]; 2252 2253 Bool program_p = TRUE; 2254 2255 2256 NSView *control; 2257 2258 // This is how you link radio buttons together. 2259 // 2260 NSButtonCell *proto = [[NSButtonCell alloc] init]; 2261 [proto setButtonType:NSRadioButton]; 2262 2263 rect.origin.x = rect.origin.y = 0; 2264 rect.size.width = rect.size.height = 10; 2265 NSMatrix *matrix = [[NSMatrix alloc] 2266 initWithFrame:rect 2267 mode:NSRadioModeMatrix 2268 prototype:proto 2269 numberOfRows: 4 + (program_p ? 1 : 0) 2270 numberOfColumns:1]; 2271 [matrix setAllowsEmptySelection:NO]; 2272 2273 NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil]; 2274 [cnames addObject:@"Computer name and time"]; 2275 [cnames addObject:@"Text"]; 2276 [cnames addObject:@"File"]; 2277 [cnames addObject:@"URL"]; 2278 if (program_p) [cnames addObject:@"Shell Cmd"]; 2279 [matrix bind:@"content" 2280 toObject:cnames 2281 withKeyPath:@"arrangedObjects" 2282 options:nil]; 2283 [cnames release]; 2284 2285 [self bindSwitch:matrix cmdline:@"-text-mode %"]; 2286 2287 [self placeChild:matrix on:group]; 2288 [self placeChild:rgroup on:group right:YES]; 2289 [proto release]; 2290 [matrix release]; 2291 [rgroup release]; 2292 2293 NSXMLNode *node2; 2294 2295# else // USE_IPHONE 2296 2297 NSView *rgroup = parent; 2298 NSXMLNode *node2; 2299 2300 // <select id="textMode"> 2301 // <option id="date" _label="Display date" arg-set="-text-mode date"/> 2302 // <option id="text" _label="Display text" arg-set="-text-mode literal"/> 2303 // <option id="url" _label="Display URL"/> 2304 // </select> 2305 2306 node2 = [[NSXMLElement alloc] initWithName:@"select"]; 2307 [node2 setAttributesAsDictionary:@{ @"id": @"textMode" }]; 2308 2309 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"]; 2310 [node3 setAttributesAsDictionary: 2311 @{ @"id": @"date", 2312 @"arg-set": @"-text-mode date", 2313 @"_label": @"Display the date and time" }]; 2314 [node3 setParent: node2]; 2315 [node3 autorelease]; 2316 2317 node3 = [[NSXMLElement alloc] initWithName:@"option"]; 2318 [node3 setAttributesAsDictionary: 2319 @{ @"id": @"text", 2320 @"arg-set": @"-text-mode literal", 2321 @"_label": @"Display static text" }]; 2322 [node3 setParent: node2]; 2323 [node3 autorelease]; 2324 2325 node3 = [[NSXMLElement alloc] initWithName:@"option"]; 2326 [node3 setAttributesAsDictionary: 2327 @{ @"id": @"url", 2328 @"_label": @"Display the contents of a URL" }]; 2329 [node3 setParent: node2]; 2330 [node3 autorelease]; 2331 2332 [self makeOptionMenu:node2 on:rgroup]; 2333 [node2 release]; 2334 2335# endif // USE_IPHONE 2336 2337 2338 // <string id="textLiteral" _label="" arg-set="-text-literal %"/> 2339 node2 = [[NSXMLElement alloc] initWithName:@"string"]; 2340 [node2 setAttributesAsDictionary: 2341 @{ @"id": @"textLiteral", 2342 @"arg": @"-text-literal %", 2343# ifdef USE_IPHONE 2344 @"_label": @"Text to display" 2345# endif 2346 }]; 2347 [self makeTextField:node2 on:rgroup 2348# ifndef USE_IPHONE 2349 withLabel:NO 2350# else 2351 withLabel:YES 2352# endif 2353 horizontal:NO]; 2354 [node2 release]; 2355 2356// rect = [last_child(rgroup) frame]; 2357 2358/* // trying to make the text fields be enabled only when the checkbox is on.. 2359 control = last_child (rgroup); 2360 [control bind:@"enabled" 2361 toObject:[matrix cellAtRow:1 column:0] 2362 withKeyPath:@"value" 2363 options:nil]; 2364 */ 2365 2366 2367# ifndef USE_IPHONE 2368 // <file id="textFile" _label="" arg-set="-text-file %"/> 2369 node2 = [[NSXMLElement alloc] initWithName:@"string"]; 2370 [node2 setAttributesAsDictionary: 2371 @{ @"id": @"textFile", 2372 @"arg": @"-text-file %" }]; 2373 [self makeFileSelector:node2 on:rgroup 2374 dirsOnly:NO withLabel:NO editable:NO]; 2375 [node2 release]; 2376# endif // !USE_IPHONE 2377 2378// rect = [last_child(rgroup) frame]; 2379 2380 // <string id="textURL" _label="" arg-set="text-url %"/> 2381 node2 = [[NSXMLElement alloc] initWithName:@"string"]; 2382 [node2 setAttributesAsDictionary: 2383 @{ @"id": @"textURL", 2384 @"arg": @"-text-url %", 2385# ifdef USE_IPHONE 2386 @"_label": @"URL to display", 2387# endif 2388 }]; 2389 [self makeTextField:node2 on:rgroup 2390# ifndef USE_IPHONE 2391 withLabel:NO 2392# else 2393 withLabel:YES 2394# endif 2395 horizontal:NO]; 2396 [node2 release]; 2397 2398// rect = [last_child(rgroup) frame]; 2399 2400# ifndef USE_IPHONE 2401 if (program_p) { 2402 // <string id="textProgram" _label="" arg-set="text-program %"/> 2403 node2 = [[NSXMLElement alloc] initWithName:@"string"]; 2404 [node2 setAttributesAsDictionary: 2405 @{ @"id": @"textProgram", 2406 @"arg": @"-text-program %", 2407 }]; 2408 [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO]; 2409 [node2 release]; 2410 } 2411 2412// rect = [last_child(rgroup) frame]; 2413 2414 layout_group (rgroup, NO); 2415 2416 rect = [rgroup frame]; 2417 rect.size.width += 35; // WTF? Why is rgroup too narrow? 2418 [rgroup setFrame:rect]; 2419 2420 2421 // Set the height of the cells in the radio-box matrix to the height of 2422 // the (last of the) text fields. 2423 control = last_child (rgroup); 2424 rect = [control frame]; 2425 rect.size.width = 30; // width of the string "Text", plus a bit... 2426 if (program_p) 2427 rect.size.width += 25; 2428 rect.size.height += LINE_SPACING; 2429 [matrix setCellSize:rect.size]; 2430 [matrix sizeToCells]; 2431 2432 layout_group (group, YES); 2433 rect = [matrix frame]; 2434 rect.origin.x += rect.size.width + COLUMN_SPACING; 2435 rect.origin.y -= [control frame].size.height - LINE_SPACING; 2436 [rgroup setFrameOrigin:rect.origin]; 2437 2438 // now cheat on the size of the matrix: allow it to overlap (underlap) 2439 // the text fields. 2440 // 2441 rect.size = [matrix cellSize]; 2442 rect.size.width = 300; 2443 [matrix setCellSize:rect.size]; 2444 [matrix sizeToCells]; 2445 2446 // Cheat on the position of the stuff on the right (the rgroup). 2447 // GAAAH, this code is such crap! 2448 rect = [rgroup frame]; 2449 rect.origin.y -= 5; 2450 [rgroup setFrame:rect]; 2451 2452 2453 rect.size.width = rect.size.height = 0; 2454 NSBox *box = [[NSBox alloc] initWithFrame:rect]; 2455 [box setTitlePosition:NSAtTop]; 2456 [box setBorderType:NSBezelBorder]; 2457 [box setTitle:NSLocalizedString(@"Display Text", @"")]; 2458 2459 rect.size.width = rect.size.height = 12; 2460 [box setContentViewMargins:rect.size]; 2461 [box setContentView:group]; 2462 [box sizeToFit]; 2463 2464 [self placeChild:box on:parent]; 2465 [group release]; 2466 [box release]; 2467 2468# endif // !USE_IPHONE 2469} 2470 2471 2472- (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent 2473{ 2474 /* 2475 [x] Grab desktop images 2476 [ ] Choose random image: 2477 [__________________________] [Choose] 2478 2479 <boolean id="grabDesktopImages" _label="Grab desktop images" 2480 arg-unset="-no-grab-desktop"/> 2481 <boolean id="chooseRandomImages" _label="Grab desktop images" 2482 arg-unset="-choose-random-images"/> 2483 <file id="imageDirectory" _label="" arg-set="-image-directory %"/> 2484 */ 2485 2486 NSXMLElement *node2; 2487 2488# ifndef USE_IPHONE 2489# define SCREENS "Grab desktop images" 2490# define PHOTOS "Choose random images" 2491# else 2492# define SCREENS "Grab screenshots" 2493# define PHOTOS "Use photo library" 2494# endif 2495 2496 node2 = [[NSXMLElement alloc] initWithName:@"boolean"]; 2497 [node2 setAttributesAsDictionary: 2498 @{ @"id": @"grabDesktopImages", 2499 @"_label": @ SCREENS, 2500 @"arg-unset": @"-no-grab-desktop", 2501 }]; 2502 [self makeCheckbox:node2 on:parent]; 2503 [node2 release]; 2504 2505 node2 = [[NSXMLElement alloc] initWithName:@"boolean"]; 2506 [node2 setAttributesAsDictionary: 2507 @{ @"id": @"chooseRandomImages", 2508 @"_label": @ PHOTOS, 2509 @"arg-set": @"-choose-random-images", 2510 }]; 2511 [self makeCheckbox:node2 on:parent]; 2512 [node2 release]; 2513 2514 node2 = [[NSXMLElement alloc] initWithName:@"string"]; 2515 [node2 setAttributesAsDictionary: 2516 @{ @"id": @"imageDirectory", 2517 @"_label": @"Images from:", 2518 @"arg": @"-image-directory %", 2519 }]; 2520 [self makeFileSelector:node2 on:parent 2521 dirsOnly:YES withLabel:YES editable:YES]; 2522 [node2 release]; 2523 2524# undef SCREENS 2525# undef PHOTOS 2526 2527# ifndef USE_IPHONE 2528 // Add a second, explanatory label below the file/URL selector. 2529 2530 LABEL *lab2 = 0; 2531 lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"]; 2532 [self placeChild:lab2 on:parent]; 2533 2534 // Pack it in a little tighter vertically. 2535 NSRect r2 = [lab2 frame]; 2536 r2.origin.x += 20; 2537 r2.origin.y += 14; 2538 [lab2 setFrameOrigin:r2.origin]; 2539# endif // USE_IPHONE 2540} 2541 2542 2543- (void) makeUpdaterControlBox:(NSXMLNode *)node on:(NSView *)parent 2544{ 2545# ifndef USE_IPHONE 2546 /* 2547 [x] Check for Updates [ Monthly ] 2548 2549 <hgroup> 2550 <boolean id="automaticallyChecksForUpdates" 2551 _label="Automatically check for updates" 2552 arg-unset="-no-automaticallyChecksForUpdates" /> 2553 <select id="updateCheckInterval"> 2554 <option="hourly" _label="Hourly" arg-set="-updateCheckInterval 3600"/> 2555 <option="daily" _label="Daily" arg-set="-updateCheckInterval 86400"/> 2556 <option="weekly" _label="Weekly" arg-set="-updateCheckInterval 604800"/> 2557 <option="monthly" _label="Monthly" arg-set="-updateCheckInterval 2629800"/> 2558 </select> 2559 </hgroup> 2560 */ 2561 2562 // <hgroup> 2563 2564 NSRect rect; 2565 rect.size.width = rect.size.height = 1; 2566 rect.origin.x = rect.origin.y = 0; 2567 NSView *group = [[NSView alloc] initWithFrame:rect]; 2568 2569 NSXMLElement *node2; 2570 2571 // <boolean ...> 2572 2573 node2 = [[NSXMLElement alloc] initWithName:@"boolean"]; 2574 [node2 setAttributesAsDictionary: 2575 @{ @"id": @SUSUEnableAutomaticChecksKey, 2576 @"_label": @"Automatically check for updates", 2577 @"arg-unset": @"-no-" SUSUEnableAutomaticChecksKey, 2578 @"disabled": (haveUpdater ? @"no" : @"yes") 2579 }]; 2580 [self makeCheckbox:node2 on:group]; 2581 [node2 release]; 2582 2583 // <select ...> 2584 2585 node2 = [[NSXMLElement alloc] initWithName:@"select"]; 2586 [node2 setAttributesAsDictionary: 2587 @{ @"id": @SUScheduledCheckIntervalKey }]; 2588 2589 // <option ...> 2590 2591 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"]; 2592 [node3 setAttributesAsDictionary: 2593 @{ @"id": @"hourly", 2594 @"arg-set": @"-" SUScheduledCheckIntervalKey " 3600", 2595 @"_label": @"Hourly" }]; 2596 [node3 setParent: node2]; 2597 [node3 autorelease]; 2598 2599 node3 = [[NSXMLElement alloc] initWithName:@"option"]; 2600 [node3 setAttributesAsDictionary: 2601 @{ @"id": @"daily", 2602 @"arg-set": @"-" SUScheduledCheckIntervalKey " 86400", 2603 @"_label": @"Daily" }]; 2604 [node3 setParent: node2]; 2605 [node3 autorelease]; 2606 2607 node3 = [[NSXMLElement alloc] initWithName:@"option"]; 2608 [node3 setAttributesAsDictionary: 2609 @{ @"id": @"weekly", 2610 // @"arg-set": @"-" SUScheduledCheckIntervalKey " 604800", 2611 @"_label": @"Weekly", 2612 }]; 2613 [node3 setParent: node2]; 2614 [node3 autorelease]; 2615 2616 node3 = [[NSXMLElement alloc] initWithName:@"option"]; 2617 [node3 setAttributesAsDictionary: 2618 @{ @"id": @"monthly", 2619 @"arg-set": @"-" SUScheduledCheckIntervalKey " 2629800", 2620 @"_label": @"Monthly", 2621 }]; 2622 [node3 setParent: node2]; 2623 [node3 autorelease]; 2624 2625 // </option> 2626 [self makeOptionMenu:node2 on:group disabled:!haveUpdater]; 2627 [node2 release]; 2628 2629 // </hgroup> 2630 layout_group (group, TRUE); 2631 2632 if (!haveUpdater) { 2633 // Add a second, explanatory label. 2634 LABEL *lab2 = 0; 2635 lab2 = [self makeLabel:@"XScreenSaverUpdater.app is not installed!\n" 2636 "Unable to check for updates."]; 2637 [self placeChild:lab2 on:group]; 2638 2639 // Pack it in a little tighter vertically. 2640 NSRect r2 = [lab2 frame]; 2641 r2.origin.x += -4; 2642 r2.origin.y += 14; 2643 [lab2 setFrameOrigin:r2.origin]; 2644 } 2645 2646 rect.size.width = rect.size.height = 0; 2647 NSBox *box = [[NSBox alloc] initWithFrame:rect]; 2648 [box setTitlePosition:NSNoTitle]; 2649 [box setBorderType:NSNoBorder]; 2650 [box setContentViewMargins:rect.size]; 2651 [box setContentView:group]; 2652 [box sizeToFit]; 2653 2654 [self placeChild:box on:parent]; 2655 2656 [group release]; 2657 [box release]; 2658 2659# endif // !USE_IPHONE 2660} 2661 2662 2663#pragma mark Layout for controls 2664 2665 2666# ifndef USE_IPHONE 2667static NSView * 2668last_child (NSView *parent) 2669{ 2670 NSArray *kids = [parent subviews]; 2671 NSUInteger nkids = [kids count]; 2672 if (nkids == 0) 2673 return 0; 2674 else 2675 return [kids objectAtIndex:nkids-1]; 2676} 2677#endif // USE_IPHONE 2678 2679 2680/* Add the child as a subview of the parent, positioning it immediately 2681 below or to the right of the previously-added child of that view. 2682 */ 2683- (void) placeChild: 2684# ifdef USE_IPHONE 2685 (NSObject *)child 2686# else 2687 (NSView *)child 2688# endif 2689 on:(NSView *)parent right:(BOOL)right_p 2690{ 2691# ifndef USE_IPHONE 2692 NSRect rect = [child frame]; 2693 NSView *last = last_child (parent); 2694 if (!last) { 2695 rect.origin.x = LEFT_MARGIN; 2696 rect.origin.y = ([parent frame].size.height - rect.size.height 2697 - LINE_SPACING); 2698 } else if (right_p) { 2699 rect = [last frame]; 2700 rect.origin.x += rect.size.width + COLUMN_SPACING; 2701 } else { 2702 rect = [last frame]; 2703 rect.origin.x = LEFT_MARGIN; 2704 rect.origin.y -= [child frame].size.height + LINE_SPACING; 2705 } 2706 NSRect r = [child frame]; 2707 r.origin = rect.origin; 2708 [child setFrame:r]; 2709 [parent addSubview:child]; 2710 2711# else // USE_IPHONE 2712 2713 /* Controls is an array of arrays of the controls, divided into sections. 2714 Each hgroup / vgroup gets a nested array, too, e.g.: 2715 2716 [ [ [ <label>, <checkbox> ], 2717 [ <label>, <checkbox> ], 2718 [ <label>, <checkbox> ] ], 2719 [ <label>, <text-field> ], 2720 [ <label>, <low-label>, <slider>, <high-label> ], 2721 [ <low-label>, <slider>, <high-label> ], 2722 <HTML-label> 2723 ]; 2724 2725 If an element begins with a label, it is terminal, otherwise it is a 2726 group. There are (currently) never more than 4 elements in a single 2727 terminal element. 2728 2729 A blank vertical spacer is placed between each hgroup / vgroup, 2730 by making each of those a new section in the TableView. 2731 */ 2732 if (! controls) 2733 controls = [[NSMutableArray arrayWithCapacity:10] retain]; 2734 if ([controls count] == 0) 2735 [controls addObject: [NSMutableArray arrayWithCapacity:10]]; 2736 NSMutableArray *current = [controls objectAtIndex:[controls count]-1]; 2737 2738 if (!right_p || [current count] == 0) { 2739 // Nothing on the current line. Add this object. 2740 [current addObject: child]; 2741 } else { 2742 // Something's on the current line already. 2743 NSObject *old = [current objectAtIndex:[current count]-1]; 2744 if ([old isKindOfClass:[NSMutableArray class]]) { 2745 // Already an array in this cell. Append. 2746 NSAssert ([(NSArray *) old count] < 4, @"internal error"); 2747 [(NSMutableArray *) old addObject: child]; 2748 } else { 2749 // Replace the control in this cell with an array, then append 2750 NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil]; 2751 [current replaceObjectAtIndex:[current count]-1 withObject:a]; 2752 } 2753 } 2754# endif // USE_IPHONE 2755} 2756 2757 2758- (void) placeChild:(NSView *)child on:(NSView *)parent 2759{ 2760 [self placeChild:child on:parent right:NO]; 2761} 2762 2763 2764#ifdef USE_IPHONE 2765 2766// Start putting subsequent children in a new group, to create a new 2767// section on the UITableView. 2768// 2769- (void) placeSeparator 2770{ 2771 if (! controls) return; 2772 if ([controls count] == 0) return; 2773 if ([[controls objectAtIndex:[controls count]-1] 2774 count] > 0) 2775 [controls addObject: [NSMutableArray arrayWithCapacity:10]]; 2776} 2777#endif // USE_IPHONE 2778 2779 2780 2781/* Creates an invisible NSBox (for layout purposes) to enclose the widgets 2782 wrapped in <hgroup> or <vgroup> in the XML. 2783 */ 2784- (void) makeGroup:(NSXMLNode *)node 2785 on:(NSView *)parent 2786 horizontal:(BOOL) horiz_p 2787{ 2788# ifdef USE_IPHONE 2789 if (!horiz_p) [self placeSeparator]; 2790 [self traverseChildren:node on:parent]; 2791 if (!horiz_p) [self placeSeparator]; 2792# else // !USE_IPHONE 2793 NSRect rect; 2794 rect.size.width = rect.size.height = 1; 2795 rect.origin.x = rect.origin.y = 0; 2796 NSView *group = [[NSView alloc] initWithFrame:rect]; 2797 [self traverseChildren:node on:group]; 2798 2799 layout_group (group, horiz_p); 2800 2801 rect.size.width = rect.size.height = 0; 2802 NSBox *box = [[NSBox alloc] initWithFrame:rect]; 2803 [box setTitlePosition:NSNoTitle]; 2804 [box setBorderType:NSNoBorder]; 2805 [box setContentViewMargins:rect.size]; 2806 [box setContentView:group]; 2807 [box sizeToFit]; 2808 2809 [self placeChild:box on:parent]; 2810 [group release]; 2811 [box release]; 2812# endif // !USE_IPHONE 2813} 2814 2815 2816#ifndef USE_IPHONE 2817static void 2818layout_group (NSView *group, BOOL horiz_p) 2819{ 2820 NSArray *kids = [group subviews]; 2821 NSUInteger nkids = [kids count]; 2822 NSUInteger i; 2823 double maxx = 0, miny = 0; 2824 for (i = 0; i < nkids; i++) { 2825 NSView *kid = [kids objectAtIndex:i]; 2826 NSRect r = [kid frame]; 2827 2828 if (horiz_p) { 2829 maxx += r.size.width + COLUMN_SPACING; 2830 if (r.size.height > -miny) miny = -r.size.height; 2831 } else { 2832 if (r.size.width > maxx) maxx = r.size.width; 2833 miny = r.origin.y - r.size.height; 2834 } 2835 } 2836 2837 NSRect rect; 2838 rect.origin.x = 0; 2839 rect.origin.y = 0; 2840 rect.size.width = maxx; 2841 rect.size.height = -miny; 2842 [group setFrame:rect]; 2843 2844 double x = 0; 2845 for (i = 0; i < nkids; i++) { 2846 NSView *kid = [kids objectAtIndex:i]; 2847 NSRect r = [kid frame]; 2848 if (horiz_p) { 2849 r.origin.y = rect.size.height - r.size.height; 2850 r.origin.x = x; 2851 x += r.size.width + COLUMN_SPACING; 2852 } else { 2853 r.origin.y -= miny; 2854 } 2855 [kid setFrame:r]; 2856 } 2857} 2858#endif // !USE_IPHONE 2859 2860 2861/* Create some kind of control corresponding to the given XML node. 2862 */ 2863-(void)makeControl:(NSXMLNode *)node on:(NSView *)parent 2864{ 2865 NSString *name = [node name]; 2866 2867 if ([node kind] == NSXMLCommentKind) 2868 return; 2869 2870 if ([node kind] == NSXMLTextKind) { 2871 NSString *s = [(NSString *) [node objectValue] 2872 stringByTrimmingCharactersInSet: 2873 [NSCharacterSet whitespaceAndNewlineCharacterSet]]; 2874 if (! [s isEqualToString:@""]) { 2875 NSAssert1 (0, @"unexpected text: %@", s); 2876 } 2877 return; 2878 } 2879 2880 if ([node kind] != NSXMLElementKind) { 2881 NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node); 2882 return; 2883 } 2884 2885 if ([name isEqualToString:@"hgroup"] || 2886 [name isEqualToString:@"vgroup"]) { 2887 2888 [self makeGroup:node on:parent 2889 horizontal:[name isEqualToString:@"hgroup"]]; 2890 2891 } else if ([name isEqualToString:@"command"]) { 2892 // do nothing: this is the "-root" business 2893 2894 } else if ([name isEqualToString:@"video"]) { 2895 // ignored 2896 2897 } else if ([name isEqualToString:@"boolean"]) { 2898 [self makeCheckbox:node on:parent]; 2899 2900 } else if ([name isEqualToString:@"string"]) { 2901 [self makeTextField:node on:parent withLabel:NO horizontal:NO]; 2902 2903 } else if ([name isEqualToString:@"file"]) { 2904 [self makeFileSelector:node on:parent 2905 dirsOnly:NO withLabel:YES editable:NO]; 2906 2907 } else if ([name isEqualToString:@"number"]) { 2908 [self makeNumberSelector:node on:parent]; 2909 2910 } else if ([name isEqualToString:@"select"]) { 2911 [self makeOptionMenu:node on:parent]; 2912 2913 } else if ([name isEqualToString:@"_description"]) { 2914 [self makeDescLabel:node on:parent]; 2915 2916 } else if ([name isEqualToString:@"xscreensaver-text"]) { 2917 [self makeTextLoaderControlBox:node on:parent]; 2918 2919 } else if ([name isEqualToString:@"xscreensaver-image"]) { 2920 [self makeImageLoaderControlBox:node on:parent]; 2921 2922 } else if ([name isEqualToString:@"xscreensaver-updater"]) { 2923 [self makeUpdaterControlBox:node on:parent]; 2924 2925 } else { 2926 NSAssert1 (0, @"unknown tag: %@", name); 2927 } 2928} 2929 2930 2931/* Iterate over and process the children of this XML node. 2932 */ 2933- (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent 2934{ 2935 NSArray *children = [node children]; 2936 NSUInteger i, count = [children count]; 2937 for (i = 0; i < count; i++) { 2938 NSXMLNode *child = [children objectAtIndex:i]; 2939 [self makeControl:child on:parent]; 2940 } 2941} 2942 2943 2944# ifndef USE_IPHONE 2945 2946/* Kludgey magic to make the window enclose the controls we created. 2947 */ 2948static void 2949fix_contentview_size (NSView *parent) 2950{ 2951 NSRect f; 2952 NSArray *kids = [parent subviews]; 2953 NSUInteger nkids = [kids count]; 2954 NSView *text = 0; // the NSText at the bottom of the window 2955 double maxx = 0, miny = 0; 2956 NSUInteger i; 2957 2958 /* Find the size of the rectangle taken up by each of the children 2959 except the final "NSText" child. 2960 */ 2961 for (i = 0; i < nkids; i++) { 2962 NSView *kid = [kids objectAtIndex:i]; 2963 if ([kid isKindOfClass:[NSText class]]) { 2964 text = kid; 2965 continue; 2966 } 2967 f = [kid frame]; 2968 if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width; 2969 if (f.origin.y - f.size.height < miny) miny = f.origin.y; 2970// NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@", 2971// f.size.width, f.size.height, f.origin.x, f.origin.y, 2972// f.origin.y + f.size.height, [kid class]); 2973 } 2974 2975 if (maxx < 400) maxx = 400; // leave room for the NSText paragraph... 2976 2977 /* Now that we know the width of the window, set the width of the NSText to 2978 that, so that it can decide what its height needs to be. 2979 */ 2980 if (! text) abort(); 2981 f = [text frame]; 2982// NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@", 2983// f.size.width, f.size.height, f.origin.x, f.origin.y, 2984// f.origin.y + f.size.height, [text class]); 2985 2986 // set the NSText's width (this changes its height). 2987 f.size.width = maxx - LEFT_MARGIN; 2988 [text setFrame:f]; 2989 2990 // position the NSText below the last child (this gives us a new miny). 2991 f = [text frame]; 2992 f.origin.y = miny - f.size.height - LINE_SPACING; 2993 miny = f.origin.y - LINE_SPACING; 2994 [text setFrame:f]; 2995 2996 // Lock the width of the field and unlock the height, and let it resize 2997 // once more, to compute the proper height of the text for that width. 2998 // 2999 [(NSText *) text setHorizontallyResizable:NO]; 3000 [(NSText *) text setVerticallyResizable:YES]; 3001 [(NSText *) text sizeToFit]; 3002 3003 // Now lock the height too: no more resizing this text field. 3004 // 3005 [(NSText *) text setVerticallyResizable:NO]; 3006 3007 // Now reposition the top edge of the text field to be back where it 3008 // was before we changed the height. 3009 // 3010 float oh = f.size.height; 3011 f = [text frame]; 3012 float dh = f.size.height - oh; 3013 f.origin.y += dh; 3014 3015 // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF?? 3016 // If we do this in 10.6, the text field moves down, off the window. 3017 // So instead we repair it at the end, at the "WTF2" comment. 3018 [text setFrame:f]; 3019 3020 // Also adjust the parent height by the change in height of the text field. 3021 miny -= dh; 3022 3023// NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@", 3024// f.size.width, f.size.height, f.origin.x, f.origin.y, 3025// f.origin.y + f.size.height, [text class]); 3026 3027 3028 /* Set the contentView to the size of the children. 3029 */ 3030 f = [parent frame]; 3031// float yoff = f.size.height; 3032 f.size.width = maxx + LEFT_MARGIN; 3033 f.size.height = -(miny - LEFT_MARGIN*2); 3034// yoff = f.size.height - yoff; 3035 [parent setFrame:f]; 3036 3037// NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f", 3038// f.size.width, f.size.height, f.origin.x, f.origin.y); 3039 3040 /* Now move all of the kids up into the window. 3041 */ 3042 f = [parent frame]; 3043 float shift = f.size.height; 3044// NSLog(@"shift: %3.0f", shift); 3045 for (i = 0; i < nkids; i++) { 3046 NSView *kid = [kids objectAtIndex:i]; 3047 f = [kid frame]; 3048 f.origin.y += shift; 3049 [kid setFrame:f]; 3050// NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@", 3051// f.size.width, f.size.height, f.origin.x, f.origin.y, 3052// f.origin.y + f.size.height, [kid class]); 3053 } 3054 3055/* 3056 Bad: 3057 parent: 420 x 541 @ 0 0 3058 text: 380 x 100 @ 20 22 miny=-501 3059 3060 Good: 3061 parent: 420 x 541 @ 0 0 3062 text: 380 x 100 @ 20 50 miny=-501 3063 */ 3064 3065 // #### WTF2: See "WTF" above. If the text field is off the screen, 3066 // move it up. We need this on 10.6 but not on 10.5. Auugh. 3067 // 3068 f = [text frame]; 3069 if (f.origin.y < 50) { // magic numbers, yay 3070 f.origin.y = 50; 3071 [text setFrame:f]; 3072 } 3073 3074 /* Set the kids to track the top left corner of the window when resized. 3075 Set the NSText to track the bottom right corner as well. 3076 */ 3077 for (i = 0; i < nkids; i++) { 3078 NSView *kid = [kids objectAtIndex:i]; 3079 unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin; 3080 if ([kid isKindOfClass:[NSText class]]) 3081 mask |= NSViewWidthSizable|NSViewHeightSizable; 3082 [kid setAutoresizingMask:mask]; 3083 } 3084} 3085# endif // !USE_IPHONE 3086 3087 3088 3089#ifndef USE_IPHONE 3090static NSView * 3091wrap_with_buttons (NSWindow *window, NSView *panel) 3092{ 3093 NSRect rect; 3094 3095 // Make a box to hold the buttons at the bottom of the window. 3096 // 3097 rect = [panel frame]; 3098 rect.origin.x = rect.origin.y = 0; 3099 rect.size.height = 10; 3100 NSBox *bbox = [[NSBox alloc] initWithFrame:rect]; 3101 [bbox setTitlePosition:NSNoTitle]; 3102 [bbox setBorderType:NSNoBorder]; 3103 3104 // Make some buttons: Default, Cancel, OK 3105 // 3106 rect.origin.x = rect.origin.y = 0; 3107 rect.size.width = rect.size.height = 10; 3108 NSButton *reset = [[NSButton alloc] initWithFrame:rect]; 3109 [reset setTitle:NSLocalizedString(@"Reset to Defaults", @"")]; 3110 [reset setBezelStyle:NSRoundedBezelStyle]; 3111 [reset sizeToFit]; 3112 3113 rect = [reset frame]; 3114 NSButton *ok = [[NSButton alloc] initWithFrame:rect]; 3115 [ok setTitle:NSLocalizedString(@"OK", @"")]; 3116 [ok setBezelStyle:NSRoundedBezelStyle]; 3117 [ok sizeToFit]; 3118 rect = [bbox frame]; 3119 rect.origin.x = rect.size.width - [ok frame].size.width; 3120 [ok setFrameOrigin:rect.origin]; 3121 3122 rect = [ok frame]; 3123 NSButton *cancel = [[NSButton alloc] initWithFrame:rect]; 3124 [cancel setTitle:NSLocalizedString(@"Cancel", @"")]; 3125 [cancel setBezelStyle:NSRoundedBezelStyle]; 3126 [cancel sizeToFit]; 3127 rect.origin.x -= [cancel frame].size.width + 10; 3128 [cancel setFrameOrigin:rect.origin]; 3129 3130 // Bind OK to RET and Cancel to ESC. 3131 [ok setKeyEquivalent:@"\r"]; 3132 [cancel setKeyEquivalent:@"\e"]; 3133 3134 // The correct width for OK and Cancel buttons is 68 pixels 3135 // ("Human Interface Guidelines: Controls: Buttons: 3136 // Push Button Specifications"). 3137 // 3138 rect = [ok frame]; 3139 rect.size.width = 68; 3140 [ok setFrame:rect]; 3141 3142 rect = [cancel frame]; 3143 rect.size.width = 68; 3144 [cancel setFrame:rect]; 3145 3146 // It puts the buttons in the box or else it gets the hose again 3147 // 3148 [bbox addSubview:ok]; 3149 [bbox addSubview:cancel]; 3150 [bbox addSubview:reset]; 3151 [bbox sizeToFit]; 3152 3153 // make a box to hold the button-box, and the preferences view 3154 // 3155 rect = [bbox frame]; 3156 rect.origin.y += rect.size.height; 3157 NSBox *pbox = [[NSBox alloc] initWithFrame:rect]; 3158 [pbox setTitlePosition:NSNoTitle]; 3159 [pbox setBorderType:NSBezelBorder]; 3160 3161 // Enforce a max height on the dialog, so that it's obvious to me 3162 // (on a big screen) when the dialog will fall off the bottom of 3163 // a small screen (e.g., 1024x768 laptop with a huge bottom dock). 3164 { 3165 NSRect f = [panel frame]; 3166 int screen_height = (768 // shortest "modern" Mac display 3167 - 22 // menu bar 3168 - 56 // System Preferences toolbar 3169 - 140 // default magnified bottom dock icon 3170 ); 3171 if (f.size.height > screen_height) { 3172 NSLog(@"%@ height was %.0f; clipping to %d", 3173 [panel class], f.size.height, screen_height); 3174 f.size.height = screen_height; 3175 [panel setFrame:f]; 3176 } 3177 } 3178 3179 [pbox addSubview:panel]; 3180 [pbox addSubview:bbox]; 3181 [pbox sizeToFit]; 3182 3183 [reset setAutoresizingMask:NSViewMaxXMargin]; 3184 [cancel setAutoresizingMask:NSViewMinXMargin]; 3185 [ok setAutoresizingMask:NSViewMinXMargin]; 3186 [bbox setAutoresizingMask:NSViewWidthSizable]; 3187 3188 // grab the clicks 3189 // 3190 [ok setTarget:window]; 3191 [cancel setTarget:window]; 3192 [reset setTarget:window]; 3193 [ok setAction:@selector(okAction:)]; 3194 [cancel setAction:@selector(cancelAction:)]; 3195 [reset setAction:@selector(resetAction:)]; 3196 3197 [bbox release]; 3198 3199 return pbox; 3200} 3201#endif // !USE_IPHONE 3202 3203 3204/* Iterate over and process the children of the root node of the XML document. 3205 */ 3206- (void)traverseTree 3207{ 3208# ifdef USE_IPHONE 3209 NSView *parent = [self view]; 3210# else 3211 NSWindow *parent = self; 3212#endif 3213 NSXMLNode *node = xml_root; 3214 3215 if (![[node name] isEqualToString:@"screensaver"]) { 3216 NSAssert (0, @"top level node is not <xscreensaver>"); 3217 } 3218 3219 saver_name = [self parseXScreenSaverTag: node]; 3220 saver_name = [saver_name stringByReplacingOccurrencesOfString:@" " 3221 withString:@""]; 3222 [saver_name retain]; 3223 3224# ifndef USE_IPHONE 3225 3226 NSRect rect; 3227 rect.origin.x = rect.origin.y = 0; 3228 rect.size.width = rect.size.height = 1; 3229 3230 NSView *panel = [[NSView alloc] initWithFrame:rect]; 3231 [self traverseChildren:node on:panel]; 3232 fix_contentview_size (panel); 3233 3234 NSView *root = wrap_with_buttons (parent, panel); 3235 [userDefaultsController setAppliesImmediately:NO]; 3236 [globalDefaultsController setAppliesImmediately:NO]; 3237 3238 [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; 3239 3240 rect = [parent frameRectForContentRect:[root frame]]; 3241 [parent setFrame:rect display:NO]; 3242 [parent setMinSize:rect.size]; 3243 3244 [parent setContentView:root]; 3245 3246 [panel release]; 3247 [root release]; 3248 3249# else // USE_IPHONE 3250 3251 CGRect r = [parent frame]; 3252 r.size = [[UIScreen mainScreen] bounds].size; 3253 [parent setFrame:r]; 3254 [self traverseChildren:node on:parent]; 3255 3256# endif // USE_IPHONE 3257} 3258 3259 3260- (void)parser:(NSXMLParser *)parser 3261 didStartElement:(NSString *)elt 3262 namespaceURI:(NSString *)ns 3263 qualifiedName:(NSString *)qn 3264 attributes:(NSDictionary *)attrs 3265{ 3266 NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt]; 3267 [e autorelease]; 3268 [e setKind:SimpleXMLElementKind]; 3269 [e setAttributesAsDictionary:attrs]; 3270 NSXMLElement *p = xml_parsing; 3271 [e setParent:p]; 3272 xml_parsing = e; 3273 if (! xml_root) 3274 xml_root = xml_parsing; 3275} 3276 3277- (void)parser:(NSXMLParser *)parser 3278 didEndElement:(NSString *)elt 3279 namespaceURI:(NSString *)ns 3280 qualifiedName:(NSString *)qn 3281{ 3282 NSXMLElement *p = xml_parsing; 3283 if (! p) { 3284 NSLog(@"extra close: %@", elt); 3285 } else if (![[p name] isEqualToString:elt]) { 3286 NSLog(@"%@ closed by %@", [p name], elt); 3287 } else { 3288 NSXMLElement *n = xml_parsing; 3289 xml_parsing = [n parent]; 3290 } 3291} 3292 3293 3294- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string 3295{ 3296 NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"]; 3297 [e setKind:SimpleXMLTextKind]; 3298 NSXMLElement *p = xml_parsing; 3299 [e setParent:p]; 3300 [e setObjectValue: string]; 3301 [e autorelease]; 3302} 3303 3304 3305# ifdef USE_IPHONE 3306# ifdef USE_PICKER_VIEW 3307 3308#pragma mark UIPickerView delegate methods 3309 3310- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv 3311{ 3312 return 1; // Columns 3313} 3314 3315- (NSInteger)pickerView:(UIPickerView *)pv 3316 numberOfRowsInComponent:(NSInteger)column 3317{ 3318 NSAssert (column == 0, @"weird column"); 3319 NSArray *a = [picker_values objectAtIndex: [pv tag]]; 3320 if (! a) return 0; // Too early? 3321 return [a count]; 3322} 3323 3324- (CGFloat)pickerView:(UIPickerView *)pv 3325 rowHeightForComponent:(NSInteger)column 3326{ 3327 return FONT_SIZE; 3328} 3329 3330- (CGFloat)pickerView:(UIPickerView *)pv 3331 widthForComponent:(NSInteger)column 3332{ 3333 NSAssert (column == 0, @"weird column"); 3334 NSArray *a = [picker_values objectAtIndex: [pv tag]]; 3335 if (! a) return 0; // Too early? 3336 3337 UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]]; 3338 CGFloat max = 0; 3339 for (NSArray *a2 in a) { 3340 NSString *s = [a2 objectAtIndex:0]; 3341 // #### sizeWithFont deprecated as of iOS 7; use boundingRectWithSize. 3342 CGSize r = [s sizeWithFont:f]; 3343 if (r.width > max) max = r.width; 3344 } 3345 3346 max *= 1.7; // WTF!! 3347 3348 if (max > 320) 3349 max = 320; 3350 else if (max < 120) 3351 max = 120; 3352 3353 return max; 3354 3355} 3356 3357 3358- (NSString *)pickerView:(UIPickerView *)pv 3359 titleForRow:(NSInteger)row 3360 forComponent:(NSInteger)column 3361{ 3362 NSAssert (column == 0, @"weird column"); 3363 NSArray *a = [picker_values objectAtIndex: [pv tag]]; 3364 if (! a) return 0; // Too early? 3365 a = [a objectAtIndex:row]; 3366 NSAssert (a, @"internal error"); 3367 return [a objectAtIndex:0]; 3368} 3369 3370# endif // USE_PICKER_VIEW 3371 3372 3373#pragma mark UITableView delegate methods 3374 3375- (void) addResetButton 3376{ 3377 [[self navigationItem] 3378 setRightBarButtonItem: [[UIBarButtonItem alloc] 3379 initWithTitle: 3380 NSLocalizedString(@"Reset to Defaults", @"") 3381 style: UIBarButtonItemStylePlain 3382 target:self 3383 action:@selector(resetAction:)]]; 3384 NSString *s = saver_name; 3385 if ([self view].frame.size.width > 320) 3386 s = [s stringByAppendingString: @" Settings"]; 3387 [self navigationItem].title = s; 3388} 3389 3390 3391#pragma clang diagnostic push 3392#pragma clang diagnostic ignored "-Wdeprecated-implementations" 3393- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o 3394{ 3395 return YES; /* Deprecated in iOS 6 */ 3396} 3397#pragma clang diagnostic pop 3398 3399- (BOOL)shouldAutorotate /* Added in iOS 6 */ 3400{ 3401 return YES; 3402} 3403 3404- (NSInteger)numberOfSectionsInTableView:(UITableView *)tv { 3405 // Number of vertically-stacked white boxes. 3406 return [controls count]; 3407} 3408 3409- (NSInteger)tableView:(UITableView *)tableView 3410 numberOfRowsInSection:(NSInteger)section 3411{ 3412 // Number of lines in each vertically-stacked white box. 3413 NSAssert (controls, @"internal error"); 3414 return [[controls objectAtIndex:section] count]; 3415} 3416 3417- (NSString *)tableView:(UITableView *)tv 3418 titleForHeaderInSection:(NSInteger)section 3419{ 3420 // Titles above each vertically-stacked white box. 3421// if (section == 0) 3422// return [saver_name stringByAppendingString:@" Settings"]; 3423 return nil; 3424} 3425 3426 3427- (CGFloat)tableView:(UITableView *)tv 3428 heightForRowAtIndexPath:(NSIndexPath *)ip 3429{ 3430 CGFloat h = 0; 3431 3432 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]] 3433 objectAtIndex:[ip indexAtPosition:1]]; 3434 3435 if ([ctl isKindOfClass:[NSArray class]]) { 3436 NSArray *set = (NSArray *) ctl; 3437 switch ([set count]) { 3438 case 4: // label + left/slider/right. 3439 case 3: // left/slider/right. 3440 h = FONT_SIZE * 3.0; 3441 break; 3442 case 2: // Checkboxes, or text fields. 3443 h = FONT_SIZE * 2.4; 3444 break; 3445 } 3446 } else if ([ctl isKindOfClass:[UILabel class]]) { 3447 // Radio buttons in a multi-select list. 3448 h = FONT_SIZE * 1.9; 3449 3450# ifdef USE_HTML_LABELS 3451 } else if ([ctl isKindOfClass:[HTMLLabel class]]) { 3452 3453 HTMLLabel *t = (HTMLLabel *) ctl; 3454 CGRect r = t.frame; 3455 r.size.width = [tv frame].size.width; 3456 r.size.width -= LEFT_MARGIN * 2; 3457 [t setFrame:r]; 3458 [t sizeToFit]; 3459 r = t.frame; 3460 h = r.size.height; 3461# endif // USE_HTML_LABELS 3462 3463 } else { // Does this ever happen? 3464 h = FONT_SIZE + LINE_SPACING * 2; 3465 } 3466 3467 if (h <= 0) abort(); 3468 return h; 3469} 3470 3471 3472- (void)refreshTableView 3473{ 3474 UITableView *tv = (UITableView *) [self view]; 3475 NSMutableArray *a = [NSMutableArray arrayWithCapacity:20]; 3476 NSInteger rows = [self numberOfSectionsInTableView:tv]; 3477 for (int i = 0; i < rows; i++) { 3478 NSInteger cols = [self tableView:tv numberOfRowsInSection:i]; 3479 for (int j = 0; j < cols; j++) { 3480 NSUInteger ip[2]; 3481 ip[0] = i; 3482 ip[1] = j; 3483 [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]]; 3484 } 3485 } 3486 3487 [tv beginUpdates]; 3488 [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone]; 3489 [tv endUpdates]; 3490 3491 // Default opacity looks bad. 3492 // #### Oh great, this only works *sometimes*. 3493 UIView *v = [[self navigationItem] titleView]; 3494 [v setBackgroundColor:[[v backgroundColor] colorWithAlphaComponent:1]]; 3495} 3496 3497 3498#pragma clang diagnostic push /* Deprecated in iOS 8 */ 3499#pragma clang diagnostic ignored "-Wdeprecated-implementations" 3500- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o 3501{ 3502 [NSTimer scheduledTimerWithTimeInterval: 0 3503 target:self 3504 selector:@selector(refreshTableView) 3505 userInfo:nil 3506 repeats:NO]; 3507} 3508#pragma clang diagnostic pop 3509 3510 3511#ifndef USE_PICKER_VIEW 3512 3513- (void)updateRadioGroupCell:(UITableViewCell *)cell 3514 button:(RadioButton *)b 3515{ 3516 NSArray *item = [[b items] objectAtIndex: [b index]]; 3517 NSString *pref_key = [item objectAtIndex:1]; 3518 NSObject *pref_val = [item objectAtIndex:2]; 3519 3520 NSObject *current = [[self controllerForKey:pref_key] objectForKey:pref_key]; 3521 3522 // Convert them both to strings and compare those, so that 3523 // we don't get screwed by int 1 versus string "1". 3524 // Will boolean true/1 screw us here too? 3525 // 3526 NSString *pref_str = ([pref_val isKindOfClass:[NSString class]] 3527 ? (NSString *) pref_val 3528 : [(NSNumber *) pref_val stringValue]); 3529 NSString *current_str = ([current isKindOfClass:[NSString class]] 3530 ? (NSString *) current 3531 : [(NSNumber *) current stringValue]); 3532 BOOL match_p = [current_str isEqualToString:pref_str]; 3533 3534 // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str); 3535 3536 if (match_p) 3537 [cell setAccessoryType:UITableViewCellAccessoryCheckmark]; 3538 else 3539 [cell setAccessoryType:UITableViewCellAccessoryNone]; 3540} 3541 3542 3543- (void)tableView:(UITableView *)tv 3544 didSelectRowAtIndexPath:(NSIndexPath *)ip 3545{ 3546 RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]] 3547 objectAtIndex:[ip indexAtPosition:1]]; 3548 if (! [ctl isKindOfClass:[RadioButton class]]) 3549 return; 3550 3551 [self radioAction:ctl]; 3552 [self refreshTableView]; 3553} 3554 3555 3556#endif // !USE_PICKER_VIEW 3557 3558 3559 3560- (UITableViewCell *)tableView:(UITableView *)tv 3561 cellForRowAtIndexPath:(NSIndexPath *)ip 3562{ 3563 CGFloat ww = [tv frame].size.width; 3564 CGFloat hh = [self tableView:tv heightForRowAtIndexPath:ip]; 3565 3566 float os_version = [[[UIDevice currentDevice] systemVersion] floatValue]; 3567 3568 // Width of the column of labels on the left. 3569 CGFloat left_width = ww * 0.4; 3570 CGFloat right_edge = ww - LEFT_MARGIN; 3571 3572 if (os_version < 7) // margins were wider on iOS 6.1 3573 right_edge -= 10; 3574 3575 CGFloat max = FONT_SIZE * 12; 3576 if (left_width > max) left_width = max; 3577 3578 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]] 3579 objectAtIndex:[ip indexAtPosition:1]]; 3580 3581 if ([ctl isKindOfClass:[NSArray class]]) { 3582 // This cell has a set of objects in it. 3583 NSArray *set = (NSArray *) ctl; 3584 switch ([set count]) { 3585 case 2: 3586 { 3587 // With 2 elements, the first of the pair must be a label. 3588 UILabel *label = (UILabel *) [set objectAtIndex: 0]; 3589 NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type"); 3590 ctl = [set objectAtIndex: 1]; 3591 3592 CGRect r = [ctl frame]; 3593 3594 if ([ctl isKindOfClass:[UISwitch class]]) { // Checkboxes. 3595 r.size.width = 80; // Magic. 3596 r.origin.x = right_edge - r.size.width + 30; // beats me 3597 3598 if (os_version < 7) // checkboxes were wider on iOS 6.1 3599 r.origin.x -= 25; 3600 3601 } else { 3602 r.origin.x = left_width; // Text fields, etc. 3603 r.size.width = right_edge - r.origin.x; 3604 } 3605 3606 r.origin.y = (hh - r.size.height) / 2; // Center vertically. 3607 [ctl setFrame:r]; 3608 3609 // Make a box and put the label and checkbox/slider into it. 3610 r.origin.x = 0; 3611 r.origin.y = 0; 3612 r.size.width = ww; 3613 r.size.height = hh; 3614 NSView *box = [[UIView alloc] initWithFrame:r]; 3615 [box addSubview: ctl]; 3616 3617 // Let the label make use of any space not taken up by the control. 3618 r = [label frame]; 3619 r.origin.x = LEFT_MARGIN; 3620 r.origin.y = 0; 3621 r.size.width = [ctl frame].origin.x - r.origin.x; 3622 r.size.height = hh; 3623 [label setFrame:r]; 3624 [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]]; 3625 [box addSubview: label]; 3626 [box autorelease]; 3627 3628 ctl = box; 3629 } 3630 break; 3631 case 3: 3632 case 4: 3633 { 3634 // With 3 elements, 1 and 3 are labels. 3635 // With 4 elements, 1, 2 and 4 are labels. 3636 int i = 0; 3637 UILabel *top = ([set count] == 4 3638 ? [set objectAtIndex: i++] 3639 : 0); 3640 UILabel *left = [set objectAtIndex: i++]; 3641 NSView *mid = [set objectAtIndex: i++]; 3642 UILabel *right = [set objectAtIndex: i++]; 3643 NSAssert (!top || [top isKindOfClass:[UILabel class]], @"WTF"); 3644 NSAssert ( [left isKindOfClass:[UILabel class]], @"WTF"); 3645 NSAssert ( ![mid isKindOfClass:[UILabel class]], @"WTF"); 3646 NSAssert ( [right isKindOfClass:[UILabel class]], @"WTF"); 3647 3648 // 3 elements: control at top of cell. 3649 // 4 elements: center the control vertically. 3650 CGRect r = [mid frame]; 3651 r.size.height = 32; // Unchangable height of the slider thumb. 3652 3653 // Center the slider between left_width and right_edge. 3654# ifdef LABEL_ABOVE_SLIDER 3655 r.origin.x = LEFT_MARGIN; 3656# else 3657 r.origin.x = left_width; 3658# endif 3659 r.origin.y = (hh - r.size.height) / 2; 3660 r.size.width = right_edge - r.origin.x; 3661 [mid setFrame:r]; 3662 3663 if (top) { 3664# ifdef LABEL_ABOVE_SLIDER 3665 // Top label goes above, flush center/top. 3666 r.origin.x = (ww - r.size.width) / 2; 3667 r.origin.y = 4; 3668 // #### sizeWithFont deprecated as of iOS 7; use boundingRectWithSize. 3669 r.size = [[top text] sizeWithFont:[top font] 3670 constrainedToSize: 3671 CGSizeMake (ww - LEFT_MARGIN*2, 100000) 3672 lineBreakMode:[top lineBreakMode]]; 3673# else // !LABEL_ABOVE_SLIDER 3674 // Label goes on the left. 3675 r.origin.x = LEFT_MARGIN; 3676 r.origin.y = 0; 3677 r.size.width = left_width - LEFT_MARGIN; 3678 r.size.height = hh; 3679# endif // !LABEL_ABOVE_SLIDER 3680 [top setFrame:r]; 3681 } 3682 3683 // Left label goes under control, flush left/bottom. 3684 left.frame = CGRectMake([mid frame].origin.x, hh - 4, 3685 ww - LEFT_MARGIN*2, 100000); 3686 [left sizeToFit]; 3687 r = left.frame; 3688 r.origin.y -= r.size.height; 3689 left.frame = r; 3690 3691 // Right label goes under control, flush right/bottom. 3692 right.frame = 3693 CGRectMake([mid frame].origin.x + [mid frame].size.width, 3694 [left frame].origin.y, ww - LEFT_MARGIN*2, 1000000); 3695 [right sizeToFit]; 3696 r = right.frame; 3697 r.origin.x -= r.size.width; 3698 right.frame = r; 3699 3700 // Make a box and put the labels and slider into it. 3701 r.origin.x = 0; 3702 r.origin.y = 0; 3703 r.size.width = ww; 3704 r.size.height = hh; 3705 NSView *box = [[UIView alloc] initWithFrame:r]; 3706 if (top) 3707 [box addSubview: top]; 3708 [box addSubview: left]; 3709 [box addSubview: right]; 3710 [box addSubview: mid]; 3711 [box autorelease]; 3712 3713 ctl = box; 3714 } 3715 break; 3716 default: 3717 NSAssert (0, @"unhandled size"); 3718 } 3719 } else { // A single view, not a pair. 3720 CGRect r = [ctl frame]; 3721 r.origin.x = LEFT_MARGIN; 3722 r.origin.y = 0; 3723 r.size.width = right_edge - r.origin.x; 3724 r.size.height = hh; 3725 [ctl setFrame:r]; 3726 } 3727 3728 NSString *id = @"Cell"; 3729 UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:id]; 3730 if (!cell) 3731 cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault 3732 reuseIdentifier: id] 3733 autorelease]; 3734 3735 for (UIView *subview in [cell.contentView subviews]) 3736 [subview removeFromSuperview]; 3737 [cell.contentView addSubview: ctl]; 3738 CGRect r = [ctl frame]; 3739 r.origin.x = 0; 3740 r.origin.y = 0; 3741 [cell setFrame:r]; 3742 cell.selectionStyle = UITableViewCellSelectionStyleNone; 3743 [cell setAccessoryType:UITableViewCellAccessoryNone]; 3744 3745# ifndef USE_PICKER_VIEW 3746 if ([ctl isKindOfClass:[RadioButton class]]) 3747 [self updateRadioGroupCell:cell button:(RadioButton *)ctl]; 3748# endif // USE_PICKER_VIEW 3749 3750 return cell; 3751} 3752# endif // USE_IPHONE 3753 3754 3755/* When this object is instantiated, it parses the XML file and creates 3756 controls on itself that are hooked up to the appropriate preferences. 3757 The default size of the view is just big enough to hold them all. 3758 */ 3759- (id)initWithXML: (NSData *) xml_data 3760 options: (const XrmOptionDescRec *) _opts 3761 controller: (NSUserDefaultsController *) _prefs 3762 globalController: (NSUserDefaultsController *) _globalPrefs 3763 defaults: (NSDictionary *) _defs 3764 haveUpdater: (BOOL) _haveUpdater 3765{ 3766# ifndef USE_IPHONE 3767 self = [super init]; 3768# else // !USE_IPHONE 3769 self = [super initWithStyle:UITableViewStyleGrouped]; 3770 self.title = [saver_name stringByAppendingString:@" Settings"]; 3771# endif // !USE_IPHONE 3772 if (! self) return 0; 3773 3774 // instance variables 3775 opts = _opts; 3776 defaultOptions = _defs; 3777 userDefaultsController = [_prefs retain]; 3778 globalDefaultsController = [_globalPrefs retain]; 3779 haveUpdater = _haveUpdater; 3780 3781 NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithData:xml_data]; 3782 3783 if (!xmlDoc) { 3784 NSAssert1 (0, @"XML Error: %@", 3785 [[NSString alloc] initWithData:xml_data 3786 encoding:NSUTF8StringEncoding]); 3787 return nil; 3788 } 3789 [xmlDoc setDelegate:self]; 3790 if (! [xmlDoc parse]) { 3791 NSError *err = [xmlDoc parserError]; 3792 NSAssert2 (0, @"XML Error: %@: %@", 3793 [[NSString alloc] initWithData:xml_data 3794 encoding:NSUTF8StringEncoding], 3795 err); 3796 return nil; 3797 } 3798 3799# ifndef USE_IPHONE 3800 TextModeTransformer *t = [[TextModeTransformer alloc] init]; 3801 [NSValueTransformer setValueTransformer:t 3802 forName:@"TextModeTransformer"]; 3803 [t release]; 3804# endif // USE_IPHONE 3805 3806 [self traverseTree]; 3807 xml_root = 0; 3808 3809# ifdef USE_IPHONE 3810 [self addResetButton]; 3811# endif 3812 3813 return self; 3814} 3815 3816 3817- (void) dealloc 3818{ 3819 [saver_name release]; 3820 [userDefaultsController release]; 3821 [globalDefaultsController release]; 3822# ifdef USE_IPHONE 3823 [controls release]; 3824 [pref_keys release]; 3825 [pref_ctls release]; 3826# ifdef USE_PICKER_VIEW 3827 [picker_values release]; 3828# endif 3829# endif 3830 [super dealloc]; 3831} 3832 3833@end 3834