9494d866 |
1 | /* |
2 | * Mac OS X / Cocoa front end to puzzles. |
3 | * |
4 | * TODO: |
5 | * |
9494d866 |
6 | * - status bar support. |
7 | * |
8 | * - preset selection. Should be reasonably simple: just a matter |
9 | * of dynamically frobbing the menu bar. |
10 | * |
11 | * - configurability. Will no doubt involve learning all about the |
12 | * dialog control side of Cocoa. |
13 | * |
14 | * - needs an icon. |
15 | * |
16 | * - not sure what I should be doing about default window |
17 | * placement. Centring new windows is a bit feeble, but what's |
18 | * better? Is there a standard way to tell the OS "here's the |
19 | * _size_ of window I want, now use your best judgment about the |
20 | * initial position"? |
21 | * |
22 | * - a brief frob of the Mac numeric keypad suggests that it |
23 | * generates numbers no matter what you do. I wonder if I should |
24 | * try to figure out a way of detecting keypad codes so I can |
6d634196 |
25 | * implement UP_LEFT and friends. Alternatively, perhaps I |
26 | * should simply assign the number keys to UP_LEFT et al? |
27 | * They're not in use for anything else right now. |
9494d866 |
28 | * |
29 | * - proper fatal errors. |
30 | * |
31 | * - is there a better approach to frontend_default_colour? |
32 | * |
33 | * - some options in the Window menu! Close and Minimise, I think, |
34 | * at least. |
6d634196 |
35 | * |
36 | * - more Mac-ish key bindings. I suspect, for example, that Undo |
37 | * should be Command-Z as well as (or even instead of?) Ctrl-Z. |
38 | * |
39 | * - Which reminds me, commands like Undo and Redo also ought to |
40 | * be available from the menus. Actually, the sensible thing is |
41 | * probably to go and look at the menus on Unix and make sure |
42 | * everything is there. |
43 | * |
44 | * - see if we can do anything to one-button-ise the puzzle UIs. |
45 | * Some are fine as is (Sixteen, Fifteen, Rectangles, Netslide, |
46 | * Cube), some are a little unwieldy with Command-clicking |
47 | * (Pattern), and some are utterly horrid (Net). |
48 | * |
49 | * - Find out how to do help, and do some. We have a help file; at |
50 | * _worst_ this should involve a new Halibut back end, but I |
51 | * think help is HTML round here anyway so perhaps we can work |
52 | * with what we already have. |
9494d866 |
53 | */ |
54 | |
55 | #include <ctype.h> |
56 | #include <sys/time.h> |
57 | #import <Cocoa/Cocoa.h> |
58 | #include "puzzles.h" |
59 | |
60 | void fatal(char *fmt, ...) |
61 | { |
62 | /* FIXME: This will do for testing, but should be GUI-ish instead. */ |
63 | va_list ap; |
64 | |
65 | fprintf(stderr, "fatal error: "); |
66 | |
67 | va_start(ap, fmt); |
68 | vfprintf(stderr, fmt, ap); |
69 | va_end(ap); |
70 | |
71 | fprintf(stderr, "\n"); |
72 | exit(1); |
73 | } |
74 | |
75 | void frontend_default_colour(frontend *fe, float *output) |
76 | { |
77 | /* FIXME */ |
78 | output[0] = output[1] = output[2] = 0.8F; |
79 | } |
80 | void status_bar(frontend *fe, char *text) |
81 | { |
82 | /* FIXME */ |
83 | } |
84 | |
85 | void get_random_seed(void **randseed, int *randseedsize) |
86 | { |
87 | time_t *tp = snew(time_t); |
88 | time(tp); |
89 | *randseed = (void *)tp; |
90 | *randseedsize = sizeof(time_t); |
91 | } |
92 | |
93 | /* ---------------------------------------------------------------------- |
94 | * The front end presented to midend.c. |
95 | * |
96 | * This is mostly a subclass of NSWindow. The actual `frontend' |
97 | * structure passed to the midend contains a variety of pointers, |
98 | * including that window object but also including the image we |
99 | * draw on, an ImageView to display it in the window, and so on. |
100 | */ |
101 | |
102 | @class GameWindow; |
103 | @class MyImageView; |
104 | |
105 | struct frontend { |
106 | GameWindow *window; |
107 | NSImage *image; |
108 | MyImageView *view; |
109 | NSColor **colours; |
110 | int ncolours; |
111 | int clipped; |
112 | }; |
113 | |
114 | @interface MyImageView : NSImageView |
115 | { |
116 | GameWindow *ourwin; |
117 | } |
118 | - (void)setWindow:(GameWindow *)win; |
119 | - (BOOL)isFlipped; |
120 | - (void)mouseEvent:(NSEvent *)ev button:(int)b; |
121 | - (void)mouseDown:(NSEvent *)ev; |
122 | - (void)mouseDragged:(NSEvent *)ev; |
123 | - (void)mouseUp:(NSEvent *)ev; |
124 | - (void)rightMouseDown:(NSEvent *)ev; |
125 | - (void)rightMouseDragged:(NSEvent *)ev; |
126 | - (void)rightMouseUp:(NSEvent *)ev; |
127 | - (void)otherMouseDown:(NSEvent *)ev; |
128 | - (void)otherMouseDragged:(NSEvent *)ev; |
129 | - (void)otherMouseUp:(NSEvent *)ev; |
130 | @end |
131 | |
132 | @interface GameWindow : NSWindow |
133 | { |
134 | const game *ourgame; |
135 | midend_data *me; |
136 | struct frontend fe; |
137 | struct timeval last_time; |
138 | NSTimer *timer; |
139 | } |
140 | - (id)initWithGame:(const game *)g; |
141 | - dealloc; |
142 | - (void)processButton:(int)b x:(int)x y:(int)y; |
143 | - (void)keyDown:(NSEvent *)ev; |
144 | - (void)activateTimer; |
145 | - (void)deactivateTimer; |
146 | @end |
147 | |
148 | @implementation MyImageView |
149 | |
150 | - (void)setWindow:(GameWindow *)win |
151 | { |
152 | ourwin = win; |
153 | } |
154 | |
155 | - (BOOL)isFlipped |
156 | { |
157 | return YES; |
158 | } |
159 | |
160 | - (void)mouseEvent:(NSEvent *)ev button:(int)b |
161 | { |
162 | NSPoint point = [self convertPoint:[ev locationInWindow] fromView:nil]; |
163 | [ourwin processButton:b x:point.x y:point.y]; |
164 | } |
165 | |
166 | - (void)mouseDown:(NSEvent *)ev |
167 | { |
168 | unsigned mod = [ev modifierFlags]; |
169 | [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_BUTTON : |
170 | (mod & NSShiftKeyMask) ? MIDDLE_BUTTON : |
171 | LEFT_BUTTON)]; |
172 | } |
173 | - (void)mouseDragged:(NSEvent *)ev |
174 | { |
175 | unsigned mod = [ev modifierFlags]; |
176 | [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_DRAG : |
177 | (mod & NSShiftKeyMask) ? MIDDLE_DRAG : |
178 | LEFT_DRAG)]; |
179 | } |
180 | - (void)mouseUp:(NSEvent *)ev |
181 | { |
182 | unsigned mod = [ev modifierFlags]; |
183 | [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_RELEASE : |
184 | (mod & NSShiftKeyMask) ? MIDDLE_RELEASE : |
185 | LEFT_RELEASE)]; |
186 | } |
187 | - (void)rightMouseDown:(NSEvent *)ev |
188 | { |
189 | unsigned mod = [ev modifierFlags]; |
190 | [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_BUTTON : |
191 | RIGHT_BUTTON)]; |
192 | } |
193 | - (void)rightMouseDragged:(NSEvent *)ev |
194 | { |
195 | unsigned mod = [ev modifierFlags]; |
196 | [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_DRAG : |
197 | RIGHT_DRAG)]; |
198 | } |
199 | - (void)rightMouseUp:(NSEvent *)ev |
200 | { |
201 | unsigned mod = [ev modifierFlags]; |
202 | [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_RELEASE : |
203 | RIGHT_RELEASE)]; |
204 | } |
205 | - (void)otherMouseDown:(NSEvent *)ev |
206 | { |
207 | [self mouseEvent:ev button:MIDDLE_BUTTON]; |
208 | } |
209 | - (void)otherMouseDragged:(NSEvent *)ev |
210 | { |
211 | [self mouseEvent:ev button:MIDDLE_DRAG]; |
212 | } |
213 | - (void)otherMouseUp:(NSEvent *)ev |
214 | { |
215 | [self mouseEvent:ev button:MIDDLE_RELEASE]; |
216 | } |
217 | @end |
218 | |
219 | @implementation GameWindow |
220 | - (id)initWithGame:(const game *)g |
221 | { |
222 | NSRect rect = { {0,0}, {0,0} }; |
223 | int w, h; |
224 | |
225 | ourgame = g; |
226 | |
227 | fe.window = self; |
228 | |
229 | me = midend_new(&fe, ourgame); |
230 | /* |
231 | * If we ever need to open a fresh window using a provided game |
232 | * ID, I think the right thing is to move most of this method |
233 | * into a new initWithGame:gameID: method, and have |
234 | * initWithGame: simply call that one and pass it NULL. |
235 | */ |
236 | midend_new_game(me); |
237 | midend_size(me, &w, &h); |
238 | rect.size.width = w; |
239 | rect.size.height = h; |
240 | |
241 | self = [super initWithContentRect:rect |
242 | styleMask:(NSTitledWindowMask | NSMiniaturizableWindowMask | |
243 | NSClosableWindowMask) |
244 | backing:NSBackingStoreBuffered |
245 | defer:true]; |
246 | [self setTitle:[NSString stringWithCString:ourgame->name]]; |
247 | |
248 | { |
249 | float *colours; |
250 | int i, ncolours; |
251 | |
252 | colours = midend_colours(me, &ncolours); |
253 | fe.ncolours = ncolours; |
254 | fe.colours = snewn(ncolours, NSColor *); |
255 | |
256 | for (i = 0; i < ncolours; i++) { |
257 | fe.colours[i] = [[NSColor colorWithDeviceRed:colours[i*3] |
258 | green:colours[i*3+1] blue:colours[i*3+2] |
259 | alpha:1.0] retain]; |
260 | } |
261 | } |
262 | |
263 | fe.image = [[NSImage alloc] initWithSize:rect.size]; |
264 | [fe.image setFlipped:YES]; |
265 | fe.view = [[MyImageView alloc] |
266 | initWithFrame:[self contentRectForFrameRect:[self frame]]]; |
267 | [fe.view setImage:fe.image]; |
268 | [fe.view setWindow:self]; |
269 | [self setContentView:fe.view]; |
270 | [self setIgnoresMouseEvents:NO]; |
271 | |
272 | midend_redraw(me); |
273 | |
274 | [self center]; /* :-) */ |
275 | |
276 | return self; |
277 | } |
278 | |
279 | - dealloc |
280 | { |
281 | int i; |
282 | for (i = 0; i < fe.ncolours; i++) { |
283 | [fe.colours[i] release]; |
284 | } |
285 | sfree(fe.colours); |
286 | midend_free(me); |
287 | return [super dealloc]; |
288 | } |
289 | |
290 | - (void)processButton:(int)b x:(int)x y:(int)y |
291 | { |
292 | if (!midend_process_key(me, x, y, b)) |
293 | [self close]; |
294 | } |
295 | |
296 | - (void)keyDown:(NSEvent *)ev |
297 | { |
298 | NSString *s = [ev characters]; |
299 | int i, n = [s length]; |
300 | |
301 | for (i = 0; i < n; i++) { |
302 | int c = [s characterAtIndex:i]; |
303 | |
304 | /* |
305 | * ASCII gets passed straight to midend_process_key. |
306 | * Anything above that has to be translated to our own |
307 | * function key codes. |
308 | */ |
309 | if (c >= 0x80) { |
310 | switch (c) { |
311 | case NSUpArrowFunctionKey: |
312 | c = CURSOR_UP; |
313 | break; |
314 | case NSDownArrowFunctionKey: |
315 | c = CURSOR_DOWN; |
316 | break; |
317 | case NSLeftArrowFunctionKey: |
318 | c = CURSOR_LEFT; |
319 | break; |
320 | case NSRightArrowFunctionKey: |
321 | c = CURSOR_RIGHT; |
322 | break; |
323 | default: |
324 | continue; |
325 | } |
326 | } |
327 | |
328 | [self processButton:c x:-1 y:-1]; |
329 | } |
330 | } |
331 | |
332 | - (void)activateTimer |
333 | { |
334 | if (timer != nil) |
335 | return; |
336 | |
337 | timer = [NSTimer scheduledTimerWithTimeInterval:0.02 |
338 | target:self selector:@selector(timerTick:) |
339 | userInfo:nil repeats:YES]; |
340 | gettimeofday(&last_time, NULL); |
341 | } |
342 | |
343 | - (void)deactivateTimer |
344 | { |
345 | if (timer == nil) |
346 | return; |
347 | |
348 | [timer invalidate]; |
349 | timer = nil; |
350 | } |
351 | |
352 | - (void)timerTick:(id)sender |
353 | { |
354 | struct timeval now; |
355 | float elapsed; |
356 | gettimeofday(&now, NULL); |
357 | elapsed = ((now.tv_usec - last_time.tv_usec) * 0.000001F + |
358 | (now.tv_sec - last_time.tv_sec)); |
359 | midend_timer(me, elapsed); |
360 | last_time = now; |
361 | } |
362 | |
363 | @end |
364 | |
365 | /* |
366 | * Drawing routines called by the midend. |
367 | */ |
368 | void draw_polygon(frontend *fe, int *coords, int npoints, |
369 | int fill, int colour) |
370 | { |
371 | NSBezierPath *path = [NSBezierPath bezierPath]; |
372 | int i; |
373 | |
374 | [[NSGraphicsContext currentContext] setShouldAntialias:YES]; |
375 | |
376 | assert(colour >= 0 && colour < fe->ncolours); |
377 | [fe->colours[colour] set]; |
378 | |
379 | for (i = 0; i < npoints; i++) { |
380 | NSPoint p = { coords[i*2] + 0.5, coords[i*2+1] + 0.5 }; |
381 | if (i == 0) |
382 | [path moveToPoint:p]; |
383 | else |
384 | [path lineToPoint:p]; |
385 | } |
386 | |
387 | [path closePath]; |
388 | |
389 | if (fill) |
390 | [path fill]; |
391 | else |
392 | [path stroke]; |
393 | } |
394 | void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour) |
395 | { |
396 | NSBezierPath *path = [NSBezierPath bezierPath]; |
397 | NSPoint p1 = { x1 + 0.5, y1 + 0.5 }, p2 = { x2 + 0.5, y2 + 0.5 }; |
398 | |
399 | [[NSGraphicsContext currentContext] setShouldAntialias:NO]; |
400 | |
401 | assert(colour >= 0 && colour < fe->ncolours); |
402 | [fe->colours[colour] set]; |
403 | |
404 | [path moveToPoint:p1]; |
405 | [path lineToPoint:p2]; |
406 | [path stroke]; |
407 | } |
408 | void draw_rect(frontend *fe, int x, int y, int w, int h, int colour) |
409 | { |
410 | NSRect r = { {x,y}, {w,h} }; |
411 | |
412 | [[NSGraphicsContext currentContext] setShouldAntialias:NO]; |
413 | |
414 | assert(colour >= 0 && colour < fe->ncolours); |
415 | [fe->colours[colour] set]; |
416 | |
417 | NSRectFill(r); |
418 | } |
419 | void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize, |
420 | int align, int colour, char *text) |
421 | { |
422 | NSString *string = [NSString stringWithCString:text]; |
423 | NSDictionary *attr; |
424 | NSFont *font; |
425 | NSSize size; |
426 | NSPoint point; |
427 | |
428 | [[NSGraphicsContext currentContext] setShouldAntialias:YES]; |
429 | |
430 | assert(colour >= 0 && colour < fe->ncolours); |
431 | |
432 | if (fonttype == FONT_FIXED) |
433 | font = [NSFont userFixedPitchFontOfSize:fontsize]; |
434 | else |
435 | font = [NSFont userFontOfSize:fontsize]; |
436 | |
437 | attr = [NSDictionary dictionaryWithObjectsAndKeys: |
438 | fe->colours[colour], NSForegroundColorAttributeName, |
439 | font, NSFontAttributeName, nil]; |
440 | |
441 | point.x = x; |
442 | point.y = y; |
443 | |
444 | size = [string sizeWithAttributes:attr]; |
445 | if (align & ALIGN_HRIGHT) |
446 | point.x -= size.width; |
447 | else if (align & ALIGN_HCENTRE) |
448 | point.x -= size.width / 2; |
449 | if (align & ALIGN_VCENTRE) |
450 | point.y -= size.height / 2; |
451 | |
452 | [string drawAtPoint:point withAttributes:attr]; |
453 | } |
454 | void draw_update(frontend *fe, int x, int y, int w, int h) |
455 | { |
456 | /* FIXME */ |
457 | } |
458 | void clip(frontend *fe, int x, int y, int w, int h) |
459 | { |
460 | NSRect r = { {x,y}, {w,h} }; |
461 | |
462 | if (!fe->clipped) |
463 | [[NSGraphicsContext currentContext] saveGraphicsState]; |
464 | [NSBezierPath clipRect:r]; |
465 | fe->clipped = TRUE; |
466 | } |
467 | void unclip(frontend *fe) |
468 | { |
469 | if (fe->clipped) |
470 | [[NSGraphicsContext currentContext] restoreGraphicsState]; |
471 | fe->clipped = FALSE; |
472 | } |
473 | void start_draw(frontend *fe) |
474 | { |
475 | [fe->image lockFocus]; |
476 | fe->clipped = FALSE; |
477 | } |
478 | void end_draw(frontend *fe) |
479 | { |
480 | [fe->image unlockFocus]; |
481 | [fe->view setNeedsDisplay]; |
482 | } |
483 | |
484 | void deactivate_timer(frontend *fe) |
485 | { |
486 | [fe->window deactivateTimer]; |
487 | } |
488 | void activate_timer(frontend *fe) |
489 | { |
490 | [fe->window activateTimer]; |
491 | } |
492 | |
493 | /* ---------------------------------------------------------------------- |
494 | * Utility routines for constructing OS X menus. |
495 | ~|~ */ |
496 | |
497 | NSMenu *newmenu(const char *title) |
498 | { |
499 | return [[[NSMenu allocWithZone:[NSMenu menuZone]] |
500 | initWithTitle:[NSString stringWithCString:title]] |
501 | autorelease]; |
502 | } |
503 | |
504 | NSMenu *newsubmenu(NSMenu *parent, const char *title) |
505 | { |
506 | NSMenuItem *item; |
507 | NSMenu *child; |
508 | |
509 | item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] |
510 | initWithTitle:[NSString stringWithCString:title] |
511 | action:NULL |
512 | keyEquivalent:@""] |
513 | autorelease]; |
514 | child = newmenu(title); |
515 | [item setEnabled:YES]; |
516 | [item setSubmenu:child]; |
517 | [parent addItem:item]; |
518 | return child; |
519 | } |
520 | |
521 | id initnewitem(NSMenuItem *item, NSMenu *parent, const char *title, |
522 | const char *key, id target, SEL action) |
523 | { |
524 | unsigned mask = NSCommandKeyMask; |
525 | |
526 | if (key[strcspn(key, "-")]) { |
527 | while (*key && *key != '-') { |
528 | int c = tolower((unsigned char)*key); |
529 | if (c == 's') { |
530 | mask |= NSShiftKeyMask; |
531 | } else if (c == 'o' || c == 'a') { |
532 | mask |= NSAlternateKeyMask; |
533 | } |
534 | key++; |
535 | } |
536 | if (*key) |
537 | key++; |
538 | } |
539 | |
540 | item = [[item initWithTitle:[NSString stringWithCString:title] |
541 | action:NULL |
542 | keyEquivalent:[NSString stringWithCString:key]] |
543 | autorelease]; |
544 | |
545 | if (*key) |
546 | [item setKeyEquivalentModifierMask: mask]; |
547 | |
548 | [item setEnabled:YES]; |
549 | [item setTarget:target]; |
550 | [item setAction:action]; |
551 | |
552 | [parent addItem:item]; |
553 | |
554 | return item; |
555 | } |
556 | |
557 | NSMenuItem *newitem(NSMenu *parent, char *title, char *key, |
558 | id target, SEL action) |
559 | { |
560 | return initnewitem([NSMenuItem allocWithZone:[NSMenu menuZone]], |
561 | parent, title, key, target, action); |
562 | } |
563 | |
564 | /* ---------------------------------------------------------------------- |
565 | * Tiny extension to NSMenuItem which carries a payload of a `const |
566 | * game *', allowing our AppController to work out _which_ game |
567 | * needs to be launched when it receives a newGame message. |
568 | */ |
569 | @interface GameMenuItem : NSMenuItem |
570 | { |
571 | const game *ourgame; |
572 | } |
573 | - (void)setGame:(const game *)g; |
574 | - (const game *)getGame; |
575 | @end |
576 | @implementation GameMenuItem |
577 | - (void)setGame:(const game *)g |
578 | { |
579 | ourgame = g; |
580 | } |
581 | - (const game *)getGame |
582 | { |
583 | return ourgame; |
584 | } |
585 | @end |
586 | |
587 | /* ---------------------------------------------------------------------- |
588 | * AppController: the object which receives the messages from all |
589 | * menu selections that aren't standard OS X functions. |
590 | */ |
591 | @interface AppController : NSObject |
592 | { |
593 | } |
594 | - (IBAction)newGame:(id)sender; |
595 | @end |
596 | |
597 | @implementation AppController |
598 | |
599 | - (IBAction)newGame:(id)sender |
600 | { |
601 | const game *g = [sender getGame]; |
602 | id win; |
603 | |
604 | win = [[GameWindow alloc] initWithGame:g]; |
605 | [win makeKeyAndOrderFront:self]; |
606 | } |
607 | |
608 | @end |
609 | |
610 | /* ---------------------------------------------------------------------- |
611 | * Main program. Constructs the menus and runs the application. |
612 | */ |
613 | int main(int argc, char **argv) |
614 | { |
615 | NSAutoreleasePool *pool; |
616 | NSMenu *menu; |
617 | NSMenuItem *item; |
618 | AppController *controller; |
619 | |
620 | pool = [[NSAutoreleasePool alloc] init]; |
621 | [NSApplication sharedApplication]; |
622 | |
623 | controller = [[[AppController alloc] init] autorelease]; |
624 | |
625 | [NSApp setMainMenu: newmenu("Main Menu")]; |
626 | |
627 | menu = newsubmenu([NSApp mainMenu], "Apple Menu"); |
628 | [NSApp setServicesMenu:newsubmenu(menu, "Services")]; |
629 | [menu addItem:[NSMenuItem separatorItem]]; |
630 | item = newitem(menu, "Hide Puzzles", "h", NSApp, @selector(hide:)); |
631 | item = newitem(menu, "Hide Others", "o-h", NSApp, @selector(hideOtherApplications:)); |
632 | item = newitem(menu, "Show All", "", NSApp, @selector(unhideAllApplications:)); |
633 | [menu addItem:[NSMenuItem separatorItem]]; |
634 | item = newitem(menu, "Quit", "q", NSApp, @selector(terminate:)); |
635 | [NSApp setAppleMenu: menu]; |
636 | |
637 | menu = newsubmenu([NSApp mainMenu], "Game"); |
638 | { |
639 | int i; |
640 | |
641 | for (i = 0; i < gamecount; i++) { |
642 | id item = |
643 | initnewitem([GameMenuItem allocWithZone:[NSMenu menuZone]], |
644 | menu, gamelist[i]->name, "", controller, |
645 | @selector(newGame:)); |
646 | [item setGame:gamelist[i]]; |
647 | } |
648 | } |
649 | |
650 | menu = newsubmenu([NSApp mainMenu], "Windows"); |
651 | [NSApp setWindowsMenu: menu]; |
652 | |
653 | [NSApp run]; |
654 | [pool release]; |
655 | } |