shithub: puzzles

Download patch

ref: c05b4697a8f87e08e52b8014186c647309976fc2
parent: 68d27f05262b7928b4d755d2e27868cfc51b3007
author: Simon Tatham <anakin@pobox.com>
date: Sun May 15 06:31:11 EDT 2005

Introduce a versioning mechanism, and an `About' box in all front
ends. Versioning will be done solely by Subversion revision number,
since development on these puzzles is very incremental and gradual
and there don't tend to be obvious points to place numbered
releases.

[originally from svn r5781]

--- a/Recipe
+++ b/Recipe
@@ -14,7 +14,7 @@
 !makefile osx Makefile.osx
 
 WINDOWS  = windows user32.lib gdi32.lib comctl32.lib
-COMMON   = midend misc malloc random
+COMMON   = midend misc malloc random version
 NET      = net tree234
 NETSLIDE = netslide tree234
 
@@ -84,3 +84,41 @@
 # be built on a regular basis.
 nullgame : [X] gtk COMMON nullgame
 nullgame : [G] WINDOWS COMMON nullgame
+
+# Version management.
+!begin vc
+version.obj: *.c *.h
+	cl $(VER) $(CFLAGS) /c version.c
+!end
+!specialobj vc version
+!begin cygwin
+version.o: FORCE
+FORCE:
+	$(CC) $(COMPAT) $(XFLAGS) $(CFLAGS) $(VER) -c version.c
+!end
+!specialobj cygwin version
+# For Unix, we also need the gross MD5 hack that causes automatic
+# version number selection in release source archives.
+!begin gtk
+version.o: FORCE;
+FORCE:
+	if test -z "$(VER)" && md5sum -c manifest; then \
+		$(CC) $(COMPAT) $(XFLAGS) $(CFLAGS) `cat version.def` -c version.c; \
+	else \
+		$(CC) $(COMPAT) $(XFLAGS) $(CFLAGS) $(VER) -c version.c; \
+	fi
+!end
+!specialobj gtk version
+# For OS X, this is made more fiddly by the fact that we don't have
+# md5sum readily available. We do, however, have `md5 -r' which
+# generates _nearly_ the same output, but it has no check function.
+!begin osx
+version.o: FORCE;
+FORCE:
+	if test -z "$(VER)" && test -f manifest && (md5 -r `awk '{print $$2}' manifest` | diff -w manifest -); then \
+		$(CC) $(COMPAT) $(XFLAGS) $(CFLAGS) `cat version.def` -c version.c; \
+	else \
+		$(CC) $(COMPAT) $(XFLAGS) $(CFLAGS) $(VER) -c version.c; \
+	fi
+!end
+!specialobj osx version
--- a/gtk.c
+++ b/gtk.c
@@ -525,7 +525,7 @@
     return FALSE;
 }
 
-void error_box(GtkWidget *parent, char *msg)
+void message_box(GtkWidget *parent, char *title, char *msg, int centre)
 {
     GtkWidget *window, *hbox, *text, *ok;
 
@@ -538,7 +538,7 @@
                        hbox, FALSE, FALSE, 20);
     gtk_widget_show(text);
     gtk_widget_show(hbox);
-    gtk_window_set_title(GTK_WINDOW(window), "Error");
+    gtk_window_set_title(GTK_WINDOW(window), title);
     gtk_label_set_line_wrap(GTK_LABEL(text), TRUE);
     ok = gtk_button_new_with_label("OK");
     gtk_box_pack_end(GTK_BOX(GTK_DIALOG(window)->action_area),
@@ -559,6 +559,11 @@
     gtk_main();
 }
 
+void error_box(GtkWidget *parent, char *msg)
+{
+    message_box(parent, "Error", msg, FALSE);
+}
+
 static void config_ok_button_clicked(GtkButton *button, gpointer data)
 {
     frontend *fe = (frontend *)data;
@@ -949,6 +954,21 @@
     fe->h = y;
 }
 
+static void menu_about_event(GtkMenuItem *menuitem, gpointer data)
+{
+    frontend *fe = (frontend *)data;
+    char titlebuf[256];
+    char textbuf[1024];
+
+    sprintf(titlebuf, "About %.200s", thegame.name);
+    sprintf(textbuf,
+	    "%.200s\n\n"
+	    "from Simon Tatham's Portable Puzzle Collection\n\n"
+	    "%.500s", thegame.name, ver);
+
+    message_box(fe->window, titlebuf, textbuf, TRUE);
+}
+
 static GtkWidget *add_menu_item_with_key(frontend *fe, GtkContainer *cont,
                                          char *text, int key)
 {
@@ -1079,6 +1099,19 @@
     }
     add_menu_separator(GTK_CONTAINER(menu));
     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Exit", 'q');
+
+    menuitem = gtk_menu_item_new_with_label("Help");
+    gtk_container_add(GTK_CONTAINER(menubar), menuitem);
+    gtk_widget_show(menuitem);
+
+    menu = gtk_menu_new();
+    gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
+
+    menuitem = gtk_menu_item_new_with_label("About");
+    gtk_container_add(GTK_CONTAINER(menu), menuitem);
+    gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
+		       GTK_SIGNAL_FUNC(menu_about_event), fe);
+    gtk_widget_show(menuitem);
 
     {
         int i, ncolours;
--- a/mkfiles.pl
+++ b/mkfiles.pl
@@ -51,6 +51,7 @@
   if ($_[0] eq "!name") { $project_name = $_[1]; next; }
   if ($_[0] eq "!srcdir") { push @srcdirs, $_[1]; next; }
   if ($_[0] eq "!makefile" and &mfval($_[1])) { $makefiles{$_[1]}=$_[2]; next;}
+  if ($_[0] eq "!specialobj" and &mfval($_[1])) { $specialobj{$_[1]}->{$_[2]} = 1; next;}
   if ($_[0] eq "!begin") {
       if (&mfval($_[1])) {
 	  $divert = \$makefile_extra{$_[1]};
@@ -299,6 +300,7 @@
   @ret = ();
   $depchar ||= ':';
   foreach $i (sort keys %depends) {
+    next if $specialobj{$mftyp}->{$i};
     if ($i =~ /^(.*)\.(res|rsrc)/) {
       next if !defined $rtmpl;
       $y = $1;
--- a/osx.m
+++ b/osx.m
@@ -233,6 +233,98 @@
 }
 
 /* ----------------------------------------------------------------------
+ * About box.
+ */
+
+@class AboutBox;
+
+@interface AboutBox : NSWindow
+{
+}
+- (id)init;
+@end
+
+@implementation AboutBox
+- (id)init
+{
+    NSRect totalrect;
+    NSView *views[16];
+    int nviews = 0;
+    NSImageView *iv;
+    NSTextField *tf;
+    NSFont *font1 = [NSFont systemFontOfSize:0];
+    NSFont *font2 = [NSFont boldSystemFontOfSize:[font1 pointSize] * 1.1];
+    const int border = 24;
+    int i;
+    double y;
+
+    /*
+     * Construct the controls that go in the About box.
+     */
+
+    iv = [[NSImageView alloc] initWithFrame:NSMakeRect(0,0,64,64)];
+    [iv setImage:[NSImage imageNamed:@"NSApplicationIcon"]];
+    views[nviews++] = iv;
+
+    tf = [[NSTextField alloc]
+	  initWithFrame:NSMakeRect(0,0,400,1)];
+    [tf setEditable:NO];
+    [tf setSelectable:NO];
+    [tf setBordered:NO];
+    [tf setDrawsBackground:NO];
+    [tf setFont:font2];
+    [tf setStringValue:@"Simon Tatham's Portable Puzzle Collection"];
+    [tf sizeToFit];
+    views[nviews++] = tf;
+
+    tf = [[NSTextField alloc]
+	  initWithFrame:NSMakeRect(0,0,400,1)];
+    [tf setEditable:NO];
+    [tf setSelectable:NO];
+    [tf setBordered:NO];
+    [tf setDrawsBackground:NO];
+    [tf setFont:font1];
+    [tf setStringValue:[NSString stringWithCString:ver]];
+    [tf sizeToFit];
+    views[nviews++] = tf;
+
+    /*
+     * Lay the controls out.
+     */
+    totalrect = NSMakeRect(0,0,0,0);
+    for (i = 0; i < nviews; i++) {
+	NSRect r = [views[i] frame];
+	if (totalrect.size.width < r.size.width)
+	    totalrect.size.width = r.size.width;
+	totalrect.size.height += border + r.size.height;
+    }
+    totalrect.size.width += 2 * border;
+    totalrect.size.height += border;
+    y = totalrect.size.height;
+    for (i = 0; i < nviews; i++) {
+	NSRect r = [views[i] frame];
+	r.origin.x = (totalrect.size.width - r.size.width) / 2;
+	y -= border + r.size.height;
+	r.origin.y = y;
+	[views[i] setFrame:r];
+    }
+
+    self = [super initWithContentRect:totalrect
+	    styleMask:(NSTitledWindowMask | NSMiniaturizableWindowMask |
+		       NSClosableWindowMask)
+	    backing:NSBackingStoreBuffered
+	    defer:YES];
+
+    for (i = 0; i < nviews; i++)
+	[[self contentView] addSubview:views[i]];
+
+    [self center];		       /* :-) */
+
+    return self;
+}
+@end
+
+/* ----------------------------------------------------------------------
  * The front end presented to midend.c.
  * 
  * This is mostly a subclass of NSWindow. The actual `frontend'
@@ -1169,6 +1261,7 @@
 {
 }
 - (void)newGameWindow:(id)sender;
+- (void)about:(id)sender;
 @end
 
 @implementation AppController
@@ -1182,6 +1275,14 @@
     [win makeKeyAndOrderFront:self];
 }
 
+- (void)about:(id)sender
+{
+    id win;
+
+    win = [[AboutBox alloc] init];
+    [win makeKeyAndOrderFront:self];    
+}
+
 - (NSMenu *)applicationDockMenu:(NSApplication *)sender
 {
     NSMenu *menu = newmenu("Dock Menu");
@@ -1224,6 +1325,8 @@
     [NSApp setMainMenu: newmenu("Main Menu")];
 
     menu = newsubmenu([NSApp mainMenu], "Apple Menu");
+    item = newitem(menu, "About Puzzles", "", NULL, @selector(about:));
+    [menu addItem:[NSMenuItem separatorItem]];
     [NSApp setServicesMenu:newsubmenu(menu, "Services")];
     [menu addItem:[NSMenuItem separatorItem]];
     item = newitem(menu, "Hide Puzzles", "h", NSApp, @selector(hide:));
--- a/puzzles.h
+++ b/puzzles.h
@@ -162,6 +162,11 @@
 void free_cfg(config_item *cfg);
 
 /*
+ * version.c
+ */
+extern char ver[];
+
+/*
  * random.c
  */
 random_state *random_init(char *seed, int len);
--- /dev/null
+++ b/version.c
@@ -1,0 +1,16 @@
+/*
+ * Puzzles version numbering.
+ */
+
+#define STR1(x) #x
+#define STR(x) STR1(x)
+
+#if defined REVISION
+
+char ver[] = "Revision: r" STR(REVISION);
+
+#else
+
+char ver[] = "Unidentified build, " __DATE__ " " __TIME__;
+
+#endif
--- a/windows.c
+++ b/windows.c
@@ -35,6 +35,7 @@
 #define IDM_HELPC     0x00A0
 #define IDM_GAMEHELP  0x00B0
 #define IDM_PRESETS   0x0100
+#define IDM_ABOUT     0x0110
 
 #define HELP_FILE_NAME  "puzzles.hlp"
 #define HELP_CNT_NAME   "puzzles.cnt"
@@ -112,7 +113,7 @@
     int nfonts, fontsize;
     config_item *cfg;
     struct cfg_aux *cfgaux;
-    int cfg_which, cfg_done;
+    int cfg_which, dlg_done;
     HFONT cfgfont;
     char *help_path;
     int help_has_contents;
@@ -517,16 +518,18 @@
 	}
 	AppendMenu(menu, MF_SEPARATOR, 0, 0);
 	AppendMenu(menu, MF_ENABLED, IDM_QUIT, "Exit");
+	menu = CreateMenu();
+	AppendMenu(bar, MF_ENABLED|MF_POPUP, (UINT)menu, "Help");
+	AppendMenu(menu, MF_ENABLED, IDM_ABOUT, "About");
         if (fe->help_path) {
-            HMENU hmenu = CreateMenu();
-            AppendMenu(bar, MF_ENABLED|MF_POPUP, (UINT)hmenu, "Help");
-            AppendMenu(hmenu, MF_ENABLED, IDM_HELPC, "Contents");
+	    AppendMenu(menu, MF_SEPARATOR, 0, 0);
+            AppendMenu(menu, MF_ENABLED, IDM_HELPC, "Contents");
             if (thegame.winhelp_topic) {
                 char *item;
                 assert(thegame.name);
                 item = snewn(9+strlen(thegame.name), char); /*ick*/
                 sprintf(item, "Help on %s", thegame.name);
-                AppendMenu(hmenu, MF_ENABLED, IDM_GAMEHELP, item);
+                AppendMenu(menu, MF_ENABLED, IDM_GAMEHELP, item);
                 sfree(item);
             }
         }
@@ -562,6 +565,30 @@
     return fe;
 }
 
+static int CALLBACK AboutDlgProc(HWND hwnd, UINT msg,
+				 WPARAM wParam, LPARAM lParam)
+{
+    frontend *fe = (frontend *)GetWindowLong(hwnd, GWL_USERDATA);
+
+    switch (msg) {
+      case WM_INITDIALOG:
+	return 0;
+
+      case WM_COMMAND:
+	if ((HIWORD(wParam) == BN_CLICKED ||
+	     HIWORD(wParam) == BN_DOUBLECLICKED) &&
+	    LOWORD(wParam) == IDOK)
+	    fe->dlg_done = 1;
+	return 0;
+
+      case WM_CLOSE:
+	fe->dlg_done = 1;
+	return 0;
+    }
+
+    return 0;
+}
+
 static int CALLBACK ConfigDlgProc(HWND hwnd, UINT msg,
 				  WPARAM wParam, LPARAM lParam)
 {
@@ -587,10 +614,10 @@
 		    MessageBox(hwnd, err, "Validation error",
 			       MB_ICONERROR | MB_OK);
 		} else {
-		    fe->cfg_done = 2;
+		    fe->dlg_done = 2;
 		}
 	    } else {
-		fe->cfg_done = 1;
+		fe->dlg_done = 1;
 	    }
 	    return 0;
 	}
@@ -624,7 +651,7 @@
 	return 0;
 
       case WM_CLOSE:
-	fe->cfg_done = 1;
+	fe->dlg_done = 1;
 	return 0;
     }
 
@@ -633,7 +660,7 @@
 
 HWND mkctrl(frontend *fe, int x1, int x2, int y1, int y2,
 	    char *wclass, int wstyle,
-	    int exstyle, char *wtext, int wid)
+	    int exstyle, const char *wtext, int wid)
 {
     HWND ret;
     ret = CreateWindowEx(exstyle, wclass, wtext,
@@ -643,6 +670,158 @@
     return ret;
 }
 
+static void about(frontend *fe)
+{
+    int i;
+    WNDCLASS wc;
+    MSG msg;
+    TEXTMETRIC tm;
+    HDC hdc;
+    HFONT oldfont;
+    SIZE size;
+    int gm, id;
+    int winwidth, winheight, y;
+    int height, width, maxwid;
+    const char *strings[16];
+    int lengths[16];
+    int nstrings = 0;
+    char titlebuf[512];
+
+    sprintf(titlebuf, "About %.250s", thegame.name);
+
+    strings[nstrings++] = thegame.name;
+    strings[nstrings++] = "from Simon Tatham's Portable Puzzle Collection";
+    strings[nstrings++] = ver;
+
+    wc.style = CS_DBLCLKS | CS_SAVEBITS | CS_BYTEALIGNWINDOW;
+    wc.lpfnWndProc = DefDlgProc;
+    wc.cbClsExtra = 0;
+    wc.cbWndExtra = DLGWINDOWEXTRA + 8;
+    wc.hInstance = fe->inst;
+    wc.hIcon = NULL;
+    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
+    wc.hbrBackground = (HBRUSH) (COLOR_BACKGROUND +1);
+    wc.lpszMenuName = NULL;
+    wc.lpszClassName = "GameAboutBox";
+    RegisterClass(&wc);
+
+    hdc = GetDC(fe->hwnd);
+    SetMapMode(hdc, MM_TEXT);
+
+    fe->dlg_done = FALSE;
+
+    fe->cfgfont = CreateFont(-MulDiv(8, GetDeviceCaps(hdc, LOGPIXELSY), 72),
+			     0, 0, 0, 0,
+			     FALSE, FALSE, FALSE, DEFAULT_CHARSET,
+			     OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
+			     DEFAULT_QUALITY,
+			     FF_SWISS,
+			     "MS Shell Dlg");
+
+    oldfont = SelectObject(hdc, fe->cfgfont);
+    if (GetTextMetrics(hdc, &tm)) {
+	height = tm.tmAscent + tm.tmDescent;
+	width = tm.tmAveCharWidth;
+    } else {
+	height = width = 30;
+    }
+
+    /*
+     * Figure out the layout of the About box by measuring the
+     * length of each piece of text.
+     */
+    maxwid = 0;
+    winheight = height/2;
+
+    for (i = 0; i < nstrings; i++) {
+	if (GetTextExtentPoint32(hdc, strings[i], strlen(strings[i]), &size))
+	    lengths[i] = size.cx;
+	else
+	    lengths[i] = 0;	       /* *shrug* */
+	if (maxwid < lengths[i])
+	    maxwid = lengths[i];
+	winheight += height * 3 / 2 + (height / 2);
+    }
+
+    winheight += height + height * 7 / 4;      /* OK button */
+    winwidth = maxwid + 4*width;
+
+    SelectObject(hdc, oldfont);
+    ReleaseDC(fe->hwnd, hdc);
+
+    /*
+     * Create the dialog, now that we know its size.
+     */
+    {
+	RECT r, r2;
+
+	r.left = r.top = 0;
+	r.right = winwidth;
+	r.bottom = winheight;
+
+	AdjustWindowRectEx(&r, (WS_OVERLAPPEDWINDOW /*|
+				DS_MODALFRAME | WS_POPUP | WS_VISIBLE |
+				WS_CAPTION | WS_SYSMENU*/) &~
+			   (WS_MAXIMIZEBOX | WS_OVERLAPPED),
+			   FALSE, 0);
+
+	/*
+	 * Centre the dialog on its parent window.
+	 */
+	r.right -= r.left;
+	r.bottom -= r.top;
+	GetWindowRect(fe->hwnd, &r2);
+	r.left = (r2.left + r2.right - r.right) / 2;
+	r.top = (r2.top + r2.bottom - r.bottom) / 2;
+	r.right += r.left;
+	r.bottom += r.top;
+
+	fe->cfgbox = CreateWindowEx(0, wc.lpszClassName, titlebuf,
+				    DS_MODALFRAME | WS_POPUP | WS_VISIBLE |
+				    WS_CAPTION | WS_SYSMENU,
+				    r.left, r.top,
+				    r.right-r.left, r.bottom-r.top,
+				    fe->hwnd, NULL, fe->inst, NULL);
+    }
+
+    SendMessage(fe->cfgbox, WM_SETFONT, (WPARAM)fe->cfgfont, FALSE);
+
+    SetWindowLong(fe->cfgbox, GWL_USERDATA, (LONG)fe);
+    SetWindowLong(fe->cfgbox, DWL_DLGPROC, (LONG)AboutDlgProc);
+
+    id = 1000;
+    y = height/2;
+    for (i = 0; i < nstrings; i++) {
+	int border = width*2 + (maxwid - lengths[i]) / 2;
+	mkctrl(fe, border, border+lengths[i], y+height*1/8, y+height*9/8,
+	       "Static", 0, 0, strings[i], id++);
+	y += height*3/2;
+
+	assert(y < winheight);
+	y += height/2;
+    }
+
+    y += height/2;		       /* extra space before OK */
+    mkctrl(fe, width*2, maxwid+width*2, y, y+height*7/4, "BUTTON",
+	   BS_PUSHBUTTON | BS_NOTIFY | WS_TABSTOP | BS_DEFPUSHBUTTON, 0,
+	   "OK", IDOK);
+
+    SendMessage(fe->cfgbox, WM_INITDIALOG, 0, 0);
+
+    EnableWindow(fe->hwnd, FALSE);
+    ShowWindow(fe->cfgbox, SW_NORMAL);
+    while ((gm=GetMessage(&msg, NULL, 0, 0)) > 0) {
+	if (!IsDialogMessage(fe->cfgbox, &msg))
+	    DispatchMessage(&msg);
+	if (fe->dlg_done)
+	    break;
+    }
+    EnableWindow(fe->hwnd, TRUE);
+    SetForegroundWindow(fe->hwnd);
+    DestroyWindow(fe->cfgbox);
+    DeleteObject(fe->cfgfont);
+}
+
 static int get_config(frontend *fe, int which)
 {
     config_item *i;
@@ -674,7 +853,7 @@
     hdc = GetDC(fe->hwnd);
     SetMapMode(hdc, MM_TEXT);
 
-    fe->cfg_done = FALSE;
+    fe->dlg_done = FALSE;
 
     fe->cfgfont = CreateFont(-MulDiv(8, GetDeviceCaps(hdc, LOGPIXELSY), 72),
 			     0, 0, 0, 0,
@@ -738,6 +917,7 @@
 	col2r = col1l+2*height+maxcheckbox;
     winwidth = col2r + 2*width;
 
+    SelectObject(hdc, oldfont);
     ReleaseDC(fe->hwnd, hdc);
 
     /*
@@ -869,7 +1049,7 @@
     while ((gm=GetMessage(&msg, NULL, 0, 0)) > 0) {
 	if (!IsDialogMessage(fe->cfgbox, &msg))
 	    DispatchMessage(&msg);
-	if (fe->cfg_done)
+	if (fe->dlg_done)
 	    break;
     }
     EnableWindow(fe->hwnd, TRUE);
@@ -880,7 +1060,7 @@
     free_cfg(fe->cfg);
     sfree(fe->cfgaux);
 
-    return (fe->cfg_done == 2);
+    return (fe->dlg_done == 2);
 }
 
 static void new_game_type(frontend *fe)
@@ -979,6 +1159,9 @@
 	    if (get_config(fe, CFG_SEED))
 		new_game_type(fe);
 	    break;
+          case IDM_ABOUT:
+	    about(fe);
+            break;
           case IDM_HELPC:
             assert(fe->help_path);
             WinHelp(hwnd, fe->help_path,