1 /* xscreensaver, Copyright (c) 2006-2012 Jamie Zawinski <jwz@jwz.org>
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
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."
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).
26 #import "XScreenSaverConfigSheet.h"
29 #import "InvertedSlider.h"
32 # define NSView UIView
33 # define NSRect CGRect
34 # define NSSize CGSize
35 # define NSTextField UITextField
36 # define NSButton UIButton
37 # define NSFont UIFont
38 # define NSStepper UIStepper
39 # define NSMenuItem UIMenuItem
40 # define NSText UILabel
41 # define minValue minimumValue
42 # define maxValue maximumValue
43 # define setMinValue setMinimumValue
44 # define setMaxValue setMaximumValue
45 # define LABEL UILabel
47 # define LABEL NSTextField
50 #undef LABEL_ABOVE_SLIDER
53 #pragma mark XML Parser
55 /* I used to use the "NSXMLDocument" XML parser, but that doesn't exist
56 on iOS. The "NSXMLParser" parser exists on both OSX and iOS, so I
57 converted to use that. However, to avoid having to re-write all of
58 the old code, I faked out a halfassed implementation of the
59 "NSXMLNode" class that "NSXMLDocument" used to return.
62 #define NSXMLNode SimpleXMLNode
63 #define NSXMLElement SimpleXMLNode
64 #define NSXMLCommentKind SimpleXMLCommentKind
65 #define NSXMLElementKind SimpleXMLElementKind
66 #define NSXMLAttributeKind SimpleXMLAttributeKind
67 #define NSXMLTextKind SimpleXMLTextKind
69 typedef enum { SimpleXMLCommentKind,
71 SimpleXMLAttributeKind,
75 @interface SimpleXMLNode : NSObject
79 SimpleXMLNode *parent;
80 NSMutableArray *children;
81 NSMutableArray *attributes;
85 @property(nonatomic) SimpleXMLKind kind;
86 @property(nonatomic, retain) NSString *name;
87 @property(nonatomic, retain) SimpleXMLNode *parent;
88 @property(nonatomic, retain) NSMutableArray *children;
89 @property(nonatomic, retain) NSMutableArray *attributes;
90 @property(nonatomic, retain, getter=objectValue, setter=setObjectValue:)
95 @implementation SimpleXMLNode
100 @synthesize children;
101 @synthesize attributes;
107 attributes = [NSMutableArray arrayWithCapacity:10];
112 - (id) initWithName:(NSString *)n
115 [self setKind:NSXMLElementKind];
121 - (void) setAttributesAsDictionary:(NSDictionary *)dict
123 for (NSString *key in dict) {
124 NSObject *val = [dict objectForKey:key];
125 SimpleXMLNode *n = [[SimpleXMLNode alloc] init];
126 [n setKind:SimpleXMLAttributeKind];
128 [n setObjectValue:val];
129 [attributes addObject:n];
133 - (SimpleXMLNode *) parent { return parent; }
135 - (void) setParent:(SimpleXMLNode *)p
137 NSAssert (!parent, @"parent already set");
140 NSMutableArray *kids = [p children];
142 kids = [NSMutableArray arrayWithCapacity:10];
143 [p setChildren:kids];
145 [kids addObject:self];
150 #pragma mark Implementing radio buttons
152 /* The UIPickerView is a hideous and uncustomizable piece of shit.
153 I can't believe Apple actually released that thing on the world.
154 Let's fake up some radio buttons instead.
157 #if defined(USE_IPHONE) && !defined(USE_PICKER_VIEW)
159 @interface RadioButton : UILabel
165 @property(nonatomic) int index;
166 @property(nonatomic, retain) NSArray *items;
170 @implementation RadioButton
175 - (id) initWithIndex:(int)_index items:_items
177 self = [super initWithFrame:CGRectZero];
179 items = [_items retain];
181 [self setText: [[items objectAtIndex:index] objectAtIndex:0]];
182 [self setBackgroundColor:[UIColor clearColor]];
191 # endif // !USE_PICKER_VIEW
195 @interface XScreenSaverConfigSheet (Private)
197 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent;
200 - (void) placeChild: (NSView *)c on:(NSView *)p right:(BOOL)r;
201 - (void) placeChild: (NSView *)c on:(NSView *)p;
202 static NSView *last_child (NSView *parent);
203 static void layout_group (NSView *group, BOOL horiz_p);
205 - (void) placeChild: (NSObject *)c on:(NSView *)p right:(BOOL)r;
206 - (void) placeChild: (NSObject *)c on:(NSView *)p;
207 - (void) placeSeparator;
208 - (void) bindResource:(NSObject *)ctl key:(NSString *)k reload:(BOOL)r;
209 - (void) refreshTableView;
210 # endif // USE_IPHONE
215 @implementation XScreenSaverConfigSheet
217 # define LEFT_MARGIN 20 // left edge of window
218 # define COLUMN_SPACING 10 // gap between e.g. labels and text fields
219 # define LEFT_LABEL_WIDTH 70 // width of all left labels
220 # define LINE_SPACING 10 // leading between each line
222 # define FONT_SIZE 17 // Magic hardcoded UITableView font size.
224 #pragma mark Talking to the resource database
227 /* Normally we read resources by looking up "KEY" in the database
228 "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone
229 app, everything is stored in the database "org.jwz.xscreensaver"
230 instead, so transform keys to "SAVERNAME.KEY".
232 NOTE: This is duplicated in PrefsReader.m, cause I suck.
234 - (NSString *) makeKey:(NSString *)key
237 NSString *prefix = [saver_name stringByAppendingString:@"."];
238 if (! [key hasPrefix:prefix]) // Don't double up!
239 key = [prefix stringByAppendingString:key];
245 - (NSString *) makeCKey:(const char *)key
247 return [self makeKey:[NSString stringWithCString:key
248 encoding:NSUTF8StringEncoding]];
252 /* Given a command-line option, returns the corresponding resource name.
253 Any arguments in the switch string are ignored (e.g., "-foo x").
255 - (NSString *) switchToResource:(NSString *)cmdline_switch
256 opts:(const XrmOptionDescRec *)opts_array
257 valRet:(NSString **)val_ret
261 NSAssert(cmdline_switch, @"cmdline switch is null");
262 if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
263 encoding:NSUTF8StringEncoding]) {
264 NSAssert1(0, @"unable to convert %@", cmdline_switch);
267 char *s = strpbrk(buf, " \t\r\n");
271 while (*tail && (*tail == ' ' || *tail == '\t'))
275 while (opts_array[0].option) {
276 if (!strcmp (opts_array[0].option, buf)) {
279 if (opts_array[0].argKind == XrmoptionNoArg) {
281 NSAssert1 (0, @"expected no args to switch: \"%@\"",
283 ret = opts_array[0].value;
286 NSAssert1 (0, @"expected args to switch: \"%@\"",
293 ? [NSString stringWithCString:ret
294 encoding:NSUTF8StringEncoding]
297 const char *res = opts_array[0].specifier;
298 while (*res && (*res == '.' || *res == '*'))
300 return [self makeCKey:res];
305 NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
312 // Called when a slider is bonked.
314 - (void)sliderAction:(UISlider*)sender
316 if ([active_text_field canResignFirstResponder])
317 [active_text_field resignFirstResponder];
318 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
319 double v = [sender value];
321 [userDefaultsController setInteger:v forKey:pref_key];
323 [userDefaultsController setDouble:v forKey:pref_key];
326 // Called when a checkbox/switch is bonked.
328 - (void)switchAction:(UISwitch*)sender
330 if ([active_text_field canResignFirstResponder])
331 [active_text_field resignFirstResponder];
332 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
333 NSString *v = ([sender isOn] ? @"true" : @"false");
334 [userDefaultsController setObject:v forKey:pref_key];
337 # ifdef USE_PICKER_VIEW
338 // Called when a picker is bonked.
340 - (void)pickerView:(UIPickerView *)pv
341 didSelectRow:(NSInteger)row
342 inComponent:(NSInteger)column
344 if ([active_text_field canResignFirstResponder])
345 [active_text_field resignFirstResponder];
347 NSAssert (column == 0, @"internal error");
348 NSArray *a = [picker_values objectAtIndex: [pv tag]];
349 if (! a) return; // Too early?
350 a = [a objectAtIndex:row];
351 NSAssert (a, @"missing row");
353 //NSString *label = [a objectAtIndex:0];
354 NSString *pref_key = [a objectAtIndex:1];
355 NSObject *pref_val = [a objectAtIndex:2];
356 [userDefaultsController setObject:pref_val forKey:pref_key];
358 # else // !USE_PICKER_VIEW
360 // Called when a RadioButton is bonked.
362 - (void)radioAction:(RadioButton*)sender
364 if ([active_text_field canResignFirstResponder])
365 [active_text_field resignFirstResponder];
367 NSArray *item = [[sender items] objectAtIndex: [sender index]];
368 NSString *pref_key = [item objectAtIndex:1];
369 NSObject *pref_val = [item objectAtIndex:2];
370 [userDefaultsController setObject:pref_val forKey:pref_key];
373 - (BOOL)textFieldShouldBeginEditing:(UITextField *)tf
375 active_text_field = tf;
379 - (void)textFieldDidEndEditing:(UITextField *)tf
381 NSString *pref_key = [pref_keys objectAtIndex: [tf tag]];
382 NSString *txt = [tf text];
383 [userDefaultsController setObject:txt forKey:pref_key];
386 - (BOOL)textFieldShouldReturn:(UITextField *)tf
388 active_text_field = nil;
389 [tf resignFirstResponder];
393 # endif // !USE_PICKER_VIEW
400 - (void) okAction:(NSObject *)arg
402 [userDefaultsController commitEditing];
403 [userDefaultsController save:self];
404 [NSApp endSheet:self returnCode:NSOKButton];
408 - (void) cancelAction:(NSObject *)arg
410 [userDefaultsController revert:self];
411 [NSApp endSheet:self returnCode:NSCancelButton];
414 # endif // !USE_IPHONE
417 - (void) resetAction:(NSObject *)arg
420 [userDefaultsController revertToInitialValues:self];
423 for (NSString *key in defaultOptions) {
424 NSObject *val = [defaultOptions objectForKey:key];
425 [userDefaultsController setObject:val forKey:key];
428 for (UIControl *ctl in pref_ctls) {
429 NSString *pref_key = [pref_keys objectAtIndex: ctl.tag];
430 [self bindResource:ctl key:pref_key reload:YES];
433 [self refreshTableView];
434 # endif // USE_IPHONE
438 /* Connects a control (checkbox, etc) to the corresponding preferences key.
440 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
441 reload:(BOOL)reload_p
444 NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
446 : ([control isKindOfClass:[NSMatrix class]]
450 toObject:userDefaultsController
451 withKeyPath:[@"values." stringByAppendingString: pref_key]
455 NSObject *val = [userDefaultsController objectForKey:pref_key];
459 if ([val isKindOfClass:[NSString class]]) {
460 sval = (NSString *) val;
461 if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] ||
462 NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] ||
463 NSOrderedSame == [sval caseInsensitiveCompare:@"1"])
466 dval = [sval doubleValue];
467 } else if ([val isKindOfClass:[NSNumber class]]) {
468 // NSBoolean (__NSCFBoolean) is really NSNumber.
469 dval = [(NSNumber *) val doubleValue];
470 sval = [(NSNumber *) val stringValue];
473 if ([control isKindOfClass:[UISlider class]]) {
474 sel = @selector(sliderAction:);
475 [(UISlider *) control setValue: dval];
476 } else if ([control isKindOfClass:[UISwitch class]]) {
477 sel = @selector(switchAction:);
478 [(UISwitch *) control setOn: ((int) dval != 0)];
479 # ifdef USE_PICKER_VIEW
480 } else if ([control isKindOfClass:[UIPickerView class]]) {
482 [(UIPickerView *) control selectRow:((int)dval) inComponent:0
484 # else // !USE_PICKER_VIEW
485 } else if ([control isKindOfClass:[RadioButton class]]) {
486 sel = 0; // radioAction: sent from didSelectRowAtIndexPath.
487 } else if ([control isKindOfClass:[UITextField class]]) {
489 [(UITextField *) control setText: sval];
490 # endif // !USE_PICKER_VIEW
492 NSAssert (0, @"unknown class");
495 // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval);
499 pref_keys = [[NSMutableArray arrayWithCapacity:10] retain];
500 pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain];
503 [pref_keys addObject: [self makeKey:pref_key]];
504 [pref_ctls addObject: control];
505 ((UIControl *) control).tag = [pref_keys count] - 1;
508 [(UIControl *) control addTarget:self action:sel
509 forControlEvents:UIControlEventValueChanged];
513 # endif // USE_IPHONE
516 NSObject *def = [[userDefaultsController defaults] objectForKey:pref_key];
517 NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
518 s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
519 s = [NSString stringWithFormat:@"%@ = \"%@\"", s, def];
520 s = [s stringByPaddingToLength:28 withString:@" " startingAtIndex:0];
521 NSLog (@"%@ %@/%@", s, [def class], [control class]);
526 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
528 [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO];
533 - (void) bindSwitch:(NSObject *)control
534 cmdline:(NSString *)cmd
536 [self bindResource:control
537 key:[self switchToResource:cmd opts:opts valRet:0]];
541 #pragma mark Text-manipulating utilities
545 unwrap (NSString *text)
547 // Unwrap lines: delete \n but do not delete \n\n.
549 NSArray *lines = [text componentsSeparatedByString:@"\n"];
550 int nlines = [lines count];
554 text = @"\n"; // start with one blank line
556 // skip trailing blank lines in file
557 for (i = nlines-1; i > 0; i--) {
558 NSString *s = (NSString *) [lines objectAtIndex:i];
564 // skip leading blank lines in file
565 for (i = 0; i < nlines; i++) {
566 NSString *s = (NSString *) [lines objectAtIndex:i];
573 for (; i < nlines; i++) {
574 NSString *s = (NSString *) [lines objectAtIndex:i];
575 if ([s length] == 0) {
576 text = [text stringByAppendingString:@"\n\n"];
578 } else if ([s characterAtIndex:0] == ' ' ||
579 [s hasPrefix:@"Copyright "] ||
580 [s hasPrefix:@"http://"]) {
581 // don't unwrap if the following line begins with whitespace,
582 // or with the word "Copyright", or if it begins with a URL.
584 text = [text stringByAppendingString:@"\n"];
585 text = [text stringByAppendingString:s];
590 text = [text stringByAppendingString:@" "];
591 text = [text stringByAppendingString:s];
602 /* Makes the text up to the first comma be bold.
605 boldify (NSText *nstext)
607 NSString *text = [nstext string];
608 NSRange r = [text rangeOfString:@"," options:0];
609 r.length = r.location+1;
613 NSFont *font = [nstext font];
614 font = [NSFont boldSystemFontOfSize:[font pointSize]];
615 [nstext setFont:font range:r];
617 # endif // !USE_IPHONE
620 /* Creates a human-readable anchor to put on a URL.
623 anchorize (const char *url)
625 const char *wiki = "http://en.wikipedia.org/wiki/";
626 const char *math = "http://mathworld.wolfram.com/";
627 if (!strncmp (wiki, url, strlen(wiki))) {
628 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
629 strcpy (anchor, "Wikipedia: \"");
630 const char *in = url + strlen(wiki);
631 char *out = anchor + strlen(anchor);
635 } else if (*in == '#') {
638 } else if (*in == '%') {
644 sscanf (hex, "%x", &n);
656 } else if (!strncmp (math, url, strlen(math))) {
657 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
658 strcpy (anchor, "MathWorld: \"");
659 const char *start = url + strlen(wiki);
660 const char *in = start;
661 char *out = anchor + strlen(anchor);
665 } else if (in != start && *in >= 'A' && *in <= 'Z') {
668 } else if (!strncmp (in, ".htm", 4)) {
685 /* Converts any http: URLs in the given text field to clickable links.
688 hreffify (NSText *nstext)
691 NSString *text = [nstext string];
692 [nstext setRichText:YES];
694 NSString *text = [nstext text];
697 int L = [text length];
698 NSRange start; // range is start-of-search to end-of-string
701 while (start.location < L) {
703 // Find the beginning of a URL...
705 NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
706 if (r2.location == NSNotFound)
709 // Next time around, start searching after this.
710 start.location = r2.location + r2.length;
711 start.length = L - start.location;
713 // Find the end of a URL (whitespace or EOF)...
715 NSRange r3 = [text rangeOfCharacterFromSet:
716 [NSCharacterSet whitespaceAndNewlineCharacterSet]
717 options:0 range:start];
718 if (r3.location == NSNotFound) // EOF
719 r3.location = L, r3.length = 0;
721 // Next time around, start searching after this.
722 start.location = r3.location;
723 start.length = L - start.location;
725 // Set r2 to the start/length of this URL.
726 r2.length = start.location - r2.location;
729 NSString *nsurl = [text substringWithRange:r2];
730 const char *url = [nsurl UTF8String];
732 // If this is a Wikipedia URL, make the linked text be prettier.
734 char *anchor = anchorize(url);
738 // Construct the RTF corresponding to <A HREF="url">anchor</A>
740 const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
741 char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
742 sprintf (rtf, fmt, url, anchor);
744 NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
745 [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
747 # else // !USE_IPHONE
748 // *anchor = 0; // Omit Wikipedia anchor
749 text = [text stringByReplacingCharactersInRange:r2
750 withString:[NSString stringWithCString:anchor
751 encoding:NSUTF8StringEncoding]];
752 // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
753 // withString:@"\n\n"];
754 # endif // !USE_IPHONE
758 int L2 = [text length]; // might have changed
759 start.location -= (L - L2);
764 [nstext setText:text];
770 #pragma mark Creating controls from XML
773 /* Parse the attributes of an XML tag into a dictionary.
774 For input, the dictionary should have as attributes the keys, each
775 with @"" as their value.
776 On output, the dictionary will set the keys to the values specified,
777 and keys that were not specified will not be present in the dictionary.
778 Warnings are printed if there are duplicate or unknown attributes.
780 - (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
782 NSArray *attrs = [(NSXMLElement *) node attributes];
783 int n = [attrs count];
786 // For each key in the dictionary, fill in the dict with the corresponding
787 // value. The value @"" is assumed to mean "un-set". Issue a warning if
788 // an attribute is specified twice.
790 for (i = 0; i < n; i++) {
791 NSXMLNode *attr = [attrs objectAtIndex:i];
792 NSString *key = [attr name];
793 NSString *val = [attr objectValue];
794 NSString *old = [dict objectForKey:key];
797 NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
798 } else if ([old length] != 0) {
799 NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
801 [dict setValue:val forKey:key];
805 // Remove from the dictionary any keys whose value is still @"",
806 // meaning there was no such attribute specified.
808 NSArray *keys = [dict allKeys];
810 for (i = 0; i < n; i++) {
811 NSString *key = [keys objectAtIndex:i];
812 NSString *val = [dict objectForKey:key];
813 if ([val length] == 0)
814 [dict removeObjectForKey:key];
819 /* Handle the options on the top level <xscreensaver> tag.
821 - (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
823 NSMutableDictionary *dict =
824 [NSMutableDictionary dictionaryWithObjectsAndKeys:
829 [self parseAttrs:dict node:node];
830 NSString *name = [dict objectForKey:@"name"];
831 NSString *label = [dict objectForKey:@"_label"];
833 NSAssert1 (label, @"no _label in %@", [node name]);
834 NSAssert1 (name, @"no name in \"%@\"", label);
839 /* Creates a label: an un-editable NSTextField displaying the given text.
841 - (LABEL *) makeLabel:(NSString *)text
844 rect.origin.x = rect.origin.y = 0;
845 rect.size.width = rect.size.height = 10;
847 NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
848 [lab setSelectable:NO];
849 [lab setEditable:NO];
851 [lab setDrawsBackground:NO];
852 [lab setStringValue:text];
855 UILabel *lab = [[UILabel alloc] initWithFrame:rect];
856 [lab setText: [text stringByTrimmingCharactersInSet:
857 [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
858 [lab setBackgroundColor:[UIColor clearColor]];
859 [lab setNumberOfLines:0]; // unlimited
860 // [lab setLineBreakMode:UILineBreakModeWordWrap];
861 [lab setLineBreakMode:UILineBreakModeHeadTruncation];
862 [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
863 UIViewAutoresizingFlexibleHeight)];
864 # endif // USE_IPHONE
869 /* Creates the checkbox (NSButton) described by the given XML node.
871 - (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
873 NSMutableDictionary *dict =
874 [NSMutableDictionary dictionaryWithObjectsAndKeys:
880 [self parseAttrs:dict node:node];
881 NSString *label = [dict objectForKey:@"_label"];
882 NSString *arg_set = [dict objectForKey:@"arg-set"];
883 NSString *arg_unset = [dict objectForKey:@"arg-unset"];
886 NSAssert1 (0, @"no _label in %@", [node name]);
889 if (!arg_set && !arg_unset) {
890 NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"",
893 if (arg_set && arg_unset) {
894 NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"",
898 // sanity-check the choice of argument names.
900 if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
901 [arg_set hasPrefix:@"--no-"]))
902 NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
904 if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
905 ![arg_unset hasPrefix:@"--no-"]))
906 NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
910 rect.origin.x = rect.origin.y = 0;
911 rect.size.width = rect.size.height = 10;
915 NSButton *button = [[NSButton alloc] initWithFrame:rect];
916 [button setButtonType:NSSwitchButton];
917 [button setTitle:label];
919 [self placeChild:button on:parent];
923 LABEL *lab = [self makeLabel:label];
924 [self placeChild:lab on:parent];
925 UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
926 [self placeChild:button on:parent right:YES];
929 # endif // USE_IPHONE
931 [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
936 /* Creates the number selection control described by the given XML node.
937 If "type=slider", it's an NSSlider.
938 If "type=spinbutton", it's a text field with up/down arrows next to it.
940 - (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
942 NSMutableDictionary *dict =
943 [NSMutableDictionary dictionaryWithObjectsAndKeys:
955 [self parseAttrs:dict node:node];
956 NSString *label = [dict objectForKey:@"_label"];
957 NSString *low_label = [dict objectForKey:@"_low-label"];
958 NSString *high_label = [dict objectForKey:@"_high-label"];
959 NSString *type = [dict objectForKey:@"type"];
960 NSString *arg = [dict objectForKey:@"arg"];
961 NSString *low = [dict objectForKey:@"low"];
962 NSString *high = [dict objectForKey:@"high"];
963 NSString *def = [dict objectForKey:@"default"];
964 NSString *cvt = [dict objectForKey:@"convert"];
966 NSAssert1 (arg, @"no arg in %@", label);
967 NSAssert1 (type, @"no type in %@", label);
970 NSAssert1 (0, @"no low in %@", [node name]);
974 NSAssert1 (0, @"no high in %@", [node name]);
978 NSAssert1 (0, @"no default in %@", [node name]);
981 if (cvt && ![cvt isEqualToString:@"invert"]) {
982 NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
986 // If either the min or max field contains a decimal point, then this
987 // option may have a floating point value; otherwise, it is constrained
990 NSCharacterSet *dot =
991 [NSCharacterSet characterSetWithCharactersInString:@"."];
992 BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
993 [high rangeOfCharacterFromSet:dot].location != NSNotFound);
995 if ([type isEqualToString:@"slider"]
996 # ifdef USE_IPHONE // On iPhone, we use sliders for all numeric values.
997 || [type isEqualToString:@"spinbutton"]
1002 rect.origin.x = rect.origin.y = 0;
1003 rect.size.width = 150;
1004 rect.size.height = 23; // apparent min height for slider with ticks...
1006 slider = [[InvertedSlider alloc] initWithFrame:rect
1008 integers: !float_p];
1009 [slider setMaxValue:[high doubleValue]];
1010 [slider setMinValue:[low doubleValue]];
1012 int range = [slider maxValue] - [slider minValue] + 1;
1015 while (range2 > max_ticks)
1018 // If we have elided ticks, leave it at the max number of ticks.
1019 if (range != range2 && range2 < max_ticks)
1022 // If it's a float, always display the max number of ticks.
1023 if (float_p && range2 < max_ticks)
1027 [slider setNumberOfTickMarks:range2];
1029 [slider setAllowsTickMarkValuesOnly:
1030 (range == range2 && // we are showing the actual number of ticks
1031 !float_p)]; // and we want integer results
1032 # endif // !USE_IPHONE
1034 // #### Note: when the slider's range is large enough that we aren't
1035 // showing all possible ticks, the slider's value is not constrained
1036 // to be an integer, even though it should be...
1037 // Maybe we need to use a value converter or something?
1041 lab = [self makeLabel:label];
1042 [self placeChild:lab on:parent];
1045 CGFloat s = [NSFont systemFontSize] + 4;
1046 [lab setFont:[NSFont boldSystemFontOfSize:s]];
1053 lab = [self makeLabel:low_label];
1054 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1056 [lab setAlignment:1]; // right aligned
1058 if (rect.size.width < LEFT_LABEL_WIDTH)
1059 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1060 rect.size.height = [slider frame].size.height;
1061 [lab setFrame:rect];
1062 [self placeChild:lab on:parent];
1063 # else // USE_IPHONE
1064 [lab setTextAlignment: UITextAlignmentRight];
1065 [self placeChild:lab on:parent right:(label ? YES : NO)];
1066 # endif // USE_IPHONE
1072 [self placeChild:slider on:parent right:(low_label ? YES : NO)];
1073 # else // USE_IPHONE
1074 [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
1075 # endif // USE_IPHONE
1078 // Make left label be same height as slider.
1080 rect.size.height = [slider frame].size.height;
1081 [lab setFrame:rect];
1085 rect = [slider frame];
1086 if (rect.origin.x < LEFT_LABEL_WIDTH)
1087 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
1088 [slider setFrame:rect];
1092 lab = [self makeLabel:high_label];
1093 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1096 // Make right label be same height as slider.
1097 rect.size.height = [slider frame].size.height;
1098 [lab setFrame:rect];
1099 [self placeChild:lab on:parent right:YES];
1103 [self bindSwitch:slider cmdline:arg];
1106 #ifndef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1108 } else if ([type isEqualToString:@"spinbutton"]) {
1111 NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
1114 NSAssert1 (!low_label,
1115 @"low-label not allowed in spinbutton \"%@\"", [node name]);
1116 NSAssert1 (!high_label,
1117 @"high-label not allowed in spinbutton \"%@\"", [node name]);
1118 NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
1122 rect.origin.x = rect.origin.y = 0;
1123 rect.size.width = rect.size.height = 10;
1125 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1126 [txt setStringValue:@"0000.0"];
1128 [txt setStringValue:@""];
1131 LABEL *lab = [self makeLabel:label];
1132 //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1133 [lab setAlignment:1]; // right aligned
1135 if (rect.size.width < LEFT_LABEL_WIDTH)
1136 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1137 rect.size.height = [txt frame].size.height;
1138 [lab setFrame:rect];
1139 [self placeChild:lab on:parent];
1143 [self placeChild:txt on:parent right:(label ? YES : NO)];
1147 if (rect.origin.x < LEFT_LABEL_WIDTH)
1148 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
1149 [txt setFrame:rect];
1152 rect.size.width = rect.size.height = 10;
1153 NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
1155 [self placeChild:step on:parent right:YES];
1156 rect = [step frame];
1157 rect.origin.x -= COLUMN_SPACING; // this one goes close
1158 rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
1159 [step setFrame:rect];
1161 [step setMinValue:[low doubleValue]];
1162 [step setMaxValue:[high doubleValue]];
1163 [step setAutorepeat:YES];
1164 [step setValueWraps:NO];
1166 double range = [high doubleValue] - [low doubleValue];
1168 [step setIncrement:range / 10.0];
1169 else if (range >= 500)
1170 [step setIncrement:range / 100.0];
1172 [step setIncrement:1.0];
1174 NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
1175 [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
1176 [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
1177 [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]];
1178 [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
1179 [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
1180 [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
1182 [fmt setGeneratesDecimalNumbers:float_p];
1183 [[txt cell] setFormatter:fmt];
1185 [self bindSwitch:step cmdline:arg];
1186 [self bindSwitch:txt cmdline:arg];
1191 # endif // USE_IPHONE
1194 NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
1201 set_menu_item_object (NSMenuItem *item, NSObject *obj)
1203 /* If the object associated with this menu item looks like a boolean,
1204 store an NSNumber instead of an NSString, since that's what
1205 will be in the preferences (due to similar logic in PrefsReader).
1207 if ([obj isKindOfClass:[NSString class]]) {
1208 NSString *string = (NSString *) obj;
1209 if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
1210 NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
1211 obj = [NSNumber numberWithBool:YES];
1212 else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
1213 NSOrderedSame == [string caseInsensitiveCompare:@"no"])
1214 obj = [NSNumber numberWithBool:NO];
1219 [item setRepresentedObject:obj];
1220 //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
1222 # endif // !USE_IPHONE
1225 /* Creates the popup menu described by the given XML node (and its children).
1227 - (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
1229 NSArray *children = [node children];
1230 int i, count = [children count];
1233 NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
1237 // get the "id" attribute off the <select> tag.
1239 NSMutableDictionary *dict =
1240 [NSMutableDictionary dictionaryWithObjectsAndKeys:
1243 [self parseAttrs:dict node:node];
1246 rect.origin.x = rect.origin.y = 0;
1247 rect.size.width = 10;
1248 rect.size.height = 10;
1250 NSString *menu_key = nil; // the resource key used by items in this menu
1253 // #### "Build and Analyze" says that all of our widgets leak, because it
1254 // seems to not realize that placeChild -> addSubview retains them.
1255 // Not sure what to do to make these warnings go away.
1257 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1259 NSMenuItem *def_item = nil;
1260 float max_width = 0;
1262 # else // USE_IPHONE
1264 NSString *def_item = nil;
1266 rect.size.width = 0;
1267 rect.size.height = 0;
1268 # ifdef USE_PICKER_VIEW
1269 UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain];
1270 popup.delegate = self;
1271 popup.dataSource = self;
1272 # endif // !USE_PICKER_VIEW
1273 NSMutableArray *items = [NSMutableArray arrayWithCapacity:10];
1275 # endif // USE_IPHONE
1277 for (i = 0; i < count; i++) {
1278 NSXMLNode *child = [children objectAtIndex:i];
1280 if ([child kind] == NSXMLCommentKind)
1282 if ([child kind] != NSXMLElementKind) {
1283 // NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
1287 // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
1289 NSMutableDictionary *dict2 =
1290 [NSMutableDictionary dictionaryWithObjectsAndKeys:
1295 [self parseAttrs:dict2 node:child];
1296 NSString *label = [dict2 objectForKey:@"_label"];
1297 NSString *arg_set = [dict2 objectForKey:@"arg-set"];
1300 NSAssert1 (0, @"no _label in %@", [child name]);
1305 // create the menu item (and then get a pointer to it)
1306 [popup addItemWithTitle:label];
1307 NSMenuItem *item = [popup itemWithTitle:label];
1308 # endif // USE_IPHONE
1311 NSString *this_val = NULL;
1312 NSString *this_key = [self switchToResource: arg_set
1315 NSAssert1 (this_val, @"this_val null for %@", arg_set);
1316 if (menu_key && ![menu_key isEqualToString:this_key])
1318 @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
1319 menu_key, this_key, this_val);
1321 menu_key = this_key;
1323 /* If this menu has the cmd line "-mode foo" then set this item's
1324 value to "foo" (the menu itself will be bound to e.g. "modeString")
1327 set_menu_item_object (item, this_val);
1329 // Array holds ["Label", "resource-key", "resource-val"].
1330 [items addObject:[NSMutableArray arrayWithObjects:
1331 label, @"", this_val, nil]];
1335 // no arg-set -- only one menu item can be missing that.
1336 NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
1341 // Array holds ["Label", "resource-key", "resource-val"].
1342 [items addObject:[NSMutableArray arrayWithObjects:
1343 label, @"", @"", nil]];
1347 /* make sure the menu button has room for the text of this item,
1348 and remember the greatest width it has reached.
1351 [popup setTitle:label];
1353 NSRect r = [popup frame];
1354 if (r.size.width > max_width) max_width = r.size.width;
1355 # endif // USE_IPHONE
1359 NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
1363 /* We've added all of the menu items. If there was an item with no
1364 command-line switch, then it's the item that represents the default
1365 value. Now we must bind to that item as well... (We have to bind
1366 this one late, because if it was the first item, then we didn't
1367 yet know what resource was associated with this menu.)
1370 NSObject *def_obj = [defaultOptions objectForKey:menu_key];
1372 @"no default value for resource \"%@\" in menu item \"%@\"",
1382 set_menu_item_object (def_item, def_obj);
1383 # else // !USE_IPHONE
1384 for (NSMutableArray *a in items) {
1385 // Make sure each array contains the resource key.
1386 [a replaceObjectAtIndex:1 withObject:menu_key];
1387 // Make sure the default item contains the default resource value.
1388 if (def_obj && def_item &&
1389 [def_item isEqualToString:[a objectAtIndex:0]])
1390 [a replaceObjectAtIndex:2 withObject:def_obj];
1392 # endif // !USE_IPHONE
1396 # ifdef USE_PICKER_VIEW
1397 /* Finish tweaking the menu button itself.
1400 [popup setTitle:[def_item title]];
1401 NSRect r = [popup frame];
1402 r.size.width = max_width;
1404 # endif // USE_PICKER_VIEW
1407 # if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW)
1408 [self placeChild:popup on:parent];
1409 [self bindResource:popup key:menu_key];
1414 # ifdef USE_PICKER_VIEW
1415 // Store the items for this picker in the picker_values array.
1416 // This is so fucking stupid.
1418 int menu_number = [pref_keys count] - 1;
1419 if (! picker_values)
1420 picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain];
1421 while ([picker_values count] <= menu_number)
1422 [picker_values addObject:[NSArray arrayWithObjects: nil]];
1423 [picker_values replaceObjectAtIndex:menu_number withObject:items];
1424 [popup reloadAllComponents];
1426 # else // !USE_PICKER_VIEW
1428 [self placeSeparator];
1431 for (NSArray *item in items) {
1432 RadioButton *b = [[RadioButton alloc] initWithIndex:i
1434 [b setLineBreakMode:UILineBreakModeHeadTruncation];
1435 [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
1436 [self placeChild:b on:parent];
1440 [self placeSeparator];
1442 # endif // !USE_PICKER_VIEW
1443 # endif // !USE_IPHONE
1448 /* Creates an uneditable, wrapping NSTextField to display the given
1449 text enclosed by <description> ... </description> in the XML.
1451 - (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent
1453 NSString *text = nil;
1454 NSArray *children = [node children];
1455 int i, count = [children count];
1457 for (i = 0; i < count; i++) {
1458 NSXMLNode *child = [children objectAtIndex:i];
1459 NSString *s = [child objectValue];
1461 text = [text stringByAppendingString:s];
1466 text = unwrap (text);
1468 NSRect rect = [parent frame];
1469 rect.origin.x = rect.origin.y = 0;
1470 rect.size.width = 200;
1471 rect.size.height = 50; // sized later
1473 NSText *lab = [[NSText alloc] initWithFrame:rect];
1474 [lab setEditable:NO];
1475 [lab setDrawsBackground:NO];
1476 [lab setHorizontallyResizable:YES];
1477 [lab setVerticallyResizable:YES];
1478 [lab setString:text];
1482 # else // USE_IPHONE
1484 /* There's no way to put rich text or links inside a UILabel.
1486 I guess Apple expects us to use a UIWebView for this -- but there's
1487 no way to measure how tall the HTML-rendered text is (the answer is
1488 not: "document.height" via JavaScript) so we can't put the
1489 properly-sized cell in the table.
1491 This is some serious bullshit.
1493 Another option would be to subclass UILabel and replace its drawRect
1494 with new code that uses CTLineDraw. But that's a huge hassle.
1497 UILabel *lab = [self makeLabel:text];
1498 [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1500 [self placeSeparator];
1501 # endif // USE_IPHONE
1503 [self placeChild:lab on:parent];
1508 /* Creates the NSTextField described by the given XML node.
1510 - (void) makeTextField: (NSXMLNode *)node
1511 on: (NSView *)parent
1512 withLabel: (BOOL) label_p
1513 horizontal: (BOOL) horiz_p
1515 NSMutableDictionary *dict =
1516 [NSMutableDictionary dictionaryWithObjectsAndKeys:
1521 [self parseAttrs:dict node:node];
1522 NSString *label = [dict objectForKey:@"_label"];
1523 NSString *arg = [dict objectForKey:@"arg"];
1525 if (!label && label_p) {
1526 NSAssert1 (0, @"no _label in %@", [node name]);
1530 NSAssert1 (arg, @"no arg in %@", label);
1533 rect.origin.x = rect.origin.y = 0;
1534 rect.size.width = rect.size.height = 10;
1536 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1540 // make the default size be around 30 columns; a typical value for
1541 // these text fields is "xscreensaver-text --cols 40".
1543 [txt setStringValue:@"123456789 123456789 123456789 "];
1545 [[txt cell] setWraps:NO];
1546 [[txt cell] setScrollable:YES];
1547 [txt setStringValue:@""];
1549 # else // USE_IPHONE
1551 txt.adjustsFontSizeToFitWidth = YES;
1552 txt.textColor = [UIColor blackColor];
1553 txt.font = [UIFont systemFontOfSize: FONT_SIZE];
1554 txt.placeholder = @"";
1555 txt.borderStyle = UITextBorderStyleRoundedRect;
1556 txt.textAlignment = UITextAlignmentRight;
1557 txt.keyboardType = UIKeyboardTypeDefault; // Full kbd
1558 txt.autocorrectionType = UITextAutocorrectionTypeNo;
1559 txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
1560 txt.clearButtonMode = UITextFieldViewModeAlways;
1561 txt.returnKeyType = UIReturnKeyDone;
1562 txt.delegate = self;
1564 [txt setEnabled: YES];
1566 rect.size.height = [txt.font lineHeight] * 1.2;
1567 [txt setFrame:rect];
1569 # endif // USE_IPHONE
1572 LABEL *lab = [self makeLabel:label];
1573 [self placeChild:lab on:parent];
1577 [self placeChild:txt on:parent right:(label ? YES : NO)];
1579 [self bindSwitch:txt cmdline:arg];
1584 /* Creates the NSTextField described by the given XML node,
1585 and hooks it up to a Choose button and a file selector widget.
1587 - (void) makeFileSelector: (NSXMLNode *)node
1588 on: (NSView *)parent
1589 dirsOnly: (BOOL) dirsOnly
1590 withLabel: (BOOL) label_p
1591 editable: (BOOL) editable_p
1593 # ifndef USE_IPHONE // No files. No selectors.
1594 NSMutableDictionary *dict =
1595 [NSMutableDictionary dictionaryWithObjectsAndKeys:
1600 [self parseAttrs:dict node:node];
1601 NSString *label = [dict objectForKey:@"_label"];
1602 NSString *arg = [dict objectForKey:@"arg"];
1604 if (!label && label_p) {
1605 NSAssert1 (0, @"no _label in %@", [node name]);
1609 NSAssert1 (arg, @"no arg in %@", label);
1612 rect.origin.x = rect.origin.y = 0;
1613 rect.size.width = rect.size.height = 10;
1615 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1617 // make the default size be around 20 columns.
1619 [txt setStringValue:@"123456789 123456789 "];
1621 [txt setSelectable:YES];
1622 [txt setEditable:editable_p];
1623 [txt setBezeled:editable_p];
1624 [txt setDrawsBackground:editable_p];
1625 [[txt cell] setWraps:NO];
1626 [[txt cell] setScrollable:YES];
1627 [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
1628 [txt setStringValue:@""];
1632 lab = [self makeLabel:label];
1633 [self placeChild:lab on:parent];
1637 [self placeChild:txt on:parent right:(label ? YES : NO)];
1639 [self bindSwitch:txt cmdline:arg];
1642 // Make the text field and label be the same height, whichever is taller.
1645 rect.size.height = ([lab frame].size.height > [txt frame].size.height
1646 ? [lab frame].size.height
1647 : [txt frame].size.height);
1648 [txt setFrame:rect];
1651 // Now put a "Choose" button next to it.
1653 rect.origin.x = rect.origin.y = 0;
1654 rect.size.width = rect.size.height = 10;
1655 NSButton *choose = [[NSButton alloc] initWithFrame:rect];
1656 [choose setTitle:@"Choose..."];
1657 [choose setBezelStyle:NSRoundedBezelStyle];
1660 [self placeChild:choose on:parent right:YES];
1662 // center the Choose button around the midpoint of the text field.
1663 rect = [choose frame];
1664 rect.origin.y = ([txt frame].origin.y +
1665 (([txt frame].size.height - rect.size.height) / 2));
1666 [choose setFrameOrigin:rect.origin];
1668 [choose setTarget:[parent window]];
1670 [choose setAction:@selector(fileSelectorChooseDirsAction:)];
1672 [choose setAction:@selector(fileSelectorChooseAction:)];
1675 # endif // !USE_IPHONE
1681 /* Runs a modal file selector and sets the text field's value to the
1682 selected file or directory.
1685 do_file_selector (NSTextField *txt, BOOL dirs_p)
1687 NSOpenPanel *panel = [NSOpenPanel openPanel];
1688 [panel setAllowsMultipleSelection:NO];
1689 [panel setCanChooseFiles:!dirs_p];
1690 [panel setCanChooseDirectories:dirs_p];
1692 NSString *file = [txt stringValue];
1693 if ([file length] <= 0) {
1694 file = NSHomeDirectory();
1696 file = [file stringByAppendingPathComponent:@"Pictures"];
1699 // NSString *dir = [file stringByDeletingLastPathComponent];
1701 int result = [panel runModalForDirectory:file //dir
1702 file:nil //[file lastPathComponent]
1704 if (result == NSOKButton) {
1705 NSArray *files = [panel filenames];
1706 file = ([files count] > 0 ? [files objectAtIndex:0] : @"");
1707 file = [file stringByAbbreviatingWithTildeInPath];
1708 [txt setStringValue:file];
1710 // Fuck me! Just setting the value of the NSTextField does not cause
1711 // that to end up in the preferences!
1713 NSDictionary *dict = [txt infoForBinding:@"value"];
1714 NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
1715 NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
1716 if ([path hasPrefix:@"values."]) // WTF.
1717 path = [path substringFromIndex:7];
1718 [[prefs values] setValue:file forKey:path];
1721 // make sure the end of the string is visible.
1722 NSText *fe = [[txt window] fieldEditor:YES forObject:txt];
1724 range.location = [file length]-3;
1726 if (! [[txt window] makeFirstResponder:[txt window]])
1727 [[txt window] endEditingFor:nil];
1728 // [[txt window] makeFirstResponder:nil];
1729 [fe setSelectedRange:range];
1730 // [tv scrollRangeToVisible:range];
1731 // [txt setNeedsDisplay:YES];
1732 // [[txt cell] setNeedsDisplay:YES];
1733 // [txt selectAll:txt];
1739 /* Returns the NSTextField that is to the left of or above the NSButton.
1741 static NSTextField *
1742 find_text_field_of_button (NSButton *button)
1744 NSView *parent = [button superview];
1745 NSArray *kids = [parent subviews];
1746 int nkids = [kids count];
1749 for (i = 0; i < nkids; i++) {
1750 NSObject *kid = [kids objectAtIndex:i];
1751 if ([kid isKindOfClass:[NSTextField class]]) {
1752 f = (NSTextField *) kid;
1753 } else if (kid == button) {
1762 - (void) fileSelectorChooseAction:(NSObject *)arg
1764 NSButton *choose = (NSButton *) arg;
1765 NSTextField *txt = find_text_field_of_button (choose);
1766 do_file_selector (txt, NO);
1769 - (void) fileSelectorChooseDirsAction:(NSObject *)arg
1771 NSButton *choose = (NSButton *) arg;
1772 NSTextField *txt = find_text_field_of_button (choose);
1773 do_file_selector (txt, YES);
1776 #endif // !USE_IPHONE
1779 - (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
1784 (x) Computer name and time
1785 ( ) Text [__________________________]
1786 ( ) Text file [_________________] [Choose]
1787 ( ) URL [__________________________]
1788 ( ) Shell Cmd [__________________________]
1790 textMode -text-mode date
1791 textMode -text-mode literal textLiteral -text-literal %
1792 textMode -text-mode file textFile -text-file %
1793 textMode -text-mode url textURL -text-url %
1794 textMode -text-mode program textProgram -text-program %
1797 rect.size.width = rect.size.height = 1;
1798 rect.origin.x = rect.origin.y = 0;
1799 NSView *group = [[NSView alloc] initWithFrame:rect];
1800 NSView *rgroup = [[NSView alloc] initWithFrame:rect];
1802 Bool program_p = TRUE;
1807 // This is how you link radio buttons together.
1809 NSButtonCell *proto = [[NSButtonCell alloc] init];
1810 [proto setButtonType:NSRadioButton];
1812 rect.origin.x = rect.origin.y = 0;
1813 rect.size.width = rect.size.height = 10;
1814 NSMatrix *matrix = [[NSMatrix alloc]
1816 mode:NSRadioModeMatrix
1818 numberOfRows: 4 + (program_p ? 1 : 0)
1820 [matrix setAllowsEmptySelection:NO];
1822 NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil];
1823 [cnames addObject:@"Computer name and time"];
1824 [cnames addObject:@"Text"];
1825 [cnames addObject:@"File"];
1826 [cnames addObject:@"URL"];
1827 if (program_p) [cnames addObject:@"Shell Cmd"];
1828 [matrix bind:@"content"
1830 withKeyPath:@"arrangedObjects"
1834 [self bindSwitch:matrix cmdline:@"-text-mode %"];
1836 [self placeChild:matrix on:group];
1837 [self placeChild:rgroup on:group right:YES];
1841 # else // USE_IPHONE
1843 NSView *rgroup = parent;
1846 // <select id="textMode">
1847 // <option id="date" _label="Display date" arg-set="-text-mode date"/>
1848 // <option id="text" _label="Display text" arg-set="-text-mode literal"/>
1849 // <option id="url" _label="Display URL"/>
1852 node2 = [[NSXMLElement alloc] initWithName:@"select"];
1853 [node2 setAttributesAsDictionary:
1854 [NSDictionary dictionaryWithObjectsAndKeys:
1858 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
1859 [node3 setAttributesAsDictionary:
1860 [NSDictionary dictionaryWithObjectsAndKeys:
1862 @"-text-mode date", @"arg-set",
1863 @"Display the date and time", @"_label",
1865 [node3 setParent: node2];
1868 node3 = [[NSXMLElement alloc] initWithName:@"option"];
1869 [node3 setAttributesAsDictionary:
1870 [NSDictionary dictionaryWithObjectsAndKeys:
1872 @"-text-mode literal", @"arg-set",
1873 @"Display static text", @"_label",
1875 [node3 setParent: node2];
1878 node3 = [[NSXMLElement alloc] initWithName:@"option"];
1879 [node3 setAttributesAsDictionary:
1880 [NSDictionary dictionaryWithObjectsAndKeys:
1882 @"Display the contents of a URL", @"_label",
1884 [node3 setParent: node2];
1887 [self makeOptionMenu:node2 on:rgroup];
1889 # endif // USE_IPHONE
1892 // <string id="textLiteral" _label="" arg-set="-text-literal %"/>
1893 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1894 [node2 setAttributesAsDictionary:
1895 [NSDictionary dictionaryWithObjectsAndKeys:
1896 @"textLiteral", @"id",
1897 @"-text-literal %", @"arg",
1899 @"Text to display", @"_label",
1902 [self makeTextField:node2 on:rgroup
1910 // rect = [last_child(rgroup) frame];
1912 /* // trying to make the text fields be enabled only when the checkbox is on..
1913 control = last_child (rgroup);
1914 [control bind:@"enabled"
1915 toObject:[matrix cellAtRow:1 column:0]
1916 withKeyPath:@"value"
1922 // <file id="textFile" _label="" arg-set="-text-file %"/>
1923 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1924 [node2 setAttributesAsDictionary:
1925 [NSDictionary dictionaryWithObjectsAndKeys:
1927 @"-text-file %", @"arg",
1929 [self makeFileSelector:node2 on:rgroup
1930 dirsOnly:NO withLabel:NO editable:NO];
1931 # endif // !USE_IPHONE
1933 // rect = [last_child(rgroup) frame];
1935 // <string id="textURL" _label="" arg-set="text-url %"/>
1936 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1937 [node2 setAttributesAsDictionary:
1938 [NSDictionary dictionaryWithObjectsAndKeys:
1940 @"-text-url %", @"arg",
1942 @"URL to display", @"_label",
1945 [self makeTextField:node2 on:rgroup
1953 // rect = [last_child(rgroup) frame];
1957 // <string id="textProgram" _label="" arg-set="text-program %"/>
1958 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1959 [node2 setAttributesAsDictionary:
1960 [NSDictionary dictionaryWithObjectsAndKeys:
1961 @"textProgram", @"id",
1962 @"-text-program %", @"arg",
1964 [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO];
1967 // rect = [last_child(rgroup) frame];
1969 layout_group (rgroup, NO);
1971 rect = [rgroup frame];
1972 rect.size.width += 35; // WTF? Why is rgroup too narrow?
1973 [rgroup setFrame:rect];
1976 // Set the height of the cells in the radio-box matrix to the height of
1977 // the (last of the) text fields.
1978 control = last_child (rgroup);
1979 rect = [control frame];
1980 rect.size.width = 30; // width of the string "Text", plus a bit...
1982 rect.size.width += 25;
1983 rect.size.height += LINE_SPACING;
1984 [matrix setCellSize:rect.size];
1985 [matrix sizeToCells];
1987 layout_group (group, YES);
1988 rect = [matrix frame];
1989 rect.origin.x += rect.size.width + COLUMN_SPACING;
1990 rect.origin.y -= [control frame].size.height - LINE_SPACING;
1991 [rgroup setFrameOrigin:rect.origin];
1993 // now cheat on the size of the matrix: allow it to overlap (underlap)
1996 rect.size = [matrix cellSize];
1997 rect.size.width = 300;
1998 [matrix setCellSize:rect.size];
1999 [matrix sizeToCells];
2001 // Cheat on the position of the stuff on the right (the rgroup).
2002 // GAAAH, this code is such crap!
2003 rect = [rgroup frame];
2005 [rgroup setFrame:rect];
2008 rect.size.width = rect.size.height = 0;
2009 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2010 [box setTitlePosition:NSAtTop];
2011 [box setBorderType:NSBezelBorder];
2012 [box setTitle:@"Display Text"];
2014 rect.size.width = rect.size.height = 12;
2015 [box setContentViewMargins:rect.size];
2016 [box setContentView:group];
2019 [self placeChild:box on:parent];
2021 # endif // !USE_IPHONE
2025 - (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2029 [x] Grab desktop images
2030 [ ] Choose random image:
2031 [__________________________] [Choose]
2033 <boolean id="grabDesktopImages" _label="Grab desktop images"
2034 arg-unset="-no-grab-desktop"/>
2035 <boolean id="chooseRandomImages" _label="Grab desktop images"
2036 arg-unset="-choose-random-images"/>
2037 <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
2040 NSXMLElement *node2;
2042 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2043 [node2 setAttributesAsDictionary:
2044 [NSDictionary dictionaryWithObjectsAndKeys:
2045 @"grabDesktopImages", @"id",
2046 @"Grab desktop images", @"_label",
2047 @"-no-grab-desktop", @"arg-unset",
2049 [self makeCheckbox:node2 on:parent];
2051 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2052 [node2 setAttributesAsDictionary:
2053 [NSDictionary dictionaryWithObjectsAndKeys:
2054 @"chooseRandomImages", @"id",
2055 @"Choose random images", @"_label",
2056 @"-choose-random-images", @"arg-set",
2058 [self makeCheckbox:node2 on:parent];
2060 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2061 [node2 setAttributesAsDictionary:
2062 [NSDictionary dictionaryWithObjectsAndKeys:
2063 @"imageDirectory", @"id",
2064 @"Images from:", @"_label",
2065 @"-image-directory %", @"arg",
2067 [self makeFileSelector:node2 on:parent
2068 dirsOnly:YES withLabel:YES editable:YES];
2070 // Add a second, explanatory label below the file/URL selector.
2073 lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"];
2074 [self placeChild:lab2 on:parent];
2076 // Pack it in a little tighter vertically.
2077 NSRect r2 = [lab2 frame];
2080 [lab2 setFrameOrigin:r2.origin];
2082 # endif // !USE_IPHONE
2086 #pragma mark Layout for controls
2091 last_child (NSView *parent)
2093 NSArray *kids = [parent subviews];
2094 int nkids = [kids count];
2098 return [kids objectAtIndex:nkids-1];
2100 #endif // USE_IPHONE
2103 /* Add the child as a subview of the parent, positioning it immediately
2104 below or to the right of the previously-added child of that view.
2106 - (void) placeChild:
2112 on:(NSView *)parent right:(BOOL)right_p
2115 NSRect rect = [child frame];
2116 NSView *last = last_child (parent);
2118 rect.origin.x = LEFT_MARGIN;
2119 rect.origin.y = ([parent frame].size.height - rect.size.height
2121 } else if (right_p) {
2122 rect = [last frame];
2123 rect.origin.x += rect.size.width + COLUMN_SPACING;
2125 rect = [last frame];
2126 rect.origin.x = LEFT_MARGIN;
2127 rect.origin.y -= [child frame].size.height + LINE_SPACING;
2129 NSRect r = [child frame];
2130 r.origin = rect.origin;
2132 [parent addSubview:child];
2134 # else // USE_IPHONE
2136 // Controls is an array of arrays of the controls, divided into sections.
2138 controls = [[NSMutableArray arrayWithCapacity:10] retain];
2139 if ([controls count] == 0)
2140 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2141 NSMutableArray *current = [controls objectAtIndex:[controls count]-1];
2143 if (!right_p || [current count] == 0) {
2144 // Nothing on the current line. Add this object.
2145 [current addObject: child];
2147 // Something's on the current line already.
2148 NSObject *old = [current objectAtIndex:[current count]-1];
2149 if ([old isKindOfClass:[NSMutableArray class]]) {
2150 // Already an array in this cell. Append.
2151 NSAssert ([(NSArray *) old count] < 4, @"internal error");
2152 [(NSMutableArray *) old addObject: child];
2154 // Replace the control in this cell with an array, then app
2155 NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil];
2156 [current replaceObjectAtIndex:[current count]-1 withObject:a];
2159 # endif // USE_IPHONE
2163 - (void) placeChild:(NSView *)child on:(NSView *)parent
2165 [self placeChild:child on:parent right:NO];
2171 // Start putting subsequent children in a new group, to create a new
2172 // section on the UITableView.
2174 - (void) placeSeparator
2176 if (! controls) return;
2177 if ([controls count] == 0) return;
2178 if ([[controls objectAtIndex:[controls count]-1]
2180 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2182 #endif // USE_IPHONE
2186 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
2187 wrapped in <hgroup> or <vgroup> in the XML.
2189 - (void) makeGroup:(NSXMLNode *)node
2191 horizontal:(BOOL) horiz_p
2194 if (!horiz_p) [self placeSeparator];
2195 [self traverseChildren:node on:parent];
2196 if (!horiz_p) [self placeSeparator];
2197 # else // !USE_IPHONE
2199 rect.size.width = rect.size.height = 1;
2200 rect.origin.x = rect.origin.y = 0;
2201 NSView *group = [[NSView alloc] initWithFrame:rect];
2202 [self traverseChildren:node on:group];
2204 layout_group (group, horiz_p);
2206 rect.size.width = rect.size.height = 0;
2207 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2208 [box setTitlePosition:NSNoTitle];
2209 [box setBorderType:NSNoBorder];
2210 [box setContentViewMargins:rect.size];
2211 [box setContentView:group];
2214 [self placeChild:box on:parent];
2215 # endif // !USE_IPHONE
2221 layout_group (NSView *group, BOOL horiz_p)
2223 NSArray *kids = [group subviews];
2224 int nkids = [kids count];
2226 double maxx = 0, miny = 0;
2227 for (i = 0; i < nkids; i++) {
2228 NSView *kid = [kids objectAtIndex:i];
2229 NSRect r = [kid frame];
2232 maxx += r.size.width + COLUMN_SPACING;
2233 if (r.size.height > -miny) miny = -r.size.height;
2235 if (r.size.width > maxx) maxx = r.size.width;
2236 miny = r.origin.y - r.size.height;
2243 rect.size.width = maxx;
2244 rect.size.height = -miny;
2245 [group setFrame:rect];
2248 for (i = 0; i < nkids; i++) {
2249 NSView *kid = [kids objectAtIndex:i];
2250 NSRect r = [kid frame];
2252 r.origin.y = rect.size.height - r.size.height;
2254 x += r.size.width + COLUMN_SPACING;
2261 #endif // !USE_IPHONE
2264 /* Create some kind of control corresponding to the given XML node.
2266 -(void)makeControl:(NSXMLNode *)node on:(NSView *)parent
2268 NSString *name = [node name];
2270 if ([node kind] == NSXMLCommentKind)
2273 if ([node kind] == NSXMLTextKind) {
2274 NSString *s = [(NSString *) [node objectValue]
2275 stringByTrimmingCharactersInSet:
2276 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
2277 if (! [s isEqualToString:@""]) {
2278 NSAssert1 (0, @"unexpected text: %@", s);
2283 if ([node kind] != NSXMLElementKind) {
2284 NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
2288 if ([name isEqualToString:@"hgroup"] ||
2289 [name isEqualToString:@"vgroup"]) {
2291 [self makeGroup:node on:parent
2292 horizontal:[name isEqualToString:@"hgroup"]];
2294 } else if ([name isEqualToString:@"command"]) {
2295 // do nothing: this is the "-root" business
2297 } else if ([name isEqualToString:@"boolean"]) {
2298 [self makeCheckbox:node on:parent];
2300 } else if ([name isEqualToString:@"string"]) {
2301 [self makeTextField:node on:parent withLabel:NO horizontal:NO];
2303 } else if ([name isEqualToString:@"file"]) {
2304 [self makeFileSelector:node on:parent
2305 dirsOnly:NO withLabel:YES editable:NO];
2307 } else if ([name isEqualToString:@"number"]) {
2308 [self makeNumberSelector:node on:parent];
2310 } else if ([name isEqualToString:@"select"]) {
2311 [self makeOptionMenu:node on:parent];
2313 } else if ([name isEqualToString:@"_description"]) {
2314 [self makeDescLabel:node on:parent];
2316 } else if ([name isEqualToString:@"xscreensaver-text"]) {
2317 [self makeTextLoaderControlBox:node on:parent];
2319 } else if ([name isEqualToString:@"xscreensaver-image"]) {
2320 [self makeImageLoaderControlBox:node on:parent];
2323 NSAssert1 (0, @"unknown tag: %@", name);
2328 /* Iterate over and process the children of this XML node.
2330 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent
2332 NSArray *children = [node children];
2333 int i, count = [children count];
2334 for (i = 0; i < count; i++) {
2335 NSXMLNode *child = [children objectAtIndex:i];
2336 [self makeControl:child on:parent];
2343 /* Kludgey magic to make the window enclose the controls we created.
2346 fix_contentview_size (NSView *parent)
2349 NSArray *kids = [parent subviews];
2350 int nkids = [kids count];
2351 NSView *text = 0; // the NSText at the bottom of the window
2352 double maxx = 0, miny = 0;
2355 /* Find the size of the rectangle taken up by each of the children
2356 except the final "NSText" child.
2358 for (i = 0; i < nkids; i++) {
2359 NSView *kid = [kids objectAtIndex:i];
2360 if ([kid isKindOfClass:[NSText class]]) {
2365 if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width;
2366 if (f.origin.y - f.size.height < miny) miny = f.origin.y;
2367 // NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2368 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2369 // f.origin.y + f.size.height, [kid class]);
2372 if (maxx < 400) maxx = 400; // leave room for the NSText paragraph...
2374 /* Now that we know the width of the window, set the width of the NSText to
2375 that, so that it can decide what its height needs to be.
2377 if (! text) abort();
2379 // NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2380 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2381 // f.origin.y + f.size.height, [text class]);
2383 // set the NSText's width (this changes its height).
2384 f.size.width = maxx - LEFT_MARGIN;
2387 // position the NSText below the last child (this gives us a new miny).
2389 f.origin.y = miny - f.size.height - LINE_SPACING;
2390 miny = f.origin.y - LINE_SPACING;
2393 // Lock the width of the field and unlock the height, and let it resize
2394 // once more, to compute the proper height of the text for that width.
2396 [(NSText *) text setHorizontallyResizable:NO];
2397 [(NSText *) text setVerticallyResizable:YES];
2398 [(NSText *) text sizeToFit];
2400 // Now lock the height too: no more resizing this text field.
2402 [(NSText *) text setVerticallyResizable:NO];
2404 // Now reposition the top edge of the text field to be back where it
2405 // was before we changed the height.
2407 float oh = f.size.height;
2409 float dh = f.size.height - oh;
2412 // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF??
2413 // If we do this in 10.6, the text field moves down, off the window.
2414 // So instead we repair it at the end, at the "WTF2" comment.
2417 // Also adjust the parent height by the change in height of the text field.
2420 // NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2421 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2422 // f.origin.y + f.size.height, [text class]);
2425 /* Set the contentView to the size of the children.
2428 // float yoff = f.size.height;
2429 f.size.width = maxx + LEFT_MARGIN;
2430 f.size.height = -(miny - LEFT_MARGIN*2);
2431 // yoff = f.size.height - yoff;
2432 [parent setFrame:f];
2434 // NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f",
2435 // f.size.width, f.size.height, f.origin.x, f.origin.y);
2437 /* Now move all of the kids up into the window.
2440 float shift = f.size.height;
2441 // NSLog(@"shift: %3.0f", shift);
2442 for (i = 0; i < nkids; i++) {
2443 NSView *kid = [kids objectAtIndex:i];
2445 f.origin.y += shift;
2447 // NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2448 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2449 // f.origin.y + f.size.height, [kid class]);
2454 parent: 420 x 541 @ 0 0
2455 text: 380 x 100 @ 20 22 miny=-501
2458 parent: 420 x 541 @ 0 0
2459 text: 380 x 100 @ 20 50 miny=-501
2462 // #### WTF2: See "WTF" above. If the text field is off the screen,
2463 // move it up. We need this on 10.6 but not on 10.5. Auugh.
2466 if (f.origin.y < 50) { // magic numbers, yay
2471 /* Set the kids to track the top left corner of the window when resized.
2472 Set the NSText to track the bottom right corner as well.
2474 for (i = 0; i < nkids; i++) {
2475 NSView *kid = [kids objectAtIndex:i];
2476 unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
2477 if ([kid isKindOfClass:[NSText class]])
2478 mask |= NSViewWidthSizable|NSViewHeightSizable;
2479 [kid setAutoresizingMask:mask];
2482 # endif // !USE_IPHONE
2488 wrap_with_buttons (NSWindow *window, NSView *panel)
2492 // Make a box to hold the buttons at the bottom of the window.
2494 rect = [panel frame];
2495 rect.origin.x = rect.origin.y = 0;
2496 rect.size.height = 10;
2497 NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
2498 [bbox setTitlePosition:NSNoTitle];
2499 [bbox setBorderType:NSNoBorder];
2501 // Make some buttons: Default, Cancel, OK
2503 rect.origin.x = rect.origin.y = 0;
2504 rect.size.width = rect.size.height = 10;
2505 NSButton *reset = [[NSButton alloc] initWithFrame:rect];
2506 [reset setTitle:@"Reset to Defaults"];
2507 [reset setBezelStyle:NSRoundedBezelStyle];
2510 rect = [reset frame];
2511 NSButton *ok = [[NSButton alloc] initWithFrame:rect];
2512 [ok setTitle:@"OK"];
2513 [ok setBezelStyle:NSRoundedBezelStyle];
2515 rect = [bbox frame];
2516 rect.origin.x = rect.size.width - [ok frame].size.width;
2517 [ok setFrameOrigin:rect.origin];
2520 NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
2521 [cancel setTitle:@"Cancel"];
2522 [cancel setBezelStyle:NSRoundedBezelStyle];
2524 rect.origin.x -= [cancel frame].size.width + 10;
2525 [cancel setFrameOrigin:rect.origin];
2527 // Bind OK to RET and Cancel to ESC.
2528 [ok setKeyEquivalent:@"\r"];
2529 [cancel setKeyEquivalent:@"\e"];
2531 // The correct width for OK and Cancel buttons is 68 pixels
2532 // ("Human Interface Guidelines: Controls: Buttons:
2533 // Push Button Specifications").
2536 rect.size.width = 68;
2539 rect = [cancel frame];
2540 rect.size.width = 68;
2541 [cancel setFrame:rect];
2543 // It puts the buttons in the box or else it gets the hose again
2545 [bbox addSubview:ok];
2546 [bbox addSubview:cancel];
2547 [bbox addSubview:reset];
2550 // make a box to hold the button-box, and the preferences view
2552 rect = [bbox frame];
2553 rect.origin.y += rect.size.height;
2554 NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
2555 [pbox setTitlePosition:NSNoTitle];
2556 [pbox setBorderType:NSBezelBorder];
2558 // Enforce a max height on the dialog, so that it's obvious to me
2559 // (on a big screen) when the dialog will fall off the bottom of
2560 // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
2562 NSRect f = [panel frame];
2563 int screen_height = (768 // shortest "modern" Mac display
2565 - 56 // System Preferences toolbar
2566 - 140 // default magnified bottom dock icon
2568 if (f.size.height > screen_height) {
2569 NSLog(@"%@ height was %.0f; clipping to %d",
2570 [panel class], f.size.height, screen_height);
2571 f.size.height = screen_height;
2576 [pbox addSubview:panel];
2577 [pbox addSubview:bbox];
2580 [reset setAutoresizingMask:NSViewMaxXMargin];
2581 [cancel setAutoresizingMask:NSViewMinXMargin];
2582 [ok setAutoresizingMask:NSViewMinXMargin];
2583 [bbox setAutoresizingMask:NSViewWidthSizable];
2587 [ok setTarget:window];
2588 [cancel setTarget:window];
2589 [reset setTarget:window];
2590 [ok setAction:@selector(okAction:)];
2591 [cancel setAction:@selector(cancelAction:)];
2592 [reset setAction:@selector(resetAction:)];
2596 #endif // !USE_IPHONE
2599 /* Iterate over and process the children of the root node of the XML document.
2601 - (void)traverseTree
2604 NSView *parent = [self view];
2606 NSWindow *parent = self;
2608 NSXMLNode *node = xml_root;
2610 if (![[node name] isEqualToString:@"screensaver"]) {
2611 NSAssert (0, @"top level node is not <xscreensaver>");
2614 saver_name = [self parseXScreenSaverTag: node];
2615 [saver_name retain];
2620 rect.origin.x = rect.origin.y = 0;
2621 rect.size.width = rect.size.height = 1;
2623 NSView *panel = [[NSView alloc] initWithFrame:rect];
2624 [self traverseChildren:node on:panel];
2625 fix_contentview_size (panel);
2627 NSView *root = wrap_with_buttons (parent, panel);
2628 [userDefaultsController setAppliesImmediately:NO];
2630 [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
2632 rect = [parent frameRectForContentRect:[root frame]];
2633 [parent setFrame:rect display:NO];
2634 [parent setMinSize:rect.size];
2636 [parent setContentView:root];
2638 # else // USE_IPHONE
2640 CGRect r = [parent frame];
2641 r.size = [[UIScreen mainScreen] bounds].size;
2642 [parent setFrame:r];
2643 [self traverseChildren:node on:parent];
2645 # endif // USE_IPHONE
2649 - (void)parser:(NSXMLParser *)parser
2650 didStartElement:(NSString *)elt
2651 namespaceURI:(NSString *)ns
2652 qualifiedName:(NSString *)qn
2653 attributes:(NSDictionary *)attrs
2655 NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt];
2656 [e setKind:SimpleXMLElementKind];
2657 [e setAttributesAsDictionary:attrs];
2658 NSXMLElement *p = xml_parsing;
2662 xml_root = xml_parsing;
2665 - (void)parser:(NSXMLParser *)parser
2666 didEndElement:(NSString *)elt
2667 namespaceURI:(NSString *)ns
2668 qualifiedName:(NSString *)qn
2670 NSXMLElement *p = xml_parsing;
2672 NSLog(@"extra close: %@", elt);
2673 } else if (![[p name] isEqualToString:elt]) {
2674 NSLog(@"%@ closed by %@", [p name], elt);
2676 NSXMLElement *n = xml_parsing;
2677 xml_parsing = [n parent];
2682 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
2684 NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"];
2685 [e setKind:SimpleXMLTextKind];
2686 NSXMLElement *p = xml_parsing;
2688 [e setObjectValue: string];
2693 # ifdef USE_PICKER_VIEW
2695 #pragma mark UIPickerView delegate methods
2697 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv
2699 return 1; // Columns
2702 - (NSInteger)pickerView:(UIPickerView *)pv
2703 numberOfRowsInComponent:(NSInteger)column
2705 NSAssert (column == 0, @"weird column");
2706 NSArray *a = [picker_values objectAtIndex: [pv tag]];
2707 if (! a) return 0; // Too early?
2711 - (CGFloat)pickerView:(UIPickerView *)pv
2712 rowHeightForComponent:(NSInteger)column
2717 - (CGFloat)pickerView:(UIPickerView *)pv
2718 widthForComponent:(NSInteger)column
2720 NSAssert (column == 0, @"weird column");
2721 NSArray *a = [picker_values objectAtIndex: [pv tag]];
2722 if (! a) return 0; // Too early?
2724 UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]];
2726 for (NSArray *a2 in a) {
2727 NSString *s = [a2 objectAtIndex:0];
2728 CGSize r = [s sizeWithFont:f];
2729 if (r.width > max) max = r.width;
2732 max *= 1.7; // WTF!!
2744 - (NSString *)pickerView:(UIPickerView *)pv
2745 titleForRow:(NSInteger)row
2746 forComponent:(NSInteger)column
2748 NSAssert (column == 0, @"weird column");
2749 NSArray *a = [picker_values objectAtIndex: [pv tag]];
2750 if (! a) return 0; // Too early?
2751 a = [a objectAtIndex:row];
2752 NSAssert (a, @"internal error");
2753 return [a objectAtIndex:0];
2756 # endif // USE_PICKER_VIEW
2759 #pragma mark UITableView delegate methods
2761 - (void) addResetButton
2763 [[self navigationItem]
2764 setRightBarButtonItem: [[UIBarButtonItem alloc]
2765 initWithTitle: @"Reset to Defaults"
2766 style: UIBarButtonItemStyleBordered
2768 action:@selector(resetAction:)]];
2769 NSString *s = saver_name;
2770 if ([self view].frame.size.width > 320)
2771 s = [s stringByAppendingString: @" Settings"];
2772 [self navigationItem].title = s;
2776 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
2781 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
2782 // Number of vertically-stacked white boxes.
2783 return [controls count];
2786 - (NSInteger)tableView:(UITableView *)tableView
2787 numberOfRowsInSection:(NSInteger)section
2789 // Number of lines in each vertically-stacked white box.
2790 NSAssert (controls, @"internal error");
2791 return [[controls objectAtIndex:section] count];
2794 - (NSString *)tableView:(UITableView *)tv
2795 titleForHeaderInSection:(NSInteger)section
2797 // Titles above each vertically-stacked white box.
2798 // if (section == 0)
2799 // return [saver_name stringByAppendingString:@" Settings"];
2804 - (CGFloat)tableView:(UITableView *)tv
2805 heightForRowAtIndexPath:(NSIndexPath *)ip
2807 CGFloat h = [tv rowHeight];
2809 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
2810 objectAtIndex:[ip indexAtPosition:1]];
2812 if ([ctl isKindOfClass:[NSArray class]]) {
2813 NSArray *set = (NSArray *) ctl;
2814 switch ([set count]) {
2816 # ifdef LABEL_ABOVE_SLIDER
2817 h *= 1.7; break; // label + left/slider/right: 2 1/2 lines
2819 case 3: h *= 1.2; break; // left/slider/right: 1 1/2 lines
2821 if ([[set objectAtIndex:1] isKindOfClass:[UITextField class]])
2825 } else if ([ctl isKindOfClass:[UILabel class]]) {
2826 UILabel *t = (UILabel *) ctl;
2828 r.size.width = 250; // WTF! Black magic!
2829 r.size.width -= LEFT_MARGIN;
2833 h = r.size.height + LINE_SPACING * 3;
2836 CGFloat h2 = [ctl frame].size.height;
2837 h2 += LINE_SPACING * 2;
2845 - (void)refreshTableView
2847 UITableView *tv = (UITableView *) [self view];
2848 NSMutableArray *a = [NSMutableArray arrayWithCapacity:20];
2849 int rows = [self numberOfSectionsInTableView:tv];
2850 for (int i = 0; i < rows; i++) {
2851 int cols = [self tableView:tv numberOfRowsInSection:i];
2852 for (int j = 0; j < cols; j++) {
2856 [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]];
2861 [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone];
2866 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o
2868 [NSTimer scheduledTimerWithTimeInterval: 0
2870 selector:@selector(refreshTableView)
2876 #ifndef USE_PICKER_VIEW
2878 - (void)updateRadioGroupCell:(UITableViewCell *)cell
2879 button:(RadioButton *)b
2881 NSArray *item = [[b items] objectAtIndex: [b index]];
2882 NSString *pref_key = [item objectAtIndex:1];
2883 NSObject *pref_val = [item objectAtIndex:2];
2884 NSObject *current = [userDefaultsController objectForKey:pref_key];
2886 // Convert them both to strings and compare those, so that
2887 // we don't get screwed by int 1 versus string "1".
2888 // Will boolean true/1 screw us here too?
2890 NSString *pref_str = ([pref_val isKindOfClass:[NSString class]]
2891 ? (NSString *) pref_val
2892 : [(NSNumber *) pref_val stringValue]);
2893 NSString *current_str = ([current isKindOfClass:[NSString class]]
2894 ? (NSString *) current
2895 : [(NSNumber *) current stringValue]);
2896 BOOL match_p = [current_str isEqualToString:pref_str];
2898 // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str);
2901 [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
2903 [cell setAccessoryType:UITableViewCellAccessoryNone];
2907 - (void)tableView:(UITableView *)tv
2908 didSelectRowAtIndexPath:(NSIndexPath *)ip
2910 RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
2911 objectAtIndex:[ip indexAtPosition:1]];
2912 if (! [ctl isKindOfClass:[RadioButton class]])
2915 [self radioAction:ctl];
2916 [self refreshTableView];
2920 #endif // !USE_PICKER_VIEW
2924 - (UITableViewCell *)tableView:(UITableView *)tv
2925 cellForRowAtIndexPath:(NSIndexPath *)ip
2928 /* #### If we re-use cells, then clicking on a checkbox RadioButton
2929 (in non-USE_PICKER_VIEW mode) makes all the cells disappear.
2930 This doesn't happen if we don't re-use any cells. Oh well.
2932 NSString *id = [NSString stringWithFormat: @"%d:%d",
2933 [ip indexAtPosition:0],
2934 [ip indexAtPosition:1]];
2935 UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier: id];
2937 if (cell) return cell;
2940 UITableViewCell *cell;
2943 cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
2944 reuseIdentifier: id]
2946 cell.selectionStyle = UITableViewCellSelectionStyleNone;
2948 CGRect p = [cell frame];
2951 p.size.height = [self tableView:tv heightForRowAtIndexPath:ip];
2954 // Allocate more space to the controls on iPad screens,
2955 // and on landscape-mode iPhones.
2956 CGFloat ww = [tv frame].size.width;
2957 CGFloat left_edge = (ww > 700
2958 ? p.size.width * 0.9
2960 ? p.size.width * 0.5
2961 : p.size.width * 0.3);
2962 CGFloat right_edge = p.origin.x + p.size.width - LEFT_MARGIN;
2965 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
2966 objectAtIndex:[ip indexAtPosition:1]];
2968 if ([ctl isKindOfClass:[NSArray class]]) {
2969 // This cell has a set of objects in it.
2970 NSArray *set = (NSArray *) ctl;
2971 switch ([set count]) {
2974 // With 2 elements, the first of the pair must be a label.
2975 UILabel *label = (UILabel *) [set objectAtIndex: 0];
2976 NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type");
2977 ctl = [set objectAtIndex: 1];
2980 if ([ctl isKindOfClass:[UISwitch class]]) {
2981 // Flush right checkboxes.
2982 ctl.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
2983 r.size.width = 80; // Magic.
2984 r.origin.x = right_edge - r.size.width;
2986 // Expandable sliders.
2987 ctl.autoresizingMask = UIViewAutoresizingFlexibleWidth;
2988 r.origin.x = left_edge;
2989 r.size.width = right_edge - r.origin.x;
2991 r.origin.y = (p.size.height - r.size.height) / 2;
2995 NSView *box = [[UIView alloc] initWithFrame:p];
2996 [box addSubview: ctl];
2998 // cell.textLabel.text = [(UILabel *) ctl text];
3000 r.origin.x = LEFT_MARGIN;
3002 r.size.width = [ctl frame].origin.x - r.origin.x;
3003 r.size.height = p.size.height;
3005 [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
3006 label.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
3007 box. autoresizingMask = UIViewAutoresizingFlexibleWidth;
3008 [box addSubview: label];
3016 // With 3 elements, the first and last must be labels.
3017 // With 4 elements, the first, second and last must be labels.
3019 UILabel *top = ([set count] == 4
3020 ? [set objectAtIndex: i++]
3022 UILabel *left = [set objectAtIndex: i++];
3023 NSView *mid = [set objectAtIndex: i++];
3024 UILabel *right = [set objectAtIndex: i++];
3025 NSAssert (!top || [top isKindOfClass:[UILabel class]], @"WTF");
3026 NSAssert ( [left isKindOfClass:[UILabel class]], @"WTF");
3027 NSAssert ( ![mid isKindOfClass:[UILabel class]], @"WTF");
3028 NSAssert ( [right isKindOfClass:[UILabel class]], @"WTF");
3030 // 3 elements: control at top of cell.
3031 // 4 elements: center the control vertically.
3033 # ifdef LABEL_ABOVE_SLIDER
3034 left_edge = LEFT_MARGIN;
3036 r.origin.x = left_edge;
3037 r.size.width = right_edge - r.origin.x;
3038 r.origin.y = ([set count] == 3
3040 : (p.size.height - r.size.height) / 2);
3043 // Top label goes above, flush center/top.
3045 r.size = [[top text] sizeWithFont:[top font]
3047 CGSizeMake (p.size.width - LEFT_MARGIN*2,
3049 lineBreakMode:[top lineBreakMode]];
3050 r.origin.x = (p.size.width - r.size.width) / 2;
3055 // Left label goes under control, flush left/bottom.
3056 r.size = [[left text] sizeWithFont:[left font]
3058 CGSizeMake(p.size.width - LEFT_MARGIN*2,
3060 lineBreakMode:[left lineBreakMode]];
3061 r.origin.x = [mid frame].origin.x;
3062 r.origin.y = p.size.height - r.size.height - 4;
3065 // Right label goes under control, flush right/bottom.
3067 r.size = [[right text] sizeWithFont:[right font]
3069 CGSizeMake(p.size.width - LEFT_MARGIN*2,
3071 lineBreakMode:[right lineBreakMode]];
3072 r.origin.x = ([mid frame].origin.x + [mid frame].size.width -
3074 r.origin.y = [left frame].origin.y;
3078 ctl = [[UIView alloc] initWithFrame:p];
3080 # ifdef LABEL_ABOVE_SLIDER
3081 [ctl addSubview: top];
3082 top.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin|
3083 UIViewAutoresizingFlexibleRightMargin);
3086 r.origin.x = LEFT_MARGIN;
3088 r.size.width = [mid frame].origin.x - r.origin.x;
3089 r.size.height = p.size.height;
3091 top.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
3092 [ctl addSubview: top];
3095 [ctl addSubview: left];
3096 [ctl addSubview: mid];
3097 [ctl addSubview: right];
3099 left. autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
3100 mid. autoresizingMask = UIViewAutoresizingFlexibleWidth;
3101 right.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
3102 ctl. autoresizingMask = UIViewAutoresizingFlexibleWidth;
3106 NSAssert (0, @"unhandled size");
3109 // A single view, not a pair.
3112 r.origin.x = LEFT_MARGIN;
3113 r.origin.y = LINE_SPACING;
3114 r.size.width = right_edge - r.origin.x;
3117 ctl.autoresizingMask = UIViewAutoresizingFlexibleWidth;
3119 # ifndef USE_PICKER_VIEW
3120 if ([ctl isKindOfClass:[RadioButton class]])
3121 [self updateRadioGroupCell:cell button:(RadioButton *)ctl];
3122 # endif // USE_PICKER_VIEW
3125 if ([ctl isKindOfClass:[UILabel class]]) {
3126 // Make label full height to allow text to line-wrap if necessary.
3128 r.origin.y = p.origin.y;
3129 r.size.height = p.size.height;
3133 [cell.contentView addSubview: ctl];
3137 # endif // USE_IPHONE
3140 /* When this object is instantiated, it parses the XML file and creates
3141 controls on itself that are hooked up to the appropriate preferences.
3142 The default size of the view is just big enough to hold them all.
3144 - (id)initWithXMLFile: (NSString *) xml_file
3145 options: (const XrmOptionDescRec *) _opts
3146 controller: (NSUserDefaultsController *) _prefs
3147 defaults: (NSDictionary *) _defs
3150 self = [super init];
3151 # else // !USE_IPHONE
3152 self = [super initWithStyle:UITableViewStyleGrouped];
3153 self.title = [saver_name stringByAppendingString:@" Settings"];
3154 # endif // !USE_IPHONE
3155 if (! self) return 0;
3157 // instance variables
3159 defaultOptions = _defs;
3160 userDefaultsController = _prefs;
3161 [userDefaultsController retain];
3163 NSURL *furl = [NSURL fileURLWithPath:xml_file];
3166 NSAssert1 (0, @"can't URLify \"%@\"", xml_file);
3170 #if 0 // -- the old way
3172 NSXMLDocument *xmlDoc = [[NSXMLDocument alloc]
3173 initWithContentsOfURL:furl
3174 options:(NSXMLNodePreserveWhitespace |
3175 NSXMLNodePreserveCDATA)
3177 if (!xmlDoc || err) {
3179 NSAssert2 (0, @"XML Error: %@: %@",
3180 xml_file, [err localizedDescription]);
3184 traverse_tree (prefs, self, opts, [xmlDoc rootElement]);
3188 NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithContentsOfURL:furl];
3190 NSAssert1 (0, @"XML Error: %@", xml_file);
3193 [xmlDoc setDelegate:self];
3194 if (! [xmlDoc parse]) {
3195 NSError *err = [xmlDoc parserError];
3196 NSAssert2 (0, @"XML Error: %@: %@", xml_file, err);
3200 [self traverseTree];
3204 [self addResetButton];
3213 [saver_name release];
3214 [userDefaultsController release];
3217 [pref_keys release];
3218 [pref_ctls release];
3219 # ifdef USE_PICKER_VIEW
3220 [picker_values release];