chiark / gitweb /
Need to impose a _maximum_ width on edit boxes, as well as a minimum
[sgt-puzzles.git] / osx.m
1 /*
2  * Mac OS X / Cocoa front end to puzzles.
3  *
4  * Still to do:
5  * 
6  *  - I'd like to be able to call up context help for a specific
7  *    game at a time.
8  * 
9  * Mac interface issues that possibly could be done better:
10  * 
11  *  - is there a better approach to frontend_default_colour?
12  *
13  *  - do we need any more options in the Window menu?
14  *
15  *  - can / should we be doing anything with the titles of the
16  *    configuration boxes?
17  * 
18  *  - not sure what I should be doing about default window
19  *    placement. Centring new windows is a bit feeble, but what's
20  *    better? Is there a standard way to tell the OS "here's the
21  *    _size_ of window I want, now use your best judgment about the
22  *    initial position"?
23  *
24  *  - a brief frob of the Mac numeric keypad suggests that it
25  *    generates numbers no matter what you do. I wonder if I should
26  *    try to figure out a way of detecting keypad codes so I can
27  *    implement UP_LEFT and friends. Alternatively, perhaps I
28  *    should simply assign the number keys to UP_LEFT et al?
29  *    They're not in use for anything else right now.
30  *
31  *  - see if we can do anything to one-button-ise the multi-button
32  *    dependent puzzle UIs:
33  *     - Pattern is a _little_ unwieldy but not too bad (since
34  *       generally you never need the middle button unless you've
35  *       made a mistake, so it's just click versus command-click).
36  *     - Net is utterly vile; having normal click be one rotate and
37  *       command-click be the other introduces a horrid asymmetry,
38  *       and yet requiring a shift key for _each_ click would be
39  *       even worse because rotation feels as if it ought to be the
40  *       default action. I fear this is why the Flash Net had the
41  *       UI it did...
42  *
43  *  - Should we _return_ to a game configuration sheet once an
44  *    error is reported by midend_set_config, to allow the user to
45  *    correct the one faulty input and keep the other five OK ones?
46  *    The Apple `one sheet at a time' restriction would require me
47  *    to do this by closing the config sheet, opening the alert
48  *    sheet, and then reopening the config sheet when the alert is
49  *    closed; and the human interface types, who presumably
50  *    invented the one-sheet-at-a-time rule for good reasons, might
51  *    look with disfavour on me trying to get round them to fake a
52  *    nested sheet. On the other hand I think there are good
53  *    practical reasons for wanting it that way. Uncertain.
54  * 
55  * Grotty implementation details that could probably be improved:
56  * 
57  *  - I am _utterly_ unconvinced that NSImageView was the right way
58  *    to go about having a window with a reliable backing store! It
59  *    just doesn't feel right; NSImageView is a _control_. Is there
60  *    a simpler way?
61  * 
62  *  - Resizing is currently very bad; rather than bother to work
63  *    out how to resize the NSImageView, I just splatter and
64  *    recreate it.
65  */
66
67 #include <ctype.h>
68 #include <sys/time.h>
69 #import <Cocoa/Cocoa.h>
70 #include "puzzles.h"
71
72 /* ----------------------------------------------------------------------
73  * Global variables.
74  */
75
76 /*
77  * The `Type' menu. We frob this dynamically to allow the user to
78  * choose a preset set of settings from the current game.
79  */
80 NSMenu *typemenu;
81
82 /* ----------------------------------------------------------------------
83  * Miscellaneous support routines that aren't part of any object or
84  * clearly defined subsystem.
85  */
86
87 void fatal(char *fmt, ...)
88 {
89     va_list ap;
90     char errorbuf[2048];
91     NSAlert *alert;
92
93     va_start(ap, fmt);
94     vsnprintf(errorbuf, lenof(errorbuf), fmt, ap);
95     va_end(ap);
96
97     alert = [NSAlert alloc];
98     /*
99      * We may have come here because we ran out of memory, in which
100      * case it's entirely likely that that alloc will fail, so we
101      * should have a fallback of some sort.
102      */
103     if (!alert) {
104         fprintf(stderr, "fatal error (and NSAlert failed): %s\n", errorbuf);
105     } else {
106         alert = [[alert init] autorelease];
107         [alert addButtonWithTitle:@"Oh dear"];
108         [alert setInformativeText:[NSString stringWithCString:errorbuf]];
109         [alert runModal];
110     }
111     exit(1);
112 }
113
114 void frontend_default_colour(frontend *fe, float *output)
115 {
116     /* FIXME: Is there a system default we can tap into for this? */
117     output[0] = output[1] = output[2] = 0.8F;
118 }
119
120 void get_random_seed(void **randseed, int *randseedsize)
121 {
122     time_t *tp = snew(time_t);
123     time(tp);
124     *randseed = (void *)tp;
125     *randseedsize = sizeof(time_t);
126 }
127
128 /* ----------------------------------------------------------------------
129  * Tiny extension to NSMenuItem which carries a payload of a `void
130  * *', allowing several menu items to invoke the same message but
131  * pass different data through it.
132  */
133 @interface DataMenuItem : NSMenuItem
134 {
135     void *payload;
136 }
137 - (void)setPayload:(void *)d;
138 - (void *)getPayload;
139 @end
140 @implementation DataMenuItem
141 - (void)setPayload:(void *)d
142 {
143     payload = d;
144 }
145 - (void *)getPayload
146 {
147     return payload;
148 }
149 @end
150
151 /* ----------------------------------------------------------------------
152  * Utility routines for constructing OS X menus.
153  */
154
155 NSMenu *newmenu(const char *title)
156 {
157     return [[[NSMenu allocWithZone:[NSMenu menuZone]]
158              initWithTitle:[NSString stringWithCString:title]]
159             autorelease];
160 }
161
162 NSMenu *newsubmenu(NSMenu *parent, const char *title)
163 {
164     NSMenuItem *item;
165     NSMenu *child;
166
167     item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]]
168              initWithTitle:[NSString stringWithCString:title]
169              action:NULL
170              keyEquivalent:@""]
171             autorelease];
172     child = newmenu(title);
173     [item setEnabled:YES];
174     [item setSubmenu:child];
175     [parent addItem:item];
176     return child;
177 }
178
179 id initnewitem(NSMenuItem *item, NSMenu *parent, const char *title,
180                const char *key, id target, SEL action)
181 {
182     unsigned mask = NSCommandKeyMask;
183
184     if (key[strcspn(key, "-")]) {
185         while (*key && *key != '-') {
186             int c = tolower((unsigned char)*key);
187             if (c == 's') {
188                 mask |= NSShiftKeyMask;
189             } else if (c == 'o' || c == 'a') {
190                 mask |= NSAlternateKeyMask;
191             }
192             key++;
193         }
194         if (*key)
195             key++;
196     }
197
198     item = [[item initWithTitle:[NSString stringWithCString:title]
199              action:NULL
200              keyEquivalent:[NSString stringWithCString:key]]
201             autorelease];
202
203     if (*key)
204         [item setKeyEquivalentModifierMask: mask];
205
206     [item setEnabled:YES];
207     [item setTarget:target];
208     [item setAction:action];
209
210     [parent addItem:item];
211
212     return item;
213 }
214
215 NSMenuItem *newitem(NSMenu *parent, char *title, char *key,
216                     id target, SEL action)
217 {
218     return initnewitem([NSMenuItem allocWithZone:[NSMenu menuZone]],
219                        parent, title, key, target, action);
220 }
221
222 /* ----------------------------------------------------------------------
223  * The front end presented to midend.c.
224  * 
225  * This is mostly a subclass of NSWindow. The actual `frontend'
226  * structure passed to the midend contains a variety of pointers,
227  * including that window object but also including the image we
228  * draw on, an ImageView to display it in the window, and so on.
229  */
230
231 @class GameWindow;
232 @class MyImageView;
233
234 struct frontend {
235     GameWindow *window;
236     NSImage *image;
237     MyImageView *view;
238     NSColor **colours;
239     int ncolours;
240     int clipped;
241 };
242
243 @interface MyImageView : NSImageView
244 {
245     GameWindow *ourwin;
246 }
247 - (void)setWindow:(GameWindow *)win;
248 - (BOOL)isFlipped;
249 - (void)mouseEvent:(NSEvent *)ev button:(int)b;
250 - (void)mouseDown:(NSEvent *)ev;
251 - (void)mouseDragged:(NSEvent *)ev;
252 - (void)mouseUp:(NSEvent *)ev;
253 - (void)rightMouseDown:(NSEvent *)ev;
254 - (void)rightMouseDragged:(NSEvent *)ev;
255 - (void)rightMouseUp:(NSEvent *)ev;
256 - (void)otherMouseDown:(NSEvent *)ev;
257 - (void)otherMouseDragged:(NSEvent *)ev;
258 - (void)otherMouseUp:(NSEvent *)ev;
259 @end
260
261 @interface GameWindow : NSWindow
262 {
263     const game *ourgame;
264     midend_data *me;
265     struct frontend fe;
266     struct timeval last_time;
267     NSTimer *timer;
268     NSWindow *sheet;
269     config_item *cfg;
270     int cfg_which;
271     NSView **cfg_controls;
272     int cfg_ncontrols;
273     NSTextField *status;
274 }
275 - (id)initWithGame:(const game *)g;
276 - dealloc;
277 - (void)processButton:(int)b x:(int)x y:(int)y;
278 - (void)keyDown:(NSEvent *)ev;
279 - (void)activateTimer;
280 - (void)deactivateTimer;
281 - (void)setStatusLine:(NSString *)text;
282 @end
283
284 @implementation MyImageView
285
286 - (void)setWindow:(GameWindow *)win
287 {
288     ourwin = win;
289 }
290
291 - (BOOL)isFlipped
292 {
293     return YES;
294 }
295
296 - (void)mouseEvent:(NSEvent *)ev button:(int)b
297 {
298     NSPoint point = [self convertPoint:[ev locationInWindow] fromView:nil];
299     [ourwin processButton:b x:point.x y:point.y];
300 }
301
302 - (void)mouseDown:(NSEvent *)ev
303 {
304     unsigned mod = [ev modifierFlags];
305     [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_BUTTON :
306                                 (mod & NSShiftKeyMask) ? MIDDLE_BUTTON :
307                                 LEFT_BUTTON)];
308 }
309 - (void)mouseDragged:(NSEvent *)ev
310 {
311     unsigned mod = [ev modifierFlags];
312     [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_DRAG :
313                                 (mod & NSShiftKeyMask) ? MIDDLE_DRAG :
314                                 LEFT_DRAG)];
315 }
316 - (void)mouseUp:(NSEvent *)ev
317 {
318     unsigned mod = [ev modifierFlags];
319     [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_RELEASE :
320                                 (mod & NSShiftKeyMask) ? MIDDLE_RELEASE :
321                                 LEFT_RELEASE)];
322 }
323 - (void)rightMouseDown:(NSEvent *)ev
324 {
325     unsigned mod = [ev modifierFlags];
326     [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_BUTTON :
327                                 RIGHT_BUTTON)];
328 }
329 - (void)rightMouseDragged:(NSEvent *)ev
330 {
331     unsigned mod = [ev modifierFlags];
332     [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_DRAG :
333                                 RIGHT_DRAG)];
334 }
335 - (void)rightMouseUp:(NSEvent *)ev
336 {
337     unsigned mod = [ev modifierFlags];
338     [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_RELEASE :
339                                 RIGHT_RELEASE)];
340 }
341 - (void)otherMouseDown:(NSEvent *)ev
342 {
343     [self mouseEvent:ev button:MIDDLE_BUTTON];
344 }
345 - (void)otherMouseDragged:(NSEvent *)ev
346 {
347     [self mouseEvent:ev button:MIDDLE_DRAG];
348 }
349 - (void)otherMouseUp:(NSEvent *)ev
350 {
351     [self mouseEvent:ev button:MIDDLE_RELEASE];
352 }
353 @end
354
355 @implementation GameWindow
356 - (void)setupContentView
357 {
358     NSRect frame;
359     int w, h;
360
361     if (status) {
362         frame = [status frame];
363         frame.origin.y = frame.size.height;
364     } else
365         frame.origin.y = 0;
366     frame.origin.x = 0;
367
368     midend_size(me, &w, &h);
369     frame.size.width = w;
370     frame.size.height = h;
371
372     fe.image = [[NSImage alloc] initWithSize:frame.size];
373     [fe.image setFlipped:YES];
374     fe.view = [[MyImageView alloc] initWithFrame:frame];
375     [fe.view setImage:fe.image];
376     [fe.view setWindow:self];
377
378     midend_redraw(me);
379
380     [[self contentView] addSubview:fe.view];
381 }
382 - (id)initWithGame:(const game *)g
383 {
384     NSRect rect = { {0,0}, {0,0} }, rect2;
385     int w, h;
386
387     ourgame = g;
388
389     fe.window = self;
390
391     me = midend_new(&fe, ourgame);
392     /*
393      * If we ever need to open a fresh window using a provided game
394      * ID, I think the right thing is to move most of this method
395      * into a new initWithGame:gameID: method, and have
396      * initWithGame: simply call that one and pass it NULL.
397      */
398     midend_new_game(me);
399     midend_size(me, &w, &h);
400     rect.size.width = w;
401     rect.size.height = h;
402
403     /*
404      * Create the status bar, which will just be an NSTextField.
405      */
406     if (ourgame->wants_statusbar()) {
407         status = [[NSTextField alloc] initWithFrame:NSMakeRect(0,0,100,50)];
408         [status setEditable:NO];
409         [status setSelectable:NO];
410         [status setBordered:YES];
411         [status setBezeled:YES];
412         [status setBezelStyle:NSTextFieldSquareBezel];
413         [status setDrawsBackground:YES];
414         [[status cell] setTitle:@""];
415         [status sizeToFit];
416         rect2 = [status frame];
417         rect.size.height += rect2.size.height;
418         rect2.size.width = rect.size.width;
419         rect2.origin.x = rect2.origin.y = 0;
420         [status setFrame:rect2];
421     } else
422         status = nil;
423
424     self = [super initWithContentRect:rect
425             styleMask:(NSTitledWindowMask | NSMiniaturizableWindowMask |
426                        NSClosableWindowMask)
427             backing:NSBackingStoreBuffered
428             defer:YES];
429     [self setTitle:[NSString stringWithCString:ourgame->name]];
430
431     {
432         float *colours;
433         int i, ncolours;
434
435         colours = midend_colours(me, &ncolours);
436         fe.ncolours = ncolours;
437         fe.colours = snewn(ncolours, NSColor *);
438
439         for (i = 0; i < ncolours; i++) {
440             fe.colours[i] = [[NSColor colorWithDeviceRed:colours[i*3]
441                               green:colours[i*3+1] blue:colours[i*3+2]
442                               alpha:1.0] retain];
443         }
444     }
445
446     [self setupContentView];
447     if (status)
448         [[self contentView] addSubview:status];
449     [self setIgnoresMouseEvents:NO];
450
451     [self center];                     /* :-) */
452
453     return self;
454 }
455
456 - dealloc
457 {
458     int i;
459     for (i = 0; i < fe.ncolours; i++) {
460         [fe.colours[i] release];
461     }
462     sfree(fe.colours);
463     midend_free(me);
464     return [super dealloc];
465 }
466
467 - (void)processButton:(int)b x:(int)x y:(int)y
468 {
469     if (!midend_process_key(me, x, y, b))
470         [self close];
471 }
472
473 - (void)keyDown:(NSEvent *)ev
474 {
475     NSString *s = [ev characters];
476     int i, n = [s length];
477
478     for (i = 0; i < n; i++) {
479         int c = [s characterAtIndex:i];
480
481         /*
482          * ASCII gets passed straight to midend_process_key.
483          * Anything above that has to be translated to our own
484          * function key codes.
485          */
486         if (c >= 0x80) {
487             switch (c) {
488               case NSUpArrowFunctionKey:
489                 c = CURSOR_UP;
490                 break;
491               case NSDownArrowFunctionKey:
492                 c = CURSOR_DOWN;
493                 break;
494               case NSLeftArrowFunctionKey:
495                 c = CURSOR_LEFT;
496                 break;
497               case NSRightArrowFunctionKey:
498                 c = CURSOR_RIGHT;
499                 break;
500               default:
501                 continue;
502             }
503         }
504
505         [self processButton:c x:-1 y:-1];
506     }
507 }
508
509 - (void)activateTimer
510 {
511     if (timer != nil)
512         return;
513
514     timer = [NSTimer scheduledTimerWithTimeInterval:0.02
515              target:self selector:@selector(timerTick:)
516              userInfo:nil repeats:YES];
517     gettimeofday(&last_time, NULL);
518 }
519
520 - (void)deactivateTimer
521 {
522     if (timer == nil)
523         return;
524
525     [timer invalidate];
526     timer = nil;
527 }
528
529 - (void)timerTick:(id)sender
530 {
531     struct timeval now;
532     float elapsed;
533     gettimeofday(&now, NULL);
534     elapsed = ((now.tv_usec - last_time.tv_usec) * 0.000001F +
535                (now.tv_sec - last_time.tv_sec));
536     midend_timer(me, elapsed);
537     last_time = now;
538 }
539
540 - (void)newGame:(id)sender
541 {
542     [self processButton:'n' x:-1 y:-1];
543 }
544 - (void)restartGame:(id)sender
545 {
546     [self processButton:'r' x:-1 y:-1];
547 }
548 - (void)undoMove:(id)sender
549 {
550     [self processButton:'u' x:-1 y:-1];
551 }
552 - (void)redoMove:(id)sender
553 {
554     [self processButton:'r'&0x1F x:-1 y:-1];
555 }
556
557 - (void)clearTypeMenu
558 {
559     while ([typemenu numberOfItems] > 1)
560         [typemenu removeItemAtIndex:0];
561 }
562
563 - (void)becomeKeyWindow
564 {
565     int n;
566
567     [self clearTypeMenu];
568
569     [super becomeKeyWindow];
570
571     n = midend_num_presets(me);
572
573     if (n > 0) {
574         [typemenu insertItem:[NSMenuItem separatorItem] atIndex:0];
575         while (n--) {
576             char *name;
577             game_params *params;
578             DataMenuItem *item;
579
580             midend_fetch_preset(me, n, &name, &params);
581
582             item = [[[DataMenuItem alloc]
583                      initWithTitle:[NSString stringWithCString:name]
584                      action:NULL keyEquivalent:@""]
585                     autorelease];
586
587             [item setEnabled:YES];
588             [item setTarget:self];
589             [item setAction:@selector(presetGame:)];
590             [item setPayload:params];
591
592             [typemenu insertItem:item atIndex:0];
593         }
594     }
595 }
596
597 - (void)resignKeyWindow
598 {
599     [self clearTypeMenu];
600     [super resignKeyWindow];
601 }
602
603 - (void)close
604 {
605     [self clearTypeMenu];
606     [super close];
607 }
608
609 - (void)resizeForNewGameParams
610 {
611     NSSize size = {0,0};
612     int w, h;
613
614     midend_size(me, &w, &h);
615     size.width = w;
616     size.height = h;
617
618     if (status) {
619         NSRect frame = [status frame];
620         size.height += frame.size.height;
621         frame.size.width = size.width;
622         [status setFrame:frame];
623     }
624
625     NSDisableScreenUpdates();
626     [self setContentSize:size];
627     [self setupContentView];
628     NSEnableScreenUpdates();
629 }
630
631 - (void)presetGame:(id)sender
632 {
633     game_params *params = [sender getPayload];
634
635     midend_set_params(me, params);
636     midend_new_game(me);
637
638     [self resizeForNewGameParams];
639 }
640
641 - (void)startConfigureSheet:(int)which
642 {
643     NSButton *ok, *cancel;
644     int actw, acth, leftw, rightw, totalw, h, thish, y;
645     int k;
646     NSRect rect, tmprect;
647     const int SPACING = 16;
648     char *title;
649     config_item *i;
650     int cfg_controlsize;
651     NSTextField *tf;
652     NSButton *b;
653     NSPopUpButton *pb;
654
655     assert(sheet == NULL);
656
657     /*
658      * Every control we create here is going to have this size
659      * until we tell it to calculate a better one.
660      */
661     tmprect = NSMakeRect(0, 0, 100, 50);
662
663     /*
664      * Set up OK and Cancel buttons. (Actually, MacOS doesn't seem
665      * to be fond of generic OK and Cancel wording, so I'm going to
666      * rename them to something nicer.)
667      */
668     actw = acth = 0;
669
670     cancel = [[NSButton alloc] initWithFrame:tmprect];
671     [cancel setBezelStyle:NSRoundedBezelStyle];
672     [cancel setTitle:@"Abandon"];
673     [cancel setTarget:self];
674     [cancel setKeyEquivalent:@"\033"];
675     [cancel setAction:@selector(sheetCancelButton:)];
676     [cancel sizeToFit];
677     rect = [cancel frame];
678     if (actw < rect.size.width) actw = rect.size.width;
679     if (acth < rect.size.height) acth = rect.size.height;
680
681     ok = [[NSButton alloc] initWithFrame:tmprect];
682     [ok setBezelStyle:NSRoundedBezelStyle];
683     [ok setTitle:@"Accept"];
684     [ok setTarget:self];
685     [ok setKeyEquivalent:@"\r"];
686     [ok setAction:@selector(sheetOKButton:)];
687     [ok sizeToFit];
688     rect = [ok frame];
689     if (actw < rect.size.width) actw = rect.size.width;
690     if (acth < rect.size.height) acth = rect.size.height;
691
692     totalw = SPACING + 2 * actw;
693     h = 2 * SPACING + acth;
694
695     /*
696      * Now fetch the midend config data and go through it creating
697      * controls.
698      */
699     cfg = midend_get_config(me, which, &title);
700     sfree(title);                      /* FIXME: should we use this somehow? */
701     cfg_which = which;
702
703     cfg_ncontrols = cfg_controlsize = 0;
704     cfg_controls = NULL;
705     leftw = rightw = 0;
706     for (i = cfg; i->type != C_END; i++) {
707         if (cfg_controlsize < cfg_ncontrols + 5) {
708             cfg_controlsize = cfg_ncontrols + 32;
709             cfg_controls = sresize(cfg_controls, cfg_controlsize, NSView *);
710         }
711
712         thish = 0;
713
714         switch (i->type) {
715           case C_STRING:
716             /*
717              * Two NSTextFields, one being a label and the other
718              * being an edit box.
719              */
720
721             tf = [[NSTextField alloc] initWithFrame:tmprect];
722             [tf setEditable:NO];
723             [tf setSelectable:NO];
724             [tf setBordered:NO];
725             [tf setDrawsBackground:NO];
726             [[tf cell] setTitle:[NSString stringWithCString:i->name]];
727             [tf sizeToFit];
728             rect = [tf frame];
729             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
730             if (leftw < rect.size.width + 1) leftw = rect.size.width + 1;
731             cfg_controls[cfg_ncontrols++] = tf;
732
733             tf = [[NSTextField alloc] initWithFrame:tmprect];
734             [tf setEditable:YES];
735             [tf setSelectable:YES];
736             [tf setBordered:YES];
737             [[tf cell] setTitle:[NSString stringWithCString:i->sval]];
738             [tf sizeToFit];
739             rect = [tf frame];
740             /*
741              * We impose a minimum and maximum width on editable
742              * NSTextFields. If we allow them to size themselves to
743              * the contents of the text within them, then they will
744              * look very silly if that text is only one or two
745              * characters, and equally silly if it's an absolutely
746              * enormous Rectangles or Pattern game ID!
747              */
748             if (rect.size.width < 75) rect.size.width = 75;
749             if (rect.size.width > 400) rect.size.width = 400;
750
751             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
752             if (rightw < rect.size.width + 1) rightw = rect.size.width + 1;
753             cfg_controls[cfg_ncontrols++] = tf;
754             break;
755
756           case C_BOOLEAN:
757             /*
758              * A checkbox is an NSButton with a type of
759              * NSSwitchButton.
760              */
761             b = [[NSButton alloc] initWithFrame:tmprect];
762             [b setBezelStyle:NSRoundedBezelStyle];
763             [b setButtonType:NSSwitchButton];
764             [b setTitle:[NSString stringWithCString:i->name]];
765             [b sizeToFit];
766             [b setState:(i->ival ? NSOnState : NSOffState)];
767             rect = [b frame];
768             if (totalw < rect.size.width + 1) totalw = rect.size.width + 1;
769             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
770             cfg_controls[cfg_ncontrols++] = b;
771             break;
772
773           case C_CHOICES:
774             /*
775              * A pop-up menu control is an NSPopUpButton, which
776              * takes an embedded NSMenu. We also need an
777              * NSTextField to act as a label.
778              */
779
780             tf = [[NSTextField alloc] initWithFrame:tmprect];
781             [tf setEditable:NO];
782             [tf setSelectable:NO];
783             [tf setBordered:NO];
784             [tf setDrawsBackground:NO];
785             [[tf cell] setTitle:[NSString stringWithCString:i->name]];
786             [tf sizeToFit];
787             rect = [tf frame];
788             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
789             if (leftw < rect.size.width + 1) leftw = rect.size.width + 1;
790             cfg_controls[cfg_ncontrols++] = tf;
791
792             pb = [[NSPopUpButton alloc] initWithFrame:tmprect pullsDown:NO];
793             [pb setBezelStyle:NSRoundedBezelStyle];
794             {
795                 char c, *p;
796
797                 p = i->sval;
798                 c = *p++;
799                 while (*p) {
800                     char *q;
801
802                     q = p;
803                     while (*p && *p != c) p++;
804
805                     [pb addItemWithTitle:[NSString stringWithCString:q
806                                           length:p-q]];
807
808                     if (*p) p++;
809                 }
810             }
811             [pb selectItemAtIndex:i->ival];
812             [pb sizeToFit];
813
814             rect = [pb frame];
815             if (rightw < rect.size.width + 1) rightw = rect.size.width + 1;
816             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
817             cfg_controls[cfg_ncontrols++] = pb;
818             break;
819         }
820
821         h += SPACING + thish;
822     }
823
824     if (totalw < leftw + SPACING + rightw)
825         totalw = leftw + SPACING + rightw;
826     if (totalw > leftw + SPACING + rightw) {
827         int excess = totalw - (leftw + SPACING + rightw);
828         int leftexcess = leftw * excess / (leftw + rightw);
829         int rightexcess = excess - leftexcess;
830         leftw += leftexcess;
831         rightw += rightexcess;
832     }
833
834     /*
835      * Now go through the list again, setting the final position
836      * for each control.
837      */
838     k = 0;
839     y = h;
840     for (i = cfg; i->type != C_END; i++) {
841         y -= SPACING;
842         thish = 0;
843         switch (i->type) {
844           case C_STRING:
845           case C_CHOICES:
846             /*
847              * These two are treated identically, since both expect
848              * a control on the left and another on the right.
849              */
850             rect = [cfg_controls[k] frame];
851             if (thish < rect.size.height + 1)
852                 thish = rect.size.height + 1;
853             rect = [cfg_controls[k+1] frame];
854             if (thish < rect.size.height + 1)
855                 thish = rect.size.height + 1;
856             rect = [cfg_controls[k] frame];
857             rect.origin.y = y - thish/2 - rect.size.height/2;
858             rect.origin.x = SPACING;
859             rect.size.width = leftw;
860             [cfg_controls[k] setFrame:rect];
861             rect = [cfg_controls[k+1] frame];
862             rect.origin.y = y - thish/2 - rect.size.height/2;
863             rect.origin.x = 2 * SPACING + leftw;
864             rect.size.width = rightw;
865             [cfg_controls[k+1] setFrame:rect];
866             k += 2;
867             break;
868
869           case C_BOOLEAN:
870             rect = [cfg_controls[k] frame];
871             if (thish < rect.size.height + 1)
872                 thish = rect.size.height + 1;
873             rect.origin.y = y - thish/2 - rect.size.height/2;
874             rect.origin.x = SPACING;
875             rect.size.width = totalw;
876             [cfg_controls[k] setFrame:rect];
877             k++;
878             break;
879         }
880         y -= thish;
881     }
882
883     assert(k == cfg_ncontrols);
884
885     [cancel setFrame:NSMakeRect(SPACING+totalw/4-actw/2, SPACING, actw, acth)];
886     [ok setFrame:NSMakeRect(SPACING+3*totalw/4-actw/2, SPACING, actw, acth)];
887
888     sheet = [[NSWindow alloc]
889              initWithContentRect:NSMakeRect(0,0,totalw + 2*SPACING,h)
890              styleMask:NSTitledWindowMask | NSClosableWindowMask
891              backing:NSBackingStoreBuffered
892              defer:YES];
893
894     [[sheet contentView] addSubview:cancel];
895     [[sheet contentView] addSubview:ok];
896
897     for (k = 0; k < cfg_ncontrols; k++)
898         [[sheet contentView] addSubview:cfg_controls[k]];
899
900     [NSApp beginSheet:sheet modalForWindow:self
901      modalDelegate:nil didEndSelector:nil contextInfo:nil];
902 }
903
904 - (void)specificGame:(id)sender
905 {
906     [self startConfigureSheet:CFG_SEED];
907 }
908
909 - (void)customGameType:(id)sender
910 {
911     [self startConfigureSheet:CFG_SETTINGS];
912 }
913
914 - (void)sheetEndWithStatus:(BOOL)update
915 {
916     assert(sheet != NULL);
917     [NSApp endSheet:sheet];
918     [sheet orderOut:self];
919     sheet = NULL;
920     if (update) {
921         int k;
922         config_item *i;
923         char *error;
924
925         k = 0;
926         for (i = cfg; i->type != C_END; i++) {
927             switch (i->type) {
928               case C_STRING:
929                 sfree(i->sval);
930                 i->sval = dupstr([[[(id)cfg_controls[k+1] cell]
931                                    title] cString]);
932                 k += 2;
933                 break;
934               case C_BOOLEAN:
935                 i->ival = [(id)cfg_controls[k] state] == NSOnState;
936                 k++;
937                 break;
938               case C_CHOICES:
939                 i->ival = [(id)cfg_controls[k+1] indexOfSelectedItem];
940                 k += 2;
941                 break;
942             }
943         }
944
945         error = midend_set_config(me, cfg_which, cfg);
946         if (error) {
947             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
948             [alert addButtonWithTitle:@"Bah"];
949             [alert setInformativeText:[NSString stringWithCString:error]];
950             [alert beginSheetModalForWindow:self modalDelegate:nil
951              didEndSelector:nil contextInfo:nil];
952         } else {
953             midend_new_game(me);
954             [self resizeForNewGameParams];
955         }
956     }
957     sfree(cfg_controls);
958     cfg_controls = NULL;
959 }
960 - (void)sheetOKButton:(id)sender
961 {
962     [self sheetEndWithStatus:YES];
963 }
964 - (void)sheetCancelButton:(id)sender
965 {
966     [self sheetEndWithStatus:NO];
967 }
968
969 - (void)setStatusLine:(NSString *)text
970 {
971     [[status cell] setTitle:text];
972 }
973
974 @end
975
976 /*
977  * Drawing routines called by the midend.
978  */
979 void draw_polygon(frontend *fe, int *coords, int npoints,
980                   int fill, int colour)
981 {
982     NSBezierPath *path = [NSBezierPath bezierPath];
983     int i;
984
985     [[NSGraphicsContext currentContext] setShouldAntialias:YES];
986
987     assert(colour >= 0 && colour < fe->ncolours);
988     [fe->colours[colour] set];
989
990     for (i = 0; i < npoints; i++) {
991         NSPoint p = { coords[i*2] + 0.5, coords[i*2+1] + 0.5 };
992         if (i == 0)
993             [path moveToPoint:p];
994         else
995             [path lineToPoint:p];
996     }
997
998     [path closePath];
999
1000     if (fill)
1001         [path fill];
1002     else
1003         [path stroke];
1004 }
1005 void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour)
1006 {
1007     NSBezierPath *path = [NSBezierPath bezierPath];
1008     NSPoint p1 = { x1 + 0.5, y1 + 0.5 }, p2 = { x2 + 0.5, y2 + 0.5 };
1009
1010     [[NSGraphicsContext currentContext] setShouldAntialias:NO];
1011
1012     assert(colour >= 0 && colour < fe->ncolours);
1013     [fe->colours[colour] set];
1014
1015     [path moveToPoint:p1];
1016     [path lineToPoint:p2];
1017     [path stroke];
1018 }
1019 void draw_rect(frontend *fe, int x, int y, int w, int h, int colour)
1020 {
1021     NSRect r = { {x,y}, {w,h} };
1022
1023     [[NSGraphicsContext currentContext] setShouldAntialias:NO];
1024
1025     assert(colour >= 0 && colour < fe->ncolours);
1026     [fe->colours[colour] set];
1027
1028     NSRectFill(r);
1029 }
1030 void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
1031                int align, int colour, char *text)
1032 {
1033     NSString *string = [NSString stringWithCString:text];
1034     NSDictionary *attr;
1035     NSFont *font;
1036     NSSize size;
1037     NSPoint point;
1038
1039     [[NSGraphicsContext currentContext] setShouldAntialias:YES];
1040
1041     assert(colour >= 0 && colour < fe->ncolours);
1042
1043     if (fonttype == FONT_FIXED)
1044         font = [NSFont userFixedPitchFontOfSize:fontsize];
1045     else
1046         font = [NSFont userFontOfSize:fontsize];
1047
1048     attr = [NSDictionary dictionaryWithObjectsAndKeys:
1049             fe->colours[colour], NSForegroundColorAttributeName,
1050             font, NSFontAttributeName, nil];
1051
1052     point.x = x;
1053     point.y = y;
1054
1055     size = [string sizeWithAttributes:attr];
1056     if (align & ALIGN_HRIGHT)
1057         point.x -= size.width;
1058     else if (align & ALIGN_HCENTRE)
1059         point.x -= size.width / 2;
1060     if (align & ALIGN_VCENTRE)
1061         point.y -= size.height / 2;
1062
1063     [string drawAtPoint:point withAttributes:attr];
1064 }
1065 void draw_update(frontend *fe, int x, int y, int w, int h)
1066 {
1067     /*
1068      * FIXME: It seems odd that nothing is required here, although
1069      * everything _seems_ to work with this routine empty. Possibly
1070      * we're always updating the entire window, and there's a
1071      * better way which would involve doing something in here?
1072      */
1073 }
1074 void clip(frontend *fe, int x, int y, int w, int h)
1075 {
1076     NSRect r = { {x,y}, {w,h} };
1077
1078     if (!fe->clipped)
1079         [[NSGraphicsContext currentContext] saveGraphicsState];
1080     [NSBezierPath clipRect:r];
1081     fe->clipped = TRUE;
1082 }
1083 void unclip(frontend *fe)
1084 {
1085     if (fe->clipped)
1086         [[NSGraphicsContext currentContext] restoreGraphicsState];
1087     fe->clipped = FALSE;
1088 }
1089 void start_draw(frontend *fe)
1090 {
1091     [fe->image lockFocus];
1092     fe->clipped = FALSE;
1093 }
1094 void end_draw(frontend *fe)
1095 {
1096     [fe->image unlockFocus];
1097     [fe->view setNeedsDisplay];
1098 }
1099
1100 void deactivate_timer(frontend *fe)
1101 {
1102     [fe->window deactivateTimer];
1103 }
1104 void activate_timer(frontend *fe)
1105 {
1106     [fe->window activateTimer];
1107 }
1108
1109 void status_bar(frontend *fe, char *text)
1110 {
1111     [fe->window setStatusLine:[NSString stringWithCString:text]];
1112 }
1113
1114 /* ----------------------------------------------------------------------
1115  * AppController: the object which receives the messages from all
1116  * menu selections that aren't standard OS X functions.
1117  */
1118 @interface AppController : NSObject
1119 {
1120 }
1121 - (void)newGame:(id)sender;
1122 @end
1123
1124 @implementation AppController
1125
1126 - (void)newGame:(id)sender
1127 {
1128     const game *g = [sender getPayload];
1129     id win;
1130
1131     win = [[GameWindow alloc] initWithGame:g];
1132     [win makeKeyAndOrderFront:self];
1133 }
1134
1135 - (NSMenu *)applicationDockMenu:(NSApplication *)sender
1136 {
1137     NSMenu *menu = newmenu("Dock Menu");
1138     {
1139         int i;
1140
1141         for (i = 0; i < gamecount; i++) {
1142             id item =
1143                 initnewitem([DataMenuItem allocWithZone:[NSMenu menuZone]],
1144                             menu, gamelist[i]->name, "", self,
1145                             @selector(newGame:));
1146             [item setPayload:(void *)gamelist[i]];
1147         }
1148     }
1149     return menu;
1150 }
1151
1152 @end
1153
1154 /* ----------------------------------------------------------------------
1155  * Main program. Constructs the menus and runs the application.
1156  */
1157 int main(int argc, char **argv)
1158 {
1159     NSAutoreleasePool *pool;
1160     NSMenu *menu;
1161     NSMenuItem *item;
1162     AppController *controller;
1163     NSImage *icon;
1164
1165     pool = [[NSAutoreleasePool alloc] init];
1166
1167     icon = [NSImage imageNamed:@"NSApplicationIcon"];
1168     [NSApplication sharedApplication];
1169     [NSApp setApplicationIconImage:icon];
1170
1171     controller = [[[AppController alloc] init] autorelease];
1172     [NSApp setDelegate:controller];
1173
1174     [NSApp setMainMenu: newmenu("Main Menu")];
1175
1176     menu = newsubmenu([NSApp mainMenu], "Apple Menu");
1177     [NSApp setServicesMenu:newsubmenu(menu, "Services")];
1178     [menu addItem:[NSMenuItem separatorItem]];
1179     item = newitem(menu, "Hide Puzzles", "h", NSApp, @selector(hide:));
1180     item = newitem(menu, "Hide Others", "o-h", NSApp, @selector(hideOtherApplications:));
1181     item = newitem(menu, "Show All", "", NSApp, @selector(unhideAllApplications:));
1182     [menu addItem:[NSMenuItem separatorItem]];
1183     item = newitem(menu, "Quit", "q", NSApp, @selector(terminate:));
1184     [NSApp setAppleMenu: menu];
1185
1186     menu = newsubmenu([NSApp mainMenu], "Open");
1187     {
1188         int i;
1189
1190         for (i = 0; i < gamecount; i++) {
1191             id item =
1192                 initnewitem([DataMenuItem allocWithZone:[NSMenu menuZone]],
1193                             menu, gamelist[i]->name, "", controller,
1194                             @selector(newGame:));
1195             [item setPayload:(void *)gamelist[i]];
1196         }
1197     }
1198
1199     menu = newsubmenu([NSApp mainMenu], "Game");
1200     item = newitem(menu, "New", "n", NULL, @selector(newGame:));
1201     item = newitem(menu, "Restart", "r", NULL, @selector(restartGame:));
1202     item = newitem(menu, "Specific", "", NULL, @selector(specificGame:));
1203     [menu addItem:[NSMenuItem separatorItem]];
1204     item = newitem(menu, "Undo", "z", NULL, @selector(undoMove:));
1205     item = newitem(menu, "Redo", "S-z", NULL, @selector(redoMove:));
1206     [menu addItem:[NSMenuItem separatorItem]];
1207     item = newitem(menu, "Close", "w", NULL, @selector(performClose:));
1208
1209     menu = newsubmenu([NSApp mainMenu], "Type");
1210     typemenu = menu;
1211     item = newitem(menu, "Custom", "", NULL, @selector(customGameType:));
1212
1213     menu = newsubmenu([NSApp mainMenu], "Window");
1214     [NSApp setWindowsMenu: menu];
1215     item = newitem(menu, "Minimise Window", "m", NULL, @selector(performMiniaturize:));
1216
1217     menu = newsubmenu([NSApp mainMenu], "Help");
1218     item = newitem(menu, "Puzzles Help", "?", NSApp, @selector(showHelp:));
1219
1220     [NSApp run];
1221     [pool release];
1222 }