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