shithub: puzzles

Download patch

ref: 6f47baddf90a4685c5662e5f6a782afbbe4cd619
parent: 118abb4fc9365807ad4174588eeb9aefda550415
author: Simon Tatham <anakin@pobox.com>
date: Thu Jun 30 14:00:37 EDT 2005

Load and Save are now supported on all three desktop platforms, and
documented. (This means the GTK temporary dependency on an
environment variable is now gone.)

[originally from svn r6042]

--- a/Recipe
+++ b/Recipe
@@ -13,7 +13,7 @@
 !makefile cygwin Makefile.cyg
 !makefile osx Makefile.osx
 
-WINDOWS  = windows user32.lib gdi32.lib comctl32.lib
+WINDOWS  = windows user32.lib gdi32.lib comctl32.lib comdlg32.lib
 COMMON   = midend misc malloc random version
 NET      = net tree234
 NETSLIDE = netslide tree234
--- a/gtk.c
+++ b/gtk.c
@@ -660,8 +660,15 @@
     gtk_main_quit();
 }
 
-static void errmsg_button_clicked(GtkButton *button, gpointer data)
+static void msgbox_button_clicked(GtkButton *button, gpointer data)
 {
+    GtkWidget *window = GTK_WIDGET(data);
+    int v, *ip;
+
+    ip = (int *)gtk_object_get_data(GTK_OBJECT(window), "user-data");
+    v = GPOINTER_TO_INT(gtk_object_get_data(GTK_OBJECT(button), "user-data"));
+    *ip = v;
+
     gtk_widget_destroy(GTK_WIDGET(data));
 }
 
@@ -680,9 +687,14 @@
     return FALSE;
 }
 
-void message_box(GtkWidget *parent, char *title, char *msg, int centre)
+enum { MB_OK, MB_YESNO };
+
+int message_box(GtkWidget *parent, char *title, char *msg, int centre,
+		int type)
 {
-    GtkWidget *window, *hbox, *text, *ok;
+    GtkWidget *window, *hbox, *text, *button;
+    char *titles;
+    int i, def, cancel;
 
     window = gtk_dialog_new();
     text = gtk_label_new(msg);
@@ -695,28 +707,54 @@
     gtk_widget_show(hbox);
     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),
-                     ok, FALSE, FALSE, 0);
-    gtk_widget_show(ok);
-    GTK_WIDGET_SET_FLAGS(ok, GTK_CAN_DEFAULT);
-    gtk_window_set_default(GTK_WINDOW(window), ok);
-    gtk_signal_connect(GTK_OBJECT(ok), "clicked",
-                       GTK_SIGNAL_FUNC(errmsg_button_clicked), window);
+
+    if (type == MB_OK) {
+	titles = "OK\0";
+	def = cancel = 0;
+    } else {
+	assert(type == MB_YESNO);
+	titles = "Yes\0No\0";
+	def = 0;
+	cancel = 1;
+    }
+    i = 0;
+    
+    while (*titles) {
+	button = gtk_button_new_with_label(titles);
+	gtk_box_pack_end(GTK_BOX(GTK_DIALOG(window)->action_area),
+			 button, FALSE, FALSE, 0);
+	gtk_widget_show(button);
+	if (i == def) {
+	    GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
+	    gtk_window_set_default(GTK_WINDOW(window), button);
+	}
+	if (i == cancel) {
+	    gtk_signal_connect(GTK_OBJECT(window), "key_press_event",
+			       GTK_SIGNAL_FUNC(win_key_press), button);
+	}
+	gtk_signal_connect(GTK_OBJECT(button), "clicked",
+			   GTK_SIGNAL_FUNC(msgbox_button_clicked), window);
+	gtk_object_set_data(GTK_OBJECT(button), "user-data",
+			    GINT_TO_POINTER(i));
+	titles += strlen(titles)+1;
+	i++;
+    }
+    gtk_object_set_data(GTK_OBJECT(window), "user-data",
+			GINT_TO_POINTER(&i));
     gtk_signal_connect(GTK_OBJECT(window), "destroy",
                        GTK_SIGNAL_FUNC(window_destroy), NULL);
-    gtk_signal_connect(GTK_OBJECT(window), "key_press_event",
-		       GTK_SIGNAL_FUNC(win_key_press), ok);
     gtk_window_set_modal(GTK_WINDOW(window), TRUE);
     gtk_window_set_transient_for(GTK_WINDOW(window), GTK_WINDOW(parent));
     /* set_transient_window_pos(parent, window); */
     gtk_widget_show(window);
+    i = -1;
     gtk_main();
+    return (type == MB_YESNO ? i == 0 : TRUE);
 }
 
 void error_box(GtkWidget *parent, char *msg)
 {
-    message_box(parent, "Error", msg, FALSE);
+    message_box(parent, "Error", msg, FALSE, MB_OK);
 }
 
 static void config_ok_button_clicked(GtkButton *button, gpointer data)
@@ -1180,7 +1218,21 @@
     name = file_selector(fe, "Enter name of game file to save", TRUE);
 
     if (name) {
-        FILE *fp = fopen(name, "w");
+        FILE *fp;
+
+	if ((fp = fopen(name, "r")) != NULL) {
+	    char buf[256 + FILENAME_MAX];
+	    fclose(fp);
+	    /* file exists */
+
+	    sprintf(buf, "Are you sure you want to overwrite the"
+		    " file \"%.*s\"?",
+		    FILENAME_MAX, name);
+	    if (!message_box(fe->window, "Question", buf, TRUE, MB_YESNO))
+		return;
+	}
+
+	fp = fopen(name, "w");
         sfree(name);
 
         if (!fp) {
@@ -1285,7 +1337,7 @@
 	    "from Simon Tatham's Portable Puzzle Collection\n\n"
 	    "%.500s", thegame.name, ver);
 
-    message_box(fe->window, titlebuf, textbuf, TRUE);
+    message_box(fe->window, titlebuf, textbuf, TRUE, MB_OK);
 }
 
 static GtkWidget *add_menu_item_with_key(frontend *fe, GtkContainer *cont,
@@ -1414,19 +1466,17 @@
 	}
     }
 
-    if (getenv("PUZZLES_EXPERIMENTAL_SAVE") != NULL) {
-        add_menu_separator(GTK_CONTAINER(menu));
-        menuitem = gtk_menu_item_new_with_label("Load");
-        gtk_container_add(GTK_CONTAINER(menu), menuitem);
-        gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
-                           GTK_SIGNAL_FUNC(menu_load_event), fe);
-        gtk_widget_show(menuitem);
-        menuitem = gtk_menu_item_new_with_label("Save");
-        gtk_container_add(GTK_CONTAINER(menu), menuitem);
-        gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
-                           GTK_SIGNAL_FUNC(menu_save_event), fe);
-        gtk_widget_show(menuitem);
-    }
+    add_menu_separator(GTK_CONTAINER(menu));
+    menuitem = gtk_menu_item_new_with_label("Load");
+    gtk_container_add(GTK_CONTAINER(menu), menuitem);
+    gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
+		       GTK_SIGNAL_FUNC(menu_load_event), fe);
+    gtk_widget_show(menuitem);
+    menuitem = gtk_menu_item_new_with_label("Save");
+    gtk_container_add(GTK_CONTAINER(menu), menuitem);
+    gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
+		       GTK_SIGNAL_FUNC(menu_save_event), fe);
+    gtk_widget_show(menuitem);
     add_menu_separator(GTK_CONTAINER(menu));
     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Undo", 'u');
     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Redo", '\x12');
--- a/osx.m
+++ b/osx.m
@@ -138,6 +138,21 @@
     *randseedsize = sizeof(time_t);
 }
 
+static void savefile_write(void *wctx, void *buf, int len)
+{
+    FILE *fp = (FILE *)wctx;
+    fwrite(buf, 1, len, fp);
+}
+
+static int savefile_read(void *wctx, void *buf, int len)
+{
+    FILE *fp = (FILE *)wctx;
+    int ret;
+
+    ret = fread(buf, 1, len, fp);
+    return (ret == len);
+}
+
 /* ----------------------------------------------------------------------
  * Tiny extension to NSMenuItem which carries a payload of a `void
  * *', allowing several menu items to invoke the same message but
@@ -384,6 +399,7 @@
 - (void)activateTimer;
 - (void)deactivateTimer;
 - (void)setStatusLine:(char *)text;
+- (void)resizeForNewGameParams;
 @end
 
 @implementation MyImageView
@@ -659,6 +675,17 @@
     last_time = now;
 }
 
+- (void)showError:(char *)message
+{
+    NSAlert *alert;
+
+    alert = [[[NSAlert alloc] init] autorelease];
+    [alert addButtonWithTitle:@"Bah"];
+    [alert setInformativeText:[NSString stringWithCString:message]];
+    [alert beginSheetModalForWindow:self modalDelegate:nil
+     didEndSelector:nil contextInfo:nil];
+}
+
 - (void)newGame:(id)sender
 {
     [self processButton:'n' x:-1 y:-1];
@@ -667,6 +694,54 @@
 {
     midend_restart_game(me);
 }
+- (void)saveGame:(id)sender
+{
+    NSSavePanel *sp = [NSSavePanel savePanel];
+
+    if ([sp runModal] == NSFileHandlingPanelOKButton) {
+	const char *name = [[sp filename] cString];
+
+        FILE *fp = fopen(name, "w");
+
+        if (!fp) {
+            [self showError:"Unable to open save file"];
+            return;
+        }
+
+        midend_serialise(me, savefile_write, fp);
+
+        fclose(fp);
+    }
+}
+- (void)loadSavedGame:(id)sender
+{
+    NSOpenPanel *op = [NSOpenPanel openPanel];
+
+    [op setAllowsMultipleSelection:NO];
+
+    if ([op runModalForTypes:nil] == NSOKButton) {
+	const char *name = [[[op filenames] objectAtIndex:0] cString];
+	char *err;
+
+        FILE *fp = fopen(name, "r");
+
+        if (!fp) {
+            [self showError:"Unable to open saved game file"];
+            return;
+        }
+
+        err = midend_deserialise(me, savefile_read, fp);
+
+        fclose(fp);
+
+        if (err) {
+            [self showError:err];
+            return;
+        }
+
+	[self resizeForNewGameParams];
+    }
+}
 - (void)undoMove:(id)sender
 {
     [self processButton:'u' x:-1 y:-1];
@@ -693,17 +768,11 @@
 - (void)solveGame:(id)sender
 {
     char *msg;
-    NSAlert *alert;
 
     msg = midend_solve(me);
 
-    if (msg) {
-	alert = [[[NSAlert alloc] init] autorelease];
-	[alert addButtonWithTitle:@"Bah"];
-	[alert setInformativeText:[NSString stringWithCString:msg]];
-	[alert beginSheetModalForWindow:self modalDelegate:nil
-	 didEndSelector:nil contextInfo:nil];
-    }
+    if (msg)
+	[self showError:msg];
 }
 
 - (BOOL)validateMenuItem:(NSMenuItem *)item
@@ -1421,6 +1490,8 @@
     [NSApp setAppleMenu: menu];
 
     menu = newsubmenu([NSApp mainMenu], "File");
+    item = newitem(menu, "Open", "o", NULL, @selector(loadSavedGame:));
+    item = newitem(menu, "Save As", "s", NULL, @selector(saveGame:));
     item = newitem(menu, "New Game", "n", NULL, @selector(newGame:));
     item = newitem(menu, "Restart Game", "r", NULL, @selector(restartGame:));
     item = newitem(menu, "Specific Game", "", NULL, @selector(specificGame:));
--- a/puzzles.but
+++ b/puzzles.but
@@ -94,6 +94,22 @@
 
 \dd Resets the current game to its initial state. (This can be undone.)
 
+\dt \ii\e{Load}
+
+\dd Loads a saved game from a file on disk.
+
+\dt \ii\e{Save}
+
+\dd Saves the current state of your game to a file on disk.
+
+\lcont{
+
+The Load and Save operations should preserve your entire game
+history (so you can save, reload, and still Undo and Redo things you
+had done before saving).
+
+}
+
 \dt \ii\e{Undo} (\q{U}, Ctrl+\q{Z}, Ctrl+\q{_})
 
 \dd Undoes a single move. (You can undo moves back to the start of the
--- a/windows.c
+++ b/windows.c
@@ -28,6 +28,8 @@
 #define IDM_HELPC     0x00B0
 #define IDM_GAMEHELP  0x00C0
 #define IDM_ABOUT     0x00D0
+#define IDM_SAVE      0x00E0
+#define IDM_LOAD      0x00F0
 #define IDM_PRESETS   0x0100
 
 #define HELP_FILE_NAME  "puzzles.hlp"
@@ -670,6 +672,9 @@
 	}
 
 	AppendMenu(menu, MF_SEPARATOR, 0, 0);
+	AppendMenu(menu, MF_ENABLED, IDM_LOAD, "Load");
+	AppendMenu(menu, MF_ENABLED, IDM_SAVE, "Save");
+	AppendMenu(menu, MF_SEPARATOR, 0, 0);
 	AppendMenu(menu, MF_ENABLED, IDM_UNDO, "Undo");
 	AppendMenu(menu, MF_ENABLED, IDM_REDO, "Redo");
 	if (thegame.can_format_as_text) {
@@ -1214,13 +1219,12 @@
     return (fe->dlg_done == 2);
 }
 
-static void new_game_type(frontend *fe)
+static void new_game_size(frontend *fe)
 {
     RECT r, sr;
     HDC hdc;
     int x, y;
 
-    midend_new_game(fe->me);
     x = y = INT_MAX;
     midend_size(fe->me, &x, &y, FALSE);
 
@@ -1257,6 +1261,12 @@
     midend_redraw(fe->me);
 }
 
+static void new_game_type(frontend *fe)
+{
+    midend_new_game(fe->me);
+    new_game_size(fe);
+}
+
 static int is_alt_pressed(void)
 {
     BYTE keystate[256];
@@ -1270,10 +1280,26 @@
     return FALSE;
 }
 
+static void savefile_write(void *wctx, void *buf, int len)
+{
+    FILE *fp = (FILE *)wctx;
+    fwrite(buf, 1, len, fp);
+}
+
+static int savefile_read(void *wctx, void *buf, int len)
+{
+    FILE *fp = (FILE *)wctx;
+    int ret;
+
+    ret = fread(buf, 1, len, fp);
+    return (ret == len);
+}
+
 static LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
 				WPARAM wParam, LPARAM lParam)
 {
     frontend *fe = (frontend *)GetWindowLong(hwnd, GWL_USERDATA);
+    int cmd;
 
     switch (message) {
       case WM_CLOSE:
@@ -1280,7 +1306,8 @@
 	DestroyWindow(hwnd);
 	return 0;
       case WM_COMMAND:
-	switch (wParam & ~0xF) {       /* low 4 bits reserved to Windows */
+	cmd = wParam & ~0xF;	       /* low 4 bits reserved to Windows */
+	switch (cmd) {
 	  case IDM_NEW:
 	    if (!midend_process_key(fe->me, 0, 0, 'n'))
 		PostQuitMessage(0);
@@ -1333,6 +1360,92 @@
           case IDM_ABOUT:
 	    about(fe);
             break;
+	  case IDM_LOAD:
+	  case IDM_SAVE:
+	    {
+		OPENFILENAME of;
+		char filename[FILENAME_MAX];
+		int ret;
+
+		memset(&of, 0, sizeof(of));
+		of.hwndOwner = hwnd;
+		of.lpstrFilter = "All Files (*.*)\0*\0\0\0";
+		of.lpstrCustomFilter = NULL;
+		of.nFilterIndex = 1;
+		of.lpstrFile = filename;
+		filename[0] = '\0';
+		of.nMaxFile = lenof(filename);
+		of.lpstrFileTitle = NULL;
+		of.lpstrTitle = (cmd == IDM_SAVE ?
+				 "Enter name of game file to save" :
+				 "Enter name of saved game file to load");
+		of.Flags = 0;
+#ifdef OPENFILENAME_SIZE_VERSION_400
+		of.lStructSize = OPENFILENAME_SIZE_VERSION_400;
+#else
+		of.lStructSize = sizeof(of);
+#endif
+		of.lpstrInitialDir = NULL;
+
+		if (cmd == IDM_SAVE)
+		    ret = GetSaveFileName(&of);
+		else
+		    ret = GetOpenFileName(&of);
+
+		if (ret) {
+		    if (cmd == IDM_SAVE) {
+			FILE *fp;
+
+			if ((fp = fopen(filename, "r")) != NULL) {
+			    char buf[256 + FILENAME_MAX];
+			    fclose(fp);
+			    /* file exists */
+
+			    sprintf(buf, "Are you sure you want to overwrite"
+				    " the file \"%.*s\"?",
+				    FILENAME_MAX, filename);
+			    if (MessageBox(hwnd, buf, "Question",
+					   MB_YESNO | MB_ICONQUESTION)
+				!= IDYES)
+				break;
+			}
+
+			fp = fopen(filename, "w");
+
+			if (!fp) {
+			    MessageBox(hwnd, "Unable to open save file",
+				       "Error", MB_ICONERROR | MB_OK);
+			    break;
+			}
+
+			midend_serialise(fe->me, savefile_write, fp);
+
+			fclose(fp);
+		    } else {
+			FILE *fp = fopen(filename, "r");
+			char *err;
+
+			if (!fp) {
+			    MessageBox(hwnd, "Unable to open saved game file",
+				       "Error", MB_ICONERROR | MB_OK);
+			    break;
+			}
+
+			err = midend_deserialise(fe->me, savefile_read, fp);
+
+			fclose(fp);
+
+			if (err) {
+			    MessageBox(hwnd, err, "Error", MB_ICONERROR|MB_OK);
+			    break;
+			}
+
+			new_game_size(fe);
+		    }
+		}
+	    }
+
+	    break;
           case IDM_HELPC:
             assert(fe->help_path);
             WinHelp(hwnd, fe->help_path,