chiark / gitweb /
Improve OS X help: split back up into multiple files (thanks to
[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             /* We impose a minimum width on editable NSTextFields to
734              * stop them looking _completely_ silly. */
735             if (rightw < 75) rightw = 75;
736
737             tf = [[NSTextField alloc] initWithFrame:tmprect];
738             [tf setEditable:YES];
739             [tf setSelectable:YES];
740             [tf setBordered:YES];
741             [[tf cell] setTitle:[NSString stringWithCString:i->sval]];
742             [tf sizeToFit];
743             rect = [tf frame];
744             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
745             if (rightw < rect.size.width + 1) rightw = rect.size.width + 1;
746             cfg_controls[cfg_ncontrols++] = tf;
747             break;
748
749           case C_BOOLEAN:
750             /*
751              * A checkbox is an NSButton with a type of
752              * NSSwitchButton.
753              */
754             b = [[NSButton alloc] initWithFrame:tmprect];
755             [b setBezelStyle:NSRoundedBezelStyle];
756             [b setButtonType:NSSwitchButton];
757             [b setTitle:[NSString stringWithCString:i->name]];
758             [b sizeToFit];
759             [b setState:(i->ival ? NSOnState : NSOffState)];
760             rect = [b frame];
761             if (totalw < rect.size.width + 1) totalw = rect.size.width + 1;
762             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
763             cfg_controls[cfg_ncontrols++] = b;
764             break;
765
766           case C_CHOICES:
767             /*
768              * A pop-up menu control is an NSPopUpButton, which
769              * takes an embedded NSMenu. We also need an
770              * NSTextField to act as a label.
771              */
772
773             tf = [[NSTextField alloc] initWithFrame:tmprect];
774             [tf setEditable:NO];
775             [tf setSelectable:NO];
776             [tf setBordered:NO];
777             [tf setDrawsBackground:NO];
778             [[tf cell] setTitle:[NSString stringWithCString:i->name]];
779             [tf sizeToFit];
780             rect = [tf frame];
781             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
782             if (leftw < rect.size.width + 1) leftw = rect.size.width + 1;
783             cfg_controls[cfg_ncontrols++] = tf;
784
785             pb = [[NSPopUpButton alloc] initWithFrame:tmprect pullsDown:NO];
786             [pb setBezelStyle:NSRoundedBezelStyle];
787             {
788                 char c, *p;
789
790                 p = i->sval;
791                 c = *p++;
792                 while (*p) {
793                     char *q;
794
795                     q = p;
796                     while (*p && *p != c) p++;
797
798                     [pb addItemWithTitle:[NSString stringWithCString:q
799                                           length:p-q]];
800
801                     if (*p) p++;
802                 }
803             }
804             [pb selectItemAtIndex:i->ival];
805             [pb sizeToFit];
806
807             rect = [pb frame];
808             if (rightw < rect.size.width + 1) rightw = rect.size.width + 1;
809             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
810             cfg_controls[cfg_ncontrols++] = pb;
811             break;
812         }
813
814         h += SPACING + thish;
815     }
816
817     if (totalw < leftw + SPACING + rightw)
818         totalw = leftw + SPACING + rightw;
819     if (totalw > leftw + SPACING + rightw) {
820         int excess = totalw - (leftw + SPACING + rightw);
821         int leftexcess = leftw * excess / (leftw + rightw);
822         int rightexcess = excess - leftexcess;
823         leftw += leftexcess;
824         rightw += rightexcess;
825     }
826
827     /*
828      * Now go through the list again, setting the final position
829      * for each control.
830      */
831     k = 0;
832     y = h;
833     for (i = cfg; i->type != C_END; i++) {
834         y -= SPACING;
835         thish = 0;
836         switch (i->type) {
837           case C_STRING:
838           case C_CHOICES:
839             /*
840              * These two are treated identically, since both expect
841              * a control on the left and another on the right.
842              */
843             rect = [cfg_controls[k] frame];
844             if (thish < rect.size.height + 1)
845                 thish = rect.size.height + 1;
846             rect = [cfg_controls[k+1] frame];
847             if (thish < rect.size.height + 1)
848                 thish = rect.size.height + 1;
849             rect = [cfg_controls[k] frame];
850             rect.origin.y = y - thish/2 - rect.size.height/2;
851             rect.origin.x = SPACING;
852             rect.size.width = leftw;
853             [cfg_controls[k] setFrame:rect];
854             rect = [cfg_controls[k+1] frame];
855             rect.origin.y = y - thish/2 - rect.size.height/2;
856             rect.origin.x = 2 * SPACING + leftw;
857             rect.size.width = rightw;
858             [cfg_controls[k+1] setFrame:rect];
859             k += 2;
860             break;
861
862           case C_BOOLEAN:
863             rect = [cfg_controls[k] frame];
864             if (thish < rect.size.height + 1)
865                 thish = rect.size.height + 1;
866             rect.origin.y = y - thish/2 - rect.size.height/2;
867             rect.origin.x = SPACING;
868             rect.size.width = totalw;
869             [cfg_controls[k] setFrame:rect];
870             k++;
871             break;
872         }
873         y -= thish;
874     }
875
876     assert(k == cfg_ncontrols);
877
878     [cancel setFrame:NSMakeRect(SPACING+totalw/4-actw/2, SPACING, actw, acth)];
879     [ok setFrame:NSMakeRect(SPACING+3*totalw/4-actw/2, SPACING, actw, acth)];
880
881     sheet = [[NSWindow alloc]
882              initWithContentRect:NSMakeRect(0,0,totalw + 2*SPACING,h)
883              styleMask:NSTitledWindowMask | NSClosableWindowMask
884              backing:NSBackingStoreBuffered
885              defer:YES];
886
887     [[sheet contentView] addSubview:cancel];
888     [[sheet contentView] addSubview:ok];
889
890     for (k = 0; k < cfg_ncontrols; k++)
891         [[sheet contentView] addSubview:cfg_controls[k]];
892
893     [NSApp beginSheet:sheet modalForWindow:self
894      modalDelegate:nil didEndSelector:nil contextInfo:nil];
895 }
896
897 - (void)specificGame:(id)sender
898 {
899     [self startConfigureSheet:CFG_SEED];
900 }
901
902 - (void)customGameType:(id)sender
903 {
904     [self startConfigureSheet:CFG_SETTINGS];
905 }
906
907 - (void)sheetEndWithStatus:(BOOL)update
908 {
909     assert(sheet != NULL);
910     [NSApp endSheet:sheet];
911     [sheet orderOut:self];
912     sheet = NULL;
913     if (update) {
914         int k;
915         config_item *i;
916         char *error;
917
918         k = 0;
919         for (i = cfg; i->type != C_END; i++) {
920             switch (i->type) {
921               case C_STRING:
922                 sfree(i->sval);
923                 i->sval = dupstr([[[(id)cfg_controls[k+1] cell]
924                                    title] cString]);
925                 k += 2;
926                 break;
927               case C_BOOLEAN:
928                 i->ival = [(id)cfg_controls[k] state] == NSOnState;
929                 k++;
930                 break;
931               case C_CHOICES:
932                 i->ival = [(id)cfg_controls[k+1] indexOfSelectedItem];
933                 k += 2;
934                 break;
935             }
936         }
937
938         error = midend_set_config(me, cfg_which, cfg);
939         if (error) {
940             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
941             [alert addButtonWithTitle:@"Bah"];
942             [alert setInformativeText:[NSString stringWithCString:error]];
943             [alert beginSheetModalForWindow:self modalDelegate:nil
944              didEndSelector:nil contextInfo:nil];
945         } else {
946             midend_new_game(me);
947             [self resizeForNewGameParams];
948         }
949     }
950     sfree(cfg_controls);
951     cfg_controls = NULL;
952 }
953 - (void)sheetOKButton:(id)sender
954 {
955     [self sheetEndWithStatus:YES];
956 }
957 - (void)sheetCancelButton:(id)sender
958 {
959     [self sheetEndWithStatus:NO];
960 }
961
962 - (void)setStatusLine:(NSString *)text
963 {
964     [[status cell] setTitle:text];
965 }
966
967 @end
968
969 /*
970  * Drawing routines called by the midend.
971  */
972 void draw_polygon(frontend *fe, int *coords, int npoints,
973                   int fill, int colour)
974 {
975     NSBezierPath *path = [NSBezierPath bezierPath];
976     int i;
977
978     [[NSGraphicsContext currentContext] setShouldAntialias:YES];
979
980     assert(colour >= 0 && colour < fe->ncolours);
981     [fe->colours[colour] set];
982
983     for (i = 0; i < npoints; i++) {
984         NSPoint p = { coords[i*2] + 0.5, coords[i*2+1] + 0.5 };
985         if (i == 0)
986             [path moveToPoint:p];
987         else
988             [path lineToPoint:p];
989     }
990
991     [path closePath];
992
993     if (fill)
994         [path fill];
995     else
996         [path stroke];
997 }
998 void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour)
999 {
1000     NSBezierPath *path = [NSBezierPath bezierPath];
1001     NSPoint p1 = { x1 + 0.5, y1 + 0.5 }, p2 = { x2 + 0.5, y2 + 0.5 };
1002
1003     [[NSGraphicsContext currentContext] setShouldAntialias:NO];
1004
1005     assert(colour >= 0 && colour < fe->ncolours);
1006     [fe->colours[colour] set];
1007
1008     [path moveToPoint:p1];
1009     [path lineToPoint:p2];
1010     [path stroke];
1011 }
1012 void draw_rect(frontend *fe, int x, int y, int w, int h, int colour)
1013 {
1014     NSRect r = { {x,y}, {w,h} };
1015
1016     [[NSGraphicsContext currentContext] setShouldAntialias:NO];
1017
1018     assert(colour >= 0 && colour < fe->ncolours);
1019     [fe->colours[colour] set];
1020
1021     NSRectFill(r);
1022 }
1023 void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
1024                int align, int colour, char *text)
1025 {
1026     NSString *string = [NSString stringWithCString:text];
1027     NSDictionary *attr;
1028     NSFont *font;
1029     NSSize size;
1030     NSPoint point;
1031
1032     [[NSGraphicsContext currentContext] setShouldAntialias:YES];
1033
1034     assert(colour >= 0 && colour < fe->ncolours);
1035
1036     if (fonttype == FONT_FIXED)
1037         font = [NSFont userFixedPitchFontOfSize:fontsize];
1038     else
1039         font = [NSFont userFontOfSize:fontsize];
1040
1041     attr = [NSDictionary dictionaryWithObjectsAndKeys:
1042             fe->colours[colour], NSForegroundColorAttributeName,
1043             font, NSFontAttributeName, nil];
1044
1045     point.x = x;
1046     point.y = y;
1047
1048     size = [string sizeWithAttributes:attr];
1049     if (align & ALIGN_HRIGHT)
1050         point.x -= size.width;
1051     else if (align & ALIGN_HCENTRE)
1052         point.x -= size.width / 2;
1053     if (align & ALIGN_VCENTRE)
1054         point.y -= size.height / 2;
1055
1056     [string drawAtPoint:point withAttributes:attr];
1057 }
1058 void draw_update(frontend *fe, int x, int y, int w, int h)
1059 {
1060     /*
1061      * FIXME: It seems odd that nothing is required here, although
1062      * everything _seems_ to work with this routine empty. Possibly
1063      * we're always updating the entire window, and there's a
1064      * better way which would involve doing something in here?
1065      */
1066 }
1067 void clip(frontend *fe, int x, int y, int w, int h)
1068 {
1069     NSRect r = { {x,y}, {w,h} };
1070
1071     if (!fe->clipped)
1072         [[NSGraphicsContext currentContext] saveGraphicsState];
1073     [NSBezierPath clipRect:r];
1074     fe->clipped = TRUE;
1075 }
1076 void unclip(frontend *fe)
1077 {
1078     if (fe->clipped)
1079         [[NSGraphicsContext currentContext] restoreGraphicsState];
1080     fe->clipped = FALSE;
1081 }
1082 void start_draw(frontend *fe)
1083 {
1084     [fe->image lockFocus];
1085     fe->clipped = FALSE;
1086 }
1087 void end_draw(frontend *fe)
1088 {
1089     [fe->image unlockFocus];
1090     [fe->view setNeedsDisplay];
1091 }
1092
1093 void deactivate_timer(frontend *fe)
1094 {
1095     [fe->window deactivateTimer];
1096 }
1097 void activate_timer(frontend *fe)
1098 {
1099     [fe->window activateTimer];
1100 }
1101
1102 void status_bar(frontend *fe, char *text)
1103 {
1104     [fe->window setStatusLine:[NSString stringWithCString:text]];
1105 }
1106
1107 /* ----------------------------------------------------------------------
1108  * AppController: the object which receives the messages from all
1109  * menu selections that aren't standard OS X functions.
1110  */
1111 @interface AppController : NSObject
1112 {
1113 }
1114 - (void)newGame:(id)sender;
1115 @end
1116
1117 @implementation AppController
1118
1119 - (void)newGame:(id)sender
1120 {
1121     const game *g = [sender getPayload];
1122     id win;
1123
1124     win = [[GameWindow alloc] initWithGame:g];
1125     [win makeKeyAndOrderFront:self];
1126 }
1127
1128 - (NSMenu *)applicationDockMenu:(NSApplication *)sender
1129 {
1130     NSMenu *menu = newmenu("Dock Menu");
1131     {
1132         int i;
1133
1134         for (i = 0; i < gamecount; i++) {
1135             id item =
1136                 initnewitem([DataMenuItem allocWithZone:[NSMenu menuZone]],
1137                             menu, gamelist[i]->name, "", self,
1138                             @selector(newGame:));
1139             [item setPayload:(void *)gamelist[i]];
1140         }
1141     }
1142     return menu;
1143 }
1144
1145 @end
1146
1147 /* ----------------------------------------------------------------------
1148  * Main program. Constructs the menus and runs the application.
1149  */
1150 int main(int argc, char **argv)
1151 {
1152     NSAutoreleasePool *pool;
1153     NSMenu *menu;
1154     NSMenuItem *item;
1155     AppController *controller;
1156     NSImage *icon;
1157
1158     pool = [[NSAutoreleasePool alloc] init];
1159
1160     icon = [NSImage imageNamed:@"NSApplicationIcon"];
1161     [NSApplication sharedApplication];
1162     [NSApp setApplicationIconImage:icon];
1163
1164     controller = [[[AppController alloc] init] autorelease];
1165     [NSApp setDelegate:controller];
1166
1167     [NSApp setMainMenu: newmenu("Main Menu")];
1168
1169     menu = newsubmenu([NSApp mainMenu], "Apple Menu");
1170     [NSApp setServicesMenu:newsubmenu(menu, "Services")];
1171     [menu addItem:[NSMenuItem separatorItem]];
1172     item = newitem(menu, "Hide Puzzles", "h", NSApp, @selector(hide:));
1173     item = newitem(menu, "Hide Others", "o-h", NSApp, @selector(hideOtherApplications:));
1174     item = newitem(menu, "Show All", "", NSApp, @selector(unhideAllApplications:));
1175     [menu addItem:[NSMenuItem separatorItem]];
1176     item = newitem(menu, "Quit", "q", NSApp, @selector(terminate:));
1177     [NSApp setAppleMenu: menu];
1178
1179     menu = newsubmenu([NSApp mainMenu], "Open");
1180     {
1181         int i;
1182
1183         for (i = 0; i < gamecount; i++) {
1184             id item =
1185                 initnewitem([DataMenuItem allocWithZone:[NSMenu menuZone]],
1186                             menu, gamelist[i]->name, "", controller,
1187                             @selector(newGame:));
1188             [item setPayload:(void *)gamelist[i]];
1189         }
1190     }
1191
1192     menu = newsubmenu([NSApp mainMenu], "Game");
1193     item = newitem(menu, "New", "n", NULL, @selector(newGame:));
1194     item = newitem(menu, "Restart", "r", NULL, @selector(restartGame:));
1195     item = newitem(menu, "Specific", "", NULL, @selector(specificGame:));
1196     [menu addItem:[NSMenuItem separatorItem]];
1197     item = newitem(menu, "Undo", "z", NULL, @selector(undoMove:));
1198     item = newitem(menu, "Redo", "S-z", NULL, @selector(redoMove:));
1199     [menu addItem:[NSMenuItem separatorItem]];
1200     item = newitem(menu, "Close", "w", NULL, @selector(performClose:));
1201
1202     menu = newsubmenu([NSApp mainMenu], "Type");
1203     typemenu = menu;
1204     item = newitem(menu, "Custom", "", NULL, @selector(customGameType:));
1205
1206     menu = newsubmenu([NSApp mainMenu], "Window");
1207     [NSApp setWindowsMenu: menu];
1208     item = newitem(menu, "Minimise Window", "m", NULL, @selector(performMiniaturize:));
1209
1210     menu = newsubmenu([NSApp mainMenu], "Help");
1211     typemenu = menu;
1212     item = newitem(menu, "Puzzles Help", "", NSApp, @selector(showHelp:));
1213
1214     [NSApp run];
1215     [pool release];
1216 }