Introduce a versioning mechanism, and an `About' box in all front
authorsimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Sun, 15 May 2005 10:31:11 +0000 (10:31 +0000)
committersimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Sun, 15 May 2005 10:31:11 +0000 (10:31 +0000)
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.

git-svn-id: svn://svn.tartarus.org/sgt/puzzles@5781 cda61777-01e9-0310-a592-d414129be87e

Recipe
gtk.c
mkfiles.pl
osx.m
puzzles.h
version.c [new file with mode: 0644]
windows.c

diff --git a/Recipe b/Recipe
index fe6c003..4c508c1 100644 (file)
--- 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 @@ Puzzles.dmg: Puzzles
 # 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
diff --git a/gtk.c b/gtk.c
index 199225a..5cd6832 100644 (file)
--- a/gtk.c
+++ b/gtk.c
@@ -525,7 +525,7 @@ static int win_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data)
     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 @@ void error_box(GtkWidget *parent, char *msg)
                        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 @@ void error_box(GtkWidget *parent, char *msg)
     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 @@ static void menu_config_event(GtkMenuItem *menuitem, gpointer data)
     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)
 {
@@ -1080,6 +1100,19 @@ static frontend *new_window(char *game_id, char **error)
     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;
         float *colours;
index f5a3d9b..b805dbe 100755 (executable)
@@ -51,6 +51,7 @@ while (<IN>) {
   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 @@ sub deps {
   @ret = ();
   $depchar ||= ':';
   foreach $i (sort keys %depends) {
+    next if $specialobj{$mftyp}->{$i};
     if ($i =~ /^(.*)\.(res|rsrc)/) {
       next if !defined $rtmpl;
       $y = $1;
diff --git a/osx.m b/osx.m
index d4dd90a..11436a8 100644 (file)
--- a/osx.m
+++ b/osx.m
@@ -233,6 +233,98 @@ NSMenuItem *newitem(NSMenu *parent, char *title, char *key,
 }
 
 /* ----------------------------------------------------------------------
+ * 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 status_bar(frontend *fe, char *text)
 {
 }
 - (void)newGameWindow:(id)sender;
+- (void)about:(id)sender;
 @end
 
 @implementation AppController
@@ -1182,6 +1275,14 @@ void status_bar(frontend *fe, char *text)
     [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 @@ int main(int argc, char **argv)
     [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:));
index 3867c2d..63de880 100644 (file)
--- a/puzzles.h
+++ b/puzzles.h
@@ -162,6 +162,11 @@ char *dupstr(const char *s);
 void free_cfg(config_item *cfg);
 
 /*
+ * version.c
+ */
+extern char ver[];
+
+/*
  * random.c
  */
 random_state *random_init(char *seed, int len);
diff --git a/version.c b/version.c
new file mode 100644 (file)
index 0000000..a44fbf6
--- /dev/null
+++ b/version.c
@@ -0,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
index 414c7cc..feee3cf 100644 (file)
--- 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 @@ struct frontend {
     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 @@ static frontend *new_window(HINSTANCE inst, char *game_id, char **error)
        }
        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 @@ static frontend *new_window(HINSTANCE inst, char *game_id, char **error)
     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 @@ static int CALLBACK ConfigDlgProc(HWND hwnd, UINT msg,
                    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 @@ static int CALLBACK ConfigDlgProc(HWND hwnd, UINT msg,
        return 0;
 
       case WM_CLOSE:
-       fe->cfg_done = 1;
+       fe->dlg_done = 1;
        return 0;
     }
 
@@ -633,7 +660,7 @@ static int CALLBACK ConfigDlgProc(HWND hwnd, UINT msg,
 
 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 @@ HWND mkctrl(frontend *fe, int x1, int x2, int y1, int y2,
     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 @@ static int get_config(frontend *fe, int which)
     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 @@ static int get_config(frontend *fe, int which)
        col2r = col1l+2*height+maxcheckbox;
     winwidth = col2r + 2*width;
 
+    SelectObject(hdc, oldfont);
     ReleaseDC(fe->hwnd, hdc);
 
     /*
@@ -869,7 +1049,7 @@ static int get_config(frontend *fe, int which)
     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 @@ static int get_config(frontend *fe, int which)
     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 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
            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,