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