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