chiark / gitweb /
0b3355e9b8617952e69add95926168582439f731
[sgt-puzzles.git] / gtk.c
1 /*
2  * gtk.c: GTK front end for my puzzle collection.
3  */
4
5 #include <stdio.h>
6 #include <assert.h>
7 #include <stdlib.h>
8 #include <time.h>
9 #include <stdarg.h>
10 #include <string.h>
11 #include <errno.h>
12 #include <math.h>
13
14 #include <sys/time.h>
15
16 #include <gtk/gtk.h>
17 #include <gdk/gdkkeysyms.h>
18
19 #include <gdk-pixbuf/gdk-pixbuf.h>
20
21 #include <gdk/gdkx.h>
22 #include <X11/Xlib.h>
23 #include <X11/Xutil.h>
24 #include <X11/Xatom.h>
25
26 #include "puzzles.h"
27
28 #if GTK_CHECK_VERSION(2,0,0)
29 # define USE_PANGO
30 # ifdef PANGO_VERSION_CHECK
31 #  if PANGO_VERSION_CHECK(1,8,0)
32 #   define HAVE_SENSIBLE_ABSOLUTE_SIZE_FUNCTION
33 #  endif
34 # endif
35 #endif
36 #if !GTK_CHECK_VERSION(2,4,0)
37 # define OLD_FILESEL
38 #endif
39 #if GTK_CHECK_VERSION(2,8,0)
40 # define USE_CAIRO
41 #endif
42
43 #ifdef DEBUGGING
44 static FILE *debug_fp = NULL;
45
46 void dputs(char *buf)
47 {
48     if (!debug_fp) {
49         debug_fp = fopen("debug.log", "w");
50     }
51
52     fputs(buf, stderr);
53
54     if (debug_fp) {
55         fputs(buf, debug_fp);
56         fflush(debug_fp);
57     }
58 }
59
60 void debug_printf(char *fmt, ...)
61 {
62     char buf[4096];
63     va_list ap;
64
65     va_start(ap, fmt);
66     vsprintf(buf, fmt, ap);
67     dputs(buf);
68     va_end(ap);
69 }
70 #endif
71
72 /* ----------------------------------------------------------------------
73  * Error reporting functions used elsewhere.
74  */
75
76 void fatal(char *fmt, ...)
77 {
78     va_list ap;
79
80     fprintf(stderr, "fatal error: ");
81
82     va_start(ap, fmt);
83     vfprintf(stderr, fmt, ap);
84     va_end(ap);
85
86     fprintf(stderr, "\n");
87     exit(1);
88 }
89
90 /* ----------------------------------------------------------------------
91  * GTK front end to puzzles.
92  */
93
94 static void changed_preset(frontend *fe);
95
96 struct font {
97 #ifdef USE_PANGO
98     PangoFontDescription *desc;
99 #else
100     GdkFont *font;
101 #endif
102     int type;
103     int size;
104 };
105
106 /*
107  * This structure holds all the data relevant to a single window.
108  * In principle this would allow us to open multiple independent
109  * puzzle windows, although I can't currently see any real point in
110  * doing so. I'm just coding cleanly because there's no
111  * particularly good reason not to.
112  */
113 struct frontend {
114     GtkWidget *window;
115     GtkAccelGroup *accelgroup;
116     GtkWidget *area;
117     GtkWidget *statusbar;
118     guint statusctx;
119     int w, h;
120     midend *me;
121 #ifdef USE_CAIRO
122     const float *colours;
123     cairo_t *cr;
124     cairo_surface_t *image;
125     GdkPixmap *pixmap;
126 #else
127     GdkPixmap *pixmap;
128     GdkGC *gc;
129     GdkColor *colours;
130     GdkColormap *colmap;
131 #endif
132     int ncolours;
133     int bbox_l, bbox_r, bbox_u, bbox_d;
134     int timer_active, timer_id;
135     struct timeval last_time;
136     struct font *fonts;
137     int nfonts, fontsize;
138     config_item *cfg;
139     int cfg_which, cfgret;
140     GtkWidget *cfgbox;
141     void *paste_data;
142     int paste_data_len;
143     int pw, ph;                        /* pixmap size (w, h are area size) */
144     int ox, oy;                        /* offset of pixmap in drawing area */
145 #ifdef OLD_FILESEL
146     char *filesel_name;
147 #endif
148     GSList *preset_radio;
149     int n_preset_menu_items;
150     int preset_threaded;
151     GtkWidget *preset_custom;
152     GtkWidget *copy_menu_item;
153 };
154
155 struct blitter {
156 #ifdef USE_CAIRO
157     cairo_surface_t *image;
158 #else
159     GdkPixmap *pixmap;
160 #endif
161     int w, h, x, y;
162 };
163
164 void get_random_seed(void **randseed, int *randseedsize)
165 {
166     struct timeval *tvp = snew(struct timeval);
167     gettimeofday(tvp, NULL);
168     *randseed = (void *)tvp;
169     *randseedsize = sizeof(struct timeval);
170 }
171
172 void frontend_default_colour(frontend *fe, float *output)
173 {
174     GdkColor col = fe->window->style->bg[GTK_STATE_NORMAL];
175     output[0] = col.red / 65535.0;
176     output[1] = col.green / 65535.0;
177     output[2] = col.blue / 65535.0;
178 }
179
180 void gtk_status_bar(void *handle, char *text)
181 {
182     frontend *fe = (frontend *)handle;
183
184     assert(fe->statusbar);
185
186     gtk_statusbar_pop(GTK_STATUSBAR(fe->statusbar), fe->statusctx);
187     gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx, text);
188 }
189
190 /* ----------------------------------------------------------------------
191  * Cairo drawing functions.
192  */
193
194 #ifdef USE_CAIRO
195
196 static void setup_drawing(frontend *fe)
197 {
198     fe->cr = cairo_create(fe->image);
199     cairo_set_antialias(fe->cr, CAIRO_ANTIALIAS_GRAY);
200     cairo_set_line_width(fe->cr, 1.0);
201     cairo_set_line_cap(fe->cr, CAIRO_LINE_CAP_SQUARE);
202     cairo_set_line_join(fe->cr, CAIRO_LINE_JOIN_ROUND);
203 }
204
205 static void teardown_drawing(frontend *fe)
206 {
207     cairo_t *cr;
208
209     cairo_destroy(fe->cr);
210     fe->cr = NULL;
211
212     cr = gdk_cairo_create(fe->pixmap);
213     cairo_set_source_surface(cr, fe->image, 0, 0);
214     cairo_rectangle(cr,
215                     fe->bbox_l - 1,
216                     fe->bbox_u - 1,
217                     fe->bbox_r - fe->bbox_l + 2,
218                     fe->bbox_d - fe->bbox_u + 2);
219     cairo_fill(cr);
220     cairo_destroy(cr);
221 }
222
223 static void snaffle_colours(frontend *fe)
224 {
225     fe->colours = midend_colours(fe->me, &fe->ncolours);
226 }
227
228 static void set_colour(frontend *fe, int colour)
229 {
230     cairo_set_source_rgb(fe->cr,
231                          fe->colours[3*colour + 0],
232                          fe->colours[3*colour + 1],
233                          fe->colours[3*colour + 2]);
234 }
235
236 static void set_window_background(frontend *fe, int colour)
237 {
238     GdkColormap *colmap;
239     GdkColor backg;
240
241     colmap = gdk_colormap_get_system();
242     backg.red = fe->colours[3*colour + 0] * 65535;
243     backg.green = fe->colours[3*colour + 1] * 65535;
244     backg.blue = fe->colours[3*colour + 2] * 65535;
245     if (!gdk_colormap_alloc_color(colmap, &backg, FALSE, FALSE)) {
246         g_error("couldn't allocate background (#%02x%02x%02x)\n",
247                 backg.red >> 8, backg.green >> 8, backg.blue >> 8);
248     }
249     gdk_window_set_background(fe->area->window, &backg);
250     gdk_window_set_background(fe->window->window, &backg);
251 }
252
253 static PangoLayout *make_pango_layout(frontend *fe)
254 {
255     return (pango_cairo_create_layout(fe->cr));
256 }
257
258 static void draw_pango_layout(frontend *fe, PangoLayout *layout,
259                               int x, int y)
260 {
261     cairo_move_to(fe->cr, x, y);
262     pango_cairo_show_layout(fe->cr, layout);
263 }
264
265 static void save_screenshot_png(frontend *fe, const char *screenshot_file)
266 {
267     cairo_surface_write_to_png(fe->image, screenshot_file);
268 }
269
270 static void do_clip(frontend *fe, int x, int y, int w, int h)
271 {
272     cairo_new_path(fe->cr);
273     cairo_rectangle(fe->cr, x, y, w, h);
274     cairo_clip(fe->cr);
275 }
276
277 static void do_unclip(frontend *fe)
278 {
279     cairo_reset_clip(fe->cr);
280 }
281
282 static void do_draw_rect(frontend *fe, int x, int y, int w, int h)
283 {
284     cairo_save(fe->cr);
285     cairo_new_path(fe->cr);
286     cairo_set_antialias(fe->cr, CAIRO_ANTIALIAS_NONE);
287     cairo_rectangle(fe->cr, x, y, w, h);
288     cairo_fill(fe->cr);
289     cairo_restore(fe->cr);
290 }
291
292 static void do_draw_line(frontend *fe, int x1, int y1, int x2, int y2)
293 {
294     cairo_new_path(fe->cr);
295     cairo_move_to(fe->cr, x1 + 0.5, y1 + 0.5);
296     cairo_line_to(fe->cr, x2 + 0.5, y2 + 0.5);
297     cairo_stroke(fe->cr);
298 }
299
300 static void do_draw_poly(frontend *fe, int *coords, int npoints,
301                          int fillcolour, int outlinecolour)
302 {
303     int i;
304
305     cairo_new_path(fe->cr);
306     for (i = 0; i < npoints; i++)
307         cairo_line_to(fe->cr, coords[i*2] + 0.5, coords[i*2 + 1] + 0.5);
308     cairo_close_path(fe->cr);
309     if (fillcolour >= 0) {
310         set_colour(fe, fillcolour);
311         cairo_fill_preserve(fe->cr);
312     }
313     assert(outlinecolour >= 0);
314     set_colour(fe, outlinecolour);
315     cairo_stroke(fe->cr);
316 }
317
318 static void do_draw_circle(frontend *fe, int cx, int cy, int radius,
319                            int fillcolour, int outlinecolour)
320 {
321     cairo_new_path(fe->cr);
322     cairo_arc(fe->cr, cx + 0.5, cy + 0.5, radius, 0, 2*PI);
323     cairo_close_path(fe->cr);           /* Just in case... */
324     if (fillcolour >= 0) {
325         set_colour(fe, fillcolour);
326         cairo_fill_preserve(fe->cr);
327     }
328     assert(outlinecolour >= 0);
329     set_colour(fe, outlinecolour);
330     cairo_stroke(fe->cr);
331 }
332
333 static void setup_blitter(blitter *bl, int w, int h)
334 {
335     bl->image = cairo_image_surface_create(CAIRO_FORMAT_RGB24, w, h);
336 }
337
338 static void teardown_blitter(blitter *bl)
339 {
340     cairo_surface_destroy(bl->image);
341 }
342
343 static void do_blitter_save(frontend *fe, blitter *bl, int x, int y)
344 {
345     cairo_t *cr = cairo_create(bl->image);
346
347     cairo_set_source_surface(cr, fe->image, -x, -y);
348     cairo_paint(cr);
349     cairo_destroy(cr);
350 }
351
352 static void do_blitter_load(frontend *fe, blitter *bl, int x, int y)
353 {
354     cairo_set_source_surface(fe->cr, bl->image, x, y);
355     cairo_paint(fe->cr);
356 }
357
358 static void clear_backing_store(frontend *fe)
359 {
360     fe->image = NULL;
361 }
362
363 static void setup_backing_store(frontend *fe)
364 {
365     cairo_t *cr;
366     int i;
367
368     fe->pixmap = gdk_pixmap_new(fe->area->window, fe->pw, fe->ph, -1);
369     fe->image = cairo_image_surface_create(CAIRO_FORMAT_RGB24,
370                                            fe->pw, fe->ph);
371
372     for (i = 0; i < 3; i++) {
373         switch (i) {
374             case 0: cr = cairo_create(fe->image); break;
375             case 1: cr = gdk_cairo_create(fe->pixmap); break;
376             case 2: cr = gdk_cairo_create(fe->area->window); break;
377         }
378         cairo_set_source_rgb(cr,
379                              fe->colours[0], fe->colours[1], fe->colours[2]);
380         cairo_paint(cr);
381         cairo_destroy(cr);
382     }
383 }
384
385 static int backing_store_ok(frontend *fe)
386 {
387     return (!!fe->image);
388 }
389
390 static void teardown_backing_store(frontend *fe)
391 {
392     cairo_surface_destroy(fe->image);
393     gdk_pixmap_unref(fe->pixmap);
394     fe->image = NULL;
395 }
396
397 static void repaint_rectangle(frontend *fe, GtkWidget *widget,
398                               int x, int y, int w, int h)
399 {
400     gdk_draw_pixmap(widget->window,
401                     widget->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
402                     fe->pixmap,
403                     x - fe->ox, y - fe->oy, x, y, w, h);
404 }
405
406 #endif
407
408 /* ----------------------------------------------------------------------
409  * GDK drawing functions.
410  */
411
412 #ifndef USE_CAIRO
413
414 static void setup_drawing(frontend *fe)
415 {
416     fe->gc = gdk_gc_new(fe->area->window);
417 }
418
419 static void teardown_drawing(frontend *fe)
420 {
421     gdk_gc_unref(fe->gc);
422     fe->gc = NULL;
423 }
424
425 static void snaffle_colours(frontend *fe)
426 {
427     int i, ncolours;
428     float *colours;
429     gboolean *success;
430
431     fe->colmap = gdk_colormap_get_system();
432     colours = midend_colours(fe->me, &ncolours);
433     fe->ncolours = ncolours;
434     fe->colours = snewn(ncolours, GdkColor);
435     for (i = 0; i < ncolours; i++) {
436         fe->colours[i].red = colours[i*3] * 0xFFFF;
437         fe->colours[i].green = colours[i*3+1] * 0xFFFF;
438         fe->colours[i].blue = colours[i*3+2] * 0xFFFF;
439     }
440     success = snewn(ncolours, gboolean);
441     gdk_colormap_alloc_colors(fe->colmap, fe->colours, ncolours,
442                               FALSE, FALSE, success);
443     for (i = 0; i < ncolours; i++) {
444         if (!success[i]) {
445             g_error("couldn't allocate colour %d (#%02x%02x%02x)\n",
446                     i, fe->colours[i].red >> 8,
447                     fe->colours[i].green >> 8,
448                     fe->colours[i].blue >> 8);
449         }
450     }
451 }
452
453 static void set_window_background(frontend *fe, int colour)
454 {
455     gdk_window_set_background(fe->area->window, &fe->colours[colour]);
456     gdk_window_set_background(fe->window->window, &fe->colours[colour]);
457 }
458
459 static void set_colour(frontend *fe, int colour)
460 {
461     gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
462 }
463
464 #ifdef USE_PANGO
465 static PangoLayout *make_pango_layout(frontend *fe)
466 {
467     return (pango_layout_new(gtk_widget_get_pango_context(fe->area)));
468 }
469
470 static void draw_pango_layout(frontend *fe, PangoLayout *layout,
471                               int x, int y)
472 {
473     gdk_draw_layout(fe->pixmap, fe->gc, x, y, layout);
474 }
475 #endif
476
477 static void save_screenshot_png(frontend *fe, const char *screenshot_file)
478 {
479     GdkPixbuf *pb;
480     GError *gerror = NULL;
481
482     midend_redraw(fe->me);
483
484     pb = gdk_pixbuf_get_from_drawable(NULL, fe->pixmap,
485                                       NULL, 0, 0, 0, 0, -1, -1);
486     gdk_pixbuf_save(pb, screenshot_file, "png", &gerror, NULL);
487 }
488
489 static void do_clip(frontend *fe, int x, int y, int w, int h)
490 {
491     GdkRectangle rect;
492
493     rect.x = x;
494     rect.y = y;
495     rect.width = w;
496     rect.height = h;
497     gdk_gc_set_clip_rectangle(fe->gc, &rect);
498 }
499
500 static void do_unclip(frontend *fe)
501 {
502     GdkRectangle rect;
503
504     rect.x = 0;
505     rect.y = 0;
506     rect.width = fe->w;
507     rect.height = fe->h;
508     gdk_gc_set_clip_rectangle(fe->gc, &rect);
509 }
510
511 static void do_draw_rect(frontend *fe, int x, int y, int w, int h)
512 {
513     gdk_draw_rectangle(fe->pixmap, fe->gc, 1, x, y, w, h);
514 }
515
516 static void do_draw_line(frontend *fe, int x1, int y1, int x2, int y2)
517 {
518     gdk_draw_line(fe->pixmap, fe->gc, x1, y1, x2, y2);
519 }
520
521 static void do_draw_poly(frontend *fe, int *coords, int npoints,
522                          int fillcolour, int outlinecolour)
523 {
524     GdkPoint *points = snewn(npoints, GdkPoint);
525     int i;
526
527     for (i = 0; i < npoints; i++) {
528         points[i].x = coords[i*2];
529         points[i].y = coords[i*2+1];
530     }
531
532     if (fillcolour >= 0) {
533         set_colour(fe, fillcolour);
534         gdk_draw_polygon(fe->pixmap, fe->gc, TRUE, points, npoints);
535     }
536     assert(outlinecolour >= 0);
537     set_colour(fe, outlinecolour);
538
539     /*
540      * In principle we ought to be able to use gdk_draw_polygon for
541      * the outline as well. In fact, it turns out to interact badly
542      * with a clipping region, for no terribly obvious reason, so I
543      * draw the outline as a sequence of lines instead.
544      */
545     for (i = 0; i < npoints; i++)
546         gdk_draw_line(fe->pixmap, fe->gc,
547                       points[i].x, points[i].y,
548                       points[(i+1)%npoints].x, points[(i+1)%npoints].y);
549
550     sfree(points);
551 }
552
553 static void do_draw_circle(frontend *fe, int cx, int cy, int radius,
554                            int fillcolour, int outlinecolour)
555 {
556     if (fillcolour >= 0) {
557         set_colour(fe, fillcolour);
558         gdk_draw_arc(fe->pixmap, fe->gc, TRUE,
559                      cx - radius, cy - radius,
560                      2 * radius, 2 * radius, 0, 360 * 64);
561     }
562
563     assert(outlinecolour >= 0);
564     set_colour(fe, outlinecolour);
565     gdk_draw_arc(fe->pixmap, fe->gc, FALSE,
566                  cx - radius, cy - radius,
567                  2 * radius, 2 * radius, 0, 360 * 64);
568 }
569
570 static void setup_blitter(blitter *bl, int w, int h)
571 {
572     /*
573      * We can't create the pixmap right now, because fe->window
574      * might not yet exist. So we just cache w and h and create it
575      * during the firs call to blitter_save.
576      */
577     bl->pixmap = NULL;
578 }
579
580 static void teardown_blitter(blitter *bl)
581 {
582     if (bl->pixmap)
583         gdk_pixmap_unref(bl->pixmap);
584 }
585
586 static void do_blitter_save(frontend *fe, blitter *bl, int x, int y)
587 {
588     if (!bl->pixmap)
589         bl->pixmap = gdk_pixmap_new(fe->area->window, bl->w, bl->h, -1);
590     gdk_draw_pixmap(bl->pixmap,
591                     fe->area->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
592                     fe->pixmap,
593                     x, y, 0, 0, bl->w, bl->h);
594 }
595
596 static void do_blitter_load(frontend *fe, blitter *bl, int x, int y)
597 {
598     assert(bl->pixmap);
599     gdk_draw_pixmap(fe->pixmap,
600                     fe->area->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
601                     bl->pixmap,
602                     0, 0, x, y, bl->w, bl->h);
603 }
604
605 static void clear_backing_store(frontend *fe)
606 {
607     fe->pixmap = NULL;
608 }
609
610 static void setup_backing_store(frontend *fe)
611 {
612     GdkGC *gc;
613
614     fe->pixmap = gdk_pixmap_new(fe->area->window, fe->pw, fe->ph, -1);
615
616     gc = gdk_gc_new(fe->area->window);
617     gdk_gc_set_foreground(gc, &fe->colours[0]);
618     gdk_draw_rectangle(fe->pixmap, gc, 1, 0, 0, fe->pw, fe->ph);
619     gdk_draw_rectangle(fe->area->window, gc, 1, 0, 0, fe->w, fe->h);
620     gdk_gc_unref(gc);
621 }
622
623 static int backing_store_ok(frontend *fe)
624 {
625     return (!!fe->pixmap);
626 }
627
628 static void teardown_backing_store(frontend *fe)
629 {
630     gdk_pixmap_unref(fe->pixmap);
631     fe->pixmap = NULL;
632 }
633
634 static void repaint_rectangle(frontend *fe, GtkWidget *widget,
635                               int x, int y, int w, int h)
636 {
637     gdk_draw_pixmap(widget->window,
638                     widget->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
639                     fe->pixmap,
640                     x - fe->ox, y - fe->oy, x, y, w, h);
641 }
642
643 #endif
644
645 /* ----------------------------------------------------------------------
646  * Pango font functions.
647  */
648
649 #ifdef USE_PANGO
650
651 static void add_font(frontend *fe, int index, int fonttype, int fontsize)
652 {
653     /*
654      * Use Pango to find the closest match to the requested
655      * font.
656      */
657     PangoFontDescription *fd;
658
659     fd = pango_font_description_new();
660     /* `Monospace' and `Sans' are meta-families guaranteed to exist */
661     pango_font_description_set_family(fd, fonttype == FONT_FIXED ?
662                                       "Monospace" : "Sans");
663     pango_font_description_set_weight(fd, PANGO_WEIGHT_BOLD);
664     /*
665      * I found some online Pango documentation which
666      * described a function called
667      * pango_font_description_set_absolute_size(), which is
668      * _exactly_ what I want here. Unfortunately, none of
669      * my local Pango installations have it (presumably
670      * they're too old), so I'm going to have to hack round
671      * it by figuring out the point size myself. This
672      * limits me to X and probably also breaks in later
673      * Pango installations, so ideally I should add another
674      * CHECK_VERSION type ifdef and use set_absolute_size
675      * where available. All very annoying.
676      */
677 #ifdef HAVE_SENSIBLE_ABSOLUTE_SIZE_FUNCTION
678     pango_font_description_set_absolute_size(fd, PANGO_SCALE*fontsize);
679 #else
680     {
681         Display *d = GDK_DISPLAY();
682         int s = DefaultScreen(d);
683         double resolution =
684             (PANGO_SCALE * 72.27 / 25.4) *
685             ((double) DisplayWidthMM(d, s) / DisplayWidth (d, s));
686         pango_font_description_set_size(fd, resolution * fontsize);
687     }
688 #endif
689     fe->fonts[index].desc = fd;
690 }
691
692 static void align_and_draw_text(frontend *fe,
693                                 int index, int align, int x, int y,
694                                 const char *text)
695 {
696     PangoLayout *layout;
697     PangoRectangle rect;
698
699     layout = make_pango_layout(fe);
700
701     /*
702      * Create a layout.
703      */
704     pango_layout_set_font_description(layout, fe->fonts[index].desc);
705     pango_layout_set_text(layout, text, strlen(text));
706     pango_layout_get_pixel_extents(layout, NULL, &rect);
707
708     if (align & ALIGN_VCENTRE)
709         rect.y -= rect.height / 2;
710     else
711         rect.y -= rect.height;
712
713     if (align & ALIGN_HCENTRE)
714         rect.x -= rect.width / 2;
715     else if (align & ALIGN_HRIGHT)
716         rect.x -= rect.width;
717
718     draw_pango_layout(fe, layout, rect.x + x, rect.y + y);
719
720     g_object_unref(layout);
721 }
722
723 #endif
724
725 /* ----------------------------------------------------------------------
726  * Old-fashioned font functions.
727  */
728
729 #ifndef USE_PANGO
730
731 static void add_font(int index, int fonttype, int fontsize)
732 {
733     /*
734      * In GTK 1.2, I don't know of any plausible way to
735      * pick a suitable font, so I'm just going to be
736      * tedious.
737      */
738     fe->fonts[i].font = gdk_font_load(fonttype == FONT_FIXED ?
739                                       "fixed" : "variable");
740 }
741
742 static void align_and_draw_text(int index, int align, int x, int y,
743                                 const char *text)
744 {
745     int lb, rb, wid, asc, desc;
746
747     /*
748      * Measure vertical string extents with respect to the same
749      * string always...
750      */
751     gdk_string_extents(fe->fonts[i].font,
752                        "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
753                        &lb, &rb, &wid, &asc, &desc);
754     if (align & ALIGN_VCENTRE)
755         y += asc - (asc+desc)/2;
756     else
757         y += asc;
758
759     /*
760      * ... but horizontal extents with respect to the provided
761      * string. This means that multiple pieces of text centred
762      * on the same y-coordinate don't have different baselines.
763      */
764     gdk_string_extents(fe->fonts[i].font, text,
765                        &lb, &rb, &wid, &asc, &desc);
766
767     if (align & ALIGN_HCENTRE)
768         x -= wid / 2;
769     else if (align & ALIGN_HRIGHT)
770         x -= wid;
771
772     /*
773      * Actually draw the text.
774      */
775     gdk_draw_string(fe->pixmap, fe->fonts[i].font, fe->gc, x, y, text);
776 }
777
778 #endif
779
780 /* ----------------------------------------------------------------------
781  * The exported drawing functions.
782  */
783
784 void gtk_start_draw(void *handle)
785 {
786     frontend *fe = (frontend *)handle;
787     fe->bbox_l = fe->w;
788     fe->bbox_r = 0;
789     fe->bbox_u = fe->h;
790     fe->bbox_d = 0;
791     setup_drawing(fe);
792 }
793
794 void gtk_clip(void *handle, int x, int y, int w, int h)
795 {
796     frontend *fe = (frontend *)handle;
797     do_clip(fe, x, y, w, h);
798 }
799
800 void gtk_unclip(void *handle)
801 {
802     frontend *fe = (frontend *)handle;
803     do_unclip(fe);
804 }
805
806 void gtk_draw_text(void *handle, int x, int y, int fonttype, int fontsize,
807                    int align, int colour, char *text)
808 {
809     frontend *fe = (frontend *)handle;
810     int i;
811
812     /*
813      * Find or create the font.
814      */
815     for (i = 0; i < fe->nfonts; i++)
816         if (fe->fonts[i].type == fonttype && fe->fonts[i].size == fontsize)
817             break;
818
819     if (i == fe->nfonts) {
820         if (fe->fontsize <= fe->nfonts) {
821             fe->fontsize = fe->nfonts + 10;
822             fe->fonts = sresize(fe->fonts, fe->fontsize, struct font);
823         }
824
825         fe->nfonts++;
826
827         fe->fonts[i].type = fonttype;
828         fe->fonts[i].size = fontsize;
829         add_font(fe, i, fonttype, fontsize);
830     }
831
832     /*
833      * Do the job.
834      */
835     set_colour(fe, colour);
836     align_and_draw_text(fe, i, align, x, y, text);
837 }
838
839 void gtk_draw_rect(void *handle, int x, int y, int w, int h, int colour)
840 {
841     frontend *fe = (frontend *)handle;
842     set_colour(fe, colour);
843     do_draw_rect(fe, x, y, w, h);
844 }
845
846 void gtk_draw_line(void *handle, int x1, int y1, int x2, int y2, int colour)
847 {
848     frontend *fe = (frontend *)handle;
849     set_colour(fe, colour);
850     do_draw_line(fe, x1, y1, x2, y2);
851 }
852
853 void gtk_draw_poly(void *handle, int *coords, int npoints,
854                    int fillcolour, int outlinecolour)
855 {
856     frontend *fe = (frontend *)handle;
857     do_draw_poly(fe, coords, npoints, fillcolour, outlinecolour);
858 }
859
860 void gtk_draw_circle(void *handle, int cx, int cy, int radius,
861                      int fillcolour, int outlinecolour)
862 {
863     frontend *fe = (frontend *)handle;
864     do_draw_circle(fe, cx, cy, radius, fillcolour, outlinecolour);
865 }
866
867 blitter *gtk_blitter_new(void *handle, int w, int h)
868 {
869     blitter *bl = snew(blitter);
870     setup_blitter(bl, w, h);
871     bl->w = w;
872     bl->h = h;
873     return bl;
874 }
875
876 void gtk_blitter_free(void *handle, blitter *bl)
877 {
878     teardown_blitter(bl);
879     sfree(bl);
880 }
881
882 void gtk_blitter_save(void *handle, blitter *bl, int x, int y)
883 {
884     frontend *fe = (frontend *)handle;
885     do_blitter_save(fe, bl, x, y);
886     bl->x = x;
887     bl->y = y;
888 }
889
890 void gtk_blitter_load(void *handle, blitter *bl, int x, int y)
891 {
892     frontend *fe = (frontend *)handle;
893     if (x == BLITTER_FROMSAVED && y == BLITTER_FROMSAVED) {
894         x = bl->x;
895         y = bl->y;
896     }
897     do_blitter_load(fe, bl, x, y);
898 }
899
900 void gtk_draw_update(void *handle, int x, int y, int w, int h)
901 {
902     frontend *fe = (frontend *)handle;
903     if (fe->bbox_l > x  ) fe->bbox_l = x  ;
904     if (fe->bbox_r < x+w) fe->bbox_r = x+w;
905     if (fe->bbox_u > y  ) fe->bbox_u = y  ;
906     if (fe->bbox_d < y+h) fe->bbox_d = y+h;
907 }
908
909 void gtk_end_draw(void *handle)
910 {
911     frontend *fe = (frontend *)handle;
912
913     teardown_drawing(fe);
914
915     if (fe->bbox_l < fe->bbox_r && fe->bbox_u < fe->bbox_d) {
916         repaint_rectangle(fe, fe->area,
917                           fe->bbox_l - 1 + fe->ox,
918                           fe->bbox_u - 1 + fe->oy,
919                           fe->bbox_r - fe->bbox_l + 2,
920                           fe->bbox_d - fe->bbox_u + 2);
921     }
922 }
923
924 #ifdef USE_PANGO
925 char *gtk_text_fallback(void *handle, const char *const *strings, int nstrings)
926 {
927     /*
928      * We assume Pango can cope with any UTF-8 likely to be emitted
929      * by a puzzle.
930      */
931     return dupstr(strings[0]);
932 }
933 #endif
934
935 const struct drawing_api gtk_drawing = {
936     gtk_draw_text,
937     gtk_draw_rect,
938     gtk_draw_line,
939     gtk_draw_poly,
940     gtk_draw_circle,
941     gtk_draw_update,
942     gtk_clip,
943     gtk_unclip,
944     gtk_start_draw,
945     gtk_end_draw,
946     gtk_status_bar,
947     gtk_blitter_new,
948     gtk_blitter_free,
949     gtk_blitter_save,
950     gtk_blitter_load,
951     NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */
952     NULL, NULL,                        /* line_width, line_dotted */
953 #ifdef USE_PANGO
954     gtk_text_fallback,
955 #else
956     NULL,
957 #endif
958 };
959
960 static void destroy(GtkWidget *widget, gpointer data)
961 {
962     frontend *fe = (frontend *)data;
963     deactivate_timer(fe);
964     midend_free(fe->me);
965     gtk_main_quit();
966 }
967
968 static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
969 {
970     frontend *fe = (frontend *)data;
971     int keyval;
972     int shift = (event->state & GDK_SHIFT_MASK) ? MOD_SHFT : 0;
973     int ctrl = (event->state & GDK_CONTROL_MASK) ? MOD_CTRL : 0;
974
975     if (!backing_store_ok(fe))
976         return TRUE;
977
978 #if !GTK_CHECK_VERSION(2,0,0)
979     /* Gtk 1.2 passes a key event to this function even if it's also
980      * defined as an accelerator.
981      * Gtk 2 doesn't do this, and this function appears not to exist there. */
982     if (fe->accelgroup &&
983         gtk_accel_group_get_entry(fe->accelgroup,
984         event->keyval, event->state))
985         return TRUE;
986 #endif
987
988     /* Handle mnemonics. */
989     if (gtk_window_activate_key(GTK_WINDOW(fe->window), event))
990         return TRUE;
991
992     if (event->keyval == GDK_Up)
993         keyval = shift | ctrl | CURSOR_UP;
994     else if (event->keyval == GDK_KP_Up || event->keyval == GDK_KP_8)
995         keyval = MOD_NUM_KEYPAD | '8';
996     else if (event->keyval == GDK_Down)
997         keyval = shift | ctrl | CURSOR_DOWN;
998     else if (event->keyval == GDK_KP_Down || event->keyval == GDK_KP_2)
999         keyval = MOD_NUM_KEYPAD | '2';
1000     else if (event->keyval == GDK_Left)
1001         keyval = shift | ctrl | CURSOR_LEFT;
1002     else if (event->keyval == GDK_KP_Left || event->keyval == GDK_KP_4)
1003         keyval = MOD_NUM_KEYPAD | '4';
1004     else if (event->keyval == GDK_Right)
1005         keyval = shift | ctrl | CURSOR_RIGHT;
1006     else if (event->keyval == GDK_KP_Right || event->keyval == GDK_KP_6)
1007         keyval = MOD_NUM_KEYPAD | '6';
1008     else if (event->keyval == GDK_KP_Home || event->keyval == GDK_KP_7)
1009         keyval = MOD_NUM_KEYPAD | '7';
1010     else if (event->keyval == GDK_KP_End || event->keyval == GDK_KP_1)
1011         keyval = MOD_NUM_KEYPAD | '1';
1012     else if (event->keyval == GDK_KP_Page_Up || event->keyval == GDK_KP_9)
1013         keyval = MOD_NUM_KEYPAD | '9';
1014     else if (event->keyval == GDK_KP_Page_Down || event->keyval == GDK_KP_3)
1015         keyval = MOD_NUM_KEYPAD | '3';
1016     else if (event->keyval == GDK_KP_Insert || event->keyval == GDK_KP_0)
1017         keyval = MOD_NUM_KEYPAD | '0';
1018     else if (event->keyval == GDK_KP_Begin || event->keyval == GDK_KP_5)
1019         keyval = MOD_NUM_KEYPAD | '5';
1020     else if (event->keyval == GDK_BackSpace ||
1021              event->keyval == GDK_Delete ||
1022              event->keyval == GDK_KP_Delete)
1023         keyval = '\177';
1024     else if (event->string[0] && !event->string[1])
1025         keyval = (unsigned char)event->string[0];
1026     else
1027         keyval = -1;
1028
1029     if (keyval >= 0 &&
1030         !midend_process_key(fe->me, 0, 0, keyval))
1031         gtk_widget_destroy(fe->window);
1032
1033     return TRUE;
1034 }
1035
1036 static gint button_event(GtkWidget *widget, GdkEventButton *event,
1037                          gpointer data)
1038 {
1039     frontend *fe = (frontend *)data;
1040     int button;
1041
1042     if (!backing_store_ok(fe))
1043         return TRUE;
1044
1045     if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE)
1046         return TRUE;
1047
1048     if (event->button == 2 || (event->state & GDK_SHIFT_MASK))
1049         button = MIDDLE_BUTTON;
1050     else if (event->button == 3 || (event->state & GDK_MOD1_MASK))
1051         button = RIGHT_BUTTON;
1052     else if (event->button == 1)
1053         button = LEFT_BUTTON;
1054     else
1055         return FALSE;                  /* don't even know what button! */
1056
1057     if (event->type == GDK_BUTTON_RELEASE)
1058         button += LEFT_RELEASE - LEFT_BUTTON;
1059
1060     if (!midend_process_key(fe->me, event->x - fe->ox,
1061                             event->y - fe->oy, button))
1062         gtk_widget_destroy(fe->window);
1063
1064     return TRUE;
1065 }
1066
1067 static gint motion_event(GtkWidget *widget, GdkEventMotion *event,
1068                          gpointer data)
1069 {
1070     frontend *fe = (frontend *)data;
1071     int button;
1072
1073     if (!backing_store_ok(fe))
1074         return TRUE;
1075
1076     if (event->state & (GDK_BUTTON2_MASK | GDK_SHIFT_MASK))
1077         button = MIDDLE_DRAG;
1078     else if (event->state & GDK_BUTTON1_MASK)
1079         button = LEFT_DRAG;
1080     else if (event->state & GDK_BUTTON3_MASK)
1081         button = RIGHT_DRAG;
1082     else
1083         return FALSE;                  /* don't even know what button! */
1084
1085     if (!midend_process_key(fe->me, event->x - fe->ox,
1086                             event->y - fe->oy, button))
1087         gtk_widget_destroy(fe->window);
1088 #if GTK_CHECK_VERSION(2,12,0)
1089     gdk_event_request_motions(event);
1090 #else
1091     gdk_window_get_pointer(widget->window, NULL, NULL, NULL);
1092 #endif
1093
1094     return TRUE;
1095 }
1096
1097 static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
1098                         gpointer data)
1099 {
1100     frontend *fe = (frontend *)data;
1101
1102     if (backing_store_ok(fe)) {
1103         repaint_rectangle(fe, widget,
1104                           event->area.x, event->area.y,
1105                           event->area.width, event->area.height);
1106     }
1107     return TRUE;
1108 }
1109
1110 static gint map_window(GtkWidget *widget, GdkEvent *event,
1111                        gpointer data)
1112 {
1113     frontend *fe = (frontend *)data;
1114
1115     /*
1116      * Apparently we need to do this because otherwise the status
1117      * bar will fail to update immediately. Annoying, but there we
1118      * go.
1119      */
1120     gtk_widget_queue_draw(fe->window);
1121
1122     return TRUE;
1123 }
1124
1125 static gint configure_area(GtkWidget *widget,
1126                            GdkEventConfigure *event, gpointer data)
1127 {
1128     frontend *fe = (frontend *)data;
1129     int x, y;
1130
1131     if (backing_store_ok(fe))
1132         teardown_backing_store(fe);
1133
1134     x = fe->w = event->width;
1135     y = fe->h = event->height;
1136     midend_size(fe->me, &x, &y, TRUE);
1137     fe->pw = x;
1138     fe->ph = y;
1139     fe->ox = (fe->w - fe->pw) / 2;
1140     fe->oy = (fe->h - fe->ph) / 2;
1141
1142     setup_backing_store(fe);
1143     midend_force_redraw(fe->me);
1144
1145     return TRUE;
1146 }
1147
1148 static gint timer_func(gpointer data)
1149 {
1150     frontend *fe = (frontend *)data;
1151
1152     if (fe->timer_active) {
1153         struct timeval now;
1154         float elapsed;
1155         gettimeofday(&now, NULL);
1156         elapsed = ((now.tv_usec - fe->last_time.tv_usec) * 0.000001F +
1157                    (now.tv_sec - fe->last_time.tv_sec));
1158         midend_timer(fe->me, elapsed);  /* may clear timer_active */
1159         fe->last_time = now;
1160     }
1161
1162     return fe->timer_active;
1163 }
1164
1165 void deactivate_timer(frontend *fe)
1166 {
1167     if (!fe)
1168         return;                        /* can happen due to --generate */
1169     if (fe->timer_active)
1170         gtk_timeout_remove(fe->timer_id);
1171     fe->timer_active = FALSE;
1172 }
1173
1174 void activate_timer(frontend *fe)
1175 {
1176     if (!fe)
1177         return;                        /* can happen due to --generate */
1178     if (!fe->timer_active) {
1179         fe->timer_id = gtk_timeout_add(20, timer_func, fe);
1180         gettimeofday(&fe->last_time, NULL);
1181     }
1182     fe->timer_active = TRUE;
1183 }
1184
1185 static void window_destroy(GtkWidget *widget, gpointer data)
1186 {
1187     gtk_main_quit();
1188 }
1189
1190 static void msgbox_button_clicked(GtkButton *button, gpointer data)
1191 {
1192     GtkWidget *window = GTK_WIDGET(data);
1193     int v, *ip;
1194
1195     ip = (int *)gtk_object_get_data(GTK_OBJECT(window), "user-data");
1196     v = GPOINTER_TO_INT(gtk_object_get_data(GTK_OBJECT(button), "user-data"));
1197     *ip = v;
1198
1199     gtk_widget_destroy(GTK_WIDGET(data));
1200 }
1201
1202 static int win_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data)
1203 {
1204     GtkObject *cancelbutton = GTK_OBJECT(data);
1205
1206     /*
1207      * `Escape' effectively clicks the cancel button
1208      */
1209     if (event->keyval == GDK_Escape) {
1210         gtk_signal_emit_by_name(GTK_OBJECT(cancelbutton), "clicked");
1211         return TRUE;
1212     }
1213
1214     return FALSE;
1215 }
1216
1217 enum { MB_OK, MB_YESNO };
1218
1219 int message_box(GtkWidget *parent, char *title, char *msg, int centre,
1220                 int type)
1221 {
1222     GtkWidget *window, *hbox, *text, *button;
1223     char *titles;
1224     int i, def, cancel;
1225
1226     window = gtk_dialog_new();
1227     text = gtk_label_new(msg);
1228     gtk_misc_set_alignment(GTK_MISC(text), 0.0, 0.0);
1229     hbox = gtk_hbox_new(FALSE, 0);
1230     gtk_box_pack_start(GTK_BOX(hbox), text, FALSE, FALSE, 20);
1231     gtk_box_pack_start(GTK_BOX(GTK_DIALOG(window)->vbox),
1232                        hbox, FALSE, FALSE, 20);
1233     gtk_widget_show(text);
1234     gtk_widget_show(hbox);
1235     gtk_window_set_title(GTK_WINDOW(window), title);
1236     gtk_label_set_line_wrap(GTK_LABEL(text), TRUE);
1237
1238     if (type == MB_OK) {
1239         titles = GTK_STOCK_OK "\0";
1240         def = cancel = 0;
1241     } else {
1242         assert(type == MB_YESNO);
1243         titles = GTK_STOCK_NO "\0" GTK_STOCK_YES "\0";
1244         def = 1;
1245         cancel = 0;
1246     }
1247     i = 0;
1248     
1249     while (*titles) {
1250         button = gtk_button_new_from_stock(titles);
1251         gtk_box_pack_end(GTK_BOX(GTK_DIALOG(window)->action_area),
1252                          button, FALSE, FALSE, 0);
1253         gtk_widget_show(button);
1254         if (i == def) {
1255             GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
1256             gtk_window_set_default(GTK_WINDOW(window), button);
1257         }
1258         if (i == cancel) {
1259             gtk_signal_connect(GTK_OBJECT(window), "key_press_event",
1260                                GTK_SIGNAL_FUNC(win_key_press), button);
1261         }
1262         gtk_signal_connect(GTK_OBJECT(button), "clicked",
1263                            GTK_SIGNAL_FUNC(msgbox_button_clicked), window);
1264         gtk_object_set_data(GTK_OBJECT(button), "user-data",
1265                             GINT_TO_POINTER(i));
1266         titles += strlen(titles)+1;
1267         i++;
1268     }
1269     gtk_object_set_data(GTK_OBJECT(window), "user-data",
1270                         GINT_TO_POINTER(&i));
1271     gtk_signal_connect(GTK_OBJECT(window), "destroy",
1272                        GTK_SIGNAL_FUNC(window_destroy), NULL);
1273     gtk_window_set_modal(GTK_WINDOW(window), TRUE);
1274     gtk_window_set_transient_for(GTK_WINDOW(window), GTK_WINDOW(parent));
1275     /* set_transient_window_pos(parent, window); */
1276     gtk_widget_show(window);
1277     i = -1;
1278     gtk_main();
1279     return (type == MB_YESNO ? i == 1 : TRUE);
1280 }
1281
1282 void error_box(GtkWidget *parent, char *msg)
1283 {
1284     message_box(parent, "Error", msg, FALSE, MB_OK);
1285 }
1286
1287 static void config_ok_button_clicked(GtkButton *button, gpointer data)
1288 {
1289     frontend *fe = (frontend *)data;
1290     char *err;
1291
1292     err = midend_set_config(fe->me, fe->cfg_which, fe->cfg);
1293
1294     if (err)
1295         error_box(fe->cfgbox, err);
1296     else {
1297         fe->cfgret = TRUE;
1298         gtk_widget_destroy(fe->cfgbox);
1299         changed_preset(fe);
1300     }
1301 }
1302
1303 static void config_cancel_button_clicked(GtkButton *button, gpointer data)
1304 {
1305     frontend *fe = (frontend *)data;
1306
1307     gtk_widget_destroy(fe->cfgbox);
1308 }
1309
1310 static int editbox_key(GtkWidget *widget, GdkEventKey *event, gpointer data)
1311 {
1312     /*
1313      * GtkEntry has a nasty habit of eating the Return key, which
1314      * is unhelpful since it doesn't actually _do_ anything with it
1315      * (it calls gtk_widget_activate, but our edit boxes never need
1316      * activating). So I catch Return before GtkEntry sees it, and
1317      * pass it straight on to the parent widget. Effect: hitting
1318      * Return in an edit box will now activate the default button
1319      * in the dialog just like it will everywhere else.
1320      */
1321     if (event->keyval == GDK_Return && widget->parent != NULL) {
1322         gint return_val;
1323         gtk_signal_emit_stop_by_name(GTK_OBJECT(widget), "key_press_event");
1324         gtk_signal_emit_by_name(GTK_OBJECT(widget->parent), "key_press_event",
1325                                 event, &return_val);
1326         return return_val;
1327     }
1328     return FALSE;
1329 }
1330
1331 static void editbox_changed(GtkEditable *ed, gpointer data)
1332 {
1333     config_item *i = (config_item *)data;
1334
1335     sfree(i->sval);
1336     i->sval = dupstr(gtk_entry_get_text(GTK_ENTRY(ed)));
1337 }
1338
1339 static void button_toggled(GtkToggleButton *tb, gpointer data)
1340 {
1341     config_item *i = (config_item *)data;
1342
1343     i->ival = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(tb));
1344 }
1345
1346 static void droplist_sel(GtkMenuItem *item, gpointer data)
1347 {
1348     config_item *i = (config_item *)data;
1349
1350     i->ival = GPOINTER_TO_INT(gtk_object_get_data(GTK_OBJECT(item),
1351                                                   "user-data"));
1352 }
1353
1354 static int get_config(frontend *fe, int which)
1355 {
1356     GtkWidget *w, *table, *cancel;
1357     char *title;
1358     config_item *i;
1359     int y;
1360
1361     fe->cfg = midend_get_config(fe->me, which, &title);
1362     fe->cfg_which = which;
1363     fe->cfgret = FALSE;
1364
1365     fe->cfgbox = gtk_dialog_new();
1366     gtk_window_set_title(GTK_WINDOW(fe->cfgbox), title);
1367     sfree(title);
1368
1369     w = gtk_button_new_from_stock(GTK_STOCK_CANCEL);
1370     gtk_box_pack_end(GTK_BOX(GTK_DIALOG(fe->cfgbox)->action_area),
1371                      w, FALSE, FALSE, 0);
1372     gtk_widget_show(w);
1373     gtk_signal_connect(GTK_OBJECT(w), "clicked",
1374                        GTK_SIGNAL_FUNC(config_cancel_button_clicked), fe);
1375     cancel = w;
1376
1377     w = gtk_button_new_from_stock(GTK_STOCK_OK);
1378     gtk_box_pack_end(GTK_BOX(GTK_DIALOG(fe->cfgbox)->action_area),
1379                      w, FALSE, FALSE, 0);
1380     gtk_widget_show(w);
1381     GTK_WIDGET_SET_FLAGS(w, GTK_CAN_DEFAULT);
1382     gtk_window_set_default(GTK_WINDOW(fe->cfgbox), w);
1383     gtk_signal_connect(GTK_OBJECT(w), "clicked",
1384                        GTK_SIGNAL_FUNC(config_ok_button_clicked), fe);
1385
1386     table = gtk_table_new(1, 2, FALSE);
1387     y = 0;
1388     gtk_box_pack_end(GTK_BOX(GTK_DIALOG(fe->cfgbox)->vbox),
1389                      table, FALSE, FALSE, 0);
1390     gtk_widget_show(table);
1391
1392     for (i = fe->cfg; i->type != C_END; i++) {
1393         gtk_table_resize(GTK_TABLE(table), y+1, 2);
1394
1395         switch (i->type) {
1396           case C_STRING:
1397             /*
1398              * Edit box with a label beside it.
1399              */
1400
1401             w = gtk_label_new(i->name);
1402             gtk_misc_set_alignment(GTK_MISC(w), 0.0, 0.5);
1403             gtk_table_attach(GTK_TABLE(table), w, 0, 1, y, y+1,
1404                              GTK_SHRINK | GTK_FILL,
1405                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1406                              3, 3);
1407             gtk_widget_show(w);
1408
1409             w = gtk_entry_new();
1410             gtk_table_attach(GTK_TABLE(table), w, 1, 2, y, y+1,
1411                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1412                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1413                              3, 3);
1414             gtk_entry_set_text(GTK_ENTRY(w), i->sval);
1415             gtk_signal_connect(GTK_OBJECT(w), "changed",
1416                                GTK_SIGNAL_FUNC(editbox_changed), i);
1417             gtk_signal_connect(GTK_OBJECT(w), "key_press_event",
1418                                GTK_SIGNAL_FUNC(editbox_key), NULL);
1419             gtk_widget_show(w);
1420
1421             break;
1422
1423           case C_BOOLEAN:
1424             /*
1425              * Simple checkbox.
1426              */
1427             w = gtk_check_button_new_with_label(i->name);
1428             gtk_signal_connect(GTK_OBJECT(w), "toggled",
1429                                GTK_SIGNAL_FUNC(button_toggled), i);
1430             gtk_table_attach(GTK_TABLE(table), w, 0, 2, y, y+1,
1431                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1432                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1433                              3, 3);
1434             gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), i->ival);
1435             gtk_widget_show(w);
1436             break;
1437
1438           case C_CHOICES:
1439             /*
1440              * Drop-down list (GtkOptionMenu).
1441              */
1442
1443             w = gtk_label_new(i->name);
1444             gtk_misc_set_alignment(GTK_MISC(w), 0.0, 0.5);
1445             gtk_table_attach(GTK_TABLE(table), w, 0, 1, y, y+1,
1446                              GTK_SHRINK | GTK_FILL,
1447                              GTK_EXPAND | GTK_SHRINK | GTK_FILL ,
1448                              3, 3);
1449             gtk_widget_show(w);
1450
1451             w = gtk_option_menu_new();
1452             gtk_table_attach(GTK_TABLE(table), w, 1, 2, y, y+1,
1453                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1454                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1455                              3, 3);
1456             gtk_widget_show(w);
1457
1458             {
1459                 int c, val;
1460                 char *p, *q, *name;
1461                 GtkWidget *menuitem;
1462                 GtkWidget *menu = gtk_menu_new();
1463
1464                 gtk_option_menu_set_menu(GTK_OPTION_MENU(w), menu);
1465
1466                 c = *i->sval;
1467                 p = i->sval+1;
1468                 val = 0;
1469
1470                 while (*p) {
1471                     q = p;
1472                     while (*q && *q != c)
1473                         q++;
1474
1475                     name = snewn(q-p+1, char);
1476                     strncpy(name, p, q-p);
1477                     name[q-p] = '\0';
1478
1479                     if (*q) q++;       /* eat delimiter */
1480
1481                     menuitem = gtk_menu_item_new_with_label(name);
1482                     gtk_container_add(GTK_CONTAINER(menu), menuitem);
1483                     gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
1484                                         GINT_TO_POINTER(val));
1485                     gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
1486                                        GTK_SIGNAL_FUNC(droplist_sel), i);
1487                     gtk_widget_show(menuitem);
1488
1489                     val++;
1490
1491                     p = q;
1492                 }
1493
1494                 gtk_option_menu_set_history(GTK_OPTION_MENU(w), i->ival);
1495             }
1496
1497             break;
1498         }
1499
1500         y++;
1501     }
1502
1503     gtk_signal_connect(GTK_OBJECT(fe->cfgbox), "destroy",
1504                        GTK_SIGNAL_FUNC(window_destroy), NULL);
1505     gtk_signal_connect(GTK_OBJECT(fe->cfgbox), "key_press_event",
1506                        GTK_SIGNAL_FUNC(win_key_press), cancel);
1507     gtk_window_set_modal(GTK_WINDOW(fe->cfgbox), TRUE);
1508     gtk_window_set_transient_for(GTK_WINDOW(fe->cfgbox),
1509                                  GTK_WINDOW(fe->window));
1510     /* set_transient_window_pos(fe->window, fe->cfgbox); */
1511     gtk_widget_show(fe->cfgbox);
1512     gtk_main();
1513
1514     free_cfg(fe->cfg);
1515
1516     return fe->cfgret;
1517 }
1518
1519 static void menu_key_event(GtkMenuItem *menuitem, gpointer data)
1520 {
1521     frontend *fe = (frontend *)data;
1522     int key = GPOINTER_TO_INT(gtk_object_get_data(GTK_OBJECT(menuitem),
1523                                                   "user-data"));
1524     if (!midend_process_key(fe->me, 0, 0, key))
1525         gtk_widget_destroy(fe->window);
1526 }
1527
1528 static void get_size(frontend *fe, int *px, int *py)
1529 {
1530     int x, y;
1531
1532     /*
1533      * Currently I don't want to make the GTK port scale large
1534      * puzzles to fit on the screen. This is because X does permit
1535      * extremely large windows and many window managers provide a
1536      * means of navigating round them, and the users I consulted
1537      * before deciding said that they'd rather have enormous puzzle
1538      * windows spanning multiple screen pages than have them
1539      * shrunk. I could change my mind later or introduce
1540      * configurability; this would be the place to do so, by
1541      * replacing the initial values of x and y with the screen
1542      * dimensions.
1543      */
1544     x = INT_MAX;
1545     y = INT_MAX;
1546     midend_size(fe->me, &x, &y, FALSE);
1547     *px = x;
1548     *py = y;
1549 }
1550
1551 #if !GTK_CHECK_VERSION(2,0,0)
1552 #define gtk_window_resize(win, x, y) \
1553         gdk_window_resize(GTK_WIDGET(win)->window, x, y)
1554 #endif
1555
1556 /*
1557  * Called when any other code in this file has changed the
1558  * selected game parameters.
1559  */
1560 static void changed_preset(frontend *fe)
1561 {
1562     int n = midend_which_preset(fe->me);
1563
1564     fe->preset_threaded = TRUE;
1565     if (n < 0 && fe->preset_custom) {
1566         gtk_check_menu_item_set_active(
1567             GTK_CHECK_MENU_ITEM(fe->preset_custom),
1568             TRUE);
1569     } else {
1570         GSList *gs = fe->preset_radio;
1571         int i = fe->n_preset_menu_items - 1 - n;
1572         if (fe->preset_custom)
1573             gs = gs->next;
1574         while (i && gs) {
1575             i--;
1576             gs = gs->next;
1577         }
1578         if (gs) {
1579             gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(gs->data),
1580                                            TRUE);
1581         } else for (gs = fe->preset_radio; gs; gs = gs->next) {
1582             gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(gs->data),
1583                                            FALSE);
1584         }
1585     }
1586     fe->preset_threaded = FALSE;
1587
1588     /*
1589      * Update the greying on the Copy menu option.
1590      */
1591     if (fe->copy_menu_item) {
1592         int enabled = midend_can_format_as_text_now(fe->me);
1593         gtk_widget_set_sensitive(fe->copy_menu_item, enabled);
1594     }
1595 }
1596
1597 static void resize_fe(frontend *fe)
1598 {
1599     int x, y;
1600
1601     get_size(fe, &x, &y);
1602     fe->w = x;
1603     fe->h = y;
1604     gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
1605     {
1606         GtkRequisition req;
1607         gtk_widget_size_request(GTK_WIDGET(fe->window), &req);
1608         gtk_window_resize(GTK_WINDOW(fe->window), req.width, req.height);
1609     }
1610     /*
1611      * Now that we've established the preferred size of the window,
1612      * reduce the drawing area's size request so the user can shrink
1613      * the window.
1614      */
1615     gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), 1, 1);
1616 }
1617
1618 static void menu_preset_event(GtkMenuItem *menuitem, gpointer data)
1619 {
1620     frontend *fe = (frontend *)data;
1621     game_params *params =
1622         (game_params *)gtk_object_get_data(GTK_OBJECT(menuitem), "user-data");
1623
1624     if (fe->preset_threaded ||
1625         (GTK_IS_CHECK_MENU_ITEM(menuitem) &&
1626          !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem))))
1627         return;
1628     midend_set_params(fe->me, params);
1629     midend_new_game(fe->me);
1630     changed_preset(fe);
1631     resize_fe(fe);
1632 }
1633
1634 GdkAtom compound_text_atom, utf8_string_atom;
1635 int paste_initialised = FALSE;
1636
1637 static void set_selection(frontend *fe, GdkAtom selection)
1638 {
1639     if (!paste_initialised) {
1640         compound_text_atom = gdk_atom_intern("COMPOUND_TEXT", FALSE);
1641         utf8_string_atom = gdk_atom_intern("UTF8_STRING", FALSE);
1642         paste_initialised = TRUE;
1643     }
1644
1645     /*
1646      * For this simple application we can safely assume that the
1647      * data passed to this function is pure ASCII, which means we
1648      * can return precisely the same stuff for types STRING,
1649      * COMPOUND_TEXT or UTF8_STRING.
1650      */
1651
1652     if (gtk_selection_owner_set(fe->area, selection, CurrentTime)) {
1653         gtk_selection_clear_targets(fe->area, selection);
1654         gtk_selection_add_target(fe->area, selection,
1655                                  GDK_SELECTION_TYPE_STRING, 1);
1656         gtk_selection_add_target(fe->area, selection, compound_text_atom, 1);
1657         gtk_selection_add_target(fe->area, selection, utf8_string_atom, 1);
1658     }
1659 }
1660
1661 void write_clip(frontend *fe, char *data)
1662 {
1663     if (fe->paste_data)
1664         sfree(fe->paste_data);
1665
1666     fe->paste_data = data;
1667     fe->paste_data_len = strlen(data);
1668
1669     set_selection(fe, GDK_SELECTION_PRIMARY);
1670     set_selection(fe, GDK_SELECTION_CLIPBOARD);
1671 }
1672
1673 void selection_get(GtkWidget *widget, GtkSelectionData *seldata,
1674                    guint info, guint time_stamp, gpointer data)
1675 {
1676     frontend *fe = (frontend *)data;
1677     gtk_selection_data_set(seldata, seldata->target, 8,
1678                            fe->paste_data, fe->paste_data_len);
1679 }
1680
1681 gint selection_clear(GtkWidget *widget, GdkEventSelection *seldata,
1682                      gpointer data)
1683 {
1684     frontend *fe = (frontend *)data;
1685
1686     if (fe->paste_data)
1687         sfree(fe->paste_data);
1688     fe->paste_data = NULL;
1689     fe->paste_data_len = 0;
1690     return TRUE;
1691 }
1692
1693 static void menu_copy_event(GtkMenuItem *menuitem, gpointer data)
1694 {
1695     frontend *fe = (frontend *)data;
1696     char *text;
1697
1698     text = midend_text_format(fe->me);
1699
1700     if (text) {
1701         write_clip(fe, text);
1702     } else {
1703         gdk_beep();
1704     }
1705 }
1706
1707 #ifdef OLD_FILESEL
1708
1709 static void filesel_ok(GtkButton *button, gpointer data)
1710 {
1711     frontend *fe = (frontend *)data;
1712
1713     gpointer filesel = gtk_object_get_data(GTK_OBJECT(button), "user-data");
1714
1715     const char *name =
1716         gtk_file_selection_get_filename(GTK_FILE_SELECTION(filesel));
1717
1718     fe->filesel_name = dupstr(name);
1719 }
1720
1721 static char *file_selector(frontend *fe, char *title, int save)
1722 {
1723     GtkWidget *filesel =
1724         gtk_file_selection_new(title);
1725
1726     fe->filesel_name = NULL;
1727
1728     gtk_window_set_modal(GTK_WINDOW(filesel), TRUE);
1729     gtk_object_set_data
1730         (GTK_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "user-data",
1731          (gpointer)filesel);
1732     gtk_signal_connect
1733         (GTK_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "clicked",
1734          GTK_SIGNAL_FUNC(filesel_ok), fe);
1735     gtk_signal_connect_object
1736         (GTK_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "clicked",
1737          GTK_SIGNAL_FUNC(gtk_widget_destroy), (gpointer)filesel);
1738     gtk_signal_connect_object
1739         (GTK_OBJECT(GTK_FILE_SELECTION(filesel)->cancel_button), "clicked",
1740          GTK_SIGNAL_FUNC(gtk_widget_destroy), (gpointer)filesel);
1741     gtk_signal_connect(GTK_OBJECT(filesel), "destroy",
1742                        GTK_SIGNAL_FUNC(window_destroy), NULL);
1743     gtk_widget_show(filesel);
1744     gtk_window_set_transient_for(GTK_WINDOW(filesel), GTK_WINDOW(fe->window));
1745     gtk_main();
1746
1747     return fe->filesel_name;
1748 }
1749
1750 #else
1751
1752 static char *file_selector(frontend *fe, char *title, int save)
1753 {
1754     char *filesel_name = NULL;
1755
1756     GtkWidget *filesel =
1757         gtk_file_chooser_dialog_new(title,
1758                                     GTK_WINDOW(fe->window),
1759                                     save ? GTK_FILE_CHOOSER_ACTION_SAVE :
1760                                     GTK_FILE_CHOOSER_ACTION_OPEN,
1761                                     GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
1762                                     save ? GTK_STOCK_SAVE : GTK_STOCK_OPEN,
1763                                     GTK_RESPONSE_ACCEPT,
1764                                     NULL);
1765
1766     if (gtk_dialog_run(GTK_DIALOG(filesel)) == GTK_RESPONSE_ACCEPT) {
1767         const char *name = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filesel));
1768         filesel_name = dupstr(name);
1769     }
1770
1771     gtk_widget_destroy(filesel);
1772
1773     return filesel_name;
1774 }
1775
1776 #endif
1777
1778 struct savefile_write_ctx {
1779     FILE *fp;
1780     int error;
1781 };
1782
1783 static void savefile_write(void *wctx, void *buf, int len)
1784 {
1785     struct savefile_write_ctx *ctx = (struct savefile_write_ctx *)wctx;
1786     if (fwrite(buf, 1, len, ctx->fp) < len)
1787         ctx->error = errno;
1788 }
1789
1790 static int savefile_read(void *wctx, void *buf, int len)
1791 {
1792     FILE *fp = (FILE *)wctx;
1793     int ret;
1794
1795     ret = fread(buf, 1, len, fp);
1796     return (ret == len);
1797 }
1798
1799 static void menu_save_event(GtkMenuItem *menuitem, gpointer data)
1800 {
1801     frontend *fe = (frontend *)data;
1802     char *name;
1803
1804     name = file_selector(fe, "Enter name of game file to save", TRUE);
1805
1806     if (name) {
1807         FILE *fp;
1808
1809         if ((fp = fopen(name, "r")) != NULL) {
1810             char buf[256 + FILENAME_MAX];
1811             fclose(fp);
1812             /* file exists */
1813
1814             sprintf(buf, "Are you sure you want to overwrite the"
1815                     " file \"%.*s\"?",
1816                     FILENAME_MAX, name);
1817             if (!message_box(fe->window, "Question", buf, TRUE, MB_YESNO))
1818                 return;
1819         }
1820
1821         fp = fopen(name, "w");
1822         sfree(name);
1823
1824         if (!fp) {
1825             error_box(fe->window, "Unable to open save file");
1826             return;
1827         }
1828
1829         {
1830             struct savefile_write_ctx ctx;
1831             ctx.fp = fp;
1832             ctx.error = 0;
1833             midend_serialise(fe->me, savefile_write, &ctx);
1834             fclose(fp);
1835             if (ctx.error) {
1836                 char boxmsg[512];
1837                 sprintf(boxmsg, "Error writing save file: %.400s",
1838                         strerror(errno));
1839                 error_box(fe->window, boxmsg);
1840                 return;
1841             }
1842         }
1843
1844     }
1845 }
1846
1847 static void menu_load_event(GtkMenuItem *menuitem, gpointer data)
1848 {
1849     frontend *fe = (frontend *)data;
1850     char *name, *err;
1851
1852     name = file_selector(fe, "Enter name of saved game file to load", FALSE);
1853
1854     if (name) {
1855         FILE *fp = fopen(name, "r");
1856         sfree(name);
1857
1858         if (!fp) {
1859             error_box(fe->window, "Unable to open saved game file");
1860             return;
1861         }
1862
1863         err = midend_deserialise(fe->me, savefile_read, fp);
1864
1865         fclose(fp);
1866
1867         if (err) {
1868             error_box(fe->window, err);
1869             return;
1870         }
1871
1872         changed_preset(fe);
1873         resize_fe(fe);
1874     }
1875 }
1876
1877 static void menu_solve_event(GtkMenuItem *menuitem, gpointer data)
1878 {
1879     frontend *fe = (frontend *)data;
1880     char *msg;
1881
1882     msg = midend_solve(fe->me);
1883
1884     if (msg)
1885         error_box(fe->window, msg);
1886 }
1887
1888 static void menu_restart_event(GtkMenuItem *menuitem, gpointer data)
1889 {
1890     frontend *fe = (frontend *)data;
1891
1892     midend_restart_game(fe->me);
1893 }
1894
1895 static void menu_config_event(GtkMenuItem *menuitem, gpointer data)
1896 {
1897     frontend *fe = (frontend *)data;
1898     int which = GPOINTER_TO_INT(gtk_object_get_data(GTK_OBJECT(menuitem),
1899                                                     "user-data"));
1900
1901     if (fe->preset_threaded ||
1902         (GTK_IS_CHECK_MENU_ITEM(menuitem) &&
1903          !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem))))
1904         return;
1905     changed_preset(fe);                 /* Put the old preset back! */
1906     if (!get_config(fe, which))
1907         return;
1908
1909     midend_new_game(fe->me);
1910     resize_fe(fe);
1911 }
1912
1913 static void menu_about_event(GtkMenuItem *menuitem, gpointer data)
1914 {
1915     frontend *fe = (frontend *)data;
1916     char titlebuf[256];
1917     char textbuf[1024];
1918
1919     sprintf(titlebuf, "About %.200s", thegame.name);
1920     sprintf(textbuf,
1921             "%.200s\n\n"
1922             "from Simon Tatham's Portable Puzzle Collection\n\n"
1923             "%.500s", thegame.name, ver);
1924
1925     message_box(fe->window, titlebuf, textbuf, TRUE, MB_OK);
1926 }
1927
1928 static GtkWidget *add_menu_item_with_key(frontend *fe, GtkContainer *cont,
1929                                          char *text, int key)
1930 {
1931     GtkWidget *menuitem = gtk_menu_item_new_with_label(text);
1932     int keyqual;
1933     gtk_container_add(cont, menuitem);
1934     gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
1935                         GINT_TO_POINTER(key));
1936     gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
1937                        GTK_SIGNAL_FUNC(menu_key_event), fe);
1938     switch (key & ~0x1F) {
1939       case 0x00:
1940         key += 0x60;
1941         keyqual = GDK_CONTROL_MASK;
1942         break;
1943       case 0x40:
1944         key += 0x20;
1945         keyqual = GDK_SHIFT_MASK;
1946         break;
1947       default:
1948         keyqual = 0;
1949         break;
1950     }
1951     gtk_widget_add_accelerator(menuitem,
1952                                "activate", fe->accelgroup,
1953                                key, keyqual,
1954                                GTK_ACCEL_VISIBLE);
1955     gtk_widget_show(menuitem);
1956     return menuitem;
1957 }
1958
1959 static void add_menu_separator(GtkContainer *cont)
1960 {
1961     GtkWidget *menuitem = gtk_menu_item_new();
1962     gtk_container_add(cont, menuitem);
1963     gtk_widget_show(menuitem);
1964 }
1965
1966 enum { ARG_EITHER, ARG_SAVE, ARG_ID }; /* for argtype */
1967
1968 static frontend *new_window(char *arg, int argtype, char **error)
1969 {
1970     frontend *fe;
1971     GtkBox *vbox, *hbox;
1972     GtkWidget *menubar, *menu, *menuitem;
1973     GdkPixmap *iconpm;
1974     GList *iconlist;
1975     int x, y, n;
1976     char errbuf[1024];
1977     extern char *const *const xpm_icons[];
1978     extern const int n_xpm_icons;
1979
1980     fe = snew(frontend);
1981
1982     fe->timer_active = FALSE;
1983     fe->timer_id = -1;
1984
1985     fe->me = midend_new(fe, &thegame, &gtk_drawing, fe);
1986
1987     if (arg) {
1988         char *err;
1989         FILE *fp;
1990
1991         errbuf[0] = '\0';
1992
1993         switch (argtype) {
1994           case ARG_ID:
1995             err = midend_game_id(fe->me, arg);
1996             if (!err)
1997                 midend_new_game(fe->me);
1998             else
1999                 sprintf(errbuf, "Invalid game ID: %.800s", err);
2000             break;
2001           case ARG_SAVE:
2002             fp = fopen(arg, "r");
2003             if (!fp) {
2004                 sprintf(errbuf, "Error opening file: %.800s", strerror(errno));
2005             } else {
2006                 err = midend_deserialise(fe->me, savefile_read, fp);
2007                 if (err)
2008                     sprintf(errbuf, "Invalid save file: %.800s", err);
2009                 fclose(fp);
2010             }
2011             break;
2012           default /*case ARG_EITHER*/:
2013             /*
2014              * First try treating the argument as a game ID.
2015              */
2016             err = midend_game_id(fe->me, arg);
2017             if (!err) {
2018                 /*
2019                  * It's a valid game ID.
2020                  */
2021                 midend_new_game(fe->me);
2022             } else {
2023                 FILE *fp = fopen(arg, "r");
2024                 if (!fp) {
2025                     sprintf(errbuf, "Supplied argument is neither a game ID (%.400s)"
2026                             " nor a save file (%.400s)", err, strerror(errno));
2027                 } else {
2028                     err = midend_deserialise(fe->me, savefile_read, fp);
2029                     if (err)
2030                         sprintf(errbuf, "%.800s", err);
2031                     fclose(fp);
2032                 }
2033             }
2034             break;
2035         }
2036         if (*errbuf) {
2037             *error = dupstr(errbuf);
2038             midend_free(fe->me);
2039             sfree(fe);
2040             return NULL;
2041         }
2042
2043     } else {
2044         midend_new_game(fe->me);
2045     }
2046
2047     fe->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
2048     gtk_window_set_title(GTK_WINDOW(fe->window), thegame.name);
2049
2050     vbox = GTK_BOX(gtk_vbox_new(FALSE, 0));
2051     gtk_container_add(GTK_CONTAINER(fe->window), GTK_WIDGET(vbox));
2052     gtk_widget_show(GTK_WIDGET(vbox));
2053
2054     fe->accelgroup = gtk_accel_group_new();
2055     gtk_window_add_accel_group(GTK_WINDOW(fe->window), fe->accelgroup);
2056
2057     hbox = GTK_BOX(gtk_hbox_new(FALSE, 0));
2058     gtk_box_pack_start(vbox, GTK_WIDGET(hbox), FALSE, FALSE, 0);
2059     gtk_widget_show(GTK_WIDGET(hbox));
2060
2061     menubar = gtk_menu_bar_new();
2062     gtk_box_pack_start(hbox, menubar, TRUE, TRUE, 0);
2063     gtk_widget_show(menubar);
2064
2065     menuitem = gtk_menu_item_new_with_mnemonic("_Game");
2066     gtk_container_add(GTK_CONTAINER(menubar), menuitem);
2067     gtk_widget_show(menuitem);
2068
2069     menu = gtk_menu_new();
2070     gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
2071
2072     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "New", 'n');
2073
2074     menuitem = gtk_menu_item_new_with_label("Restart");
2075     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2076     gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
2077                        GTK_SIGNAL_FUNC(menu_restart_event), fe);
2078     gtk_widget_show(menuitem);
2079
2080     menuitem = gtk_menu_item_new_with_label("Specific...");
2081     gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
2082                         GINT_TO_POINTER(CFG_DESC));
2083     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2084     gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
2085                        GTK_SIGNAL_FUNC(menu_config_event), fe);
2086     gtk_widget_show(menuitem);
2087
2088     menuitem = gtk_menu_item_new_with_label("Random Seed...");
2089     gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
2090                         GINT_TO_POINTER(CFG_SEED));
2091     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2092     gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
2093                        GTK_SIGNAL_FUNC(menu_config_event), fe);
2094     gtk_widget_show(menuitem);
2095
2096     fe->preset_radio = NULL;
2097     fe->preset_custom = NULL;
2098     fe->n_preset_menu_items = 0;
2099     fe->preset_threaded = FALSE;
2100     if ((n = midend_num_presets(fe->me)) > 0 || thegame.can_configure) {
2101         GtkWidget *submenu;
2102         int i;
2103
2104         menuitem = gtk_menu_item_new_with_mnemonic("_Type");
2105         gtk_container_add(GTK_CONTAINER(menubar), menuitem);
2106         gtk_widget_show(menuitem);
2107
2108         submenu = gtk_menu_new();
2109         gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu);
2110
2111         for (i = 0; i < n; i++) {
2112             char *name;
2113             game_params *params;
2114
2115             midend_fetch_preset(fe->me, i, &name, &params);
2116
2117             menuitem =
2118                 gtk_radio_menu_item_new_with_label(fe->preset_radio, name);
2119             fe->preset_radio =
2120                 gtk_radio_menu_item_group(GTK_RADIO_MENU_ITEM(menuitem));
2121             fe->n_preset_menu_items++;
2122             gtk_container_add(GTK_CONTAINER(submenu), menuitem);
2123             gtk_object_set_data(GTK_OBJECT(menuitem), "user-data", params);
2124             gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
2125                                GTK_SIGNAL_FUNC(menu_preset_event), fe);
2126             gtk_widget_show(menuitem);
2127         }
2128
2129         if (thegame.can_configure) {
2130             menuitem = fe->preset_custom =
2131                 gtk_radio_menu_item_new_with_label(fe->preset_radio,
2132                                                    "Custom...");
2133             fe->preset_radio =
2134                 gtk_radio_menu_item_group(GTK_RADIO_MENU_ITEM(menuitem));
2135             gtk_container_add(GTK_CONTAINER(submenu), menuitem);
2136             gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
2137                                 GPOINTER_TO_INT(CFG_SETTINGS));
2138             gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
2139                                GTK_SIGNAL_FUNC(menu_config_event), fe);
2140             gtk_widget_show(menuitem);
2141         }
2142
2143     }
2144
2145     add_menu_separator(GTK_CONTAINER(menu));
2146     menuitem = gtk_menu_item_new_with_label("Load...");
2147     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2148     gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
2149                        GTK_SIGNAL_FUNC(menu_load_event), fe);
2150     gtk_widget_show(menuitem);
2151     menuitem = gtk_menu_item_new_with_label("Save...");
2152     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2153     gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
2154                        GTK_SIGNAL_FUNC(menu_save_event), fe);
2155     gtk_widget_show(menuitem);
2156 #ifndef STYLUS_BASED
2157     add_menu_separator(GTK_CONTAINER(menu));
2158     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Undo", 'u');
2159     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Redo", 'r');
2160 #endif
2161     if (thegame.can_format_as_text_ever) {
2162         add_menu_separator(GTK_CONTAINER(menu));
2163         menuitem = gtk_menu_item_new_with_label("Copy");
2164         gtk_container_add(GTK_CONTAINER(menu), menuitem);
2165         gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
2166                            GTK_SIGNAL_FUNC(menu_copy_event), fe);
2167         gtk_widget_show(menuitem);
2168         fe->copy_menu_item = menuitem;
2169     } else {
2170         fe->copy_menu_item = NULL;
2171     }
2172     if (thegame.can_solve) {
2173         add_menu_separator(GTK_CONTAINER(menu));
2174         menuitem = gtk_menu_item_new_with_label("Solve");
2175         gtk_container_add(GTK_CONTAINER(menu), menuitem);
2176         gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
2177                            GTK_SIGNAL_FUNC(menu_solve_event), fe);
2178         gtk_widget_show(menuitem);
2179     }
2180     add_menu_separator(GTK_CONTAINER(menu));
2181     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Exit", 'q');
2182
2183     menuitem = gtk_menu_item_new_with_mnemonic("_Help");
2184     gtk_container_add(GTK_CONTAINER(menubar), menuitem);
2185     gtk_widget_show(menuitem);
2186
2187     menu = gtk_menu_new();
2188     gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
2189
2190     menuitem = gtk_menu_item_new_with_label("About");
2191     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2192     gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
2193                        GTK_SIGNAL_FUNC(menu_about_event), fe);
2194     gtk_widget_show(menuitem);
2195
2196 #ifdef STYLUS_BASED
2197     menuitem=gtk_button_new_with_mnemonic("_Redo");
2198     gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
2199                         GINT_TO_POINTER((int)('r')));
2200     gtk_signal_connect(GTK_OBJECT(menuitem), "clicked",
2201                        GTK_SIGNAL_FUNC(menu_key_event), fe);
2202     gtk_box_pack_end(hbox, menuitem, FALSE, FALSE, 0);
2203     gtk_widget_show(menuitem);
2204
2205     menuitem=gtk_button_new_with_mnemonic("_Undo");
2206     gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
2207                         GINT_TO_POINTER((int)('u')));
2208     gtk_signal_connect(GTK_OBJECT(menuitem), "clicked",
2209                        GTK_SIGNAL_FUNC(menu_key_event), fe);
2210     gtk_box_pack_end(hbox, menuitem, FALSE, FALSE, 0);
2211     gtk_widget_show(menuitem);
2212
2213     if (thegame.flags & REQUIRE_NUMPAD) {
2214         hbox = GTK_BOX(gtk_hbox_new(FALSE, 0));
2215         gtk_box_pack_start(vbox, GTK_WIDGET(hbox), FALSE, FALSE, 0);
2216         gtk_widget_show(GTK_WIDGET(hbox));
2217
2218         *((int*)errbuf)=0;
2219         errbuf[1]='\0';
2220         for(errbuf[0]='0';errbuf[0]<='9';errbuf[0]++) {
2221             menuitem=gtk_button_new_with_label(errbuf);
2222             gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
2223                                 GINT_TO_POINTER((int)(errbuf[0])));
2224             gtk_signal_connect(GTK_OBJECT(menuitem), "clicked",
2225                                GTK_SIGNAL_FUNC(menu_key_event), fe);
2226             gtk_box_pack_start(hbox, menuitem, TRUE, TRUE, 0);
2227             gtk_widget_show(menuitem);
2228         }
2229     }
2230 #endif /* STYLUS_BASED */
2231
2232     changed_preset(fe);
2233
2234     snaffle_colours(fe);
2235
2236     if (midend_wants_statusbar(fe->me)) {
2237         GtkWidget *viewport;
2238         GtkRequisition req;
2239
2240         viewport = gtk_viewport_new(NULL, NULL);
2241         gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
2242         fe->statusbar = gtk_statusbar_new();
2243         gtk_container_add(GTK_CONTAINER(viewport), fe->statusbar);
2244         gtk_widget_show(viewport);
2245         gtk_box_pack_end(vbox, viewport, FALSE, FALSE, 0);
2246         gtk_widget_show(fe->statusbar);
2247         fe->statusctx = gtk_statusbar_get_context_id
2248             (GTK_STATUSBAR(fe->statusbar), "game");
2249         gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx,
2250                            "test");
2251         gtk_widget_size_request(fe->statusbar, &req);
2252 #if 0
2253         /* For GTK 2.0, should we be using gtk_widget_set_size_request? */
2254 #endif
2255         gtk_widget_set_usize(viewport, -1, req.height);
2256     } else
2257         fe->statusbar = NULL;
2258
2259     fe->area = gtk_drawing_area_new();
2260 #if GTK_CHECK_VERSION(2,0,0)
2261     GTK_WIDGET_UNSET_FLAGS(fe->area, GTK_DOUBLE_BUFFERED);
2262 #endif
2263     get_size(fe, &x, &y);
2264     gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
2265     fe->w = x;
2266     fe->h = y;
2267
2268     gtk_box_pack_end(vbox, fe->area, TRUE, TRUE, 0);
2269
2270     clear_backing_store(fe);
2271     fe->fonts = NULL;
2272     fe->nfonts = fe->fontsize = 0;
2273
2274     fe->paste_data = NULL;
2275     fe->paste_data_len = 0;
2276
2277     gtk_signal_connect(GTK_OBJECT(fe->window), "destroy",
2278                        GTK_SIGNAL_FUNC(destroy), fe);
2279     gtk_signal_connect(GTK_OBJECT(fe->window), "key_press_event",
2280                        GTK_SIGNAL_FUNC(key_event), fe);
2281     gtk_signal_connect(GTK_OBJECT(fe->area), "button_press_event",
2282                        GTK_SIGNAL_FUNC(button_event), fe);
2283     gtk_signal_connect(GTK_OBJECT(fe->area), "button_release_event",
2284                        GTK_SIGNAL_FUNC(button_event), fe);
2285     gtk_signal_connect(GTK_OBJECT(fe->area), "motion_notify_event",
2286                        GTK_SIGNAL_FUNC(motion_event), fe);
2287     gtk_signal_connect(GTK_OBJECT(fe->area), "selection_get",
2288                        GTK_SIGNAL_FUNC(selection_get), fe);
2289     gtk_signal_connect(GTK_OBJECT(fe->area), "selection_clear_event",
2290                        GTK_SIGNAL_FUNC(selection_clear), fe);
2291     gtk_signal_connect(GTK_OBJECT(fe->area), "expose_event",
2292                        GTK_SIGNAL_FUNC(expose_area), fe);
2293     gtk_signal_connect(GTK_OBJECT(fe->window), "map_event",
2294                        GTK_SIGNAL_FUNC(map_window), fe);
2295     gtk_signal_connect(GTK_OBJECT(fe->area), "configure_event",
2296                        GTK_SIGNAL_FUNC(configure_area), fe);
2297
2298     gtk_widget_add_events(GTK_WIDGET(fe->area),
2299                           GDK_BUTTON_PRESS_MASK |
2300                           GDK_BUTTON_RELEASE_MASK |
2301                           GDK_BUTTON_MOTION_MASK |
2302                           GDK_POINTER_MOTION_HINT_MASK);
2303
2304     if (n_xpm_icons) {
2305         gtk_widget_realize(fe->window);
2306         iconpm = gdk_pixmap_create_from_xpm_d(fe->window->window, NULL,
2307                                               NULL, (gchar **)xpm_icons[0]);
2308         gdk_window_set_icon(fe->window->window, NULL, iconpm, NULL);
2309         iconlist = NULL;
2310         for (n = 0; n < n_xpm_icons; n++) {
2311             iconlist =
2312                 g_list_append(iconlist,
2313                               gdk_pixbuf_new_from_xpm_data((const gchar **)
2314                                                            xpm_icons[n]));
2315         }
2316         gdk_window_set_icon_list(fe->window->window, iconlist);
2317     }
2318
2319     gtk_widget_show(fe->area);
2320     gtk_widget_show(fe->window);
2321
2322     /*
2323      * Now that we've established the preferred size of the window,
2324      * reduce the drawing area's size request so the user can shrink
2325      * the window.
2326      */
2327     gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), 1, 1);
2328     set_window_background(fe, 0);
2329
2330     return fe;
2331 }
2332
2333 char *fgetline(FILE *fp)
2334 {
2335     char *ret = snewn(512, char);
2336     int size = 512, len = 0;
2337     while (fgets(ret + len, size - len, fp)) {
2338         len += strlen(ret + len);
2339         if (ret[len-1] == '\n')
2340             break;                     /* got a newline, we're done */
2341         size = len + 512;
2342         ret = sresize(ret, size, char);
2343     }
2344     if (len == 0) {                    /* first fgets returned NULL */
2345         sfree(ret);
2346         return NULL;
2347     }
2348     ret[len] = '\0';
2349     return ret;
2350 }
2351
2352 int main(int argc, char **argv)
2353 {
2354     char *pname = argv[0];
2355     char *error;
2356     int ngenerate = 0, print = FALSE, px = 1, py = 1;
2357     int soln = FALSE, colour = FALSE;
2358     float scale = 1.0F;
2359     float redo_proportion = 0.0F;
2360     char *savefile = NULL, *savesuffix = NULL;
2361     char *arg = NULL;
2362     int argtype = ARG_EITHER;
2363     char *screenshot_file = NULL;
2364     int doing_opts = TRUE;
2365     int ac = argc;
2366     char **av = argv;
2367     char errbuf[500];
2368
2369     /*
2370      * Command line parsing in this function is rather fiddly,
2371      * because GTK wants to have a go at argc/argv _first_ - and
2372      * yet we can't let it, because gtk_init() will bomb out if it
2373      * can't open an X display, whereas in fact we want to permit
2374      * our --generate and --print modes to run without an X
2375      * display.
2376      * 
2377      * So what we do is:
2378      *  - we parse the command line ourselves, without modifying
2379      *    argc/argv
2380      *  - if we encounter an error which might plausibly be the
2381      *    result of a GTK command line (i.e. not detailed errors in
2382      *    particular options of ours) we store the error message
2383      *    and terminate parsing.
2384      *  - if we got enough out of the command line to know it
2385      *    specifies a non-X mode of operation, we either display
2386      *    the stored error and return failure, or if there is no
2387      *    stored error we do the non-X operation and return
2388      *    success.
2389      *  - otherwise, we go straight to gtk_init().
2390      */
2391
2392     errbuf[0] = '\0';
2393     while (--ac > 0) {
2394         char *p = *++av;
2395         if (doing_opts && !strcmp(p, "--version")) {
2396             printf("%s, from Simon Tatham's Portable Puzzle Collection\n%s\n",
2397                    thegame.name, ver);
2398             return 0;
2399         } else if (doing_opts && !strcmp(p, "--generate")) {
2400             if (--ac > 0) {
2401                 ngenerate = atoi(*++av);
2402                 if (!ngenerate) {
2403                     fprintf(stderr, "%s: '--generate' expected a number\n",
2404                             pname);
2405                     return 1;
2406                 }
2407             } else
2408                 ngenerate = 1;
2409         } else if (doing_opts && !strcmp(p, "--save")) {
2410             if (--ac > 0) {
2411                 savefile = *++av;
2412             } else {
2413                 fprintf(stderr, "%s: '--save' expected a filename\n",
2414                         pname);
2415                 return 1;
2416             }
2417         } else if (doing_opts && (!strcmp(p, "--save-suffix") ||
2418                                   !strcmp(p, "--savesuffix"))) {
2419             if (--ac > 0) {
2420                 savesuffix = *++av;
2421             } else {
2422                 fprintf(stderr, "%s: '--save-suffix' expected a filename\n",
2423                         pname);
2424                 return 1;
2425             }
2426         } else if (doing_opts && !strcmp(p, "--print")) {
2427             if (!thegame.can_print) {
2428                 fprintf(stderr, "%s: this game does not support printing\n",
2429                         pname);
2430                 return 1;
2431             }
2432             print = TRUE;
2433             if (--ac > 0) {
2434                 char *dim = *++av;
2435                 if (sscanf(dim, "%dx%d", &px, &py) != 2) {
2436                     fprintf(stderr, "%s: unable to parse argument '%s' to "
2437                             "'--print'\n", pname, dim);
2438                     return 1;
2439                 }
2440             } else {
2441                 px = py = 1;
2442             }
2443         } else if (doing_opts && !strcmp(p, "--scale")) {
2444             if (--ac > 0) {
2445                 scale = atof(*++av);
2446             } else {
2447                 fprintf(stderr, "%s: no argument supplied to '--scale'\n",
2448                         pname);
2449                 return 1;
2450             }
2451         } else if (doing_opts && !strcmp(p, "--redo")) {
2452             /*
2453              * This is an internal option which I don't expect
2454              * users to have any particular use for. The effect of
2455              * --redo is that once the game has been loaded and
2456              * initialised, the next move in the redo chain is
2457              * replayed, and the game screen is redrawn part way
2458              * through the making of the move. This is only
2459              * meaningful if there _is_ a next move in the redo
2460              * chain, which means in turn that this option is only
2461              * useful if you're also passing a save file on the
2462              * command line.
2463              *
2464              * This option is used by the script which generates
2465              * the puzzle icons and website screenshots, and I
2466              * don't imagine it's useful for anything else.
2467              * (Unless, I suppose, users don't like my screenshots
2468              * and want to generate their own in the same way for
2469              * some repackaged version of the puzzles.)
2470              */
2471             if (--ac > 0) {
2472                 redo_proportion = atof(*++av);
2473             } else {
2474                 fprintf(stderr, "%s: no argument supplied to '--redo'\n",
2475                         pname);
2476                 return 1;
2477             }
2478         } else if (doing_opts && !strcmp(p, "--screenshot")) {
2479             /*
2480              * Another internal option for the icon building
2481              * script. This causes a screenshot of the central
2482              * drawing area (i.e. not including the menu bar or
2483              * status bar) to be saved to a PNG file once the
2484              * window has been drawn, and then the application
2485              * quits immediately.
2486              */
2487             if (--ac > 0) {
2488                 screenshot_file = *++av;
2489             } else {
2490                 fprintf(stderr, "%s: no argument supplied to '--screenshot'\n",
2491                         pname);
2492                 return 1;
2493             }
2494         } else if (doing_opts && (!strcmp(p, "--with-solutions") ||
2495                                   !strcmp(p, "--with-solution") ||
2496                                   !strcmp(p, "--with-solns") ||
2497                                   !strcmp(p, "--with-soln") ||
2498                                   !strcmp(p, "--solutions") ||
2499                                   !strcmp(p, "--solution") ||
2500                                   !strcmp(p, "--solns") ||
2501                                   !strcmp(p, "--soln"))) {
2502             soln = TRUE;
2503         } else if (doing_opts && !strcmp(p, "--colour")) {
2504             if (!thegame.can_print_in_colour) {
2505                 fprintf(stderr, "%s: this game does not support colour"
2506                         " printing\n", pname);
2507                 return 1;
2508             }
2509             colour = TRUE;
2510         } else if (doing_opts && !strcmp(p, "--load")) {
2511             argtype = ARG_SAVE;
2512         } else if (doing_opts && !strcmp(p, "--game")) {
2513             argtype = ARG_ID;
2514         } else if (doing_opts && !strcmp(p, "--")) {
2515             doing_opts = FALSE;
2516         } else if (!doing_opts || p[0] != '-') {
2517             if (arg) {
2518                 fprintf(stderr, "%s: more than one argument supplied\n",
2519                         pname);
2520                 return 1;
2521             }
2522             arg = p;
2523         } else {
2524             sprintf(errbuf, "%.100s: unrecognised option '%.100s'\n",
2525                     pname, p);
2526             break;
2527         }
2528     }
2529
2530     if (*errbuf) {
2531         fputs(errbuf, stderr);
2532         return 1;
2533     }
2534
2535     /*
2536      * Special standalone mode for generating puzzle IDs on the
2537      * command line. Useful for generating puzzles to be printed
2538      * out and solved offline (for puzzles where that even makes
2539      * sense - Solo, for example, is a lot more pencil-and-paper
2540      * friendly than Twiddle!)
2541      * 
2542      * Usage:
2543      * 
2544      *   <puzzle-name> --generate [<n> [<params>]]
2545      * 
2546      * <n>, if present, is the number of puzzle IDs to generate.
2547      * <params>, if present, is the same type of parameter string
2548      * you would pass to the puzzle when running it in GUI mode,
2549      * including optional extras such as the expansion factor in
2550      * Rectangles and the difficulty level in Solo.
2551      * 
2552      * If you specify <params>, you must also specify <n> (although
2553      * you may specify it to be 1). Sorry; that was the
2554      * simplest-to-parse command-line syntax I came up with.
2555      */
2556     if (ngenerate > 0 || print || savefile || savesuffix) {
2557         int i, n = 1;
2558         midend *me;
2559         char *id;
2560         document *doc = NULL;
2561
2562         n = ngenerate;
2563
2564         me = midend_new(NULL, &thegame, NULL, NULL);
2565         i = 0;
2566
2567         if (savefile && !savesuffix)
2568             savesuffix = "";
2569         if (!savefile && savesuffix)
2570             savefile = "";
2571
2572         if (print)
2573             doc = document_new(px, py, scale);
2574
2575         /*
2576          * In this loop, we either generate a game ID or read one
2577          * from stdin depending on whether we're in generate mode;
2578          * then we either write it to stdout or print it, depending
2579          * on whether we're in print mode. Thus, this loop handles
2580          * generate-to-stdout, print-from-stdin and generate-and-
2581          * immediately-print modes.
2582          * 
2583          * (It could also handle a copy-stdin-to-stdout mode,
2584          * although there's currently no combination of options
2585          * which will cause this loop to be activated in that mode.
2586          * It wouldn't be _entirely_ pointless, though, because
2587          * stdin could contain bare params strings or random-seed
2588          * IDs, and stdout would contain nothing but fully
2589          * generated descriptive game IDs.)
2590          */
2591         while (ngenerate == 0 || i < n) {
2592             char *pstr, *err;
2593
2594             if (ngenerate == 0) {
2595                 pstr = fgetline(stdin);
2596                 if (!pstr)
2597                     break;
2598                 pstr[strcspn(pstr, "\r\n")] = '\0';
2599             } else {
2600                 if (arg) {
2601                     pstr = snewn(strlen(arg) + 40, char);
2602
2603                     strcpy(pstr, arg);
2604                     if (i > 0 && strchr(arg, '#'))
2605                         sprintf(pstr + strlen(pstr), "-%d", i);
2606                 } else
2607                     pstr = NULL;
2608             }
2609
2610             if (pstr) {
2611                 err = midend_game_id(me, pstr);
2612                 if (err) {
2613                     fprintf(stderr, "%s: error parsing '%s': %s\n",
2614                             pname, pstr, err);
2615                     return 1;
2616                 }
2617             }
2618             sfree(pstr);
2619
2620             midend_new_game(me);
2621
2622             if (doc) {
2623                 err = midend_print_puzzle(me, doc, soln);
2624                 if (err) {
2625                     fprintf(stderr, "%s: error in printing: %s\n", pname, err);
2626                     return 1;
2627                 }
2628             }
2629             if (savefile) {
2630                 struct savefile_write_ctx ctx;
2631                 char *realname = snewn(40 + strlen(savefile) +
2632                                        strlen(savesuffix), char);
2633                 sprintf(realname, "%s%d%s", savefile, i, savesuffix);
2634                 ctx.fp = fopen(realname, "w");
2635                 if (!ctx.fp) {
2636                     fprintf(stderr, "%s: open: %s\n", realname,
2637                             strerror(errno));
2638                     return 1;
2639                 }
2640                 sfree(realname);
2641                 midend_serialise(me, savefile_write, &ctx);
2642                 if (ctx.error) {
2643                     fprintf(stderr, "%s: write: %s\n", realname,
2644                             strerror(ctx.error));
2645                     return 1;
2646                 }
2647                 if (fclose(ctx.fp)) {
2648                     fprintf(stderr, "%s: close: %s\n", realname,
2649                             strerror(errno));
2650                     return 1;
2651                 }
2652             }
2653             if (!doc && !savefile) {
2654                 id = midend_get_game_id(me);
2655                 puts(id);
2656                 sfree(id);
2657             }
2658
2659             i++;
2660         }
2661
2662         if (doc) {
2663             psdata *ps = ps_init(stdout, colour);
2664             document_print(doc, ps_drawing_api(ps));
2665             document_free(doc);
2666             ps_free(ps);
2667         }
2668
2669         midend_free(me);
2670
2671         return 0;
2672     } else {
2673         frontend *fe;
2674
2675         gtk_init(&argc, &argv);
2676
2677         fe = new_window(arg, argtype, &error);
2678
2679         if (!fe) {
2680             fprintf(stderr, "%s: %s\n", pname, error);
2681             return 1;
2682         }
2683
2684         if (screenshot_file) {
2685             /*
2686              * Some puzzles will not redraw their entire area if
2687              * given a partially completed animation, which means
2688              * we must redraw now and _then_ redraw again after
2689              * freezing the move timer.
2690              */
2691             midend_force_redraw(fe->me);
2692         }
2693
2694         if (redo_proportion) {
2695             /* Start a redo. */
2696             midend_process_key(fe->me, 0, 0, 'r');
2697             /* And freeze the timer at the specified position. */
2698             midend_freeze_timer(fe->me, redo_proportion);
2699         }
2700
2701         if (screenshot_file) {
2702             save_screenshot_png(fe, screenshot_file);
2703             exit(0);
2704         }
2705
2706         gtk_main();
2707     }
2708
2709     return 0;
2710 }