X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?p=xscreensaver;a=blobdiff_plain;f=OSX%2FXScreenSaverConfigSheet.m;h=12980fa7cf2f6222c15a3dd8394e797ca46dda62;hp=2b0cb916bf35a9e2269ab3d6a35d7f439de11863;hb=f8cf5ac7b2f53510f80a0eaf286a25298be17bfe;hpb=ec8d2b32b63649e6d32bdfb306eda062769af823 diff --git a/OSX/XScreenSaverConfigSheet.m b/OSX/XScreenSaverConfigSheet.m index 2b0cb916..12980fa7 100644 --- a/OSX/XScreenSaverConfigSheet.m +++ b/OSX/XScreenSaverConfigSheet.m @@ -1,4 +1,4 @@ -/* xscreensaver, Copyright (c) 2006-2011 Jamie Zawinski +/* xscreensaver, Copyright (c) 2006-2012 Jamie Zawinski * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that @@ -27,32 +27,233 @@ #import "jwxyz.h" #import "InvertedSlider.h" -#import + +#ifdef USE_IPHONE +# define NSView UIView +# define NSRect CGRect +# define NSSize CGSize +# define NSTextField UITextField +# define NSButton UIButton +# define NSFont UIFont +# define NSStepper UIStepper +# define NSMenuItem UIMenuItem +# define NSText UILabel +# define minValue minimumValue +# define maxValue maximumValue +# define setMinValue setMinimumValue +# define setMaxValue setMaximumValue +# define LABEL UILabel +#else +# define LABEL NSTextField +#endif // USE_IPHONE + +#undef LABEL_ABOVE_SLIDER + + +#pragma mark XML Parser + +/* I used to use the "NSXMLDocument" XML parser, but that doesn't exist + on iOS. The "NSXMLParser" parser exists on both OSX and iOS, so I + converted to use that. However, to avoid having to re-write all of + the old code, I faked out a halfassed implementation of the + "NSXMLNode" class that "NSXMLDocument" used to return. + */ + +#define NSXMLNode SimpleXMLNode +#define NSXMLElement SimpleXMLNode +#define NSXMLCommentKind SimpleXMLCommentKind +#define NSXMLElementKind SimpleXMLElementKind +#define NSXMLAttributeKind SimpleXMLAttributeKind +#define NSXMLTextKind SimpleXMLTextKind + +typedef enum { SimpleXMLCommentKind, + SimpleXMLElementKind, + SimpleXMLAttributeKind, + SimpleXMLTextKind, +} SimpleXMLKind; + +@interface SimpleXMLNode : NSObject +{ + SimpleXMLKind kind; + NSString *name; + SimpleXMLNode *parent; + NSMutableArray *children; + NSMutableArray *attributes; + id object; +} + +@property(nonatomic) SimpleXMLKind kind; +@property(nonatomic, retain) NSString *name; +@property(nonatomic, retain) SimpleXMLNode *parent; +@property(nonatomic, retain) NSMutableArray *children; +@property(nonatomic, retain) NSMutableArray *attributes; +@property(nonatomic, retain, getter=objectValue, setter=setObjectValue:) + id object; + +@end + +@implementation SimpleXMLNode + +@synthesize kind; +@synthesize name; +//@synthesize parent; +@synthesize children; +@synthesize attributes; +@synthesize object; + +- (id) init +{ + self = [super init]; + attributes = [NSMutableArray arrayWithCapacity:10]; + return self; +} + + +- (id) initWithName:(NSString *)n +{ + self = [self init]; + [self setKind:NSXMLElementKind]; + [self setName:n]; + return self; +} + + +- (void) setAttributesAsDictionary:(NSDictionary *)dict +{ + for (NSString *key in dict) { + NSObject *val = [dict objectForKey:key]; + SimpleXMLNode *n = [[SimpleXMLNode alloc] init]; + [n setKind:SimpleXMLAttributeKind]; + [n setName:key]; + [n setObjectValue:val]; + [attributes addObject:n]; + } +} + +- (SimpleXMLNode *) parent { return parent; } + +- (void) setParent:(SimpleXMLNode *)p +{ + NSAssert (!parent, @"parent already set"); + if (!p) return; + parent = p; + NSMutableArray *kids = [p children]; + if (!kids) { + kids = [NSMutableArray arrayWithCapacity:10]; + [p setChildren:kids]; + } + [kids addObject:self]; +} +@end + + +#pragma mark Implementing radio buttons + +/* The UIPickerView is a hideous and uncustomizable piece of shit. + I can't believe Apple actually released that thing on the world. + Let's fake up some radio buttons instead. + */ + +#if defined(USE_IPHONE) && !defined(USE_PICKER_VIEW) + +@interface RadioButton : UILabel +{ + int index; + NSArray *items; +} + +@property(nonatomic) int index; +@property(nonatomic, retain) NSArray *items; + +@end + +@implementation RadioButton + +@synthesize index; +@synthesize items; + +- (id) initWithIndex:(int)_index items:_items +{ + self = [super initWithFrame:CGRectZero]; + index = _index; + items = [_items retain]; + + [self setText: [[items objectAtIndex:index] objectAtIndex:0]]; + [self setBackgroundColor:[UIColor clearColor]]; + [self sizeToFit]; + + return self; +} + +@end + + +# endif // !USE_PICKER_VIEW + + + +@interface XScreenSaverConfigSheet (Private) + +- (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent; + +# ifndef USE_IPHONE +- (void) placeChild: (NSView *)c on:(NSView *)p right:(BOOL)r; +- (void) placeChild: (NSView *)c on:(NSView *)p; +static NSView *last_child (NSView *parent); +static void layout_group (NSView *group, BOOL horiz_p); +# else // USE_IPHONE +- (void) placeChild: (NSObject *)c on:(NSView *)p right:(BOOL)r; +- (void) placeChild: (NSObject *)c on:(NSView *)p; +- (void) placeSeparator; +- (void) bindResource:(NSObject *)ctl key:(NSString *)k reload:(BOOL)r; +- (void) refreshTableView; +# endif // USE_IPHONE + +@end + @implementation XScreenSaverConfigSheet -#define LEFT_MARGIN 20 // left edge of window -#define COLUMN_SPACING 10 // gap between e.g. labels and text fields -#define LEFT_LABEL_WIDTH 70 // width of all left labels -#define LINE_SPACING 10 // leading between each line +# define LEFT_MARGIN 20 // left edge of window +# define COLUMN_SPACING 10 // gap between e.g. labels and text fields +# define LEFT_LABEL_WIDTH 70 // width of all left labels +# define LINE_SPACING 10 // leading between each line + -// redefine these since they don't work when not inside an ObjC method -#undef NSAssert -#undef NSAssert1 -#undef NSAssert2 -#undef NSAssert3 -#define NSAssert(CC,S) do { if (!(CC)) { NSLog(S); }} while(0) -#define NSAssert1(CC,S,A) do { if (!(CC)) { NSLog(S,A); }} while(0) -#define NSAssert2(CC,S,A,B) do { if (!(CC)) { NSLog(S,A,B); }} while(0) -#define NSAssert3(CC,S,A,B,C) do { if (!(CC)) { NSLog(S,A,B,C); }} while(0) +#pragma mark Talking to the resource database + + +/* Normally we read resources by looking up "KEY" in the database + "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone + app, everything is stored in the database "org.jwz.xscreensaver" + instead, so transform keys to "SAVERNAME.KEY". + + NOTE: This is duplicated in PrefsReader.m, cause I suck. +*/ +- (NSString *) makeKey:(NSString *)key +{ +# ifdef USE_IPHONE + NSString *prefix = [saver_name stringByAppendingString:@"."]; + if (! [key hasPrefix:prefix]) // Don't double up! + key = [prefix stringByAppendingString:key]; +# endif + return key; +} + + +- (NSString *) makeCKey:(const char *)key +{ + return [self makeKey:[NSString stringWithCString:key + encoding:NSUTF8StringEncoding]]; +} /* Given a command-line option, returns the corresponding resource name. Any arguments in the switch string are ignored (e.g., "-foo x"). */ -static NSString * -switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts, - NSString **val_ret) +- (NSString *) switchToResource:(NSString *)cmdline_switch + opts:(const XrmOptionDescRec *)opts_array + valRet:(NSString **)val_ret { char buf[255]; char *tail = 0; @@ -60,7 +261,7 @@ switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts, if (! [cmdline_switch getCString:buf maxLength:sizeof(buf) encoding:NSUTF8StringEncoding]) { NSAssert1(0, @"unable to convert %@", cmdline_switch); - abort(); + return 0; } char *s = strpbrk(buf, " \t\r\n"); if (s && *s) { @@ -70,15 +271,15 @@ switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts, tail++; } - while (opts[0].option) { - if (!strcmp (opts[0].option, buf)) { + while (opts_array[0].option) { + if (!strcmp (opts_array[0].option, buf)) { const char *ret = 0; - if (opts[0].argKind == XrmoptionNoArg) { + if (opts_array[0].argKind == XrmoptionNoArg) { if (tail && *tail) NSAssert1 (0, @"expected no args to switch: \"%@\"", cmdline_switch); - ret = opts[0].value; + ret = opts_array[0].value; } else { if (!tail || !*tail) NSAssert1 (0, @"expected args to switch: \"%@\"", @@ -92,40 +293,226 @@ switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts, encoding:NSUTF8StringEncoding] : 0); - const char *res = opts[0].specifier; + const char *res = opts_array[0].specifier; while (*res && (*res == '.' || *res == '*')) res++; - return [NSString stringWithCString:res - encoding:NSUTF8StringEncoding]; + return [self makeCKey:res]; } - opts++; + opts_array++; } NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch); - abort(); + return 0; +} + + +#ifdef USE_IPHONE + +// Called when a slider is bonked. +// +- (void)sliderAction:(UISlider*)sender +{ + if ([active_text_field canResignFirstResponder]) + [active_text_field resignFirstResponder]; + NSString *pref_key = [pref_keys objectAtIndex: [sender tag]]; + double v = [sender value]; + if (v == (int) v) + [userDefaultsController setInteger:v forKey:pref_key]; + else + [userDefaultsController setDouble:v forKey:pref_key]; +} + +// Called when a checkbox/switch is bonked. +// +- (void)switchAction:(UISwitch*)sender +{ + if ([active_text_field canResignFirstResponder]) + [active_text_field resignFirstResponder]; + NSString *pref_key = [pref_keys objectAtIndex: [sender tag]]; + NSString *v = ([sender isOn] ? @"true" : @"false"); + [userDefaultsController setObject:v forKey:pref_key]; +} + +# ifdef USE_PICKER_VIEW +// Called when a picker is bonked. +// +- (void)pickerView:(UIPickerView *)pv + didSelectRow:(NSInteger)row + inComponent:(NSInteger)column +{ + if ([active_text_field canResignFirstResponder]) + [active_text_field resignFirstResponder]; + + NSAssert (column == 0, @"internal error"); + NSArray *a = [picker_values objectAtIndex: [pv tag]]; + if (! a) return; // Too early? + a = [a objectAtIndex:row]; + NSAssert (a, @"missing row"); + +//NSString *label = [a objectAtIndex:0]; + NSString *pref_key = [a objectAtIndex:1]; + NSObject *pref_val = [a objectAtIndex:2]; + [userDefaultsController setObject:pref_val forKey:pref_key]; +} +# else // !USE_PICKER_VIEW + +// Called when a RadioButton is bonked. +// +- (void)radioAction:(RadioButton*)sender +{ + if ([active_text_field canResignFirstResponder]) + [active_text_field resignFirstResponder]; + + NSArray *item = [[sender items] objectAtIndex: [sender index]]; + NSString *pref_key = [item objectAtIndex:1]; + NSObject *pref_val = [item objectAtIndex:2]; + [userDefaultsController setObject:pref_val forKey:pref_key]; +} + +- (BOOL)textFieldShouldBeginEditing:(UITextField *)tf +{ + active_text_field = tf; + return YES; +} + +- (void)textFieldDidEndEditing:(UITextField *)tf +{ + NSString *pref_key = [pref_keys objectAtIndex: [tf tag]]; + NSString *txt = [tf text]; + [userDefaultsController setObject:txt forKey:pref_key]; +} + +- (BOOL)textFieldShouldReturn:(UITextField *)tf +{ + active_text_field = nil; + [tf resignFirstResponder]; + return YES; +} + +# endif // !USE_PICKER_VIEW + +#endif // USE_IPHONE + + +# ifndef USE_IPHONE + +- (void) okAction:(NSObject *)arg +{ + [userDefaultsController commitEditing]; + [userDefaultsController save:self]; + [NSApp endSheet:self returnCode:NSOKButton]; + [self close]; +} + +- (void) cancelAction:(NSObject *)arg +{ + [userDefaultsController revert:self]; + [NSApp endSheet:self returnCode:NSCancelButton]; + [self close]; +} +# endif // !USE_IPHONE + + +- (void) resetAction:(NSObject *)arg +{ +# ifndef USE_IPHONE + [userDefaultsController revertToInitialValues:self]; +# else // USE_IPHONE + + for (NSString *key in defaultOptions) { + NSObject *val = [defaultOptions objectForKey:key]; + [userDefaultsController setObject:val forKey:key]; + } + + for (UIControl *ctl in pref_ctls) { + NSString *pref_key = [pref_keys objectAtIndex: ctl.tag]; + [self bindResource:ctl key:pref_key reload:YES]; + } + + [self refreshTableView]; +# endif // USE_IPHONE } /* Connects a control (checkbox, etc) to the corresponding preferences key. */ -static void -bind_resource_to_preferences (NSUserDefaultsController *prefs, - NSObject *control, - NSString *pref_key, - const XrmOptionDescRec *opts) +- (void) bindResource:(NSObject *)control key:(NSString *)pref_key + reload:(BOOL)reload_p { +# ifndef USE_IPHONE NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]] ? @"selectedObject" : ([control isKindOfClass:[NSMatrix class]] ? @"selectedIndex" : @"value")); [control bind:bindto - toObject:prefs + toObject:userDefaultsController withKeyPath:[@"values." stringByAppendingString: pref_key] options:nil]; +# else // USE_IPHONE + SEL sel; + NSObject *val = [userDefaultsController objectForKey:pref_key]; + NSString *sval = 0; + double dval = 0; + + if ([val isKindOfClass:[NSString class]]) { + sval = (NSString *) val; + if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] || + NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] || + NSOrderedSame == [sval caseInsensitiveCompare:@"1"]) + dval = 1; + else + dval = [sval doubleValue]; + } else if ([val isKindOfClass:[NSNumber class]]) { + // NSBoolean (__NSCFBoolean) is really NSNumber. + dval = [(NSNumber *) val doubleValue]; + sval = [(NSNumber *) val stringValue]; + } + + if ([control isKindOfClass:[UISlider class]]) { + sel = @selector(sliderAction:); + [(UISlider *) control setValue: dval]; + } else if ([control isKindOfClass:[UISwitch class]]) { + sel = @selector(switchAction:); + [(UISwitch *) control setOn: ((int) dval != 0)]; +# ifdef USE_PICKER_VIEW + } else if ([control isKindOfClass:[UIPickerView class]]) { + sel = 0; + [(UIPickerView *) control selectRow:((int)dval) inComponent:0 + animated:NO]; +# else // !USE_PICKER_VIEW + } else if ([control isKindOfClass:[RadioButton class]]) { + sel = 0; // radioAction: sent from didSelectRowAtIndexPath. + } else if ([control isKindOfClass:[UITextField class]]) { + sel = 0; // #### + [(UITextField *) control setText: sval]; +# endif // !USE_PICKER_VIEW + } else { + NSAssert (0, @"unknown class"); + } + + // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval); + + if (!reload_p) { + if (! pref_keys) { + pref_keys = [[NSMutableArray arrayWithCapacity:10] retain]; + pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain]; + } + + [pref_keys addObject: [self makeKey:pref_key]]; + [pref_ctls addObject: control]; + ((UIControl *) control).tag = [pref_keys count] - 1; + + if (sel) { + [(UIControl *) control addTarget:self action:sel + forControlEvents:UIControlEventValueChanged]; + } + } -# if 0 // #### - NSObject *def = [[prefs defaults] objectForKey:pref_key]; +# endif // USE_IPHONE + +# if 0 + NSObject *def = [[userDefaultsController defaults] objectForKey:pref_key]; NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key]; s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0]; s = [NSString stringWithFormat:@"%@ = \"%@\"", s, def]; @@ -134,17 +521,254 @@ bind_resource_to_preferences (NSUserDefaultsController *prefs, # endif } + +- (void) bindResource:(NSObject *)control key:(NSString *)pref_key +{ + [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO]; +} + + + +- (void) bindSwitch:(NSObject *)control + cmdline:(NSString *)cmd +{ + [self bindResource:control + key:[self switchToResource:cmd opts:opts valRet:0]]; +} + + +#pragma mark Text-manipulating utilities + + +static NSString * +unwrap (NSString *text) +{ + // Unwrap lines: delete \n but do not delete \n\n. + // + NSArray *lines = [text componentsSeparatedByString:@"\n"]; + int nlines = [lines count]; + BOOL eolp = YES; + int i; + + text = @"\n"; // start with one blank line + + // skip trailing blank lines in file + for (i = nlines-1; i > 0; i--) { + NSString *s = (NSString *) [lines objectAtIndex:i]; + if ([s length] > 0) + break; + nlines--; + } + + // skip leading blank lines in file + for (i = 0; i < nlines; i++) { + NSString *s = (NSString *) [lines objectAtIndex:i]; + if ([s length] > 0) + break; + } + + // unwrap + Bool any = NO; + for (; i < nlines; i++) { + NSString *s = (NSString *) [lines objectAtIndex:i]; + if ([s length] == 0) { + text = [text stringByAppendingString:@"\n\n"]; + eolp = YES; + } else if ([s characterAtIndex:0] == ' ' || + [s hasPrefix:@"Copyright "] || + [s hasPrefix:@"http://"]) { + // don't unwrap if the following line begins with whitespace, + // or with the word "Copyright", or if it begins with a URL. + if (any && !eolp) + text = [text stringByAppendingString:@"\n"]; + text = [text stringByAppendingString:s]; + any = YES; + eolp = NO; + } else { + if (!eolp) + text = [text stringByAppendingString:@" "]; + text = [text stringByAppendingString:s]; + eolp = NO; + any = YES; + } + } + + return text; +} + + +# ifndef USE_IPHONE +/* Makes the text up to the first comma be bold. + */ static void -bind_switch_to_preferences (NSUserDefaultsController *prefs, - NSObject *control, - NSString *cmdline_switch, - const XrmOptionDescRec *opts) +boldify (NSText *nstext) +{ + NSString *text = [nstext string]; + NSRange r = [text rangeOfString:@"," options:0]; + r.length = r.location+1; + + r.location = 0; + + NSFont *font = [nstext font]; + font = [NSFont boldSystemFontOfSize:[font pointSize]]; + [nstext setFont:font range:r]; +} +# endif // !USE_IPHONE + + +/* Creates a human-readable anchor to put on a URL. + */ +static char * +anchorize (const char *url) { - NSString *pref_key = switch_to_resource (cmdline_switch, opts, 0); - bind_resource_to_preferences (prefs, control, pref_key, opts); + const char *wiki = "http://en.wikipedia.org/wiki/"; + const char *math = "http://mathworld.wolfram.com/"; + if (!strncmp (wiki, url, strlen(wiki))) { + char *anchor = (char *) malloc (strlen(url) * 3 + 10); + strcpy (anchor, "Wikipedia: \""); + const char *in = url + strlen(wiki); + char *out = anchor + strlen(anchor); + while (*in) { + if (*in == '_') { + *out++ = ' '; + } else if (*in == '#') { + *out++ = ':'; + *out++ = ' '; + } else if (*in == '%') { + char hex[3]; + hex[0] = in[1]; + hex[1] = in[2]; + hex[2] = 0; + int n = 0; + sscanf (hex, "%x", &n); + *out++ = (char) n; + in += 2; + } else { + *out++ = *in; + } + in++; + } + *out++ = '"'; + *out = 0; + return anchor; + + } else if (!strncmp (math, url, strlen(math))) { + char *anchor = (char *) malloc (strlen(url) * 3 + 10); + strcpy (anchor, "MathWorld: \""); + const char *start = url + strlen(wiki); + const char *in = start; + char *out = anchor + strlen(anchor); + while (*in) { + if (*in == '_') { + *out++ = ' '; + } else if (in != start && *in >= 'A' && *in <= 'Z') { + *out++ = ' '; + *out++ = *in; + } else if (!strncmp (in, ".htm", 4)) { + break; + } else { + *out++ = *in; + } + in++; + } + *out++ = '"'; + *out = 0; + return anchor; + + } else { + return strdup (url); + } +} + + +/* Converts any http: URLs in the given text field to clickable links. + */ +static void +hreffify (NSText *nstext) +{ +# ifndef USE_IPHONE + NSString *text = [nstext string]; + [nstext setRichText:YES]; +# else + NSString *text = [nstext text]; +# endif + + int L = [text length]; + NSRange start; // range is start-of-search to end-of-string + start.location = 0; + start.length = L; + while (start.location < L) { + + // Find the beginning of a URL... + // + NSRange r2 = [text rangeOfString:@"http://" options:0 range:start]; + if (r2.location == NSNotFound) + break; + + // Next time around, start searching after this. + start.location = r2.location + r2.length; + start.length = L - start.location; + + // Find the end of a URL (whitespace or EOF)... + // + NSRange r3 = [text rangeOfCharacterFromSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet] + options:0 range:start]; + if (r3.location == NSNotFound) // EOF + r3.location = L, r3.length = 0; + + // Next time around, start searching after this. + start.location = r3.location; + start.length = L - start.location; + + // Set r2 to the start/length of this URL. + r2.length = start.location - r2.location; + + // Extract the URL. + NSString *nsurl = [text substringWithRange:r2]; + const char *url = [nsurl UTF8String]; + + // If this is a Wikipedia URL, make the linked text be prettier. + // + char *anchor = anchorize(url); + +# ifndef USE_IPHONE + + // Construct the RTF corresponding to anchor + // + const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}"; + char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10); + sprintf (rtf, fmt, url, anchor); + + NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)]; + [nstext replaceCharactersInRange:r2 withRTF:rtfdata]; + +# else // !USE_IPHONE + // *anchor = 0; // Omit Wikipedia anchor + text = [text stringByReplacingCharactersInRange:r2 + withString:[NSString stringWithCString:anchor + encoding:NSUTF8StringEncoding]]; + // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n" + // withString:@"\n\n"]; +# endif // !USE_IPHONE + + free (anchor); + + int L2 = [text length]; // might have changed + start.location -= (L - L2); + L = L2; + } + +# ifdef USE_IPHONE + [nstext setText:text]; + [nstext sizeToFit]; +# endif } +#pragma mark Creating controls from XML + + /* Parse the attributes of an XML tag into a dictionary. For input, the dictionary should have as attributes the keys, each with @"" as their value. @@ -152,8 +776,7 @@ bind_switch_to_preferences (NSUserDefaultsController *prefs, and keys that were not specified will not be present in the dictionary. Warnings are printed if there are duplicate or unknown attributes. */ -static void -parse_attrs (NSMutableDictionary *dict, NSXMLNode *node) +- (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node { NSArray *attrs = [(NSXMLElement *) node attributes]; int n = [attrs count]; @@ -191,73 +814,59 @@ parse_attrs (NSMutableDictionary *dict, NSXMLNode *node) } } - -/* Creates a label: an un-editable NSTextField displaying the given text. - */ -static NSTextField * -make_label (NSString *text) -{ - NSRect rect; - rect.origin.x = rect.origin.y = 0; - rect.size.width = rect.size.height = 10; - NSTextField *lab = [[NSTextField alloc] initWithFrame:rect]; - [lab setSelectable:NO]; - [lab setEditable:NO]; - [lab setBezeled:NO]; - [lab setDrawsBackground:NO]; - [lab setStringValue:text]; - [lab sizeToFit]; - return lab; -} - - -static NSView * -last_child (NSView *parent) -{ - NSArray *kids = [parent subviews]; - int nkids = [kids count]; - if (nkids == 0) - return 0; - else - return [kids objectAtIndex:nkids-1]; -} - - -/* Add the child as a subview of the parent, positioning it immediately - below or to the right of the previously-added child of that view. - */ -static void -place_child (NSView *parent, NSView *child, BOOL right_p) -{ - NSRect rect = [child frame]; - NSView *last = last_child (parent); - if (!last) { - rect.origin.x = LEFT_MARGIN; - rect.origin.y = [parent frame].size.height - rect.size.height - - LINE_SPACING; - } else if (right_p) { - rect = [last frame]; - rect.origin.x += rect.size.width + COLUMN_SPACING; - } else { - rect = [last frame]; - rect.origin.x = LEFT_MARGIN; - rect.origin.y -= [child frame].size.height + LINE_SPACING; - } - [child setFrameOrigin:rect.origin]; - [parent addSubview:child]; + +/* Handle the options on the top level tag. + */ +- (NSString *) parseXScreenSaverTag:(NSXMLNode *)node +{ + NSMutableDictionary *dict = + [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"", @"name", + @"", @"_label", + @"", @"gl", + nil]; + [self parseAttrs:dict node:node]; + NSString *name = [dict objectForKey:@"name"]; + NSString *label = [dict objectForKey:@"_label"]; + + NSAssert1 (label, @"no _label in %@", [node name]); + NSAssert1 (name, @"no name in \"%@\"", label); + return label; } -static void traverse_children (NSUserDefaultsController *, - const XrmOptionDescRec *, - NSView *, NSXMLNode *); +/* Creates a label: an un-editable NSTextField displaying the given text. + */ +- (LABEL *) makeLabel:(NSString *)text +{ + NSRect rect; + rect.origin.x = rect.origin.y = 0; + rect.size.width = rect.size.height = 10; +# ifndef USE_IPHONE + NSTextField *lab = [[NSTextField alloc] initWithFrame:rect]; + [lab setSelectable:NO]; + [lab setEditable:NO]; + [lab setBezeled:NO]; + [lab setDrawsBackground:NO]; + [lab setStringValue:text]; + [lab sizeToFit]; +# else // USE_IPHONE + UILabel *lab = [[UILabel alloc] initWithFrame:rect]; + [lab setText: [text stringByTrimmingCharactersInSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]]; + [lab setBackgroundColor:[UIColor clearColor]]; + [lab setNumberOfLines:0]; // unlimited + [lab setLineBreakMode:UILineBreakModeWordWrap]; + [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleHeight)]; +# endif // USE_IPHONE + return lab; +} /* Creates the checkbox (NSButton) described by the given XML node. */ -static void -make_checkbox (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node) +- (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent { NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObjectsAndKeys: @@ -266,7 +875,7 @@ make_checkbox (NSUserDefaultsController *prefs, @"", @"arg-set", @"", @"arg-unset", nil]; - parse_attrs (dict, node); + [self parseAttrs:dict node:node]; NSString *label = [dict objectForKey:@"_label"]; NSString *arg_set = [dict objectForKey:@"arg-set"]; NSString *arg_unset = [dict objectForKey:@"arg-unset"]; @@ -299,260 +908,26 @@ make_checkbox (NSUserDefaultsController *prefs, rect.origin.x = rect.origin.y = 0; rect.size.width = rect.size.height = 10; +# ifndef USE_IPHONE + NSButton *button = [[NSButton alloc] initWithFrame:rect]; - [button setButtonType:([[node name] isEqualToString:@"radio"] - ? NSRadioButton - : NSSwitchButton)]; + [button setButtonType:NSSwitchButton]; [button setTitle:label]; [button sizeToFit]; - place_child (parent, button, NO); - - bind_switch_to_preferences (prefs, button, - (arg_set ? arg_set : arg_unset), - opts); - [button release]; -} - - -/* Creates the NSTextField described by the given XML node. -*/ -static void -make_text_field (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node, - BOOL no_label_p) -{ - NSMutableDictionary *dict = - [NSMutableDictionary dictionaryWithObjectsAndKeys: - @"", @"id", - @"", @"_label", - @"", @"arg", - nil]; - parse_attrs (dict, node); - NSString *label = [dict objectForKey:@"_label"]; - NSString *arg = [dict objectForKey:@"arg"]; - - if (!label && !no_label_p) { - NSAssert1 (0, @"no _label in %@", [node name]); - return; - } - - NSAssert1 (arg, @"no arg in %@", label); - - NSRect rect; - rect.origin.x = rect.origin.y = 0; - rect.size.width = rect.size.height = 10; - - NSTextField *txt = [[NSTextField alloc] initWithFrame:rect]; - - // make the default size be around 30 columns; a typical value for - // these text fields is "xscreensaver-text --cols 40". - // - [txt setStringValue:@"123456789 123456789 123456789 "]; - [txt sizeToFit]; - [[txt cell] setWraps:NO]; - [[txt cell] setScrollable:YES]; - [txt setStringValue:@""]; - - if (label) { - NSTextField *lab = make_label (label); - place_child (parent, lab, NO); - [lab release]; - } - - place_child (parent, txt, (label ? YES : NO)); - - bind_switch_to_preferences (prefs, txt, arg, opts); - [txt release]; -} - - -/* Creates the NSTextField described by the given XML node, - and hooks it up to a Choose button and a file selector widget. -*/ -static void -make_file_selector (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, - NSView *parent, NSXMLNode *node, - BOOL dirs_only_p, - BOOL no_label_p, - BOOL editable_text_p) -{ - NSMutableDictionary *dict = - [NSMutableDictionary dictionaryWithObjectsAndKeys: - @"", @"id", - @"", @"_label", - @"", @"arg", - nil]; - parse_attrs (dict, node); - NSString *label = [dict objectForKey:@"_label"]; - NSString *arg = [dict objectForKey:@"arg"]; + [self placeChild:button on:parent]; - if (!label && !no_label_p) { - NSAssert1 (0, @"no _label in %@", [node name]); - return; - } +# else // USE_IPHONE - NSAssert1 (arg, @"no arg in %@", label); + LABEL *lab = [self makeLabel:label]; + [self placeChild:lab on:parent]; + UISwitch *button = [[UISwitch alloc] initWithFrame:rect]; + [self placeChild:button on:parent right:YES]; + [lab release]; - NSRect rect; - rect.origin.x = rect.origin.y = 0; - rect.size.width = rect.size.height = 10; +# endif // USE_IPHONE - NSTextField *txt = [[NSTextField alloc] initWithFrame:rect]; - - // make the default size be around 20 columns. - // - [txt setStringValue:@"123456789 123456789 "]; - [txt sizeToFit]; - [txt setSelectable:YES]; - [txt setEditable:editable_text_p]; - [txt setBezeled:editable_text_p]; - [txt setDrawsBackground:editable_text_p]; - [[txt cell] setWraps:NO]; - [[txt cell] setScrollable:YES]; - [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead]; - [txt setStringValue:@""]; - - NSTextField *lab = 0; - if (label) { - lab = make_label (label); - place_child (parent, lab, NO); - [lab release]; - } - - place_child (parent, txt, (label ? YES : NO)); - - bind_switch_to_preferences (prefs, txt, arg, opts); - [txt release]; - - // Make the text field and label be the same height, whichever is taller. - if (lab) { - rect = [txt frame]; - rect.size.height = ([lab frame].size.height > [txt frame].size.height - ? [lab frame].size.height - : [txt frame].size.height); - [txt setFrame:rect]; - } - - // Now put a "Choose" button next to it. - // - rect.origin.x = rect.origin.y = 0; - rect.size.width = rect.size.height = 10; - NSButton *choose = [[NSButton alloc] initWithFrame:rect]; - [choose setTitle:@"Choose..."]; - [choose setBezelStyle:NSRoundedBezelStyle]; - [choose sizeToFit]; - - place_child (parent, choose, YES); - - // center the Choose button around the midpoint of the text field. - rect = [choose frame]; - rect.origin.y = ([txt frame].origin.y + - (([txt frame].size.height - rect.size.height) / 2)); - [choose setFrameOrigin:rect.origin]; - - [choose setTarget:[parent window]]; - if (dirs_only_p) - [choose setAction:@selector(chooseClickedDirs:)]; - else - [choose setAction:@selector(chooseClicked:)]; - - [choose release]; -} - - -/* Runs a modal file selector and sets the text field's value to the - selected file or directory. - */ -static void -do_file_selector (NSTextField *txt, BOOL dirs_p) -{ - NSOpenPanel *panel = [NSOpenPanel openPanel]; - [panel setAllowsMultipleSelection:NO]; - [panel setCanChooseFiles:!dirs_p]; - [panel setCanChooseDirectories:dirs_p]; - - NSString *file = [txt stringValue]; - if ([file length] <= 0) { - file = NSHomeDirectory(); - if (dirs_p) - file = [file stringByAppendingPathComponent:@"Pictures"]; - } - -// NSString *dir = [file stringByDeletingLastPathComponent]; - - int result = [panel runModalForDirectory:file //dir - file:nil //[file lastPathComponent] - types:nil]; - if (result == NSOKButton) { - NSArray *files = [panel filenames]; - file = ([files count] > 0 ? [files objectAtIndex:0] : @""); - file = [file stringByAbbreviatingWithTildeInPath]; - [txt setStringValue:file]; - - // Fuck me! Just setting the value of the NSTextField does not cause - // that to end up in the preferences! - // - NSDictionary *dict = [txt infoForBinding:@"value"]; - NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"]; - NSString *path = [dict objectForKey:@"NSObservedKeyPath"]; - if ([path hasPrefix:@"values."]) // WTF. - path = [path substringFromIndex:7]; - [[prefs values] setValue:file forKey:path]; - -#if 0 - // make sure the end of the string is visible. - NSText *fe = [[txt window] fieldEditor:YES forObject:txt]; - NSRange range; - range.location = [file length]-3; - range.length = 1; - if (! [[txt window] makeFirstResponder:[txt window]]) - [[txt window] endEditingFor:nil]; -// [[txt window] makeFirstResponder:nil]; - [fe setSelectedRange:range]; -// [tv scrollRangeToVisible:range]; -// [txt setNeedsDisplay:YES]; -// [[txt cell] setNeedsDisplay:YES]; -// [txt selectAll:txt]; -#endif - } -} - -/* Returns the NSTextField that is to the left of or above the NSButton. - */ -static NSTextField * -find_text_field_of_button (NSButton *button) -{ - NSView *parent = [button superview]; - NSArray *kids = [parent subviews]; - int nkids = [kids count]; - int i; - NSTextField *f = 0; - for (i = 0; i < nkids; i++) { - NSObject *kid = [kids objectAtIndex:i]; - if ([kid isKindOfClass:[NSTextField class]]) { - f = (NSTextField *) kid; - } else if (kid == button) { - if (! f) abort(); - return f; - } - } - abort(); -} - - -- (void) chooseClicked:(NSObject *)arg -{ - NSButton *choose = (NSButton *) arg; - NSTextField *txt = find_text_field_of_button (choose); - do_file_selector (txt, NO); -} - -- (void) chooseClickedDirs:(NSObject *)arg -{ - NSButton *choose = (NSButton *) arg; - NSTextField *txt = find_text_field_of_button (choose); - do_file_selector (txt, YES); + [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)]; + [button release]; } @@ -560,10 +935,7 @@ find_text_field_of_button (NSButton *button) If "type=slider", it's an NSSlider. If "type=spinbutton", it's a text field with up/down arrows next to it. */ -static void -make_number_selector (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, - NSView *parent, NSXMLNode *node) +- (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent { NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObjectsAndKeys: @@ -578,7 +950,7 @@ make_number_selector (NSUserDefaultsController *prefs, @"", @"default", @"", @"convert", nil]; - parse_attrs (dict, node); + [self parseAttrs:dict node:node]; NSString *label = [dict objectForKey:@"_label"]; NSString *low_label = [dict objectForKey:@"_low-label"]; NSString *high_label = [dict objectForKey:@"_high-label"]; @@ -618,18 +990,20 @@ make_number_selector (NSUserDefaultsController *prefs, BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound || [high rangeOfCharacterFromSet:dot].location != NSNotFound); - if ([type isEqualToString:@"slider"]) { + if ([type isEqualToString:@"slider"] +# ifdef USE_IPHONE // On iPhone, we use sliders for all numeric values. + || [type isEqualToString:@"spinbutton"] +# endif + ) { NSRect rect; rect.origin.x = rect.origin.y = 0; rect.size.width = 150; rect.size.height = 23; // apparent min height for slider with ticks... NSSlider *slider; - if (cvt) - slider = [[InvertedSlider alloc] initWithFrame:rect]; - else - slider = [[NSSlider alloc] initWithFrame:rect]; - + slider = [[InvertedSlider alloc] initWithFrame:rect + inverted: !!cvt + integers: !float_p]; [slider setMaxValue:[high doubleValue]]; [slider setMinValue:[low doubleValue]]; @@ -647,38 +1021,64 @@ make_number_selector (NSUserDefaultsController *prefs, if (float_p && range2 < max_ticks) range2 = max_ticks; +# ifndef USE_IPHONE [slider setNumberOfTickMarks:range2]; [slider setAllowsTickMarkValuesOnly: (range == range2 && // we are showing the actual number of ticks !float_p)]; // and we want integer results +# endif // !USE_IPHONE // #### Note: when the slider's range is large enough that we aren't // showing all possible ticks, the slider's value is not constrained // to be an integer, even though it should be... // Maybe we need to use a value converter or something? + LABEL *lab; if (label) { - NSTextField *lab = make_label (label); - place_child (parent, lab, NO); + lab = [self makeLabel:label]; + [self placeChild:lab on:parent]; +# ifdef USE_IPHONE + if (low_label) { + CGFloat s = [NSFont systemFontSize] + 4; + [lab setFont:[NSFont boldSystemFontOfSize:s]]; + } +# endif [lab release]; } if (low_label) { - NSTextField *lab = make_label (low_label); + lab = [self makeLabel:low_label]; [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; +# ifndef USE_IPHONE [lab setAlignment:1]; // right aligned rect = [lab frame]; if (rect.size.width < LEFT_LABEL_WIDTH) rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size rect.size.height = [slider frame].size.height; [lab setFrame:rect]; - place_child (parent, lab, NO); + [self placeChild:lab on:parent]; +# else // USE_IPHONE + [lab setTextAlignment: UITextAlignmentRight]; + [self placeChild:lab on:parent right:(label ? YES : NO)]; +# endif // USE_IPHONE + [lab release]; } - place_child (parent, slider, (low_label ? YES : NO)); +# ifndef USE_IPHONE + [self placeChild:slider on:parent right:(low_label ? YES : NO)]; +# else // USE_IPHONE + [self placeChild:slider on:parent right:(label || low_label ? YES : NO)]; +# endif // USE_IPHONE + if (low_label) { + // Make left label be same height as slider. + rect = [lab frame]; + rect.size.height = [slider frame].size.height; + [lab setFrame:rect]; + } + if (! low_label) { rect = [slider frame]; if (rect.origin.x < LEFT_LABEL_WIDTH) @@ -687,18 +1087,22 @@ make_number_selector (NSUserDefaultsController *prefs, } if (high_label) { - NSTextField *lab = make_label (high_label); - [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + lab = [self makeLabel:high_label]; + [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; rect = [lab frame]; + + // Make right label be same height as slider. rect.size.height = [slider frame].size.height; [lab setFrame:rect]; - place_child (parent, lab, YES); + [self placeChild:lab on:parent right:YES]; [lab release]; } - bind_switch_to_preferences (prefs, slider, arg, opts); + [self bindSwitch:slider cmdline:arg]; [slider release]; +#ifndef USE_IPHONE // On iPhone, we use sliders for all numeric values. + } else if ([type isEqualToString:@"spinbutton"]) { if (! label) { @@ -722,19 +1126,19 @@ make_number_selector (NSUserDefaultsController *prefs, [txt setStringValue:@""]; if (label) { - NSTextField *lab = make_label (label); - //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + LABEL *lab = [self makeLabel:label]; + //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; [lab setAlignment:1]; // right aligned rect = [lab frame]; if (rect.size.width < LEFT_LABEL_WIDTH) rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size rect.size.height = [txt frame].size.height; [lab setFrame:rect]; - place_child (parent, lab, NO); + [self placeChild:lab on:parent]; [lab release]; } - place_child (parent, txt, (label ? YES : NO)); + [self placeChild:txt on:parent right:(label ? YES : NO)]; if (! label) { rect = [txt frame]; @@ -746,7 +1150,7 @@ make_number_selector (NSUserDefaultsController *prefs, rect.size.width = rect.size.height = 10; NSStepper *step = [[NSStepper alloc] initWithFrame:rect]; [step sizeToFit]; - place_child (parent, step, YES); + [self placeChild:step on:parent right:YES]; rect = [step frame]; rect.origin.x -= COLUMN_SPACING; // this one goes close rect.origin.y += ([txt frame].size.height - rect.size.height) / 2; @@ -776,19 +1180,21 @@ make_number_selector (NSUserDefaultsController *prefs, [fmt setGeneratesDecimalNumbers:float_p]; [[txt cell] setFormatter:fmt]; - - bind_switch_to_preferences (prefs, step, arg, opts); - bind_switch_to_preferences (prefs, txt, arg, opts); + [self bindSwitch:step cmdline:arg]; + [self bindSwitch:txt cmdline:arg]; [step release]; [txt release]; - + +# endif // USE_IPHONE + } else { NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label); } } +# ifndef USE_IPHONE static void set_menu_item_object (NSMenuItem *item, NSObject *obj) { @@ -811,14 +1217,12 @@ set_menu_item_object (NSMenuItem *item, NSObject *obj) [item setRepresentedObject:obj]; //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]); } +# endif // !USE_IPHONE /* Creates the popup menu described by the given XML node (and its children). */ -static void -make_option_menu (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, - NSView *parent, NSXMLNode *node) +- (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent { NSArray *children = [node children]; int i, count = [children count]; @@ -834,24 +1238,39 @@ make_option_menu (NSUserDefaultsController *prefs, [NSMutableDictionary dictionaryWithObjectsAndKeys: @"", @"id", nil]; - parse_attrs (dict, node); + [self parseAttrs:dict node:node]; NSRect rect; rect.origin.x = rect.origin.y = 0; rect.size.width = 10; rect.size.height = 10; + NSString *menu_key = nil; // the resource key used by items in this menu + +# ifndef USE_IPHONE // #### "Build and Analyze" says that all of our widgets leak, because it - // seems to not realize that place_child -> addSubview retains them. + // seems to not realize that placeChild -> addSubview retains them. // Not sure what to do to make these warnings go away. NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect pullsDown:NO]; - NSMenuItem *def_item = nil; float max_width = 0; - - NSString *menu_key = nil; // the resource key used by items in this menu + +# else // USE_IPHONE + + NSString *def_item = nil; + + rect.size.width = 0; + rect.size.height = 0; +# ifdef USE_PICKER_VIEW + UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain]; + popup.delegate = self; + popup.dataSource = self; +# endif // !USE_PICKER_VIEW + NSMutableArray *items = [NSMutableArray arrayWithCapacity:10]; + +# endif // USE_IPHONE for (i = 0; i < count; i++) { NSXMLNode *child = [children objectAtIndex:i]; @@ -859,7 +1278,7 @@ make_option_menu (NSUserDefaultsController *prefs, if ([child kind] == NSXMLCommentKind) continue; if ([child kind] != NSXMLElementKind) { - NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node); +// NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node); continue; } @@ -871,22 +1290,26 @@ make_option_menu (NSUserDefaultsController *prefs, @"", @"_label", @"", @"arg-set", nil]; - parse_attrs (dict2, child); + [self parseAttrs:dict2 node:child]; NSString *label = [dict2 objectForKey:@"_label"]; NSString *arg_set = [dict2 objectForKey:@"arg-set"]; if (!label) { NSAssert1 (0, @"no _label in %@", [child name]); - return; + continue; } +# ifndef USE_IPHONE // create the menu item (and then get a pointer to it) [popup addItemWithTitle:label]; NSMenuItem *item = [popup itemWithTitle:label]; +# endif // USE_IPHONE if (arg_set) { NSString *this_val = NULL; - NSString *this_key = switch_to_resource (arg_set, opts, &this_val); + NSString *this_key = [self switchToResource: arg_set + opts: opts + valRet: &this_val]; NSAssert1 (this_val, @"this_val null for %@", arg_set); if (menu_key && ![menu_key isEqualToString:this_key]) NSAssert3 (0, @@ -898,26 +1321,41 @@ make_option_menu (NSUserDefaultsController *prefs, /* If this menu has the cmd line "-mode foo" then set this item's value to "foo" (the menu itself will be bound to e.g. "modeString") */ +# ifndef USE_IPHONE set_menu_item_object (item, this_val); +# else + // Array holds ["Label", "resource-key", "resource-val"]. + [items addObject:[NSMutableArray arrayWithObjects: + label, @"", this_val, nil]]; +# endif } else { // no arg-set -- only one menu item can be missing that. NSAssert1 (!def_item, @"no arg-set in \"%@\"", label); +# ifndef USE_IPHONE def_item = item; +# else + def_item = label; + // Array holds ["Label", "resource-key", "resource-val"]. + [items addObject:[NSMutableArray arrayWithObjects: + label, @"", @"", nil]]; +# endif } /* make sure the menu button has room for the text of this item, and remember the greatest width it has reached. */ +# ifndef USE_IPHONE [popup setTitle:label]; [popup sizeToFit]; NSRect r = [popup frame]; if (r.size.width > max_width) max_width = r.size.width; +# endif // USE_IPHONE } if (!menu_key) { NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]); - abort(); + return; } /* We've added all of the menu items. If there was an item with no @@ -927,16 +1365,33 @@ make_option_menu (NSUserDefaultsController *prefs, yet know what resource was associated with this menu.) */ if (def_item) { - NSDictionary *defs = [prefs initialValues]; - NSObject *def_obj = [defs objectForKey:menu_key]; - + NSObject *def_obj = [defaultOptions objectForKey:menu_key]; NSAssert2 (def_obj, @"no default value for resource \"%@\" in menu item \"%@\"", - menu_key, [def_item title]); + menu_key, +# ifndef USE_IPHONE + [def_item title] +# else + def_item +# endif + ); +# ifndef USE_IPHONE set_menu_item_object (def_item, def_obj); +# else // !USE_IPHONE + for (NSMutableArray *a in items) { + // Make sure each array contains the resource key. + [a replaceObjectAtIndex:1 withObject:menu_key]; + // Make sure the default item contains the default resource value. + if (def_obj && def_item && + [def_item isEqualToString:[a objectAtIndex:0]]) + [a replaceObjectAtIndex:2 withObject:def_obj]; + } +# endif // !USE_IPHONE } +# ifndef USE_IPHONE +# ifdef USE_PICKER_VIEW /* Finish tweaking the menu button itself. */ if (def_item) @@ -944,22 +1399,56 @@ make_option_menu (NSUserDefaultsController *prefs, NSRect r = [popup frame]; r.size.width = max_width; [popup setFrame:r]; - place_child (parent, popup, NO); +# endif // USE_PICKER_VIEW +# endif - bind_resource_to_preferences (prefs, popup, menu_key, opts); +# if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW) + [self placeChild:popup on:parent]; + [self bindResource:popup key:menu_key]; [popup release]; -} +# endif + +# ifdef USE_IPHONE +# ifdef USE_PICKER_VIEW + // Store the items for this picker in the picker_values array. + // This is so fucking stupid. + + int menu_number = [pref_keys count] - 1; + if (! picker_values) + picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain]; + while ([picker_values count] <= menu_number) + [picker_values addObject:[NSArray arrayWithObjects: nil]]; + [picker_values replaceObjectAtIndex:menu_number withObject:items]; + [popup reloadAllComponents]; + +# else // !USE_PICKER_VIEW + + [self placeSeparator]; + + i = 0; + for (NSArray *item in items) { + RadioButton *b = [[RadioButton alloc] initWithIndex:i + items:items]; + [b setFont:[NSFont boldSystemFontOfSize: + // #### Fucking hardcoded UITableView font size BS! + 17 // [NSFont systemFontSize] + ]]; + [self placeChild:b on:parent]; + i++; + } + + [self placeSeparator]; +# endif // !USE_PICKER_VIEW +# endif // !USE_IPHONE + +} -static NSString *unwrap (NSString *); -static void hreffify (NSText *); -static void boldify (NSText *); /* Creates an uneditable, wrapping NSTextField to display the given text enclosed by ... in the XML. */ -static void -make_desc_label (NSView *parent, NSXMLNode *node) +- (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent { NSString *text = nil; NSArray *children = [node children]; @@ -980,6 +1469,7 @@ make_desc_label (NSView *parent, NSXMLNode *node) rect.origin.x = rect.origin.y = 0; rect.size.width = 200; rect.size.height = 50; // sized later +# ifndef USE_IPHONE NSText *lab = [[NSText alloc] initWithFrame:rect]; [lab setEditable:NO]; [lab setDrawsBackground:NO]; @@ -989,290 +1479,293 @@ make_desc_label (NSView *parent, NSXMLNode *node) hreffify (lab); boldify (lab); [lab sizeToFit]; +# else // USE_IPHONE + UILabel *lab = [self makeLabel:text]; + [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]]; + hreffify (lab); + [self placeSeparator]; +# endif // USE_IPHONE - place_child (parent, lab, NO); + [self placeChild:lab on:parent]; [lab release]; } -static NSString * -unwrap (NSString *text) -{ - // Unwrap lines: delete \n but do not delete \n\n. - // - NSArray *lines = [text componentsSeparatedByString:@"\n"]; - int nlines = [lines count]; - BOOL eolp = YES; - int i; - text = @"\n"; // start with one blank line +/* Creates the NSTextField described by the given XML node. +*/ +- (void) makeTextField: (NSXMLNode *)node + on: (NSView *)parent + withLabel: (BOOL) label_p + horizontal: (BOOL) horiz_p +{ + NSMutableDictionary *dict = + [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"", @"id", + @"", @"_label", + @"", @"arg", + nil]; + [self parseAttrs:dict node:node]; + NSString *label = [dict objectForKey:@"_label"]; + NSString *arg = [dict objectForKey:@"arg"]; - // skip trailing blank lines in file - for (i = nlines-1; i > 0; i--) { - NSString *s = (NSString *) [lines objectAtIndex:i]; - if ([s length] > 0) - break; - nlines--; + if (!label && label_p) { + NSAssert1 (0, @"no _label in %@", [node name]); + return; } - // skip leading blank lines in file - for (i = 0; i < nlines; i++) { - NSString *s = (NSString *) [lines objectAtIndex:i]; - if ([s length] > 0) - break; - } + NSAssert1 (arg, @"no arg in %@", label); - // unwrap - Bool any = NO; - for (; i < nlines; i++) { - NSString *s = (NSString *) [lines objectAtIndex:i]; - if ([s length] == 0) { - text = [text stringByAppendingString:@"\n\n"]; - eolp = YES; - } else if ([s characterAtIndex:0] == ' ' || - [s hasPrefix:@"Copyright "] || - [s hasPrefix:@"http://"]) { - // don't unwrap if the following line begins with whitespace, - // or with the word "Copyright", or if it begins with a URL. - if (any && !eolp) - text = [text stringByAppendingString:@"\n"]; - text = [text stringByAppendingString:s]; - any = YES; - eolp = NO; - } else { - if (!eolp) - text = [text stringByAppendingString:@" "]; - text = [text stringByAppendingString:s]; - eolp = NO; - any = YES; - } - } + NSRect rect; + rect.origin.x = rect.origin.y = 0; + rect.size.width = rect.size.height = 10; + + NSTextField *txt = [[NSTextField alloc] initWithFrame:rect]; - return text; -} +# ifndef USE_IPHONE + // make the default size be around 30 columns; a typical value for + // these text fields is "xscreensaver-text --cols 40". + // + [txt setStringValue:@"123456789 123456789 123456789 "]; + [txt sizeToFit]; + [[txt cell] setWraps:NO]; + [[txt cell] setScrollable:YES]; + [txt setStringValue:@""]; + +# else // USE_IPHONE + + txt.adjustsFontSizeToFitWidth = YES; + txt.textColor = [UIColor blackColor]; + // #### Fucking hardcoded UITableView font size BS! + txt.font = [UIFont systemFontOfSize: 17]; + txt.placeholder = @""; + txt.borderStyle = UITextBorderStyleRoundedRect; + txt.textAlignment = UITextAlignmentRight; + txt.keyboardType = UIKeyboardTypeDefault; // Full kbd + txt.autocorrectionType = UITextAutocorrectionTypeNo; + txt.autocapitalizationType = UITextAutocapitalizationTypeNone; + txt.clearButtonMode = UITextFieldViewModeAlways; + txt.returnKeyType = UIReturnKeyDone; + txt.delegate = self; + txt.text = @""; + [txt setEnabled: YES]; + + rect.size.height = [txt.font lineHeight] * 1.2; + [txt setFrame:rect]; + +# endif // USE_IPHONE -static char * -anchorize (const char *url) -{ - const char *wiki = "http://en.wikipedia.org/wiki/"; - const char *math = "http://mathworld.wolfram.com/"; - if (!strncmp (wiki, url, strlen(wiki))) { - char *anchor = (char *) malloc (strlen(url) * 3 + 10); - strcpy (anchor, "Wikipedia: \""); - const char *in = url + strlen(wiki); - char *out = anchor + strlen(anchor); - while (*in) { - if (*in == '_') { - *out++ = ' '; - } else if (*in == '#') { - *out++ = ':'; - *out++ = ' '; - } else if (*in == '%') { - char hex[3]; - hex[0] = in[1]; - hex[1] = in[2]; - hex[2] = 0; - int n = 0; - sscanf (hex, "%x", &n); - *out++ = (char) n; - in += 2; - } else { - *out++ = *in; - } - in++; - } - *out++ = '"'; - *out = 0; - return anchor; + if (label) { + LABEL *lab = [self makeLabel:label]; + [self placeChild:lab on:parent]; + [lab release]; + } - } else if (!strncmp (math, url, strlen(math))) { - char *anchor = (char *) malloc (strlen(url) * 3 + 10); - strcpy (anchor, "MathWorld: \""); - const char *start = url + strlen(wiki); - const char *in = start; - char *out = anchor + strlen(anchor); - while (*in) { - if (*in == '_') { - *out++ = ' '; - } else if (in != start && *in >= 'A' && *in <= 'Z') { - *out++ = ' '; - *out++ = *in; - } else if (!strncmp (in, ".htm", 4)) { - break; - } else { - *out++ = *in; - } - in++; - } - *out++ = '"'; - *out = 0; - return anchor; + [self placeChild:txt on:parent right:(label ? YES : NO)]; - } else { - return strdup (url); - } + [self bindSwitch:txt cmdline:arg]; + [txt release]; } -/* Converts any http: URLs in the given text field to clickable links. - */ -static void -hreffify (NSText *nstext) +/* Creates the NSTextField described by the given XML node, + and hooks it up to a Choose button and a file selector widget. +*/ +- (void) makeFileSelector: (NSXMLNode *)node + on: (NSView *)parent + dirsOnly: (BOOL) dirsOnly + withLabel: (BOOL) label_p + editable: (BOOL) editable_p { - NSString *text = [nstext string]; - [nstext setRichText:YES]; +# ifndef USE_IPHONE // No files. No selectors. + NSMutableDictionary *dict = + [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"", @"id", + @"", @"_label", + @"", @"arg", + nil]; + [self parseAttrs:dict node:node]; + NSString *label = [dict objectForKey:@"_label"]; + NSString *arg = [dict objectForKey:@"arg"]; - int L = [text length]; - NSRange start; // range is start-of-search to end-of-string - start.location = 0; - start.length = L; - while (start.location < L) { + if (!label && label_p) { + NSAssert1 (0, @"no _label in %@", [node name]); + return; + } - // Find the beginning of a URL... - // - NSRange r2 = [text rangeOfString:@"http://" options:0 range:start]; - if (r2.location == NSNotFound) - break; + NSAssert1 (arg, @"no arg in %@", label); - // Next time around, start searching after this. - start.location = r2.location + r2.length; - start.length = L - start.location; + NSRect rect; + rect.origin.x = rect.origin.y = 0; + rect.size.width = rect.size.height = 10; + + NSTextField *txt = [[NSTextField alloc] initWithFrame:rect]; - // Find the end of a URL (whitespace or EOF)... - // - NSRange r3 = [text rangeOfCharacterFromSet: - [NSCharacterSet whitespaceAndNewlineCharacterSet] - options:0 range:start]; - if (r3.location == NSNotFound) // EOF - r3.location = L, r3.length = 0; + // make the default size be around 20 columns. + // + [txt setStringValue:@"123456789 123456789 "]; + [txt sizeToFit]; + [txt setSelectable:YES]; + [txt setEditable:editable_p]; + [txt setBezeled:editable_p]; + [txt setDrawsBackground:editable_p]; + [[txt cell] setWraps:NO]; + [[txt cell] setScrollable:YES]; + [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead]; + [txt setStringValue:@""]; - // Next time around, start searching after this. - start.location = r3.location; - start.length = L - start.location; + LABEL *lab = 0; + if (label) { + lab = [self makeLabel:label]; + [self placeChild:lab on:parent]; + [lab release]; + } - // Set r2 to the start/length of this URL. - r2.length = start.location - r2.location; + [self placeChild:txt on:parent right:(label ? YES : NO)]; - // Extract the URL. - NSString *nsurl = [text substringWithRange:r2]; - const char *url = [nsurl UTF8String]; + [self bindSwitch:txt cmdline:arg]; + [txt release]; - // If this is a Wikipedia URL, make the linked text be prettier. - // - char *anchor = anchorize(url); + // Make the text field and label be the same height, whichever is taller. + if (lab) { + rect = [txt frame]; + rect.size.height = ([lab frame].size.height > [txt frame].size.height + ? [lab frame].size.height + : [txt frame].size.height); + [txt setFrame:rect]; + } - // Construct the RTF corresponding to anchor - // - const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}"; - char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10); - sprintf (rtf, fmt, url, anchor); - free (anchor); - NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)]; + // Now put a "Choose" button next to it. + // + rect.origin.x = rect.origin.y = 0; + rect.size.width = rect.size.height = 10; + NSButton *choose = [[NSButton alloc] initWithFrame:rect]; + [choose setTitle:@"Choose..."]; + [choose setBezelStyle:NSRoundedBezelStyle]; + [choose sizeToFit]; - // Insert the RTF into the NSText. - [nstext replaceCharactersInRange:r2 withRTF:rtfdata]; + [self placeChild:choose on:parent right:YES]; - int L2 = [text length]; // might have changed - start.location -= (L - L2); - L = L2; - } -} + // center the Choose button around the midpoint of the text field. + rect = [choose frame]; + rect.origin.y = ([txt frame].origin.y + + (([txt frame].size.height - rect.size.height) / 2)); + [choose setFrameOrigin:rect.origin]; -/* Makes the text up to the first comma be bold. - */ -static void -boldify (NSText *nstext) -{ - NSString *text = [nstext string]; - NSRange r = [text rangeOfString:@"," options:0]; - r.length = r.location+1; - r.location = 0; + [choose setTarget:[parent window]]; + if (dirsOnly) + [choose setAction:@selector(fileSelectorChooseDirsAction:)]; + else + [choose setAction:@selector(fileSelectorChooseAction:)]; - NSFont *font = [nstext font]; - font = [NSFont boldSystemFontOfSize:[font pointSize]]; - [nstext setFont:font range:r]; + [choose release]; +# endif // !USE_IPHONE } -static void layout_group (NSView *group, BOOL horiz_p); - +# ifndef USE_IPHONE -/* Creates an invisible NSBox (for layout purposes) to enclose the widgets - wrapped in
or in the XML. +/* Runs a modal file selector and sets the text field's value to the + selected file or directory. */ static void -make_group (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node, - BOOL horiz_p) +do_file_selector (NSTextField *txt, BOOL dirs_p) { - NSRect rect; - rect.size.width = rect.size.height = 1; - rect.origin.x = rect.origin.y = 0; - NSView *group = [[NSView alloc] initWithFrame:rect]; - traverse_children (prefs, opts, group, node); + NSOpenPanel *panel = [NSOpenPanel openPanel]; + [panel setAllowsMultipleSelection:NO]; + [panel setCanChooseFiles:!dirs_p]; + [panel setCanChooseDirectories:dirs_p]; - layout_group (group, horiz_p); + NSString *file = [txt stringValue]; + if ([file length] <= 0) { + file = NSHomeDirectory(); + if (dirs_p) + file = [file stringByAppendingPathComponent:@"Pictures"]; + } - rect.size.width = rect.size.height = 0; - NSBox *box = [[NSBox alloc] initWithFrame:rect]; - [box setTitlePosition:NSNoTitle]; - [box setBorderType:NSNoBorder]; - [box setContentViewMargins:rect.size]; - [box setContentView:group]; - [box sizeToFit]; +// NSString *dir = [file stringByDeletingLastPathComponent]; - place_child (parent, box, NO); + int result = [panel runModalForDirectory:file //dir + file:nil //[file lastPathComponent] + types:nil]; + if (result == NSOKButton) { + NSArray *files = [panel filenames]; + file = ([files count] > 0 ? [files objectAtIndex:0] : @""); + file = [file stringByAbbreviatingWithTildeInPath]; + [txt setStringValue:file]; + + // Fuck me! Just setting the value of the NSTextField does not cause + // that to end up in the preferences! + // + NSDictionary *dict = [txt infoForBinding:@"value"]; + NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"]; + NSString *path = [dict objectForKey:@"NSObservedKeyPath"]; + if ([path hasPrefix:@"values."]) // WTF. + path = [path substringFromIndex:7]; + [[prefs values] setValue:file forKey:path]; + +#if 0 + // make sure the end of the string is visible. + NSText *fe = [[txt window] fieldEditor:YES forObject:txt]; + NSRange range; + range.location = [file length]-3; + range.length = 1; + if (! [[txt window] makeFirstResponder:[txt window]]) + [[txt window] endEditingFor:nil]; +// [[txt window] makeFirstResponder:nil]; + [fe setSelectedRange:range]; +// [tv scrollRangeToVisible:range]; +// [txt setNeedsDisplay:YES]; +// [[txt cell] setNeedsDisplay:YES]; +// [txt selectAll:txt]; +#endif + } } -static void -layout_group (NSView *group, BOOL horiz_p) +/* Returns the NSTextField that is to the left of or above the NSButton. + */ +static NSTextField * +find_text_field_of_button (NSButton *button) { - NSArray *kids = [group subviews]; + NSView *parent = [button superview]; + NSArray *kids = [parent subviews]; int nkids = [kids count]; int i; - double maxx = 0, miny = 0; + NSTextField *f = 0; for (i = 0; i < nkids; i++) { - NSView *kid = [kids objectAtIndex:i]; - NSRect r = [kid frame]; - - if (horiz_p) { - maxx += r.size.width + COLUMN_SPACING; - if (r.size.height > -miny) miny = -r.size.height; - } else { - if (r.size.width > maxx) maxx = r.size.width; - miny = r.origin.y - r.size.height; + NSObject *kid = [kids objectAtIndex:i]; + if ([kid isKindOfClass:[NSTextField class]]) { + f = (NSTextField *) kid; + } else if (kid == button) { + if (! f) abort(); + return f; } } - - NSRect rect; - rect.origin.x = 0; - rect.origin.y = 0; - rect.size.width = maxx; - rect.size.height = -miny; - [group setFrame:rect]; + abort(); +} - double x = 0; - for (i = 0; i < nkids; i++) { - NSView *kid = [kids objectAtIndex:i]; - NSRect r = [kid frame]; - if (horiz_p) { - r.origin.y = rect.size.height - r.size.height; - r.origin.x = x; - x += r.size.width + COLUMN_SPACING; - } else { - r.origin.y -= miny; - } - [kid setFrame:r]; - } + +- (void) fileSelectorChooseAction:(NSObject *)arg +{ + NSButton *choose = (NSButton *) arg; + NSTextField *txt = find_text_field_of_button (choose); + do_file_selector (txt, NO); } +- (void) fileSelectorChooseDirsAction:(NSObject *)arg +{ + NSButton *choose = (NSButton *) arg; + NSTextField *txt = find_text_field_of_button (choose); + do_file_selector (txt, YES); +} -static void -make_text_controls (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, - NSView *parent, NSXMLNode *node) +#endif // !USE_IPHONE + + +- (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent { +# ifndef USE_IPHONE /* Display Text: (x) Computer name and time @@ -1296,7 +1789,6 @@ make_text_controls (NSUserDefaultsController *prefs, Bool program_p = TRUE; - NSXMLElement *node2; NSView *control; // This is how you link radio buttons together. @@ -1326,10 +1818,63 @@ make_text_controls (NSUserDefaultsController *prefs, options:nil]; [cnames release]; - bind_switch_to_preferences (prefs, matrix, @"-text-mode %", opts); + [self bindSwitch:matrix cmdline:@"-text-mode %"]; + + [self placeChild:matrix on:group]; + [self placeChild:rgroup on:group right:YES]; + + NSXMLNode *node2; + +# else // USE_IPHONE + + NSView *rgroup = parent; + NSXMLNode *node2; + + // + + node2 = [[NSXMLElement alloc] initWithName:@"select"]; + [node2 setAttributesAsDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + @"textMode", @"id", + nil]]; + + NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"]; + [node3 setAttributesAsDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + @"date", @"id", + @"-text-mode date", @"arg-set", + @"Display the date and time", @"_label", + nil]]; + [node3 setParent: node2]; + //[node3 release]; + + node3 = [[NSXMLElement alloc] initWithName:@"option"]; + [node3 setAttributesAsDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + @"text", @"id", + @"-text-mode literal", @"arg-set", + @"Display static text", @"_label", + nil]]; + [node3 setParent: node2]; + //[node3 release]; + + node3 = [[NSXMLElement alloc] initWithName:@"option"]; + [node3 setAttributesAsDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + @"url", @"id", + @"Display the contents of a URL", @"_label", + nil]]; + [node3 setParent: node2]; + //[node3 release]; + + [self makeOptionMenu:node2 on:rgroup]; + +# endif // USE_IPHONE - place_child (group, matrix, NO); - place_child (group, rgroup, YES); // node2 = [[NSXMLElement alloc] initWithName:@"string"]; @@ -1337,9 +1882,17 @@ make_text_controls (NSUserDefaultsController *prefs, [NSDictionary dictionaryWithObjectsAndKeys: @"textLiteral", @"id", @"-text-literal %", @"arg", +# ifdef USE_IPHONE + @"Text to display", @"_label", +# endif nil]]; - make_text_field (prefs, opts, rgroup, node2, YES); - [node2 release]; + [self makeTextField:node2 on:rgroup +# ifndef USE_IPHONE + withLabel:NO +# else + withLabel:YES +# endif + horizontal:NO]; // rect = [last_child(rgroup) frame]; @@ -1352,6 +1905,7 @@ make_text_controls (NSUserDefaultsController *prefs, */ +# ifndef USE_IPHONE // node2 = [[NSXMLElement alloc] initWithName:@"string"]; [node2 setAttributesAsDictionary: @@ -1359,8 +1913,9 @@ make_text_controls (NSUserDefaultsController *prefs, @"textFile", @"id", @"-text-file %", @"arg", nil]]; - make_file_selector (prefs, opts, rgroup, node2, NO, YES, NO); - [node2 release]; + [self makeFileSelector:node2 on:rgroup + dirsOnly:NO withLabel:NO editable:NO]; +# endif // !USE_IPHONE // rect = [last_child(rgroup) frame]; @@ -1370,12 +1925,21 @@ make_text_controls (NSUserDefaultsController *prefs, [NSDictionary dictionaryWithObjectsAndKeys: @"textURL", @"id", @"-text-url %", @"arg", +# ifdef USE_IPHONE + @"URL to display", @"_label", +# endif nil]]; - make_text_field (prefs, opts, rgroup, node2, YES); - [node2 release]; + [self makeTextField:node2 on:rgroup +# ifndef USE_IPHONE + withLabel:NO +# else + withLabel:YES +# endif + horizontal:NO]; // rect = [last_child(rgroup) frame]; +# ifndef USE_IPHONE if (program_p) { // node2 = [[NSXMLElement alloc] initWithName:@"string"]; @@ -1384,8 +1948,7 @@ make_text_controls (NSUserDefaultsController *prefs, @"textProgram", @"id", @"-text-program %", @"arg", nil]]; - make_text_field (prefs, opts, rgroup, node2, YES); - [node2 release]; + [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO]; } // rect = [last_child(rgroup) frame]; @@ -1440,15 +2003,15 @@ make_text_controls (NSUserDefaultsController *prefs, [box setContentView:group]; [box sizeToFit]; - place_child (parent, box, NO); + [self placeChild:box on:parent]; + +# endif // !USE_IPHONE } -static void -make_image_controls (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, - NSView *parent, NSXMLNode *node) +- (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent { +# ifndef USE_IPHONE /* [x] Grab desktop images [ ] Choose random image: @@ -1470,8 +2033,7 @@ make_image_controls (NSUserDefaultsController *prefs, @"Grab desktop images", @"_label", @"-no-grab-desktop", @"arg-unset", nil]]; - make_checkbox (prefs, opts, parent, node2); - [node2 release]; + [self makeCheckbox:node2 on:parent]; node2 = [[NSXMLElement alloc] initWithName:@"boolean"]; [node2 setAttributesAsDictionary: @@ -1480,45 +2042,231 @@ make_image_controls (NSUserDefaultsController *prefs, @"Choose random images", @"_label", @"-choose-random-images", @"arg-set", nil]]; - make_checkbox (prefs, opts, parent, node2); - [node2 release]; + [self makeCheckbox:node2 on:parent]; + + node2 = [[NSXMLElement alloc] initWithName:@"string"]; + [node2 setAttributesAsDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + @"imageDirectory", @"id", + @"Images from:", @"_label", + @"-image-directory %", @"arg", + nil]]; + [self makeFileSelector:node2 on:parent + dirsOnly:YES withLabel:YES editable:YES]; + + // Add a second, explanatory label below the file/URL selector. + + LABEL *lab2 = 0; + lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"]; + [self placeChild:lab2 on:parent]; + + // Pack it in a little tighter vertically. + NSRect r2 = [lab2 frame]; + r2.origin.x += 20; + r2.origin.y += 14; + [lab2 setFrameOrigin:r2.origin]; + [lab2 release]; +# endif // !USE_IPHONE +} + + +#pragma mark Layout for controls + + +# ifndef USE_IPHONE +static NSView * +last_child (NSView *parent) +{ + NSArray *kids = [parent subviews]; + int nkids = [kids count]; + if (nkids == 0) + return 0; + else + return [kids objectAtIndex:nkids-1]; +} +#endif // USE_IPHONE + + +/* Add the child as a subview of the parent, positioning it immediately + below or to the right of the previously-added child of that view. + */ +- (void) placeChild: +# ifdef USE_IPHONE + (NSObject *)child +# else + (NSView *)child +# endif + on:(NSView *)parent right:(BOOL)right_p +{ +# ifndef USE_IPHONE + NSRect rect = [child frame]; + NSView *last = last_child (parent); + if (!last) { + rect.origin.x = LEFT_MARGIN; + rect.origin.y = ([parent frame].size.height - rect.size.height + - LINE_SPACING); + } else if (right_p) { + rect = [last frame]; + rect.origin.x += rect.size.width + COLUMN_SPACING; + } else { + rect = [last frame]; + rect.origin.x = LEFT_MARGIN; + rect.origin.y -= [child frame].size.height + LINE_SPACING; + } + NSRect r = [child frame]; + r.origin = rect.origin; + [child setFrame:r]; + [parent addSubview:child]; + +# else // USE_IPHONE + + // Controls is an array of arrays of the controls, divided into sections. + if (! controls) + controls = [[NSMutableArray arrayWithCapacity:10] retain]; + if ([controls count] == 0) + [controls addObject: [NSMutableArray arrayWithCapacity:10]]; + NSMutableArray *current = [controls objectAtIndex:[controls count]-1]; + + if (!right_p || [current count] == 0) { + // Nothing on the current line. Add this object. + [current addObject: child]; + } else { + // Something's on the current line already. + NSObject *old = [current objectAtIndex:[current count]-1]; + if ([old isKindOfClass:[NSMutableArray class]]) { + // Already an array in this cell. Append. + NSAssert ([(NSArray *) old count] < 4, @"internal error"); + [(NSMutableArray *) old addObject: child]; + } else { + // Replace the control in this cell with an array, then app + NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil]; + [current replaceObjectAtIndex:[current count]-1 withObject:a]; + } + } +# endif // USE_IPHONE +} + + +- (void) placeChild:(NSView *)child on:(NSView *)parent +{ + [self placeChild:child on:parent right:NO]; +} + + +#ifdef USE_IPHONE + +// Start putting subsequent children in a new group, to create a new +// section on the UITableView. +// +- (void) placeSeparator +{ + if (! controls) return; + if ([controls count] == 0) return; + if ([[controls objectAtIndex:[controls count]-1] + count] > 0) + [controls addObject: [NSMutableArray arrayWithCapacity:10]]; +} +#endif // USE_IPHONE - node2 = [[NSXMLElement alloc] initWithName:@"string"]; - [node2 setAttributesAsDictionary: - [NSDictionary dictionaryWithObjectsAndKeys: - @"imageDirectory", @"id", - @"Images from:", @"_label", - @"-image-directory %", @"arg", - nil]]; - make_file_selector (prefs, opts, parent, node2, YES, NO, YES); - [node2 release]; - // Add a second, explanatory label below the file/URL selector. - NSTextField *lab2 = 0; - lab2 = make_label (@"(Local folder, or URL of RSS or Atom feed)"); - place_child (parent, lab2, NO); +/* Creates an invisible NSBox (for layout purposes) to enclose the widgets + wrapped in
or in the XML. + */ +- (void) makeGroup:(NSXMLNode *)node + on:(NSView *)parent + horizontal:(BOOL) horiz_p +{ +# ifdef USE_IPHONE + if (!horiz_p) [self placeSeparator]; + [self traverseChildren:node on:parent]; + if (!horiz_p) [self placeSeparator]; +# else // !USE_IPHONE + NSRect rect; + rect.size.width = rect.size.height = 1; + rect.origin.x = rect.origin.y = 0; + NSView *group = [[NSView alloc] initWithFrame:rect]; + [self traverseChildren:node on:group]; + + layout_group (group, horiz_p); - // Pack it in a little tighter vertically. - NSRect r2 = [lab2 frame]; - r2.origin.x += 20; - r2.origin.y += 14; - [lab2 setFrameOrigin:r2.origin]; - [lab2 release]; + rect.size.width = rect.size.height = 0; + NSBox *box = [[NSBox alloc] initWithFrame:rect]; + [box setTitlePosition:NSNoTitle]; + [box setBorderType:NSNoBorder]; + [box setContentViewMargins:rect.size]; + [box setContentView:group]; + [box sizeToFit]; + + [self placeChild:box on:parent]; +# endif // !USE_IPHONE } +#ifndef USE_IPHONE +static void +layout_group (NSView *group, BOOL horiz_p) +{ + NSArray *kids = [group subviews]; + int nkids = [kids count]; + int i; + double maxx = 0, miny = 0; + for (i = 0; i < nkids; i++) { + NSView *kid = [kids objectAtIndex:i]; + NSRect r = [kid frame]; + + if (horiz_p) { + maxx += r.size.width + COLUMN_SPACING; + if (r.size.height > -miny) miny = -r.size.height; + } else { + if (r.size.width > maxx) maxx = r.size.width; + miny = r.origin.y - r.size.height; + } + } + + NSRect rect; + rect.origin.x = 0; + rect.origin.y = 0; + rect.size.width = maxx; + rect.size.height = -miny; + [group setFrame:rect]; + + double x = 0; + for (i = 0; i < nkids; i++) { + NSView *kid = [kids objectAtIndex:i]; + NSRect r = [kid frame]; + if (horiz_p) { + r.origin.y = rect.size.height - r.size.height; + r.origin.x = x; + x += r.size.width + COLUMN_SPACING; + } else { + r.origin.y -= miny; + } + [kid setFrame:r]; + } +} +#endif // !USE_IPHONE + /* Create some kind of control corresponding to the given XML node. */ -static void -make_control (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node) +-(void)makeControl:(NSXMLNode *)node on:(NSView *)parent { NSString *name = [node name]; if ([node kind] == NSXMLCommentKind) return; + + if ([node kind] == NSXMLTextKind) { + NSString *s = [(NSString *) [node objectValue] + stringByTrimmingCharactersInSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if (! [s isEqualToString:@""]) { + NSAssert1 (0, @"unexpected text: %@", s); + } + return; + } + if ([node kind] != NSXMLElementKind) { NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node); return; @@ -1527,35 +2275,36 @@ make_control (NSUserDefaultsController *prefs, if ([name isEqualToString:@"hgroup"] || [name isEqualToString:@"vgroup"]) { - BOOL horiz_p = [name isEqualToString:@"hgroup"]; - make_group (prefs, opts, parent, node, horiz_p); + [self makeGroup:node on:parent + horizontal:[name isEqualToString:@"hgroup"]]; } else if ([name isEqualToString:@"command"]) { // do nothing: this is the "-root" business } else if ([name isEqualToString:@"boolean"]) { - make_checkbox (prefs, opts, parent, node); + [self makeCheckbox:node on:parent]; } else if ([name isEqualToString:@"string"]) { - make_text_field (prefs, opts, parent, node, NO); + [self makeTextField:node on:parent withLabel:NO horizontal:NO]; } else if ([name isEqualToString:@"file"]) { - make_file_selector (prefs, opts, parent, node, NO, NO, NO); + [self makeFileSelector:node on:parent + dirsOnly:NO withLabel:YES editable:NO]; } else if ([name isEqualToString:@"number"]) { - make_number_selector (prefs, opts, parent, node); + [self makeNumberSelector:node on:parent]; } else if ([name isEqualToString:@"select"]) { - make_option_menu (prefs, opts, parent, node); + [self makeOptionMenu:node on:parent]; } else if ([name isEqualToString:@"_description"]) { - make_desc_label (parent, node); + [self makeDescLabel:node on:parent]; } else if ([name isEqualToString:@"xscreensaver-text"]) { - make_text_controls (prefs, opts, parent, node); + [self makeTextLoaderControlBox:node on:parent]; } else if ([name isEqualToString:@"xscreensaver-image"]) { - make_image_controls (prefs, opts, parent, node); + [self makeImageLoaderControlBox:node on:parent]; } else { NSAssert1 (0, @"unknown tag: %@", name); @@ -1565,45 +2314,18 @@ make_control (NSUserDefaultsController *prefs, /* Iterate over and process the children of this XML node. */ -static void -traverse_children (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, - NSView *parent, NSXMLNode *node) +- (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent { NSArray *children = [node children]; int i, count = [children count]; for (i = 0; i < count; i++) { NSXMLNode *child = [children objectAtIndex:i]; - make_control (prefs, opts, parent, child); + [self makeControl:child on:parent]; } } -/* Handle the options on the top level tag. - */ -static void -parse_xscreensaver_tag (NSXMLNode *node) -{ - NSMutableDictionary *dict = - [NSMutableDictionary dictionaryWithObjectsAndKeys: - @"", @"name", - @"", @"_label", - nil]; - parse_attrs (dict, node); - NSString *name = [dict objectForKey:@"name"]; - NSString *label = [dict objectForKey:@"_label"]; - - if (!label) { - NSAssert1 (0, @"no _label in %@", [node name]); - return; - } - if (!name) { - NSAssert1 (0, @"no name in \"%@\"", label); - return; - } - - // #### do any callers need the "name" field for anything? -} +# ifndef USE_IPHONE /* Kludgey magic to make the window enclose the controls we created. */ @@ -1744,29 +2466,11 @@ Good: [kid setAutoresizingMask:mask]; } } +# endif // !USE_IPHONE -- (void) okClicked:(NSObject *)arg -{ - [userDefaultsController commitEditing]; - [userDefaultsController save:self]; - [NSApp endSheet:self returnCode:NSOKButton]; - [self close]; -} - -- (void) cancelClicked:(NSObject *)arg -{ - [userDefaultsController revert:self]; - [NSApp endSheet:self returnCode:NSCancelButton]; - [self close]; -} - -- (void) resetClicked:(NSObject *)arg -{ - [userDefaultsController revertToInitialValues:self]; -} - +#ifndef USE_IPHONE static NSView * wrap_with_buttons (NSWindow *window, NSView *panel) { @@ -1870,46 +2574,519 @@ wrap_with_buttons (NSWindow *window, NSView *panel) [ok setTarget:window]; [cancel setTarget:window]; [reset setTarget:window]; - [ok setAction:@selector(okClicked:)]; - [cancel setAction:@selector(cancelClicked:)]; - [reset setAction:@selector(resetClicked:)]; + [ok setAction:@selector(okAction:)]; + [cancel setAction:@selector(cancelAction:)]; + [reset setAction:@selector(resetAction:)]; return pbox; } +#endif // !USE_IPHONE /* Iterate over and process the children of the root node of the XML document. */ -static void -traverse_tree (NSUserDefaultsController *prefs, - NSWindow *window, const XrmOptionDescRec *opts, NSXMLNode *node) +- (void)traverseTree { +# ifdef USE_IPHONE + NSView *parent = [self view]; +# else + NSWindow *parent = self; +#endif + NSXMLNode *node = xml_root; + if (![[node name] isEqualToString:@"screensaver"]) { NSAssert (0, @"top level node is not "); } - parse_xscreensaver_tag (node); + saver_name = [self parseXScreenSaverTag: node]; + [saver_name retain]; +# ifndef USE_IPHONE + NSRect rect; rect.origin.x = rect.origin.y = 0; rect.size.width = rect.size.height = 1; NSView *panel = [[NSView alloc] initWithFrame:rect]; - - traverse_children (prefs, opts, panel, node); + [self traverseChildren:node on:panel]; fix_contentview_size (panel); - NSView *root = wrap_with_buttons (window, panel); - [prefs setAppliesImmediately:NO]; + NSView *root = wrap_with_buttons (parent, panel); + [userDefaultsController setAppliesImmediately:NO]; [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; - rect = [window frameRectForContentRect:[root frame]]; - [window setFrame:rect display:NO]; - [window setMinSize:rect.size]; + rect = [parent frameRectForContentRect:[root frame]]; + [parent setFrame:rect display:NO]; + [parent setMinSize:rect.size]; - [window setContentView:root]; + [parent setContentView:root]; + +# else // USE_IPHONE + + CGRect r = [parent frame]; + r.size = [[UIScreen mainScreen] bounds].size; + [parent setFrame:r]; + [self traverseChildren:node on:parent]; + +# endif // USE_IPHONE +} + + +- (void)parser:(NSXMLParser *)parser + didStartElement:(NSString *)elt + namespaceURI:(NSString *)ns + qualifiedName:(NSString *)qn + attributes:(NSDictionary *)attrs +{ + NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt]; + [e setKind:SimpleXMLElementKind]; + [e setAttributesAsDictionary:attrs]; + NSXMLElement *p = xml_parsing; + [e setParent:p]; + xml_parsing = e; + if (! xml_root) + xml_root = xml_parsing; +} + +- (void)parser:(NSXMLParser *)parser + didEndElement:(NSString *)elt + namespaceURI:(NSString *)ns + qualifiedName:(NSString *)qn +{ + NSXMLElement *p = xml_parsing; + if (! p) { + NSLog(@"extra close: %@", elt); + } else if (![[p name] isEqualToString:elt]) { + NSLog(@"%@ closed by %@", [p name], elt); + } else { + NSXMLElement *n = xml_parsing; + xml_parsing = [n parent]; + } +} + + +- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string +{ + NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"]; + [e setKind:SimpleXMLTextKind]; + NSXMLElement *p = xml_parsing; + [e setParent:p]; + [e setObjectValue: string]; +} + + +# ifdef USE_IPHONE +# ifdef USE_PICKER_VIEW + +#pragma mark UIPickerView delegate methods + +- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv +{ + return 1; // Columns +} + +- (NSInteger)pickerView:(UIPickerView *)pv + numberOfRowsInComponent:(NSInteger)column +{ + NSAssert (column == 0, @"weird column"); + NSArray *a = [picker_values objectAtIndex: [pv tag]]; + if (! a) return 0; // Too early? + return [a count]; +} + +- (CGFloat)pickerView:(UIPickerView *)pv + rowHeightForComponent:(NSInteger)column +{ + return [NSFont systemFontSize] * 1.5; // #### WHAT +} + +- (CGFloat)pickerView:(UIPickerView *)pv + widthForComponent:(NSInteger)column +{ + NSAssert (column == 0, @"weird column"); + NSArray *a = [picker_values objectAtIndex: [pv tag]]; + if (! a) return 0; // Too early? + + UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]]; + CGFloat max = 0; + for (NSArray *a2 in a) { + NSString *s = [a2 objectAtIndex:0]; + CGSize r = [s sizeWithFont:f]; + if (r.width > max) max = r.width; + } + + max *= 1.7; // WTF!! + + if (max > 320) + max = 320; + else if (max < 120) + max = 120; + + return max; + +} + + +- (NSString *)pickerView:(UIPickerView *)pv + titleForRow:(NSInteger)row + forComponent:(NSInteger)column +{ + NSAssert (column == 0, @"weird column"); + NSArray *a = [picker_values objectAtIndex: [pv tag]]; + if (! a) return 0; // Too early? + a = [a objectAtIndex:row]; + NSAssert (a, @"internal error"); + return [a objectAtIndex:0]; +} + +# endif // USE_PICKER_VIEW + + +#pragma mark UITableView delegate methods + +- (void) addResetButton +{ + [[self navigationItem] + setRightBarButtonItem: [[UIBarButtonItem alloc] + initWithTitle: @"Reset to Defaults" + style: UIBarButtonItemStyleBordered + target:self + action:@selector(resetAction:)]]; +} + + +- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o +{ + return YES; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tv { + // Number of vertically-stacked white boxes. + return [controls count]; +} + +- (NSInteger)tableView:(UITableView *)tableView + numberOfRowsInSection:(NSInteger)section +{ + // Number of lines in each vertically-stacked white box. + NSAssert (controls, @"internal error"); + return [[controls objectAtIndex:section] count]; +} + +- (NSString *)tableView:(UITableView *)tv + titleForHeaderInSection:(NSInteger)section +{ + // Titles above each vertically-stacked white box. + if (section == 0) + return [saver_name stringByAppendingString:@" Settings"]; + return nil; +} + + +- (CGFloat)tableView:(UITableView *)tv + heightForRowAtIndexPath:(NSIndexPath *)ip +{ + CGFloat h = [tv rowHeight]; + + NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]] + objectAtIndex:[ip indexAtPosition:1]]; + + if ([ctl isKindOfClass:[NSArray class]]) { + NSArray *set = (NSArray *) ctl; + switch ([set count]) { + case 4: +# ifdef LABEL_ABOVE_SLIDER + h *= 1.7; break; // label + left/slider/right: 2 1/2 lines +# endif + case 3: h *= 1.2; break; // left/slider/right: 1 1/2 lines + case 2: + if ([[set objectAtIndex:1] isKindOfClass:[UITextField class]]) + h *= 1.2; + break; + } + } else if ([ctl isKindOfClass:[UILabel class]]) { + UILabel *t = (UILabel *) ctl; + CGRect r = t.frame; + r.size.width = 250; // WTF! Black magic! + r.size.width -= LEFT_MARGIN; + [t setFrame:r]; + [t sizeToFit]; + r = t.frame; + h = r.size.height + LINE_SPACING * 3; + + } else { + CGFloat h2 = [ctl frame].size.height; + h2 += LINE_SPACING * 2; + if (h2 > h) h = h2; + } + + return h; +} + + +- (void)refreshTableView +{ + UITableView *tv = (UITableView *) [self view]; + NSMutableArray *a = [NSMutableArray arrayWithCapacity:20]; + int rows = [self numberOfSectionsInTableView:tv]; + for (int i = 0; i < rows; i++) { + int cols = [self tableView:tv numberOfRowsInSection:i]; + for (int j = 0; j < cols; j++) { + NSUInteger ip[2]; + ip[0] = i; + ip[1] = j; + [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]]; + } + } + + [tv beginUpdates]; + [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone]; + [tv endUpdates]; +} + + +- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o +{ + [NSTimer scheduledTimerWithTimeInterval: 0 + target:self + selector:@selector(refreshTableView) + userInfo:nil + repeats:NO]; +} + + +#ifndef USE_PICKER_VIEW + +- (void)updateRadioGroupCell:(UITableViewCell *)cell + button:(RadioButton *)b +{ + NSArray *item = [[b items] objectAtIndex: [b index]]; + NSString *pref_key = [item objectAtIndex:1]; + NSObject *pref_val = [item objectAtIndex:2]; + NSObject *current = [userDefaultsController objectForKey:pref_key]; + + // Convert them both to strings and compare those, so that + // we don't get screwed by int 1 versus string "1". + // Will boolean true/1 screw us here too? + // + NSString *pref_str = ([pref_val isKindOfClass:[NSString class]] + ? (NSString *) pref_val + : [(NSNumber *) pref_val stringValue]); + NSString *current_str = ([current isKindOfClass:[NSString class]] + ? (NSString *) current + : [(NSNumber *) current stringValue]); + BOOL match_p = [current_str isEqualToString:pref_str]; + + // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str); + + if (match_p) + [cell setAccessoryType:UITableViewCellAccessoryCheckmark]; + else + [cell setAccessoryType:UITableViewCellAccessoryNone]; +} + + +- (void)tableView:(UITableView *)tv + didSelectRowAtIndexPath:(NSIndexPath *)ip +{ + RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]] + objectAtIndex:[ip indexAtPosition:1]]; + if (! [ctl isKindOfClass:[RadioButton class]]) + return; + + [self radioAction:ctl]; + [self refreshTableView]; +} + + +#endif // !USE_PICKER_VIEW + + + +- (UITableViewCell *)tableView:(UITableView *)tv + cellForRowAtIndexPath:(NSIndexPath *)ip +{ +#if 0 + /* #### If we re-use cells, then clicking on a checkbox RadioButton + (in non-USE_PICKER_VIEW mode) makes all the cells disappear. + This doesn't happen if we don't re-use any cells. Oh well. + */ + NSString *id = [NSString stringWithFormat: @"%d:%d", + [ip indexAtPosition:0], + [ip indexAtPosition:1]]; + UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier: id]; + + if (cell) return cell; +#else + NSString *id = nil; + UITableViewCell *cell; +#endif + + cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault + reuseIdentifier: id] + autorelease]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + + CGRect p = [cell frame]; + CGRect r; + + p.size.height = [self tableView:tv heightForRowAtIndexPath:ip]; + [cell setFrame:p]; + + // Allocate more space to the controls on iPad screens, + // and on landscape-mode iPhones. + CGFloat ww = [tv frame].size.width; + CGFloat left_edge = (ww > 700 + ? p.size.width * 0.9 + : ww > 320 + ? p.size.width * 0.5 + : p.size.width * 0.3); + CGFloat right_edge = p.origin.x + p.size.width - LEFT_MARGIN; + + + NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]] + objectAtIndex:[ip indexAtPosition:1]]; + + if ([ctl isKindOfClass:[NSArray class]]) { + // This cell has a set of objects in it. + NSArray *set = (NSArray *) ctl; + switch ([set count]) { + case 2: + { + // With 2 elements, the first of the pair must be a label. + ctl = [set objectAtIndex: 0]; + NSAssert ([ctl isKindOfClass:[UILabel class]], @"unhandled type"); + cell.textLabel.text = [(UILabel *) ctl text]; + ctl = [set objectAtIndex: 1]; + + r = [ctl frame]; + if ([ctl isKindOfClass:[UISwitch class]]) { + // Flush right checkboxes. + ctl.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + r.size.width = 80; // Magic. + r.origin.x = right_edge - r.size.width; + } else { + // Expandable sliders. + ctl.autoresizingMask = UIViewAutoresizingFlexibleWidth; + r.origin.x = left_edge; + r.size.width = right_edge - r.origin.x; + } + r.origin.y = (p.size.height - r.size.height) / 2; + [ctl setFrame:r]; + } + break; + case 3: + case 4: + { + // With 3 elements, the first and last must be labels. + // With 4 elements, the first, second and last must be labels. + int i = 0; + UILabel *top = ([set count] == 4 + ? [set objectAtIndex: i++] + : 0); + UILabel *left = [set objectAtIndex: i++]; + NSView *mid = [set objectAtIndex: i++]; + UILabel *right = [set objectAtIndex: i++]; + NSAssert (!top || [top isKindOfClass:[UILabel class]], @"WTF"); + NSAssert ( [left isKindOfClass:[UILabel class]], @"WTF"); + NSAssert ( ![mid isKindOfClass:[UILabel class]], @"WTF"); + NSAssert ( [right isKindOfClass:[UILabel class]], @"WTF"); + + // 3 elements: control at top of cell. + // 4 elements: center the control vertically. + r = [mid frame]; +# ifdef LABEL_ABOVE_SLIDER + left_edge = LEFT_MARGIN; +# endif + r.origin.x = left_edge; + r.size.width = right_edge - r.origin.x; + r.origin.y = ([set count] == 3 + ? 8 + : (p.size.height - r.size.height) / 2); + [mid setFrame:r]; + + // Top label goes above, flush center/top. + if (top) { + // [top setFont:[[cell textLabel] font]]; // 0 point? + r.size = [[top text] sizeWithFont:[top font] + constrainedToSize: + CGSizeMake (p.size.width - LEFT_MARGIN*2, + 100000) + lineBreakMode:[top lineBreakMode]]; + r.origin.x = (p.size.width - r.size.width) / 2; + r.origin.y = 4; + [top setFrame:r]; + } + + // Left label goes under control, flush left/bottom. + r.size = [[left text] sizeWithFont:[left font] + constrainedToSize: + CGSizeMake(p.size.width - LEFT_MARGIN*2, + 100000) + lineBreakMode:[left lineBreakMode]]; + r.origin.x = [mid frame].origin.x; + r.origin.y = p.size.height - r.size.height - 4; + [left setFrame:r]; + + // Right label goes under control, flush right/bottom. + r = [right frame]; + r.size = [[right text] sizeWithFont:[right font] + constrainedToSize: + CGSizeMake(p.size.width - LEFT_MARGIN*2, + 1000000) + lineBreakMode:[right lineBreakMode]]; + r.origin.x = ([mid frame].origin.x + [mid frame].size.width - + r.size.width); + r.origin.y = [left frame].origin.y; + [right setFrame:r]; + + // Then make a box. + ctl = [[UIView alloc] initWithFrame:p]; + if (top) { +# ifdef LABEL_ABOVE_SLIDER + [ctl addSubview: top]; + top.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin| + UIViewAutoresizingFlexibleRightMargin); +# else + cell.textLabel.text = [top text]; +# endif + } + [ctl addSubview: left]; + [ctl addSubview: mid]; + [ctl addSubview: right]; + + left. autoresizingMask = UIViewAutoresizingFlexibleRightMargin; + mid. autoresizingMask = UIViewAutoresizingFlexibleWidth; + right.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + ctl. autoresizingMask = UIViewAutoresizingFlexibleWidth; + } + break; + default: + NSAssert (0, @"unhandled size"); + } + } else { + // A single view, not a pair. + + r = [ctl frame]; + r.origin.x = LEFT_MARGIN; + r.origin.y = LINE_SPACING; + r.size.width = right_edge - r.origin.x; + [ctl setFrame:r]; + + ctl.autoresizingMask = UIViewAutoresizingFlexibleWidth; + +# ifndef USE_PICKER_VIEW + if ([ctl isKindOfClass:[RadioButton class]]) + [self updateRadioGroupCell:cell button:(RadioButton *)ctl]; +# endif // USE_PICKER_VIEW + } + + [cell.contentView addSubview: ctl]; + + return cell; } +# endif // USE_IPHONE /* When this object is instantiated, it parses the XML file and creates @@ -1917,15 +3094,23 @@ traverse_tree (NSUserDefaultsController *prefs, The default size of the view is just big enough to hold them all. */ - (id)initWithXMLFile: (NSString *) xml_file - options: (const XrmOptionDescRec *) opts - controller: (NSUserDefaultsController *) prefs + options: (const XrmOptionDescRec *) _opts + controller: (NSUserDefaultsController *) _prefs + defaults: (NSDictionary *) _defs { - if (! (self = [super init])) - return 0; - - // instance variable - userDefaultsController = prefs; - [prefs retain]; +# ifndef USE_IPHONE + self = [super init]; +# else // !USE_IPHONE + self = [super initWithStyle:UITableViewStyleGrouped]; + self.title = [saver_name stringByAppendingString:@" Settings"]; +# endif // !USE_IPHONE + if (! self) return 0; + + // instance variables + opts = _opts; + defaultOptions = _defs; + userDefaultsController = _prefs; + [userDefaultsController retain]; NSURL *furl = [NSURL fileURLWithPath:xml_file]; @@ -1934,6 +3119,7 @@ traverse_tree (NSUserDefaultsController *prefs, return nil; } +#if 0 // -- the old way NSError *err = nil; NSXMLDocument *xmlDoc = [[NSXMLDocument alloc] initWithContentsOfURL:furl @@ -1948,7 +3134,27 @@ traverse_tree (NSUserDefaultsController *prefs, } traverse_tree (prefs, self, opts, [xmlDoc rootElement]); - [xmlDoc release]; +#endif /* 0 */ + + + NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithContentsOfURL:furl]; + if (!xmlDoc) { + NSAssert1 (0, @"XML Error: %@", xml_file); + return nil; + } + [xmlDoc setDelegate:self]; + if (! [xmlDoc parse]) { + NSError *err = [xmlDoc parserError]; + NSAssert2 (0, @"XML Error: %@: %@", xml_file, err); + return nil; + } + + [self traverseTree]; + xml_root = 0; + +# ifdef USE_IPHONE + [self addResetButton]; +# endif return self; } @@ -1956,7 +3162,16 @@ traverse_tree (NSUserDefaultsController *prefs, - (void) dealloc { + [saver_name release]; [userDefaultsController release]; +# ifdef USE_IPHONE + [controls release]; + [pref_keys release]; + [pref_ctls release]; +# ifdef USE_PICKER_VIEW + [picker_values release]; +# endif +# endif [super dealloc]; }