chiark / gitweb /
changelog: document last change
[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  *     + there's a standard _policy_ on window placement, given in
24  *       the HI guidelines. Have to implement it ourselves though,
25  *       bah.
26  *
27  *  - a brief frob of the Mac numeric keypad suggests that it
28  *    generates numbers no matter what you do. I wonder if I should
29  *    try to figure out a way of detecting keypad codes so I can
30  *    implement UP_LEFT and friends. Alternatively, perhaps I
31  *    should simply assign the number keys to UP_LEFT et al?
32  *    They're not in use for anything else right now.
33  *
34  *  - see if we can do anything to one-button-ise the multi-button
35  *    dependent puzzle UIs:
36  *     - Pattern is a _little_ unwieldy but not too bad (since
37  *       generally you never need the middle button unless you've
38  *       made a mistake, so it's just click versus command-click).
39  *     - Net is utterly vile; having normal click be one rotate and
40  *       command-click be the other introduces a horrid asymmetry,
41  *       and yet requiring a shift key for _each_ click would be
42  *       even worse because rotation feels as if it ought to be the
43  *       default action. I fear this is why the Flash Net had the
44  *       UI it did...
45  *        + I've tried out an alternative dragging interface for
46  *          Net; it might work nicely for stylus-based platforms
47  *          where you have better hand/eye feedback for the thing
48  *          you're clicking on, but it's rather unwieldy on the
49  *          Mac. I fear even shift-clicking is better than that.
50  *
51  *  - Should we _return_ to a game configuration sheet once an
52  *    error is reported by midend_set_config, to allow the user to
53  *    correct the one faulty input and keep the other five OK ones?
54  *    The Apple `one sheet at a time' restriction would require me
55  *    to do this by closing the config sheet, opening the alert
56  *    sheet, and then reopening the config sheet when the alert is
57  *    closed; and the human interface types, who presumably
58  *    invented the one-sheet-at-a-time rule for good reasons, might
59  *    look with disfavour on me trying to get round them to fake a
60  *    nested sheet. On the other hand I think there are good
61  *    practical reasons for wanting it that way. Uncertain.
62  * 
63  *  - User feedback dislikes nothing happening when you start the
64  *    app; they suggest a finder-like window containing an icon for
65  *    each puzzle type, enabling you to start one easily. Needs
66  *    thought.
67  * 
68  * Grotty implementation details that could probably be improved:
69  * 
70  *  - I am _utterly_ unconvinced that NSImageView was the right way
71  *    to go about having a window with a reliable backing store! It
72  *    just doesn't feel right; NSImageView is a _control_. Is there
73  *    a simpler way?
74  * 
75  *  - Resizing is currently very bad; rather than bother to work
76  *    out how to resize the NSImageView, I just splatter and
77  *    recreate it.
78  */
79
80 #define COMBINED /* we put all the puzzles in one binary in this port */
81
82 #include <ctype.h>
83 #include <time.h>
84 #include <sys/time.h>
85 #import <Cocoa/Cocoa.h>
86 #include "puzzles.h"
87
88 /* ----------------------------------------------------------------------
89  * Global variables.
90  */
91
92 /*
93  * The `Type' menu. We frob this dynamically to allow the user to
94  * choose a preset set of settings from the current game.
95  */
96 NSMenu *typemenu;
97
98 /*
99  * Forward reference.
100  */
101 extern const struct drawing_api osx_drawing;
102
103 /*
104  * The NSApplication shared instance, which I'll want to refer to from
105  * a few places here and there.
106  */
107 NSApplication *app;
108
109 /* ----------------------------------------------------------------------
110  * Miscellaneous support routines that aren't part of any object or
111  * clearly defined subsystem.
112  */
113
114 void fatal(char *fmt, ...)
115 {
116     va_list ap;
117     char errorbuf[2048];
118     NSAlert *alert;
119
120     va_start(ap, fmt);
121     vsnprintf(errorbuf, lenof(errorbuf), fmt, ap);
122     va_end(ap);
123
124     alert = [NSAlert alloc];
125     /*
126      * We may have come here because we ran out of memory, in which
127      * case it's entirely likely that that alloc will fail, so we
128      * should have a fallback of some sort.
129      */
130     if (!alert) {
131         fprintf(stderr, "fatal error (and NSAlert failed): %s\n", errorbuf);
132     } else {
133         alert = [[alert init] autorelease];
134         [alert addButtonWithTitle:@"Oh dear"];
135         [alert setInformativeText:[NSString stringWithUTF8String:errorbuf]];
136         [alert runModal];
137     }
138     exit(1);
139 }
140
141 void frontend_default_colour(frontend *fe, float *output)
142 {
143     /* FIXME: Is there a system default we can tap into for this? */
144     output[0] = output[1] = output[2] = 0.8F;
145 }
146
147 void get_random_seed(void **randseed, int *randseedsize)
148 {
149     time_t *tp = snew(time_t);
150     time(tp);
151     *randseed = (void *)tp;
152     *randseedsize = sizeof(time_t);
153 }
154
155 static void savefile_write(void *wctx, void *buf, int len)
156 {
157     FILE *fp = (FILE *)wctx;
158     fwrite(buf, 1, len, fp);
159 }
160
161 static int savefile_read(void *wctx, void *buf, int len)
162 {
163     FILE *fp = (FILE *)wctx;
164     int ret;
165
166     ret = fread(buf, 1, len, fp);
167     return (ret == len);
168 }
169
170 /*
171  * Since this front end does not support printing (yet), we need
172  * this stub to satisfy the reference in midend_print_puzzle().
173  */
174 void document_add_puzzle(document *doc, const game *game, game_params *par,
175                          game_state *st, game_state *st2)
176 {
177 }
178
179 /*
180  * setAppleMenu isn't listed in the NSApplication header, but an
181  * NSApp responds to it, so we're adding it here to silence
182  * warnings. (This was removed from the headers in 10.4, so we
183  * only need to include it for 10.4+.)
184  */
185 #if MAC_OS_X_VERSION_MAX_ALLOWED >= 1040
186 @interface NSApplication(NSAppleMenu)
187 - (void)setAppleMenu:(NSMenu *)menu;
188 @end
189 #endif
190
191 /* ----------------------------------------------------------------------
192  * Tiny extension to NSMenuItem which carries a payload of a `void
193  * *', allowing several menu items to invoke the same message but
194  * pass different data through it.
195  */
196 @interface DataMenuItem : NSMenuItem
197 {
198     void *payload;
199 }
200 - (void)setPayload:(void *)d;
201 - (void *)getPayload;
202 @end
203 @implementation DataMenuItem
204 - (void)setPayload:(void *)d
205 {
206     payload = d;
207 }
208 - (void *)getPayload
209 {
210     return payload;
211 }
212 @end
213
214 /* ----------------------------------------------------------------------
215  * Utility routines for constructing OS X menus.
216  */
217
218 NSMenu *newmenu(const char *title)
219 {
220     return [[[NSMenu allocWithZone:[NSMenu menuZone]]
221              initWithTitle:[NSString stringWithUTF8String:title]]
222             autorelease];
223 }
224
225 NSMenu *newsubmenu(NSMenu *parent, const char *title)
226 {
227     NSMenuItem *item;
228     NSMenu *child;
229
230     item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]]
231              initWithTitle:[NSString stringWithUTF8String:title]
232              action:NULL
233              keyEquivalent:@""]
234             autorelease];
235     child = newmenu(title);
236     [item setEnabled:YES];
237     [item setSubmenu:child];
238     [parent addItem:item];
239     return child;
240 }
241
242 id initnewitem(NSMenuItem *item, NSMenu *parent, const char *title,
243                const char *key, id target, SEL action)
244 {
245     unsigned mask = NSCommandKeyMask;
246
247     if (key[strcspn(key, "-")]) {
248         while (*key && *key != '-') {
249             int c = tolower((unsigned char)*key);
250             if (c == 's') {
251                 mask |= NSShiftKeyMask;
252             } else if (c == 'o' || c == 'a') {
253                 mask |= NSAlternateKeyMask;
254             }
255             key++;
256         }
257         if (*key)
258             key++;
259     }
260
261     item = [[item initWithTitle:[NSString stringWithUTF8String:title]
262              action:NULL
263              keyEquivalent:[NSString stringWithUTF8String:key]]
264             autorelease];
265
266     if (*key)
267         [item setKeyEquivalentModifierMask: mask];
268
269     [item setEnabled:YES];
270     [item setTarget:target];
271     [item setAction:action];
272
273     [parent addItem:item];
274
275     return item;
276 }
277
278 NSMenuItem *newitem(NSMenu *parent, char *title, char *key,
279                     id target, SEL action)
280 {
281     return initnewitem([NSMenuItem allocWithZone:[NSMenu menuZone]],
282                        parent, title, key, target, action);
283 }
284
285 /* ----------------------------------------------------------------------
286  * About box.
287  */
288
289 @class AboutBox;
290
291 @interface AboutBox : NSWindow
292 {
293 }
294 - (id)init;
295 @end
296
297 @implementation AboutBox
298 - (id)init
299 {
300     NSRect totalrect;
301     NSView *views[16];
302     int nviews = 0;
303     NSImageView *iv;
304     NSTextField *tf;
305     NSFont *font1 = [NSFont systemFontOfSize:0];
306     NSFont *font2 = [NSFont boldSystemFontOfSize:[font1 pointSize] * 1.1];
307     const int border = 24;
308     int i;
309     double y;
310
311     /*
312      * Construct the controls that go in the About box.
313      */
314
315     iv = [[NSImageView alloc] initWithFrame:NSMakeRect(0,0,64,64)];
316     [iv setImage:[NSImage imageNamed:@"NSApplicationIcon"]];
317     views[nviews++] = iv;
318
319     tf = [[NSTextField alloc]
320           initWithFrame:NSMakeRect(0,0,400,1)];
321     [tf setEditable:NO];
322     [tf setSelectable:NO];
323     [tf setBordered:NO];
324     [tf setDrawsBackground:NO];
325     [tf setFont:font2];
326     [tf setStringValue:@"Simon Tatham's Portable Puzzle Collection"];
327     [tf sizeToFit];
328     views[nviews++] = tf;
329
330     tf = [[NSTextField alloc]
331           initWithFrame:NSMakeRect(0,0,400,1)];
332     [tf setEditable:NO];
333     [tf setSelectable:NO];
334     [tf setBordered:NO];
335     [tf setDrawsBackground:NO];
336     [tf setFont:font1];
337     [tf setStringValue:[NSString stringWithUTF8String:ver]];
338     [tf sizeToFit];
339     views[nviews++] = tf;
340
341     /*
342      * Lay the controls out.
343      */
344     totalrect = NSMakeRect(0,0,0,0);
345     for (i = 0; i < nviews; i++) {
346         NSRect r = [views[i] frame];
347         if (totalrect.size.width < r.size.width)
348             totalrect.size.width = r.size.width;
349         totalrect.size.height += border + r.size.height;
350     }
351     totalrect.size.width += 2 * border;
352     totalrect.size.height += border;
353     y = totalrect.size.height;
354     for (i = 0; i < nviews; i++) {
355         NSRect r = [views[i] frame];
356         r.origin.x = (totalrect.size.width - r.size.width) / 2;
357         y -= border + r.size.height;
358         r.origin.y = y;
359         [views[i] setFrame:r];
360     }
361
362     self = [super initWithContentRect:totalrect
363             styleMask:(NSTitledWindowMask | NSMiniaturizableWindowMask |
364                        NSClosableWindowMask)
365             backing:NSBackingStoreBuffered
366             defer:YES];
367
368     for (i = 0; i < nviews; i++)
369         [[self contentView] addSubview:views[i]];
370
371     [self center];                     /* :-) */
372
373     return self;
374 }
375 @end
376
377 /* ----------------------------------------------------------------------
378  * The front end presented to midend.c.
379  * 
380  * This is mostly a subclass of NSWindow. The actual `frontend'
381  * structure passed to the midend contains a variety of pointers,
382  * including that window object but also including the image we
383  * draw on, an ImageView to display it in the window, and so on.
384  */
385
386 @class GameWindow;
387 @class MyImageView;
388
389 struct frontend {
390     GameWindow *window;
391     NSImage *image;
392     MyImageView *view;
393     NSColor **colours;
394     int ncolours;
395     int clipped;
396     int w, h;
397 };
398
399 @interface MyImageView : NSImageView
400 {
401     GameWindow *ourwin;
402 }
403 - (void)setWindow:(GameWindow *)win;
404 - (void)mouseEvent:(NSEvent *)ev button:(int)b;
405 - (void)mouseDown:(NSEvent *)ev;
406 - (void)mouseDragged:(NSEvent *)ev;
407 - (void)mouseUp:(NSEvent *)ev;
408 - (void)rightMouseDown:(NSEvent *)ev;
409 - (void)rightMouseDragged:(NSEvent *)ev;
410 - (void)rightMouseUp:(NSEvent *)ev;
411 - (void)otherMouseDown:(NSEvent *)ev;
412 - (void)otherMouseDragged:(NSEvent *)ev;
413 - (void)otherMouseUp:(NSEvent *)ev;
414 @end
415
416 @interface GameWindow : NSWindow
417 {
418     const game *ourgame;
419     midend *me;
420     struct frontend fe;
421     struct timeval last_time;
422     NSTimer *timer;
423     NSWindow *sheet;
424     config_item *cfg;
425     int cfg_which;
426     NSView **cfg_controls;
427     int cfg_ncontrols;
428     NSTextField *status;
429     struct preset_menu *preset_menu;
430     NSMenuItem **preset_menu_items;
431     int n_preset_menu_items;
432 }
433 - (id)initWithGame:(const game *)g;
434 - (void)dealloc;
435 - (void)processButton:(int)b x:(int)x y:(int)y;
436 - (void)processKey:(int)b;
437 - (void)keyDown:(NSEvent *)ev;
438 - (void)activateTimer;
439 - (void)deactivateTimer;
440 - (void)setStatusLine:(char *)text;
441 - (void)resizeForNewGameParams;
442 - (void)updateTypeMenuTick;
443 @end
444
445 @implementation MyImageView
446
447 - (void)setWindow:(GameWindow *)win
448 {
449     ourwin = win;
450 }
451
452 - (void)mouseEvent:(NSEvent *)ev button:(int)b
453 {
454     NSPoint point = [self convertPoint:[ev locationInWindow] fromView:nil];
455     [ourwin processButton:b x:point.x y:point.y];
456 }
457
458 - (void)mouseDown:(NSEvent *)ev
459 {
460     unsigned mod = [ev modifierFlags];
461     [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_BUTTON :
462                                 (mod & NSShiftKeyMask) ? MIDDLE_BUTTON :
463                                 LEFT_BUTTON)];
464 }
465 - (void)mouseDragged:(NSEvent *)ev
466 {
467     unsigned mod = [ev modifierFlags];
468     [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_DRAG :
469                                 (mod & NSShiftKeyMask) ? MIDDLE_DRAG :
470                                 LEFT_DRAG)];
471 }
472 - (void)mouseUp:(NSEvent *)ev
473 {
474     unsigned mod = [ev modifierFlags];
475     [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_RELEASE :
476                                 (mod & NSShiftKeyMask) ? MIDDLE_RELEASE :
477                                 LEFT_RELEASE)];
478 }
479 - (void)rightMouseDown:(NSEvent *)ev
480 {
481     unsigned mod = [ev modifierFlags];
482     [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_BUTTON :
483                                 RIGHT_BUTTON)];
484 }
485 - (void)rightMouseDragged:(NSEvent *)ev
486 {
487     unsigned mod = [ev modifierFlags];
488     [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_DRAG :
489                                 RIGHT_DRAG)];
490 }
491 - (void)rightMouseUp:(NSEvent *)ev
492 {
493     unsigned mod = [ev modifierFlags];
494     [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_RELEASE :
495                                 RIGHT_RELEASE)];
496 }
497 - (void)otherMouseDown:(NSEvent *)ev
498 {
499     [self mouseEvent:ev button:MIDDLE_BUTTON];
500 }
501 - (void)otherMouseDragged:(NSEvent *)ev
502 {
503     [self mouseEvent:ev button:MIDDLE_DRAG];
504 }
505 - (void)otherMouseUp:(NSEvent *)ev
506 {
507     [self mouseEvent:ev button:MIDDLE_RELEASE];
508 }
509 @end
510
511 @implementation GameWindow
512 - (void)setupContentView
513 {
514     NSRect frame;
515     int w, h;
516
517     if (status) {
518         frame = [status frame];
519         frame.origin.y = frame.size.height;
520     } else
521         frame.origin.y = 0;
522     frame.origin.x = 0;
523
524     w = h = INT_MAX;
525     midend_size(me, &w, &h, FALSE);
526     frame.size.width = w;
527     frame.size.height = h;
528     fe.w = w;
529     fe.h = h;
530
531     fe.image = [[NSImage alloc] initWithSize:frame.size];
532     fe.view = [[MyImageView alloc] initWithFrame:frame];
533     [fe.view setImage:fe.image];
534     [fe.view setWindow:self];
535
536     midend_redraw(me);
537
538     [[self contentView] addSubview:fe.view];
539 }
540 - (id)initWithGame:(const game *)g
541 {
542     NSRect rect = { {0,0}, {0,0} }, rect2;
543     int w, h;
544
545     ourgame = g;
546     preset_menu = NULL;
547     preset_menu_items = NULL;
548
549     fe.window = self;
550
551     me = midend_new(&fe, ourgame, &osx_drawing, &fe);
552     /*
553      * If we ever need to open a fresh window using a provided game
554      * ID, I think the right thing is to move most of this method
555      * into a new initWithGame:gameID: method, and have
556      * initWithGame: simply call that one and pass it NULL.
557      */
558     midend_new_game(me);
559     w = h = INT_MAX;
560     midend_size(me, &w, &h, FALSE);
561     rect.size.width = w;
562     rect.size.height = h;
563     fe.w = w;
564     fe.h = h;
565
566     /*
567      * Create the status bar, which will just be an NSTextField.
568      */
569     if (midend_wants_statusbar(me)) {
570         status = [[NSTextField alloc] initWithFrame:NSMakeRect(0,0,100,50)];
571         [status setEditable:NO];
572         [status setSelectable:NO];
573         [status setBordered:YES];
574         [status setBezeled:YES];
575         [status setBezelStyle:NSTextFieldSquareBezel];
576         [status setDrawsBackground:YES];
577         [[status cell] setTitle:@DEFAULT_STATUSBAR_TEXT];
578         [status sizeToFit];
579         rect2 = [status frame];
580         rect.size.height += rect2.size.height;
581         rect2.size.width = rect.size.width;
582         rect2.origin.x = rect2.origin.y = 0;
583         [status setFrame:rect2];
584     } else
585         status = nil;
586
587     self = [super initWithContentRect:rect
588             styleMask:(NSTitledWindowMask | NSMiniaturizableWindowMask |
589                        NSClosableWindowMask)
590             backing:NSBackingStoreBuffered
591             defer:YES];
592     [self setTitle:[NSString stringWithUTF8String:ourgame->name]];
593
594     {
595         float *colours;
596         int i, ncolours;
597
598         colours = midend_colours(me, &ncolours);
599         fe.ncolours = ncolours;
600         fe.colours = snewn(ncolours, NSColor *);
601
602         for (i = 0; i < ncolours; i++) {
603             fe.colours[i] = [[NSColor colorWithDeviceRed:colours[i*3]
604                               green:colours[i*3+1] blue:colours[i*3+2]
605                               alpha:1.0] retain];
606         }
607     }
608
609     [self setupContentView];
610     if (status)
611         [[self contentView] addSubview:status];
612     [self setIgnoresMouseEvents:NO];
613
614     [self center];                     /* :-) */
615
616     return self;
617 }
618
619 - (void)dealloc
620 {
621     int i;
622     for (i = 0; i < fe.ncolours; i++) {
623         [fe.colours[i] release];
624     }
625     sfree(fe.colours);
626     sfree(preset_menu_items);
627     midend_free(me);
628     [super dealloc];
629 }
630
631 - (void)processButton:(int)b x:(int)x y:(int)y
632 {
633     if (!midend_process_key(me, x, fe.h - 1 - y, b))
634         [self close];
635 }
636
637 - (void)processKey:(int)b
638 {
639     if (!midend_process_key(me, -1, -1, b))
640         [self close];
641 }
642
643 - (void)keyDown:(NSEvent *)ev
644 {
645     NSString *s = [ev characters];
646     int i, n = [s length];
647
648     for (i = 0; i < n; i++) {
649         int c = [s characterAtIndex:i];
650
651         /*
652          * ASCII gets passed straight to midend_process_key.
653          * Anything above that has to be translated to our own
654          * function key codes.
655          */
656         if (c >= 0x80) {
657             int mods = FALSE;
658             switch (c) {
659               case NSUpArrowFunctionKey:
660                 c = CURSOR_UP;
661                 mods = TRUE;
662                 break;
663               case NSDownArrowFunctionKey:
664                 c = CURSOR_DOWN;
665                 mods = TRUE;
666                 break;
667               case NSLeftArrowFunctionKey:
668                 c = CURSOR_LEFT;
669                 mods = TRUE;
670                 break;
671               case NSRightArrowFunctionKey:
672                 c = CURSOR_RIGHT;
673                 mods = TRUE;
674                 break;
675               default:
676                 continue;
677             }
678
679             if (mods) {
680                 if ([ev modifierFlags] & NSShiftKeyMask)
681                     c |= MOD_SHFT;
682                 if ([ev modifierFlags] & NSControlKeyMask)
683                     c |= MOD_CTRL;
684             }
685         }
686
687         if (c >= '0' && c <= '9' && ([ev modifierFlags] & NSNumericPadKeyMask))
688             c |= MOD_NUM_KEYPAD;
689
690         if (c == 26 &&
691             !((NSShiftKeyMask | NSControlKeyMask) & ~[ev modifierFlags]))
692             c = UI_REDO;
693
694         [self processKey:c];
695     }
696 }
697
698 - (void)activateTimer
699 {
700     if (timer != nil)
701         return;
702
703     timer = [NSTimer scheduledTimerWithTimeInterval:0.02
704              target:self selector:@selector(timerTick:)
705              userInfo:nil repeats:YES];
706     gettimeofday(&last_time, NULL);
707 }
708
709 - (void)deactivateTimer
710 {
711     if (timer == nil)
712         return;
713
714     [timer invalidate];
715     timer = nil;
716 }
717
718 - (void)timerTick:(id)sender
719 {
720     struct timeval now;
721     float elapsed;
722     gettimeofday(&now, NULL);
723     elapsed = ((now.tv_usec - last_time.tv_usec) * 0.000001F +
724                (now.tv_sec - last_time.tv_sec));
725     midend_timer(me, elapsed);
726     last_time = now;
727 }
728
729 - (void)showError:(char *)message
730 {
731     NSAlert *alert;
732
733     alert = [[[NSAlert alloc] init] autorelease];
734     [alert addButtonWithTitle:@"Bah"];
735     [alert setInformativeText:[NSString stringWithUTF8String:message]];
736     [alert beginSheetModalForWindow:self modalDelegate:nil
737      didEndSelector:NULL contextInfo:nil];
738 }
739
740 - (void)newGame:(id)sender
741 {
742     [self processKey:UI_NEWGAME];
743 }
744 - (void)restartGame:(id)sender
745 {
746     midend_restart_game(me);
747 }
748 - (void)saveGame:(id)sender
749 {
750     NSSavePanel *sp = [NSSavePanel savePanel];
751
752     if ([sp runModal] == NSFileHandlingPanelOKButton) {
753        const char *name = [[sp filename] UTF8String];
754
755         FILE *fp = fopen(name, "w");
756
757         if (!fp) {
758             [self showError:"Unable to open save file"];
759             return;
760         }
761
762         midend_serialise(me, savefile_write, fp);
763
764         fclose(fp);
765     }
766 }
767 - (void)loadSavedGame:(id)sender
768 {
769     NSOpenPanel *op = [NSOpenPanel openPanel];
770
771     [op setAllowsMultipleSelection:NO];
772
773     if ([op runModalForTypes:nil] == NSOKButton) {
774         /*
775          * This used to be
776          *
777          *    [[[op filenames] objectAtIndex:0] cString]
778          *
779          * but the plain cString method became deprecated and Xcode 7
780          * started complaining about it. Since OS X 10.9 we can
781          * apparently use the more modern API
782          *
783          *    [[[op URLs] objectAtIndex:0] fileSystemRepresentation]
784          *
785          * but the alternative below still compiles with Xcode 7 and
786          * is a bit more backwards compatible, so I'll try it for the
787          * moment.
788          */
789         const char *name = [[[op filenames] objectAtIndex:0]
790                                cStringUsingEncoding:
791                                    [NSString defaultCStringEncoding]];
792         char *err;
793
794         FILE *fp = fopen(name, "r");
795
796         if (!fp) {
797             [self showError:"Unable to open saved game file"];
798             return;
799         }
800
801         err = midend_deserialise(me, savefile_read, fp);
802
803         fclose(fp);
804
805         if (err) {
806             [self showError:err];
807             return;
808         }
809
810         [self resizeForNewGameParams];
811         [self updateTypeMenuTick];
812     }
813 }
814 - (void)undoMove:(id)sender
815 {
816     [self processKey:UI_UNDO];
817 }
818 - (void)redoMove:(id)sender
819 {
820     [self processKey:UI_REDO];
821 }
822
823 - (void)copy:(id)sender
824 {
825     char *text;
826
827     if ((text = midend_text_format(me)) != NULL) {
828         NSPasteboard *pb = [NSPasteboard generalPasteboard];
829         NSArray *a = [NSArray arrayWithObject:NSStringPboardType];
830         [pb declareTypes:a owner:nil];
831         [pb setString:[NSString stringWithUTF8String:text]
832          forType:NSStringPboardType];
833     } else
834         NSBeep();
835 }
836
837 - (void)solveGame:(id)sender
838 {
839     char *msg;
840
841     msg = midend_solve(me);
842
843     if (msg)
844         [self showError:msg];
845 }
846
847 - (BOOL)validateMenuItem:(NSMenuItem *)item
848 {
849     if ([item action] == @selector(copy:))
850         return (ourgame->can_format_as_text_ever &&
851                 midend_can_format_as_text_now(me) ? YES : NO);
852     else if ([item action] == @selector(solveGame:))
853         return (ourgame->can_solve ? YES : NO);
854     else
855         return [super validateMenuItem:item];
856 }
857
858 - (void)clearTypeMenu
859 {
860     int i;
861
862     while ([typemenu numberOfItems] > 1)
863         [typemenu removeItemAtIndex:0];
864     [[typemenu itemAtIndex:0] setState:NSOffState];
865
866     for (i = 0; i < n_preset_menu_items; i++)
867         preset_menu_items[i] = NULL;
868 }
869
870 - (void)updateTypeMenuTick
871 {
872     int i, n;
873
874     n = midend_which_preset(me);
875
876     for (i = 0; i < n_preset_menu_items; i++)
877         if (preset_menu_items[i])
878             [preset_menu_items[i] setState:(i == n ? NSOnState : NSOffState)];
879
880     /*
881      * The Custom menu item is always right at the bottom of the
882      * Type menu.
883      */
884     [[typemenu itemAtIndex:[typemenu numberOfItems]-1]
885              setState:(n < 0 ? NSOnState : NSOffState)];
886 }
887
888 - (void)populateTypeMenu:(NSMenu *)nsmenu from:(struct preset_menu *)menu
889 {
890     int i;
891
892     /*
893      * We process the entries in reverse order so that (in the
894      * top-level Type menu at least) we don't disturb the 'Custom'
895      * item which remains fixed even when we change back and forth
896      * between puzzle type windows.
897      */
898     for (i = menu->n_entries; i-- > 0 ;) {
899         struct preset_menu_entry *entry = &menu->entries[i];
900         NSMenuItem *item;
901
902         if (entry->params) {
903             DataMenuItem *ditem;
904             ditem = [[[DataMenuItem alloc]
905                         initWithTitle:[NSString stringWithUTF8String:
906                                                     entry->title]
907                                action:NULL keyEquivalent:@""]
908                        autorelease];
909
910             [ditem setTarget:self];
911             [ditem setAction:@selector(presetGame:)];
912             [ditem setPayload:entry->params];
913
914             preset_menu_items[entry->id] = ditem;
915
916             item = ditem;
917         } else {
918             NSMenu *nssubmenu;
919
920             item = [[[NSMenuItem alloc]
921                         initWithTitle:[NSString stringWithUTF8String:
922                                                     entry->title]
923                                action:NULL keyEquivalent:@""]
924                        autorelease];
925             nssubmenu = newmenu(entry->title);
926             [item setSubmenu:nssubmenu];
927
928             [self populateTypeMenu:nssubmenu from:entry->submenu];
929         }
930
931         [item setEnabled:YES];
932         [nsmenu insertItem:item atIndex:0];
933     }
934 }
935
936 - (void)becomeKeyWindow
937 {
938     [self clearTypeMenu];
939
940     [super becomeKeyWindow];
941
942     if (!preset_menu) {
943         int i;
944         preset_menu = midend_get_presets(me, &n_preset_menu_items);
945         preset_menu_items = snewn(n_preset_menu_items, NSMenuItem *);
946         for (i = 0; i < n_preset_menu_items; i++)
947             preset_menu_items[i] = NULL;
948     }
949
950     if (preset_menu->n_entries > 0) {
951         [typemenu insertItem:[NSMenuItem separatorItem] atIndex:0];
952         [self populateTypeMenu:typemenu from:preset_menu];
953     }
954
955     [self updateTypeMenuTick];
956 }
957
958 - (void)resignKeyWindow
959 {
960     [self clearTypeMenu];
961     [super resignKeyWindow];
962 }
963
964 - (void)close
965 {
966     [self clearTypeMenu];
967     [super close];
968 }
969
970 - (void)resizeForNewGameParams
971 {
972     NSSize size = {0,0};
973     int w, h;
974
975     w = h = INT_MAX;
976     midend_size(me, &w, &h, FALSE);
977     size.width = w;
978     size.height = h;
979     fe.w = w;
980     fe.h = h;
981
982     if (status) {
983         NSRect frame = [status frame];
984         size.height += frame.size.height;
985         frame.size.width = size.width;
986         [status setFrame:frame];
987     }
988
989 #ifndef GNUSTEP
990     NSDisableScreenUpdates();
991 #endif
992     [self setContentSize:size];
993     [self setupContentView];
994 #ifndef GNUSTEP
995     NSEnableScreenUpdates();
996 #endif
997 }
998
999 - (void)presetGame:(id)sender
1000 {
1001     game_params *params = [sender getPayload];
1002
1003     midend_set_params(me, params);
1004     midend_new_game(me);
1005
1006     [self resizeForNewGameParams];
1007     [self updateTypeMenuTick];
1008 }
1009
1010 - (void)startConfigureSheet:(int)which
1011 {
1012     NSButton *ok, *cancel;
1013     int actw, acth, leftw, rightw, totalw, h, thish, y;
1014     int k;
1015     NSRect rect, tmprect;
1016     const int SPACING = 16;
1017     char *title;
1018     config_item *i;
1019     int cfg_controlsize;
1020     NSTextField *tf;
1021     NSButton *b;
1022     NSPopUpButton *pb;
1023
1024     assert(sheet == NULL);
1025
1026     /*
1027      * Every control we create here is going to have this size
1028      * until we tell it to calculate a better one.
1029      */
1030     tmprect = NSMakeRect(0, 0, 100, 50);
1031
1032     /*
1033      * Set up OK and Cancel buttons. (Actually, MacOS doesn't seem
1034      * to be fond of generic OK and Cancel wording, so I'm going to
1035      * rename them to something nicer.)
1036      */
1037     actw = acth = 0;
1038
1039     cancel = [[NSButton alloc] initWithFrame:tmprect];
1040     [cancel setBezelStyle:NSRoundedBezelStyle];
1041     [cancel setTitle:@"Abandon"];
1042     [cancel setTarget:self];
1043     [cancel setKeyEquivalent:@"\033"];
1044     [cancel setAction:@selector(sheetCancelButton:)];
1045     [cancel sizeToFit];
1046     rect = [cancel frame];
1047     if (actw < rect.size.width) actw = rect.size.width;
1048     if (acth < rect.size.height) acth = rect.size.height;
1049
1050     ok = [[NSButton alloc] initWithFrame:tmprect];
1051     [ok setBezelStyle:NSRoundedBezelStyle];
1052     [ok setTitle:@"Accept"];
1053     [ok setTarget:self];
1054     [ok setKeyEquivalent:@"\r"];
1055     [ok setAction:@selector(sheetOKButton:)];
1056     [ok sizeToFit];
1057     rect = [ok frame];
1058     if (actw < rect.size.width) actw = rect.size.width;
1059     if (acth < rect.size.height) acth = rect.size.height;
1060
1061     totalw = SPACING + 2 * actw;
1062     h = 2 * SPACING + acth;
1063
1064     /*
1065      * Now fetch the midend config data and go through it creating
1066      * controls.
1067      */
1068     cfg = midend_get_config(me, which, &title);
1069     sfree(title);                      /* FIXME: should we use this somehow? */
1070     cfg_which = which;
1071
1072     cfg_ncontrols = cfg_controlsize = 0;
1073     cfg_controls = NULL;
1074     leftw = rightw = 0;
1075     for (i = cfg; i->type != C_END; i++) {
1076         if (cfg_controlsize < cfg_ncontrols + 5) {
1077             cfg_controlsize = cfg_ncontrols + 32;
1078             cfg_controls = sresize(cfg_controls, cfg_controlsize, NSView *);
1079         }
1080
1081         thish = 0;
1082
1083         switch (i->type) {
1084           case C_STRING:
1085             /*
1086              * Two NSTextFields, one being a label and the other
1087              * being an edit box.
1088              */
1089
1090             tf = [[NSTextField alloc] initWithFrame:tmprect];
1091             [tf setEditable:NO];
1092             [tf setSelectable:NO];
1093             [tf setBordered:NO];
1094             [tf setDrawsBackground:NO];
1095             [[tf cell] setTitle:[NSString stringWithUTF8String:i->name]];
1096             [tf sizeToFit];
1097             rect = [tf frame];
1098             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
1099             if (leftw < rect.size.width + 1) leftw = rect.size.width + 1;
1100             cfg_controls[cfg_ncontrols++] = tf;
1101
1102             tf = [[NSTextField alloc] initWithFrame:tmprect];
1103             [tf setEditable:YES];
1104             [tf setSelectable:YES];
1105             [tf setBordered:YES];
1106             [[tf cell] setTitle:[NSString stringWithUTF8String:i->sval]];
1107             [tf sizeToFit];
1108             rect = [tf frame];
1109             /*
1110              * We impose a minimum and maximum width on editable
1111              * NSTextFields. If we allow them to size themselves to
1112              * the contents of the text within them, then they will
1113              * look very silly if that text is only one or two
1114              * characters, and equally silly if it's an absolutely
1115              * enormous Rectangles or Pattern game ID!
1116              */
1117             if (rect.size.width < 75) rect.size.width = 75;
1118             if (rect.size.width > 400) rect.size.width = 400;
1119
1120             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
1121             if (rightw < rect.size.width + 1) rightw = rect.size.width + 1;
1122             cfg_controls[cfg_ncontrols++] = tf;
1123             break;
1124
1125           case C_BOOLEAN:
1126             /*
1127              * A checkbox is an NSButton with a type of
1128              * NSSwitchButton.
1129              */
1130             b = [[NSButton alloc] initWithFrame:tmprect];
1131             [b setBezelStyle:NSRoundedBezelStyle];
1132             [b setButtonType:NSSwitchButton];
1133             [b setTitle:[NSString stringWithUTF8String:i->name]];
1134             [b sizeToFit];
1135             [b setState:(i->ival ? NSOnState : NSOffState)];
1136             rect = [b frame];
1137             if (totalw < rect.size.width + 1) totalw = rect.size.width + 1;
1138             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
1139             cfg_controls[cfg_ncontrols++] = b;
1140             break;
1141
1142           case C_CHOICES:
1143             /*
1144              * A pop-up menu control is an NSPopUpButton, which
1145              * takes an embedded NSMenu. We also need an
1146              * NSTextField to act as a label.
1147              */
1148
1149             tf = [[NSTextField alloc] initWithFrame:tmprect];
1150             [tf setEditable:NO];
1151             [tf setSelectable:NO];
1152             [tf setBordered:NO];
1153             [tf setDrawsBackground:NO];
1154             [[tf cell] setTitle:[NSString stringWithUTF8String:i->name]];
1155             [tf sizeToFit];
1156             rect = [tf frame];
1157             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
1158             if (leftw < rect.size.width + 1) leftw = rect.size.width + 1;
1159             cfg_controls[cfg_ncontrols++] = tf;
1160
1161             pb = [[NSPopUpButton alloc] initWithFrame:tmprect pullsDown:NO];
1162             [pb setBezelStyle:NSRoundedBezelStyle];
1163             {
1164                 char c, *p;
1165
1166                 p = i->sval;
1167                 c = *p++;
1168                 while (*p) {
1169                     char *q, *copy;
1170
1171                     q = p;
1172                     while (*p && *p != c) p++;
1173
1174                     copy = snewn((p-q) + 1, char);
1175                     memcpy(copy, q, p-q);
1176                     copy[p-q] = '\0';
1177                     [pb addItemWithTitle:[NSString stringWithUTF8String:copy]];
1178                     sfree(copy);
1179
1180                     if (*p) p++;
1181                 }
1182             }
1183             [pb selectItemAtIndex:i->ival];
1184             [pb sizeToFit];
1185
1186             rect = [pb frame];
1187             if (rightw < rect.size.width + 1) rightw = rect.size.width + 1;
1188             if (thish < rect.size.height + 1) thish = rect.size.height + 1;
1189             cfg_controls[cfg_ncontrols++] = pb;
1190             break;
1191         }
1192
1193         h += SPACING + thish;
1194     }
1195
1196     if (totalw < leftw + SPACING + rightw)
1197         totalw = leftw + SPACING + rightw;
1198     if (totalw > leftw + SPACING + rightw) {
1199         int excess = totalw - (leftw + SPACING + rightw);
1200         int leftexcess = leftw * excess / (leftw + rightw);
1201         int rightexcess = excess - leftexcess;
1202         leftw += leftexcess;
1203         rightw += rightexcess;
1204     }
1205
1206     /*
1207      * Now go through the list again, setting the final position
1208      * for each control.
1209      */
1210     k = 0;
1211     y = h;
1212     for (i = cfg; i->type != C_END; i++) {
1213         y -= SPACING;
1214         thish = 0;
1215         switch (i->type) {
1216           case C_STRING:
1217           case C_CHOICES:
1218             /*
1219              * These two are treated identically, since both expect
1220              * a control on the left and another on the right.
1221              */
1222             rect = [cfg_controls[k] frame];
1223             if (thish < rect.size.height + 1)
1224                 thish = rect.size.height + 1;
1225             rect = [cfg_controls[k+1] frame];
1226             if (thish < rect.size.height + 1)
1227                 thish = rect.size.height + 1;
1228             rect = [cfg_controls[k] frame];
1229             rect.origin.y = y - thish/2 - rect.size.height/2;
1230             rect.origin.x = SPACING;
1231             rect.size.width = leftw;
1232             [cfg_controls[k] setFrame:rect];
1233             rect = [cfg_controls[k+1] frame];
1234             rect.origin.y = y - thish/2 - rect.size.height/2;
1235             rect.origin.x = 2 * SPACING + leftw;
1236             rect.size.width = rightw;
1237             [cfg_controls[k+1] setFrame:rect];
1238             k += 2;
1239             break;
1240
1241           case C_BOOLEAN:
1242             rect = [cfg_controls[k] frame];
1243             if (thish < rect.size.height + 1)
1244                 thish = rect.size.height + 1;
1245             rect.origin.y = y - thish/2 - rect.size.height/2;
1246             rect.origin.x = SPACING;
1247             rect.size.width = totalw;
1248             [cfg_controls[k] setFrame:rect];
1249             k++;
1250             break;
1251         }
1252         y -= thish;
1253     }
1254
1255     assert(k == cfg_ncontrols);
1256
1257     [cancel setFrame:NSMakeRect(SPACING+totalw/4-actw/2, SPACING, actw, acth)];
1258     [ok setFrame:NSMakeRect(SPACING+3*totalw/4-actw/2, SPACING, actw, acth)];
1259
1260     sheet = [[NSWindow alloc]
1261              initWithContentRect:NSMakeRect(0,0,totalw + 2*SPACING,h)
1262              styleMask:NSTitledWindowMask | NSClosableWindowMask
1263              backing:NSBackingStoreBuffered
1264              defer:YES];
1265
1266     [[sheet contentView] addSubview:cancel];
1267     [[sheet contentView] addSubview:ok];
1268
1269     for (k = 0; k < cfg_ncontrols; k++)
1270         [[sheet contentView] addSubview:cfg_controls[k]];
1271
1272     [app beginSheet:sheet modalForWindow:self
1273      modalDelegate:nil didEndSelector:NULL contextInfo:nil];
1274 }
1275
1276 - (void)specificGame:(id)sender
1277 {
1278     [self startConfigureSheet:CFG_DESC];
1279 }
1280
1281 - (void)specificRandomGame:(id)sender
1282 {
1283     [self startConfigureSheet:CFG_SEED];
1284 }
1285
1286 - (void)customGameType:(id)sender
1287 {
1288     [self startConfigureSheet:CFG_SETTINGS];
1289 }
1290
1291 - (void)sheetEndWithStatus:(BOOL)update
1292 {
1293     assert(sheet != NULL);
1294     [app endSheet:sheet];
1295     [sheet orderOut:self];
1296     sheet = NULL;
1297     if (update) {
1298         int k;
1299         config_item *i;
1300         char *error;
1301
1302         k = 0;
1303         for (i = cfg; i->type != C_END; i++) {
1304             switch (i->type) {
1305               case C_STRING:
1306                 sfree(i->sval);
1307                 i->sval = dupstr([[[(id)cfg_controls[k+1] cell]
1308                                   title] UTF8String]);
1309                 k += 2;
1310                 break;
1311               case C_BOOLEAN:
1312                 i->ival = [(id)cfg_controls[k] state] == NSOnState;
1313                 k++;
1314                 break;
1315               case C_CHOICES:
1316                 i->ival = [(id)cfg_controls[k+1] indexOfSelectedItem];
1317                 k += 2;
1318                 break;
1319             }
1320         }
1321
1322         error = midend_set_config(me, cfg_which, cfg);
1323         if (error) {
1324             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
1325             [alert addButtonWithTitle:@"Bah"];
1326             [alert setInformativeText:[NSString stringWithUTF8String:error]];
1327             [alert beginSheetModalForWindow:self modalDelegate:nil
1328              didEndSelector:NULL contextInfo:nil];
1329         } else {
1330             midend_new_game(me);
1331             [self resizeForNewGameParams];
1332             [self updateTypeMenuTick];
1333         }
1334     }
1335     sfree(cfg_controls);
1336     cfg_controls = NULL;
1337 }
1338 - (void)sheetOKButton:(id)sender
1339 {
1340     [self sheetEndWithStatus:YES];
1341 }
1342 - (void)sheetCancelButton:(id)sender
1343 {
1344     [self sheetEndWithStatus:NO];
1345 }
1346
1347 - (void)setStatusLine:(char *)text
1348 {
1349     [[status cell] setTitle:[NSString stringWithUTF8String:text]];
1350 }
1351
1352 @end
1353
1354 /*
1355  * Drawing routines called by the midend.
1356  */
1357 static void osx_draw_polygon(void *handle, int *coords, int npoints,
1358                              int fillcolour, int outlinecolour)
1359 {
1360     frontend *fe = (frontend *)handle;
1361     NSBezierPath *path = [NSBezierPath bezierPath];
1362     int i;
1363
1364     [[NSGraphicsContext currentContext] setShouldAntialias:YES];
1365
1366     for (i = 0; i < npoints; i++) {
1367         NSPoint p = { coords[i*2] + 0.5, fe->h - coords[i*2+1] - 0.5 };
1368         if (i == 0)
1369             [path moveToPoint:p];
1370         else
1371             [path lineToPoint:p];
1372     }
1373
1374     [path closePath];
1375
1376     if (fillcolour >= 0) {
1377         assert(fillcolour >= 0 && fillcolour < fe->ncolours);
1378         [fe->colours[fillcolour] set];
1379         [path fill];
1380     }
1381
1382     assert(outlinecolour >= 0 && outlinecolour < fe->ncolours);
1383     [fe->colours[outlinecolour] set];
1384     [path stroke];
1385 }
1386 static void osx_draw_circle(void *handle, int cx, int cy, int radius,
1387                             int fillcolour, int outlinecolour)
1388 {
1389     frontend *fe = (frontend *)handle;
1390     NSBezierPath *path = [NSBezierPath bezierPath];
1391
1392     [[NSGraphicsContext currentContext] setShouldAntialias:YES];
1393
1394     [path appendBezierPathWithArcWithCenter:NSMakePoint(cx+0.5, fe->h-cy-0.5)
1395         radius:radius startAngle:0.0 endAngle:360.0];
1396
1397     [path closePath];
1398
1399     if (fillcolour >= 0) {
1400         assert(fillcolour >= 0 && fillcolour < fe->ncolours);
1401         [fe->colours[fillcolour] set];
1402         [path fill];
1403     }
1404
1405     assert(outlinecolour >= 0 && outlinecolour < fe->ncolours);
1406     [fe->colours[outlinecolour] set];
1407     [path stroke];
1408 }
1409 static void osx_draw_line(void *handle, int x1, int y1, int x2, int y2, int colour)
1410 {
1411     frontend *fe = (frontend *)handle;
1412     NSBezierPath *path = [NSBezierPath bezierPath];
1413     NSPoint p1 = { x1 + 0.5, fe->h - y1 - 0.5 };
1414     NSPoint p2 = { x2 + 0.5, fe->h - y2 - 0.5 };
1415
1416     [[NSGraphicsContext currentContext] setShouldAntialias:NO];
1417
1418     assert(colour >= 0 && colour < fe->ncolours);
1419     [fe->colours[colour] set];
1420
1421     [path moveToPoint:p1];
1422     [path lineToPoint:p2];
1423     [path stroke];
1424     NSRectFill(NSMakeRect(x1, fe->h-y1-1, 1, 1));
1425     NSRectFill(NSMakeRect(x2, fe->h-y2-1, 1, 1));
1426 }
1427
1428 static void osx_draw_thick_line(
1429     void *handle, float thickness,
1430     float x1, float y1,
1431     float x2, float y2,
1432     int colour)
1433 {
1434     frontend *fe = (frontend *)handle;
1435     NSBezierPath *path = [NSBezierPath bezierPath];
1436
1437     assert(colour >= 0 && colour < fe->ncolours);
1438     [fe->colours[colour] set];
1439     [[NSGraphicsContext currentContext] setShouldAntialias: YES];
1440     [path setLineWidth: thickness];
1441     [path setLineCapStyle: NSButtLineCapStyle];
1442     [path moveToPoint: NSMakePoint(x1, fe->h-y1)];
1443     [path lineToPoint: NSMakePoint(x2, fe->h-y2)];
1444     [path stroke];
1445 }
1446
1447 static void osx_draw_rect(void *handle, int x, int y, int w, int h, int colour)
1448 {
1449     frontend *fe = (frontend *)handle;
1450     NSRect r = { {x, fe->h - y - h}, {w,h} };
1451     
1452     [[NSGraphicsContext currentContext] setShouldAntialias:NO];
1453
1454     assert(colour >= 0 && colour < fe->ncolours);
1455     [fe->colours[colour] set];
1456
1457     NSRectFill(r);
1458 }
1459 static void osx_draw_text(void *handle, int x, int y, int fonttype,
1460                           int fontsize, int align, int colour, char *text)
1461 {
1462     frontend *fe = (frontend *)handle;
1463     NSString *string = [NSString stringWithUTF8String:text];
1464     NSDictionary *attr;
1465     NSFont *font;
1466     NSSize size;
1467     NSPoint point;
1468
1469     [[NSGraphicsContext currentContext] setShouldAntialias:YES];
1470
1471     assert(colour >= 0 && colour < fe->ncolours);
1472
1473     if (fonttype == FONT_FIXED)
1474         font = [NSFont userFixedPitchFontOfSize:fontsize];
1475     else
1476         font = [NSFont userFontOfSize:fontsize];
1477
1478     attr = [NSDictionary dictionaryWithObjectsAndKeys:
1479             fe->colours[colour], NSForegroundColorAttributeName,
1480             font, NSFontAttributeName, nil];
1481
1482     point.x = x;
1483     point.y = fe->h - y;
1484
1485     size = [string sizeWithAttributes:attr];
1486     if (align & ALIGN_HRIGHT)
1487         point.x -= size.width;
1488     else if (align & ALIGN_HCENTRE)
1489         point.x -= size.width / 2;
1490     if (align & ALIGN_VCENTRE)
1491         point.y -= size.height / 2;
1492
1493     [string drawAtPoint:point withAttributes:attr];
1494 }
1495 static char *osx_text_fallback(void *handle, const char *const *strings,
1496                                int nstrings)
1497 {
1498     /*
1499      * We assume OS X can cope with any UTF-8 likely to be emitted
1500      * by a puzzle.
1501      */
1502     return dupstr(strings[0]);
1503 }
1504 struct blitter {
1505     int w, h;
1506     int x, y;
1507     NSImage *img;
1508 };
1509 static blitter *osx_blitter_new(void *handle, int w, int h)
1510 {
1511     blitter *bl = snew(blitter);
1512     bl->x = bl->y = -1;
1513     bl->w = w;
1514     bl->h = h;
1515     bl->img = [[NSImage alloc] initWithSize:NSMakeSize(w, h)];
1516     return bl;
1517 }
1518 static void osx_blitter_free(void *handle, blitter *bl)
1519 {
1520     [bl->img release];
1521     sfree(bl);
1522 }
1523 static void osx_blitter_save(void *handle, blitter *bl, int x, int y)
1524 {
1525     frontend *fe = (frontend *)handle;
1526     int sx, sy, sX, sY, dx, dy, dX, dY;
1527     [fe->image unlockFocus];
1528     [bl->img lockFocus];
1529
1530     /*
1531      * Find the intersection of the source and destination rectangles,
1532      * so as to avoid trying to copy from outside the source image,
1533      * which GNUstep dislikes.
1534      *
1535      * Lower-case x,y coordinates are bottom left box corners;
1536      * upper-case X,Y are the top right.
1537      */
1538     sx = x; sy = fe->h - y - bl->h;
1539     sX = sx + bl->w; sY = sy + bl->h;
1540     dx = dy = 0;
1541     dX = bl->w; dY = bl->h;
1542     if (sx < 0) {
1543         dx += -sx;
1544         sx = 0;
1545     }
1546     if (sy < 0) {
1547         dy += -sy;
1548         sy = 0;
1549     }
1550     if (sX > fe->w) {
1551         dX -= (sX - fe->w);
1552         sX = fe->w;
1553     }
1554     if (sY > fe->h) {
1555         dY -= (sY - fe->h);
1556         sY = fe->h;
1557     }
1558
1559     [fe->image drawInRect:NSMakeRect(dx, dy, dX-dx, dY-dy)
1560                  fromRect:NSMakeRect(sx, sy, sX-sx, sY-sy)
1561                 operation:NSCompositeCopy fraction:1.0];
1562     [bl->img unlockFocus];
1563     [fe->image lockFocus];
1564     bl->x = x;
1565     bl->y = y;
1566 }
1567 static void osx_blitter_load(void *handle, blitter *bl, int x, int y)
1568 {
1569     frontend *fe = (frontend *)handle;
1570     if (x == BLITTER_FROMSAVED && y == BLITTER_FROMSAVED) {
1571         x = bl->x;
1572         y = bl->y;
1573     }
1574     [bl->img drawInRect:NSMakeRect(x, fe->h - y - bl->h, bl->w, bl->h)
1575         fromRect:NSMakeRect(0, 0, bl->w, bl->h)
1576         operation:NSCompositeCopy fraction:1.0];
1577 }
1578 static void osx_draw_update(void *handle, int x, int y, int w, int h)
1579 {
1580     frontend *fe = (frontend *)handle;
1581     [fe->view setNeedsDisplayInRect:NSMakeRect(x, fe->h - y - h, w, h)];
1582 }
1583 static void osx_clip(void *handle, int x, int y, int w, int h)
1584 {
1585     frontend *fe = (frontend *)handle;
1586     NSRect r = { {x, fe->h - y - h}, {w, h} };
1587     
1588     if (!fe->clipped)
1589         [[NSGraphicsContext currentContext] saveGraphicsState];
1590     [NSBezierPath clipRect:r];
1591     fe->clipped = TRUE;
1592 }
1593 static void osx_unclip(void *handle)
1594 {
1595     frontend *fe = (frontend *)handle;
1596     if (fe->clipped)
1597         [[NSGraphicsContext currentContext] restoreGraphicsState];
1598     fe->clipped = FALSE;
1599 }
1600 static void osx_start_draw(void *handle)
1601 {
1602     frontend *fe = (frontend *)handle;
1603     [fe->image lockFocus];
1604     fe->clipped = FALSE;
1605 }
1606 static void osx_end_draw(void *handle)
1607 {
1608     frontend *fe = (frontend *)handle;
1609     [fe->image unlockFocus];
1610 }
1611 static void osx_status_bar(void *handle, char *text)
1612 {
1613     frontend *fe = (frontend *)handle;
1614     [fe->window setStatusLine:text];
1615 }
1616
1617 const struct drawing_api osx_drawing = {
1618     osx_draw_text,
1619     osx_draw_rect,
1620     osx_draw_line,
1621     osx_draw_polygon,
1622     osx_draw_circle,
1623     osx_draw_update,
1624     osx_clip,
1625     osx_unclip,
1626     osx_start_draw,
1627     osx_end_draw,
1628     osx_status_bar,
1629     osx_blitter_new,
1630     osx_blitter_free,
1631     osx_blitter_save,
1632     osx_blitter_load,
1633     NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */
1634     NULL, NULL,                        /* line_width, line_dotted */
1635     osx_text_fallback,
1636     osx_draw_thick_line,
1637 };
1638
1639 void deactivate_timer(frontend *fe)
1640 {
1641     [fe->window deactivateTimer];
1642 }
1643 void activate_timer(frontend *fe)
1644 {
1645     [fe->window activateTimer];
1646 }
1647
1648 /* ----------------------------------------------------------------------
1649  * AppController: the object which receives the messages from all
1650  * menu selections that aren't standard OS X functions.
1651  */
1652 @interface AppController : NSObject <NSApplicationDelegate>
1653 {
1654 }
1655 - (void)newGameWindow:(id)sender;
1656 - (void)about:(id)sender;
1657 @end
1658
1659 @implementation AppController
1660
1661 - (void)newGameWindow:(id)sender
1662 {
1663     const game *g = [sender getPayload];
1664     id win;
1665
1666     win = [[GameWindow alloc] initWithGame:g];
1667     [win makeKeyAndOrderFront:self];
1668 }
1669
1670 - (void)about:(id)sender
1671 {
1672     id win;
1673
1674     win = [[AboutBox alloc] init];
1675     [win makeKeyAndOrderFront:self];    
1676 }
1677
1678 - (NSMenu *)applicationDockMenu:(NSApplication *)sender
1679 {
1680     NSMenu *menu = newmenu("Dock Menu");
1681     {
1682         int i;
1683
1684         for (i = 0; i < gamecount; i++) {
1685             id item =
1686                 initnewitem([DataMenuItem allocWithZone:[NSMenu menuZone]],
1687                             menu, gamelist[i]->name, "", self,
1688                             @selector(newGameWindow:));
1689             [item setPayload:(void *)gamelist[i]];
1690         }
1691     }
1692     return menu;
1693 }
1694
1695 @end
1696
1697 /* ----------------------------------------------------------------------
1698  * Main program. Constructs the menus and runs the application.
1699  */
1700 int main(int argc, char **argv)
1701 {
1702     NSAutoreleasePool *pool;
1703     NSMenu *menu;
1704     AppController *controller;
1705     NSImage *icon;
1706
1707     pool = [[NSAutoreleasePool alloc] init];
1708
1709     icon = [NSImage imageNamed:@"NSApplicationIcon"];
1710     app = [NSApplication sharedApplication];
1711     [app setApplicationIconImage:icon];
1712
1713     controller = [[[AppController alloc] init] autorelease];
1714     [app setDelegate:controller];
1715
1716     [app setMainMenu: newmenu("Main Menu")];
1717
1718     menu = newsubmenu([app mainMenu], "Apple Menu");
1719     newitem(menu, "About Puzzles", "", NULL, @selector(about:));
1720     [menu addItem:[NSMenuItem separatorItem]];
1721     [app setServicesMenu:newsubmenu(menu, "Services")];
1722     [menu addItem:[NSMenuItem separatorItem]];
1723     newitem(menu, "Hide Puzzles", "h", app, @selector(hide:));
1724     newitem(menu, "Hide Others", "o-h", app, @selector(hideOtherApplications:));
1725     newitem(menu, "Show All", "", app, @selector(unhideAllApplications:));
1726     [menu addItem:[NSMenuItem separatorItem]];
1727     newitem(menu, "Quit", "q", app, @selector(terminate:));
1728     [app setAppleMenu: menu];
1729
1730     menu = newsubmenu([app mainMenu], "File");
1731     newitem(menu, "Open", "o", NULL, @selector(loadSavedGame:));
1732     newitem(menu, "Save As", "s", NULL, @selector(saveGame:));
1733     newitem(menu, "New Game", "n", NULL, @selector(newGame:));
1734     newitem(menu, "Restart Game", "r", NULL, @selector(restartGame:));
1735     newitem(menu, "Specific Game", "", NULL, @selector(specificGame:));
1736     newitem(menu, "Specific Random Seed", "", NULL,
1737                    @selector(specificRandomGame:));
1738     [menu addItem:[NSMenuItem separatorItem]];
1739     {
1740         NSMenu *submenu = newsubmenu(menu, "New Window");
1741         int i;
1742
1743         for (i = 0; i < gamecount; i++) {
1744             id item =
1745                 initnewitem([DataMenuItem allocWithZone:[NSMenu menuZone]],
1746                             submenu, gamelist[i]->name, "", controller,
1747                             @selector(newGameWindow:));
1748             [item setPayload:(void *)gamelist[i]];
1749         }
1750     }
1751     [menu addItem:[NSMenuItem separatorItem]];
1752     newitem(menu, "Close", "w", NULL, @selector(performClose:));
1753
1754     menu = newsubmenu([app mainMenu], "Edit");
1755     newitem(menu, "Undo", "z", NULL, @selector(undoMove:));
1756     newitem(menu, "Redo", "S-z", NULL, @selector(redoMove:));
1757     [menu addItem:[NSMenuItem separatorItem]];
1758     newitem(menu, "Cut", "x", NULL, @selector(cut:));
1759     newitem(menu, "Copy", "c", NULL, @selector(copy:));
1760     newitem(menu, "Paste", "v", NULL, @selector(paste:));
1761     [menu addItem:[NSMenuItem separatorItem]];
1762     newitem(menu, "Solve", "S-s", NULL, @selector(solveGame:));
1763
1764     menu = newsubmenu([app mainMenu], "Type");
1765     typemenu = menu;
1766     newitem(menu, "Custom", "", NULL, @selector(customGameType:));
1767
1768     menu = newsubmenu([app mainMenu], "Window");
1769     [app setWindowsMenu: menu];
1770     newitem(menu, "Minimise Window", "m", NULL, @selector(performMiniaturize:));
1771
1772     menu = newsubmenu([app mainMenu], "Help");
1773     newitem(menu, "Puzzles Help", "?", app, @selector(showHelp:));
1774
1775     [app run];
1776     [pool release];
1777
1778     return 0;
1779 }