2 * Mac OS X / Cocoa front end to puzzles.
6 * - status bar support.
8 * - preset selection. Should be reasonably simple: just a matter
9 * of dynamically frobbing the menu bar.
11 * - configurability. Will no doubt involve learning all about the
12 * dialog control side of Cocoa.
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
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
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.
29 * - proper fatal errors.
31 * - is there a better approach to frontend_default_colour?
33 * - some options in the Window menu! Close and Minimise, I think,
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.
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.
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).
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.
57 #import <Cocoa/Cocoa.h>
60 void fatal(char *fmt, ...)
62 /* FIXME: This will do for testing, but should be GUI-ish instead. */
65 fprintf(stderr, "fatal error: ");
68 vfprintf(stderr, fmt, ap);
71 fprintf(stderr, "\n");
75 void frontend_default_colour(frontend *fe, float *output)
78 output[0] = output[1] = output[2] = 0.8F;
80 void status_bar(frontend *fe, char *text)
85 void get_random_seed(void **randseed, int *randseedsize)
87 time_t *tp = snew(time_t);
89 *randseed = (void *)tp;
90 *randseedsize = sizeof(time_t);
93 /* ----------------------------------------------------------------------
94 * The front end presented to midend.c.
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.
114 @interface MyImageView : NSImageView
118 - (void)setWindow:(GameWindow *)win;
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;
132 @interface GameWindow : NSWindow
137 struct timeval last_time;
140 - (id)initWithGame:(const game *)g;
142 - (void)processButton:(int)b x:(int)x y:(int)y;
143 - (void)keyDown:(NSEvent *)ev;
144 - (void)activateTimer;
145 - (void)deactivateTimer;
148 @implementation MyImageView
150 - (void)setWindow:(GameWindow *)win
160 - (void)mouseEvent:(NSEvent *)ev button:(int)b
162 NSPoint point = [self convertPoint:[ev locationInWindow] fromView:nil];
163 [ourwin processButton:b x:point.x y:point.y];
166 - (void)mouseDown:(NSEvent *)ev
168 unsigned mod = [ev modifierFlags];
169 [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_BUTTON :
170 (mod & NSShiftKeyMask) ? MIDDLE_BUTTON :
173 - (void)mouseDragged:(NSEvent *)ev
175 unsigned mod = [ev modifierFlags];
176 [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_DRAG :
177 (mod & NSShiftKeyMask) ? MIDDLE_DRAG :
180 - (void)mouseUp:(NSEvent *)ev
182 unsigned mod = [ev modifierFlags];
183 [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_RELEASE :
184 (mod & NSShiftKeyMask) ? MIDDLE_RELEASE :
187 - (void)rightMouseDown:(NSEvent *)ev
189 unsigned mod = [ev modifierFlags];
190 [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_BUTTON :
193 - (void)rightMouseDragged:(NSEvent *)ev
195 unsigned mod = [ev modifierFlags];
196 [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_DRAG :
199 - (void)rightMouseUp:(NSEvent *)ev
201 unsigned mod = [ev modifierFlags];
202 [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_RELEASE :
205 - (void)otherMouseDown:(NSEvent *)ev
207 [self mouseEvent:ev button:MIDDLE_BUTTON];
209 - (void)otherMouseDragged:(NSEvent *)ev
211 [self mouseEvent:ev button:MIDDLE_DRAG];
213 - (void)otherMouseUp:(NSEvent *)ev
215 [self mouseEvent:ev button:MIDDLE_RELEASE];
219 @implementation GameWindow
220 - (id)initWithGame:(const game *)g
222 NSRect rect = { {0,0}, {0,0} };
229 me = midend_new(&fe, ourgame);
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.
237 midend_size(me, &w, &h);
239 rect.size.height = h;
241 self = [super initWithContentRect:rect
242 styleMask:(NSTitledWindowMask | NSMiniaturizableWindowMask |
243 NSClosableWindowMask)
244 backing:NSBackingStoreBuffered
246 [self setTitle:[NSString stringWithCString:ourgame->name]];
252 colours = midend_colours(me, &ncolours);
253 fe.ncolours = ncolours;
254 fe.colours = snewn(ncolours, NSColor *);
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]
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];
274 [self center]; /* :-) */
282 for (i = 0; i < fe.ncolours; i++) {
283 [fe.colours[i] release];
287 return [super dealloc];
290 - (void)processButton:(int)b x:(int)x y:(int)y
292 if (!midend_process_key(me, x, y, b))
296 - (void)keyDown:(NSEvent *)ev
298 NSString *s = [ev characters];
299 int i, n = [s length];
301 for (i = 0; i < n; i++) {
302 int c = [s characterAtIndex:i];
305 * ASCII gets passed straight to midend_process_key.
306 * Anything above that has to be translated to our own
307 * function key codes.
311 case NSUpArrowFunctionKey:
314 case NSDownArrowFunctionKey:
317 case NSLeftArrowFunctionKey:
320 case NSRightArrowFunctionKey:
328 [self processButton:c x:-1 y:-1];
332 - (void)activateTimer
337 timer = [NSTimer scheduledTimerWithTimeInterval:0.02
338 target:self selector:@selector(timerTick:)
339 userInfo:nil repeats:YES];
340 gettimeofday(&last_time, NULL);
343 - (void)deactivateTimer
352 - (void)timerTick:(id)sender
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);
366 * Drawing routines called by the midend.
368 void draw_polygon(frontend *fe, int *coords, int npoints,
369 int fill, int colour)
371 NSBezierPath *path = [NSBezierPath bezierPath];
374 [[NSGraphicsContext currentContext] setShouldAntialias:YES];
376 assert(colour >= 0 && colour < fe->ncolours);
377 [fe->colours[colour] set];
379 for (i = 0; i < npoints; i++) {
380 NSPoint p = { coords[i*2] + 0.5, coords[i*2+1] + 0.5 };
382 [path moveToPoint:p];
384 [path lineToPoint:p];
394 void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour)
396 NSBezierPath *path = [NSBezierPath bezierPath];
397 NSPoint p1 = { x1 + 0.5, y1 + 0.5 }, p2 = { x2 + 0.5, y2 + 0.5 };
399 [[NSGraphicsContext currentContext] setShouldAntialias:NO];
401 assert(colour >= 0 && colour < fe->ncolours);
402 [fe->colours[colour] set];
404 [path moveToPoint:p1];
405 [path lineToPoint:p2];
408 void draw_rect(frontend *fe, int x, int y, int w, int h, int colour)
410 NSRect r = { {x,y}, {w,h} };
412 [[NSGraphicsContext currentContext] setShouldAntialias:NO];
414 assert(colour >= 0 && colour < fe->ncolours);
415 [fe->colours[colour] set];
419 void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
420 int align, int colour, char *text)
422 NSString *string = [NSString stringWithCString:text];
428 [[NSGraphicsContext currentContext] setShouldAntialias:YES];
430 assert(colour >= 0 && colour < fe->ncolours);
432 if (fonttype == FONT_FIXED)
433 font = [NSFont userFixedPitchFontOfSize:fontsize];
435 font = [NSFont userFontOfSize:fontsize];
437 attr = [NSDictionary dictionaryWithObjectsAndKeys:
438 fe->colours[colour], NSForegroundColorAttributeName,
439 font, NSFontAttributeName, nil];
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;
452 [string drawAtPoint:point withAttributes:attr];
454 void draw_update(frontend *fe, int x, int y, int w, int h)
458 void clip(frontend *fe, int x, int y, int w, int h)
460 NSRect r = { {x,y}, {w,h} };
463 [[NSGraphicsContext currentContext] saveGraphicsState];
464 [NSBezierPath clipRect:r];
467 void unclip(frontend *fe)
470 [[NSGraphicsContext currentContext] restoreGraphicsState];
473 void start_draw(frontend *fe)
475 [fe->image lockFocus];
478 void end_draw(frontend *fe)
480 [fe->image unlockFocus];
481 [fe->view setNeedsDisplay];
484 void deactivate_timer(frontend *fe)
486 [fe->window deactivateTimer];
488 void activate_timer(frontend *fe)
490 [fe->window activateTimer];
493 /* ----------------------------------------------------------------------
494 * Utility routines for constructing OS X menus.
497 NSMenu *newmenu(const char *title)
499 return [[[NSMenu allocWithZone:[NSMenu menuZone]]
500 initWithTitle:[NSString stringWithCString:title]]
504 NSMenu *newsubmenu(NSMenu *parent, const char *title)
509 item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]]
510 initWithTitle:[NSString stringWithCString:title]
514 child = newmenu(title);
515 [item setEnabled:YES];
516 [item setSubmenu:child];
517 [parent addItem:item];
521 id initnewitem(NSMenuItem *item, NSMenu *parent, const char *title,
522 const char *key, id target, SEL action)
524 unsigned mask = NSCommandKeyMask;
526 if (key[strcspn(key, "-")]) {
527 while (*key && *key != '-') {
528 int c = tolower((unsigned char)*key);
530 mask |= NSShiftKeyMask;
531 } else if (c == 'o' || c == 'a') {
532 mask |= NSAlternateKeyMask;
540 item = [[item initWithTitle:[NSString stringWithCString:title]
542 keyEquivalent:[NSString stringWithCString:key]]
546 [item setKeyEquivalentModifierMask: mask];
548 [item setEnabled:YES];
549 [item setTarget:target];
550 [item setAction:action];
552 [parent addItem:item];
557 NSMenuItem *newitem(NSMenu *parent, char *title, char *key,
558 id target, SEL action)
560 return initnewitem([NSMenuItem allocWithZone:[NSMenu menuZone]],
561 parent, title, key, target, action);
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.
569 @interface GameMenuItem : NSMenuItem
573 - (void)setGame:(const game *)g;
574 - (const game *)getGame;
576 @implementation GameMenuItem
577 - (void)setGame:(const game *)g
581 - (const game *)getGame
587 /* ----------------------------------------------------------------------
588 * AppController: the object which receives the messages from all
589 * menu selections that aren't standard OS X functions.
591 @interface AppController : NSObject
594 - (IBAction)newGame:(id)sender;
597 @implementation AppController
599 - (IBAction)newGame:(id)sender
601 const game *g = [sender getGame];
604 win = [[GameWindow alloc] initWithGame:g];
605 [win makeKeyAndOrderFront:self];
610 /* ----------------------------------------------------------------------
611 * Main program. Constructs the menus and runs the application.
613 int main(int argc, char **argv)
615 NSAutoreleasePool *pool;
618 AppController *controller;
620 pool = [[NSAutoreleasePool alloc] init];
621 [NSApplication sharedApplication];
623 controller = [[[AppController alloc] init] autorelease];
625 [NSApp setMainMenu: newmenu("Main Menu")];
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];
637 menu = newsubmenu([NSApp mainMenu], "Game");
641 for (i = 0; i < gamecount; i++) {
643 initnewitem([GameMenuItem allocWithZone:[NSMenu menuZone]],
644 menu, gamelist[i]->name, "", controller,
645 @selector(newGame:));
646 [item setGame:gamelist[i]];
650 menu = newsubmenu([NSApp mainMenu], "Windows");
651 [NSApp setWindowsMenu: menu];