chiark / gitweb /
Substantial infrastructure upheaval. I've separated the drawing API
[sgt-puzzles.git] / gtk.c
diff --git a/gtk.c b/gtk.c
index 645b25038c010535942e1d9f65368206b8e805be..6fef7b864338bca15b12ec5cc498505331fa2e44 100644 (file)
--- a/gtk.c
+++ b/gtk.c
@@ -8,6 +8,7 @@
 #include <time.h>
 #include <stdarg.h>
 #include <string.h>
+#include <errno.h>
 
 #include <sys/time.h>
 
 
 #include "puzzles.h"
 
+#if GTK_CHECK_VERSION(2,0,0)
+#define USE_PANGO
+#endif
+
+#ifdef DEBUGGING
+static FILE *debug_fp = NULL;
+
+void dputs(char *buf)
+{
+    if (!debug_fp) {
+        debug_fp = fopen("debug.log", "w");
+    }
+
+    fputs(buf, stderr);
+
+    if (debug_fp) {
+        fputs(buf, debug_fp);
+        fflush(debug_fp);
+    }
+}
+
+void debug_printf(char *fmt, ...)
+{
+    char buf[4096];
+    va_list ap;
+
+    va_start(ap, fmt);
+    vsprintf(buf, fmt, ap);
+    dputs(buf);
+    va_end(ap);
+}
+#endif
+
 /* ----------------------------------------------------------------------
  * Error reporting functions used elsewhere.
  */
@@ -44,7 +78,11 @@ void fatal(char *fmt, ...)
  */
 
 struct font {
+#ifdef USE_PANGO
+    PangoFontDescription *desc;
+#else
     GdkFont *font;
+#endif
     int type;
     int size;
 };
@@ -66,7 +104,7 @@ struct frontend {
     int ncolours;
     GdkColormap *colmap;
     int w, h;
-    midend_data *me;
+    midend *me;
     GdkGC *gc;
     int bbox_l, bbox_r, bbox_u, bbox_d;
     int timer_active, timer_id;
@@ -76,8 +114,12 @@ struct frontend {
     config_item *cfg;
     int cfg_which, cfgret;
     GtkWidget *cfgbox;
-    char *paste_data;
+    void *paste_data;
     int paste_data_len;
+    char *laststatus;
+    int pw, ph;                        /* pixmap size (w, h are area size) */
+    int ox, oy;                        /* offset of pixmap in drawing area */
+    char *filesel_name;
 };
 
 void get_random_seed(void **randseed, int *randseedsize)
@@ -96,20 +138,27 @@ void frontend_default_colour(frontend *fe, float *output)
     output[2] = col.blue / 65535.0;
 }
 
-void status_bar(frontend *fe, char *text)
+void gtk_status_bar(void *handle, char *text)
 {
+    frontend *fe = (frontend *)handle;
     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, rewritten);
-    sfree(rewritten);
+    if (!fe->laststatus || strcmp(rewritten, fe->laststatus)) {
+       gtk_statusbar_pop(GTK_STATUSBAR(fe->statusbar), fe->statusctx);
+       gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx, rewritten);
+       sfree(fe->laststatus);
+       fe->laststatus = rewritten;
+    } else {
+       sfree(rewritten);
+    }
 }
 
-void start_draw(frontend *fe)
+void gtk_start_draw(void *handle)
 {
+    frontend *fe = (frontend *)handle;
     fe->gc = gdk_gc_new(fe->area->window);
     fe->bbox_l = fe->w;
     fe->bbox_r = 0;
@@ -117,8 +166,9 @@ void start_draw(frontend *fe)
     fe->bbox_d = 0;
 }
 
-void clip(frontend *fe, int x, int y, int w, int h)
+void gtk_clip(void *handle, int x, int y, int w, int h)
 {
+    frontend *fe = (frontend *)handle;
     GdkRectangle rect;
 
     rect.x = x;
@@ -129,8 +179,9 @@ void clip(frontend *fe, int x, int y, int w, int h)
     gdk_gc_set_clip_rectangle(fe->gc, &rect);
 }
 
-void unclip(frontend *fe)
+void gtk_unclip(void *handle)
 {
+    frontend *fe = (frontend *)handle;
     GdkRectangle rect;
 
     rect.x = 0;
@@ -141,9 +192,10 @@ void unclip(frontend *fe)
     gdk_gc_set_clip_rectangle(fe->gc, &rect);
 }
 
-void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
-               int align, int colour, char *text)
+void gtk_draw_text(void *handle, int x, int y, int fonttype, int fontsize,
+                  int align, int colour, char *text)
 {
+    frontend *fe = (frontend *)handle;
     int i;
 
     /*
@@ -164,7 +216,7 @@ 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)
+#ifdef USE_PANGO
         /*
          * Use Pango to find the closest match to the requested
          * font.
@@ -202,24 +254,54 @@ void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
                 pango_font_description_set_size(fd, resolution * fontsize);
             }
 #endif
-            fe->fonts[i].font = gdk_font_from_description(fd);
-            pango_font_description_free(fd);
+            fe->fonts[i].desc = fd;
         }
 
-        if (!fe->fonts[i].font)
+#else
+       /*
+        * 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.
+        */
+       fe->fonts[i].font = gdk_font_load(fonttype == FONT_FIXED ?
+                                         "fixed" : "variable");
 #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");
+
     }
 
+    /*
+     * Set the colour.
+     */
+    gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
+
+#ifdef USE_PANGO
+
+    {
+       PangoLayout *layout;
+       PangoRectangle rect;
+
+       /*
+        * Create a layout.
+        */
+       layout = pango_layout_new(gtk_widget_get_pango_context(fe->area));
+       pango_layout_set_font_description(layout, fe->fonts[i].desc);
+       pango_layout_set_text(layout, text, strlen(text));
+       pango_layout_get_pixel_extents(layout, NULL, &rect);
+
+        if (align & ALIGN_VCENTRE)
+            rect.y -= rect.height / 2;
+
+        if (align & ALIGN_HCENTRE)
+            rect.x -= rect.width / 2;
+        else if (align & ALIGN_HRIGHT)
+            rect.x -= rect.width;
+
+       gdk_draw_layout(fe->pixmap, fe->gc, rect.x + x, rect.y + y, layout);
+
+       g_object_unref(layout);
+    }
+
+#else
     /*
      * Find string dimensions and process alignment.
      */
@@ -252,27 +334,31 @@ void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
     }
 
     /*
-     * Set colour and actually draw text.
+     * Actually draw the text.
      */
-    gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
     gdk_draw_string(fe->pixmap, fe->fonts[i].font, fe->gc, x, y, text);
+#endif
+
 }
 
-void draw_rect(frontend *fe, int x, int y, int w, int h, int colour)
+void gtk_draw_rect(void *handle, int x, int y, int w, int h, int colour)
 {
+    frontend *fe = (frontend *)handle;
     gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
     gdk_draw_rectangle(fe->pixmap, fe->gc, 1, x, y, w, h);
 }
 
-void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour)
+void gtk_draw_line(void *handle, int x1, int y1, int x2, int y2, int colour)
 {
+    frontend *fe = (frontend *)handle;
     gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
     gdk_draw_line(fe->pixmap, fe->gc, x1, y1, x2, y2);
 }
 
-void draw_polygon(frontend *fe, int *coords, int npoints,
-                  int fill, int colour)
+void gtk_draw_poly(void *handle, int *coords, int npoints,
+                  int fillcolour, int outlinecolour)
 {
+    frontend *fe = (frontend *)handle;
     GdkPoint *points = snewn(npoints, GdkPoint);
     int i;
 
@@ -281,22 +367,100 @@ void draw_polygon(frontend *fe, int *coords, int npoints,
         points[i].y = coords[i*2+1];
     }
 
-    gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
-    gdk_draw_polygon(fe->pixmap, fe->gc, fill, points, npoints);
+    if (fillcolour >= 0) {
+       gdk_gc_set_foreground(fe->gc, &fe->colours[fillcolour]);
+       gdk_draw_polygon(fe->pixmap, fe->gc, TRUE, points, npoints);
+    }
+    assert(outlinecolour >= 0);
+    gdk_gc_set_foreground(fe->gc, &fe->colours[outlinecolour]);
+    gdk_draw_polygon(fe->pixmap, fe->gc, FALSE, points, npoints);
 
     sfree(points);
 }
 
-void draw_update(frontend *fe, int x, int y, int w, int h)
+void gtk_draw_circle(void *handle, int cx, int cy, int radius,
+                    int fillcolour, int outlinecolour)
 {
+    frontend *fe = (frontend *)handle;
+    if (fillcolour >= 0) {
+       gdk_gc_set_foreground(fe->gc, &fe->colours[fillcolour]);
+       gdk_draw_arc(fe->pixmap, fe->gc, TRUE,
+                    cx - radius, cy - radius,
+                    2 * radius, 2 * radius, 0, 360 * 64);
+    }
+
+    assert(outlinecolour >= 0);
+    gdk_gc_set_foreground(fe->gc, &fe->colours[outlinecolour]);
+    gdk_draw_arc(fe->pixmap, fe->gc, FALSE,
+                cx - radius, cy - radius,
+                2 * radius, 2 * radius, 0, 360 * 64);
+}
+
+struct blitter {
+    GdkPixmap *pixmap;
+    int w, h, x, y;
+};
+
+blitter *gtk_blitter_new(void *handle, int w, int h)
+{
+    /*
+     * We can't create the pixmap right now, because fe->window
+     * might not yet exist. So we just cache w and h and create it
+     * during the firs call to blitter_save.
+     */
+    blitter *bl = snew(blitter);
+    bl->pixmap = NULL;
+    bl->w = w;
+    bl->h = h;
+    return bl;
+}
+
+void gtk_blitter_free(void *handle, blitter *bl)
+{
+    if (bl->pixmap)
+        gdk_pixmap_unref(bl->pixmap);
+    sfree(bl);
+}
+
+void gtk_blitter_save(void *handle, blitter *bl, int x, int y)
+{
+    frontend *fe = (frontend *)handle;
+    if (!bl->pixmap)
+        bl->pixmap = gdk_pixmap_new(fe->area->window, bl->w, bl->h, -1);
+    bl->x = x;
+    bl->y = y;
+    gdk_draw_pixmap(bl->pixmap,
+                    fe->area->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
+                    fe->pixmap,
+                    x, y, 0, 0, bl->w, bl->h);
+}
+
+void gtk_blitter_load(void *handle, blitter *bl, int x, int y)
+{
+    frontend *fe = (frontend *)handle;
+    assert(bl->pixmap);
+    if (x == BLITTER_FROMSAVED && y == BLITTER_FROMSAVED) {
+        x = bl->x;
+        y = bl->y;
+    }
+    gdk_draw_pixmap(fe->pixmap,
+                    fe->area->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
+                    bl->pixmap,
+                    0, 0, x, y, bl->w, bl->h);
+}
+
+void gtk_draw_update(void *handle, int x, int y, int w, int h)
+{
+    frontend *fe = (frontend *)handle;
     if (fe->bbox_l > x  ) fe->bbox_l = x  ;
     if (fe->bbox_r < x+w) fe->bbox_r = x+w;
     if (fe->bbox_u > y  ) fe->bbox_u = y  ;
     if (fe->bbox_d < y+h) fe->bbox_d = y+h;
 }
 
-void end_draw(frontend *fe)
+void gtk_end_draw(void *handle)
 {
+    frontend *fe = (frontend *)handle;
     gdk_gc_unref(fe->gc);
     fe->gc = NULL;
 
@@ -305,15 +469,36 @@ void end_draw(frontend *fe)
                        fe->area->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
                        fe->pixmap,
                         fe->bbox_l, fe->bbox_u,
-                        fe->bbox_l, fe->bbox_u,
+                        fe->ox + fe->bbox_l, fe->oy + fe->bbox_u,
                         fe->bbox_r - fe->bbox_l, fe->bbox_d - fe->bbox_u);
     }
 }
 
+const struct drawing_api gtk_drawing = {
+    gtk_draw_text,
+    gtk_draw_rect,
+    gtk_draw_line,
+    gtk_draw_poly,
+    gtk_draw_circle,
+    gtk_draw_update,
+    gtk_clip,
+    gtk_unclip,
+    gtk_start_draw,
+    gtk_end_draw,
+    gtk_status_bar,
+    gtk_blitter_new,
+    gtk_blitter_free,
+    gtk_blitter_save,
+    gtk_blitter_load,
+    NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */
+    NULL,                             /* line_width */
+};
+
 static void destroy(GtkWidget *widget, gpointer data)
 {
     frontend *fe = (frontend *)data;
     deactivate_timer(fe);
+    midend_free(fe->me);
     gtk_main_quit();
 }
 
@@ -381,17 +566,18 @@ static gint button_event(GtkWidget *widget, GdkEventButton *event,
 
     if (event->button == 2 || (event->state & GDK_SHIFT_MASK))
        button = MIDDLE_BUTTON;
+    else if (event->button == 3 || (event->state & GDK_MOD1_MASK))
+       button = RIGHT_BUTTON;
     else if (event->button == 1)
        button = LEFT_BUTTON;
-    else if (event->button == 3)
-       button = RIGHT_BUTTON;
     else
        return FALSE;                  /* don't even know what button! */
 
     if (event->type == GDK_BUTTON_RELEASE)
         button += LEFT_RELEASE - LEFT_BUTTON;
 
-    if (!midend_process_key(fe->me, event->x, event->y, button))
+    if (!midend_process_key(fe->me, event->x - fe->ox,
+                            event->y - fe->oy, button))
        gtk_widget_destroy(fe->window);
 
     return TRUE;
@@ -415,7 +601,8 @@ static gint motion_event(GtkWidget *widget, GdkEventMotion *event,
     else
        return FALSE;                  /* don't even know what button! */
 
-    if (!midend_process_key(fe->me, event->x, event->y, button))
+    if (!midend_process_key(fe->me, event->x - fe->ox,
+                            event->y - fe->oy, button))
        gtk_widget_destroy(fe->window);
 
     return TRUE;
@@ -430,7 +617,7 @@ static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
        gdk_draw_pixmap(widget->window,
                        widget->style->fg_gc[GTK_WIDGET_STATE(widget)],
                        fe->pixmap,
-                       event->area.x, event->area.y,
+                       event->area.x - fe->ox, event->area.y - fe->oy,
                        event->area.x, event->area.y,
                        event->area.width, event->area.height);
     }
@@ -457,18 +644,27 @@ static gint configure_area(GtkWidget *widget,
 {
     frontend *fe = (frontend *)data;
     GdkGC *gc;
+    int x, y;
 
     if (fe->pixmap)
         gdk_pixmap_unref(fe->pixmap);
 
-    fe->pixmap = gdk_pixmap_new(widget->window, fe->w, fe->h, -1);
+    x = fe->w = event->width;
+    y = fe->h = event->height;
+    midend_size(fe->me, &x, &y, TRUE);
+    fe->pw = x;
+    fe->ph = y;
+    fe->ox = (fe->w - fe->pw) / 2;
+    fe->oy = (fe->h - fe->ph) / 2;
+
+    fe->pixmap = gdk_pixmap_new(widget->window, fe->pw, fe->ph, -1);
 
     gc = gdk_gc_new(fe->area->window);
     gdk_gc_set_foreground(gc, &fe->colours[0]);
-    gdk_draw_rectangle(fe->pixmap, gc, 1, 0, 0, fe->w, fe->h);
+    gdk_draw_rectangle(fe->pixmap, gc, 1, 0, 0, fe->pw, fe->ph);
     gdk_gc_unref(gc);
 
-    midend_redraw(fe->me);
+    midend_force_redraw(fe->me);
 
     return TRUE;
 }
@@ -492,6 +688,8 @@ static gint timer_func(gpointer data)
 
 void deactivate_timer(frontend *fe)
 {
+    if (!fe)
+       return;                        /* can happen due to --generate */
     if (fe->timer_active)
         gtk_timeout_remove(fe->timer_id);
     fe->timer_active = FALSE;
@@ -499,6 +697,8 @@ void deactivate_timer(frontend *fe)
 
 void activate_timer(frontend *fe)
 {
+    if (!fe)
+       return;                        /* can happen due to --generate */
     if (!fe->timer_active) {
         fe->timer_id = gtk_timeout_add(20, timer_func, fe);
        gettimeofday(&fe->last_time, NULL);
@@ -511,8 +711,15 @@ static void window_destroy(GtkWidget *widget, gpointer data)
     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));
 }
 
@@ -531,9 +738,14 @@ static int win_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data)
     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);
@@ -546,28 +758,54 @@ void message_box(GtkWidget *parent, char *title, char *msg, int centre)
     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);
+    /* 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)
@@ -792,7 +1030,7 @@ static int get_config(frontend *fe, int which)
     gtk_window_set_modal(GTK_WINDOW(fe->cfgbox), TRUE);
     gtk_window_set_transient_for(GTK_WINDOW(fe->cfgbox),
                                 GTK_WINDOW(fe->window));
-    //set_transient_window_pos(fe->window, fe->cfgbox);
+    /* set_transient_window_pos(fe->window, fe->cfgbox); */
     gtk_widget_show(fe->cfgbox);
     gtk_main();
 
@@ -810,6 +1048,34 @@ static void menu_key_event(GtkMenuItem *menuitem, gpointer data)
        gtk_widget_destroy(fe->window);
 }
 
+static void get_size(frontend *fe, int *px, int *py)
+{
+    int x, y;
+
+    /*
+     * Currently I don't want to make the GTK port scale large
+     * puzzles to fit on the screen. This is because X does permit
+     * extremely large windows and many window managers provide a
+     * means of navigating round them, and the users I consulted
+     * before deciding said that they'd rather have enormous puzzle
+     * windows spanning multiple screen pages than have them
+     * shrunk. I could change my mind later or introduce
+     * configurability; this would be the place to do so, by
+     * replacing the initial values of x and y with the screen
+     * dimensions.
+     */
+    x = INT_MAX;
+    y = INT_MAX;
+    midend_size(fe->me, &x, &y, FALSE);
+    *px = x;
+    *py = y;
+}
+
+#if !GTK_CHECK_VERSION(2,0,0)
+#define gtk_window_resize(win, x, y) \
+       gdk_window_resize(GTK_WIDGET(win)->window, x, y)
+#endif
+
 static void menu_preset_event(GtkMenuItem *menuitem, gpointer data)
 {
     frontend *fe = (frontend *)data;
@@ -819,10 +1085,15 @@ static void menu_preset_event(GtkMenuItem *menuitem, gpointer data)
 
     midend_set_params(fe->me, params);
     midend_new_game(fe->me);
-    midend_size(fe->me, &x, &y);
-    gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
+    get_size(fe, &x, &y);
     fe->w = x;
     fe->h = y;
+    gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
+    {
+        GtkRequisition req;
+        gtk_widget_size_request(GTK_WIDGET(fe->window), &req);
+        gtk_window_resize(GTK_WINDOW(fe->window), req.width, req.height);
+    }
 }
 
 GdkAtom compound_text_atom, utf8_string_atom;
@@ -830,6 +1101,8 @@ int paste_initialised = FALSE;
 
 void init_paste()
 {
+    unsigned char empty[] = { 0 };
+
     if (paste_initialised)
        return;
 
@@ -843,21 +1116,21 @@ void init_paste()
      * 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);
+                   XA_CUT_BUFFER0, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER1, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER1, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER2, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER2, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER3, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER3, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER4, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER4, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER5, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER5, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER6, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER6, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER7, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER7, XA_STRING, 8, PropModeAppend, empty, 0);
 }
 
 /* Store data in a cut-buffer. */
@@ -932,6 +1205,137 @@ static void menu_copy_event(GtkMenuItem *menuitem, gpointer data)
     }
 }
 
+static void filesel_ok(GtkButton *button, gpointer data)
+{
+    frontend *fe = (frontend *)data;
+
+    gpointer filesel = gtk_object_get_data(GTK_OBJECT(button), "user-data");
+
+    const char *name =
+        gtk_file_selection_get_filename(GTK_FILE_SELECTION(filesel));
+
+    fe->filesel_name = dupstr(name);
+}
+
+static char *file_selector(frontend *fe, char *title, int save)
+{
+    GtkWidget *filesel =
+        gtk_file_selection_new(title);
+
+    fe->filesel_name = NULL;
+
+    gtk_window_set_modal(GTK_WINDOW(filesel), TRUE);
+    gtk_object_set_data
+        (GTK_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "user-data",
+         (gpointer)filesel);
+    gtk_signal_connect
+        (GTK_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "clicked",
+         GTK_SIGNAL_FUNC(filesel_ok), fe);
+    gtk_signal_connect_object
+        (GTK_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "clicked",
+         GTK_SIGNAL_FUNC(gtk_widget_destroy), (gpointer)filesel);
+    gtk_signal_connect_object
+        (GTK_OBJECT(GTK_FILE_SELECTION(filesel)->cancel_button), "clicked",
+         GTK_SIGNAL_FUNC(gtk_widget_destroy), (gpointer)filesel);
+    gtk_signal_connect(GTK_OBJECT(filesel), "destroy",
+                       GTK_SIGNAL_FUNC(window_destroy), NULL);
+    gtk_widget_show(filesel);
+    gtk_window_set_transient_for(GTK_WINDOW(filesel), GTK_WINDOW(fe->window));
+    gtk_main();
+
+    return fe->filesel_name;
+}
+
+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 void menu_save_event(GtkMenuItem *menuitem, gpointer data)
+{
+    frontend *fe = (frontend *)data;
+    char *name;
+
+    name = file_selector(fe, "Enter name of game file to save", TRUE);
+
+    if (name) {
+        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) {
+            error_box(fe->window, "Unable to open save file");
+            return;
+        }
+
+        midend_serialise(fe->me, savefile_write, fp);
+
+        fclose(fp);
+    }
+}
+
+static void menu_load_event(GtkMenuItem *menuitem, gpointer data)
+{
+    frontend *fe = (frontend *)data;
+    char *name, *err;
+    int x, y;
+
+    name = file_selector(fe, "Enter name of saved game file to load", FALSE);
+
+    if (name) {
+        FILE *fp = fopen(name, "r");
+        sfree(name);
+
+        if (!fp) {
+            error_box(fe->window, "Unable to open saved game file");
+            return;
+        }
+
+        err = midend_deserialise(fe->me, savefile_read, fp);
+
+        fclose(fp);
+
+        if (err) {
+            error_box(fe->window, err);
+            return;
+        }
+
+        get_size(fe, &x, &y);
+        fe->w = x;
+        fe->h = y;
+        gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
+        {
+            GtkRequisition req;
+            gtk_widget_size_request(GTK_WIDGET(fe->window), &req);
+            gtk_window_resize(GTK_WINDOW(fe->window), req.width, req.height);
+        }
+
+    }
+}
+
 static void menu_solve_event(GtkMenuItem *menuitem, gpointer data)
 {
     frontend *fe = (frontend *)data;
@@ -961,10 +1365,15 @@ static void menu_config_event(GtkMenuItem *menuitem, gpointer data)
        return;
 
     midend_new_game(fe->me);
-    midend_size(fe->me, &x, &y);
-    gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
+    get_size(fe, &x, &y);
     fe->w = x;
     fe->h = y;
+    gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
+    {
+        GtkRequisition req;
+        gtk_widget_size_request(GTK_WIDGET(fe->window), &req);
+        gtk_window_resize(GTK_WINDOW(fe->window), req.width, req.height);
+    }
 }
 
 static void menu_about_event(GtkMenuItem *menuitem, gpointer data)
@@ -979,7 +1388,7 @@ static void menu_about_event(GtkMenuItem *menuitem, gpointer data)
            "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,
@@ -1002,25 +1411,56 @@ static void add_menu_separator(GtkContainer *cont)
     gtk_widget_show(menuitem);
 }
 
-static frontend *new_window(char *game_id, char **error)
+static frontend *new_window(char *arg, char **error)
 {
     frontend *fe;
     GtkBox *vbox;
     GtkWidget *menubar, *menu, *menuitem;
     int x, y, n;
+    char errbuf[1024];
 
     fe = snew(frontend);
 
-    fe->me = midend_new(fe, &thegame);
-    if (game_id) {
-        *error = midend_game_id(fe->me, game_id);
-        if (*error) {
-            midend_free(fe->me);
-            sfree(fe);
-            return NULL;
+    fe->timer_active = FALSE;
+    fe->timer_id = -1;
+
+    fe->me = midend_new(fe, &thegame, &gtk_drawing, fe);
+
+    if (arg) {
+       char *err;
+
+       errbuf[0] = '\0';
+
+       /*
+        * Try treating the argument as a game ID.
+        */
+        err = midend_game_id(fe->me, arg);
+        if (!err) {
+           /*
+            * It's a valid game ID.
+            */
+           midend_new_game(fe->me);
+       } else {
+           FILE *fp = fopen(arg, "r");
+           if (!fp) {
+               sprintf(errbuf, "Supplied argument is neither a game ID (%.400s)"
+                       " nor a save file (%.400s)", err, strerror(errno));
+           } else {
+               err = midend_deserialise(fe->me, savefile_read, fp);
+               sprintf(errbuf, "%.800s", err);
+               fclose(fp);
+           }
         }
+       if (*errbuf) {
+           *error = dupstr(errbuf);
+           midend_free(fe->me);
+           sfree(fe);
+           return NULL;
+       }
+
+    } else {
+       midend_new_game(fe->me);
     }
-    midend_new_game(fe->me);
 
     fe->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
     gtk_window_set_title(GTK_WINDOW(fe->window), thegame.name);
@@ -1104,6 +1544,17 @@ static frontend *new_window(char *game_id, char **error)
        }
     }
 
+    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');
@@ -1189,18 +1640,18 @@ static frontend *new_window(char *game_id, char **error)
        fe->statusbar = NULL;
 
     fe->area = gtk_drawing_area_new();
-    midend_size(fe->me, &x, &y);
+    get_size(fe, &x, &y);
     gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
     fe->w = x;
     fe->h = y;
 
-    gtk_box_pack_end(vbox, fe->area, FALSE, FALSE, 0);
+    gtk_box_pack_end(vbox, fe->area, TRUE, TRUE, 0);
 
     fe->pixmap = NULL;
     fe->fonts = NULL;
     fe->nfonts = fe->fontsize = 0;
 
-    fe->timer_active = FALSE;
+    fe->laststatus = NULL;
 
     fe->paste_data = NULL;
     fe->paste_data_len = 0;
@@ -1234,18 +1685,139 @@ static frontend *new_window(char *game_id, char **error)
     gtk_widget_show(fe->area);
     gtk_widget_show(fe->window);
 
+    gdk_window_set_background(fe->area->window, &fe->colours[0]);
+    gdk_window_set_background(fe->window->window, &fe->colours[0]);
+
     return fe;
 }
 
+char *fgetline(FILE *fp)
+{
+    char *ret = snewn(512, char);
+    int size = 512, len = 0;
+    while (fgets(ret + len, size - len, fp)) {
+       len += strlen(ret + len);
+       if (ret[len-1] == '\n')
+           break;                     /* got a newline, we're done */
+       size = len + 512;
+       ret = sresize(ret, size, char);
+    }
+    if (len == 0) {                   /* first fgets returned NULL */
+       sfree(ret);
+       return NULL;
+    }
+    ret[len] = '\0';
+    return ret;
+}
+
 int main(int argc, char **argv)
 {
     char *pname = argv[0];
     char *error;
+    int ngenerate = 0, print = FALSE, px = 1, py = 1;
+    int soln = FALSE, colour = FALSE;
+    float scale = 1.0F;
+    char *arg = NULL;
+    int doing_opts = TRUE;
+    int ac = argc;
+    char **av = argv;
+    char errbuf[500];
 
-    if (argc > 1 && !strcmp(argv[1], "--version")) {
-       printf("%s, from Simon Tatham's Portable Puzzle Collection\n%s\n",
-              thegame.name, ver);
-       return 0;
+    /*
+     * Command line parsing in this function is rather fiddly,
+     * because GTK wants to have a go at argc/argv _first_ - and
+     * yet we can't let it, because gtk_init() will bomb out if it
+     * can't open an X display, whereas in fact we want to permit
+     * our --generate and --print modes to run without an X
+     * display.
+     * 
+     * So what we do is:
+     *         - we parse the command line ourselves, without modifying
+     *           argc/argv
+     *         - if we encounter an error which might plausibly be the
+     *           result of a GTK command line (i.e. not detailed errors in
+     *           particular options of ours) we store the error message
+     *           and terminate parsing.
+     *         - if we got enough out of the command line to know it
+     *           specifies a non-X mode of operation, we either display
+     *           the stored error and return failure, or if there is no
+     *           stored error we do the non-X operation and return
+     *           success.
+     *  - otherwise, we go straight to gtk_init().
+     */
+
+    errbuf[0] = '\0';
+    while (--ac > 0) {
+       char *p = *++av;
+       if (doing_opts && !strcmp(p, "--version")) {
+           printf("%s, from Simon Tatham's Portable Puzzle Collection\n%s\n",
+                  thegame.name, ver);
+           return 0;
+       } else if (doing_opts && !strcmp(p, "--generate")) {
+           if (--ac > 0) {
+               ngenerate = atoi(*++av);
+               if (!ngenerate) {
+                   fprintf(stderr, "%s: '--generate' expected a number\n",
+                           pname);
+                   return 1;
+               }
+           } else
+               ngenerate = 1;
+       } else if (doing_opts && !strcmp(p, "--print")) {
+           if (!thegame.can_print) {
+               fprintf(stderr, "%s: this game does not support printing\n",
+                       pname);
+               return 1;
+           }
+           print = TRUE;
+           if (--ac > 0) {
+               char *dim = *++av;
+               if (sscanf(dim, "%dx%d", &px, &py) != 2) {
+                   fprintf(stderr, "%s: unable to parse argument '%s' to "
+                           "'--print'\n", pname, dim);
+                   return 1;
+               }
+           } else {
+               px = py = 1;
+           }
+       } else if (doing_opts && !strcmp(p, "--scale")) {
+           if (--ac > 0) {
+               scale = atof(*++av);
+           } else {
+               fprintf(stderr, "%s: no argument supplied to '--scale'\n",
+                       pname);
+               return 1;
+           }
+       } else if (doing_opts && (!strcmp(p, "--with-solutions") ||
+                                 !strcmp(p, "--with-solution") ||
+                                 !strcmp(p, "--with-solns") ||
+                                 !strcmp(p, "--with-soln") ||
+                                 !strcmp(p, "--solutions") ||
+                                 !strcmp(p, "--solution") ||
+                                 !strcmp(p, "--solns") ||
+                                 !strcmp(p, "--soln"))) {
+           soln = TRUE;
+       } else if (doing_opts && !strcmp(p, "--colour")) {
+           if (!thegame.can_print_in_colour) {
+               fprintf(stderr, "%s: this game does not support colour"
+                       " printing\n", pname);
+               return 1;
+           }
+           colour = TRUE;
+       } else if (doing_opts && !strcmp(p, "--")) {
+           doing_opts = FALSE;
+       } else if (!doing_opts || p[0] != '-') {
+           if (arg) {
+               fprintf(stderr, "%s: more than one argument supplied\n",
+                       pname);
+               return 1;
+           }
+           arg = p;
+       } else {
+           sprintf(errbuf, "%.100s: unrecognised option '%.100s'\n",
+                   pname, p);
+           break;
+       }
     }
 
     /*
@@ -1253,7 +1825,7 @@ int main(int argc, char **argv)
      * 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!)
+     * friendly than Twiddle!)
      * 
      * Usage:
      * 
@@ -1269,51 +1841,96 @@ int main(int argc, char **argv)
      * 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);
+    if (ngenerate > 0 || print) {
+       int i, n = 1;
+       midend *me;
+       char *id;
+       document *doc = NULL;
+
+       if (*errbuf) {
+           fputs(errbuf, stderr);
+           return 1;
+       }
+
+       n = ngenerate;
+
+       me = midend_new(NULL, &thegame, NULL, NULL);
+       i = 0;
+
+       if (print)
+           doc = document_new(px, py, scale);
+
+       /*
+        * In this loop, we either generate a game ID or read one
+        * from stdin depending on whether we're in generate mode;
+        * then we either write it to stdout or print it, depending
+        * on whether we're in print mode. Thus, this loop handles
+        * generate-to-stdout, print-from-stdin and generate-and-
+        * immediately-print modes.
+        * 
+        * (It could also handle a copy-stdin-to-stdout mode,
+        * although there's currently no combination of options
+        * which will cause this loop to be activated in that mode.
+        * It wouldn't be _entirely_ pointless, though, because
+        * stdin could contain bare params strings or random-seed
+        * IDs, and stdout would contain nothing but fully
+        * generated descriptive game IDs.)
+        */
+       while (ngenerate == 0 || i < n) {
+           char *pstr, *err;
+
+           if (ngenerate == 0) {
+               pstr = fgetline(stdin);
+               if (!pstr)
+                   break;
+               pstr[strcspn(pstr, "\r\n")] = '\0';
+           } else {
+               if (arg) {
+                   pstr = snewn(strlen(arg) + 40, char);
+
+                   strcpy(pstr, arg);
+                   if (i > 0 && strchr(arg, '#'))
+                       sprintf(pstr + strlen(pstr), "-%d", i);
+               } else
+                   pstr = NULL;
+           }
+
+           if (pstr) {
+               err = midend_game_id(me, pstr);
+               if (err) {
+                   fprintf(stderr, "%s: error parsing '%s': %s\n",
+                           pname, pstr, err);
+                   return 1;
+               }
+           }
+           sfree(pstr);
+
+           midend_new_game(me);
+
+           if (doc) {
+               err = midend_print_puzzle(me, doc, soln);
+               if (err) {
+                   fprintf(stderr, "%s: error in printing: %s\n", pname, err);
+                   return 1;
+               }
+           } else {
+               id = midend_get_game_id(me);
+               puts(id);
+               sfree(id);
+           }
+
+           i++;
        }
 
-       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);
+       if (doc) {
+           psdata *ps = ps_init(stdout, colour);
+           document_print(doc, ps_drawing_api(ps));
+           document_free(doc);
+           ps_free(ps);
        }
 
+       midend_free(me);
+
        return 0;
     } else {