Added an `interactive' flag to new_game_desc(), which toggles Mines
[sgt/puzzles] / gtk.c
diff --git a/gtk.c b/gtk.c
index 3501f83..645b250 100644 (file)
--- a/gtk.c
+++ b/gtk.c
 #include <gtk/gtk.h>
 #include <gdk/gdkkeysyms.h>
 
+#include <gdk/gdkx.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/Xatom.h>
+
 #include "puzzles.h"
 
 /* ----------------------------------------------------------------------
@@ -71,8 +76,18 @@ struct frontend {
     config_item *cfg;
     int cfg_which, cfgret;
     GtkWidget *cfgbox;
+    char *paste_data;
+    int paste_data_len;
 };
 
+void get_random_seed(void **randseed, int *randseedsize)
+{
+    struct timeval *tvp = snew(struct timeval);
+    gettimeofday(tvp, NULL);
+    *randseed = (void *)tvp;
+    *randseedsize = sizeof(struct timeval);
+}
+
 void frontend_default_colour(frontend *fe, float *output)
 {
     GdkColor col = fe->window->style->bg[GTK_STATE_NORMAL];
@@ -83,10 +98,14 @@ void frontend_default_colour(frontend *fe, float *output)
 
 void status_bar(frontend *fe, char *text)
 {
+    char *rewritten;
+
     assert(fe->statusbar);
 
+    rewritten = midend_rewrite_statusbar(fe->me, text);
     gtk_statusbar_pop(GTK_STATUSBAR(fe->statusbar), fe->statusctx);
-    gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx, text);
+    gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx, rewritten);
+    sfree(rewritten);
 }
 
 void start_draw(frontend *fe)
@@ -145,11 +164,60 @@ void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
         fe->fonts[i].type = fonttype;
         fe->fonts[i].size = fontsize;
 
+#if GTK_CHECK_VERSION(2,0,0)
         /*
-         * FIXME: Really I should make at least _some_ effort to
-         * pick the correct font.
+         * Use Pango to find the closest match to the requested
+         * font.
          */
-        fe->fonts[i].font = gdk_font_load("variable");
+        {
+            PangoFontDescription *fd;
+
+            fd = pango_font_description_new();
+            /* `Monospace' and `Sans' are meta-families guaranteed to exist */
+            pango_font_description_set_family(fd, fonttype == FONT_FIXED ?
+                                              "Monospace" : "Sans");
+            pango_font_description_set_weight(fd, PANGO_WEIGHT_BOLD);
+            /*
+             * I found some online Pango documentation which
+             * described a function called
+             * pango_font_description_set_absolute_size(), which is
+             * _exactly_ what I want here. Unfortunately, none of
+             * my local Pango installations have it (presumably
+             * they're too old), so I'm going to have to hack round
+             * it by figuring out the point size myself. This
+             * limits me to X and probably also breaks in later
+             * Pango installations, so ideally I should add another
+             * CHECK_VERSION type ifdef and use set_absolute_size
+             * where available. All very annoying.
+             */
+#ifdef HAVE_SENSIBLE_ABSOLUTE_SIZE_FUNCTION
+            pango_font_description_set_absolute_size(fd, PANGO_SCALE*fontsize);
+#else
+            {
+                Display *d = GDK_DISPLAY();
+                int s = DefaultScreen(d);
+                double resolution =
+                    (PANGO_SCALE * 72.27 / 25.4) * 
+                    ((double) DisplayWidthMM(d, s) / DisplayWidth (d, s));
+                pango_font_description_set_size(fd, resolution * fontsize);
+            }
+#endif
+            fe->fonts[i].font = gdk_font_from_description(fd);
+            pango_font_description_free(fd);
+        }
+
+        if (!fe->fonts[i].font)
+#endif
+            /*
+             * In GTK 1.2, I don't know of any plausible way to
+             * pick a suitable font, so I'm just going to be
+             * tedious.
+             * 
+             * This is also fallback code called if the Pango
+             * approach fails to find an appropriate font.
+             */
+            fe->fonts[i].font = gdk_font_load(fonttype == FONT_FIXED ?
+                                              "fixed" : "variable");
     }
 
     /*
@@ -158,11 +226,24 @@ void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
     {
         int lb, rb, wid, asc, desc;
 
-        gdk_string_extents(fe->fonts[i].font, text,
+       /*
+        * Measure vertical string extents with respect to the same
+        * string always...
+        */
+        gdk_string_extents(fe->fonts[i].font,
+                          "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
                            &lb, &rb, &wid, &asc, &desc);
         if (align & ALIGN_VCENTRE)
             y += asc - (asc+desc)/2;
 
+       /*
+        * ... but horizontal extents with respect to the provided
+        * string. This means that multiple pieces of text centred
+        * on the same y-coordinate don't have different baselines.
+        */
+        gdk_string_extents(fe->fonts[i].font, text,
+                           &lb, &rb, &wid, &asc, &desc);
+
         if (align & ALIGN_HCENTRE)
             x -= wid / 2;
         else if (align & ALIGN_HRIGHT)
@@ -231,6 +312,8 @@ void end_draw(frontend *fe)
 
 static void destroy(GtkWidget *widget, gpointer data)
 {
+    frontend *fe = (frontend *)data;
+    deactivate_timer(fe);
     gtk_main_quit();
 }
 
@@ -238,32 +321,42 @@ static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
 {
     frontend *fe = (frontend *)data;
     int keyval;
+    int shift = (event->state & GDK_SHIFT_MASK) ? MOD_SHFT : 0;
+    int ctrl = (event->state & GDK_CONTROL_MASK) ? MOD_CTRL : 0;
 
     if (!fe->pixmap)
         return TRUE;
 
-    if (event->string[0] && !event->string[1])
-        keyval = (unsigned char)event->string[0];
-    else if (event->keyval == GDK_Up || event->keyval == GDK_KP_Up ||
-             event->keyval == GDK_KP_8)
-        keyval = CURSOR_UP;
-    else if (event->keyval == GDK_Down || event->keyval == GDK_KP_Down ||
-             event->keyval == GDK_KP_2)
-        keyval = CURSOR_DOWN;
-    else if (event->keyval == GDK_Left || event->keyval == GDK_KP_Left ||
-             event->keyval == GDK_KP_4)
-        keyval = CURSOR_LEFT;
-    else if (event->keyval == GDK_Right || event->keyval == GDK_KP_Right ||
-             event->keyval == GDK_KP_6)
-        keyval = CURSOR_RIGHT;
+    if (event->keyval == GDK_Up)
+        keyval = shift | ctrl | CURSOR_UP;
+    else if (event->keyval == GDK_KP_Up || event->keyval == GDK_KP_8)
+       keyval = MOD_NUM_KEYPAD | '8';
+    else if (event->keyval == GDK_Down)
+        keyval = shift | ctrl | CURSOR_DOWN;
+    else if (event->keyval == GDK_KP_Down || event->keyval == GDK_KP_2)
+       keyval = MOD_NUM_KEYPAD | '2';
+    else if (event->keyval == GDK_Left)
+        keyval = shift | ctrl | CURSOR_LEFT;
+    else if (event->keyval == GDK_KP_Left || event->keyval == GDK_KP_4)
+       keyval = MOD_NUM_KEYPAD | '4';
+    else if (event->keyval == GDK_Right)
+        keyval = shift | ctrl | CURSOR_RIGHT;
+    else if (event->keyval == GDK_KP_Right || event->keyval == GDK_KP_6)
+       keyval = MOD_NUM_KEYPAD | '6';
     else if (event->keyval == GDK_KP_Home || event->keyval == GDK_KP_7)
-        keyval = CURSOR_UP_LEFT;
+        keyval = MOD_NUM_KEYPAD | '7';
     else if (event->keyval == GDK_KP_End || event->keyval == GDK_KP_1)
-        keyval = CURSOR_DOWN_LEFT;
+        keyval = MOD_NUM_KEYPAD | '1';
     else if (event->keyval == GDK_KP_Page_Up || event->keyval == GDK_KP_9)
-        keyval = CURSOR_UP_RIGHT;
+        keyval = MOD_NUM_KEYPAD | '9';
     else if (event->keyval == GDK_KP_Page_Down || event->keyval == GDK_KP_3)
-        keyval = CURSOR_DOWN_RIGHT;
+        keyval = MOD_NUM_KEYPAD | '3';
+    else if (event->keyval == GDK_KP_Insert || event->keyval == GDK_KP_0)
+        keyval = MOD_NUM_KEYPAD | '0';
+    else if (event->keyval == GDK_KP_Begin || event->keyval == GDK_KP_5)
+        keyval = MOD_NUM_KEYPAD | '5';
+    else if (event->string[0] && !event->string[1])
+        keyval = (unsigned char)event->string[0];
     else
         keyval = -1;
 
@@ -438,7 +531,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;
 
@@ -451,7 +544,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),
@@ -461,6 +554,8 @@ void error_box(GtkWidget *parent, char *msg)
     gtk_window_set_default(GTK_WINDOW(window), ok);
     gtk_signal_connect(GTK_OBJECT(ok), "clicked",
                        GTK_SIGNAL_FUNC(errmsg_button_clicked), window);
+    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);
@@ -470,6 +565,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;
@@ -725,6 +825,131 @@ static void menu_preset_event(GtkMenuItem *menuitem, gpointer data)
     fe->h = y;
 }
 
+GdkAtom compound_text_atom, utf8_string_atom;
+int paste_initialised = FALSE;
+
+void init_paste()
+{
+    if (paste_initialised)
+       return;
+
+    if (!compound_text_atom)
+        compound_text_atom = gdk_atom_intern("COMPOUND_TEXT", FALSE);
+    if (!utf8_string_atom)
+        utf8_string_atom = gdk_atom_intern("UTF8_STRING", FALSE);
+
+    /*
+     * Ensure that all the cut buffers exist - according to the
+     * ICCCM, we must do this before we start using cut buffers.
+     */
+    XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
+                   XA_CUT_BUFFER0, XA_STRING, 8, PropModeAppend, "", 0);
+    XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
+                   XA_CUT_BUFFER1, XA_STRING, 8, PropModeAppend, "", 0);
+    XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
+                   XA_CUT_BUFFER2, XA_STRING, 8, PropModeAppend, "", 0);
+    XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
+                   XA_CUT_BUFFER3, XA_STRING, 8, PropModeAppend, "", 0);
+    XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
+                   XA_CUT_BUFFER4, XA_STRING, 8, PropModeAppend, "", 0);
+    XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
+                   XA_CUT_BUFFER5, XA_STRING, 8, PropModeAppend, "", 0);
+    XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
+                   XA_CUT_BUFFER6, XA_STRING, 8, PropModeAppend, "", 0);
+    XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
+                   XA_CUT_BUFFER7, XA_STRING, 8, PropModeAppend, "", 0);
+}
+
+/* Store data in a cut-buffer. */
+void store_cutbuffer(char *ptr, int len)
+{
+    /* ICCCM says we must rotate the buffers before storing to buffer 0. */
+    XRotateBuffers(GDK_DISPLAY(), 1);
+    XStoreBytes(GDK_DISPLAY(), ptr, len);
+}
+
+void write_clip(frontend *fe, char *data)
+{
+    init_paste();
+
+    if (fe->paste_data)
+       sfree(fe->paste_data);
+
+    /*
+     * For this simple application we can safely assume that the
+     * data passed to this function is pure ASCII, which means we
+     * can return precisely the same stuff for types STRING,
+     * COMPOUND_TEXT or UTF8_STRING.
+     */
+
+    fe->paste_data = data;
+    fe->paste_data_len = strlen(data);
+
+    store_cutbuffer(fe->paste_data, fe->paste_data_len);
+
+    if (gtk_selection_owner_set(fe->area, GDK_SELECTION_PRIMARY,
+                               CurrentTime)) {
+       gtk_selection_add_target(fe->area, GDK_SELECTION_PRIMARY,
+                                GDK_SELECTION_TYPE_STRING, 1);
+       gtk_selection_add_target(fe->area, GDK_SELECTION_PRIMARY,
+                                compound_text_atom, 1);
+       gtk_selection_add_target(fe->area, GDK_SELECTION_PRIMARY,
+                                utf8_string_atom, 1);
+    }
+}
+
+void selection_get(GtkWidget *widget, GtkSelectionData *seldata,
+                  guint info, guint time_stamp, gpointer data)
+{
+    frontend *fe = (frontend *)data;
+    gtk_selection_data_set(seldata, seldata->target, 8,
+                          fe->paste_data, fe->paste_data_len);
+}
+
+gint selection_clear(GtkWidget *widget, GdkEventSelection *seldata,
+                    gpointer data)
+{
+    frontend *fe = (frontend *)data;
+
+    if (fe->paste_data)
+       sfree(fe->paste_data);
+    fe->paste_data = NULL;
+    fe->paste_data_len = 0;
+    return TRUE;
+}
+
+static void menu_copy_event(GtkMenuItem *menuitem, gpointer data)
+{
+    frontend *fe = (frontend *)data;
+    char *text;
+
+    text = midend_text_format(fe->me);
+
+    if (text) {
+       write_clip(fe, text);
+    } else {
+       gdk_beep();
+    }
+}
+
+static void menu_solve_event(GtkMenuItem *menuitem, gpointer data)
+{
+    frontend *fe = (frontend *)data;
+    char *msg;
+
+    msg = midend_solve(fe->me);
+
+    if (msg)
+       error_box(fe->window, msg);
+}
+
+static void menu_restart_event(GtkMenuItem *menuitem, gpointer data)
+{
+    frontend *fe = (frontend *)data;
+
+    midend_restart_game(fe->me);
+}
+
 static void menu_config_event(GtkMenuItem *menuitem, gpointer data)
 {
     frontend *fe = (frontend *)data;
@@ -742,6 +967,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)
 {
@@ -768,14 +1008,12 @@ static frontend *new_window(char *game_id, char **error)
     GtkBox *vbox;
     GtkWidget *menubar, *menu, *menuitem;
     int x, y, n;
-    time_t t;
 
     fe = snew(frontend);
 
-    time(&t);
-    fe->me = midend_new(fe, &t, sizeof(t));
+    fe->me = midend_new(fe, &thegame);
     if (game_id) {
-        *error = midend_game_id(fe->me, game_id, FALSE);
+        *error = midend_game_id(fe->me, game_id);
         if (*error) {
             midend_free(fe->me);
             sfree(fe);
@@ -785,7 +1023,7 @@ static frontend *new_window(char *game_id, char **error)
     midend_new_game(fe->me);
 
     fe->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
-    gtk_window_set_title(GTK_WINDOW(fe->window), game_name);
+    gtk_window_set_title(GTK_WINDOW(fe->window), thegame.name);
 #if 0
     gtk_window_set_resizable(GTK_WINDOW(fe->window), FALSE);
 #else
@@ -807,17 +1045,30 @@ static frontend *new_window(char *game_id, char **error)
     gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
 
     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "New", 'n');
-    add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Restart", 'r');
+
+    menuitem = gtk_menu_item_new_with_label("Restart");
+    gtk_container_add(GTK_CONTAINER(menu), menuitem);
+    gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
+                      GTK_SIGNAL_FUNC(menu_restart_event), fe);
+    gtk_widget_show(menuitem);
 
     menuitem = gtk_menu_item_new_with_label("Specific...");
     gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
+                       GINT_TO_POINTER(CFG_DESC));
+    gtk_container_add(GTK_CONTAINER(menu), menuitem);
+    gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
+                      GTK_SIGNAL_FUNC(menu_config_event), fe);
+    gtk_widget_show(menuitem);
+
+    menuitem = gtk_menu_item_new_with_label("Random Seed...");
+    gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
                        GINT_TO_POINTER(CFG_SEED));
     gtk_container_add(GTK_CONTAINER(menu), menuitem);
     gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
                       GTK_SIGNAL_FUNC(menu_config_event), fe);
     gtk_widget_show(menuitem);
 
-    if ((n = midend_num_presets(fe->me)) > 0 || game_can_configure) {
+    if ((n = midend_num_presets(fe->me)) > 0 || thegame.can_configure) {
         GtkWidget *submenu;
         int i;
 
@@ -842,7 +1093,7 @@ static frontend *new_window(char *game_id, char **error)
             gtk_widget_show(menuitem);
         }
 
-       if (game_can_configure) {
+       if (thegame.can_configure) {
             menuitem = gtk_menu_item_new_with_label("Custom...");
             gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
                                GPOINTER_TO_INT(CFG_SETTINGS));
@@ -856,9 +1107,38 @@ static frontend *new_window(char *game_id, char **error)
     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');
+    if (thegame.can_format_as_text) {
+       add_menu_separator(GTK_CONTAINER(menu));
+       menuitem = gtk_menu_item_new_with_label("Copy");
+       gtk_container_add(GTK_CONTAINER(menu), menuitem);
+       gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
+                          GTK_SIGNAL_FUNC(menu_copy_event), fe);
+       gtk_widget_show(menuitem);
+    }
+    if (thegame.can_solve) {
+       add_menu_separator(GTK_CONTAINER(menu));
+       menuitem = gtk_menu_item_new_with_label("Solve");
+       gtk_container_add(GTK_CONTAINER(menu), menuitem);
+       gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
+                          GTK_SIGNAL_FUNC(menu_solve_event), fe);
+       gtk_widget_show(menuitem);
+    }
     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;
@@ -922,6 +1202,9 @@ static frontend *new_window(char *game_id, char **error)
 
     fe->timer_active = FALSE;
 
+    fe->paste_data = NULL;
+    fe->paste_data_len = 0;
+
     gtk_signal_connect(GTK_OBJECT(fe->window), "destroy",
                       GTK_SIGNAL_FUNC(destroy), fe);
     gtk_signal_connect(GTK_OBJECT(fe->window), "key_press_event",
@@ -932,6 +1215,10 @@ static frontend *new_window(char *game_id, char **error)
                       GTK_SIGNAL_FUNC(button_event), fe);
     gtk_signal_connect(GTK_OBJECT(fe->area), "motion_notify_event",
                       GTK_SIGNAL_FUNC(motion_event), fe);
+    gtk_signal_connect(GTK_OBJECT(fe->area), "selection_get",
+                      GTK_SIGNAL_FUNC(selection_get), fe);
+    gtk_signal_connect(GTK_OBJECT(fe->area), "selection_clear_event",
+                      GTK_SIGNAL_FUNC(selection_clear), fe);
     gtk_signal_connect(GTK_OBJECT(fe->area), "expose_event",
                       GTK_SIGNAL_FUNC(expose_area), fe);
     gtk_signal_connect(GTK_OBJECT(fe->window), "map_event",
@@ -955,14 +1242,90 @@ int main(int argc, char **argv)
     char *pname = argv[0];
     char *error;
 
-    gtk_init(&argc, &argv);
-
-    if (!new_window(argc > 1 ? argv[1] : NULL, &error)) {
-        fprintf(stderr, "%s: %s\n", pname, error);
-        return 1;
+    if (argc > 1 && !strcmp(argv[1], "--version")) {
+       printf("%s, from Simon Tatham's Portable Puzzle Collection\n%s\n",
+              thegame.name, ver);
+       return 0;
     }
 
-    gtk_main();
+    /*
+     * Special standalone mode for generating puzzle IDs on the
+     * command line. Useful for generating puzzles to be printed
+     * out and solved offline (for puzzles where that even makes
+     * sense - Solo, for example, is a lot more pencil-and-paper
+     * friendly than Net!)
+     * 
+     * Usage:
+     * 
+     *   <puzzle-name> --generate [<n> [<params>]]
+     * 
+     * <n>, if present, is the number of puzzle IDs to generate.
+     * <params>, if present, is the same type of parameter string
+     * you would pass to the puzzle when running it in GUI mode,
+     * including optional extras such as the expansion factor in
+     * Rectangles and the difficulty level in Solo.
+     * 
+     * If you specify <params>, you must also specify <n> (although
+     * you may specify it to be 1). Sorry; that was the
+     * simplest-to-parse command-line syntax I came up with.
+     */
+    if (argc > 1 && !strcmp(argv[1], "--generate")) {
+       int n = 1;
+       char *params = NULL, *seed = NULL;
+       game_params *par;
+       random_state *rs;
+       char *parstr;
+
+       if (argc > 2)
+           n = atoi(argv[2]);
+       if (argc > 3)
+           params = argv[3];
+
+        par = thegame.default_params();
+       if (params) {
+            if ( (seed = strchr(params, '#')) != NULL )
+                *seed++ = '\0';
+           thegame.decode_params(par, params);
+        }
+        if ((error = thegame.validate_params(par)) != NULL) {
+           fprintf(stderr, "%s: %s\n", pname, error);
+            return 1;
+        }
+       parstr = thegame.encode_params(par, FALSE);
+
+       {
+           void *seeddata;
+           int seedlen;
+            if (seed) {
+                seeddata = seed;
+                seedlen = strlen(seed);
+            } else {
+                get_random_seed(&seeddata, &seedlen);
+            }
+           rs = random_init(seeddata, seedlen);
+       }
+
+       while (n-- > 0) {
+           game_aux_info *aux = NULL;
+           char *desc = thegame.new_desc(par, rs, &aux, FALSE);
+           printf("%s:%s\n", parstr, desc);
+           sfree(desc);
+           if (aux)
+               thegame.free_aux_info(aux);
+       }
+
+       return 0;
+    } else {
+
+       gtk_init(&argc, &argv);
+
+       if (!new_window(argc > 1 ? argv[1] : NULL, &error)) {
+           fprintf(stderr, "%s: %s\n", pname, error);
+           return 1;
+       }
+
+       gtk_main();
+    }
 
     return 0;
 }