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