1 /* xscreensaver, Copyright (c) 2006-2009 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 NSAssert2 (0, @"duplicate %@: \"%@\", \"%@\"", 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,
379 NSMutableDictionary *dict =
380 [NSMutableDictionary dictionaryWithObjectsAndKeys:
385 parse_attrs (dict, node);
386 NSString *label = [dict objectForKey:@"_label"];
387 NSString *arg = [dict objectForKey:@"arg"];
389 if (!label && !no_label_p) {
390 NSAssert1 (0, @"no _label in %@", [node name]);
394 NSAssert1 (arg, @"no arg in %@", label);
397 rect.origin.x = rect.origin.y = 0;
398 rect.size.width = rect.size.height = 10;
400 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
402 // make the default size be around 20 columns.
404 [txt setStringValue:@"123456789 123456789 "];
406 [txt setSelectable:YES];
407 [txt setEditable:NO];
409 [txt setDrawsBackground:NO];
410 [[txt cell] setWraps:NO];
411 [[txt cell] setScrollable:YES];
412 [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
413 [txt setStringValue:@""];
415 NSTextField *lab = 0;
417 lab = make_label (label);
418 place_child (parent, lab, NO);
422 place_child (parent, txt, (label ? YES : NO));
424 bind_switch_to_preferences (prefs, txt, arg, opts);
427 // Make the text field be the same height as the label.
430 rect.size.height = [lab frame].size.height;
434 // Now put a "Choose" button next to it.
436 rect.origin.x = rect.origin.y = 0;
437 rect.size.width = rect.size.height = 10;
438 NSButton *choose = [[NSButton alloc] initWithFrame:rect];
439 [choose setTitle:@"Choose..."];
440 [choose setBezelStyle:NSRoundedBezelStyle];
443 place_child (parent, choose, YES);
445 // center the Choose button around the midpoint of the text field.
446 rect = [choose frame];
447 rect.origin.y = ([txt frame].origin.y +
448 (([txt frame].size.height - rect.size.height) / 2));
449 [choose setFrameOrigin:rect.origin];
451 [choose setTarget:[parent window]];
453 [choose setAction:@selector(chooseClickedDirs:)];
455 [choose setAction:@selector(chooseClicked:)];
461 /* Runs a modal file selector and sets the text field's value to the
462 selected file or directory.
465 do_file_selector (NSTextField *txt, BOOL dirs_p)
467 NSOpenPanel *panel = [NSOpenPanel openPanel];
468 [panel setAllowsMultipleSelection:NO];
469 [panel setCanChooseFiles:!dirs_p];
470 [panel setCanChooseDirectories:dirs_p];
472 NSString *file = [txt stringValue];
473 if ([file length] <= 0) {
474 file = NSHomeDirectory();
476 file = [file stringByAppendingPathComponent:@"Pictures"];
479 // NSString *dir = [file stringByDeletingLastPathComponent];
481 int result = [panel runModalForDirectory:file //dir
482 file:nil //[file lastPathComponent]
484 if (result == NSOKButton) {
485 NSArray *files = [panel filenames];
486 file = ([files count] > 0 ? [files objectAtIndex:0] : @"");
487 file = [file stringByAbbreviatingWithTildeInPath];
488 [txt setStringValue:file];
490 // Fuck me! Just setting the value of the NSTextField does not cause
491 // that to end up in the preferences!
493 NSDictionary *dict = [txt infoForBinding:@"value"];
494 NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
495 NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
496 if ([path hasPrefix:@"values."]) // WTF.
497 path = [path substringFromIndex:7];
498 [[prefs values] setValue:file forKey:path];
501 // make sure the end of the string is visible.
502 NSText *fe = [[txt window] fieldEditor:YES forObject:txt];
504 range.location = [file length]-3;
506 if (! [[txt window] makeFirstResponder:[txt window]])
507 [[txt window] endEditingFor:nil];
508 // [[txt window] makeFirstResponder:nil];
509 [fe setSelectedRange:range];
510 // [tv scrollRangeToVisible:range];
511 // [txt setNeedsDisplay:YES];
512 // [[txt cell] setNeedsDisplay:YES];
513 // [txt selectAll:txt];
518 /* Returns the NSTextField that is to the left of or above the NSButton.
521 find_text_field_of_button (NSButton *button)
523 NSView *parent = [button superview];
524 NSArray *kids = [parent subviews];
525 int nkids = [kids count];
528 for (i = 0; i < nkids; i++) {
529 NSObject *kid = [kids objectAtIndex:i];
530 if ([kid isKindOfClass:[NSTextField class]]) {
531 f = (NSTextField *) kid;
532 } else if (kid == button) {
541 - (void) chooseClicked:(NSObject *)arg
543 NSButton *choose = (NSButton *) arg;
544 NSTextField *txt = find_text_field_of_button (choose);
545 do_file_selector (txt, NO);
548 - (void) chooseClickedDirs:(NSObject *)arg
550 NSButton *choose = (NSButton *) arg;
551 NSTextField *txt = find_text_field_of_button (choose);
552 do_file_selector (txt, YES);
556 /* Creates the number selection control described by the given XML node.
557 If "type=slider", it's an NSSlider.
558 If "type=spinbutton", it's a text field with up/down arrows next to it.
561 make_number_selector (NSUserDefaultsController *prefs,
562 const XrmOptionDescRec *opts,
563 NSView *parent, NSXMLNode *node)
565 NSMutableDictionary *dict =
566 [NSMutableDictionary dictionaryWithObjectsAndKeys:
578 parse_attrs (dict, node);
579 NSString *label = [dict objectForKey:@"_label"];
580 NSString *low_label = [dict objectForKey:@"_low-label"];
581 NSString *high_label = [dict objectForKey:@"_high-label"];
582 NSString *type = [dict objectForKey:@"type"];
583 NSString *arg = [dict objectForKey:@"arg"];
584 NSString *low = [dict objectForKey:@"low"];
585 NSString *high = [dict objectForKey:@"high"];
586 NSString *def = [dict objectForKey:@"default"];
587 NSString *cvt = [dict objectForKey:@"convert"];
589 NSAssert1 (arg, @"no arg in %@", label);
590 NSAssert1 (type, @"no type in %@", label);
593 NSAssert1 (0, @"no low in %@", [node name]);
597 NSAssert1 (0, @"no high in %@", [node name]);
601 NSAssert1 (0, @"no default in %@", [node name]);
604 if (cvt && ![cvt isEqualToString:@"invert"]) {
605 NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
609 // If either the min or max field contains a decimal point, then this
610 // option may have a floating point value; otherwise, it is constrained
613 NSCharacterSet *dot =
614 [NSCharacterSet characterSetWithCharactersInString:@"."];
615 BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
616 [high rangeOfCharacterFromSet:dot].location != NSNotFound);
618 if ([type isEqualToString:@"slider"]) {
621 rect.origin.x = rect.origin.y = 0;
622 rect.size.width = 150;
623 rect.size.height = 23; // apparent min height for slider with ticks...
626 slider = [[InvertedSlider alloc] initWithFrame:rect];
628 slider = [[NSSlider alloc] initWithFrame:rect];
630 [slider setMaxValue:[high doubleValue]];
631 [slider setMinValue:[low doubleValue]];
633 int range = [slider maxValue] - [slider minValue] + 1;
636 while (range2 > max_ticks)
639 // If we have elided ticks, leave it at the max number of ticks.
640 if (range != range2 && range2 < max_ticks)
643 // If it's a float, always display the max number of ticks.
644 if (float_p && range2 < max_ticks)
647 [slider setNumberOfTickMarks:range2];
649 [slider setAllowsTickMarkValuesOnly:
650 (range == range2 && // we are showing the actual number of ticks
651 !float_p)]; // and we want integer results
653 // #### Note: when the slider's range is large enough that we aren't
654 // showing all possible ticks, the slider's value is not constrained
655 // to be an integer, even though it should be...
656 // Maybe we need to use a value converter or something?
659 NSTextField *lab = make_label (label);
660 place_child (parent, lab, NO);
665 NSTextField *lab = make_label (low_label);
666 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
667 [lab setAlignment:1]; // right aligned
669 if (rect.size.width < LEFT_LABEL_WIDTH)
670 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
671 rect.size.height = [slider frame].size.height;
673 place_child (parent, lab, NO);
677 place_child (parent, slider, (low_label ? YES : NO));
680 rect = [slider frame];
681 if (rect.origin.x < LEFT_LABEL_WIDTH)
682 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
683 [slider setFrame:rect];
687 NSTextField *lab = make_label (high_label);
688 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
690 rect.size.height = [slider frame].size.height;
692 place_child (parent, lab, YES);
696 bind_switch_to_preferences (prefs, slider, arg, opts);
699 } else if ([type isEqualToString:@"spinbutton"]) {
702 NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
705 NSAssert1 (!low_label,
706 @"low-label not allowed in spinbutton \"%@\"", [node name]);
707 NSAssert1 (!high_label,
708 @"high-label not allowed in spinbutton \"%@\"", [node name]);
709 NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
713 rect.origin.x = rect.origin.y = 0;
714 rect.size.width = rect.size.height = 10;
716 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
717 [txt setStringValue:@"0000.0"];
719 [txt setStringValue:@""];
722 NSTextField *lab = make_label (label);
723 //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
724 [lab setAlignment:1]; // right aligned
726 if (rect.size.width < LEFT_LABEL_WIDTH)
727 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
728 rect.size.height = [txt frame].size.height;
730 place_child (parent, lab, NO);
734 place_child (parent, txt, (label ? YES : NO));
738 if (rect.origin.x < LEFT_LABEL_WIDTH)
739 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
743 rect.size.width = rect.size.height = 10;
744 NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
746 place_child (parent, step, YES);
748 rect.origin.x -= COLUMN_SPACING; // this one goes close
749 rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
750 [step setFrame:rect];
752 [step setMinValue:[low doubleValue]];
753 [step setMaxValue:[high doubleValue]];
754 [step setAutorepeat:YES];
755 [step setValueWraps:NO];
757 double range = [high doubleValue] - [low doubleValue];
759 [step setIncrement:range / 10.0];
760 else if (range >= 500)
761 [step setIncrement:range / 100.0];
763 [step setIncrement:1.0];
765 NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
766 [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
767 [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
768 [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]];
769 [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
770 [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
771 [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
773 [fmt setGeneratesDecimalNumbers:float_p];
774 [[txt cell] setFormatter:fmt];
777 bind_switch_to_preferences (prefs, step, arg, opts);
778 bind_switch_to_preferences (prefs, txt, arg, opts);
784 NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
790 set_menu_item_object (NSMenuItem *item, NSObject *obj)
792 /* If the object associated with this menu item looks like a boolean,
793 store an NSNumber instead of an NSString, since that's what
794 will be in the preferences (due to similar logic in PrefsReader).
796 if ([obj isKindOfClass:[NSString class]]) {
797 NSString *string = (NSString *) obj;
798 if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
799 NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
800 obj = [NSNumber numberWithBool:YES];
801 else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
802 NSOrderedSame == [string caseInsensitiveCompare:@"no"])
803 obj = [NSNumber numberWithBool:NO];
808 [item setRepresentedObject:obj];
809 //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
813 /* Creates the popup menu described by the given XML node (and its children).
816 make_option_menu (NSUserDefaultsController *prefs,
817 const XrmOptionDescRec *opts,
818 NSView *parent, NSXMLNode *node)
820 NSArray *children = [node children];
821 int i, count = [children count];
824 NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
828 // get the "id" attribute off the <select> tag.
830 NSMutableDictionary *dict =
831 [NSMutableDictionary dictionaryWithObjectsAndKeys:
834 parse_attrs (dict, node);
837 rect.origin.x = rect.origin.y = 0;
838 rect.size.width = 10;
839 rect.size.height = 10;
841 // #### "Build and Analyze" says that all of our widgets leak, because it
842 // seems to not realize that place_child -> addSubview retains them.
843 // Not sure what to do to make these warnings go away.
845 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
848 NSMenuItem *def_item = nil;
851 NSString *menu_key = nil; // the resource key used by items in this menu
853 for (i = 0; i < count; i++) {
854 NSXMLNode *child = [children objectAtIndex:i];
856 if ([child kind] == NSXMLCommentKind)
858 if ([child kind] != NSXMLElementKind) {
859 NSAssert2 (0, @"weird XML node kind: %d: %@", [child kind], node);
863 // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
865 NSMutableDictionary *dict2 =
866 [NSMutableDictionary dictionaryWithObjectsAndKeys:
871 parse_attrs (dict2, child);
872 NSString *label = [dict2 objectForKey:@"_label"];
873 NSString *arg_set = [dict2 objectForKey:@"arg-set"];
876 NSAssert1 (0, @"no _label in %@", [child name]);
880 // create the menu item (and then get a pointer to it)
881 [popup addItemWithTitle:label];
882 NSMenuItem *item = [popup itemWithTitle:label];
885 NSString *this_val = NULL;
886 NSString *this_key = switch_to_resource (arg_set, opts, &this_val);
887 NSAssert1 (this_val, @"this_val null for %@", arg_set);
888 if (menu_key && ![menu_key isEqualToString:this_key])
890 @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
891 menu_key, this_key, this_val);
895 /* If this menu has the cmd line "-mode foo" then set this item's
896 value to "foo" (the menu itself will be bound to e.g. "modeString")
898 set_menu_item_object (item, this_val);
901 // no arg-set -- only one menu item can be missing that.
902 NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
906 /* make sure the menu button has room for the text of this item,
907 and remember the greatest width it has reached.
909 [popup setTitle:label];
911 NSRect r = [popup frame];
912 if (r.size.width > max_width) max_width = r.size.width;
916 NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
920 /* We've added all of the menu items. If there was an item with no
921 command-line switch, then it's the item that represents the default
922 value. Now we must bind to that item as well... (We have to bind
923 this one late, because if it was the first item, then we didn't
924 yet know what resource was associated with this menu.)
927 NSDictionary *defs = [prefs initialValues];
928 NSObject *def_obj = [defs objectForKey:menu_key];
931 @"no default value for resource \"%@\" in menu item \"%@\"",
932 menu_key, [def_item title]);
934 set_menu_item_object (def_item, def_obj);
937 /* Finish tweaking the menu button itself.
940 [popup setTitle:[def_item title]];
941 NSRect r = [popup frame];
942 r.size.width = max_width;
944 place_child (parent, popup, NO);
946 bind_resource_to_preferences (prefs, popup, menu_key, opts);
951 static NSString *unwrap (NSString *);
952 static void hreffify (NSText *);
953 static void boldify (NSText *);
955 /* Creates an uneditable, wrapping NSTextField to display the given
956 text enclosed by <description> ... </description> in the XML.
959 make_desc_label (NSView *parent, NSXMLNode *node)
961 NSString *text = nil;
962 NSArray *children = [node children];
963 int i, count = [children count];
965 for (i = 0; i < count; i++) {
966 NSXMLNode *child = [children objectAtIndex:i];
967 NSString *s = [child objectValue];
969 text = [text stringByAppendingString:s];
974 text = unwrap (text);
976 NSRect rect = [parent frame];
977 rect.origin.x = rect.origin.y = 0;
978 rect.size.width = 200;
979 rect.size.height = 50; // sized later
980 NSText *lab = [[NSText alloc] initWithFrame:rect];
981 [lab setEditable:NO];
982 [lab setDrawsBackground:NO];
983 [lab setHorizontallyResizable:YES];
984 [lab setVerticallyResizable:YES];
985 [lab setString:text];
990 place_child (parent, lab, NO);
995 unwrap (NSString *text)
997 // Unwrap lines: delete \n but do not delete \n\n.
999 NSArray *lines = [text componentsSeparatedByString:@"\n"];
1000 int nlines = [lines count];
1004 text = @"\n"; // start with one blank line
1006 // skip trailing blank lines in file
1007 for (i = nlines-1; i > 0; i--) {
1008 NSString *s = (NSString *) [lines objectAtIndex:i];
1014 // skip leading blank lines in file
1015 for (i = 0; i < nlines; i++) {
1016 NSString *s = (NSString *) [lines objectAtIndex:i];
1023 for (; i < nlines; i++) {
1024 NSString *s = (NSString *) [lines objectAtIndex:i];
1025 if ([s length] == 0) {
1026 text = [text stringByAppendingString:@"\n\n"];
1028 } else if ([s characterAtIndex:0] == ' ' ||
1029 [s hasPrefix:@"Copyright "] ||
1030 [s hasPrefix:@"http://"]) {
1031 // don't unwrap if the following line begins with whitespace,
1032 // or with the word "Copyright", or if it begins with a URL.
1034 text = [text stringByAppendingString:@"\n"];
1035 text = [text stringByAppendingString:s];
1040 text = [text stringByAppendingString:@" "];
1041 text = [text stringByAppendingString:s];
1052 anchorize (const char *url)
1054 const char *wiki = "http://en.wikipedia.org/wiki/";
1055 const char *math = "http://mathworld.wolfram.com/";
1056 if (!strncmp (wiki, url, strlen(wiki))) {
1057 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
1058 strcpy (anchor, "Wikipedia: \"");
1059 const char *in = url + strlen(wiki);
1060 char *out = anchor + strlen(anchor);
1064 } else if (*in == '#') {
1067 } else if (*in == '%') {
1073 sscanf (hex, "%x", &n);
1085 } else if (!strncmp (math, url, strlen(math))) {
1086 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
1087 strcpy (anchor, "MathWorld: \"");
1088 const char *start = url + strlen(wiki);
1089 const char *in = start;
1090 char *out = anchor + strlen(anchor);
1094 } else if (in != start && *in >= 'A' && *in <= 'Z') {
1097 } else if (!strncmp (in, ".htm", 4)) {
1109 return strdup (url);
1114 /* Converts any http: URLs in the given text field to clickable links.
1117 hreffify (NSText *nstext)
1119 NSString *text = [nstext string];
1120 [nstext setRichText:YES];
1122 int L = [text length];
1123 NSRange start; // range is start-of-search to end-of-string
1126 while (start.location < L) {
1128 // Find the beginning of a URL...
1130 NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
1131 if (r2.location == NSNotFound)
1134 // Next time around, start searching after this.
1135 start.location = r2.location + r2.length;
1136 start.length = L - start.location;
1138 // Find the end of a URL (whitespace or EOF)...
1140 NSRange r3 = [text rangeOfCharacterFromSet:
1141 [NSCharacterSet whitespaceAndNewlineCharacterSet]
1142 options:0 range:start];
1143 if (r3.location == NSNotFound) // EOF
1144 r3.location = L, r3.length = 0;
1146 // Next time around, start searching after this.
1147 start.location = r3.location;
1148 start.length = L - start.location;
1150 // Set r2 to the start/length of this URL.
1151 r2.length = start.location - r2.location;
1154 NSString *nsurl = [text substringWithRange:r2];
1155 const char *url = [nsurl UTF8String];
1157 // If this is a Wikipedia URL, make the linked text be prettier.
1159 char *anchor = anchorize(url);
1161 // Construct the RTF corresponding to <A HREF="url">anchor</A>
1163 const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
1164 char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
1165 sprintf (rtf, fmt, url, anchor);
1167 NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
1169 // Insert the RTF into the NSText.
1170 [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
1172 int L2 = [text length]; // might have changed
1173 start.location -= (L - L2);
1178 /* Makes the text up to the first comma be bold.
1181 boldify (NSText *nstext)
1183 NSString *text = [nstext string];
1184 NSRange r = [text rangeOfString:@"," options:0];
1185 r.length = r.location+1;
1188 NSFont *font = [nstext font];
1189 font = [NSFont boldSystemFontOfSize:[font pointSize]];
1190 [nstext setFont:font range:r];
1194 static void layout_group (NSView *group, BOOL horiz_p);
1197 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
1198 wrapped in <hgroup> or <vgroup> in the XML.
1201 make_group (NSUserDefaultsController *prefs,
1202 const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node,
1206 rect.size.width = rect.size.height = 1;
1207 rect.origin.x = rect.origin.y = 0;
1208 NSView *group = [[NSView alloc] initWithFrame:rect];
1209 traverse_children (prefs, opts, group, node);
1211 layout_group (group, horiz_p);
1213 rect.size.width = rect.size.height = 0;
1214 NSBox *box = [[NSBox alloc] initWithFrame:rect];
1215 [box setTitlePosition:NSNoTitle];
1216 [box setBorderType:NSNoBorder];
1217 [box setContentViewMargins:rect.size];
1218 [box setContentView:group];
1221 place_child (parent, box, NO);
1226 layout_group (NSView *group, BOOL horiz_p)
1228 NSArray *kids = [group subviews];
1229 int nkids = [kids count];
1231 double maxx = 0, miny = 0;
1232 for (i = 0; i < nkids; i++) {
1233 NSView *kid = [kids objectAtIndex:i];
1234 NSRect r = [kid frame];
1237 maxx += r.size.width + COLUMN_SPACING;
1238 if (r.size.height > -miny) miny = -r.size.height;
1240 if (r.size.width > maxx) maxx = r.size.width;
1241 miny = r.origin.y - r.size.height;
1246 rect.size.width = maxx;
1247 rect.size.height = -miny;
1248 [group setFrame:rect];
1251 for (i = 0; i < nkids; i++) {
1252 NSView *kid = [kids objectAtIndex:i];
1253 NSRect r = [kid frame];
1255 r.origin.y = rect.size.height - r.size.height;
1257 x += r.size.width + COLUMN_SPACING;
1267 make_text_controls (NSUserDefaultsController *prefs,
1268 const XrmOptionDescRec *opts,
1269 NSView *parent, NSXMLNode *node)
1273 (x) Computer name and time
1274 ( ) Text [__________________________]
1275 ( ) Text file [_________________] [Choose]
1276 ( ) URL [__________________________]
1278 textMode -text-mode date
1279 textMode -text-mode literal textLiteral -text-literal %
1280 textMode -text-mode file textFile -text-file %
1281 textMode -text-mode url textURL -text-url %
1284 rect.size.width = rect.size.height = 1;
1285 rect.origin.x = rect.origin.y = 0;
1286 NSView *group = [[NSView alloc] initWithFrame:rect];
1287 NSView *rgroup = [[NSView alloc] initWithFrame:rect];
1290 NSXMLElement *node2;
1293 // This is how you link radio buttons together.
1295 NSButtonCell *proto = [[NSButtonCell alloc] init];
1296 [proto setButtonType:NSRadioButton];
1298 rect.origin.x = rect.origin.y = 0;
1299 rect.size.width = rect.size.height = 10;
1300 NSMatrix *matrix = [[NSMatrix alloc]
1302 mode:NSRadioModeMatrix
1306 [matrix setAllowsEmptySelection:NO];
1308 NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil];
1309 [cnames addObject:@"Computer name and time"];
1310 [cnames addObject:@"Text"];
1311 [cnames addObject:@"File"];
1312 [cnames addObject:@"URL"];
1313 [matrix bind:@"content"
1315 withKeyPath:@"arrangedObjects"
1319 bind_switch_to_preferences (prefs, matrix, @"-text-mode %", opts);
1321 place_child (group, matrix, NO);
1322 place_child (group, rgroup, YES);
1324 // <string id="textLiteral" _label="" arg-set="-text-literal %"/>
1325 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1326 [node2 setAttributesAsDictionary:
1327 [NSDictionary dictionaryWithObjectsAndKeys:
1328 @"textLiteral", @"id",
1329 @"-text-literal %", @"arg",
1331 make_text_field (prefs, opts, rgroup, node2, YES);
1334 // rect = [last_child(rgroup) frame];
1336 /* // trying to make the text fields be enabled only when the checkbox is on..
1337 control = last_child (rgroup);
1338 [control bind:@"enabled"
1339 toObject:[matrix cellAtRow:1 column:0]
1340 withKeyPath:@"value"
1345 // <file id="textFile" _label="" arg-set="-text-file %"/>
1346 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1347 [node2 setAttributesAsDictionary:
1348 [NSDictionary dictionaryWithObjectsAndKeys:
1350 @"-text-file %", @"arg",
1352 make_file_selector (prefs, opts, rgroup, node2, NO, YES);
1355 // rect = [last_child(rgroup) frame];
1357 // <string id="textURL" _label="" arg-set="text-url %"/>
1358 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1359 [node2 setAttributesAsDictionary:
1360 [NSDictionary dictionaryWithObjectsAndKeys:
1362 @"-text-url %", @"arg",
1364 make_text_field (prefs, opts, rgroup, node2, YES);
1367 // rect = [last_child(rgroup) frame];
1369 layout_group (rgroup, NO);
1371 rect = [rgroup frame];
1372 rect.size.width += 35; // WTF? Why is rgroup too narrow?
1373 [rgroup setFrame:rect];
1376 // Set the height of the cells in the radio-box matrix to the height of
1377 // the (last of the) text fields.
1378 control = last_child (rgroup);
1379 rect = [control frame];
1380 rect.size.width = 30; // width of the string "Text", plus a bit...
1381 rect.size.height += LINE_SPACING;
1382 [matrix setCellSize:rect.size];
1383 [matrix sizeToCells];
1385 layout_group (group, YES);
1386 rect = [matrix frame];
1387 rect.origin.x += rect.size.width + COLUMN_SPACING;
1388 rect.origin.y -= [control frame].size.height - LINE_SPACING;
1389 [rgroup setFrameOrigin:rect.origin];
1391 // now cheat on the size of the matrix: allow it to overlap (underlap)
1394 rect.size = [matrix cellSize];
1395 rect.size.width *= 10;
1396 [matrix setCellSize:rect.size];
1397 [matrix sizeToCells];
1399 // Cheat on the position of the stuff on the right (the rgroup).
1400 // GAAAH, this code is such crap!
1401 rect = [rgroup frame];
1403 [rgroup setFrame:rect];
1406 rect.size.width = rect.size.height = 0;
1407 NSBox *box = [[NSBox alloc] initWithFrame:rect];
1408 [box setTitlePosition:NSAtTop];
1409 [box setBorderType:NSBezelBorder];
1410 [box setTitle:@"Display Text"];
1412 rect.size.width = rect.size.height = 12;
1413 [box setContentViewMargins:rect.size];
1414 [box setContentView:group];
1417 place_child (parent, box, NO);
1422 make_image_controls (NSUserDefaultsController *prefs,
1423 const XrmOptionDescRec *opts,
1424 NSView *parent, NSXMLNode *node)
1427 [x] Grab desktop images
1428 [ ] Choose random image:
1429 [__________________________] [Choose]
1431 <boolean id="grabDesktopImages" _label="Grab desktop images"
1432 arg-unset="-no-grab-desktop"/>
1433 <boolean id="chooseRandomImages" _label="Grab desktop images"
1434 arg-unset="-choose-random-images"/>
1435 <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
1438 NSXMLElement *node2;
1440 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
1441 [node2 setAttributesAsDictionary:
1442 [NSDictionary dictionaryWithObjectsAndKeys:
1443 @"grabDesktopImages", @"id",
1444 @"Grab desktop images", @"_label",
1445 @"-no-grab-desktop", @"arg-unset",
1447 make_checkbox (prefs, opts, parent, node2);
1450 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
1451 [node2 setAttributesAsDictionary:
1452 [NSDictionary dictionaryWithObjectsAndKeys:
1453 @"chooseRandomImages", @"id",
1454 @"Choose random images", @"_label",
1455 @"-choose-random-images", @"arg-set",
1457 make_checkbox (prefs, opts, parent, node2);
1460 node2 = [[NSXMLElement alloc] initWithName:@"string"];
1461 [node2 setAttributesAsDictionary:
1462 [NSDictionary dictionaryWithObjectsAndKeys:
1463 @"imageDirectory", @"id",
1464 @"Images directory:", @"_label",
1465 @"-image-directory %", @"arg",
1467 make_file_selector (prefs, opts, parent, node2, YES, NO);
1473 /* Create some kind of control corresponding to the given XML node.
1476 make_control (NSUserDefaultsController *prefs,
1477 const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node)
1479 NSString *name = [node name];
1481 if ([node kind] == NSXMLCommentKind)
1483 if ([node kind] != NSXMLElementKind) {
1484 NSAssert2 (0, @"weird XML node kind: %d: %@", [node kind], node);
1488 if ([name isEqualToString:@"hgroup"] ||
1489 [name isEqualToString:@"vgroup"]) {
1491 BOOL horiz_p = [name isEqualToString:@"hgroup"];
1492 make_group (prefs, opts, parent, node, horiz_p);
1494 } else if ([name isEqualToString:@"command"]) {
1495 // do nothing: this is the "-root" business
1497 } else if ([name isEqualToString:@"boolean"]) {
1498 make_checkbox (prefs, opts, parent, node);
1500 } else if ([name isEqualToString:@"string"]) {
1501 make_text_field (prefs, opts, parent, node, NO);
1503 } else if ([name isEqualToString:@"file"]) {
1504 make_file_selector (prefs, opts, parent, node, NO, NO);
1506 } else if ([name isEqualToString:@"number"]) {
1507 make_number_selector (prefs, opts, parent, node);
1509 } else if ([name isEqualToString:@"select"]) {
1510 make_option_menu (prefs, opts, parent, node);
1512 } else if ([name isEqualToString:@"_description"]) {
1513 make_desc_label (parent, node);
1515 } else if ([name isEqualToString:@"xscreensaver-text"]) {
1516 make_text_controls (prefs, opts, parent, node);
1518 } else if ([name isEqualToString:@"xscreensaver-image"]) {
1519 make_image_controls (prefs, opts, parent, node);
1522 NSAssert1 (0, @"unknown tag: %@", name);
1527 /* Iterate over and process the children of this XML node.
1530 traverse_children (NSUserDefaultsController *prefs,
1531 const XrmOptionDescRec *opts,
1532 NSView *parent, NSXMLNode *node)
1534 NSArray *children = [node children];
1535 int i, count = [children count];
1536 for (i = 0; i < count; i++) {
1537 NSXMLNode *child = [children objectAtIndex:i];
1538 make_control (prefs, opts, parent, child);
1542 /* Handle the options on the top level <xscreensaver> tag.
1545 parse_xscreensaver_tag (NSXMLNode *node)
1547 NSMutableDictionary *dict =
1548 [NSMutableDictionary dictionaryWithObjectsAndKeys:
1552 parse_attrs (dict, node);
1553 NSString *name = [dict objectForKey:@"name"];
1554 NSString *label = [dict objectForKey:@"_label"];
1557 NSAssert1 (0, @"no _label in %@", [node name]);
1561 NSAssert1 (0, @"no name in \"%@\"", label);
1565 // #### do any callers need the "name" field for anything?
1569 /* Kludgey magic to make the window enclose the controls we created.
1572 fix_contentview_size (NSView *parent)
1575 NSArray *kids = [parent subviews];
1576 int nkids = [kids count];
1577 NSView *text = 0; // the NSText at the bottom of the window
1578 double maxx = 0, miny = 0;
1581 /* Find the size of the rectangle taken up by each of the children
1582 except the final "NSText" child.
1584 for (i = 0; i < nkids; i++) {
1585 NSView *kid = [kids objectAtIndex:i];
1586 if ([kid isKindOfClass:[NSText class]]) {
1591 if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width;
1592 if (f.origin.y - f.size.height < miny) miny = f.origin.y;
1593 // NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
1594 // f.size.width, f.size.height, f.origin.x, f.origin.y,
1595 // f.origin.y + f.size.height, [kid class]);
1598 if (maxx < 400) maxx = 400; // leave room for the NSText paragraph...
1600 /* Now that we know the width of the window, set the width of the NSText to
1601 that, so that it can decide what its height needs to be.
1603 if (! text) abort();
1605 // NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
1606 // f.size.width, f.size.height, f.origin.x, f.origin.y,
1607 // f.origin.y + f.size.height, [text class]);
1609 // set the NSText's width (this changes its height).
1610 f.size.width = maxx - LEFT_MARGIN;
1613 // position the NSText below the last child (this gives us a new miny).
1615 f.origin.y = miny - f.size.height - LINE_SPACING;
1616 miny = f.origin.y - LINE_SPACING;
1619 // Lock the width of the field and unlock the height, and let it resize
1620 // once more, to compute the proper height of the text for that width.
1622 [(NSText *) text setHorizontallyResizable:NO];
1623 [(NSText *) text setVerticallyResizable:YES];
1624 [(NSText *) text sizeToFit];
1626 // Now lock the height too: no more resizing this text field.
1628 [(NSText *) text setVerticallyResizable:NO];
1630 // Now reposition the top edge of the text field to be back where it
1631 // was before we changed the height.
1633 float oh = f.size.height;
1635 float dh = f.size.height - oh;
1638 // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF??
1639 // If we do this in 10.6, the text field moves down, off the window.
1640 // So instead we repair it at the end, at the "WTF2" comment.
1643 // Also adjust the parent height by the change in height of the text field.
1646 // NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
1647 // f.size.width, f.size.height, f.origin.x, f.origin.y,
1648 // f.origin.y + f.size.height, [text class]);
1651 /* Set the contentView to the size of the children.
1654 // float yoff = f.size.height;
1655 f.size.width = maxx + LEFT_MARGIN;
1656 f.size.height = -(miny - LEFT_MARGIN*2);
1657 // yoff = f.size.height - yoff;
1658 [parent setFrame:f];
1660 // NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f",
1661 // f.size.width, f.size.height, f.origin.x, f.origin.y);
1663 /* Now move all of the kids up into the window.
1666 float shift = f.size.height;
1667 // NSLog(@"shift: %3.0f", shift);
1668 for (i = 0; i < nkids; i++) {
1669 NSView *kid = [kids objectAtIndex:i];
1671 f.origin.y += shift;
1673 // NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
1674 // f.size.width, f.size.height, f.origin.x, f.origin.y,
1675 // f.origin.y + f.size.height, [kid class]);
1680 parent: 420 x 541 @ 0 0
1681 text: 380 x 100 @ 20 22 miny=-501
1684 parent: 420 x 541 @ 0 0
1685 text: 380 x 100 @ 20 50 miny=-501
1688 // #### WTF2: See "WTF" above. If the text field is off the screen,
1689 // move it up. We need this on 10.6 but not on 10.5. Auugh.
1692 if (f.origin.y < 50) { // magic numbers, yay
1697 /* Set the kids to track the top left corner of the window when resized.
1698 Set the NSText to track the bottom right corner as well.
1700 for (i = 0; i < nkids; i++) {
1701 NSView *kid = [kids objectAtIndex:i];
1702 unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
1703 if ([kid isKindOfClass:[NSText class]])
1704 mask |= NSViewWidthSizable|NSViewHeightSizable;
1705 [kid setAutoresizingMask:mask];
1710 - (void) okClicked:(NSObject *)arg
1712 [userDefaultsController commitEditing];
1713 [userDefaultsController save:self];
1714 [NSApp endSheet:self returnCode:NSOKButton];
1718 - (void) cancelClicked:(NSObject *)arg
1720 [userDefaultsController revert:self];
1721 [NSApp endSheet:self returnCode:NSCancelButton];
1725 - (void) resetClicked:(NSObject *)arg
1727 [userDefaultsController revertToInitialValues:self];
1732 wrap_with_buttons (NSWindow *window, NSView *panel)
1736 // Make a box to hold the buttons at the bottom of the window.
1738 rect = [panel frame];
1739 rect.origin.x = rect.origin.y = 0;
1740 rect.size.height = 10;
1741 NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
1742 [bbox setTitlePosition:NSNoTitle];
1743 [bbox setBorderType:NSNoBorder];
1745 // Make some buttons: Default, Cancel, OK
1747 rect.origin.x = rect.origin.y = 0;
1748 rect.size.width = rect.size.height = 10;
1749 NSButton *reset = [[NSButton alloc] initWithFrame:rect];
1750 [reset setTitle:@"Reset to Defaults"];
1751 [reset setBezelStyle:NSRoundedBezelStyle];
1754 rect = [reset frame];
1755 NSButton *ok = [[NSButton alloc] initWithFrame:rect];
1756 [ok setTitle:@"OK"];
1757 [ok setBezelStyle:NSRoundedBezelStyle];
1759 rect = [bbox frame];
1760 rect.origin.x = rect.size.width - [ok frame].size.width;
1761 [ok setFrameOrigin:rect.origin];
1764 NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
1765 [cancel setTitle:@"Cancel"];
1766 [cancel setBezelStyle:NSRoundedBezelStyle];
1768 rect.origin.x -= [cancel frame].size.width + 10;
1769 [cancel setFrameOrigin:rect.origin];
1771 // Bind OK to RET and Cancel to ESC.
1772 [ok setKeyEquivalent:@"\r"];
1773 [cancel setKeyEquivalent:@"\e"];
1775 // The correct width for OK and Cancel buttons is 68 pixels
1776 // ("Human Interface Guidelines: Controls: Buttons:
1777 // Push Button Specifications").
1780 rect.size.width = 68;
1783 rect = [cancel frame];
1784 rect.size.width = 68;
1785 [cancel setFrame:rect];
1787 // It puts the buttons in the box or else it gets the hose again
1789 [bbox addSubview:ok];
1790 [bbox addSubview:cancel];
1791 [bbox addSubview:reset];
1794 // make a box to hold the button-box, and the preferences view
1796 rect = [bbox frame];
1797 rect.origin.y += rect.size.height;
1798 NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
1799 [pbox setTitlePosition:NSNoTitle];
1800 [pbox setBorderType:NSBezelBorder];
1802 // Enforce a max height on the dialog, so that it's obvious to me
1803 // (on a big screen) when the dialog will fall off the bottom of
1804 // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
1806 NSRect f = [panel frame];
1807 int screen_height = (768 // shortest "modern" Mac display
1809 - 56 // System Preferences toolbar
1810 - 140 // default magnified bottom dock icon
1812 if (f.size.height > screen_height) {
1813 NSLog(@"%@ height was %.0f; clipping to %d",
1814 [panel class], f.size.height, screen_height);
1815 f.size.height = screen_height;
1820 [pbox addSubview:panel];
1821 [pbox addSubview:bbox];
1824 [reset setAutoresizingMask:NSViewMaxXMargin];
1825 [cancel setAutoresizingMask:NSViewMinXMargin];
1826 [ok setAutoresizingMask:NSViewMinXMargin];
1827 [bbox setAutoresizingMask:NSViewWidthSizable];
1831 [ok setTarget:window];
1832 [cancel setTarget:window];
1833 [reset setTarget:window];
1834 [ok setAction:@selector(okClicked:)];
1835 [cancel setAction:@selector(cancelClicked:)];
1836 [reset setAction:@selector(resetClicked:)];
1842 /* Iterate over and process the children of the root node of the XML document.
1845 traverse_tree (NSUserDefaultsController *prefs,
1846 NSWindow *window, const XrmOptionDescRec *opts, NSXMLNode *node)
1848 if (![[node name] isEqualToString:@"screensaver"]) {
1849 NSAssert (0, @"top level node is not <xscreensaver>");
1852 parse_xscreensaver_tag (node);
1855 rect.origin.x = rect.origin.y = 0;
1856 rect.size.width = rect.size.height = 1;
1858 NSView *panel = [[NSView alloc] initWithFrame:rect];
1860 traverse_children (prefs, opts, panel, node);
1861 fix_contentview_size (panel);
1863 NSView *root = wrap_with_buttons (window, panel);
1864 [prefs setAppliesImmediately:NO];
1866 [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1868 rect = [window frameRectForContentRect:[root frame]];
1869 [window setFrame:rect display:NO];
1870 [window setMinSize:rect.size];
1872 [window setContentView:root];
1876 /* When this object is instantiated, it parses the XML file and creates
1877 controls on itself that are hooked up to the appropriate preferences.
1878 The default size of the view is just big enough to hold them all.
1880 - (id)initWithXMLFile: (NSString *) xml_file
1881 options: (const XrmOptionDescRec *) opts
1882 controller: (NSUserDefaultsController *) prefs
1884 if (! (self = [super init]))
1887 // instance variable
1888 userDefaultsController = prefs;
1891 NSURL *furl = [NSURL fileURLWithPath:xml_file];
1894 NSAssert1 (0, @"can't URLify \"%@\"", xml_file);
1899 NSXMLDocument *xmlDoc = [[NSXMLDocument alloc]
1900 initWithContentsOfURL:furl
1901 options:(NSXMLNodePreserveWhitespace |
1902 NSXMLNodePreserveCDATA)
1904 if (!xmlDoc || err) {
1906 NSAssert2 (0, @"XML Error: %@: %@",
1907 xml_file, [err localizedDescription]);
1911 traverse_tree (prefs, self, opts, [xmlDoc rootElement]);
1920 [userDefaultsController release];