Update TODO.
[sgt/puzzles] / macosx.m
CommitLineData
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
60void 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
75void frontend_default_colour(frontend *fe, float *output)
76{
77 /* FIXME */
78 output[0] = output[1] = output[2] = 0.8F;
79}
80void status_bar(frontend *fe, char *text)
81{
82 /* FIXME */
83}
84
85void 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
105struct 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 */
368void 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}
394void 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}
408void 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}
419void 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}
454void draw_update(frontend *fe, int x, int y, int w, int h)
455{
456 /* FIXME */
457}
458void 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}
467void unclip(frontend *fe)
468{
469 if (fe->clipped)
470 [[NSGraphicsContext currentContext] restoreGraphicsState];
471 fe->clipped = FALSE;
472}
473void start_draw(frontend *fe)
474{
475 [fe->image lockFocus];
476 fe->clipped = FALSE;
477}
478void end_draw(frontend *fe)
479{
480 [fe->image unlockFocus];
481 [fe->view setNeedsDisplay];
482}
483
484void deactivate_timer(frontend *fe)
485{
486 [fe->window deactivateTimer];
487}
488void activate_timer(frontend *fe)
489{
490 [fe->window activateTimer];
491}
492
493/* ----------------------------------------------------------------------
494 * Utility routines for constructing OS X menus.
495~|~ */
496
497NSMenu *newmenu(const char *title)
498{
499 return [[[NSMenu allocWithZone:[NSMenu menuZone]]
500 initWithTitle:[NSString stringWithCString:title]]
501 autorelease];
502}
503
504NSMenu *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
521id 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
557NSMenuItem *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 */
613int 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}