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
223 #pragma mark Talking to the resource database
226 /* Normally we read resources by looking up "KEY" in the database
227 "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone
228 app, everything is stored in the database "org.jwz.xscreensaver"
229 instead, so transform keys to "SAVERNAME.KEY".
231 NOTE: This is duplicated in PrefsReader.m, cause I suck.
233 - (NSString *) makeKey:(NSString *)key
236 NSString *prefix = [saver_name stringByAppendingString:@"."];
237 if (! [key hasPrefix:prefix]) // Don't double up!
238 key = [prefix stringByAppendingString:key];
244 - (NSString *) makeCKey:(const char *)key
246 return [self makeKey:[NSString stringWithCString:key
247 encoding:NSUTF8StringEncoding]];
251 /* Given a command-line option, returns the corresponding resource name.
252 Any arguments in the switch string are ignored (e.g., "-foo x").
254 - (NSString *) switchToResource:(NSString *)cmdline_switch
255 opts:(const XrmOptionDescRec *)opts_array
256 valRet:(NSString **)val_ret
260 NSAssert(cmdline_switch, @"cmdline switch is null");
261 if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
262 encoding:NSUTF8StringEncoding]) {
263 NSAssert1(0, @"unable to convert %@", cmdline_switch);
266 char *s = strpbrk(buf, " \t\r\n");
270 while (*tail && (*tail == ' ' || *tail == '\t'))
274 while (opts_array[0].option) {
275 if (!strcmp (opts_array[0].option, buf)) {
278 if (opts_array[0].argKind == XrmoptionNoArg) {
280 NSAssert1 (0, @"expected no args to switch: \"%@\"",
282 ret = opts_array[0].value;
285 NSAssert1 (0, @"expected args to switch: \"%@\"",
292 ? [NSString stringWithCString:ret
293 encoding:NSUTF8StringEncoding]
296 const char *res = opts_array[0].specifier;
297 while (*res && (*res == '.' || *res == '*'))
299 return [self makeCKey:res];
304 NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
311 // Called when a slider is bonked.
313 - (void)sliderAction:(UISlider*)sender
315 if ([active_text_field canResignFirstResponder])
316 [active_text_field resignFirstResponder];
317 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
318 double v = [sender value];
320 [userDefaultsController setInteger:v forKey:pref_key];
322 [userDefaultsController setDouble:v forKey:pref_key];
325 // Called when a checkbox/switch is bonked.
327 - (void)switchAction:(UISwitch*)sender
329 if ([active_text_field canResignFirstResponder])
330 [active_text_field resignFirstResponder];
331 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
332 NSString *v = ([sender isOn] ? @"true" : @"false");
333 [userDefaultsController setObject:v forKey:pref_key];
336 # ifdef USE_PICKER_VIEW
337 // Called when a picker is bonked.
339 - (void)pickerView:(UIPickerView *)pv
340 didSelectRow:(NSInteger)row
341 inComponent:(NSInteger)column
343 if ([active_text_field canResignFirstResponder])
344 [active_text_field resignFirstResponder];
346 NSAssert (column == 0, @"internal error");
347 NSArray *a = [picker_values objectAtIndex: [pv tag]];
348 if (! a) return; // Too early?
349 a = [a objectAtIndex:row];
350 NSAssert (a, @"missing row");
352 //NSString *label = [a objectAtIndex:0];
353 NSString *pref_key = [a objectAtIndex:1];
354 NSObject *pref_val = [a objectAtIndex:2];
355 [userDefaultsController setObject:pref_val forKey:pref_key];
357 # else // !USE_PICKER_VIEW
359 // Called when a RadioButton is bonked.
361 - (void)radioAction:(RadioButton*)sender
363 if ([active_text_field canResignFirstResponder])
364 [active_text_field resignFirstResponder];
366 NSArray *item = [[sender items] objectAtIndex: [sender index]];
367 NSString *pref_key = [item objectAtIndex:1];
368 NSObject *pref_val = [item objectAtIndex:2];
369 [userDefaultsController setObject:pref_val forKey:pref_key];
372 - (BOOL)textFieldShouldBeginEditing:(UITextField *)tf
374 active_text_field = tf;
378 - (void)textFieldDidEndEditing:(UITextField *)tf
380 NSString *pref_key = [pref_keys objectAtIndex: [tf tag]];
381 NSString *txt = [tf text];
382 [userDefaultsController setObject:txt forKey:pref_key];
385 - (BOOL)textFieldShouldReturn:(UITextField *)tf
387 active_text_field = nil;
388 [tf resignFirstResponder];
392 # endif // !USE_PICKER_VIEW
399 - (void) okAction:(NSObject *)arg
401 [userDefaultsController commitEditing];
402 [userDefaultsController save:self];
403 [NSApp endSheet:self returnCode:NSOKButton];
407 - (void) cancelAction:(NSObject *)arg
409 [userDefaultsController revert:self];
410 [NSApp endSheet:self returnCode:NSCancelButton];
413 # endif // !USE_IPHONE
416 - (void) resetAction:(NSObject *)arg
419 [userDefaultsController revertToInitialValues:self];
422 for (NSString *key in defaultOptions) {
423 NSObject *val = [defaultOptions objectForKey:key];
424 [userDefaultsController setObject:val forKey:key];
427 for (UIControl *ctl in pref_ctls) {
428 NSString *pref_key = [pref_keys objectAtIndex: ctl.tag];
429 [self bindResource:ctl key:pref_key reload:YES];
432 [self refreshTableView];
433 # endif // USE_IPHONE
437 /* Connects a control (checkbox, etc) to the corresponding preferences key.
439 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
440 reload:(BOOL)reload_p
443 NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
445 : ([control isKindOfClass:[NSMatrix class]]
449 toObject:userDefaultsController
450 withKeyPath:[@"values." stringByAppendingString: pref_key]
454 NSObject *val = [userDefaultsController objectForKey:pref_key];
458 if ([val isKindOfClass:[NSString class]]) {
459 sval = (NSString *) val;
460 if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] ||
461 NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] ||
462 NSOrderedSame == [sval caseInsensitiveCompare:@"1"])
465 dval = [sval doubleValue];
466 } else if ([val isKindOfClass:[NSNumber class]]) {
467 // NSBoolean (__NSCFBoolean) is really NSNumber.
468 dval = [(NSNumber *) val doubleValue];
469 sval = [(NSNumber *) val stringValue];
472 if ([control isKindOfClass:[UISlider class]]) {
473 sel = @selector(sliderAction:);
474 [(UISlider *) control setValue: dval];
475 } else if ([control isKindOfClass:[UISwitch class]]) {
476 sel = @selector(switchAction:);
477 [(UISwitch *) control setOn: ((int) dval != 0)];
478 # ifdef USE_PICKER_VIEW
479 } else if ([control isKindOfClass:[UIPickerView class]]) {
481 [(UIPickerView *) control selectRow:((int)dval) inComponent:0
483 # else // !USE_PICKER_VIEW
484 } else if ([control isKindOfClass:[RadioButton class]]) {
485 sel = 0; // radioAction: sent from didSelectRowAtIndexPath.
486 } else if ([control isKindOfClass:[UITextField class]]) {
488 [(UITextField *) control setText: sval];
489 # endif // !USE_PICKER_VIEW
491 NSAssert (0, @"unknown class");
494 // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval);
498 pref_keys = [[NSMutableArray arrayWithCapacity:10] retain];
499 pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain];
502 [pref_keys addObject: [self makeKey:pref_key]];
503 [pref_ctls addObject: control];
504 ((UIControl *) control).tag = [pref_keys count] - 1;
507 [(UIControl *) control addTarget:self action:sel
508 forControlEvents:UIControlEventValueChanged];
512 # endif // USE_IPHONE
515 NSObject *def = [[userDefaultsController defaults] objectForKey:pref_key];
516 NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
517 s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
518 s = [NSString stringWithFormat:@"%@ = \"%@\"", s, def];
519 s = [s stringByPaddingToLength:28 withString:@" " startingAtIndex:0];
520 NSLog (@"%@ %@/%@", s, [def class], [control class]);
525 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
527 [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO];
532 - (void) bindSwitch:(NSObject *)control
533 cmdline:(NSString *)cmd
535 [self bindResource:control
536 key:[self switchToResource:cmd opts:opts valRet:0]];
540 #pragma mark Text-manipulating utilities
544 unwrap (NSString *text)
546 // Unwrap lines: delete \n but do not delete \n\n.
548 NSArray *lines = [text componentsSeparatedByString:@"\n"];
549 int nlines = [lines count];
553 text = @"\n"; // start with one blank line
555 // skip trailing blank lines in file
556 for (i = nlines-1; i > 0; i--) {
557 NSString *s = (NSString *) [lines objectAtIndex:i];
563 // skip leading blank lines in file
564 for (i = 0; i < nlines; i++) {
565 NSString *s = (NSString *) [lines objectAtIndex:i];
572 for (; i < nlines; i++) {
573 NSString *s = (NSString *) [lines objectAtIndex:i];
574 if ([s length] == 0) {
575 text = [text stringByAppendingString:@"\n\n"];
577 } else if ([s characterAtIndex:0] == ' ' ||
578 [s hasPrefix:@"Copyright "] ||
579 [s hasPrefix:@"http://"]) {
580 // don't unwrap if the following line begins with whitespace,
581 // or with the word "Copyright", or if it begins with a URL.
583 text = [text stringByAppendingString:@"\n"];
584 text = [text stringByAppendingString:s];
589 text = [text stringByAppendingString:@" "];
590 text = [text stringByAppendingString:s];
601 /* Makes the text up to the first comma be bold.
604 boldify (NSText *nstext)
606 NSString *text = [nstext string];
607 NSRange r = [text rangeOfString:@"," options:0];
608 r.length = r.location+1;
612 NSFont *font = [nstext font];
613 font = [NSFont boldSystemFontOfSize:[font pointSize]];
614 [nstext setFont:font range:r];
616 # endif // !USE_IPHONE
619 /* Creates a human-readable anchor to put on a URL.
622 anchorize (const char *url)
624 const char *wiki = "http://en.wikipedia.org/wiki/";
625 const char *math = "http://mathworld.wolfram.com/";
626 if (!strncmp (wiki, url, strlen(wiki))) {
627 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
628 strcpy (anchor, "Wikipedia: \"");
629 const char *in = url + strlen(wiki);
630 char *out = anchor + strlen(anchor);
634 } else if (*in == '#') {
637 } else if (*in == '%') {
643 sscanf (hex, "%x", &n);
655 } else if (!strncmp (math, url, strlen(math))) {
656 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
657 strcpy (anchor, "MathWorld: \"");
658 const char *start = url + strlen(wiki);
659 const char *in = start;
660 char *out = anchor + strlen(anchor);
664 } else if (in != start && *in >= 'A' && *in <= 'Z') {
667 } else if (!strncmp (in, ".htm", 4)) {
684 /* Converts any http: URLs in the given text field to clickable links.
687 hreffify (NSText *nstext)
690 NSString *text = [nstext string];
691 [nstext setRichText:YES];
693 NSString *text = [nstext text];
696 int L = [text length];
697 NSRange start; // range is start-of-search to end-of-string
700 while (start.location < L) {
702 // Find the beginning of a URL...
704 NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
705 if (r2.location == NSNotFound)
708 // Next time around, start searching after this.
709 start.location = r2.location + r2.length;
710 start.length = L - start.location;
712 // Find the end of a URL (whitespace or EOF)...
714 NSRange r3 = [text rangeOfCharacterFromSet:
715 [NSCharacterSet whitespaceAndNewlineCharacterSet]
716 options:0 range:start];
717 if (r3.location == NSNotFound) // EOF
718 r3.location = L, r3.length = 0;
720 // Next time around, start searching after this.
721 start.location = r3.location;
722 start.length = L - start.location;
724 // Set r2 to the start/length of this URL.
725 r2.length = start.location - r2.location;
728 NSString *nsurl = [text substringWithRange:r2];
729 const char *url = [nsurl UTF8String];
731 // If this is a Wikipedia URL, make the linked text be prettier.
733 char *anchor = anchorize(url);
737 // Construct the RTF corresponding to <A HREF="url">anchor</A>
739 const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
740 char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
741 sprintf (rtf, fmt, url, anchor);
743 NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
744 [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
746 # else // !USE_IPHONE
747 // *anchor = 0; // Omit Wikipedia anchor
748 text = [text stringByReplacingCharactersInRange:r2
749 withString:[NSString stringWithCString:anchor
750 encoding:NSUTF8StringEncoding]];
751 // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
752 // withString:@"\n\n"];
753 # endif // !USE_IPHONE
757 int L2 = [text length]; // might have changed
758 start.location -= (L - L2);
763 [nstext setText:text];
769 #pragma mark Creating controls from XML
772 /* Parse the attributes of an XML tag into a dictionary.
773 For input, the dictionary should have as attributes the keys, each
774 with @"" as their value.
775 On output, the dictionary will set the keys to the values specified,
776 and keys that were not specified will not be present in the dictionary.
777 Warnings are printed if there are duplicate or unknown attributes.
779 - (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
781 NSArray *attrs = [(NSXMLElement *) node attributes];
782 int n = [attrs count];
785 // For each key in the dictionary, fill in the dict with the corresponding
786 // value. The value @"" is assumed to mean "un-set". Issue a warning if
787 // an attribute is specified twice.
789 for (i = 0; i < n; i++) {
790 NSXMLNode *attr = [attrs objectAtIndex:i];
791 NSString *key = [attr name];
792 NSString *val = [attr objectValue];
793 NSString *old = [dict objectForKey:key];
796 NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
797 } else if ([old length] != 0) {
798 NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
800 [dict setValue:val forKey:key];
804 // Remove from the dictionary any keys whose value is still @"",
805 // meaning there was no such attribute specified.
807 NSArray *keys = [dict allKeys];
809 for (i = 0; i < n; i++) {
810 NSString *key = [keys objectAtIndex:i];
811 NSString *val = [dict objectForKey:key];
812 if ([val length] == 0)
813 [dict removeObjectForKey:key];
818 /* Handle the options on the top level <xscreensaver> tag.
820 - (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
822 NSMutableDictionary *dict =
823 [NSMutableDictionary dictionaryWithObjectsAndKeys:
828 [self parseAttrs:dict node:node];
829 NSString *name = [dict objectForKey:@"name"];
830 NSString *label = [dict objectForKey:@"_label"];
832 NSAssert1 (label, @"no _label in %@", [node name]);
833 NSAssert1 (name, @"no name in \"%@\"", label);
838 /* Creates a label: an un-editable NSTextField displaying the given text.
840 - (LABEL *) makeLabel:(NSString *)text
843 rect.origin.x = rect.origin.y = 0;
844 rect.size.width = rect.size.height = 10;
846 NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
847 [lab setSelectable:NO];
848 [lab setEditable:NO];
850 [lab setDrawsBackground:NO];
851 [lab setStringValue:text];
854 UILabel *lab = [[UILabel alloc] initWithFrame:rect];
855 [lab setText: [text stringByTrimmingCharactersInSet:
856 [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
857 [lab setBackgroundColor:[UIColor clearColor]];
858 [lab setNumberOfLines:0]; // unlimited
859 [lab setLineBreakMode:UILineBreakModeWordWrap];
860 [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
861 UIViewAutoresizingFlexibleHeight)];
862 # endif // USE_IPHONE
867 /* Creates the checkbox (NSButton) described by the given XML node.
869 - (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
871 NSMutableDictionary *dict =
872 [NSMutableDictionary dictionaryWithObjectsAndKeys:
878 [self parseAttrs:dict node:node];
879 NSString *label = [dict objectForKey:@"_label"];
880 NSString *arg_set = [dict objectForKey:@"arg-set"];
881 NSString *arg_unset = [dict objectForKey:@"arg-unset"];
884 NSAssert1 (0, @"no _label in %@", [node name]);
887 if (!arg_set && !arg_unset) {
888 NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"",
891 if (arg_set && arg_unset) {
892 NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"",
896 // sanity-check the choice of argument names.
898 if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
899 [arg_set hasPrefix:@"--no-"]))
900 NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
902 if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
903 ![arg_unset hasPrefix:@"--no-"]))
904 NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
908 rect.origin.x = rect.origin.y = 0;
909 rect.size.width = rect.size.height = 10;
913 NSButton *button = [[NSButton alloc] initWithFrame:rect];
914 [button setButtonType:NSSwitchButton];
915 [button setTitle:label];
917 [self placeChild:button on:parent];
921 LABEL *lab = [self makeLabel:label];
922 [self placeChild:lab on:parent];
923 UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
924 [self placeChild:button on:parent right:YES];
927 # endif // USE_IPHONE
929 [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
934 /* Creates the number selection control described by the given XML node.
935 If "type=slider", it's an NSSlider.
936 If "type=spinbutton", it's a text field with up/down arrows next to it.
938 - (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
940 NSMutableDictionary *dict =
941 [NSMutableDictionary dictionaryWithObjectsAndKeys:
953 [self parseAttrs:dict node:node];
954 NSString *label = [dict objectForKey:@"_label"];
955 NSString *low_label = [dict objectForKey:@"_low-label"];
956 NSString *high_label = [dict objectForKey:@"_high-label"];
957 NSString *type = [dict objectForKey:@"type"];
958 NSString *arg = [dict objectForKey:@"arg"];
959 NSString *low = [dict objectForKey:@"low"];
960 NSString *high = [dict objectForKey:@"high"];
961 NSString *def = [dict objectForKey:@"default"];
962 NSString *cvt = [dict objectForKey:@"convert"];
964 NSAssert1 (arg, @"no arg in %@", label);
965 NSAssert1 (type, @"no type in %@", label);
968 NSAssert1 (0, @"no low in %@", [node name]);
972 NSAssert1 (0, @"no high in %@", [node name]);
976 NSAssert1 (0, @"no default in %@", [node name]);
979 if (cvt && ![cvt isEqualToString:@"invert"]) {
980 NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
984 // If either the min or max field contains a decimal point, then this
985 // option may have a floating point value; otherwise, it is constrained
988 NSCharacterSet *dot =
989 [NSCharacterSet characterSetWithCharactersInString:@"."];
990 BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
991 [high rangeOfCharacterFromSet:dot].location != NSNotFound);
993 if ([type isEqualToString:@"slider"]
994 # ifdef USE_IPHONE // On iPhone, we use sliders for all numeric values.
995 || [type isEqualToString:@"spinbutton"]
1000 rect.origin.x = rect.origin.y = 0;
1001 rect.size.width = 150;
1002 rect.size.height = 23; // apparent min height for slider with ticks...
1004 slider = [[InvertedSlider alloc] initWithFrame:rect
1006 integers: !float_p];
1007 [slider setMaxValue:[high doubleValue]];
1008 [slider setMinValue:[low doubleValue]];
1010 int range = [slider maxValue] - [slider minValue] + 1;
1013 while (range2 > max_ticks)
1016 // If we have elided ticks, leave it at the max number of ticks.
1017 if (range != range2 && range2 < max_ticks)
1020 // If it's a float, always display the max number of ticks.
1021 if (float_p && range2 < max_ticks)
1025 [slider setNumberOfTickMarks:range2];
1027 [slider setAllowsTickMarkValuesOnly:
1028 (range == range2 && // we are showing the actual number of ticks
1029 !float_p)]; // and we want integer results
1030 # endif // !USE_IPHONE
1032 // #### Note: when the slider's range is large enough that we aren't
1033 // showing all possible ticks, the slider's value is not constrained
1034 // to be an integer, even though it should be...
1035 // Maybe we need to use a value converter or something?
1039 lab = [self makeLabel:label];
1040 [self placeChild:lab on:parent];
1043 CGFloat s = [NSFont systemFontSize] + 4;
1044 [lab setFont:[NSFont boldSystemFontOfSize:s]];
1051 lab = [self makeLabel:low_label];
1052 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1054 [lab setAlignment:1]; // right aligned
1056 if (rect.size.width < LEFT_LABEL_WIDTH)
1057 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1058 rect.size.height = [slider frame].size.height;
1059 [lab setFrame:rect];
1060 [self placeChild:lab on:parent];
1061 # else // USE_IPHONE
1062 [lab setTextAlignment: UITextAlignmentRight];
1063 [self placeChild:lab on:parent right:(label ? YES : NO)];
1064 # endif // USE_IPHONE
1070 [self placeChild:slider on:parent right:(low_label ? YES : NO)];
1071 # else // USE_IPHONE
1072 [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
1073 # endif // USE_IPHONE
1076 // Make left label be same height as slider.
1078 rect.size.height = [slider frame].size.height;
1079 [lab setFrame:rect];
1083 rect = [slider frame];
1084 if (rect.origin.x < LEFT_LABEL_WIDTH)
1085 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
1086 [slider setFrame:rect];
1090 lab = [self makeLabel:high_label];
1091 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1094 // Make right label be same height as slider.
1095 rect.size.height = [slider frame].size.height;
1096 [lab setFrame:rect];
1097 [self placeChild:lab on:parent right:YES];
1101 [self bindSwitch:slider cmdline:arg];
1104 #ifndef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1106 } else if ([type isEqualToString:@"spinbutton"]) {
1109 NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
1112 NSAssert1 (!low_label,
1113 @"low-label not allowed in spinbutton \"%@\"", [node name]);
1114 NSAssert1 (!high_label,
1115 @"high-label not allowed in spinbutton \"%@\"", [node name]);
1116 NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
1120 rect.origin.x = rect.origin.y = 0;
1121 rect.size.width = rect.size.height = 10;
1123 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1124 [txt setStringValue:@"0000.0"];
1126 [txt setStringValue:@""];
1129 LABEL *lab = [self makeLabel:label];
1130 //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1131 [lab setAlignment:1]; // right aligned
1133 if (rect.size.width < LEFT_LABEL_WIDTH)
1134 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1135 rect.size.height = [txt frame].size.height;
1136 [lab setFrame:rect];
1137 [self placeChild:lab on:parent];
1141 [self placeChild:txt on:parent right:(label ? YES : NO)];
1145 if (rect.origin.x < LEFT_LABEL_WIDTH)
1146 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
1147 [txt setFrame:rect];
1150 rect.size.width = rect.size.height = 10;
1151 NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
1153 [self placeChild:step on:parent right:YES];
1154 rect = [step frame];
1155 rect.origin.x -= COLUMN_SPACING; // this one goes close
1156 rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
1157 [step setFrame:rect];
1159 [step setMinValue:[low doubleValue]];
1160 [step setMaxValue:[high doubleValue]];
1161 [step setAutorepeat:YES];
1162 [step setValueWraps:NO];
1164 double range = [high doubleValue] - [low doubleValue];
1166 [step setIncrement:range / 10.0];
1167 else if (range >= 500)
1168 [step setIncrement:range / 100.0];
1170 [step setIncrement:1.0];
1172 NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
1173 [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
1174 [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
1175 [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]];
1176 [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
1177 [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
1178 [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
1180 [fmt setGeneratesDecimalNumbers:float_p];
1181 [[txt cell] setFormatter:fmt];
1183 [self bindSwitch:step cmdline:arg];
1184 [self bindSwitch:txt cmdline:arg];
1189 # endif // USE_IPHONE
1192 NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
1199 set_menu_item_object (NSMenuItem *item, NSObject *obj)
1201 /* If the object associated with this menu item looks like a boolean,
1202 store an NSNumber instead of an NSString, since that's what
1203 will be in the preferences (due to similar logic in PrefsReader).
1205 if ([obj isKindOfClass:[NSString class]]) {
1206 NSString *string = (NSString *) obj;
1207 if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
1208 NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
1209 obj = [NSNumber numberWithBool:YES];
1210 else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
1211 NSOrderedSame == [string caseInsensitiveCompare:@"no"])
1212 obj = [NSNumber numberWithBool:NO];
1217 [item setRepresentedObject:obj];
1218 //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
1220 # endif // !USE_IPHONE
1223 /* Creates the popup menu described by the given XML node (and its children).
1225 - (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
1227 NSArray *children = [node children];
1228 int i, count = [children count];
1231 NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
1235 // get the "id" attribute off the <select> tag.
1237 NSMutableDictionary *dict =
1238 [NSMutableDictionary dictionaryWithObjectsAndKeys:
1241 [self parseAttrs:dict node:node];
1244 rect.origin.x = rect.origin.y = 0;
1245 rect.size.width = 10;
1246 rect.size.height = 10;
1248 NSString *menu_key = nil; // the resource key used by items in this menu
1251 // #### "Build and Analyze" says that all of our widgets leak, because it
1252 // seems to not realize that placeChild -> addSubview retains them.
1253 // Not sure what to do to make these warnings go away.
1255 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1257 NSMenuItem *def_item = nil;
1258 float max_width = 0;
1260 # else // USE_IPHONE
1262 NSString *def_item = nil;
1264 rect.size.width = 0;
1265 rect.size.height = 0;
1266 # ifdef USE_PICKER_VIEW
1267 UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain];
1268 popup.delegate = self;
1269 popup.dataSource = self;
1270 # endif // !USE_PICKER_VIEW
1271 NSMutableArray *items = [NSMutableArray arrayWithCapacity:10];
1273 # endif // USE_IPHONE
1275 for (i = 0; i < count; i++) {
1276 NSXMLNode *child = [children objectAtIndex:i];
1278 if ([child kind] == NSXMLCommentKind)
1280 if ([child kind] != NSXMLElementKind) {
1281 // NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
1285 // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
1287 NSMutableDictionary *dict2 =
1288 [NSMutableDictionary dictionaryWithObjectsAndKeys:
1293 [self parseAttrs:dict2 node:child];
1294 NSString *label = [dict2 objectForKey:@"_label"];
1295 NSString *arg_set = [dict2 objectForKey:@"arg-set"];
1298 NSAssert1 (0, @"no _label in %@", [child name]);
1303 // create the menu item (and then get a pointer to it)
1304 [popup addItemWithTitle:label];
1305 NSMenuItem *item = [popup itemWithTitle:label];
1306 # endif // USE_IPHONE
1309 NSString *this_val = NULL;
1310 NSString *this_key = [self switchToResource: arg_set
1313 NSAssert1 (this_val, @"this_val null for %@", arg_set);
1314 if (menu_key && ![menu_key isEqualToString:this_key])
1316 @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
1317 menu_key, this_key, this_val);
1319 menu_key = this_key;
1321 /* If this menu has the cmd line "-mode foo" then set this item's
1322 value to "foo" (the menu itself will be bound to e.g. "modeString")
1325 set_menu_item_object (item, this_val);
1327 // Array holds ["Label", "resource-key", "resource-val"].
1328 [items addObject:[NSMutableArray arrayWithObjects:
1329 label, @"", this_val, nil]];
1333 // no arg-set -- only one menu item can be missing that.
1334 NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
1339 // Array holds ["Label", "resource-key", "resource-val"].
1340 [items addObject:[NSMutableArray arrayWithObjects:
1341 label, @"", @"", nil]];
1345 /* make sure the menu button has room for the text of this item,
1346 and remember the greatest width it has reached.
1349 [popup setTitle:label];
1351 NSRect r = [popup frame];
1352 if (r.size.width > max_width) max_width = r.size.width;
1353 # endif // USE_IPHONE
1357 NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
1361 /* We've added all of the menu items. If there was an item with no
1362 command-line switch, then it's the item that represents the default
1363 value. Now we must bind to that item as well... (We have to bind
1364 this one late, because if it was the first item, then we didn't
1365 yet know what resource was associated with this menu.)
1368 NSObject *def_obj = [defaultOptions objectForKey:menu_key];
1370 @"no default value for resource \"%@\" in menu item \"%@\"",
1380 set_menu_item_object (def_item, def_obj);
1381 # else // !USE_IPHONE
1382 for (NSMutableArray *a in items) {
1383 // Make sure each array contains the resource key.
1384 [a replaceObjectAtIndex:1 withObject:menu_key];
1385 // Make sure the default item contains the default resource value.
1386 if (def_obj && def_item &&
1387 [def_item isEqualToString:[a objectAtIndex:0]])
1388 [a replaceObjectAtIndex:2 withObject:def_obj];
1390 # endif // !USE_IPHONE
1394 # ifdef USE_PICKER_VIEW
1395 /* Finish tweaking the menu button itself.
1398 [popup setTitle:[def_item title]];
1399 NSRect r = [popup frame];
1400 r.size.width = max_width;
1402 # endif // USE_PICKER_VIEW
1405 # if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW)
1406 [self placeChild:popup on:parent];
1407 [self bindResource:popup key:menu_key];
1412 # ifdef USE_PICKER_VIEW
1413 // Store the items for this picker in the picker_values array.
1414 // This is so fucking stupid.
1416 int menu_number = [pref_keys count] - 1;
1417 if (! picker_values)
1418 picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain];
1419 while ([picker_values count] <= menu_number)
1420 [picker_values addObject:[NSArray arrayWithObjects: nil]];
1421 [picker_values replaceObjectAtIndex:menu_number withObject:items];
1422 [popup reloadAllComponents];
1424 # else // !USE_PICKER_VIEW
1426 [self placeSeparator];
1429 for (NSArray *item in items) {
1430 RadioButton *b = [[RadioButton alloc] initWithIndex:i
1432 [b setFont:[NSFont boldSystemFontOfSize:
1433 // #### Fucking hardcoded UITableView font size BS!
1434 17 // [NSFont systemFontSize]
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
1483 UILabel *lab = [self makeLabel:text];
1484 [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1486 [self placeSeparator];
1487 # endif // USE_IPHONE
1489 [self placeChild:lab on:parent];
1494 /* Creates the NSTextField described by the given XML node.
1496 - (void) makeTextField: (NSXMLNode *)node
1497 on: (NSView *)parent
1498 withLabel: (BOOL) label_p
1499 horizontal: (BOOL) horiz_p
1501 NSMutableDictionary *dict =
1502 [NSMutableDictionary dictionaryWithObjectsAndKeys:
1507 [self parseAttrs:dict node:node];
1508 NSString *label = [dict objectForKey:@"_label"];
1509 NSString *arg = [dict objectForKey:@"arg"];
1511 if (!label && label_p) {
1512 NSAssert1 (0, @"no _label in %@", [node name]);
1516 NSAssert1 (arg, @"no arg in %@", label);
1519 rect.origin.x = rect.origin.y = 0;
1520 rect.size.width = rect.size.height = 10;
1522 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1526 // make the default size be around 30 columns; a typical value for
1527 // these text fields is "xscreensaver-text --cols 40".
1529 [txt setStringValue:@"123456789 123456789 123456789 "];
1531 [[txt cell] setWraps:NO];
1532 [[txt cell] setScrollable:YES];
1533 [txt setStringValue:@""];
1535 # else // USE_IPHONE
1537 txt.adjustsFontSizeToFitWidth = YES;
1538 txt.textColor = [UIColor blackColor];
1539 // #### Fucking hardcoded UITableView font size BS!
1540 txt.font = [UIFont systemFontOfSize: 17];
1541 txt.placeholder = @"";
1542 txt.borderStyle = UITextBorderStyleRoundedRect;
1543 txt.textAlignment = UITextAlignmentRight;
1544 txt.keyboardType = UIKeyboardTypeDefault; // Full kbd
1545 txt.autocorrectionType = UITextAutocorrectionTypeNo;
1546 txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
1547 txt.clearButtonMode = UITextFieldViewModeAlways;
1548 txt.returnKeyType = UIReturnKeyDone;
1549 txt.delegate = self;
1551 [txt setEnabled: YES];
1553 rect.size.height = [txt.font lineHeight] * 1.2;
1554 [txt setFrame:rect];
1556 # endif // USE_IPHONE
1559 LABEL *lab = [self makeLabel:label];
1560 [self placeChild:lab on:parent];
1564 [self placeChild:txt on:parent right:(label ? YES : NO)];
1566 [self bindSwitch:txt cmdline:arg];
1571 /* Creates the NSTextField described by the given XML node,
1572 and hooks it up to a Choose button and a file selector widget.
1574 - (void) makeFileSelector: (NSXMLNode *)node
1575 on: (NSView *)parent
1576 dirsOnly: (BOOL) dirsOnly
1577 withLabel: (BOOL) label_p
1578 editable: (BOOL) editable_p
1580 # ifndef USE_IPHONE // No files. No selectors.
1581 NSMutableDictionary *dict =
1582 [NSMutableDictionary dictionaryWithObjectsAndKeys:
1587 [self parseAttrs:dict node:node];
1588 NSString *label = [dict objectForKey:@"_label"];
1589 NSString *arg = [dict objectForKey:@"arg"];
1591 if (!label && label_p) {
1592 NSAssert1 (0, @"no _label in %@", [node name]);
1596 NSAssert1 (arg, @"no arg in %@", label);
1599 rect.origin.x = rect.origin.y = 0;
1600 rect.size.width = rect.size.height = 10;
1602 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1604 // make the default size be around 20 columns.
1606 [txt setStringValue:@"123456789 123456789 "];
1608 [txt setSelectable:YES];
1609 [txt setEditable:editable_p];
1610 [txt setBezeled:editable_p];
1611 [txt setDrawsBackground:editable_p];
1612 [[txt cell] setWraps:NO];
1613 [[txt cell] setScrollable:YES];
1614 [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
1615 [txt setStringValue:@""];
1619 lab = [self makeLabel:label];
1620 [self placeChild:lab on:parent];
1624 [self placeChild:txt on:parent right:(label ? YES : NO)];
1626 [self bindSwitch:txt cmdline:arg];
1629 // Make the text field and label be the same height, whichever is taller.
1632 rect.size.height = ([lab frame].size.height > [txt frame].size.height
1633 ? [lab frame].size.height
1634 : [txt frame].size.height);
1635 [txt setFrame:rect];
1638 // Now put a "Choose" button next to it.
1640 rect.origin.x = rect.origin.y = 0;
1641 rect.size.width = rect.size.height = 10;
1642 NSButton *choose = [[NSButton alloc] initWithFrame:rect];
1643 [choose setTitle:@"Choose..."];
1644 [choose setBezelStyle:NSRoundedBezelStyle];
1647 [self placeChild:choose on:parent right:YES];
1649 // center the Choose button around the midpoint of the text field.
1650 rect = [choose frame];
1651 rect.origin.y = ([txt frame].origin.y +
1652 (([txt frame].size.height - rect.size.height) / 2));
1653 [choose setFrameOrigin:rect.origin];
1655 [choose setTarget:[parent window]];
1657 [choose setAction:@selector(fileSelectorChooseDirsAction:)];
1659 [choose setAction:@selector(fileSelectorChooseAction:)];
1662 # endif // !USE_IPHONE
1668 /* Runs a modal file selector and sets the text field's value to the
1669 selected file or directory.
1672 do_file_selector (NSTextField *txt, BOOL dirs_p)
1674 NSOpenPanel *panel = [NSOpenPanel openPanel];
1675 [panel setAllowsMultipleSelection:NO];
1676 [panel setCanChooseFiles:!dirs_p];
1677 [panel setCanChooseDirectories:dirs_p];
1679 NSString *file = [txt stringValue];
1680 if ([file length] <= 0) {
1681 file = NSHomeDirectory();
1683 file = [file stringByAppendingPathComponent:@"Pictures"];
1686 // NSString *dir = [file stringByDeletingLastPathComponent];
1688 int result = [panel runModalForDirectory:file //dir
1689 file:nil //[file lastPathComponent]
1691 if (result == NSOKButton) {
1692 NSArray *files = [panel filenames];
1693 file = ([files count] > 0 ? [files objectAtIndex:0] : @"");
1694 file = [file stringByAbbreviatingWithTildeInPath];
1695 [txt setStringValue:file];
1697 // Fuck me! Just setting the value of the NSTextField does not cause
1698 // that to end up in the preferences!
1700 NSDictionary *dict = [txt infoForBinding:@"value"];
1701 NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
1702 NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
1703 if ([path hasPrefix:@"values."]) // WTF.
1704 path = [path substringFromIndex:7];
1705 [[prefs values] setValue:file forKey:path];
1708 // make sure the end of the string is visible.
1709 NSText *fe = [[txt window] fieldEditor:YES forObject:txt];
1711 range.location = [file length]-3;
1713 if (! [[txt window] makeFirstResponder:[txt window]])
1714 [[txt window] endEditingFor:nil];
1715 // [[txt window] makeFirstResponder:nil];
1716 [fe setSelectedRange:range];
1717 // [tv scrollRangeToVisible:range];
1718 // [txt setNeedsDisplay:YES];
1719 // [[txt cell] setNeedsDisplay:YES];
1720 // [txt selectAll:txt];
1726 /* Returns the NSTextField that is to the left of or above the NSButton.
1728 static NSTextField *
1729 find_text_field_of_button (NSButton *button)
1731 NSView *parent = [button superview];
1732 NSArray *kids = [parent subviews];
1733 int nkids = [kids count];
1736 for (i = 0; i < nkids; i++) {
1737 NSObject *kid = [kids objectAtIndex:i];
1738 if ([kid isKindOfClass:[NSTextField class]]) {
1739 f = (NSTextField *) kid;
1740 } else if (kid == button) {
1749 - (void) fileSelectorChooseAction:(NSObject *)arg
1751 NSButton *choose = (NSButton *) arg;
1752 NSTextField *txt = find_text_field_of_button (choose);
1753 do_file_selector (txt, NO);
1756 - (void) fileSelectorChooseDirsAction:(NSObject *)arg
1758 NSButton *choose = (NSButton *) arg;
1759 NSTextField *txt = find_text_field_of_button (choose);
1760 do_file_selector (txt, YES);
1763 #endif // !USE_IPHONE
1766 - (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
1771 (x) Computer name and time
1772 ( ) Text [__________________________]
1773 ( ) Text file [_________________] [Choose]
1774 ( ) URL [__________________________]
1775 ( ) Shell Cmd [__________________________]
1777 textMode -text-mode date
1778 textMode -text-mode literal textLiteral -text-literal %
1779 textMode -text-mode file textFile -text-file %
1780 textMode -text-mode url textURL -text-url %
1781 textMode -text-mode program textProgram -text-program %
1784 rect.size.width = rect.size.height = 1;
1785 rect.origin.x = rect.origin.y = 0;
1786 NSView *group = [[NSView alloc] initWithFrame:rect];
1787 NSView *rgroup = [[NSView alloc] initWithFrame:rect];
1789 Bool program_p = TRUE;
1794 // This is how you link radio buttons together.
1796 NSButtonCell *proto = [[NSButtonCell alloc] init];
1797 [proto setButtonType:NSRadioButton];
1799 rect.origin.x = rect.origin.y = 0;
1800 rect.size.width = rect.size.height = 10;
1801 NSMatrix *matrix = [[NSMatrix alloc]
1803 mode:NSRadioModeMatrix
1805 numberOfRows: 4 + (program_p ? 1 : 0)
1807 [matrix setAllowsEmptySelection:NO];
1809 NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil];
1810 [cnames addObject:@"Computer name and time"];
1811 [cnames addObject:@"Text"];
1812 [cnames addObject:@"File"];
1813 [cnames addObject:@"URL"];
1814 if (program_p) [cnames addObject:@"Shell Cmd"];
1815 [matrix bind:@"content"
1817 withKeyPath:@"arrangedObjects"
1821 [self bindSwitch:matrix cmdline:@"-text-mode %"];
1823 [self placeChild:matrix on:group];
1824 [self placeChild:rgroup on:group right:YES];
1828 # else // USE_IPHONE
1830 NSView *rgroup = parent;
1833 // <select id="textMode">
1834 // <option id="date" _label="Display date" arg-set="-text-mode date"/>
1835 // <option id="text" _label="Display text" arg-set="-text-mode literal"/>
1836 // <option id="url" _label="Display URL"/>
1839 node2 = [[NSXMLElement alloc] initWithName:@"select"];
1840 [node2 setAttributesAsDictionary:
1841 [NSDictionary dictionaryWithObjectsAndKeys:
1845 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
1846 [node3 setAttributesAsDictionary:
1847 [NSDictionary dictionaryWithObjectsAndKeys:
1849 @"-text-mode date", @"arg-set",
1850 @"Display the date and time", @"_label",
1852 [node3 setParent: node2];
1855 node3 = [[NSXMLElement alloc] initWithName:@"option"];
1856 [node3 setAttributesAsDictionary:
1857 [NSDictionary dictionaryWithObjectsAndKeys:
1859 @"-text-mode literal", @"arg-set",
1860 @"Display static text", @"_label",
1862 [node3 setParent: node2];
1865 node3 = [[NSXMLElement alloc] initWithName:@"option"];
1866 [node3 setAttributesAsDictionary:
1867 [NSDictionary dictionaryWithObjectsAndKeys:
1869 @"Display the contents of a URL", @"_label",
1871 [node3 setParent: node2];
1874 [self makeOptionMenu:node2 on:rgroup];
1876 # endif // USE_IPHONE
1879 // <string id="textLiteral" _label="" arg-set="-text-literal %"/>
1880 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1881 [node2 setAttributesAsDictionary:
1882 [NSDictionary dictionaryWithObjectsAndKeys:
1883 @"textLiteral", @"id",
1884 @"-text-literal %", @"arg",
1886 @"Text to display", @"_label",
1889 [self makeTextField:node2 on:rgroup
1897 // rect = [last_child(rgroup) frame];
1899 /* // trying to make the text fields be enabled only when the checkbox is on..
1900 control = last_child (rgroup);
1901 [control bind:@"enabled"
1902 toObject:[matrix cellAtRow:1 column:0]
1903 withKeyPath:@"value"
1909 // <file id="textFile" _label="" arg-set="-text-file %"/>
1910 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1911 [node2 setAttributesAsDictionary:
1912 [NSDictionary dictionaryWithObjectsAndKeys:
1914 @"-text-file %", @"arg",
1916 [self makeFileSelector:node2 on:rgroup
1917 dirsOnly:NO withLabel:NO editable:NO];
1918 # endif // !USE_IPHONE
1920 // rect = [last_child(rgroup) frame];
1922 // <string id="textURL" _label="" arg-set="text-url %"/>
1923 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1924 [node2 setAttributesAsDictionary:
1925 [NSDictionary dictionaryWithObjectsAndKeys:
1927 @"-text-url %", @"arg",
1929 @"URL to display", @"_label",
1932 [self makeTextField:node2 on:rgroup
1940 // rect = [last_child(rgroup) frame];
1944 // <string id="textProgram" _label="" arg-set="text-program %"/>
1945 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1946 [node2 setAttributesAsDictionary:
1947 [NSDictionary dictionaryWithObjectsAndKeys:
1948 @"textProgram", @"id",
1949 @"-text-program %", @"arg",
1951 [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO];
1954 // rect = [last_child(rgroup) frame];
1956 layout_group (rgroup, NO);
1958 rect = [rgroup frame];
1959 rect.size.width += 35; // WTF? Why is rgroup too narrow?
1960 [rgroup setFrame:rect];
1963 // Set the height of the cells in the radio-box matrix to the height of
1964 // the (last of the) text fields.
1965 control = last_child (rgroup);
1966 rect = [control frame];
1967 rect.size.width = 30; // width of the string "Text", plus a bit...
1969 rect.size.width += 25;
1970 rect.size.height += LINE_SPACING;
1971 [matrix setCellSize:rect.size];
1972 [matrix sizeToCells];
1974 layout_group (group, YES);
1975 rect = [matrix frame];
1976 rect.origin.x += rect.size.width + COLUMN_SPACING;
1977 rect.origin.y -= [control frame].size.height - LINE_SPACING;
1978 [rgroup setFrameOrigin:rect.origin];
1980 // now cheat on the size of the matrix: allow it to overlap (underlap)
1983 rect.size = [matrix cellSize];
1984 rect.size.width = 300;
1985 [matrix setCellSize:rect.size];
1986 [matrix sizeToCells];
1988 // Cheat on the position of the stuff on the right (the rgroup).
1989 // GAAAH, this code is such crap!
1990 rect = [rgroup frame];
1992 [rgroup setFrame:rect];
1995 rect.size.width = rect.size.height = 0;
1996 NSBox *box = [[NSBox alloc] initWithFrame:rect];
1997 [box setTitlePosition:NSAtTop];
1998 [box setBorderType:NSBezelBorder];
1999 [box setTitle:@"Display Text"];
2001 rect.size.width = rect.size.height = 12;
2002 [box setContentViewMargins:rect.size];
2003 [box setContentView:group];
2006 [self placeChild:box on:parent];
2008 # endif // !USE_IPHONE
2012 - (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2016 [x] Grab desktop images
2017 [ ] Choose random image:
2018 [__________________________] [Choose]
2020 <boolean id="grabDesktopImages" _label="Grab desktop images"
2021 arg-unset="-no-grab-desktop"/>
2022 <boolean id="chooseRandomImages" _label="Grab desktop images"
2023 arg-unset="-choose-random-images"/>
2024 <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
2027 NSXMLElement *node2;
2029 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2030 [node2 setAttributesAsDictionary:
2031 [NSDictionary dictionaryWithObjectsAndKeys:
2032 @"grabDesktopImages", @"id",
2033 @"Grab desktop images", @"_label",
2034 @"-no-grab-desktop", @"arg-unset",
2036 [self makeCheckbox:node2 on:parent];
2038 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2039 [node2 setAttributesAsDictionary:
2040 [NSDictionary dictionaryWithObjectsAndKeys:
2041 @"chooseRandomImages", @"id",
2042 @"Choose random images", @"_label",
2043 @"-choose-random-images", @"arg-set",
2045 [self makeCheckbox:node2 on:parent];
2047 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2048 [node2 setAttributesAsDictionary:
2049 [NSDictionary dictionaryWithObjectsAndKeys:
2050 @"imageDirectory", @"id",
2051 @"Images from:", @"_label",
2052 @"-image-directory %", @"arg",
2054 [self makeFileSelector:node2 on:parent
2055 dirsOnly:YES withLabel:YES editable:YES];
2057 // Add a second, explanatory label below the file/URL selector.
2060 lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"];
2061 [self placeChild:lab2 on:parent];
2063 // Pack it in a little tighter vertically.
2064 NSRect r2 = [lab2 frame];
2067 [lab2 setFrameOrigin:r2.origin];
2069 # endif // !USE_IPHONE
2073 #pragma mark Layout for controls
2078 last_child (NSView *parent)
2080 NSArray *kids = [parent subviews];
2081 int nkids = [kids count];
2085 return [kids objectAtIndex:nkids-1];
2087 #endif // USE_IPHONE
2090 /* Add the child as a subview of the parent, positioning it immediately
2091 below or to the right of the previously-added child of that view.
2093 - (void) placeChild:
2099 on:(NSView *)parent right:(BOOL)right_p
2102 NSRect rect = [child frame];
2103 NSView *last = last_child (parent);
2105 rect.origin.x = LEFT_MARGIN;
2106 rect.origin.y = ([parent frame].size.height - rect.size.height
2108 } else if (right_p) {
2109 rect = [last frame];
2110 rect.origin.x += rect.size.width + COLUMN_SPACING;
2112 rect = [last frame];
2113 rect.origin.x = LEFT_MARGIN;
2114 rect.origin.y -= [child frame].size.height + LINE_SPACING;
2116 NSRect r = [child frame];
2117 r.origin = rect.origin;
2119 [parent addSubview:child];
2121 # else // USE_IPHONE
2123 // Controls is an array of arrays of the controls, divided into sections.
2125 controls = [[NSMutableArray arrayWithCapacity:10] retain];
2126 if ([controls count] == 0)
2127 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2128 NSMutableArray *current = [controls objectAtIndex:[controls count]-1];
2130 if (!right_p || [current count] == 0) {
2131 // Nothing on the current line. Add this object.
2132 [current addObject: child];
2134 // Something's on the current line already.
2135 NSObject *old = [current objectAtIndex:[current count]-1];
2136 if ([old isKindOfClass:[NSMutableArray class]]) {
2137 // Already an array in this cell. Append.
2138 NSAssert ([(NSArray *) old count] < 4, @"internal error");
2139 [(NSMutableArray *) old addObject: child];
2141 // Replace the control in this cell with an array, then app
2142 NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil];
2143 [current replaceObjectAtIndex:[current count]-1 withObject:a];
2146 # endif // USE_IPHONE
2150 - (void) placeChild:(NSView *)child on:(NSView *)parent
2152 [self placeChild:child on:parent right:NO];
2158 // Start putting subsequent children in a new group, to create a new
2159 // section on the UITableView.
2161 - (void) placeSeparator
2163 if (! controls) return;
2164 if ([controls count] == 0) return;
2165 if ([[controls objectAtIndex:[controls count]-1]
2167 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2169 #endif // USE_IPHONE
2173 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
2174 wrapped in <hgroup> or <vgroup> in the XML.
2176 - (void) makeGroup:(NSXMLNode *)node
2178 horizontal:(BOOL) horiz_p
2181 if (!horiz_p) [self placeSeparator];
2182 [self traverseChildren:node on:parent];
2183 if (!horiz_p) [self placeSeparator];
2184 # else // !USE_IPHONE
2186 rect.size.width = rect.size.height = 1;
2187 rect.origin.x = rect.origin.y = 0;
2188 NSView *group = [[NSView alloc] initWithFrame:rect];
2189 [self traverseChildren:node on:group];
2191 layout_group (group, horiz_p);
2193 rect.size.width = rect.size.height = 0;
2194 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2195 [box setTitlePosition:NSNoTitle];
2196 [box setBorderType:NSNoBorder];
2197 [box setContentViewMargins:rect.size];
2198 [box setContentView:group];
2201 [self placeChild:box on:parent];
2202 # endif // !USE_IPHONE
2208 layout_group (NSView *group, BOOL horiz_p)
2210 NSArray *kids = [group subviews];
2211 int nkids = [kids count];
2213 double maxx = 0, miny = 0;
2214 for (i = 0; i < nkids; i++) {
2215 NSView *kid = [kids objectAtIndex:i];
2216 NSRect r = [kid frame];
2219 maxx += r.size.width + COLUMN_SPACING;
2220 if (r.size.height > -miny) miny = -r.size.height;
2222 if (r.size.width > maxx) maxx = r.size.width;
2223 miny = r.origin.y - r.size.height;
2230 rect.size.width = maxx;
2231 rect.size.height = -miny;
2232 [group setFrame:rect];
2235 for (i = 0; i < nkids; i++) {
2236 NSView *kid = [kids objectAtIndex:i];
2237 NSRect r = [kid frame];
2239 r.origin.y = rect.size.height - r.size.height;
2241 x += r.size.width + COLUMN_SPACING;
2248 #endif // !USE_IPHONE
2251 /* Create some kind of control corresponding to the given XML node.
2253 -(void)makeControl:(NSXMLNode *)node on:(NSView *)parent
2255 NSString *name = [node name];
2257 if ([node kind] == NSXMLCommentKind)
2260 if ([node kind] == NSXMLTextKind) {
2261 NSString *s = [(NSString *) [node objectValue]
2262 stringByTrimmingCharactersInSet:
2263 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
2264 if (! [s isEqualToString:@""]) {
2265 NSAssert1 (0, @"unexpected text: %@", s);
2270 if ([node kind] != NSXMLElementKind) {
2271 NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
2275 if ([name isEqualToString:@"hgroup"] ||
2276 [name isEqualToString:@"vgroup"]) {
2278 [self makeGroup:node on:parent
2279 horizontal:[name isEqualToString:@"hgroup"]];
2281 } else if ([name isEqualToString:@"command"]) {
2282 // do nothing: this is the "-root" business
2284 } else if ([name isEqualToString:@"boolean"]) {
2285 [self makeCheckbox:node on:parent];
2287 } else if ([name isEqualToString:@"string"]) {
2288 [self makeTextField:node on:parent withLabel:NO horizontal:NO];
2290 } else if ([name isEqualToString:@"file"]) {
2291 [self makeFileSelector:node on:parent
2292 dirsOnly:NO withLabel:YES editable:NO];
2294 } else if ([name isEqualToString:@"number"]) {
2295 [self makeNumberSelector:node on:parent];
2297 } else if ([name isEqualToString:@"select"]) {
2298 [self makeOptionMenu:node on:parent];
2300 } else if ([name isEqualToString:@"_description"]) {
2301 [self makeDescLabel:node on:parent];
2303 } else if ([name isEqualToString:@"xscreensaver-text"]) {
2304 [self makeTextLoaderControlBox:node on:parent];
2306 } else if ([name isEqualToString:@"xscreensaver-image"]) {
2307 [self makeImageLoaderControlBox:node on:parent];
2310 NSAssert1 (0, @"unknown tag: %@", name);
2315 /* Iterate over and process the children of this XML node.
2317 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent
2319 NSArray *children = [node children];
2320 int i, count = [children count];
2321 for (i = 0; i < count; i++) {
2322 NSXMLNode *child = [children objectAtIndex:i];
2323 [self makeControl:child on:parent];
2330 /* Kludgey magic to make the window enclose the controls we created.
2333 fix_contentview_size (NSView *parent)
2336 NSArray *kids = [parent subviews];
2337 int nkids = [kids count];
2338 NSView *text = 0; // the NSText at the bottom of the window
2339 double maxx = 0, miny = 0;
2342 /* Find the size of the rectangle taken up by each of the children
2343 except the final "NSText" child.
2345 for (i = 0; i < nkids; i++) {
2346 NSView *kid = [kids objectAtIndex:i];
2347 if ([kid isKindOfClass:[NSText class]]) {
2352 if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width;
2353 if (f.origin.y - f.size.height < miny) miny = f.origin.y;
2354 // NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2355 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2356 // f.origin.y + f.size.height, [kid class]);
2359 if (maxx < 400) maxx = 400; // leave room for the NSText paragraph...
2361 /* Now that we know the width of the window, set the width of the NSText to
2362 that, so that it can decide what its height needs to be.
2364 if (! text) abort();
2366 // NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2367 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2368 // f.origin.y + f.size.height, [text class]);
2370 // set the NSText's width (this changes its height).
2371 f.size.width = maxx - LEFT_MARGIN;
2374 // position the NSText below the last child (this gives us a new miny).
2376 f.origin.y = miny - f.size.height - LINE_SPACING;
2377 miny = f.origin.y - LINE_SPACING;
2380 // Lock the width of the field and unlock the height, and let it resize
2381 // once more, to compute the proper height of the text for that width.
2383 [(NSText *) text setHorizontallyResizable:NO];
2384 [(NSText *) text setVerticallyResizable:YES];
2385 [(NSText *) text sizeToFit];
2387 // Now lock the height too: no more resizing this text field.
2389 [(NSText *) text setVerticallyResizable:NO];
2391 // Now reposition the top edge of the text field to be back where it
2392 // was before we changed the height.
2394 float oh = f.size.height;
2396 float dh = f.size.height - oh;
2399 // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF??
2400 // If we do this in 10.6, the text field moves down, off the window.
2401 // So instead we repair it at the end, at the "WTF2" comment.
2404 // Also adjust the parent height by the change in height of the text field.
2407 // NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2408 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2409 // f.origin.y + f.size.height, [text class]);
2412 /* Set the contentView to the size of the children.
2415 // float yoff = f.size.height;
2416 f.size.width = maxx + LEFT_MARGIN;
2417 f.size.height = -(miny - LEFT_MARGIN*2);
2418 // yoff = f.size.height - yoff;
2419 [parent setFrame:f];
2421 // NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f",
2422 // f.size.width, f.size.height, f.origin.x, f.origin.y);
2424 /* Now move all of the kids up into the window.
2427 float shift = f.size.height;
2428 // NSLog(@"shift: %3.0f", shift);
2429 for (i = 0; i < nkids; i++) {
2430 NSView *kid = [kids objectAtIndex:i];
2432 f.origin.y += shift;
2434 // NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2435 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2436 // f.origin.y + f.size.height, [kid class]);
2441 parent: 420 x 541 @ 0 0
2442 text: 380 x 100 @ 20 22 miny=-501
2445 parent: 420 x 541 @ 0 0
2446 text: 380 x 100 @ 20 50 miny=-501
2449 // #### WTF2: See "WTF" above. If the text field is off the screen,
2450 // move it up. We need this on 10.6 but not on 10.5. Auugh.
2453 if (f.origin.y < 50) { // magic numbers, yay
2458 /* Set the kids to track the top left corner of the window when resized.
2459 Set the NSText to track the bottom right corner as well.
2461 for (i = 0; i < nkids; i++) {
2462 NSView *kid = [kids objectAtIndex:i];
2463 unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
2464 if ([kid isKindOfClass:[NSText class]])
2465 mask |= NSViewWidthSizable|NSViewHeightSizable;
2466 [kid setAutoresizingMask:mask];
2469 # endif // !USE_IPHONE
2475 wrap_with_buttons (NSWindow *window, NSView *panel)
2479 // Make a box to hold the buttons at the bottom of the window.
2481 rect = [panel frame];
2482 rect.origin.x = rect.origin.y = 0;
2483 rect.size.height = 10;
2484 NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
2485 [bbox setTitlePosition:NSNoTitle];
2486 [bbox setBorderType:NSNoBorder];
2488 // Make some buttons: Default, Cancel, OK
2490 rect.origin.x = rect.origin.y = 0;
2491 rect.size.width = rect.size.height = 10;
2492 NSButton *reset = [[NSButton alloc] initWithFrame:rect];
2493 [reset setTitle:@"Reset to Defaults"];
2494 [reset setBezelStyle:NSRoundedBezelStyle];
2497 rect = [reset frame];
2498 NSButton *ok = [[NSButton alloc] initWithFrame:rect];
2499 [ok setTitle:@"OK"];
2500 [ok setBezelStyle:NSRoundedBezelStyle];
2502 rect = [bbox frame];
2503 rect.origin.x = rect.size.width - [ok frame].size.width;
2504 [ok setFrameOrigin:rect.origin];
2507 NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
2508 [cancel setTitle:@"Cancel"];
2509 [cancel setBezelStyle:NSRoundedBezelStyle];
2511 rect.origin.x -= [cancel frame].size.width + 10;
2512 [cancel setFrameOrigin:rect.origin];
2514 // Bind OK to RET and Cancel to ESC.
2515 [ok setKeyEquivalent:@"\r"];
2516 [cancel setKeyEquivalent:@"\e"];
2518 // The correct width for OK and Cancel buttons is 68 pixels
2519 // ("Human Interface Guidelines: Controls: Buttons:
2520 // Push Button Specifications").
2523 rect.size.width = 68;
2526 rect = [cancel frame];
2527 rect.size.width = 68;
2528 [cancel setFrame:rect];
2530 // It puts the buttons in the box or else it gets the hose again
2532 [bbox addSubview:ok];
2533 [bbox addSubview:cancel];
2534 [bbox addSubview:reset];
2537 // make a box to hold the button-box, and the preferences view
2539 rect = [bbox frame];
2540 rect.origin.y += rect.size.height;
2541 NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
2542 [pbox setTitlePosition:NSNoTitle];
2543 [pbox setBorderType:NSBezelBorder];
2545 // Enforce a max height on the dialog, so that it's obvious to me
2546 // (on a big screen) when the dialog will fall off the bottom of
2547 // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
2549 NSRect f = [panel frame];
2550 int screen_height = (768 // shortest "modern" Mac display
2552 - 56 // System Preferences toolbar
2553 - 140 // default magnified bottom dock icon
2555 if (f.size.height > screen_height) {
2556 NSLog(@"%@ height was %.0f; clipping to %d",
2557 [panel class], f.size.height, screen_height);
2558 f.size.height = screen_height;
2563 [pbox addSubview:panel];
2564 [pbox addSubview:bbox];
2567 [reset setAutoresizingMask:NSViewMaxXMargin];
2568 [cancel setAutoresizingMask:NSViewMinXMargin];
2569 [ok setAutoresizingMask:NSViewMinXMargin];
2570 [bbox setAutoresizingMask:NSViewWidthSizable];
2574 [ok setTarget:window];
2575 [cancel setTarget:window];
2576 [reset setTarget:window];
2577 [ok setAction:@selector(okAction:)];
2578 [cancel setAction:@selector(cancelAction:)];
2579 [reset setAction:@selector(resetAction:)];
2583 #endif // !USE_IPHONE
2586 /* Iterate over and process the children of the root node of the XML document.
2588 - (void)traverseTree
2591 NSView *parent = [self view];
2593 NSWindow *parent = self;
2595 NSXMLNode *node = xml_root;
2597 if (![[node name] isEqualToString:@"screensaver"]) {
2598 NSAssert (0, @"top level node is not <xscreensaver>");
2601 saver_name = [self parseXScreenSaverTag: node];
2602 [saver_name retain];
2607 rect.origin.x = rect.origin.y = 0;
2608 rect.size.width = rect.size.height = 1;
2610 NSView *panel = [[NSView alloc] initWithFrame:rect];
2611 [self traverseChildren:node on:panel];
2612 fix_contentview_size (panel);
2614 NSView *root = wrap_with_buttons (parent, panel);
2615 [userDefaultsController setAppliesImmediately:NO];
2617 [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
2619 rect = [parent frameRectForContentRect:[root frame]];
2620 [parent setFrame:rect display:NO];
2621 [parent setMinSize:rect.size];
2623 [parent setContentView:root];
2625 # else // USE_IPHONE
2627 CGRect r = [parent frame];
2628 r.size = [[UIScreen mainScreen] bounds].size;
2629 [parent setFrame:r];
2630 [self traverseChildren:node on:parent];
2632 # endif // USE_IPHONE
2636 - (void)parser:(NSXMLParser *)parser
2637 didStartElement:(NSString *)elt
2638 namespaceURI:(NSString *)ns
2639 qualifiedName:(NSString *)qn
2640 attributes:(NSDictionary *)attrs
2642 NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt];
2643 [e setKind:SimpleXMLElementKind];
2644 [e setAttributesAsDictionary:attrs];
2645 NSXMLElement *p = xml_parsing;
2649 xml_root = xml_parsing;
2652 - (void)parser:(NSXMLParser *)parser
2653 didEndElement:(NSString *)elt
2654 namespaceURI:(NSString *)ns
2655 qualifiedName:(NSString *)qn
2657 NSXMLElement *p = xml_parsing;
2659 NSLog(@"extra close: %@", elt);
2660 } else if (![[p name] isEqualToString:elt]) {
2661 NSLog(@"%@ closed by %@", [p name], elt);
2663 NSXMLElement *n = xml_parsing;
2664 xml_parsing = [n parent];
2669 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
2671 NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"];
2672 [e setKind:SimpleXMLTextKind];
2673 NSXMLElement *p = xml_parsing;
2675 [e setObjectValue: string];
2680 # ifdef USE_PICKER_VIEW
2682 #pragma mark UIPickerView delegate methods
2684 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv
2686 return 1; // Columns
2689 - (NSInteger)pickerView:(UIPickerView *)pv
2690 numberOfRowsInComponent:(NSInteger)column
2692 NSAssert (column == 0, @"weird column");
2693 NSArray *a = [picker_values objectAtIndex: [pv tag]];
2694 if (! a) return 0; // Too early?
2698 - (CGFloat)pickerView:(UIPickerView *)pv
2699 rowHeightForComponent:(NSInteger)column
2701 return [NSFont systemFontSize] * 1.5; // #### WHAT
2704 - (CGFloat)pickerView:(UIPickerView *)pv
2705 widthForComponent:(NSInteger)column
2707 NSAssert (column == 0, @"weird column");
2708 NSArray *a = [picker_values objectAtIndex: [pv tag]];
2709 if (! a) return 0; // Too early?
2711 UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]];
2713 for (NSArray *a2 in a) {
2714 NSString *s = [a2 objectAtIndex:0];
2715 CGSize r = [s sizeWithFont:f];
2716 if (r.width > max) max = r.width;
2719 max *= 1.7; // WTF!!
2731 - (NSString *)pickerView:(UIPickerView *)pv
2732 titleForRow:(NSInteger)row
2733 forComponent:(NSInteger)column
2735 NSAssert (column == 0, @"weird column");
2736 NSArray *a = [picker_values objectAtIndex: [pv tag]];
2737 if (! a) return 0; // Too early?
2738 a = [a objectAtIndex:row];
2739 NSAssert (a, @"internal error");
2740 return [a objectAtIndex:0];
2743 # endif // USE_PICKER_VIEW
2746 #pragma mark UITableView delegate methods
2748 - (void) addResetButton
2750 [[self navigationItem]
2751 setRightBarButtonItem: [[UIBarButtonItem alloc]
2752 initWithTitle: @"Reset to Defaults"
2753 style: UIBarButtonItemStyleBordered
2755 action:@selector(resetAction:)]];
2759 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
2764 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
2765 // Number of vertically-stacked white boxes.
2766 return [controls count];
2769 - (NSInteger)tableView:(UITableView *)tableView
2770 numberOfRowsInSection:(NSInteger)section
2772 // Number of lines in each vertically-stacked white box.
2773 NSAssert (controls, @"internal error");
2774 return [[controls objectAtIndex:section] count];
2777 - (NSString *)tableView:(UITableView *)tv
2778 titleForHeaderInSection:(NSInteger)section
2780 // Titles above each vertically-stacked white box.
2782 return [saver_name stringByAppendingString:@" Settings"];
2787 - (CGFloat)tableView:(UITableView *)tv
2788 heightForRowAtIndexPath:(NSIndexPath *)ip
2790 CGFloat h = [tv rowHeight];
2792 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
2793 objectAtIndex:[ip indexAtPosition:1]];
2795 if ([ctl isKindOfClass:[NSArray class]]) {
2796 NSArray *set = (NSArray *) ctl;
2797 switch ([set count]) {
2799 # ifdef LABEL_ABOVE_SLIDER
2800 h *= 1.7; break; // label + left/slider/right: 2 1/2 lines
2802 case 3: h *= 1.2; break; // left/slider/right: 1 1/2 lines
2804 if ([[set objectAtIndex:1] isKindOfClass:[UITextField class]])
2808 } else if ([ctl isKindOfClass:[UILabel class]]) {
2809 UILabel *t = (UILabel *) ctl;
2811 r.size.width = 250; // WTF! Black magic!
2812 r.size.width -= LEFT_MARGIN;
2816 h = r.size.height + LINE_SPACING * 3;
2819 CGFloat h2 = [ctl frame].size.height;
2820 h2 += LINE_SPACING * 2;
2828 - (void)refreshTableView
2830 UITableView *tv = (UITableView *) [self view];
2831 NSMutableArray *a = [NSMutableArray arrayWithCapacity:20];
2832 int rows = [self numberOfSectionsInTableView:tv];
2833 for (int i = 0; i < rows; i++) {
2834 int cols = [self tableView:tv numberOfRowsInSection:i];
2835 for (int j = 0; j < cols; j++) {
2839 [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]];
2844 [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone];
2849 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o
2851 [NSTimer scheduledTimerWithTimeInterval: 0
2853 selector:@selector(refreshTableView)
2859 #ifndef USE_PICKER_VIEW
2861 - (void)updateRadioGroupCell:(UITableViewCell *)cell
2862 button:(RadioButton *)b
2864 NSArray *item = [[b items] objectAtIndex: [b index]];
2865 NSString *pref_key = [item objectAtIndex:1];
2866 NSObject *pref_val = [item objectAtIndex:2];
2867 NSObject *current = [userDefaultsController objectForKey:pref_key];
2869 // Convert them both to strings and compare those, so that
2870 // we don't get screwed by int 1 versus string "1".
2871 // Will boolean true/1 screw us here too?
2873 NSString *pref_str = ([pref_val isKindOfClass:[NSString class]]
2874 ? (NSString *) pref_val
2875 : [(NSNumber *) pref_val stringValue]);
2876 NSString *current_str = ([current isKindOfClass:[NSString class]]
2877 ? (NSString *) current
2878 : [(NSNumber *) current stringValue]);
2879 BOOL match_p = [current_str isEqualToString:pref_str];
2881 // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str);
2884 [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
2886 [cell setAccessoryType:UITableViewCellAccessoryNone];
2890 - (void)tableView:(UITableView *)tv
2891 didSelectRowAtIndexPath:(NSIndexPath *)ip
2893 RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
2894 objectAtIndex:[ip indexAtPosition:1]];
2895 if (! [ctl isKindOfClass:[RadioButton class]])
2898 [self radioAction:ctl];
2899 [self refreshTableView];
2903 #endif // !USE_PICKER_VIEW
2907 - (UITableViewCell *)tableView:(UITableView *)tv
2908 cellForRowAtIndexPath:(NSIndexPath *)ip
2911 /* #### If we re-use cells, then clicking on a checkbox RadioButton
2912 (in non-USE_PICKER_VIEW mode) makes all the cells disappear.
2913 This doesn't happen if we don't re-use any cells. Oh well.
2915 NSString *id = [NSString stringWithFormat: @"%d:%d",
2916 [ip indexAtPosition:0],
2917 [ip indexAtPosition:1]];
2918 UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier: id];
2920 if (cell) return cell;
2923 UITableViewCell *cell;
2926 cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
2927 reuseIdentifier: id]
2929 cell.selectionStyle = UITableViewCellSelectionStyleNone;
2931 CGRect p = [cell frame];
2934 p.size.height = [self tableView:tv heightForRowAtIndexPath:ip];
2937 // Allocate more space to the controls on iPad screens,
2938 // and on landscape-mode iPhones.
2939 CGFloat ww = [tv frame].size.width;
2940 CGFloat left_edge = (ww > 700
2941 ? p.size.width * 0.9
2943 ? p.size.width * 0.5
2944 : p.size.width * 0.3);
2945 CGFloat right_edge = p.origin.x + p.size.width - LEFT_MARGIN;
2948 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
2949 objectAtIndex:[ip indexAtPosition:1]];
2951 if ([ctl isKindOfClass:[NSArray class]]) {
2952 // This cell has a set of objects in it.
2953 NSArray *set = (NSArray *) ctl;
2954 switch ([set count]) {
2957 // With 2 elements, the first of the pair must be a label.
2958 ctl = [set objectAtIndex: 0];
2959 NSAssert ([ctl isKindOfClass:[UILabel class]], @"unhandled type");
2960 cell.textLabel.text = [(UILabel *) ctl text];
2961 ctl = [set objectAtIndex: 1];
2964 if ([ctl isKindOfClass:[UISwitch class]]) {
2965 // Flush right checkboxes.
2966 ctl.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
2967 r.size.width = 80; // Magic.
2968 r.origin.x = right_edge - r.size.width;
2970 // Expandable sliders.
2971 ctl.autoresizingMask = UIViewAutoresizingFlexibleWidth;
2972 r.origin.x = left_edge;
2973 r.size.width = right_edge - r.origin.x;
2975 r.origin.y = (p.size.height - r.size.height) / 2;
2982 // With 3 elements, the first and last must be labels.
2983 // With 4 elements, the first, second and last must be labels.
2985 UILabel *top = ([set count] == 4
2986 ? [set objectAtIndex: i++]
2988 UILabel *left = [set objectAtIndex: i++];
2989 NSView *mid = [set objectAtIndex: i++];
2990 UILabel *right = [set objectAtIndex: i++];
2991 NSAssert (!top || [top isKindOfClass:[UILabel class]], @"WTF");
2992 NSAssert ( [left isKindOfClass:[UILabel class]], @"WTF");
2993 NSAssert ( ![mid isKindOfClass:[UILabel class]], @"WTF");
2994 NSAssert ( [right isKindOfClass:[UILabel class]], @"WTF");
2996 // 3 elements: control at top of cell.
2997 // 4 elements: center the control vertically.
2999 # ifdef LABEL_ABOVE_SLIDER
3000 left_edge = LEFT_MARGIN;
3002 r.origin.x = left_edge;
3003 r.size.width = right_edge - r.origin.x;
3004 r.origin.y = ([set count] == 3
3006 : (p.size.height - r.size.height) / 2);
3009 // Top label goes above, flush center/top.
3011 // [top setFont:[[cell textLabel] font]]; // 0 point?
3012 r.size = [[top text] sizeWithFont:[top font]
3014 CGSizeMake (p.size.width - LEFT_MARGIN*2,
3016 lineBreakMode:[top lineBreakMode]];
3017 r.origin.x = (p.size.width - r.size.width) / 2;
3022 // Left label goes under control, flush left/bottom.
3023 r.size = [[left text] sizeWithFont:[left font]
3025 CGSizeMake(p.size.width - LEFT_MARGIN*2,
3027 lineBreakMode:[left lineBreakMode]];
3028 r.origin.x = [mid frame].origin.x;
3029 r.origin.y = p.size.height - r.size.height - 4;
3032 // Right label goes under control, flush right/bottom.
3034 r.size = [[right text] sizeWithFont:[right font]
3036 CGSizeMake(p.size.width - LEFT_MARGIN*2,
3038 lineBreakMode:[right lineBreakMode]];
3039 r.origin.x = ([mid frame].origin.x + [mid frame].size.width -
3041 r.origin.y = [left frame].origin.y;
3045 ctl = [[UIView alloc] initWithFrame:p];
3047 # ifdef LABEL_ABOVE_SLIDER
3048 [ctl addSubview: top];
3049 top.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin|
3050 UIViewAutoresizingFlexibleRightMargin);
3052 cell.textLabel.text = [top text];
3055 [ctl addSubview: left];
3056 [ctl addSubview: mid];
3057 [ctl addSubview: right];
3059 left. autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
3060 mid. autoresizingMask = UIViewAutoresizingFlexibleWidth;
3061 right.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
3062 ctl. autoresizingMask = UIViewAutoresizingFlexibleWidth;
3066 NSAssert (0, @"unhandled size");
3069 // A single view, not a pair.
3072 r.origin.x = LEFT_MARGIN;
3073 r.origin.y = LINE_SPACING;
3074 r.size.width = right_edge - r.origin.x;
3077 ctl.autoresizingMask = UIViewAutoresizingFlexibleWidth;
3079 # ifndef USE_PICKER_VIEW
3080 if ([ctl isKindOfClass:[RadioButton class]])
3081 [self updateRadioGroupCell:cell button:(RadioButton *)ctl];
3082 # endif // USE_PICKER_VIEW
3085 [cell.contentView addSubview: ctl];
3089 # endif // USE_IPHONE
3092 /* When this object is instantiated, it parses the XML file and creates
3093 controls on itself that are hooked up to the appropriate preferences.
3094 The default size of the view is just big enough to hold them all.
3096 - (id)initWithXMLFile: (NSString *) xml_file
3097 options: (const XrmOptionDescRec *) _opts
3098 controller: (NSUserDefaultsController *) _prefs
3099 defaults: (NSDictionary *) _defs
3102 self = [super init];
3103 # else // !USE_IPHONE
3104 self = [super initWithStyle:UITableViewStyleGrouped];
3105 self.title = [saver_name stringByAppendingString:@" Settings"];
3106 # endif // !USE_IPHONE
3107 if (! self) return 0;
3109 // instance variables
3111 defaultOptions = _defs;
3112 userDefaultsController = _prefs;
3113 [userDefaultsController retain];
3115 NSURL *furl = [NSURL fileURLWithPath:xml_file];
3118 NSAssert1 (0, @"can't URLify \"%@\"", xml_file);
3122 #if 0 // -- the old way
3124 NSXMLDocument *xmlDoc = [[NSXMLDocument alloc]
3125 initWithContentsOfURL:furl
3126 options:(NSXMLNodePreserveWhitespace |
3127 NSXMLNodePreserveCDATA)
3129 if (!xmlDoc || err) {
3131 NSAssert2 (0, @"XML Error: %@: %@",
3132 xml_file, [err localizedDescription]);
3136 traverse_tree (prefs, self, opts, [xmlDoc rootElement]);
3140 NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithContentsOfURL:furl];
3142 NSAssert1 (0, @"XML Error: %@", xml_file);
3145 [xmlDoc setDelegate:self];
3146 if (! [xmlDoc parse]) {
3147 NSError *err = [xmlDoc parserError];
3148 NSAssert2 (0, @"XML Error: %@: %@", xml_file, err);
3152 [self traverseTree];
3156 [self addResetButton];
3165 [saver_name release];
3166 [userDefaultsController release];
3169 [pref_keys release];
3170 [pref_ctls release];
3171 # ifdef USE_PICKER_VIEW
3172 [picker_values release];