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 | |
69 | void 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 | |
84 | void frontend_default_colour(frontend *fe, float *output) |
85 | { |
86 | /* FIXME */ |
87 | output[0] = output[1] = output[2] = 0.8F; |
88 | } |
89 | void status_bar(frontend *fe, char *text) |
90 | { |
91 | /* FIXME */ |
92 | } |
93 | |
94 | void 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 | */ |
110 | NSMenu *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 | |
167 | struct 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, ¶ms); |
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 | */ |
537 | void 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 | } |
563 | void 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 | } |
577 | void 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 | } |
588 | void 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 | } |
623 | void draw_update(frontend *fe, int x, int y, int w, int h) |
624 | { |
625 | /* FIXME */ |
626 | } |
627 | void 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 | } |
636 | void unclip(frontend *fe) |
637 | { |
638 | if (fe->clipped) |
639 | [[NSGraphicsContext currentContext] restoreGraphicsState]; |
640 | fe->clipped = FALSE; |
641 | } |
642 | void start_draw(frontend *fe) |
643 | { |
644 | [fe->image lockFocus]; |
645 | fe->clipped = FALSE; |
646 | } |
647 | void end_draw(frontend *fe) |
648 | { |
649 | [fe->image unlockFocus]; |
650 | [fe->view setNeedsDisplay]; |
651 | } |
652 | |
653 | void deactivate_timer(frontend *fe) |
654 | { |
655 | [fe->window deactivateTimer]; |
656 | } |
657 | void activate_timer(frontend *fe) |
658 | { |
659 | [fe->window activateTimer]; |
660 | } |
661 | |
662 | /* ---------------------------------------------------------------------- |
663 | * Utility routines for constructing OS X menus. |
00461804 |
664 | */ |
9494d866 |
665 | |
666 | NSMenu *newmenu(const char *title) |
667 | { |
668 | return [[[NSMenu allocWithZone:[NSMenu menuZone]] |
669 | initWithTitle:[NSString stringWithCString:title]] |
670 | autorelease]; |
671 | } |
672 | |
673 | NSMenu *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 | |
690 | id 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 | |
726 | NSMenuItem *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 | */ |
759 | int 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 | } |