1 /* xscreensaver, Copyright (c) 2006-2011 Jamie Zawinski <jwz@jwz.org>
3 * Permission to use, copy, modify, distribute, and sell this software and its
4 * documentation for any purpose is hereby granted without fee, provided that
5 * the above copyright notice appear in all copies and that both that
6 * copyright notice and this permission notice appear in supporting
7 * documentation. No representations are made about the suitability of this
8 * software for any purpose. It is provided "as is" without express or
12 /* XScreenSaver uses XML files to describe the user interface for configuring
13 the various screen savers. These files live in .../hacks/config/ and
14 say relatively high level things like: "there should be a checkbox
15 labelled "Leave Trails", and when it is checked, add the option '-trails'
16 to the command line when launching the program."
18 This code reads that XML and constructs a Cocoa interface from it.
19 The Cocoa controls are hooked up to NSUserDefaultsController to save
20 those settings into the MacOS preferences system. The Cocoa preferences
21 names are the same as the resource names specified in the screenhack's
22 'options' array (we use that array to map the command line switches
23 specified in the XML to the resource names to use).
26 #import "XScreenSaverConfigSheet.h"
29 #import "InvertedSlider.h"
30 #import <Foundation/NSXMLDocument.h>
32 @implementation XScreenSaverConfigSheet
34 #define LEFT_MARGIN 20 // left edge of window
35 #define COLUMN_SPACING 10 // gap between e.g. labels and text fields
36 #define LEFT_LABEL_WIDTH 70 // width of all left labels
37 #define LINE_SPACING 10 // leading between each line
39 // redefine these since they don't work when not inside an ObjC method
44 #define NSAssert(CC,S) do { if (!(CC)) { NSLog(S); }} while(0)
45 #define NSAssert1(CC,S,A) do { if (!(CC)) { NSLog(S,A); }} while(0)
46 #define NSAssert2(CC,S,A,B) do { if (!(CC)) { NSLog(S,A,B); }} while(0)
47 #define NSAssert3(CC,S,A,B,C) do { if (!(CC)) { NSLog(S,A,B,C); }} while(0)
50 /* Given a command-line option, returns the corresponding resource name.
51 Any arguments in the switch string are ignored (e.g., "-foo x").
54 switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts,
59 NSAssert(cmdline_switch, @"cmdline switch is null");
60 if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
61 encoding:NSUTF8StringEncoding]) {
62 NSAssert1(0, @"unable to convert %@", cmdline_switch);
65 char *s = strpbrk(buf, " \t\r\n");
69 while (*tail && (*tail == ' ' || *tail == '\t'))
73 while (opts[0].option) {
74 if (!strcmp (opts[0].option, buf)) {
77 if (opts[0].argKind == XrmoptionNoArg) {
79 NSAssert1 (0, @"expected no args to switch: \"%@\"",
84 NSAssert1 (0, @"expected args to switch: \"%@\"",
91 ? [NSString stringWithCString:ret
92 encoding:NSUTF8StringEncoding]
95 const char *res = opts[0].specifier;
96 while (*res && (*res == '.' || *res == '*'))
98 return [NSString stringWithCString:res
99 encoding:NSUTF8StringEncoding];
104 NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
109 /* Connects a control (checkbox, etc) to the corresponding preferences key.
112 bind_resource_to_preferences (NSUserDefaultsController *prefs,
115 const XrmOptionDescRec *opts)
117 NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
119 : ([control isKindOfClass:[NSMatrix class]]
124 withKeyPath:[@"values." stringByAppendingString: pref_key]
128 NSObject *def = [[prefs defaults] objectForKey:pref_key];
129 NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
130 s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
131 s = [NSString stringWithFormat:@"%@ = \"%@\"", s, def];
132 s = [s stringByPaddingToLength:28 withString:@" " startingAtIndex:0];
133 NSLog (@"%@ %@/%@", s, [def class], [control class]);
138 bind_switch_to_preferences (NSUserDefaultsController *prefs,
140 NSString *cmdline_switch,
141 const XrmOptionDescRec *opts)
143 NSString *pref_key = switch_to_resource (cmdline_switch, opts, 0);
144 bind_resource_to_preferences (prefs, control, pref_key, opts);
148 /* Parse the attributes of an XML tag into a dictionary.
149 For input, the dictionary should have as attributes the keys, each
150 with @"" as their value.
151 On output, the dictionary will set the keys to the values specified,
152 and keys that were not specified will not be present in the dictionary.
153 Warnings are printed if there are duplicate or unknown attributes.
156 parse_attrs (NSMutableDictionary *dict, NSXMLNode *node)
158 NSArray *attrs = [(NSXMLElement *) node attributes];
159 int n = [attrs count];
162 // For each key in the dictionary, fill in the dict with the corresponding
163 // value. The value @"" is assumed to mean "un-set". Issue a warning if
164 // an attribute is specified twice.
166 for (i = 0; i < n; i++) {
167 NSXMLNode *attr = [attrs objectAtIndex:i];
168 NSString *key = [attr name];
169 NSString *val = [attr objectValue];
170 NSString *old = [dict objectForKey:key];
173 NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
174 } else if ([old length] != 0) {
175 NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
177 [dict setValue:val forKey:key];
181 // Remove from the dictionary any keys whose value is still @"",
182 // meaning there was no such attribute specified.
184 NSArray *keys = [dict allKeys];
186 for (i = 0; i < n; i++) {
187 NSString *key = [keys objectAtIndex:i];
188 NSString *val = [dict objectForKey:key];
189 if ([val length] == 0)
190 [dict removeObjectForKey:key];
195 /* Creates a label: an un-editable NSTextField displaying the given text.
198 make_label (NSString *text)
201 rect.origin.x = rect.origin.y = 0;
202 rect.size.width = rect.size.height = 10;
203 NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
204 [lab setSelectable:NO];
205 [lab setEditable:NO];
207 [lab setDrawsBackground:NO];
208 [lab setStringValue:text];
215 last_child (NSView *parent)
217 NSArray *kids = [parent subviews];
218 int nkids = [kids count];
222 return [kids objectAtIndex:nkids-1];
226 /* Add the child as a subview of the parent, positioning it immediately
227 below or to the right of the previously-added child of that view.
230 place_child (NSView *parent, NSView *child, BOOL right_p)
232 NSRect rect = [child frame];
233 NSView *last = last_child (parent);
235 rect.origin.x = LEFT_MARGIN;
236 rect.origin.y = [parent frame].size.height - rect.size.height
238 } else if (right_p) {
240 rect.origin.x += rect.size.width + COLUMN_SPACING;
243 rect.origin.x = LEFT_MARGIN;
244 rect.origin.y -= [child frame].size.height + LINE_SPACING;
246 [child setFrameOrigin:rect.origin];
247 [parent addSubview:child];
251 static void traverse_children (NSUserDefaultsController *,
252 const XrmOptionDescRec *,
253 NSView *, NSXMLNode *);
256 /* Creates the checkbox (NSButton) described by the given XML node.
259 make_checkbox (NSUserDefaultsController *prefs,
260 const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node)
262 NSMutableDictionary *dict =
263 [NSMutableDictionary dictionaryWithObjectsAndKeys:
269 parse_attrs (dict, node);
270 NSString *label = [dict objectForKey:@"_label"];
271 NSString *arg_set = [dict objectForKey:@"arg-set"];
272 NSString *arg_unset = [dict objectForKey:@"arg-unset"];
275 NSAssert1 (0, @"no _label in %@", [node name]);
278 if (!arg_set && !arg_unset) {
279 NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"",
282 if (arg_set && arg_unset) {
283 NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"",
287 // sanity-check the choice of argument names.
289 if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
290 [arg_set hasPrefix:@"--no-"]))
291 NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
293 if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
294 ![arg_unset hasPrefix:@"--no-"]))
295 NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
299 rect.origin.x = rect.origin.y = 0;
300 rect.size.width = rect.size.height = 10;
302 NSButton *button = [[NSButton alloc] initWithFrame:rect];
303 [button setButtonType:([[node name] isEqualToString:@"radio"]
306 [button setTitle:label];
308 place_child (parent, button, NO);
310 bind_switch_to_preferences (prefs, button,
311 (arg_set ? arg_set : arg_unset),
317 /* Creates the NSTextField described by the given XML node.
320 make_text_field (NSUserDefaultsController *prefs,
321 const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node,
324 NSMutableDictionary *dict =
325 [NSMutableDictionary dictionaryWithObjectsAndKeys:
330 parse_attrs (dict, node);
331 NSString *label = [dict objectForKey:@"_label"];
332 NSString *arg = [dict objectForKey:@"arg"];
334 if (!label && !no_label_p) {
335 NSAssert1 (0, @"no _label in %@", [node name]);
339 NSAssert1 (arg, @"no arg in %@", label);
342 rect.origin.x = rect.origin.y = 0;
343 rect.size.width = rect.size.height = 10;
345 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
347 // make the default size be around 30 columns; a typical value for
348 // these text fields is "xscreensaver-text --cols 40".
350 [txt setStringValue:@"123456789 123456789 123456789 "];
352 [[txt cell] setWraps:NO];
353 [[txt cell] setScrollable:YES];
354 [txt setStringValue:@""];
357 NSTextField *lab = make_label (label);
358 place_child (parent, lab, NO);
362 place_child (parent, txt, (label ? YES : NO));
364 bind_switch_to_preferences (prefs, txt, arg, opts);
369 /* Creates the NSTextField described by the given XML node,
370 and hooks it up to a Choose button and a file selector widget.
373 make_file_selector (NSUserDefaultsController *prefs,
374 const XrmOptionDescRec *opts,
375 NSView *parent, NSXMLNode *node,
378 BOOL editable_text_p)
380 NSMutableDictionary *dict =
381 [NSMutableDictionary dictionaryWithObjectsAndKeys:
386 parse_attrs (dict, node);
387 NSString *label = [dict objectForKey:@"_label"];
388 NSString *arg = [dict objectForKey:@"arg"];
390 if (!label && !no_label_p) {
391 NSAssert1 (0, @"no _label in %@", [node name]);
395 NSAssert1 (arg, @"no arg in %@", label);
398 rect.origin.x = rect.origin.y = 0;
399 rect.size.width = rect.size.height = 10;
401 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
403 // make the default size be around 20 columns.
405 [txt setStringValue:@"123456789 123456789 "];
407 [txt setSelectable:YES];
408 [txt setEditable:editable_text_p];
409 [txt setBezeled:editable_text_p];
410 [txt setDrawsBackground:editable_text_p];
411 [[txt cell] setWraps:NO];
412 [[txt cell] setScrollable:YES];
413 [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
414 [txt setStringValue:@""];
416 NSTextField *lab = 0;
418 lab = make_label (label);
419 place_child (parent, lab, NO);
423 place_child (parent, txt, (label ? YES : NO));
425 bind_switch_to_preferences (prefs, txt, arg, opts);
428 // Make the text field and label be the same height, whichever is taller.
431 rect.size.height = ([lab frame].size.height > [txt frame].size.height
432 ? [lab frame].size.height
433 : [txt frame].size.height);
437 // Now put a "Choose" button next to it.
439 rect.origin.x = rect.origin.y = 0;
440 rect.size.width = rect.size.height = 10;
441 NSButton *choose = [[NSButton alloc] initWithFrame:rect];
442 [choose setTitle:@"Choose..."];
443 [choose setBezelStyle:NSRoundedBezelStyle];
446 place_child (parent, choose, YES);
448 // center the Choose button around the midpoint of the text field.
449 rect = [choose frame];
450 rect.origin.y = ([txt frame].origin.y +
451 (([txt frame].size.height - rect.size.height) / 2));
452 [choose setFrameOrigin:rect.origin];
454 [choose setTarget:[parent window]];
456 [choose setAction:@selector(chooseClickedDirs:)];
458 [choose setAction:@selector(chooseClicked:)];
464 /* Runs a modal file selector and sets the text field's value to the
465 selected file or directory.
468 do_file_selector (NSTextField *txt, BOOL dirs_p)
470 NSOpenPanel *panel = [NSOpenPanel openPanel];
471 [panel setAllowsMultipleSelection:NO];
472 [panel setCanChooseFiles:!dirs_p];
473 [panel setCanChooseDirectories:dirs_p];
475 NSString *file = [txt stringValue];
476 if ([file length] <= 0) {
477 file = NSHomeDirectory();
479 file = [file stringByAppendingPathComponent:@"Pictures"];
482 // NSString *dir = [file stringByDeletingLastPathComponent];
484 int result = [panel runModalForDirectory:file //dir
485 file:nil //[file lastPathComponent]
487 if (result == NSOKButton) {
488 NSArray *files = [panel filenames];
489 file = ([files count] > 0 ? [files objectAtIndex:0] : @"");
490 file = [file stringByAbbreviatingWithTildeInPath];
491 [txt setStringValue:file];
493 // Fuck me! Just setting the value of the NSTextField does not cause
494 // that to end up in the preferences!
496 NSDictionary *dict = [txt infoForBinding:@"value"];
497 NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
498 NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
499 if ([path hasPrefix:@"values."]) // WTF.
500 path = [path substringFromIndex:7];
501 [[prefs values] setValue:file forKey:path];
504 // make sure the end of the string is visible.
505 NSText *fe = [[txt window] fieldEditor:YES forObject:txt];
507 range.location = [file length]-3;
509 if (! [[txt window] makeFirstResponder:[txt window]])
510 [[txt window] endEditingFor:nil];
511 // [[txt window] makeFirstResponder:nil];
512 [fe setSelectedRange:range];
513 // [tv scrollRangeToVisible:range];
514 // [txt setNeedsDisplay:YES];
515 // [[txt cell] setNeedsDisplay:YES];
516 // [txt selectAll:txt];
521 /* Returns the NSTextField that is to the left of or above the NSButton.
524 find_text_field_of_button (NSButton *button)
526 NSView *parent = [button superview];
527 NSArray *kids = [parent subviews];
528 int nkids = [kids count];
531 for (i = 0; i < nkids; i++) {
532 NSObject *kid = [kids objectAtIndex:i];
533 if ([kid isKindOfClass:[NSTextField class]]) {
534 f = (NSTextField *) kid;
535 } else if (kid == button) {
544 - (void) chooseClicked:(NSObject *)arg
546 NSButton *choose = (NSButton *) arg;
547 NSTextField *txt = find_text_field_of_button (choose);
548 do_file_selector (txt, NO);
551 - (void) chooseClickedDirs:(NSObject *)arg
553 NSButton *choose = (NSButton *) arg;
554 NSTextField *txt = find_text_field_of_button (choose);
555 do_file_selector (txt, YES);
559 /* Creates the number selection control described by the given XML node.
560 If "type=slider", it's an NSSlider.
561 If "type=spinbutton", it's a text field with up/down arrows next to it.
564 make_number_selector (NSUserDefaultsController *prefs,
565 const XrmOptionDescRec *opts,
566 NSView *parent, NSXMLNode *node)
568 NSMutableDictionary *dict =
569 [NSMutableDictionary dictionaryWithObjectsAndKeys:
581 parse_attrs (dict, node);
582 NSString *label = [dict objectForKey:@"_label"];
583 NSString *low_label = [dict objectForKey:@"_low-label"];
584 NSString *high_label = [dict objectForKey:@"_high-label"];
585 NSString *type = [dict objectForKey:@"type"];
586 NSString *arg = [dict objectForKey:@"arg"];
587 NSString *low = [dict objectForKey:@"low"];
588 NSString *high = [dict objectForKey:@"high"];
589 NSString *def = [dict objectForKey:@"default"];
590 NSString *cvt = [dict objectForKey:@"convert"];
592 NSAssert1 (arg, @"no arg in %@", label);
593 NSAssert1 (type, @"no type in %@", label);
596 NSAssert1 (0, @"no low in %@", [node name]);
600 NSAssert1 (0, @"no high in %@", [node name]);
604 NSAssert1 (0, @"no default in %@", [node name]);
607 if (cvt && ![cvt isEqualToString:@"invert"]) {
608 NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
612 // If either the min or max field contains a decimal point, then this
613 // option may have a floating point value; otherwise, it is constrained
616 NSCharacterSet *dot =
617 [NSCharacterSet characterSetWithCharactersInString:@"."];
618 BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
619 [high rangeOfCharacterFromSet:dot].location != NSNotFound);
621 if ([type isEqualToString:@"slider"]) {
624 rect.origin.x = rect.origin.y = 0;
625 rect.size.width = 150;
626 rect.size.height = 23; // apparent min height for slider with ticks...
629 slider = [[InvertedSlider alloc] initWithFrame:rect];
631 slider = [[NSSlider alloc] initWithFrame:rect];
633 [slider setMaxValue:[high doubleValue]];
634 [slider setMinValue:[low doubleValue]];
636 int range = [slider maxValue] - [slider minValue] + 1;
639 while (range2 > max_ticks)
642 // If we have elided ticks, leave it at the max number of ticks.
643 if (range != range2 && range2 < max_ticks)
646 // If it's a float, always display the max number of ticks.
647 if (float_p && range2 < max_ticks)
650 [slider setNumberOfTickMarks:range2];
652 [slider setAllowsTickMarkValuesOnly:
653 (range == range2 && // we are showing the actual number of ticks
654 !float_p)]; // and we want integer results
656 // #### Note: when the slider's range is large enough that we aren't
657 // showing all possible ticks, the slider's value is not constrained
658 // to be an integer, even though it should be...
659 // Maybe we need to use a value converter or something?
662 NSTextField *lab = make_label (label);
663 place_child (parent, lab, NO);
668 NSTextField *lab = make_label (low_label);
669 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
670 [lab setAlignment:1]; // right aligned
672 if (rect.size.width < LEFT_LABEL_WIDTH)
673 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
674 rect.size.height = [slider frame].size.height;
676 place_child (parent, lab, NO);
680 place_child (parent, slider, (low_label ? YES : NO));
683 rect = [slider frame];
684 if (rect.origin.x < LEFT_LABEL_WIDTH)
685 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
686 [slider setFrame:rect];
690 NSTextField *lab = make_label (high_label);
691 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
693 rect.size.height = [slider frame].size.height;
695 place_child (parent, lab, YES);
699 bind_switch_to_preferences (prefs, slider, arg, opts);
702 } else if ([type isEqualToString:@"spinbutton"]) {
705 NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
708 NSAssert1 (!low_label,
709 @"low-label not allowed in spinbutton \"%@\"", [node name]);
710 NSAssert1 (!high_label,
711 @"high-label not allowed in spinbutton \"%@\"", [node name]);
712 NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
716 rect.origin.x = rect.origin.y = 0;
717 rect.size.width = rect.size.height = 10;
719 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
720 [txt setStringValue:@"0000.0"];
722 [txt setStringValue:@""];
725 NSTextField *lab = make_label (label);
726 //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
727 [lab setAlignment:1]; // right aligned
729 if (rect.size.width < LEFT_LABEL_WIDTH)
730 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
731 rect.size.height = [txt frame].size.height;
733 place_child (parent, lab, NO);
737 place_child (parent, txt, (label ? YES : NO));
741 if (rect.origin.x < LEFT_LABEL_WIDTH)
742 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
746 rect.size.width = rect.size.height = 10;
747 NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
749 place_child (parent, step, YES);
751 rect.origin.x -= COLUMN_SPACING; // this one goes close
752 rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
753 [step setFrame:rect];
755 [step setMinValue:[low doubleValue]];
756 [step setMaxValue:[high doubleValue]];
757 [step setAutorepeat:YES];
758 [step setValueWraps:NO];
760 double range = [high doubleValue] - [low doubleValue];
762 [step setIncrement:range / 10.0];
763 else if (range >= 500)
764 [step setIncrement:range / 100.0];
766 [step setIncrement:1.0];
768 NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
769 [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
770 [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
771 [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]];
772 [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
773 [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
774 [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
776 [fmt setGeneratesDecimalNumbers:float_p];
777 [[txt cell] setFormatter:fmt];
780 bind_switch_to_preferences (prefs, step, arg, opts);
781 bind_switch_to_preferences (prefs, txt, arg, opts);
787 NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
793 set_menu_item_object (NSMenuItem *item, NSObject *obj)
795 /* If the object associated with this menu item looks like a boolean,
796 store an NSNumber instead of an NSString, since that's what
797 will be in the preferences (due to similar logic in PrefsReader).
799 if ([obj isKindOfClass:[NSString class]]) {
800 NSString *string = (NSString *) obj;
801 if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
802 NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
803 obj = [NSNumber numberWithBool:YES];
804 else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
805 NSOrderedSame == [string caseInsensitiveCompare:@"no"])
806 obj = [NSNumber numberWithBool:NO];
811 [item setRepresentedObject:obj];
812 //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
816 /* Creates the popup menu described by the given XML node (and its children).
819 make_option_menu (NSUserDefaultsController *prefs,
820 const XrmOptionDescRec *opts,
821 NSView *parent, NSXMLNode *node)
823 NSArray *children = [node children];
824 int i, count = [children count];
827 NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
831 // get the "id" attribute off the <select> tag.
833 NSMutableDictionary *dict =
834 [NSMutableDictionary dictionaryWithObjectsAndKeys:
837 parse_attrs (dict, node);
840 rect.origin.x = rect.origin.y = 0;
841 rect.size.width = 10;
842 rect.size.height = 10;
844 // #### "Build and Analyze" says that all of our widgets leak, because it
845 // seems to not realize that place_child -> addSubview retains them.
846 // Not sure what to do to make these warnings go away.
848 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
851 NSMenuItem *def_item = nil;
854 NSString *menu_key = nil; // the resource key used by items in this menu
856 for (i = 0; i < count; i++) {
857 NSXMLNode *child = [children objectAtIndex:i];
859 if ([child kind] == NSXMLCommentKind)
861 if ([child kind] != NSXMLElementKind) {
862 NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
866 // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
868 NSMutableDictionary *dict2 =
869 [NSMutableDictionary dictionaryWithObjectsAndKeys:
874 parse_attrs (dict2, child);
875 NSString *label = [dict2 objectForKey:@"_label"];
876 NSString *arg_set = [dict2 objectForKey:@"arg-set"];
879 NSAssert1 (0, @"no _label in %@", [child name]);
883 // create the menu item (and then get a pointer to it)
884 [popup addItemWithTitle:label];
885 NSMenuItem *item = [popup itemWithTitle:label];
888 NSString *this_val = NULL;
889 NSString *this_key = switch_to_resource (arg_set, opts, &this_val);
890 NSAssert1 (this_val, @"this_val null for %@", arg_set);
891 if (menu_key && ![menu_key isEqualToString:this_key])
893 @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
894 menu_key, this_key, this_val);
898 /* If this menu has the cmd line "-mode foo" then set this item's
899 value to "foo" (the menu itself will be bound to e.g. "modeString")
901 set_menu_item_object (item, this_val);
904 // no arg-set -- only one menu item can be missing that.
905 NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
909 /* make sure the menu button has room for the text of this item,
910 and remember the greatest width it has reached.
912 [popup setTitle:label];
914 NSRect r = [popup frame];
915 if (r.size.width > max_width) max_width = r.size.width;
919 NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
923 /* We've added all of the menu items. If there was an item with no
924 command-line switch, then it's the item that represents the default
925 value. Now we must bind to that item as well... (We have to bind
926 this one late, because if it was the first item, then we didn't
927 yet know what resource was associated with this menu.)
930 NSDictionary *defs = [prefs initialValues];
931 NSObject *def_obj = [defs objectForKey:menu_key];
934 @"no default value for resource \"%@\" in menu item \"%@\"",
935 menu_key, [def_item title]);
937 set_menu_item_object (def_item, def_obj);
940 /* Finish tweaking the menu button itself.
943 [popup setTitle:[def_item title]];
944 NSRect r = [popup frame];
945 r.size.width = max_width;
947 place_child (parent, popup, NO);
949 bind_resource_to_preferences (prefs, popup, menu_key, opts);
954 static NSString *unwrap (NSString *);
955 static void hreffify (NSText *);
956 static void boldify (NSText *);
958 /* Creates an uneditable, wrapping NSTextField to display the given
959 text enclosed by <description> ... </description> in the XML.
962 make_desc_label (NSView *parent, NSXMLNode *node)
964 NSString *text = nil;
965 NSArray *children = [node children];
966 int i, count = [children count];
968 for (i = 0; i < count; i++) {
969 NSXMLNode *child = [children objectAtIndex:i];
970 NSString *s = [child objectValue];
972 text = [text stringByAppendingString:s];
977 text = unwrap (text);
979 NSRect rect = [parent frame];
980 rect.origin.x = rect.origin.y = 0;
981 rect.size.width = 200;
982 rect.size.height = 50; // sized later
983 NSText *lab = [[NSText alloc] initWithFrame:rect];
984 [lab setEditable:NO];
985 [lab setDrawsBackground:NO];
986 [lab setHorizontallyResizable:YES];
987 [lab setVerticallyResizable:YES];
988 [lab setString:text];
993 place_child (parent, lab, NO);
998 unwrap (NSString *text)
1000 // Unwrap lines: delete \n but do not delete \n\n.
1002 NSArray *lines = [text componentsSeparatedByString:@"\n"];
1003 int nlines = [lines count];
1007 text = @"\n"; // start with one blank line
1009 // skip trailing blank lines in file
1010 for (i = nlines-1; i > 0; i--) {
1011 NSString *s = (NSString *) [lines objectAtIndex:i];
1017 // skip leading blank lines in file
1018 for (i = 0; i < nlines; i++) {
1019 NSString *s = (NSString *) [lines objectAtIndex:i];
1026 for (; i < nlines; i++) {
1027 NSString *s = (NSString *) [lines objectAtIndex:i];
1028 if ([s length] == 0) {
1029 text = [text stringByAppendingString:@"\n\n"];
1031 } else if ([s characterAtIndex:0] == ' ' ||
1032 [s hasPrefix:@"Copyright "] ||
1033 [s hasPrefix:@"http://"]) {
1034 // don't unwrap if the following line begins with whitespace,
1035 // or with the word "Copyright", or if it begins with a URL.
1037 text = [text stringByAppendingString:@"\n"];
1038 text = [text stringByAppendingString:s];
1043 text = [text stringByAppendingString:@" "];
1044 text = [text stringByAppendingString:s];
1055 anchorize (const char *url)
1057 const char *wiki = "http://en.wikipedia.org/wiki/";
1058 const char *math = "http://mathworld.wolfram.com/";
1059 if (!strncmp (wiki, url, strlen(wiki))) {
1060 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
1061 strcpy (anchor, "Wikipedia: \"");
1062 const char *in = url + strlen(wiki);
1063 char *out = anchor + strlen(anchor);
1067 } else if (*in == '#') {
1070 } else if (*in == '%') {
1076 sscanf (hex, "%x", &n);
1088 } else if (!strncmp (math, url, strlen(math))) {
1089 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
1090 strcpy (anchor, "MathWorld: \"");
1091 const char *start = url + strlen(wiki);
1092 const char *in = start;
1093 char *out = anchor + strlen(anchor);
1097 } else if (in != start && *in >= 'A' && *in <= 'Z') {
1100 } else if (!strncmp (in, ".htm", 4)) {
1112 return strdup (url);
1117 /* Converts any http: URLs in the given text field to clickable links.
1120 hreffify (NSText *nstext)
1122 NSString *text = [nstext string];
1123 [nstext setRichText:YES];
1125 int L = [text length];
1126 NSRange start; // range is start-of-search to end-of-string
1129 while (start.location < L) {
1131 // Find the beginning of a URL...
1133 NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
1134 if (r2.location == NSNotFound)
1137 // Next time around, start searching after this.
1138 start.location = r2.location + r2.length;
1139 start.length = L - start.location;
1141 // Find the end of a URL (whitespace or EOF)...
1143 NSRange r3 = [text rangeOfCharacterFromSet:
1144 [NSCharacterSet whitespaceAndNewlineCharacterSet]
1145 options:0 range:start];
1146 if (r3.location == NSNotFound) // EOF
1147 r3.location = L, r3.length = 0;
1149 // Next time around, start searching after this.
1150 start.location = r3.location;
1151 start.length = L - start.location;
1153 // Set r2 to the start/length of this URL.
1154 r2.length = start.location - r2.location;
1157 NSString *nsurl = [text substringWithRange:r2];
1158 const char *url = [nsurl UTF8String];
1160 // If this is a Wikipedia URL, make the linked text be prettier.
1162 char *anchor = anchorize(url);
1164 // Construct the RTF corresponding to <A HREF="url">anchor</A>
1166 const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
1167 char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
1168 sprintf (rtf, fmt, url, anchor);
1170 NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
1172 // Insert the RTF into the NSText.
1173 [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
1175 int L2 = [text length]; // might have changed
1176 start.location -= (L - L2);
1181 /* Makes the text up to the first comma be bold.
1184 boldify (NSText *nstext)
1186 NSString *text = [nstext string];
1187 NSRange r = [text rangeOfString:@"," options:0];
1188 r.length = r.location+1;
1191 NSFont *font = [nstext font];
1192 font = [NSFont boldSystemFontOfSize:[font pointSize]];
1193 [nstext setFont:font range:r];
1197 static void layout_group (NSView *group, BOOL horiz_p);
1200 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
1201 wrapped in <hgroup> or <vgroup> in the XML.
1204 make_group (NSUserDefaultsController *prefs,
1205 const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node,
1209 rect.size.width = rect.size.height = 1;
1210 rect.origin.x = rect.origin.y = 0;
1211 NSView *group = [[NSView alloc] initWithFrame:rect];
1212 traverse_children (prefs, opts, group, node);
1214 layout_group (group, horiz_p);
1216 rect.size.width = rect.size.height = 0;
1217 NSBox *box = [[NSBox alloc] initWithFrame:rect];
1218 [box setTitlePosition:NSNoTitle];
1219 [box setBorderType:NSNoBorder];
1220 [box setContentViewMargins:rect.size];
1221 [box setContentView:group];
1224 place_child (parent, box, NO);
1229 layout_group (NSView *group, BOOL horiz_p)
1231 NSArray *kids = [group subviews];
1232 int nkids = [kids count];
1234 double maxx = 0, miny = 0;
1235 for (i = 0; i < nkids; i++) {
1236 NSView *kid = [kids objectAtIndex:i];
1237 NSRect r = [kid frame];
1240 maxx += r.size.width + COLUMN_SPACING;
1241 if (r.size.height > -miny) miny = -r.size.height;
1243 if (r.size.width > maxx) maxx = r.size.width;
1244 miny = r.origin.y - r.size.height;
1251 rect.size.width = maxx;
1252 rect.size.height = -miny;
1253 [group setFrame:rect];
1256 for (i = 0; i < nkids; i++) {
1257 NSView *kid = [kids objectAtIndex:i];
1258 NSRect r = [kid frame];
1260 r.origin.y = rect.size.height - r.size.height;
1262 x += r.size.width + COLUMN_SPACING;
1272 make_text_controls (NSUserDefaultsController *prefs,
1273 const XrmOptionDescRec *opts,
1274 NSView *parent, NSXMLNode *node)
1278 (x) Computer name and time
1279 ( ) Text [__________________________]
1280 ( ) Text file [_________________] [Choose]
1281 ( ) URL [__________________________]
1282 ( ) Shell Cmd [__________________________]
1284 textMode -text-mode date
1285 textMode -text-mode literal textLiteral -text-literal %
1286 textMode -text-mode file textFile -text-file %
1287 textMode -text-mode url textURL -text-url %
1288 textMode -text-mode program textProgram -text-program %
1291 rect.size.width = rect.size.height = 1;
1292 rect.origin.x = rect.origin.y = 0;
1293 NSView *group = [[NSView alloc] initWithFrame:rect];
1294 NSView *rgroup = [[NSView alloc] initWithFrame:rect];
1296 Bool program_p = TRUE;
1299 NSXMLElement *node2;
1302 // This is how you link radio buttons together.
1304 NSButtonCell *proto = [[NSButtonCell alloc] init];
1305 [proto setButtonType:NSRadioButton];
1307 rect.origin.x = rect.origin.y = 0;
1308 rect.size.width = rect.size.height = 10;
1309 NSMatrix *matrix = [[NSMatrix alloc]
1311 mode:NSRadioModeMatrix
1313 numberOfRows: 4 + (program_p ? 1 : 0)
1315 [matrix setAllowsEmptySelection:NO];
1317 NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil];
1318 [cnames addObject:@"Computer name and time"];
1319 [cnames addObject:@"Text"];
1320 [cnames addObject:@"File"];
1321 [cnames addObject:@"URL"];
1322 if (program_p) [cnames addObject:@"Shell Cmd"];
1323 [matrix bind:@"content"
1325 withKeyPath:@"arrangedObjects"
1329 bind_switch_to_preferences (prefs, matrix, @"-text-mode %", opts);
1331 place_child (group, matrix, NO);
1332 place_child (group, rgroup, YES);
1334 // <string id="textLiteral" _label="" arg-set="-text-literal %"/>
1335 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1336 [node2 setAttributesAsDictionary:
1337 [NSDictionary dictionaryWithObjectsAndKeys:
1338 @"textLiteral", @"id",
1339 @"-text-literal %", @"arg",
1341 make_text_field (prefs, opts, rgroup, node2, YES);
1344 // rect = [last_child(rgroup) frame];
1346 /* // trying to make the text fields be enabled only when the checkbox is on..
1347 control = last_child (rgroup);
1348 [control bind:@"enabled"
1349 toObject:[matrix cellAtRow:1 column:0]
1350 withKeyPath:@"value"
1355 // <file id="textFile" _label="" arg-set="-text-file %"/>
1356 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1357 [node2 setAttributesAsDictionary:
1358 [NSDictionary dictionaryWithObjectsAndKeys:
1360 @"-text-file %", @"arg",
1362 make_file_selector (prefs, opts, rgroup, node2, NO, YES, NO);
1365 // rect = [last_child(rgroup) frame];
1367 // <string id="textURL" _label="" arg-set="text-url %"/>
1368 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1369 [node2 setAttributesAsDictionary:
1370 [NSDictionary dictionaryWithObjectsAndKeys:
1372 @"-text-url %", @"arg",
1374 make_text_field (prefs, opts, rgroup, node2, YES);
1377 // rect = [last_child(rgroup) frame];
1380 // <string id="textProgram" _label="" arg-set="text-program %"/>
1381 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1382 [node2 setAttributesAsDictionary:
1383 [NSDictionary dictionaryWithObjectsAndKeys:
1384 @"textProgram", @"id",
1385 @"-text-program %", @"arg",
1387 make_text_field (prefs, opts, rgroup, node2, YES);
1391 // rect = [last_child(rgroup) frame];
1393 layout_group (rgroup, NO);
1395 rect = [rgroup frame];
1396 rect.size.width += 35; // WTF? Why is rgroup too narrow?
1397 [rgroup setFrame:rect];
1400 // Set the height of the cells in the radio-box matrix to the height of
1401 // the (last of the) text fields.
1402 control = last_child (rgroup);
1403 rect = [control frame];
1404 rect.size.width = 30; // width of the string "Text", plus a bit...
1406 rect.size.width += 25;
1407 rect.size.height += LINE_SPACING;
1408 [matrix setCellSize:rect.size];
1409 [matrix sizeToCells];
1411 layout_group (group, YES);
1412 rect = [matrix frame];
1413 rect.origin.x += rect.size.width + COLUMN_SPACING;
1414 rect.origin.y -= [control frame].size.height - LINE_SPACING;
1415 [rgroup setFrameOrigin:rect.origin];
1417 // now cheat on the size of the matrix: allow it to overlap (underlap)
1420 rect.size = [matrix cellSize];
1421 rect.size.width = 300;
1422 [matrix setCellSize:rect.size];
1423 [matrix sizeToCells];
1425 // Cheat on the position of the stuff on the right (the rgroup).
1426 // GAAAH, this code is such crap!
1427 rect = [rgroup frame];
1429 [rgroup setFrame:rect];
1432 rect.size.width = rect.size.height = 0;
1433 NSBox *box = [[NSBox alloc] initWithFrame:rect];
1434 [box setTitlePosition:NSAtTop];
1435 [box setBorderType:NSBezelBorder];
1436 [box setTitle:@"Display Text"];
1438 rect.size.width = rect.size.height = 12;
1439 [box setContentViewMargins:rect.size];
1440 [box setContentView:group];
1443 place_child (parent, box, NO);
1448 make_image_controls (NSUserDefaultsController *prefs,
1449 const XrmOptionDescRec *opts,
1450 NSView *parent, NSXMLNode *node)
1453 [x] Grab desktop images
1454 [ ] Choose random image:
1455 [__________________________] [Choose]
1457 <boolean id="grabDesktopImages" _label="Grab desktop images"
1458 arg-unset="-no-grab-desktop"/>
1459 <boolean id="chooseRandomImages" _label="Grab desktop images"
1460 arg-unset="-choose-random-images"/>
1461 <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
1464 NSXMLElement *node2;
1466 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
1467 [node2 setAttributesAsDictionary:
1468 [NSDictionary dictionaryWithObjectsAndKeys:
1469 @"grabDesktopImages", @"id",
1470 @"Grab desktop images", @"_label",
1471 @"-no-grab-desktop", @"arg-unset",
1473 make_checkbox (prefs, opts, parent, node2);
1476 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
1477 [node2 setAttributesAsDictionary:
1478 [NSDictionary dictionaryWithObjectsAndKeys:
1479 @"chooseRandomImages", @"id",
1480 @"Choose random images", @"_label",
1481 @"-choose-random-images", @"arg-set",
1483 make_checkbox (prefs, opts, parent, node2);
1486 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1487 [node2 setAttributesAsDictionary:
1488 [NSDictionary dictionaryWithObjectsAndKeys:
1489 @"imageDirectory", @"id",
1490 @"Images from:", @"_label",
1491 @"-image-directory %", @"arg",
1493 make_file_selector (prefs, opts, parent, node2, YES, NO, YES);
1496 // Add a second, explanatory label below the file/URL selector.
1498 NSTextField *lab2 = 0;
1499 lab2 = make_label (@"(Local folder, or URL of RSS or Atom feed)");
1500 place_child (parent, lab2, NO);
1502 // Pack it in a little tighter vertically.
1503 NSRect r2 = [lab2 frame];
1506 [lab2 setFrameOrigin:r2.origin];
1512 /* Create some kind of control corresponding to the given XML node.
1515 make_control (NSUserDefaultsController *prefs,
1516 const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node)
1518 NSString *name = [node name];
1520 if ([node kind] == NSXMLCommentKind)
1522 if ([node kind] != NSXMLElementKind) {
1523 NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
1527 if ([name isEqualToString:@"hgroup"] ||
1528 [name isEqualToString:@"vgroup"]) {
1530 BOOL horiz_p = [name isEqualToString:@"hgroup"];
1531 make_group (prefs, opts, parent, node, horiz_p);
1533 } else if ([name isEqualToString:@"command"]) {
1534 // do nothing: this is the "-root" business
1536 } else if ([name isEqualToString:@"boolean"]) {
1537 make_checkbox (prefs, opts, parent, node);
1539 } else if ([name isEqualToString:@"string"]) {
1540 make_text_field (prefs, opts, parent, node, NO);
1542 } else if ([name isEqualToString:@"file"]) {
1543 make_file_selector (prefs, opts, parent, node, NO, NO, NO);
1545 } else if ([name isEqualToString:@"number"]) {
1546 make_number_selector (prefs, opts, parent, node);
1548 } else if ([name isEqualToString:@"select"]) {
1549 make_option_menu (prefs, opts, parent, node);
1551 } else if ([name isEqualToString:@"_description"]) {
1552 make_desc_label (parent, node);
1554 } else if ([name isEqualToString:@"xscreensaver-text"]) {
1555 make_text_controls (prefs, opts, parent, node);
1557 } else if ([name isEqualToString:@"xscreensaver-image"]) {
1558 make_image_controls (prefs, opts, parent, node);
1561 NSAssert1 (0, @"unknown tag: %@", name);
1566 /* Iterate over and process the children of this XML node.
1569 traverse_children (NSUserDefaultsController *prefs,
1570 const XrmOptionDescRec *opts,
1571 NSView *parent, NSXMLNode *node)
1573 NSArray *children = [node children];
1574 int i, count = [children count];
1575 for (i = 0; i < count; i++) {
1576 NSXMLNode *child = [children objectAtIndex:i];
1577 make_control (prefs, opts, parent, child);
1581 /* Handle the options on the top level <xscreensaver> tag.
1584 parse_xscreensaver_tag (NSXMLNode *node)
1586 NSMutableDictionary *dict =
1587 [NSMutableDictionary dictionaryWithObjectsAndKeys:
1591 parse_attrs (dict, node);
1592 NSString *name = [dict objectForKey:@"name"];
1593 NSString *label = [dict objectForKey:@"_label"];
1596 NSAssert1 (0, @"no _label in %@", [node name]);
1600 NSAssert1 (0, @"no name in \"%@\"", label);
1604 // #### do any callers need the "name" field for anything?
1608 /* Kludgey magic to make the window enclose the controls we created.
1611 fix_contentview_size (NSView *parent)
1614 NSArray *kids = [parent subviews];
1615 int nkids = [kids count];
1616 NSView *text = 0; // the NSText at the bottom of the window
1617 double maxx = 0, miny = 0;
1620 /* Find the size of the rectangle taken up by each of the children
1621 except the final "NSText" child.
1623 for (i = 0; i < nkids; i++) {
1624 NSView *kid = [kids objectAtIndex:i];
1625 if ([kid isKindOfClass:[NSText class]]) {
1630 if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width;
1631 if (f.origin.y - f.size.height < miny) miny = f.origin.y;
1632 // NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
1633 // f.size.width, f.size.height, f.origin.x, f.origin.y,
1634 // f.origin.y + f.size.height, [kid class]);
1637 if (maxx < 400) maxx = 400; // leave room for the NSText paragraph...
1639 /* Now that we know the width of the window, set the width of the NSText to
1640 that, so that it can decide what its height needs to be.
1642 if (! text) abort();
1644 // NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
1645 // f.size.width, f.size.height, f.origin.x, f.origin.y,
1646 // f.origin.y + f.size.height, [text class]);
1648 // set the NSText's width (this changes its height).
1649 f.size.width = maxx - LEFT_MARGIN;
1652 // position the NSText below the last child (this gives us a new miny).
1654 f.origin.y = miny - f.size.height - LINE_SPACING;
1655 miny = f.origin.y - LINE_SPACING;
1658 // Lock the width of the field and unlock the height, and let it resize
1659 // once more, to compute the proper height of the text for that width.
1661 [(NSText *) text setHorizontallyResizable:NO];
1662 [(NSText *) text setVerticallyResizable:YES];
1663 [(NSText *) text sizeToFit];
1665 // Now lock the height too: no more resizing this text field.
1667 [(NSText *) text setVerticallyResizable:NO];
1669 // Now reposition the top edge of the text field to be back where it
1670 // was before we changed the height.
1672 float oh = f.size.height;
1674 float dh = f.size.height - oh;
1677 // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF??
1678 // If we do this in 10.6, the text field moves down, off the window.
1679 // So instead we repair it at the end, at the "WTF2" comment.
1682 // Also adjust the parent height by the change in height of the text field.
1685 // NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
1686 // f.size.width, f.size.height, f.origin.x, f.origin.y,
1687 // f.origin.y + f.size.height, [text class]);
1690 /* Set the contentView to the size of the children.
1693 // float yoff = f.size.height;
1694 f.size.width = maxx + LEFT_MARGIN;
1695 f.size.height = -(miny - LEFT_MARGIN*2);
1696 // yoff = f.size.height - yoff;
1697 [parent setFrame:f];
1699 // NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f",
1700 // f.size.width, f.size.height, f.origin.x, f.origin.y);
1702 /* Now move all of the kids up into the window.
1705 float shift = f.size.height;
1706 // NSLog(@"shift: %3.0f", shift);
1707 for (i = 0; i < nkids; i++) {
1708 NSView *kid = [kids objectAtIndex:i];
1710 f.origin.y += shift;
1712 // NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
1713 // f.size.width, f.size.height, f.origin.x, f.origin.y,
1714 // f.origin.y + f.size.height, [kid class]);
1719 parent: 420 x 541 @ 0 0
1720 text: 380 x 100 @ 20 22 miny=-501
1723 parent: 420 x 541 @ 0 0
1724 text: 380 x 100 @ 20 50 miny=-501
1727 // #### WTF2: See "WTF" above. If the text field is off the screen,
1728 // move it up. We need this on 10.6 but not on 10.5. Auugh.
1731 if (f.origin.y < 50) { // magic numbers, yay
1736 /* Set the kids to track the top left corner of the window when resized.
1737 Set the NSText to track the bottom right corner as well.
1739 for (i = 0; i < nkids; i++) {
1740 NSView *kid = [kids objectAtIndex:i];
1741 unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
1742 if ([kid isKindOfClass:[NSText class]])
1743 mask |= NSViewWidthSizable|NSViewHeightSizable;
1744 [kid setAutoresizingMask:mask];
1749 - (void) okClicked:(NSObject *)arg
1751 [userDefaultsController commitEditing];
1752 [userDefaultsController save:self];
1753 [NSApp endSheet:self returnCode:NSOKButton];
1757 - (void) cancelClicked:(NSObject *)arg
1759 [userDefaultsController revert:self];
1760 [NSApp endSheet:self returnCode:NSCancelButton];
1764 - (void) resetClicked:(NSObject *)arg
1766 [userDefaultsController revertToInitialValues:self];
1771 wrap_with_buttons (NSWindow *window, NSView *panel)
1775 // Make a box to hold the buttons at the bottom of the window.
1777 rect = [panel frame];
1778 rect.origin.x = rect.origin.y = 0;
1779 rect.size.height = 10;
1780 NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
1781 [bbox setTitlePosition:NSNoTitle];
1782 [bbox setBorderType:NSNoBorder];
1784 // Make some buttons: Default, Cancel, OK
1786 rect.origin.x = rect.origin.y = 0;
1787 rect.size.width = rect.size.height = 10;
1788 NSButton *reset = [[NSButton alloc] initWithFrame:rect];
1789 [reset setTitle:@"Reset to Defaults"];
1790 [reset setBezelStyle:NSRoundedBezelStyle];
1793 rect = [reset frame];
1794 NSButton *ok = [[NSButton alloc] initWithFrame:rect];
1795 [ok setTitle:@"OK"];
1796 [ok setBezelStyle:NSRoundedBezelStyle];
1798 rect = [bbox frame];
1799 rect.origin.x = rect.size.width - [ok frame].size.width;
1800 [ok setFrameOrigin:rect.origin];
1803 NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
1804 [cancel setTitle:@"Cancel"];
1805 [cancel setBezelStyle:NSRoundedBezelStyle];
1807 rect.origin.x -= [cancel frame].size.width + 10;
1808 [cancel setFrameOrigin:rect.origin];
1810 // Bind OK to RET and Cancel to ESC.
1811 [ok setKeyEquivalent:@"\r"];
1812 [cancel setKeyEquivalent:@"\e"];
1814 // The correct width for OK and Cancel buttons is 68 pixels
1815 // ("Human Interface Guidelines: Controls: Buttons:
1816 // Push Button Specifications").
1819 rect.size.width = 68;
1822 rect = [cancel frame];
1823 rect.size.width = 68;
1824 [cancel setFrame:rect];
1826 // It puts the buttons in the box or else it gets the hose again
1828 [bbox addSubview:ok];
1829 [bbox addSubview:cancel];
1830 [bbox addSubview:reset];
1833 // make a box to hold the button-box, and the preferences view
1835 rect = [bbox frame];
1836 rect.origin.y += rect.size.height;
1837 NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
1838 [pbox setTitlePosition:NSNoTitle];
1839 [pbox setBorderType:NSBezelBorder];
1841 // Enforce a max height on the dialog, so that it's obvious to me
1842 // (on a big screen) when the dialog will fall off the bottom of
1843 // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
1845 NSRect f = [panel frame];
1846 int screen_height = (768 // shortest "modern" Mac display
1848 - 56 // System Preferences toolbar
1849 - 140 // default magnified bottom dock icon
1851 if (f.size.height > screen_height) {
1852 NSLog(@"%@ height was %.0f; clipping to %d",
1853 [panel class], f.size.height, screen_height);
1854 f.size.height = screen_height;
1859 [pbox addSubview:panel];
1860 [pbox addSubview:bbox];
1863 [reset setAutoresizingMask:NSViewMaxXMargin];
1864 [cancel setAutoresizingMask:NSViewMinXMargin];
1865 [ok setAutoresizingMask:NSViewMinXMargin];
1866 [bbox setAutoresizingMask:NSViewWidthSizable];
1870 [ok setTarget:window];
1871 [cancel setTarget:window];
1872 [reset setTarget:window];
1873 [ok setAction:@selector(okClicked:)];
1874 [cancel setAction:@selector(cancelClicked:)];
1875 [reset setAction:@selector(resetClicked:)];
1881 /* Iterate over and process the children of the root node of the XML document.
1884 traverse_tree (NSUserDefaultsController *prefs,
1885 NSWindow *window, const XrmOptionDescRec *opts, NSXMLNode *node)
1887 if (![[node name] isEqualToString:@"screensaver"]) {
1888 NSAssert (0, @"top level node is not <xscreensaver>");
1891 parse_xscreensaver_tag (node);
1894 rect.origin.x = rect.origin.y = 0;
1895 rect.size.width = rect.size.height = 1;
1897 NSView *panel = [[NSView alloc] initWithFrame:rect];
1899 traverse_children (prefs, opts, panel, node);
1900 fix_contentview_size (panel);
1902 NSView *root = wrap_with_buttons (window, panel);
1903 [prefs setAppliesImmediately:NO];
1905 [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1907 rect = [window frameRectForContentRect:[root frame]];
1908 [window setFrame:rect display:NO];
1909 [window setMinSize:rect.size];
1911 [window setContentView:root];
1915 /* When this object is instantiated, it parses the XML file and creates
1916 controls on itself that are hooked up to the appropriate preferences.
1917 The default size of the view is just big enough to hold them all.
1919 - (id)initWithXMLFile: (NSString *) xml_file
1920 options: (const XrmOptionDescRec *) opts
1921 controller: (NSUserDefaultsController *) prefs
1923 if (! (self = [super init]))
1926 // instance variable
1927 userDefaultsController = prefs;
1930 NSURL *furl = [NSURL fileURLWithPath:xml_file];
1933 NSAssert1 (0, @"can't URLify \"%@\"", xml_file);
1938 NSXMLDocument *xmlDoc = [[NSXMLDocument alloc]
1939 initWithContentsOfURL:furl
1940 options:(NSXMLNodePreserveWhitespace |
1941 NSXMLNodePreserveCDATA)
1943 if (!xmlDoc || err) {
1945 NSAssert2 (0, @"XML Error: %@: %@",
1946 xml_file, [err localizedDescription]);
1950 traverse_tree (prefs, self, opts, [xmlDoc rootElement]);
1959 [userDefaultsController release];