From 0ae8782bc6a7ee69b3e7cf079f946e52546882bb Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 23 Jan 2005 11:20:31 +0000 Subject: Added a framework for putting things other than the binary into a Mac OS X application bundle, and provided an icon for Puzzles. Also renamed the OS X source file from macosx.m to osx.m, so that it can sit beside other things such as osx-info.plist and not cause enormously long filenames. [originally from svn r5179] --- Recipe | 2 +- macosx.m | 798 -------------------------------------------------------- mkfiles.pl | 39 ++- osx-info.plist | 8 + osx.icns | Bin 0 -> 48589 bytes osx.m | 800 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 840 insertions(+), 807 deletions(-) delete mode 100644 macosx.m create mode 100644 osx-info.plist create mode 100644 osx.icns create mode 100644 osx.m diff --git a/Recipe b/Recipe index 99c800b..36143a9 100644 --- a/Recipe +++ b/Recipe @@ -39,7 +39,7 @@ rect : [G] WINDOWS COMMON rect pattern : [G] WINDOWS COMMON pattern # Mac OS X unified application containing all the puzzles. -Puzzles : [MX] macosx COMMON ALL +Puzzles : [MX] osx osx.icns osx-info.plist COMMON ALL # The `nullgame' source file is a largely blank one, which contains # all the correct function definitions to compile and link, but diff --git a/macosx.m b/macosx.m deleted file mode 100644 index 52a70ef..0000000 --- a/macosx.m +++ /dev/null @@ -1,798 +0,0 @@ -/* - * Mac OS X / Cocoa front end to puzzles. - * - * TODO: - * - * - status bar support. - * - * - configurability. Will no doubt involve learning all about the - * dialog control side of Cocoa. - * - * - needs an icon. - * - * - not sure what I should be doing about default window - * placement. Centring new windows is a bit feeble, but what's - * better? Is there a standard way to tell the OS "here's the - * _size_ of window I want, now use your best judgment about the - * initial position"? - * - * - a brief frob of the Mac numeric keypad suggests that it - * generates numbers no matter what you do. I wonder if I should - * try to figure out a way of detecting keypad codes so I can - * implement UP_LEFT and friends. Alternatively, perhaps I - * should simply assign the number keys to UP_LEFT et al? - * They're not in use for anything else right now. - * - * - proper fatal errors. - * - * - is there a better approach to frontend_default_colour? - * - * - do we need any more options in the Window menu? - * - * - see if we can do anything to one-button-ise the multi-button - * dependent puzzle UIs: - * - Pattern is a _little_ unwieldy but not too bad (since - * generally you never need the middle button unless you've - * made a mistake, so it's just click versus command-click). - * - Net is utterly vile; having normal click be one rotate and - * command-click be the other introduces a horrid asymmetry, - * and yet requiring a shift key for _each_ click would be - * even worse because rotation feels as if it ought to be the - * default action. I fear this is why the Flash Net had the - * UI it did... - * - * - Find out how to do help, and do some. We have a help file; at - * _worst_ this should involve a new Halibut back end, but I - * think help is HTML round here anyway so perhaps we can work - * with what we already have. - * - * - Can we arrange for a pop-up menu from the Dock icon which - * launches specific games, perhaps? - * - * - Why are the right and bottom edges of the Pattern grid one - * pixel thinner than they should be? - * - * Grotty implementation details that could probably be improved: - * - * - I am _utterly_ unconvinced that NSImageView was the right way - * to go about having a window with a reliable backing store! It - * just doesn't feel right; NSImageView is a _control_. Is there - * a simpler way? - * - * - Resizing is currently very bad; rather than bother to work - * out how to resize the NSImageView, I just splatter and - * recreate it. - */ - -#include -#include -#import -#include "puzzles.h" - -void fatal(char *fmt, ...) -{ - /* FIXME: This will do for testing, but should be GUI-ish instead. */ - va_list ap; - - fprintf(stderr, "fatal error: "); - - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - - fprintf(stderr, "\n"); - exit(1); -} - -void frontend_default_colour(frontend *fe, float *output) -{ - /* FIXME */ - output[0] = output[1] = output[2] = 0.8F; -} -void status_bar(frontend *fe, char *text) -{ - /* FIXME */ -} - -void get_random_seed(void **randseed, int *randseedsize) -{ - time_t *tp = snew(time_t); - time(tp); - *randseed = (void *)tp; - *randseedsize = sizeof(time_t); -} - -/* ---------------------------------------------------------------------- - * Global variables. - */ - -/* - * The `Type' menu. We frob this dynamically to allow the user to - * choose a preset set of settings from the current game. - */ -NSMenu *typemenu; - -/* ---------------------------------------------------------------------- - * Tiny extension to NSMenuItem which carries a payload of a `void - * *', allowing several menu items to invoke the same message but - * pass different data through it. - */ -@interface DataMenuItem : NSMenuItem -{ - void *payload; -} -- (void)setPayload:(void *)d; -- (void *)getPayload; -@end -@implementation DataMenuItem -- (void)setPayload:(void *)d -{ - payload = d; -} -- (void *)getPayload -{ - return payload; -} -@end - -/* ---------------------------------------------------------------------- - * The front end presented to midend.c. - * - * This is mostly a subclass of NSWindow. The actual `frontend' - * structure passed to the midend contains a variety of pointers, - * including that window object but also including the image we - * draw on, an ImageView to display it in the window, and so on. - */ - -@class GameWindow; -@class MyImageView; - -struct frontend { - GameWindow *window; - NSImage *image; - MyImageView *view; - NSColor **colours; - int ncolours; - int clipped; -}; - -@interface MyImageView : NSImageView -{ - GameWindow *ourwin; -} -- (void)setWindow:(GameWindow *)win; -- (BOOL)isFlipped; -- (void)mouseEvent:(NSEvent *)ev button:(int)b; -- (void)mouseDown:(NSEvent *)ev; -- (void)mouseDragged:(NSEvent *)ev; -- (void)mouseUp:(NSEvent *)ev; -- (void)rightMouseDown:(NSEvent *)ev; -- (void)rightMouseDragged:(NSEvent *)ev; -- (void)rightMouseUp:(NSEvent *)ev; -- (void)otherMouseDown:(NSEvent *)ev; -- (void)otherMouseDragged:(NSEvent *)ev; -- (void)otherMouseUp:(NSEvent *)ev; -@end - -@interface GameWindow : NSWindow -{ - const game *ourgame; - midend_data *me; - struct frontend fe; - struct timeval last_time; - NSTimer *timer; -} -- (id)initWithGame:(const game *)g; -- dealloc; -- (void)processButton:(int)b x:(int)x y:(int)y; -- (void)keyDown:(NSEvent *)ev; -- (void)activateTimer; -- (void)deactivateTimer; -@end - -@implementation MyImageView - -- (void)setWindow:(GameWindow *)win -{ - ourwin = win; -} - -- (BOOL)isFlipped -{ - return YES; -} - -- (void)mouseEvent:(NSEvent *)ev button:(int)b -{ - NSPoint point = [self convertPoint:[ev locationInWindow] fromView:nil]; - [ourwin processButton:b x:point.x y:point.y]; -} - -- (void)mouseDown:(NSEvent *)ev -{ - unsigned mod = [ev modifierFlags]; - [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_BUTTON : - (mod & NSShiftKeyMask) ? MIDDLE_BUTTON : - LEFT_BUTTON)]; -} -- (void)mouseDragged:(NSEvent *)ev -{ - unsigned mod = [ev modifierFlags]; - [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_DRAG : - (mod & NSShiftKeyMask) ? MIDDLE_DRAG : - LEFT_DRAG)]; -} -- (void)mouseUp:(NSEvent *)ev -{ - unsigned mod = [ev modifierFlags]; - [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_RELEASE : - (mod & NSShiftKeyMask) ? MIDDLE_RELEASE : - LEFT_RELEASE)]; -} -- (void)rightMouseDown:(NSEvent *)ev -{ - unsigned mod = [ev modifierFlags]; - [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_BUTTON : - RIGHT_BUTTON)]; -} -- (void)rightMouseDragged:(NSEvent *)ev -{ - unsigned mod = [ev modifierFlags]; - [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_DRAG : - RIGHT_DRAG)]; -} -- (void)rightMouseUp:(NSEvent *)ev -{ - unsigned mod = [ev modifierFlags]; - [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_RELEASE : - RIGHT_RELEASE)]; -} -- (void)otherMouseDown:(NSEvent *)ev -{ - [self mouseEvent:ev button:MIDDLE_BUTTON]; -} -- (void)otherMouseDragged:(NSEvent *)ev -{ - [self mouseEvent:ev button:MIDDLE_DRAG]; -} -- (void)otherMouseUp:(NSEvent *)ev -{ - [self mouseEvent:ev button:MIDDLE_RELEASE]; -} -@end - -@implementation GameWindow -- (void)setupContentView -{ - NSSize size = {0,0}; - int w, h; - - midend_size(me, &w, &h); - size.width = w; - size.height = h; - - fe.image = [[NSImage alloc] initWithSize:size]; - [fe.image setFlipped:YES]; - fe.view = [[MyImageView alloc] - initWithFrame:[self contentRectForFrameRect:[self frame]]]; - [fe.view setImage:fe.image]; - [fe.view setWindow:self]; - - midend_redraw(me); - - [self setContentView:fe.view]; -} -- (id)initWithGame:(const game *)g -{ - NSRect rect = { {0,0}, {0,0} }; - int w, h; - - ourgame = g; - - fe.window = self; - - me = midend_new(&fe, ourgame); - /* - * If we ever need to open a fresh window using a provided game - * ID, I think the right thing is to move most of this method - * into a new initWithGame:gameID: method, and have - * initWithGame: simply call that one and pass it NULL. - */ - midend_new_game(me); - midend_size(me, &w, &h); - rect.size.width = w; - rect.size.height = h; - - self = [super initWithContentRect:rect - styleMask:(NSTitledWindowMask | NSMiniaturizableWindowMask | - NSClosableWindowMask) - backing:NSBackingStoreBuffered - defer:true]; - [self setTitle:[NSString stringWithCString:ourgame->name]]; - - { - float *colours; - int i, ncolours; - - colours = midend_colours(me, &ncolours); - fe.ncolours = ncolours; - fe.colours = snewn(ncolours, NSColor *); - - for (i = 0; i < ncolours; i++) { - fe.colours[i] = [[NSColor colorWithDeviceRed:colours[i*3] - green:colours[i*3+1] blue:colours[i*3+2] - alpha:1.0] retain]; - } - } - - [self setupContentView]; - [self setIgnoresMouseEvents:NO]; - - [self center]; /* :-) */ - - return self; -} - -- dealloc -{ - int i; - for (i = 0; i < fe.ncolours; i++) { - [fe.colours[i] release]; - } - sfree(fe.colours); - midend_free(me); - return [super dealloc]; -} - -- (void)processButton:(int)b x:(int)x y:(int)y -{ - if (!midend_process_key(me, x, y, b)) - [self close]; -} - -- (void)keyDown:(NSEvent *)ev -{ - NSString *s = [ev characters]; - int i, n = [s length]; - - for (i = 0; i < n; i++) { - int c = [s characterAtIndex:i]; - - /* - * ASCII gets passed straight to midend_process_key. - * Anything above that has to be translated to our own - * function key codes. - */ - if (c >= 0x80) { - switch (c) { - case NSUpArrowFunctionKey: - c = CURSOR_UP; - break; - case NSDownArrowFunctionKey: - c = CURSOR_DOWN; - break; - case NSLeftArrowFunctionKey: - c = CURSOR_LEFT; - break; - case NSRightArrowFunctionKey: - c = CURSOR_RIGHT; - break; - default: - continue; - } - } - - [self processButton:c x:-1 y:-1]; - } -} - -- (void)activateTimer -{ - if (timer != nil) - return; - - timer = [NSTimer scheduledTimerWithTimeInterval:0.02 - target:self selector:@selector(timerTick:) - userInfo:nil repeats:YES]; - gettimeofday(&last_time, NULL); -} - -- (void)deactivateTimer -{ - if (timer == nil) - return; - - [timer invalidate]; - timer = nil; -} - -- (void)timerTick:(id)sender -{ - struct timeval now; - float elapsed; - gettimeofday(&now, NULL); - elapsed = ((now.tv_usec - last_time.tv_usec) * 0.000001F + - (now.tv_sec - last_time.tv_sec)); - midend_timer(me, elapsed); - last_time = now; -} - -- (void)newGame:(id)sender -{ - [self processButton:'n' x:-1 y:-1]; -} -- (void)restartGame:(id)sender -{ - [self processButton:'r' x:-1 y:-1]; -} -- (void)undoMove:(id)sender -{ - [self processButton:'u' x:-1 y:-1]; -} -- (void)redoMove:(id)sender -{ - [self processButton:'r'&0x1F x:-1 y:-1]; -} - -- (void)clearTypeMenu -{ - while ([typemenu numberOfItems] > 1) - [typemenu removeItemAtIndex:0]; -} - -- (void)becomeKeyWindow -{ - int n; - - [self clearTypeMenu]; - - [super becomeKeyWindow]; - - n = midend_num_presets(me); - - if (n > 0) { - [typemenu insertItem:[NSMenuItem separatorItem] atIndex:0]; - while (n--) { - char *name; - game_params *params; - DataMenuItem *item; - - midend_fetch_preset(me, n, &name, ¶ms); - - item = [[[DataMenuItem alloc] - initWithTitle:[NSString stringWithCString:name] - action:NULL keyEquivalent:@""] - autorelease]; - - [item setEnabled:YES]; - [item setTarget:self]; - [item setAction:@selector(presetGame:)]; - [item setPayload:params]; - - [typemenu insertItem:item atIndex:0]; - } - } -} - -- (void)resignKeyWindow -{ - [self clearTypeMenu]; - [super resignKeyWindow]; -} - -- (void)close -{ - [self clearTypeMenu]; - [super close]; -} - -- (void)resizeForNewGameParams -{ - NSSize size = {0,0}; - int w, h; - - midend_size(me, &w, &h); - size.width = w; - size.height = h; - - NSDisableScreenUpdates(); - [self setContentSize:size]; - [self setupContentView]; - NSEnableScreenUpdates(); -} - -- (void)presetGame:(id)sender -{ - game_params *params = [sender getPayload]; - - midend_set_params(me, params); - midend_new_game(me); - - [self resizeForNewGameParams]; -} - -@end - -/* - * Drawing routines called by the midend. - */ -void draw_polygon(frontend *fe, int *coords, int npoints, - int fill, int colour) -{ - NSBezierPath *path = [NSBezierPath bezierPath]; - int i; - - [[NSGraphicsContext currentContext] setShouldAntialias:YES]; - - assert(colour >= 0 && colour < fe->ncolours); - [fe->colours[colour] set]; - - for (i = 0; i < npoints; i++) { - NSPoint p = { coords[i*2] + 0.5, coords[i*2+1] + 0.5 }; - if (i == 0) - [path moveToPoint:p]; - else - [path lineToPoint:p]; - } - - [path closePath]; - - if (fill) - [path fill]; - else - [path stroke]; -} -void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour) -{ - NSBezierPath *path = [NSBezierPath bezierPath]; - NSPoint p1 = { x1 + 0.5, y1 + 0.5 }, p2 = { x2 + 0.5, y2 + 0.5 }; - - [[NSGraphicsContext currentContext] setShouldAntialias:NO]; - - assert(colour >= 0 && colour < fe->ncolours); - [fe->colours[colour] set]; - - [path moveToPoint:p1]; - [path lineToPoint:p2]; - [path stroke]; -} -void draw_rect(frontend *fe, int x, int y, int w, int h, int colour) -{ - NSRect r = { {x,y}, {w,h} }; - - [[NSGraphicsContext currentContext] setShouldAntialias:NO]; - - assert(colour >= 0 && colour < fe->ncolours); - [fe->colours[colour] set]; - - NSRectFill(r); -} -void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize, - int align, int colour, char *text) -{ - NSString *string = [NSString stringWithCString:text]; - NSDictionary *attr; - NSFont *font; - NSSize size; - NSPoint point; - - [[NSGraphicsContext currentContext] setShouldAntialias:YES]; - - assert(colour >= 0 && colour < fe->ncolours); - - if (fonttype == FONT_FIXED) - font = [NSFont userFixedPitchFontOfSize:fontsize]; - else - font = [NSFont userFontOfSize:fontsize]; - - attr = [NSDictionary dictionaryWithObjectsAndKeys: - fe->colours[colour], NSForegroundColorAttributeName, - font, NSFontAttributeName, nil]; - - point.x = x; - point.y = y; - - size = [string sizeWithAttributes:attr]; - if (align & ALIGN_HRIGHT) - point.x -= size.width; - else if (align & ALIGN_HCENTRE) - point.x -= size.width / 2; - if (align & ALIGN_VCENTRE) - point.y -= size.height / 2; - - [string drawAtPoint:point withAttributes:attr]; -} -void draw_update(frontend *fe, int x, int y, int w, int h) -{ - /* FIXME */ -} -void clip(frontend *fe, int x, int y, int w, int h) -{ - NSRect r = { {x,y}, {w,h} }; - - if (!fe->clipped) - [[NSGraphicsContext currentContext] saveGraphicsState]; - [NSBezierPath clipRect:r]; - fe->clipped = TRUE; -} -void unclip(frontend *fe) -{ - if (fe->clipped) - [[NSGraphicsContext currentContext] restoreGraphicsState]; - fe->clipped = FALSE; -} -void start_draw(frontend *fe) -{ - [fe->image lockFocus]; - fe->clipped = FALSE; -} -void end_draw(frontend *fe) -{ - [fe->image unlockFocus]; - [fe->view setNeedsDisplay]; -} - -void deactivate_timer(frontend *fe) -{ - [fe->window deactivateTimer]; -} -void activate_timer(frontend *fe) -{ - [fe->window activateTimer]; -} - -/* ---------------------------------------------------------------------- - * Utility routines for constructing OS X menus. - */ - -NSMenu *newmenu(const char *title) -{ - return [[[NSMenu allocWithZone:[NSMenu menuZone]] - initWithTitle:[NSString stringWithCString:title]] - autorelease]; -} - -NSMenu *newsubmenu(NSMenu *parent, const char *title) -{ - NSMenuItem *item; - NSMenu *child; - - item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] - initWithTitle:[NSString stringWithCString:title] - action:NULL - keyEquivalent:@""] - autorelease]; - child = newmenu(title); - [item setEnabled:YES]; - [item setSubmenu:child]; - [parent addItem:item]; - return child; -} - -id initnewitem(NSMenuItem *item, NSMenu *parent, const char *title, - const char *key, id target, SEL action) -{ - unsigned mask = NSCommandKeyMask; - - if (key[strcspn(key, "-")]) { - while (*key && *key != '-') { - int c = tolower((unsigned char)*key); - if (c == 's') { - mask |= NSShiftKeyMask; - } else if (c == 'o' || c == 'a') { - mask |= NSAlternateKeyMask; - } - key++; - } - if (*key) - key++; - } - - item = [[item initWithTitle:[NSString stringWithCString:title] - action:NULL - keyEquivalent:[NSString stringWithCString:key]] - autorelease]; - - if (*key) - [item setKeyEquivalentModifierMask: mask]; - - [item setEnabled:YES]; - [item setTarget:target]; - [item setAction:action]; - - [parent addItem:item]; - - return item; -} - -NSMenuItem *newitem(NSMenu *parent, char *title, char *key, - id target, SEL action) -{ - return initnewitem([NSMenuItem allocWithZone:[NSMenu menuZone]], - parent, title, key, target, action); -} - -/* ---------------------------------------------------------------------- - * AppController: the object which receives the messages from all - * menu selections that aren't standard OS X functions. - */ -@interface AppController : NSObject -{ -} -- (IBAction)newGame:(id)sender; -@end - -@implementation AppController - -- (IBAction)newGame:(id)sender -{ - const game *g = [sender getPayload]; - id win; - - win = [[GameWindow alloc] initWithGame:g]; - [win makeKeyAndOrderFront:self]; -} - -@end - -/* ---------------------------------------------------------------------- - * Main program. Constructs the menus and runs the application. - */ -int main(int argc, char **argv) -{ - NSAutoreleasePool *pool; - NSMenu *menu; - NSMenuItem *item; - AppController *controller; - - pool = [[NSAutoreleasePool alloc] init]; - [NSApplication sharedApplication]; - - controller = [[[AppController alloc] init] autorelease]; - - [NSApp setMainMenu: newmenu("Main Menu")]; - - menu = newsubmenu([NSApp mainMenu], "Apple Menu"); - [NSApp setServicesMenu:newsubmenu(menu, "Services")]; - [menu addItem:[NSMenuItem separatorItem]]; - item = newitem(menu, "Hide Puzzles", "h", NSApp, @selector(hide:)); - item = newitem(menu, "Hide Others", "o-h", NSApp, @selector(hideOtherApplications:)); - item = newitem(menu, "Show All", "", NSApp, @selector(unhideAllApplications:)); - [menu addItem:[NSMenuItem separatorItem]]; - item = newitem(menu, "Quit", "q", NSApp, @selector(terminate:)); - [NSApp setAppleMenu: menu]; - - menu = newsubmenu([NSApp mainMenu], "Open"); - { - int i; - - for (i = 0; i < gamecount; i++) { - id item = - initnewitem([DataMenuItem allocWithZone:[NSMenu menuZone]], - menu, gamelist[i]->name, "", controller, - @selector(newGame:)); - [item setPayload:(void *)gamelist[i]]; - } - } - - menu = newsubmenu([NSApp mainMenu], "Game"); - item = newitem(menu, "New", "n", NULL, @selector(newGame:)); - item = newitem(menu, "Restart", "r", NULL, @selector(restartGame:)); - item = newitem(menu, "Specific", "", NULL, @selector(specificGame:)); - [menu addItem:[NSMenuItem separatorItem]]; - item = newitem(menu, "Undo", "z", NULL, @selector(undoMove:)); - item = newitem(menu, "Redo", "S-z", NULL, @selector(redoMove:)); - [menu addItem:[NSMenuItem separatorItem]]; - item = newitem(menu, "Close", "w", NULL, @selector(performClose:)); - - menu = newsubmenu([NSApp mainMenu], "Type"); - typemenu = menu; - item = newitem(menu, "Custom", "", NULL, @selector(customGameType:)); - - menu = newsubmenu([NSApp mainMenu], "Window"); - [NSApp setWindowsMenu: menu]; - item = newitem(menu, "Minimise Window", "m", NULL, @selector(performMiniaturize:)); - - [NSApp run]; - [pool release]; -} diff --git a/mkfiles.pl b/mkfiles.pl index 382a40b..8af1939 100755 --- a/mkfiles.pl +++ b/mkfiles.pl @@ -135,9 +135,7 @@ foreach $i (@prognames) { $file = "$1.r"; $depends{$j} = [$file]; push @scanlist, $file; - } elsif ($j =~ /\.lib$/) { - # libraries don't have dependencies - } else { + } elsif ($j !~ /\./) { $file = "$j.c"; $file = "$j.m" unless &findfile($file); $depends{$j} = [$file]; @@ -259,7 +257,7 @@ sub objects { } elsif ($i =~ /^(.*)\.lib/) { $y = $1; ($x = $ltmpl) =~ s/X/$y/; - } else { + } elsif ($i !~ /\./) { ($x = $otmpl) =~ s/X/$i/; } push @ret, $x if $x ne ""; @@ -267,6 +265,19 @@ sub objects { return join " ", @ret; } +sub special { + my ($prog, $suffix) = @_; + my @ret; + my ($i, $x, $y); + @ret = (); + foreach $i (@{$programs{$prog}}) { + if (substr($i, (length $i) - (length $suffix)) eq $suffix) { + push @ret, $i; + } + } + return join " ", @ret; +} + sub splitline { my ($line, $width, $splitchar) = @_; my ($result, $len); @@ -1133,10 +1144,22 @@ if (defined $makefiles{'osx'}) { foreach $p (&prognames("MX")) { ($prog, $type) = split ",", $p; $objstr = &objects($p, "X.o", undef, undef); - print "${prog}: ${prog}.app/Contents/MacOS/$prog\n\n"; - print "${prog}.app:\n\tmkdir \$\@\n"; - print "${prog}.app/Contents: ${prog}.app\n\tmkdir \$\@\n"; - print "${prog}.app/Contents/MacOS: ${prog}.app/Contents\n\tmkdir \$\@\n"; + $icon = &special($p, ".icns"); + $infoplist = &special($p, "info.plist"); + print "${prog}.app:\n\tmkdir -p \$\@\n"; + print "${prog}.app/Contents: ${prog}.app\n\tmkdir -p \$\@\n"; + print "${prog}.app/Contents/MacOS: ${prog}.app/Contents\n\tmkdir -p \$\@\n"; + $targets = "${prog}.app/Contents/MacOS/$prog"; + if (defined $icon) { + print "${prog}.app/Contents/Resources: ${prog}.app/Contents\n\tmkdir -p \$\@\n"; + print "${prog}.app/Contents/Resources/${prog}.icns: ${prog}.app/Contents/Resources $icon\n\tcp $icon \$\@\n"; + $targets .= " ${prog}.app/Contents/Resources/${prog}.icns"; + } + if (defined $infoplist) { + print "${prog}.app/Contents/Info.plist: ${prog}.app/Contents/Resources $infoplist\n\tcp $infoplist \$\@\n"; + $targets .= " ${prog}.app/Contents/Info.plist"; + } + print &splitline("${prog}: $targets", 69) . "\n\n"; print &splitline("${prog}.app/Contents/MacOS/$prog: ". "${prog}.app/Contents/MacOS " . $objstr), "\n"; $libstr = &objects($p, undef, undef, "-lX"); diff --git a/osx-info.plist b/osx-info.plist new file mode 100644 index 0000000..54e8af3 --- /dev/null +++ b/osx-info.plist @@ -0,0 +1,8 @@ + + + + + CFBundleIconFile + Puzzles.icns + + diff --git a/osx.icns b/osx.icns new file mode 100644 index 0000000..b4346a0 Binary files /dev/null and b/osx.icns differ diff --git a/osx.m b/osx.m new file mode 100644 index 0000000..2fad086 --- /dev/null +++ b/osx.m @@ -0,0 +1,800 @@ +/* + * Mac OS X / Cocoa front end to puzzles. + * + * TODO: + * + * - status bar support. + * + * - configurability. Will no doubt involve learning all about the + * dialog control side of Cocoa. + * + * - not sure what I should be doing about default window + * placement. Centring new windows is a bit feeble, but what's + * better? Is there a standard way to tell the OS "here's the + * _size_ of window I want, now use your best judgment about the + * initial position"? + * + * - a brief frob of the Mac numeric keypad suggests that it + * generates numbers no matter what you do. I wonder if I should + * try to figure out a way of detecting keypad codes so I can + * implement UP_LEFT and friends. Alternatively, perhaps I + * should simply assign the number keys to UP_LEFT et al? + * They're not in use for anything else right now. + * + * - proper fatal errors. + * + * - is there a better approach to frontend_default_colour? + * + * - do we need any more options in the Window menu? + * + * - see if we can do anything to one-button-ise the multi-button + * dependent puzzle UIs: + * - Pattern is a _little_ unwieldy but not too bad (since + * generally you never need the middle button unless you've + * made a mistake, so it's just click versus command-click). + * - Net is utterly vile; having normal click be one rotate and + * command-click be the other introduces a horrid asymmetry, + * and yet requiring a shift key for _each_ click would be + * even worse because rotation feels as if it ought to be the + * default action. I fear this is why the Flash Net had the + * UI it did... + * + * - Find out how to do help, and do some. We have a help file; at + * _worst_ this should involve a new Halibut back end, but I + * think help is HTML round here anyway so perhaps we can work + * with what we already have. + * + * - Can we arrange for a pop-up menu from the Dock icon which + * launches specific games, perhaps? + * + * - Why are the right and bottom edges of the Pattern grid one + * pixel thinner than they should be? + * + * Grotty implementation details that could probably be improved: + * + * - I am _utterly_ unconvinced that NSImageView was the right way + * to go about having a window with a reliable backing store! It + * just doesn't feel right; NSImageView is a _control_. Is there + * a simpler way? + * + * - Resizing is currently very bad; rather than bother to work + * out how to resize the NSImageView, I just splatter and + * recreate it. + */ + +#include +#include +#import +#include "puzzles.h" + +void fatal(char *fmt, ...) +{ + /* FIXME: This will do for testing, but should be GUI-ish instead. */ + va_list ap; + + fprintf(stderr, "fatal error: "); + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + fprintf(stderr, "\n"); + exit(1); +} + +void frontend_default_colour(frontend *fe, float *output) +{ + /* FIXME */ + output[0] = output[1] = output[2] = 0.8F; +} +void status_bar(frontend *fe, char *text) +{ + /* FIXME */ +} + +void get_random_seed(void **randseed, int *randseedsize) +{ + time_t *tp = snew(time_t); + time(tp); + *randseed = (void *)tp; + *randseedsize = sizeof(time_t); +} + +/* ---------------------------------------------------------------------- + * Global variables. + */ + +/* + * The `Type' menu. We frob this dynamically to allow the user to + * choose a preset set of settings from the current game. + */ +NSMenu *typemenu; + +/* ---------------------------------------------------------------------- + * Tiny extension to NSMenuItem which carries a payload of a `void + * *', allowing several menu items to invoke the same message but + * pass different data through it. + */ +@interface DataMenuItem : NSMenuItem +{ + void *payload; +} +- (void)setPayload:(void *)d; +- (void *)getPayload; +@end +@implementation DataMenuItem +- (void)setPayload:(void *)d +{ + payload = d; +} +- (void *)getPayload +{ + return payload; +} +@end + +/* ---------------------------------------------------------------------- + * The front end presented to midend.c. + * + * This is mostly a subclass of NSWindow. The actual `frontend' + * structure passed to the midend contains a variety of pointers, + * including that window object but also including the image we + * draw on, an ImageView to display it in the window, and so on. + */ + +@class GameWindow; +@class MyImageView; + +struct frontend { + GameWindow *window; + NSImage *image; + MyImageView *view; + NSColor **colours; + int ncolours; + int clipped; +}; + +@interface MyImageView : NSImageView +{ + GameWindow *ourwin; +} +- (void)setWindow:(GameWindow *)win; +- (BOOL)isFlipped; +- (void)mouseEvent:(NSEvent *)ev button:(int)b; +- (void)mouseDown:(NSEvent *)ev; +- (void)mouseDragged:(NSEvent *)ev; +- (void)mouseUp:(NSEvent *)ev; +- (void)rightMouseDown:(NSEvent *)ev; +- (void)rightMouseDragged:(NSEvent *)ev; +- (void)rightMouseUp:(NSEvent *)ev; +- (void)otherMouseDown:(NSEvent *)ev; +- (void)otherMouseDragged:(NSEvent *)ev; +- (void)otherMouseUp:(NSEvent *)ev; +@end + +@interface GameWindow : NSWindow +{ + const game *ourgame; + midend_data *me; + struct frontend fe; + struct timeval last_time; + NSTimer *timer; +} +- (id)initWithGame:(const game *)g; +- dealloc; +- (void)processButton:(int)b x:(int)x y:(int)y; +- (void)keyDown:(NSEvent *)ev; +- (void)activateTimer; +- (void)deactivateTimer; +@end + +@implementation MyImageView + +- (void)setWindow:(GameWindow *)win +{ + ourwin = win; +} + +- (BOOL)isFlipped +{ + return YES; +} + +- (void)mouseEvent:(NSEvent *)ev button:(int)b +{ + NSPoint point = [self convertPoint:[ev locationInWindow] fromView:nil]; + [ourwin processButton:b x:point.x y:point.y]; +} + +- (void)mouseDown:(NSEvent *)ev +{ + unsigned mod = [ev modifierFlags]; + [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_BUTTON : + (mod & NSShiftKeyMask) ? MIDDLE_BUTTON : + LEFT_BUTTON)]; +} +- (void)mouseDragged:(NSEvent *)ev +{ + unsigned mod = [ev modifierFlags]; + [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_DRAG : + (mod & NSShiftKeyMask) ? MIDDLE_DRAG : + LEFT_DRAG)]; +} +- (void)mouseUp:(NSEvent *)ev +{ + unsigned mod = [ev modifierFlags]; + [self mouseEvent:ev button:((mod & NSCommandKeyMask) ? RIGHT_RELEASE : + (mod & NSShiftKeyMask) ? MIDDLE_RELEASE : + LEFT_RELEASE)]; +} +- (void)rightMouseDown:(NSEvent *)ev +{ + unsigned mod = [ev modifierFlags]; + [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_BUTTON : + RIGHT_BUTTON)]; +} +- (void)rightMouseDragged:(NSEvent *)ev +{ + unsigned mod = [ev modifierFlags]; + [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_DRAG : + RIGHT_DRAG)]; +} +- (void)rightMouseUp:(NSEvent *)ev +{ + unsigned mod = [ev modifierFlags]; + [self mouseEvent:ev button:((mod & NSShiftKeyMask) ? MIDDLE_RELEASE : + RIGHT_RELEASE)]; +} +- (void)otherMouseDown:(NSEvent *)ev +{ + [self mouseEvent:ev button:MIDDLE_BUTTON]; +} +- (void)otherMouseDragged:(NSEvent *)ev +{ + [self mouseEvent:ev button:MIDDLE_DRAG]; +} +- (void)otherMouseUp:(NSEvent *)ev +{ + [self mouseEvent:ev button:MIDDLE_RELEASE]; +} +@end + +@implementation GameWindow +- (void)setupContentView +{ + NSSize size = {0,0}; + int w, h; + + midend_size(me, &w, &h); + size.width = w; + size.height = h; + + fe.image = [[NSImage alloc] initWithSize:size]; + [fe.image setFlipped:YES]; + fe.view = [[MyImageView alloc] + initWithFrame:[self contentRectForFrameRect:[self frame]]]; + [fe.view setImage:fe.image]; + [fe.view setWindow:self]; + + midend_redraw(me); + + [self setContentView:fe.view]; +} +- (id)initWithGame:(const game *)g +{ + NSRect rect = { {0,0}, {0,0} }; + int w, h; + + ourgame = g; + + fe.window = self; + + me = midend_new(&fe, ourgame); + /* + * If we ever need to open a fresh window using a provided game + * ID, I think the right thing is to move most of this method + * into a new initWithGame:gameID: method, and have + * initWithGame: simply call that one and pass it NULL. + */ + midend_new_game(me); + midend_size(me, &w, &h); + rect.size.width = w; + rect.size.height = h; + + self = [super initWithContentRect:rect + styleMask:(NSTitledWindowMask | NSMiniaturizableWindowMask | + NSClosableWindowMask) + backing:NSBackingStoreBuffered + defer:true]; + [self setTitle:[NSString stringWithCString:ourgame->name]]; + + { + float *colours; + int i, ncolours; + + colours = midend_colours(me, &ncolours); + fe.ncolours = ncolours; + fe.colours = snewn(ncolours, NSColor *); + + for (i = 0; i < ncolours; i++) { + fe.colours[i] = [[NSColor colorWithDeviceRed:colours[i*3] + green:colours[i*3+1] blue:colours[i*3+2] + alpha:1.0] retain]; + } + } + + [self setupContentView]; + [self setIgnoresMouseEvents:NO]; + + [self center]; /* :-) */ + + return self; +} + +- dealloc +{ + int i; + for (i = 0; i < fe.ncolours; i++) { + [fe.colours[i] release]; + } + sfree(fe.colours); + midend_free(me); + return [super dealloc]; +} + +- (void)processButton:(int)b x:(int)x y:(int)y +{ + if (!midend_process_key(me, x, y, b)) + [self close]; +} + +- (void)keyDown:(NSEvent *)ev +{ + NSString *s = [ev characters]; + int i, n = [s length]; + + for (i = 0; i < n; i++) { + int c = [s characterAtIndex:i]; + + /* + * ASCII gets passed straight to midend_process_key. + * Anything above that has to be translated to our own + * function key codes. + */ + if (c >= 0x80) { + switch (c) { + case NSUpArrowFunctionKey: + c = CURSOR_UP; + break; + case NSDownArrowFunctionKey: + c = CURSOR_DOWN; + break; + case NSLeftArrowFunctionKey: + c = CURSOR_LEFT; + break; + case NSRightArrowFunctionKey: + c = CURSOR_RIGHT; + break; + default: + continue; + } + } + + [self processButton:c x:-1 y:-1]; + } +} + +- (void)activateTimer +{ + if (timer != nil) + return; + + timer = [NSTimer scheduledTimerWithTimeInterval:0.02 + target:self selector:@selector(timerTick:) + userInfo:nil repeats:YES]; + gettimeofday(&last_time, NULL); +} + +- (void)deactivateTimer +{ + if (timer == nil) + return; + + [timer invalidate]; + timer = nil; +} + +- (void)timerTick:(id)sender +{ + struct timeval now; + float elapsed; + gettimeofday(&now, NULL); + elapsed = ((now.tv_usec - last_time.tv_usec) * 0.000001F + + (now.tv_sec - last_time.tv_sec)); + midend_timer(me, elapsed); + last_time = now; +} + +- (void)newGame:(id)sender +{ + [self processButton:'n' x:-1 y:-1]; +} +- (void)restartGame:(id)sender +{ + [self processButton:'r' x:-1 y:-1]; +} +- (void)undoMove:(id)sender +{ + [self processButton:'u' x:-1 y:-1]; +} +- (void)redoMove:(id)sender +{ + [self processButton:'r'&0x1F x:-1 y:-1]; +} + +- (void)clearTypeMenu +{ + while ([typemenu numberOfItems] > 1) + [typemenu removeItemAtIndex:0]; +} + +- (void)becomeKeyWindow +{ + int n; + + [self clearTypeMenu]; + + [super becomeKeyWindow]; + + n = midend_num_presets(me); + + if (n > 0) { + [typemenu insertItem:[NSMenuItem separatorItem] atIndex:0]; + while (n--) { + char *name; + game_params *params; + DataMenuItem *item; + + midend_fetch_preset(me, n, &name, ¶ms); + + item = [[[DataMenuItem alloc] + initWithTitle:[NSString stringWithCString:name] + action:NULL keyEquivalent:@""] + autorelease]; + + [item setEnabled:YES]; + [item setTarget:self]; + [item setAction:@selector(presetGame:)]; + [item setPayload:params]; + + [typemenu insertItem:item atIndex:0]; + } + } +} + +- (void)resignKeyWindow +{ + [self clearTypeMenu]; + [super resignKeyWindow]; +} + +- (void)close +{ + [self clearTypeMenu]; + [super close]; +} + +- (void)resizeForNewGameParams +{ + NSSize size = {0,0}; + int w, h; + + midend_size(me, &w, &h); + size.width = w; + size.height = h; + + NSDisableScreenUpdates(); + [self setContentSize:size]; + [self setupContentView]; + NSEnableScreenUpdates(); +} + +- (void)presetGame:(id)sender +{ + game_params *params = [sender getPayload]; + + midend_set_params(me, params); + midend_new_game(me); + + [self resizeForNewGameParams]; +} + +@end + +/* + * Drawing routines called by the midend. + */ +void draw_polygon(frontend *fe, int *coords, int npoints, + int fill, int colour) +{ + NSBezierPath *path = [NSBezierPath bezierPath]; + int i; + + [[NSGraphicsContext currentContext] setShouldAntialias:YES]; + + assert(colour >= 0 && colour < fe->ncolours); + [fe->colours[colour] set]; + + for (i = 0; i < npoints; i++) { + NSPoint p = { coords[i*2] + 0.5, coords[i*2+1] + 0.5 }; + if (i == 0) + [path moveToPoint:p]; + else + [path lineToPoint:p]; + } + + [path closePath]; + + if (fill) + [path fill]; + else + [path stroke]; +} +void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour) +{ + NSBezierPath *path = [NSBezierPath bezierPath]; + NSPoint p1 = { x1 + 0.5, y1 + 0.5 }, p2 = { x2 + 0.5, y2 + 0.5 }; + + [[NSGraphicsContext currentContext] setShouldAntialias:NO]; + + assert(colour >= 0 && colour < fe->ncolours); + [fe->colours[colour] set]; + + [path moveToPoint:p1]; + [path lineToPoint:p2]; + [path stroke]; +} +void draw_rect(frontend *fe, int x, int y, int w, int h, int colour) +{ + NSRect r = { {x,y}, {w,h} }; + + [[NSGraphicsContext currentContext] setShouldAntialias:NO]; + + assert(colour >= 0 && colour < fe->ncolours); + [fe->colours[colour] set]; + + NSRectFill(r); +} +void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize, + int align, int colour, char *text) +{ + NSString *string = [NSString stringWithCString:text]; + NSDictionary *attr; + NSFont *font; + NSSize size; + NSPoint point; + + [[NSGraphicsContext currentContext] setShouldAntialias:YES]; + + assert(colour >= 0 && colour < fe->ncolours); + + if (fonttype == FONT_FIXED) + font = [NSFont userFixedPitchFontOfSize:fontsize]; + else + font = [NSFont userFontOfSize:fontsize]; + + attr = [NSDictionary dictionaryWithObjectsAndKeys: + fe->colours[colour], NSForegroundColorAttributeName, + font, NSFontAttributeName, nil]; + + point.x = x; + point.y = y; + + size = [string sizeWithAttributes:attr]; + if (align & ALIGN_HRIGHT) + point.x -= size.width; + else if (align & ALIGN_HCENTRE) + point.x -= size.width / 2; + if (align & ALIGN_VCENTRE) + point.y -= size.height / 2; + + [string drawAtPoint:point withAttributes:attr]; +} +void draw_update(frontend *fe, int x, int y, int w, int h) +{ + /* FIXME */ +} +void clip(frontend *fe, int x, int y, int w, int h) +{ + NSRect r = { {x,y}, {w,h} }; + + if (!fe->clipped) + [[NSGraphicsContext currentContext] saveGraphicsState]; + [NSBezierPath clipRect:r]; + fe->clipped = TRUE; +} +void unclip(frontend *fe) +{ + if (fe->clipped) + [[NSGraphicsContext currentContext] restoreGraphicsState]; + fe->clipped = FALSE; +} +void start_draw(frontend *fe) +{ + [fe->image lockFocus]; + fe->clipped = FALSE; +} +void end_draw(frontend *fe) +{ + [fe->image unlockFocus]; + [fe->view setNeedsDisplay]; +} + +void deactivate_timer(frontend *fe) +{ + [fe->window deactivateTimer]; +} +void activate_timer(frontend *fe) +{ + [fe->window activateTimer]; +} + +/* ---------------------------------------------------------------------- + * Utility routines for constructing OS X menus. + */ + +NSMenu *newmenu(const char *title) +{ + return [[[NSMenu allocWithZone:[NSMenu menuZone]] + initWithTitle:[NSString stringWithCString:title]] + autorelease]; +} + +NSMenu *newsubmenu(NSMenu *parent, const char *title) +{ + NSMenuItem *item; + NSMenu *child; + + item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] + initWithTitle:[NSString stringWithCString:title] + action:NULL + keyEquivalent:@""] + autorelease]; + child = newmenu(title); + [item setEnabled:YES]; + [item setSubmenu:child]; + [parent addItem:item]; + return child; +} + +id initnewitem(NSMenuItem *item, NSMenu *parent, const char *title, + const char *key, id target, SEL action) +{ + unsigned mask = NSCommandKeyMask; + + if (key[strcspn(key, "-")]) { + while (*key && *key != '-') { + int c = tolower((unsigned char)*key); + if (c == 's') { + mask |= NSShiftKeyMask; + } else if (c == 'o' || c == 'a') { + mask |= NSAlternateKeyMask; + } + key++; + } + if (*key) + key++; + } + + item = [[item initWithTitle:[NSString stringWithCString:title] + action:NULL + keyEquivalent:[NSString stringWithCString:key]] + autorelease]; + + if (*key) + [item setKeyEquivalentModifierMask: mask]; + + [item setEnabled:YES]; + [item setTarget:target]; + [item setAction:action]; + + [parent addItem:item]; + + return item; +} + +NSMenuItem *newitem(NSMenu *parent, char *title, char *key, + id target, SEL action) +{ + return initnewitem([NSMenuItem allocWithZone:[NSMenu menuZone]], + parent, title, key, target, action); +} + +/* ---------------------------------------------------------------------- + * AppController: the object which receives the messages from all + * menu selections that aren't standard OS X functions. + */ +@interface AppController : NSObject +{ +} +- (IBAction)newGame:(id)sender; +@end + +@implementation AppController + +- (IBAction)newGame:(id)sender +{ + const game *g = [sender getPayload]; + id win; + + win = [[GameWindow alloc] initWithGame:g]; + [win makeKeyAndOrderFront:self]; +} + +@end + +/* ---------------------------------------------------------------------- + * Main program. Constructs the menus and runs the application. + */ +int main(int argc, char **argv) +{ + NSAutoreleasePool *pool; + NSMenu *menu; + NSMenuItem *item; + AppController *controller; + NSImage *icon; + + pool = [[NSAutoreleasePool alloc] init]; + + icon = [NSImage imageNamed:@"NSApplicationIcon"]; + [NSApplication sharedApplication]; + [NSApp setApplicationIconImage:icon]; + + controller = [[[AppController alloc] init] autorelease]; + + [NSApp setMainMenu: newmenu("Main Menu")]; + + menu = newsubmenu([NSApp mainMenu], "Apple Menu"); + [NSApp setServicesMenu:newsubmenu(menu, "Services")]; + [menu addItem:[NSMenuItem separatorItem]]; + item = newitem(menu, "Hide Puzzles", "h", NSApp, @selector(hide:)); + item = newitem(menu, "Hide Others", "o-h", NSApp, @selector(hideOtherApplications:)); + item = newitem(menu, "Show All", "", NSApp, @selector(unhideAllApplications:)); + [menu addItem:[NSMenuItem separatorItem]]; + item = newitem(menu, "Quit", "q", NSApp, @selector(terminate:)); + [NSApp setAppleMenu: menu]; + + menu = newsubmenu([NSApp mainMenu], "Open"); + { + int i; + + for (i = 0; i < gamecount; i++) { + id item = + initnewitem([DataMenuItem allocWithZone:[NSMenu menuZone]], + menu, gamelist[i]->name, "", controller, + @selector(newGame:)); + [item setPayload:(void *)gamelist[i]]; + } + } + + menu = newsubmenu([NSApp mainMenu], "Game"); + item = newitem(menu, "New", "n", NULL, @selector(newGame:)); + item = newitem(menu, "Restart", "r", NULL, @selector(restartGame:)); + item = newitem(menu, "Specific", "", NULL, @selector(specificGame:)); + [menu addItem:[NSMenuItem separatorItem]]; + item = newitem(menu, "Undo", "z", NULL, @selector(undoMove:)); + item = newitem(menu, "Redo", "S-z", NULL, @selector(redoMove:)); + [menu addItem:[NSMenuItem separatorItem]]; + item = newitem(menu, "Close", "w", NULL, @selector(performClose:)); + + menu = newsubmenu([NSApp mainMenu], "Type"); + typemenu = menu; + item = newitem(menu, "Custom", "", NULL, @selector(customGameType:)); + + menu = newsubmenu([NSApp mainMenu], "Window"); + [NSApp setWindowsMenu: menu]; + item = newitem(menu, "Minimise Window", "m", NULL, @selector(performMiniaturize:)); + + [NSApp run]; + [pool release]; +} -- cgit v1.1