Dynamically frob the menu bar to achieve selection of game presets.
[sgt/puzzles] / macosx.m
CommitLineData
9494d866 1/*
2 * Mac OS X / Cocoa front end to puzzles.
00461804 3 *
9494d866 4 * TODO:
00461804 5 *
9494d866 6 * - status bar support.
00461804 7 *
9494d866 8 * - configurability. Will no doubt involve learning all about the
9 * dialog control side of Cocoa.
00461804 10 *
9494d866 11 * - needs an icon.
00461804 12 *
9494d866 13 * - not sure what I should be doing about default window
14 * placement. Centring new windows is a bit feeble, but what's
15 * better? Is there a standard way to tell the OS "here's the
16 * _size_ of window I want, now use your best judgment about the
17 * initial position"?
00461804 18 *
9494d866 19 * - a brief frob of the Mac numeric keypad suggests that it
20 * generates numbers no matter what you do. I wonder if I should
21 * try to figure out a way of detecting keypad codes so I can
6d634196 22 * implement UP_LEFT and friends. Alternatively, perhaps I
23 * should simply assign the number keys to UP_LEFT et al?
24 * They're not in use for anything else right now.
00461804 25 *
9494d866 26 * - proper fatal errors.
00461804 27 *
9494d866 28 * - is there a better approach to frontend_default_colour?
00461804 29 *
30 * - do we need any more options in the Window menu?
31 *
32 * - see if we can do anything to one-button-ise the multi-button
33 * dependent puzzle UIs:
34 * - Pattern is a _little_ unwieldy but not too bad (since
35 * generally you never need the middle button unless you've
36 * made a mistake, so it's just click versus command-click).
37 * - Net is utterly vile; having normal click be one rotate and
38 * command-click be the other introduces a horrid asymmetry,
39 * and yet requiring a shift key for _each_ click would be
40 * even worse because rotation feels as if it ought to be the
41 * default action. I fear this is why the Flash Net had the
42 * UI it did...
43 *
6d634196 44 * - Find out how to do help, and do some. We have a help file; at
45 * _worst_ this should involve a new Halibut back end, but I
46 * think help is HTML round here anyway so perhaps we can work
47 * with what we already have.
7e04cb48 48 *
49 * - Can we arrange for a pop-up menu from the Dock icon which
50 * launches specific games, perhaps?
51 *
52 * Grotty implementation details that could probably be improved:
53 *
54 * - I am _utterly_ unconvinced that NSImageView was the right way
55 * to go about having a window with a reliable backing store! It
56 * just doesn't feel right; NSImageView is a _control_. Is there
57 * a simpler way?
58 *
59 * - Resizing is currently very bad; rather than bother to work
60 * out how to resize the NSImageView, I just splatter and
61 * recreate it.
9494d866 62 */
63
64#include <ctype.h>
65#include <sys/time.h>
66#import <Cocoa/Cocoa.h>
67#include "puzzles.h"
68
69void fatal(char *fmt, ...)
70{
71 /* FIXME: This will do for testing, but should be GUI-ish instead. */
72 va_list ap;
73
74 fprintf(stderr, "fatal error: ");
75
76 va_start(ap, fmt);
77 vfprintf(stderr, fmt, ap);
78 va_end(ap);
79
80 fprintf(stderr, "\n");
81 exit(1);
82}
83
84void frontend_default_colour(frontend *fe, float *output)
85{
86 /* FIXME */
87 output[0] = output[1] = output[2] = 0.8F;
88}
89void status_bar(frontend *fe, char *text)
90{
91 /* FIXME */
92}
93
94void get_random_seed(void **randseed, int *randseedsize)
95{
96 time_t *tp = snew(time_t);
97 time(tp);
98 *randseed = (void *)tp;
99 *randseedsize = sizeof(time_t);
100}
101
102/* ----------------------------------------------------------------------
7e04cb48 103 * Global variables.
104 */
105
106/*
107 * The `Type' menu. We frob this dynamically to allow the user to
108 * choose a preset set of settings from the current game.
109 */
110NSMenu *typemenu;
111
112/* ----------------------------------------------------------------------
113 * Tiny extension to NSMenuItem which carries a payload of a `void
114 * *', allowing several menu items to invoke the same message but
115 * pass different data through it.
116 */
117@interface DataMenuItem : NSMenuItem
118{
119 void *payload;
120 int payload_free;
121}
122- (void)setPayload:(void *)d;
123- (void)setPayloadFree:(BOOL)yesno;
124- (void *)getPayload;
125@end
126@implementation DataMenuItem
127- (id)initWithTitle:(NSString *)title
128 action:(SEL)act
129 keyEquivalent:(NSString *)key
130{
131 payload = NULL;
132 payload_free = NO;
133 return [super initWithTitle:title action:act keyEquivalent:key];
134}
135- (void)setPayload:(void *)d
136{
137 payload = d;
138}
139- (void)setPayloadFree:(BOOL)yesno
140{
141 payload_free = yesno;
142}
143- (void *)getPayload
144{
145 return payload;
146}
147- (void)dealloc
148{
149 if (payload_free)
150 sfree(payload);
151 [super dealloc];
152}
153@end
154
155/* ----------------------------------------------------------------------
9494d866 156 * The front end presented to midend.c.
157 *
158 * This is mostly a subclass of NSWindow. The actual `frontend'
159 * structure passed to the midend contains a variety of pointers,
160 * including that window object but also including the image we
161 * draw on, an ImageView to display it in the window, and so on.
162 */
163
164@class GameWindow;
165@class MyImageView;
166
167struct frontend {
168 GameWindow *window;
169 NSImage *image;
170 MyImageView *view;
171 NSColor **colours;
172 int ncolours;
173 int clipped;
174};
175
176@interface MyImageView : NSImageView
177{
178 GameWindow *ourwin;
179}
180- (void)setWindow:(GameWindow *)win;
181- (BOOL)isFlipped;
182- (void)mouseEvent:(NSEvent *)ev button:(int)b;
183- (void)mouseDown:(NSEvent *)ev;
184- (void)mouseDragged:(NSEvent *)ev;
185- (void)mouseUp:(NSEvent *)ev;
186- (void)rightMouseDown:(NSEvent *)ev;
187- (void)rightMouseDragged:(NSEvent *)ev;
188- (void)rightMouseUp:(NSEvent *)ev;
189- (void)otherMouseDown:(NSEvent *)ev;
190- (void)otherMouseDragged:(NSEvent *)ev;
191- (void)otherMouseUp:(NSEvent *)ev;
192@end
193
194@interface GameWindow : NSWindow
195{
196 const game *ourgame;
197 midend_data *me;
198 struct frontend fe;
199 struct timeval last_time;
200 NSTimer *timer;
201}
202- (id)initWithGame:(const game *)g;
203- dealloc;
204- (void)processButton:(int)b x:(int)x y:(int)y;
205- (void)keyDown:(NSEvent *)ev;
206- (void)activateTimer;
207- (void)deactivateTimer;
208@end
209
210@implementation MyImageView
211
212- (void)setWindow:(GameWindow *)win
213{
214 ourwin = win;
215}
216
217- (BOOL)isFlipped
218{
219 return YES;
220}
221
222- (void)mouseEvent:(NSEvent *)ev button:(int)b
223{
224 NSPoint point = [self convertPoint:[ev locationInWindow] fromView:nil];
225 [ourwin processButton:b x:point.x y:point.y];
226}
227
228- (void)mouseDown:(NSEvent *)ev
229{
230 unsigned mod = [ev modifierFlags];
231 [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_BUTTON :
232 (mod & NSShiftKeyMask) ? MIDDLE_BUTTON :
233 LEFT_BUTTON)];
234}
235- (void)mouseDragged:(NSEvent *)ev
236{
237 unsigned mod = [ev modifierFlags];
238 [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_DRAG :
239 (mod & NSShiftKeyMask) ? MIDDLE_DRAG :
240 LEFT_DRAG)];
241}
242- (void)mouseUp:(NSEvent *)ev
243{
244 unsigned mod = [ev modifierFlags];
245 [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_RELEASE :
246 (mod & NSShiftKeyMask) ? MIDDLE_RELEASE :
247 LEFT_RELEASE)];
248}
249- (void)rightMouseDown:(NSEvent *)ev
250{
251 unsigned mod = [ev modifierFlags];
252 [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_BUTTON :
253 RIGHT_BUTTON)];
254}
255- (void)rightMouseDragged:(NSEvent *)ev
256{
257 unsigned mod = [ev modifierFlags];
258 [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_DRAG :
259 RIGHT_DRAG)];
260}
261- (void)rightMouseUp:(NSEvent *)ev
262{
263 unsigned mod = [ev modifierFlags];
264 [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_RELEASE :
265 RIGHT_RELEASE)];
266}
267- (void)otherMouseDown:(NSEvent *)ev
268{
269 [self mouseEvent:ev button:MIDDLE_BUTTON];
270}
271- (void)otherMouseDragged:(NSEvent *)ev
272{
273 [self mouseEvent:ev button:MIDDLE_DRAG];
274}
275- (void)otherMouseUp:(NSEvent *)ev
276{
277 [self mouseEvent:ev button:MIDDLE_RELEASE];
278}
279@end
280
281@implementation GameWindow
7e04cb48 282- (void)setupContentView
283{
284 NSSize size = {0,0};
285 int w, h;
286
287 midend_size(me, &w, &h);
288 size.width = w;
289 size.height = h;
290
291 fe.image = [[NSImage alloc] initWithSize:size];
292 [fe.image setFlipped:YES];
293 fe.view = [[MyImageView alloc]
294 initWithFrame:[self contentRectForFrameRect:[self frame]]];
295 [fe.view setImage:fe.image];
296 [fe.view setWindow:self];
297
298 midend_redraw(me);
299
300 [self setContentView:fe.view];
301}
9494d866 302- (id)initWithGame:(const game *)g
303{
304 NSRect rect = { {0,0}, {0,0} };
305 int w, h;
306
307 ourgame = g;
308
309 fe.window = self;
310
311 me = midend_new(&fe, ourgame);
312 /*
313 * If we ever need to open a fresh window using a provided game
314 * ID, I think the right thing is to move most of this method
315 * into a new initWithGame:gameID: method, and have
316 * initWithGame: simply call that one and pass it NULL.
317 */
318 midend_new_game(me);
319 midend_size(me, &w, &h);
320 rect.size.width = w;
321 rect.size.height = h;
322
323 self = [super initWithContentRect:rect
324 styleMask:(NSTitledWindowMask | NSMiniaturizableWindowMask |
325 NSClosableWindowMask)
326 backing:NSBackingStoreBuffered
327 defer:true];
328 [self setTitle:[NSString stringWithCString:ourgame->name]];
329
330 {
331 float *colours;
332 int i, ncolours;
333
334 colours = midend_colours(me, &ncolours);
335 fe.ncolours = ncolours;
336 fe.colours = snewn(ncolours, NSColor *);
337
338 for (i = 0; i < ncolours; i++) {
339 fe.colours[i] = [[NSColor colorWithDeviceRed:colours[i*3]
340 green:colours[i*3+1] blue:colours[i*3+2]
341 alpha:1.0] retain];
342 }
343 }
344
7e04cb48 345 [self setupContentView];
9494d866 346 [self setIgnoresMouseEvents:NO];
347
9494d866 348 [self center]; /* :-) */
349
350 return self;
351}
352
353- dealloc
354{
355 int i;
356 for (i = 0; i < fe.ncolours; i++) {
357 [fe.colours[i] release];
358 }
359 sfree(fe.colours);
360 midend_free(me);
361 return [super dealloc];
362}
363
364- (void)processButton:(int)b x:(int)x y:(int)y
365{
366 if (!midend_process_key(me, x, y, b))
367 [self close];
368}
369
370- (void)keyDown:(NSEvent *)ev
371{
372 NSString *s = [ev characters];
373 int i, n = [s length];
374
375 for (i = 0; i < n; i++) {
376 int c = [s characterAtIndex:i];
377
378 /*
379 * ASCII gets passed straight to midend_process_key.
380 * Anything above that has to be translated to our own
381 * function key codes.
382 */
383 if (c >= 0x80) {
384 switch (c) {
385 case NSUpArrowFunctionKey:
386 c = CURSOR_UP;
387 break;
388 case NSDownArrowFunctionKey:
389 c = CURSOR_DOWN;
390 break;
391 case NSLeftArrowFunctionKey:
392 c = CURSOR_LEFT;
393 break;
394 case NSRightArrowFunctionKey:
395 c = CURSOR_RIGHT;
396 break;
397 default:
398 continue;
399 }
400 }
401
402 [self processButton:c x:-1 y:-1];
403 }
404}
405
406- (void)activateTimer
407{
408 if (timer != nil)
409 return;
410
411 timer = [NSTimer scheduledTimerWithTimeInterval:0.02
412 target:self selector:@selector(timerTick:)
413 userInfo:nil repeats:YES];
414 gettimeofday(&last_time, NULL);
415}
416
417- (void)deactivateTimer
418{
419 if (timer == nil)
420 return;
421
422 [timer invalidate];
423 timer = nil;
424}
425
426- (void)timerTick:(id)sender
427{
428 struct timeval now;
429 float elapsed;
430 gettimeofday(&now, NULL);
431 elapsed = ((now.tv_usec - last_time.tv_usec) * 0.000001F +
432 (now.tv_sec - last_time.tv_sec));
433 midend_timer(me, elapsed);
434 last_time = now;
435}
436
00461804 437- (void)newGame:(id)sender
438{
439 [self processButton:'n' x:-1 y:-1];
440}
441- (void)restartGame:(id)sender
442{
443 [self processButton:'r' x:-1 y:-1];
444}
445- (void)undoMove:(id)sender
446{
447 [self processButton:'u' x:-1 y:-1];
448}
449- (void)redoMove:(id)sender
450{
451 [self processButton:'r'&0x1F x:-1 y:-1];
452}
453
7e04cb48 454- (void)clearTypeMenu
455{
456 while ([typemenu numberOfItems] > 1)
457 [typemenu removeItemAtIndex:0];
458}
459
460- (void)becomeKeyWindow
461{
462 int n;
463
464 [self clearTypeMenu];
465
466 [super becomeKeyWindow];
467
468 n = midend_num_presets(me);
469
470 if (n > 0) {
471 [typemenu insertItem:[NSMenuItem separatorItem] atIndex:0];
472 while (n--) {
473 char *name;
474 game_params *params;
475 DataMenuItem *item;
476
477 midend_fetch_preset(me, n, &name, &params);
478
479 item = [[[DataMenuItem alloc]
480 initWithTitle:[NSString stringWithCString:name]
481 action:NULL keyEquivalent:@""]
482 autorelease];
483
484 [item setEnabled:YES];
485 [item setTarget:self];
486 [item setAction:@selector(presetGame:)];
487 [item setPayload:params];
488 [item setPayloadFree:YES];
489
490 [typemenu insertItem:item atIndex:0];
491 }
492 }
493}
494
495- (void)resignKeyWindow
496{
497 [self clearTypeMenu];
498 [super resignKeyWindow];
499}
500
501- (void)close
502{
503 [self clearTypeMenu];
504 [super close];
505}
506
507- (void)resizeForNewGameParams
508{
509 NSSize size = {0,0};
510 int w, h;
511
512 midend_size(me, &w, &h);
513 size.width = w;
514 size.height = h;
515
516 NSDisableScreenUpdates();
517 [self setContentSize:size];
518 [self setupContentView];
519 NSEnableScreenUpdates();
520}
521
522- (void)presetGame:(id)sender
523{
524 game_params *params = [sender getPayload];
525
526 midend_set_params(me, params);
527 midend_new_game(me);
528
529 [self resizeForNewGameParams];
530}
531
9494d866 532@end
533
534/*
535 * Drawing routines called by the midend.
536 */
537void draw_polygon(frontend *fe, int *coords, int npoints,
538 int fill, int colour)
539{
540 NSBezierPath *path = [NSBezierPath bezierPath];
541 int i;
542
543 [[NSGraphicsContext currentContext] setShouldAntialias:YES];
544
545 assert(colour >= 0 && colour < fe->ncolours);
546 [fe->colours[colour] set];
547
548 for (i = 0; i < npoints; i++) {
549 NSPoint p = { coords[i*2] + 0.5, coords[i*2+1] + 0.5 };
550 if (i == 0)
551 [path moveToPoint:p];
552 else
553 [path lineToPoint:p];
554 }
555
556 [path closePath];
557
558 if (fill)
559 [path fill];
560 else
561 [path stroke];
562}
563void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour)
564{
565 NSBezierPath *path = [NSBezierPath bezierPath];
566 NSPoint p1 = { x1 + 0.5, y1 + 0.5 }, p2 = { x2 + 0.5, y2 + 0.5 };
567
568 [[NSGraphicsContext currentContext] setShouldAntialias:NO];
569
570 assert(colour >= 0 && colour < fe->ncolours);
571 [fe->colours[colour] set];
572
573 [path moveToPoint:p1];
574 [path lineToPoint:p2];
575 [path stroke];
576}
577void draw_rect(frontend *fe, int x, int y, int w, int h, int colour)
578{
579 NSRect r = { {x,y}, {w,h} };
580
581 [[NSGraphicsContext currentContext] setShouldAntialias:NO];
582
583 assert(colour >= 0 && colour < fe->ncolours);
584 [fe->colours[colour] set];
585
586 NSRectFill(r);
587}
588void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
589 int align, int colour, char *text)
590{
591 NSString *string = [NSString stringWithCString:text];
592 NSDictionary *attr;
593 NSFont *font;
594 NSSize size;
595 NSPoint point;
596
597 [[NSGraphicsContext currentContext] setShouldAntialias:YES];
598
599 assert(colour >= 0 && colour < fe->ncolours);
600
601 if (fonttype == FONT_FIXED)
602 font = [NSFont userFixedPitchFontOfSize:fontsize];
603 else
604 font = [NSFont userFontOfSize:fontsize];
605
606 attr = [NSDictionary dictionaryWithObjectsAndKeys:
607 fe->colours[colour], NSForegroundColorAttributeName,
608 font, NSFontAttributeName, nil];
609
610 point.x = x;
611 point.y = y;
612
613 size = [string sizeWithAttributes:attr];
614 if (align & ALIGN_HRIGHT)
615 point.x -= size.width;
616 else if (align & ALIGN_HCENTRE)
617 point.x -= size.width / 2;
618 if (align & ALIGN_VCENTRE)
619 point.y -= size.height / 2;
620
621 [string drawAtPoint:point withAttributes:attr];
622}
623void draw_update(frontend *fe, int x, int y, int w, int h)
624{
625 /* FIXME */
626}
627void clip(frontend *fe, int x, int y, int w, int h)
628{
629 NSRect r = { {x,y}, {w,h} };
630
631 if (!fe->clipped)
632 [[NSGraphicsContext currentContext] saveGraphicsState];
633 [NSBezierPath clipRect:r];
634 fe->clipped = TRUE;
635}
636void unclip(frontend *fe)
637{
638 if (fe->clipped)
639 [[NSGraphicsContext currentContext] restoreGraphicsState];
640 fe->clipped = FALSE;
641}
642void start_draw(frontend *fe)
643{
644 [fe->image lockFocus];
645 fe->clipped = FALSE;
646}
647void end_draw(frontend *fe)
648{
649 [fe->image unlockFocus];
650 [fe->view setNeedsDisplay];
651}
652
653void deactivate_timer(frontend *fe)
654{
655 [fe->window deactivateTimer];
656}
657void activate_timer(frontend *fe)
658{
659 [fe->window activateTimer];
660}
661
662/* ----------------------------------------------------------------------
663 * Utility routines for constructing OS X menus.
00461804 664 */
9494d866 665
666NSMenu *newmenu(const char *title)
667{
668 return [[[NSMenu allocWithZone:[NSMenu menuZone]]
669 initWithTitle:[NSString stringWithCString:title]]
670 autorelease];
671}
672
673NSMenu *newsubmenu(NSMenu *parent, const char *title)
674{
675 NSMenuItem *item;
676 NSMenu *child;
677
678 item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]]
679 initWithTitle:[NSString stringWithCString:title]
680 action:NULL
681 keyEquivalent:@""]
682 autorelease];
683 child = newmenu(title);
684 [item setEnabled:YES];
685 [item setSubmenu:child];
686 [parent addItem:item];
687 return child;
688}
689
690id initnewitem(NSMenuItem *item, NSMenu *parent, const char *title,
691 const char *key, id target, SEL action)
692{
693 unsigned mask = NSCommandKeyMask;
694
695 if (key[strcspn(key, "-")]) {
696 while (*key && *key != '-') {
697 int c = tolower((unsigned char)*key);
698 if (c == 's') {
699 mask |= NSShiftKeyMask;
700 } else if (c == 'o' || c == 'a') {
701 mask |= NSAlternateKeyMask;
702 }
703 key++;
704 }
705 if (*key)
706 key++;
707 }
708
709 item = [[item initWithTitle:[NSString stringWithCString:title]
710 action:NULL
711 keyEquivalent:[NSString stringWithCString:key]]
712 autorelease];
713
714 if (*key)
715 [item setKeyEquivalentModifierMask: mask];
716
717 [item setEnabled:YES];
718 [item setTarget:target];
719 [item setAction:action];
720
721 [parent addItem:item];
722
723 return item;
724}
725
726NSMenuItem *newitem(NSMenu *parent, char *title, char *key,
727 id target, SEL action)
728{
729 return initnewitem([NSMenuItem allocWithZone:[NSMenu menuZone]],
730 parent, title, key, target, action);
731}
732
733/* ----------------------------------------------------------------------
9494d866 734 * AppController: the object which receives the messages from all
735 * menu selections that aren't standard OS X functions.
736 */
737@interface AppController : NSObject
738{
739}
740- (IBAction)newGame:(id)sender;
741@end
742
743@implementation AppController
744
745- (IBAction)newGame:(id)sender
746{
7e04cb48 747 const game *g = [sender getPayload];
9494d866 748 id win;
749
750 win = [[GameWindow alloc] initWithGame:g];
751 [win makeKeyAndOrderFront:self];
752}
753
754@end
755
756/* ----------------------------------------------------------------------
757 * Main program. Constructs the menus and runs the application.
758 */
759int main(int argc, char **argv)
760{
761 NSAutoreleasePool *pool;
762 NSMenu *menu;
763 NSMenuItem *item;
764 AppController *controller;
765
766 pool = [[NSAutoreleasePool alloc] init];
767 [NSApplication sharedApplication];
768
769 controller = [[[AppController alloc] init] autorelease];
770
771 [NSApp setMainMenu: newmenu("Main Menu")];
772
773 menu = newsubmenu([NSApp mainMenu], "Apple Menu");
774 [NSApp setServicesMenu:newsubmenu(menu, "Services")];
775 [menu addItem:[NSMenuItem separatorItem]];
776 item = newitem(menu, "Hide Puzzles", "h", NSApp, @selector(hide:));
777 item = newitem(menu, "Hide Others", "o-h", NSApp, @selector(hideOtherApplications:));
778 item = newitem(menu, "Show All", "", NSApp, @selector(unhideAllApplications:));
779 [menu addItem:[NSMenuItem separatorItem]];
780 item = newitem(menu, "Quit", "q", NSApp, @selector(terminate:));
781 [NSApp setAppleMenu: menu];
782
00461804 783 menu = newsubmenu([NSApp mainMenu], "Open");
9494d866 784 {
785 int i;
786
787 for (i = 0; i < gamecount; i++) {
788 id item =
7e04cb48 789 initnewitem([DataMenuItem allocWithZone:[NSMenu menuZone]],
9494d866 790 menu, gamelist[i]->name, "", controller,
791 @selector(newGame:));
7e04cb48 792 [item setPayload:(void *)gamelist[i]];
9494d866 793 }
794 }
795
00461804 796 menu = newsubmenu([NSApp mainMenu], "Game");
797 item = newitem(menu, "New", "n", NULL, @selector(newGame:));
798 item = newitem(menu, "Restart", "r", NULL, @selector(restartGame:));
799 item = newitem(menu, "Specific", "", NULL, @selector(specificGame:));
800 [menu addItem:[NSMenuItem separatorItem]];
801 item = newitem(menu, "Undo", "z", NULL, @selector(undoMove:));
802 item = newitem(menu, "Redo", "S-z", NULL, @selector(redoMove:));
803 [menu addItem:[NSMenuItem separatorItem]];
804 item = newitem(menu, "Close", "w", NULL, @selector(performClose:));
805
806 menu = newsubmenu([NSApp mainMenu], "Type");
7e04cb48 807 typemenu = menu;
00461804 808 item = newitem(menu, "Custom", "", NULL, @selector(customGameType:));
809
810 menu = newsubmenu([NSApp mainMenu], "Window");
9494d866 811 [NSApp setWindowsMenu: menu];
00461804 812 item = newitem(menu, "Minimise Window", "m", NULL, @selector(performMiniaturize:));
9494d866 813
814 [NSApp run];
815 [pool release];
816}