chiark / gitweb /
Update changelog for 20170923.ff218728-0+iwj2~3.gbpc58e0c release
[sgt-puzzles.git] / gtk.c
1 /*
2  * gtk.c: GTK front end for my puzzle collection.
3  */
4
5 #ifndef _POSIX_C_SOURCE
6 #define _POSIX_C_SOURCE 1 /* for PATH_MAX */
7 #endif
8
9 #include <stdio.h>
10 #include <assert.h>
11 #include <stdlib.h>
12 #include <time.h>
13 #include <stdarg.h>
14 #include <string.h>
15 #include <errno.h>
16 #include <math.h>
17 #include <limits.h>
18 #include <unistd.h>
19 #include <locale.h>
20
21 #include <sys/time.h>
22 #include <sys/resource.h>
23
24 #include <gtk/gtk.h>
25 #include <gdk/gdkkeysyms.h>
26
27 #include <gdk-pixbuf/gdk-pixbuf.h>
28
29 #include <gdk/gdkx.h>
30 #include <X11/Xlib.h>
31 #include <X11/Xutil.h>
32 #include <X11/Xatom.h>
33
34 #include "puzzles.h"
35
36 #if GTK_CHECK_VERSION(2,0,0)
37 # define USE_PANGO
38 # ifdef PANGO_VERSION_CHECK
39 #  if PANGO_VERSION_CHECK(1,8,0)
40 #   define HAVE_SENSIBLE_ABSOLUTE_SIZE_FUNCTION
41 #  endif
42 # endif
43 #endif
44 #if !GTK_CHECK_VERSION(2,4,0)
45 # define OLD_FILESEL
46 #endif
47 #if GTK_CHECK_VERSION(2,8,0)
48 # define USE_CAIRO
49 # if GTK_CHECK_VERSION(3,0,0) || defined(GDK_DISABLE_DEPRECATED)
50 #  define USE_CAIRO_WITHOUT_PIXMAP
51 # endif
52 #endif
53
54 #if GTK_CHECK_VERSION(3,0,0)
55 /* The old names are still more concise! */
56 #define gtk_hbox_new(x,y) gtk_box_new(GTK_ORIENTATION_HORIZONTAL,y)
57 #define gtk_vbox_new(x,y) gtk_box_new(GTK_ORIENTATION_VERTICAL,y)
58 /* GTK 3 has retired stock button labels */
59 #define LABEL_OK "_OK"
60 #define LABEL_CANCEL "_Cancel"
61 #define LABEL_NO "_No"
62 #define LABEL_YES "_Yes"
63 #define LABEL_SAVE "_Save"
64 #define LABEL_OPEN "_Open"
65 #define gtk_button_new_with_our_label gtk_button_new_with_mnemonic
66 #else
67 #define LABEL_OK GTK_STOCK_OK
68 #define LABEL_CANCEL GTK_STOCK_CANCEL
69 #define LABEL_NO GTK_STOCK_NO
70 #define LABEL_YES GTK_STOCK_YES
71 #define LABEL_SAVE GTK_STOCK_SAVE
72 #define LABEL_OPEN GTK_STOCK_OPEN
73 #define gtk_button_new_with_our_label gtk_button_new_from_stock
74 #endif
75
76 /* #undef USE_CAIRO */
77 /* #define NO_THICK_LINE */
78 #ifdef DEBUGGING
79 static FILE *debug_fp = NULL;
80
81 void dputs(const char *buf)
82 {
83     if (!debug_fp) {
84         debug_fp = fopen("debug.log", "w");
85     }
86
87     fputs(buf, stderr);
88
89     if (debug_fp) {
90         fputs(buf, debug_fp);
91         fflush(debug_fp);
92     }
93 }
94
95 void debug_printf(const char *fmt, ...)
96 {
97     char buf[4096];
98     va_list ap;
99
100     va_start(ap, fmt);
101     vsprintf(buf, fmt, ap);
102     dputs(buf);
103     va_end(ap);
104 }
105 #endif
106
107 /* ----------------------------------------------------------------------
108  * Error reporting functions used elsewhere.
109  */
110
111 void fatal(const char *fmt, ...)
112 {
113     va_list ap;
114
115     fprintf(stderr, "fatal error: ");
116
117     va_start(ap, fmt);
118     vfprintf(stderr, fmt, ap);
119     va_end(ap);
120
121     fprintf(stderr, "\n");
122     exit(1);
123 }
124
125 /* ----------------------------------------------------------------------
126  * GTK front end to puzzles.
127  */
128
129 static void changed_preset(frontend *fe);
130
131 struct font {
132 #ifdef USE_PANGO
133     PangoFontDescription *desc;
134 #else
135     GdkFont *font;
136 #endif
137     int type;
138     int size;
139 };
140
141 /*
142  * This structure holds all the data relevant to a single window.
143  * In principle this would allow us to open multiple independent
144  * puzzle windows, although I can't currently see any real point in
145  * doing so. I'm just coding cleanly because there's no
146  * particularly good reason not to.
147  */
148 struct frontend {
149     GtkWidget *window;
150     GtkAccelGroup *dummy_accelgroup;
151     GtkWidget *area;
152     GtkWidget *statusbar;
153     GtkWidget *menubar;
154 #if GTK_CHECK_VERSION(3,20,0)
155     GtkCssProvider *css_provider;
156 #endif
157     guint statusctx;
158     int w, h;
159     midend *me;
160 #ifdef USE_CAIRO
161     const float *colours;
162     cairo_t *cr;
163     cairo_surface_t *image;
164 #ifndef USE_CAIRO_WITHOUT_PIXMAP
165     GdkPixmap *pixmap;
166 #endif
167     GdkColor background;               /* for painting outside puzzle area */
168 #else
169     GdkPixmap *pixmap;
170     GdkGC *gc;
171     GdkColor *colours;
172     GdkColormap *colmap;
173     int backgroundindex;               /* which of colours[] is background */
174 #endif
175     int ncolours;
176     int bbox_l, bbox_r, bbox_u, bbox_d;
177     int timer_active, timer_id;
178     struct timeval last_time;
179     struct font *fonts;
180     int nfonts, fontsize;
181     config_item *cfg;
182     int cfg_which, cfgret;
183     GtkWidget *cfgbox;
184     void *paste_data;
185     int paste_data_len;
186     int pw, ph;                        /* pixmap size (w, h are area size) */
187     int ox, oy;                        /* offset of pixmap in drawing area */
188 #ifdef OLD_FILESEL
189     char *filesel_name;
190 #endif
191     GSList *preset_radio;
192     int preset_threaded;
193     GtkWidget *preset_custom;
194     GtkWidget *copy_menu_item;
195 #if !GTK_CHECK_VERSION(3,0,0)
196     int drawing_area_shrink_pending;
197     int menubar_is_local;
198 #endif
199 #if GTK_CHECK_VERSION(3,0,0)
200     /*
201      * This is used to get round an annoying lack of GTK notification
202      * message. If we request a window resize with
203      * gtk_window_resize(), we normally get back a "configure" event
204      * on the window and on its drawing area, and we respond to the
205      * latter by doing an appropriate resize of the puzzle. If the
206      * window is maximised, so that gtk_window_resize() _doesn't_
207      * change its size, then that configure event never shows up. But
208      * if we requested the resize in response to a change of puzzle
209      * parameters (say, the user selected a differently-sized preset
210      * from the menu), then we would still like to be _notified_ that
211      * the window size was staying the same, so that we can respond by
212      * choosing an appropriate tile size for the new puzzle preset in
213      * the existing window size.
214      *
215      * Fortunately, in GTK 3, we may not get a "configure" event on
216      * the drawing area in this situation, but we still get a
217      * "size_allocate" event on the whole window (which, in other
218      * situations when we _do_ get a "configure" on the area, turns up
219      * second). So we treat _that_ event as indicating that if the
220      * "configure" event hasn't already shown up then it's not going
221      * to arrive.
222      *
223      * This flag is where we bookkeep this system. On
224      * gtk_window_resize we set this flag to true; the area's
225      * configure handler sets it back to false; then if that doesn't
226      * happen, the window's size_allocate handler does a fallback
227      * puzzle resize when it sees this flag still set to true.
228      */
229     int awaiting_resize_ack;
230 #endif
231 };
232
233 struct blitter {
234 #ifdef USE_CAIRO
235     cairo_surface_t *image;
236 #else
237     GdkPixmap *pixmap;
238 #endif
239     int w, h, x, y;
240 };
241
242 void get_random_seed(void **randseed, int *randseedsize)
243 {
244     struct timeval *tvp = snew(struct timeval);
245     gettimeofday(tvp, NULL);
246     *randseed = (void *)tvp;
247     *randseedsize = sizeof(struct timeval);
248 }
249
250 void frontend_default_colour(frontend *fe, float *output)
251 {
252 #if !GTK_CHECK_VERSION(3,0,0)
253     /*
254      * Use the widget style's default background colour as the
255      * background for the puzzle drawing area.
256      */
257     GdkColor col = gtk_widget_get_style(fe->window)->bg[GTK_STATE_NORMAL];
258     output[0] = col.red / 65535.0;
259     output[1] = col.green / 65535.0;
260     output[2] = col.blue / 65535.0;
261 #else
262     /*
263      * GTK 3 has decided that there's no such thing as a 'default
264      * background colour' any more, because widget styles might set
265      * the background to something more complicated like a background
266      * image. We don't want to get into overlaying our entire puzzle
267      * on an arbitrary background image, so we'll just make up a
268      * reasonable shade of grey.
269      */
270     output[0] = output[1] = output[2] = 0.9F;
271 #endif
272 }
273
274 void gtk_status_bar(void *handle, const char *text)
275 {
276     frontend *fe = (frontend *)handle;
277
278     assert(fe->statusbar);
279
280     gtk_statusbar_pop(GTK_STATUSBAR(fe->statusbar), fe->statusctx);
281     gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx, text);
282 }
283
284 /* ----------------------------------------------------------------------
285  * Cairo drawing functions.
286  */
287
288 #ifdef USE_CAIRO
289
290 static void setup_drawing(frontend *fe)
291 {
292     fe->cr = cairo_create(fe->image);
293     cairo_set_antialias(fe->cr, CAIRO_ANTIALIAS_GRAY);
294     cairo_set_line_width(fe->cr, 1.0);
295     cairo_set_line_cap(fe->cr, CAIRO_LINE_CAP_SQUARE);
296     cairo_set_line_join(fe->cr, CAIRO_LINE_JOIN_ROUND);
297 }
298
299 static void teardown_drawing(frontend *fe)
300 {
301     cairo_destroy(fe->cr);
302     fe->cr = NULL;
303
304 #ifndef USE_CAIRO_WITHOUT_PIXMAP
305     {
306         cairo_t *cr = gdk_cairo_create(fe->pixmap);
307         cairo_set_source_surface(cr, fe->image, 0, 0);
308         cairo_rectangle(cr,
309                         fe->bbox_l - 1,
310                         fe->bbox_u - 1,
311                         fe->bbox_r - fe->bbox_l + 2,
312                         fe->bbox_d - fe->bbox_u + 2);
313         cairo_fill(cr);
314         cairo_destroy(cr);
315     }
316 #endif
317 }
318
319 static void snaffle_colours(frontend *fe)
320 {
321     fe->colours = midend_colours(fe->me, &fe->ncolours);
322 }
323
324 static void set_colour(frontend *fe, int colour)
325 {
326     cairo_set_source_rgb(fe->cr,
327                          fe->colours[3*colour + 0],
328                          fe->colours[3*colour + 1],
329                          fe->colours[3*colour + 2]);
330 }
331
332 static void set_window_background(frontend *fe, int colour)
333 {
334 #if GTK_CHECK_VERSION(3,20,0)
335     char css_buf[512];
336     sprintf(css_buf, ".background { "
337             "background-color: #%02x%02x%02x; }",
338             (unsigned)(fe->colours[3*colour + 0] * 255),
339             (unsigned)(fe->colours[3*colour + 1] * 255),
340             (unsigned)(fe->colours[3*colour + 2] * 255));
341     if (!fe->css_provider)
342         fe->css_provider = gtk_css_provider_new();
343     if (!gtk_css_provider_load_from_data(
344             GTK_CSS_PROVIDER(fe->css_provider), css_buf, -1, NULL))
345         assert(0 && "Couldn't load CSS");
346     gtk_style_context_add_provider(
347         gtk_widget_get_style_context(fe->window),
348         GTK_STYLE_PROVIDER(fe->css_provider),
349         GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
350     gtk_style_context_add_provider(
351         gtk_widget_get_style_context(fe->area),
352         GTK_STYLE_PROVIDER(fe->css_provider),
353         GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
354 #elif GTK_CHECK_VERSION(3,0,0)
355     GdkRGBA rgba;
356     rgba.red = fe->colours[3*colour + 0];
357     rgba.green = fe->colours[3*colour + 1];
358     rgba.blue = fe->colours[3*colour + 2];
359     rgba.alpha = 1.0;
360     gdk_window_set_background_rgba(gtk_widget_get_window(fe->area), &rgba);
361     gdk_window_set_background_rgba(gtk_widget_get_window(fe->window), &rgba);
362 #else
363     GdkColormap *colmap;
364
365     colmap = gdk_colormap_get_system();
366     fe->background.red = fe->colours[3*colour + 0] * 65535;
367     fe->background.green = fe->colours[3*colour + 1] * 65535;
368     fe->background.blue = fe->colours[3*colour + 2] * 65535;
369     if (!gdk_colormap_alloc_color(colmap, &fe->background, FALSE, FALSE)) {
370         g_error("couldn't allocate background (#%02x%02x%02x)\n",
371                 fe->background.red >> 8, fe->background.green >> 8,
372                 fe->background.blue >> 8);
373     }
374     gdk_window_set_background(gtk_widget_get_window(fe->area),
375                               &fe->background);
376     gdk_window_set_background(gtk_widget_get_window(fe->window),
377                               &fe->background);
378 #endif
379 }
380
381 static PangoLayout *make_pango_layout(frontend *fe)
382 {
383     return (pango_cairo_create_layout(fe->cr));
384 }
385
386 static void draw_pango_layout(frontend *fe, PangoLayout *layout,
387                               int x, int y)
388 {
389     cairo_move_to(fe->cr, x, y);
390     pango_cairo_show_layout(fe->cr, layout);
391 }
392
393 static void save_screenshot_png(frontend *fe, const char *screenshot_file)
394 {
395     cairo_surface_write_to_png(fe->image, screenshot_file);
396 }
397
398 static void do_clip(frontend *fe, int x, int y, int w, int h)
399 {
400     cairo_new_path(fe->cr);
401     cairo_rectangle(fe->cr, x, y, w, h);
402     cairo_clip(fe->cr);
403 }
404
405 static void do_unclip(frontend *fe)
406 {
407     cairo_reset_clip(fe->cr);
408 }
409
410 static void do_draw_rect(frontend *fe, int x, int y, int w, int h)
411 {
412     cairo_save(fe->cr);
413     cairo_new_path(fe->cr);
414     cairo_set_antialias(fe->cr, CAIRO_ANTIALIAS_NONE);
415     cairo_rectangle(fe->cr, x, y, w, h);
416     cairo_fill(fe->cr);
417     cairo_restore(fe->cr);
418 }
419
420 static void do_draw_line(frontend *fe, int x1, int y1, int x2, int y2)
421 {
422     cairo_new_path(fe->cr);
423     cairo_move_to(fe->cr, x1 + 0.5, y1 + 0.5);
424     cairo_line_to(fe->cr, x2 + 0.5, y2 + 0.5);
425     cairo_stroke(fe->cr);
426 }
427
428 static void do_draw_thick_line(frontend *fe, float thickness,
429                                float x1, float y1, float x2, float y2)
430 {
431     cairo_save(fe->cr);
432     cairo_set_line_width(fe->cr, thickness);
433     cairo_new_path(fe->cr);
434     cairo_move_to(fe->cr, x1, y1);
435     cairo_line_to(fe->cr, x2, y2);
436     cairo_stroke(fe->cr);
437     cairo_restore(fe->cr);
438 }
439
440 static void do_draw_poly(frontend *fe, int *coords, int npoints,
441                          int fillcolour, int outlinecolour)
442 {
443     int i;
444
445     cairo_new_path(fe->cr);
446     for (i = 0; i < npoints; i++)
447         cairo_line_to(fe->cr, coords[i*2] + 0.5, coords[i*2 + 1] + 0.5);
448     cairo_close_path(fe->cr);
449     if (fillcolour >= 0) {
450         set_colour(fe, fillcolour);
451         cairo_fill_preserve(fe->cr);
452     }
453     assert(outlinecolour >= 0);
454     set_colour(fe, outlinecolour);
455     cairo_stroke(fe->cr);
456 }
457
458 static void do_draw_circle(frontend *fe, int cx, int cy, int radius,
459                            int fillcolour, int outlinecolour)
460 {
461     cairo_new_path(fe->cr);
462     cairo_arc(fe->cr, cx + 0.5, cy + 0.5, radius, 0, 2*PI);
463     cairo_close_path(fe->cr);           /* Just in case... */
464     if (fillcolour >= 0) {
465         set_colour(fe, fillcolour);
466         cairo_fill_preserve(fe->cr);
467     }
468     assert(outlinecolour >= 0);
469     set_colour(fe, outlinecolour);
470     cairo_stroke(fe->cr);
471 }
472
473 static void setup_blitter(blitter *bl, int w, int h)
474 {
475     bl->image = cairo_image_surface_create(CAIRO_FORMAT_RGB24, w, h);
476 }
477
478 static void teardown_blitter(blitter *bl)
479 {
480     cairo_surface_destroy(bl->image);
481 }
482
483 static void do_blitter_save(frontend *fe, blitter *bl, int x, int y)
484 {
485     cairo_t *cr = cairo_create(bl->image);
486
487     cairo_set_source_surface(cr, fe->image, -x, -y);
488     cairo_paint(cr);
489     cairo_destroy(cr);
490 }
491
492 static void do_blitter_load(frontend *fe, blitter *bl, int x, int y)
493 {
494     cairo_set_source_surface(fe->cr, bl->image, x, y);
495     cairo_paint(fe->cr);
496 }
497
498 static void clear_backing_store(frontend *fe)
499 {
500     fe->image = NULL;
501 }
502
503 static void wipe_and_maybe_destroy_cairo(frontend *fe, cairo_t *cr,
504                                          int destroy)
505 {
506     cairo_set_source_rgb(cr, fe->colours[0], fe->colours[1], fe->colours[2]);
507     cairo_paint(cr);
508     if (destroy)
509         cairo_destroy(cr);
510 }
511
512 static void setup_backing_store(frontend *fe)
513 {
514 #ifndef USE_CAIRO_WITHOUT_PIXMAP
515     fe->pixmap = gdk_pixmap_new(gtk_widget_get_window(fe->area),
516                                 fe->pw, fe->ph, -1);
517 #endif
518     fe->image = cairo_image_surface_create(CAIRO_FORMAT_RGB24,
519                                            fe->pw, fe->ph);
520
521     wipe_and_maybe_destroy_cairo(fe, cairo_create(fe->image), TRUE);
522 #ifndef USE_CAIRO_WITHOUT_PIXMAP
523     wipe_and_maybe_destroy_cairo(fe, gdk_cairo_create(fe->pixmap), TRUE);
524 #endif
525 #if GTK_CHECK_VERSION(3,22,0)
526     {
527         GdkWindow *gdkwin;
528         cairo_region_t *region;
529         GdkDrawingContext *drawctx;
530         cairo_t *cr;
531
532         gdkwin = gtk_widget_get_window(fe->area);
533         region = gdk_window_get_clip_region(gdkwin);
534         drawctx = gdk_window_begin_draw_frame(gdkwin, region);
535         cr = gdk_drawing_context_get_cairo_context(drawctx);
536         wipe_and_maybe_destroy_cairo(fe, cr, FALSE);
537         gdk_window_end_draw_frame(gdkwin, drawctx);
538         cairo_region_destroy(region);
539     }
540 #else
541     wipe_and_maybe_destroy_cairo(
542         fe, gdk_cairo_create(gtk_widget_get_window(fe->area)), TRUE);
543 #endif
544 }
545
546 static int backing_store_ok(frontend *fe)
547 {
548     return (!!fe->image);
549 }
550
551 static void teardown_backing_store(frontend *fe)
552 {
553     cairo_surface_destroy(fe->image);
554 #ifndef USE_CAIRO_WITHOUT_PIXMAP
555     gdk_pixmap_unref(fe->pixmap);
556 #endif
557     fe->image = NULL;
558 }
559
560 #endif
561
562 /* ----------------------------------------------------------------------
563  * GDK drawing functions.
564  */
565
566 #ifndef USE_CAIRO
567
568 static void setup_drawing(frontend *fe)
569 {
570     fe->gc = gdk_gc_new(fe->area->window);
571 }
572
573 static void teardown_drawing(frontend *fe)
574 {
575     gdk_gc_unref(fe->gc);
576     fe->gc = NULL;
577 }
578
579 static void snaffle_colours(frontend *fe)
580 {
581     int i, ncolours;
582     float *colours;
583     gboolean *success;
584
585     fe->colmap = gdk_colormap_get_system();
586     colours = midend_colours(fe->me, &ncolours);
587     fe->ncolours = ncolours;
588     fe->colours = snewn(ncolours, GdkColor);
589     for (i = 0; i < ncolours; i++) {
590         fe->colours[i].red = colours[i*3] * 0xFFFF;
591         fe->colours[i].green = colours[i*3+1] * 0xFFFF;
592         fe->colours[i].blue = colours[i*3+2] * 0xFFFF;
593     }
594     success = snewn(ncolours, gboolean);
595     gdk_colormap_alloc_colors(fe->colmap, fe->colours, ncolours,
596                               FALSE, FALSE, success);
597     for (i = 0; i < ncolours; i++) {
598         if (!success[i]) {
599             g_error("couldn't allocate colour %d (#%02x%02x%02x)\n",
600                     i, fe->colours[i].red >> 8,
601                     fe->colours[i].green >> 8,
602                     fe->colours[i].blue >> 8);
603         }
604     }
605 }
606
607 static void set_window_background(frontend *fe, int colour)
608 {
609     fe->backgroundindex = colour;
610     gdk_window_set_background(fe->area->window, &fe->colours[colour]);
611     gdk_window_set_background(fe->window->window, &fe->colours[colour]);
612 }
613
614 static void set_colour(frontend *fe, int colour)
615 {
616     gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
617 }
618
619 #ifdef USE_PANGO
620 static PangoLayout *make_pango_layout(frontend *fe)
621 {
622     return (pango_layout_new(gtk_widget_get_pango_context(fe->area)));
623 }
624
625 static void draw_pango_layout(frontend *fe, PangoLayout *layout,
626                               int x, int y)
627 {
628     gdk_draw_layout(fe->pixmap, fe->gc, x, y, layout);
629 }
630 #endif
631
632 static void save_screenshot_png(frontend *fe, const char *screenshot_file)
633 {
634     GdkPixbuf *pb;
635     GError *gerror = NULL;
636
637     midend_redraw(fe->me);
638
639     pb = gdk_pixbuf_get_from_drawable(NULL, fe->pixmap,
640                                       NULL, 0, 0, 0, 0, -1, -1);
641     gdk_pixbuf_save(pb, screenshot_file, "png", &gerror, NULL);
642 }
643
644 static void do_clip(frontend *fe, int x, int y, int w, int h)
645 {
646     GdkRectangle rect;
647
648     rect.x = x;
649     rect.y = y;
650     rect.width = w;
651     rect.height = h;
652     gdk_gc_set_clip_rectangle(fe->gc, &rect);
653 }
654
655 static void do_unclip(frontend *fe)
656 {
657     GdkRectangle rect;
658
659     rect.x = 0;
660     rect.y = 0;
661     rect.width = fe->w;
662     rect.height = fe->h;
663     gdk_gc_set_clip_rectangle(fe->gc, &rect);
664 }
665
666 static void do_draw_rect(frontend *fe, int x, int y, int w, int h)
667 {
668     gdk_draw_rectangle(fe->pixmap, fe->gc, 1, x, y, w, h);
669 }
670
671 static void do_draw_line(frontend *fe, int x1, int y1, int x2, int y2)
672 {
673     gdk_draw_line(fe->pixmap, fe->gc, x1, y1, x2, y2);
674 }
675
676 static void do_draw_thick_line(frontend *fe, float thickness,
677                                float x1, float y1, float x2, float y2)
678 {
679     GdkGCValues save;
680
681     gdk_gc_get_values(fe->gc, &save);
682     gdk_gc_set_line_attributes(fe->gc,
683                                thickness,
684                                GDK_LINE_SOLID,
685                                GDK_CAP_BUTT,
686                                GDK_JOIN_BEVEL);
687     gdk_draw_line(fe->pixmap, fe->gc, x1, y1, x2, y2);
688     gdk_gc_set_line_attributes(fe->gc,
689                                save.line_width,
690                                save.line_style,
691                                save.cap_style,
692                                save.join_style);
693 }
694
695 static void do_draw_poly(frontend *fe, int *coords, int npoints,
696                          int fillcolour, int outlinecolour)
697 {
698     GdkPoint *points = snewn(npoints, GdkPoint);
699     int i;
700
701     for (i = 0; i < npoints; i++) {
702         points[i].x = coords[i*2];
703         points[i].y = coords[i*2+1];
704     }
705
706     if (fillcolour >= 0) {
707         set_colour(fe, fillcolour);
708         gdk_draw_polygon(fe->pixmap, fe->gc, TRUE, points, npoints);
709     }
710     assert(outlinecolour >= 0);
711     set_colour(fe, outlinecolour);
712
713     /*
714      * In principle we ought to be able to use gdk_draw_polygon for
715      * the outline as well. In fact, it turns out to interact badly
716      * with a clipping region, for no terribly obvious reason, so I
717      * draw the outline as a sequence of lines instead.
718      */
719     for (i = 0; i < npoints; i++)
720         gdk_draw_line(fe->pixmap, fe->gc,
721                       points[i].x, points[i].y,
722                       points[(i+1)%npoints].x, points[(i+1)%npoints].y);
723
724     sfree(points);
725 }
726
727 static void do_draw_circle(frontend *fe, int cx, int cy, int radius,
728                            int fillcolour, int outlinecolour)
729 {
730     if (fillcolour >= 0) {
731         set_colour(fe, fillcolour);
732         gdk_draw_arc(fe->pixmap, fe->gc, TRUE,
733                      cx - radius, cy - radius,
734                      2 * radius, 2 * radius, 0, 360 * 64);
735     }
736
737     assert(outlinecolour >= 0);
738     set_colour(fe, outlinecolour);
739     gdk_draw_arc(fe->pixmap, fe->gc, FALSE,
740                  cx - radius, cy - radius,
741                  2 * radius, 2 * radius, 0, 360 * 64);
742 }
743
744 static void setup_blitter(blitter *bl, int w, int h)
745 {
746     /*
747      * We can't create the pixmap right now, because fe->window
748      * might not yet exist. So we just cache w and h and create it
749      * during the firs call to blitter_save.
750      */
751     bl->pixmap = NULL;
752 }
753
754 static void teardown_blitter(blitter *bl)
755 {
756     if (bl->pixmap)
757         gdk_pixmap_unref(bl->pixmap);
758 }
759
760 static void do_blitter_save(frontend *fe, blitter *bl, int x, int y)
761 {
762     if (!bl->pixmap)
763         bl->pixmap = gdk_pixmap_new(fe->area->window, bl->w, bl->h, -1);
764     gdk_draw_pixmap(bl->pixmap,
765                     fe->area->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
766                     fe->pixmap,
767                     x, y, 0, 0, bl->w, bl->h);
768 }
769
770 static void do_blitter_load(frontend *fe, blitter *bl, int x, int y)
771 {
772     assert(bl->pixmap);
773     gdk_draw_pixmap(fe->pixmap,
774                     fe->area->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
775                     bl->pixmap,
776                     0, 0, x, y, bl->w, bl->h);
777 }
778
779 static void clear_backing_store(frontend *fe)
780 {
781     fe->pixmap = NULL;
782 }
783
784 static void setup_backing_store(frontend *fe)
785 {
786     GdkGC *gc;
787
788     fe->pixmap = gdk_pixmap_new(fe->area->window, fe->pw, fe->ph, -1);
789
790     gc = gdk_gc_new(fe->area->window);
791     gdk_gc_set_foreground(gc, &fe->colours[0]);
792     gdk_draw_rectangle(fe->pixmap, gc, 1, 0, 0, fe->pw, fe->ph);
793     gdk_draw_rectangle(fe->area->window, gc, 1, 0, 0, fe->w, fe->h);
794     gdk_gc_unref(gc);
795 }
796
797 static int backing_store_ok(frontend *fe)
798 {
799     return (!!fe->pixmap);
800 }
801
802 static void teardown_backing_store(frontend *fe)
803 {
804     gdk_pixmap_unref(fe->pixmap);
805     fe->pixmap = NULL;
806 }
807
808 #endif
809
810 #ifndef USE_CAIRO_WITHOUT_PIXMAP
811 static void repaint_rectangle(frontend *fe, GtkWidget *widget,
812                               int x, int y, int w, int h)
813 {
814     GdkGC *gc = gdk_gc_new(gtk_widget_get_window(widget));
815 #ifdef USE_CAIRO
816     gdk_gc_set_foreground(gc, &fe->background);
817 #else
818     gdk_gc_set_foreground(gc, &fe->colours[fe->backgroundindex]);
819 #endif
820     if (x < fe->ox) {
821         gdk_draw_rectangle(gtk_widget_get_window(widget), gc,
822                            TRUE, x, y, fe->ox - x, h);
823         w -= (fe->ox - x);
824         x = fe->ox;
825     }
826     if (y < fe->oy) {
827         gdk_draw_rectangle(gtk_widget_get_window(widget), gc,
828                            TRUE, x, y, w, fe->oy - y);
829         h -= (fe->oy - y);
830         y = fe->oy;
831     }
832     if (w > fe->pw) {
833         gdk_draw_rectangle(gtk_widget_get_window(widget), gc,
834                            TRUE, x + fe->pw, y, w - fe->pw, h);
835         w = fe->pw;
836     }
837     if (h > fe->ph) {
838         gdk_draw_rectangle(gtk_widget_get_window(widget), gc,
839                            TRUE, x, y + fe->ph, w, h - fe->ph);
840         h = fe->ph;
841     }
842     gdk_draw_pixmap(gtk_widget_get_window(widget), gc, fe->pixmap,
843                     x - fe->ox, y - fe->oy, x, y, w, h);
844     gdk_gc_unref(gc);
845 }
846 #endif
847
848 /* ----------------------------------------------------------------------
849  * Pango font functions.
850  */
851
852 #ifdef USE_PANGO
853
854 static void add_font(frontend *fe, int index, int fonttype, int fontsize)
855 {
856     /*
857      * Use Pango to find the closest match to the requested
858      * font.
859      */
860     PangoFontDescription *fd;
861
862     fd = pango_font_description_new();
863     /* `Monospace' and `Sans' are meta-families guaranteed to exist */
864     pango_font_description_set_family(fd, fonttype == FONT_FIXED ?
865                                       "Monospace" : "Sans");
866     pango_font_description_set_weight(fd, PANGO_WEIGHT_BOLD);
867     /*
868      * I found some online Pango documentation which
869      * described a function called
870      * pango_font_description_set_absolute_size(), which is
871      * _exactly_ what I want here. Unfortunately, none of
872      * my local Pango installations have it (presumably
873      * they're too old), so I'm going to have to hack round
874      * it by figuring out the point size myself. This
875      * limits me to X and probably also breaks in later
876      * Pango installations, so ideally I should add another
877      * CHECK_VERSION type ifdef and use set_absolute_size
878      * where available. All very annoying.
879      */
880 #ifdef HAVE_SENSIBLE_ABSOLUTE_SIZE_FUNCTION
881     pango_font_description_set_absolute_size(fd, PANGO_SCALE*fontsize);
882 #else
883     {
884         Display *d = GDK_DISPLAY();
885         int s = DefaultScreen(d);
886         double resolution =
887             (PANGO_SCALE * 72.27 / 25.4) *
888             ((double) DisplayWidthMM(d, s) / DisplayWidth (d, s));
889         pango_font_description_set_size(fd, resolution * fontsize);
890     }
891 #endif
892     fe->fonts[index].desc = fd;
893 }
894
895 static void align_and_draw_text(frontend *fe,
896                                 int index, int align, int x, int y,
897                                 const char *text)
898 {
899     PangoLayout *layout;
900     PangoRectangle rect;
901
902     layout = make_pango_layout(fe);
903
904     /*
905      * Create a layout.
906      */
907     pango_layout_set_font_description(layout, fe->fonts[index].desc);
908     pango_layout_set_text(layout, text, strlen(text));
909     pango_layout_get_pixel_extents(layout, NULL, &rect);
910
911     if (align & ALIGN_VCENTRE)
912         rect.y -= rect.height / 2;
913     else
914         rect.y -= rect.height;
915
916     if (align & ALIGN_HCENTRE)
917         rect.x -= rect.width / 2;
918     else if (align & ALIGN_HRIGHT)
919         rect.x -= rect.width;
920
921     draw_pango_layout(fe, layout, rect.x + x, rect.y + y);
922
923     g_object_unref(layout);
924 }
925
926 #endif
927
928 /* ----------------------------------------------------------------------
929  * Old-fashioned font functions.
930  */
931
932 #ifndef USE_PANGO
933
934 static void add_font(int index, int fonttype, int fontsize)
935 {
936     /*
937      * In GTK 1.2, I don't know of any plausible way to
938      * pick a suitable font, so I'm just going to be
939      * tedious.
940      */
941     fe->fonts[i].font = gdk_font_load(fonttype == FONT_FIXED ?
942                                       "fixed" : "variable");
943 }
944
945 static void align_and_draw_text(int index, int align, int x, int y,
946                                 const char *text)
947 {
948     int lb, rb, wid, asc, desc;
949
950     /*
951      * Measure vertical string extents with respect to the same
952      * string always...
953      */
954     gdk_string_extents(fe->fonts[i].font,
955                        "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
956                        &lb, &rb, &wid, &asc, &desc);
957     if (align & ALIGN_VCENTRE)
958         y += asc - (asc+desc)/2;
959     else
960         y += asc;
961
962     /*
963      * ... but horizontal extents with respect to the provided
964      * string. This means that multiple pieces of text centred
965      * on the same y-coordinate don't have different baselines.
966      */
967     gdk_string_extents(fe->fonts[i].font, text,
968                        &lb, &rb, &wid, &asc, &desc);
969
970     if (align & ALIGN_HCENTRE)
971         x -= wid / 2;
972     else if (align & ALIGN_HRIGHT)
973         x -= wid;
974
975     /*
976      * Actually draw the text.
977      */
978     gdk_draw_string(fe->pixmap, fe->fonts[i].font, fe->gc, x, y, text);
979 }
980
981 #endif
982
983 /* ----------------------------------------------------------------------
984  * The exported drawing functions.
985  */
986
987 void gtk_start_draw(void *handle)
988 {
989     frontend *fe = (frontend *)handle;
990     fe->bbox_l = fe->w;
991     fe->bbox_r = 0;
992     fe->bbox_u = fe->h;
993     fe->bbox_d = 0;
994     setup_drawing(fe);
995 }
996
997 void gtk_clip(void *handle, int x, int y, int w, int h)
998 {
999     frontend *fe = (frontend *)handle;
1000     do_clip(fe, x, y, w, h);
1001 }
1002
1003 void gtk_unclip(void *handle)
1004 {
1005     frontend *fe = (frontend *)handle;
1006     do_unclip(fe);
1007 }
1008
1009 void gtk_draw_text(void *handle, int x, int y, int fonttype, int fontsize,
1010                    int align, int colour, const char *text)
1011 {
1012     frontend *fe = (frontend *)handle;
1013     int i;
1014
1015     /*
1016      * Find or create the font.
1017      */
1018     for (i = 0; i < fe->nfonts; i++)
1019         if (fe->fonts[i].type == fonttype && fe->fonts[i].size == fontsize)
1020             break;
1021
1022     if (i == fe->nfonts) {
1023         if (fe->fontsize <= fe->nfonts) {
1024             fe->fontsize = fe->nfonts + 10;
1025             fe->fonts = sresize(fe->fonts, fe->fontsize, struct font);
1026         }
1027
1028         fe->nfonts++;
1029
1030         fe->fonts[i].type = fonttype;
1031         fe->fonts[i].size = fontsize;
1032         add_font(fe, i, fonttype, fontsize);
1033     }
1034
1035     /*
1036      * Do the job.
1037      */
1038     set_colour(fe, colour);
1039     align_and_draw_text(fe, i, align, x, y, text);
1040 }
1041
1042 void gtk_draw_rect(void *handle, int x, int y, int w, int h, int colour)
1043 {
1044     frontend *fe = (frontend *)handle;
1045     set_colour(fe, colour);
1046     do_draw_rect(fe, x, y, w, h);
1047 }
1048
1049 void gtk_draw_line(void *handle, int x1, int y1, int x2, int y2, int colour)
1050 {
1051     frontend *fe = (frontend *)handle;
1052     set_colour(fe, colour);
1053     do_draw_line(fe, x1, y1, x2, y2);
1054 }
1055
1056 void gtk_draw_thick_line(void *handle, float thickness,
1057                          float x1, float y1, float x2, float y2, int colour)
1058 {
1059     frontend *fe = (frontend *)handle;
1060     set_colour(fe, colour);
1061     do_draw_thick_line(fe, thickness, x1, y1, x2, y2);
1062 }
1063
1064 void gtk_draw_poly(void *handle, int *coords, int npoints,
1065                    int fillcolour, int outlinecolour)
1066 {
1067     frontend *fe = (frontend *)handle;
1068     do_draw_poly(fe, coords, npoints, fillcolour, outlinecolour);
1069 }
1070
1071 void gtk_draw_circle(void *handle, int cx, int cy, int radius,
1072                      int fillcolour, int outlinecolour)
1073 {
1074     frontend *fe = (frontend *)handle;
1075     do_draw_circle(fe, cx, cy, radius, fillcolour, outlinecolour);
1076 }
1077
1078 blitter *gtk_blitter_new(void *handle, int w, int h)
1079 {
1080     blitter *bl = snew(blitter);
1081     setup_blitter(bl, w, h);
1082     bl->w = w;
1083     bl->h = h;
1084     return bl;
1085 }
1086
1087 void gtk_blitter_free(void *handle, blitter *bl)
1088 {
1089     teardown_blitter(bl);
1090     sfree(bl);
1091 }
1092
1093 void gtk_blitter_save(void *handle, blitter *bl, int x, int y)
1094 {
1095     frontend *fe = (frontend *)handle;
1096     do_blitter_save(fe, bl, x, y);
1097     bl->x = x;
1098     bl->y = y;
1099 }
1100
1101 void gtk_blitter_load(void *handle, blitter *bl, int x, int y)
1102 {
1103     frontend *fe = (frontend *)handle;
1104     if (x == BLITTER_FROMSAVED && y == BLITTER_FROMSAVED) {
1105         x = bl->x;
1106         y = bl->y;
1107     }
1108     do_blitter_load(fe, bl, x, y);
1109 }
1110
1111 void gtk_draw_update(void *handle, int x, int y, int w, int h)
1112 {
1113     frontend *fe = (frontend *)handle;
1114     if (fe->bbox_l > x  ) fe->bbox_l = x  ;
1115     if (fe->bbox_r < x+w) fe->bbox_r = x+w;
1116     if (fe->bbox_u > y  ) fe->bbox_u = y  ;
1117     if (fe->bbox_d < y+h) fe->bbox_d = y+h;
1118 }
1119
1120 void gtk_end_draw(void *handle)
1121 {
1122     frontend *fe = (frontend *)handle;
1123
1124     teardown_drawing(fe);
1125
1126     if (fe->bbox_l < fe->bbox_r && fe->bbox_u < fe->bbox_d) {
1127 #ifdef USE_CAIRO_WITHOUT_PIXMAP
1128         gtk_widget_queue_draw_area(fe->area,
1129                                    fe->bbox_l - 1 + fe->ox,
1130                                    fe->bbox_u - 1 + fe->oy,
1131                                    fe->bbox_r - fe->bbox_l + 2,
1132                                    fe->bbox_d - fe->bbox_u + 2);
1133 #else
1134         repaint_rectangle(fe, fe->area,
1135                           fe->bbox_l - 1 + fe->ox,
1136                           fe->bbox_u - 1 + fe->oy,
1137                           fe->bbox_r - fe->bbox_l + 2,
1138                           fe->bbox_d - fe->bbox_u + 2);
1139 #endif
1140     }
1141 }
1142
1143 #ifdef USE_PANGO
1144 char *gtk_text_fallback(void *handle, const char *const *strings, int nstrings)
1145 {
1146     /*
1147      * We assume Pango can cope with any UTF-8 likely to be emitted
1148      * by a puzzle.
1149      */
1150     return dupstr(strings[0]);
1151 }
1152 #endif
1153
1154 const struct drawing_api gtk_drawing = {
1155     gtk_draw_text,
1156     gtk_draw_rect,
1157     gtk_draw_line,
1158     gtk_draw_poly,
1159     gtk_draw_circle,
1160     gtk_draw_update,
1161     gtk_clip,
1162     gtk_unclip,
1163     gtk_start_draw,
1164     gtk_end_draw,
1165     gtk_status_bar,
1166     gtk_blitter_new,
1167     gtk_blitter_free,
1168     gtk_blitter_save,
1169     gtk_blitter_load,
1170     NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */
1171     NULL, NULL,                        /* line_width, line_dotted */
1172 #ifdef USE_PANGO
1173     gtk_text_fallback,
1174 #else
1175     NULL,
1176 #endif
1177 #ifdef NO_THICK_LINE
1178     NULL,
1179 #else
1180     gtk_draw_thick_line,
1181 #endif
1182 };
1183
1184 static void destroy(GtkWidget *widget, gpointer data)
1185 {
1186     frontend *fe = (frontend *)data;
1187     deactivate_timer(fe);
1188     midend_free(fe->me);
1189     gtk_main_quit();
1190 }
1191
1192 static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
1193 {
1194     frontend *fe = (frontend *)data;
1195     int keyval;
1196     int shift = (event->state & GDK_SHIFT_MASK) ? MOD_SHFT : 0;
1197     int ctrl = (event->state & GDK_CONTROL_MASK) ? MOD_CTRL : 0;
1198
1199     if (!backing_store_ok(fe))
1200         return TRUE;
1201
1202     /* Handle mnemonics. */
1203     if (gtk_window_activate_key(GTK_WINDOW(fe->window), event))
1204         return TRUE;
1205
1206     if (event->keyval == GDK_KEY_Up)
1207         keyval = shift | ctrl | CURSOR_UP;
1208     else if (event->keyval == GDK_KEY_KP_Up ||
1209              event->keyval == GDK_KEY_KP_8)
1210         keyval = MOD_NUM_KEYPAD | '8';
1211     else if (event->keyval == GDK_KEY_Down)
1212         keyval = shift | ctrl | CURSOR_DOWN;
1213     else if (event->keyval == GDK_KEY_KP_Down ||
1214              event->keyval == GDK_KEY_KP_2)
1215         keyval = MOD_NUM_KEYPAD | '2';
1216     else if (event->keyval == GDK_KEY_Left)
1217         keyval = shift | ctrl | CURSOR_LEFT;
1218     else if (event->keyval == GDK_KEY_KP_Left ||
1219              event->keyval == GDK_KEY_KP_4)
1220         keyval = MOD_NUM_KEYPAD | '4';
1221     else if (event->keyval == GDK_KEY_Right)
1222         keyval = shift | ctrl | CURSOR_RIGHT;
1223     else if (event->keyval == GDK_KEY_KP_Right ||
1224              event->keyval == GDK_KEY_KP_6)
1225         keyval = MOD_NUM_KEYPAD | '6';
1226     else if (event->keyval == GDK_KEY_KP_Home ||
1227              event->keyval == GDK_KEY_KP_7)
1228         keyval = MOD_NUM_KEYPAD | '7';
1229     else if (event->keyval == GDK_KEY_KP_End ||
1230              event->keyval == GDK_KEY_KP_1)
1231         keyval = MOD_NUM_KEYPAD | '1';
1232     else if (event->keyval == GDK_KEY_KP_Page_Up ||
1233              event->keyval == GDK_KEY_KP_9)
1234         keyval = MOD_NUM_KEYPAD | '9';
1235     else if (event->keyval == GDK_KEY_KP_Page_Down ||
1236              event->keyval == GDK_KEY_KP_3)
1237         keyval = MOD_NUM_KEYPAD | '3';
1238     else if (event->keyval == GDK_KEY_KP_Insert ||
1239              event->keyval == GDK_KEY_KP_0)
1240         keyval = MOD_NUM_KEYPAD | '0';
1241     else if (event->keyval == GDK_KEY_KP_Begin ||
1242              event->keyval == GDK_KEY_KP_5)
1243         keyval = MOD_NUM_KEYPAD | '5';
1244     else if (event->keyval == GDK_KEY_BackSpace ||
1245              event->keyval == GDK_KEY_Delete ||
1246              event->keyval == GDK_KEY_KP_Delete)
1247         keyval = '\177';
1248     else if ((event->keyval == 'z' || event->keyval == 'Z') && shift && ctrl)
1249         keyval = UI_REDO;
1250     else if (event->string[0] && !event->string[1])
1251         keyval = (unsigned char)event->string[0];
1252     else
1253         keyval = -1;
1254
1255     if (keyval >= 0 &&
1256         !midend_process_key(fe->me, 0, 0, keyval))
1257         gtk_widget_destroy(fe->window);
1258
1259     return TRUE;
1260 }
1261
1262 static gint button_event(GtkWidget *widget, GdkEventButton *event,
1263                          gpointer data)
1264 {
1265     frontend *fe = (frontend *)data;
1266     int button;
1267
1268     if (!backing_store_ok(fe))
1269         return TRUE;
1270
1271     if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE)
1272         return TRUE;
1273
1274     if (event->button == 2 || (event->state & GDK_SHIFT_MASK))
1275         button = MIDDLE_BUTTON;
1276     else if (event->button == 3 || (event->state & GDK_MOD1_MASK))
1277         button = RIGHT_BUTTON;
1278     else if (event->button == 1)
1279         button = LEFT_BUTTON;
1280     else if (event->button == 8 && event->type == GDK_BUTTON_PRESS)
1281         button = 'u';
1282     else if (event->button == 9 && event->type == GDK_BUTTON_PRESS)
1283         button = 'r';
1284     else
1285         return FALSE;                  /* don't even know what button! */
1286
1287     if (event->type == GDK_BUTTON_RELEASE && button >= LEFT_BUTTON)
1288         button += LEFT_RELEASE - LEFT_BUTTON;
1289
1290     if (!midend_process_key(fe->me, event->x - fe->ox,
1291                             event->y - fe->oy, button))
1292         gtk_widget_destroy(fe->window);
1293
1294     return TRUE;
1295 }
1296
1297 static gint motion_event(GtkWidget *widget, GdkEventMotion *event,
1298                          gpointer data)
1299 {
1300     frontend *fe = (frontend *)data;
1301     int button;
1302
1303     if (!backing_store_ok(fe))
1304         return TRUE;
1305
1306     if (event->state & (GDK_BUTTON2_MASK | GDK_SHIFT_MASK))
1307         button = MIDDLE_DRAG;
1308     else if (event->state & GDK_BUTTON1_MASK)
1309         button = LEFT_DRAG;
1310     else if (event->state & GDK_BUTTON3_MASK)
1311         button = RIGHT_DRAG;
1312     else
1313         return FALSE;                  /* don't even know what button! */
1314
1315     if (!midend_process_key(fe->me, event->x - fe->ox,
1316                             event->y - fe->oy, button))
1317         gtk_widget_destroy(fe->window);
1318 #if GTK_CHECK_VERSION(2,12,0)
1319     gdk_event_request_motions(event);
1320 #else
1321     gdk_window_get_pointer(gtk_widget_get_window(widget), NULL, NULL, NULL);
1322 #endif
1323
1324     return TRUE;
1325 }
1326
1327 #if GTK_CHECK_VERSION(3,0,0)
1328 static gint draw_area(GtkWidget *widget, cairo_t *cr, gpointer data)
1329 {
1330     frontend *fe = (frontend *)data;
1331     GdkRectangle dirtyrect;
1332
1333     gdk_cairo_get_clip_rectangle(cr, &dirtyrect);
1334     cairo_set_source_surface(cr, fe->image, fe->ox, fe->oy);
1335     cairo_rectangle(cr, dirtyrect.x, dirtyrect.y,
1336                     dirtyrect.width, dirtyrect.height);
1337     cairo_fill(cr);
1338
1339     return TRUE;
1340 }
1341 #else
1342 static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
1343                         gpointer data)
1344 {
1345     frontend *fe = (frontend *)data;
1346
1347     if (backing_store_ok(fe)) {
1348 #ifdef USE_CAIRO_WITHOUT_PIXMAP
1349         cairo_t *cr = gdk_cairo_create(gtk_widget_get_window(widget));
1350         cairo_set_source_surface(cr, fe->image, fe->ox, fe->oy);
1351         cairo_rectangle(cr, event->area.x, event->area.y,
1352                         event->area.width, event->area.height);
1353         cairo_fill(cr);
1354         cairo_destroy(cr);
1355 #else
1356         repaint_rectangle(fe, widget,
1357                           event->area.x, event->area.y,
1358                           event->area.width, event->area.height);
1359 #endif
1360     }
1361     return TRUE;
1362 }
1363 #endif
1364
1365 static gint map_window(GtkWidget *widget, GdkEvent *event,
1366                        gpointer data)
1367 {
1368     frontend *fe = (frontend *)data;
1369
1370     /*
1371      * Apparently we need to do this because otherwise the status
1372      * bar will fail to update immediately. Annoying, but there we
1373      * go.
1374      */
1375     gtk_widget_queue_draw(fe->window);
1376
1377     return TRUE;
1378 }
1379
1380 static void resize_puzzle_to_area(frontend *fe, int x, int y)
1381 {
1382     int oldw = fe->w, oldpw = fe->pw, oldh = fe->h, oldph = fe->ph;
1383
1384     fe->w = x;
1385     fe->h = y;
1386     midend_size(fe->me, &x, &y, TRUE);
1387     fe->pw = x;
1388     fe->ph = y;
1389     fe->ox = (fe->w - fe->pw) / 2;
1390     fe->oy = (fe->h - fe->ph) / 2;
1391
1392     if (oldw != fe->w || oldpw != fe->pw ||
1393         oldh != fe->h || oldph != fe->ph || !backing_store_ok(fe)) {
1394         if (backing_store_ok(fe))
1395             teardown_backing_store(fe);
1396         setup_backing_store(fe);
1397     }
1398
1399     midend_force_redraw(fe->me);
1400 }
1401
1402 static gint configure_area(GtkWidget *widget,
1403                            GdkEventConfigure *event, gpointer data)
1404 {
1405     frontend *fe = (frontend *)data;
1406     resize_puzzle_to_area(fe, event->width, event->height);
1407     fe->awaiting_resize_ack = FALSE;
1408     return TRUE;
1409 }
1410
1411 #if GTK_CHECK_VERSION(3,0,0)
1412 static void window_size_alloc(GtkWidget *widget, GtkAllocation *allocation,
1413                               gpointer data)
1414 {
1415     frontend *fe = (frontend *)data;
1416     if (fe->awaiting_resize_ack) {
1417         GtkAllocation a;
1418         gtk_widget_get_allocation(fe->area, &a);
1419         resize_puzzle_to_area(fe, a.width, a.height);
1420         fe->awaiting_resize_ack = FALSE;
1421     }
1422 }
1423 #endif
1424
1425 static gint timer_func(gpointer data)
1426 {
1427     frontend *fe = (frontend *)data;
1428
1429     if (fe->timer_active) {
1430         struct timeval now;
1431         float elapsed;
1432         gettimeofday(&now, NULL);
1433         elapsed = ((now.tv_usec - fe->last_time.tv_usec) * 0.000001F +
1434                    (now.tv_sec - fe->last_time.tv_sec));
1435         midend_timer(fe->me, elapsed);  /* may clear timer_active */
1436         fe->last_time = now;
1437     }
1438
1439     return fe->timer_active;
1440 }
1441
1442 void deactivate_timer(frontend *fe)
1443 {
1444     if (!fe)
1445         return;                        /* can happen due to --generate */
1446     if (fe->timer_active)
1447         g_source_remove(fe->timer_id);
1448     fe->timer_active = FALSE;
1449 }
1450
1451 void activate_timer(frontend *fe)
1452 {
1453     if (!fe)
1454         return;                        /* can happen due to --generate */
1455     if (!fe->timer_active) {
1456         fe->timer_id = g_timeout_add(20, timer_func, fe);
1457         gettimeofday(&fe->last_time, NULL);
1458     }
1459     fe->timer_active = TRUE;
1460 }
1461
1462 static void window_destroy(GtkWidget *widget, gpointer data)
1463 {
1464     gtk_main_quit();
1465 }
1466
1467 static int win_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data)
1468 {
1469     GObject *cancelbutton = G_OBJECT(data);
1470
1471     /*
1472      * `Escape' effectively clicks the cancel button
1473      */
1474     if (event->keyval == GDK_KEY_Escape) {
1475         g_signal_emit_by_name(cancelbutton, "clicked");
1476         return TRUE;
1477     }
1478
1479     return FALSE;
1480 }
1481
1482 enum { MB_OK, MB_YESNO };
1483
1484 static void align_label(GtkLabel *label, double x, double y)
1485 {
1486 #if GTK_CHECK_VERSION(3,16,0)
1487     gtk_label_set_xalign(label, x);
1488     gtk_label_set_yalign(label, y);
1489 #elif GTK_CHECK_VERSION(3,14,0)
1490     gtk_widget_set_halign(GTK_WIDGET(label),
1491                           x == 0 ? GTK_ALIGN_START :
1492                           x == 1 ? GTK_ALIGN_END : GTK_ALIGN_CENTER);
1493     gtk_widget_set_valign(GTK_WIDGET(label),
1494                           y == 0 ? GTK_ALIGN_START :
1495                           y == 1 ? GTK_ALIGN_END : GTK_ALIGN_CENTER);
1496 #else
1497     gtk_misc_set_alignment(GTK_MISC(label), x, y);
1498 #endif
1499 }
1500
1501 #if GTK_CHECK_VERSION(3,0,0)
1502 int message_box(GtkWidget *parent, const char *title, const char *msg,
1503                 int centre, int type)
1504 {
1505     GtkWidget *window;
1506     gint ret;
1507
1508     window = gtk_message_dialog_new
1509         (GTK_WINDOW(parent),
1510          (GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
1511          (type == MB_OK ? GTK_MESSAGE_INFO : GTK_MESSAGE_QUESTION),
1512          (type == MB_OK ? GTK_BUTTONS_OK   : GTK_BUTTONS_YES_NO),
1513          "%s", msg);
1514     gtk_window_set_title(GTK_WINDOW(window), title);
1515     ret = gtk_dialog_run(GTK_DIALOG(window));
1516     gtk_widget_destroy(window);
1517     return (type == MB_OK ? TRUE : (ret == GTK_RESPONSE_YES));
1518 }
1519 #else /* GTK_CHECK_VERSION(3,0,0) */
1520 static void msgbox_button_clicked(GtkButton *button, gpointer data)
1521 {
1522     GtkWidget *window = GTK_WIDGET(data);
1523     int v, *ip;
1524
1525     ip = (int *)g_object_get_data(G_OBJECT(window), "user-data");
1526     v = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "user-data"));
1527     *ip = v;
1528
1529     gtk_widget_destroy(GTK_WIDGET(data));
1530 }
1531
1532 int message_box(GtkWidget *parent, char *title, char *msg, int centre,
1533                 int type)
1534 {
1535     GtkWidget *window, *hbox, *text, *button;
1536     char *titles;
1537     int i, def, cancel;
1538
1539     window = gtk_dialog_new();
1540     text = gtk_label_new(msg);
1541     align_label(GTK_LABEL(text), 0.0, 0.0);
1542     hbox = gtk_hbox_new(FALSE, 0);
1543     gtk_box_pack_start(GTK_BOX(hbox), text, FALSE, FALSE, 20);
1544     gtk_box_pack_start
1545         (GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(window))),
1546          hbox, FALSE, FALSE, 20);
1547     gtk_widget_show(text);
1548     gtk_widget_show(hbox);
1549     gtk_window_set_title(GTK_WINDOW(window), title);
1550     gtk_label_set_line_wrap(GTK_LABEL(text), TRUE);
1551
1552     if (type == MB_OK) {
1553         titles = LABEL_OK "\0";
1554         def = cancel = 0;
1555     } else {
1556         assert(type == MB_YESNO);
1557         titles = LABEL_NO "\0" LABEL_YES "\0";
1558         def = 1;
1559         cancel = 0;
1560     }
1561     i = 0;
1562     
1563     while (*titles) {
1564         button = gtk_button_new_with_our_label(titles);
1565         gtk_box_pack_end
1566             (GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(window))),
1567              button, FALSE, FALSE, 0);
1568         gtk_widget_show(button);
1569         if (i == def) {
1570             gtk_widget_set_can_default(button, TRUE);
1571             gtk_window_set_default(GTK_WINDOW(window), button);
1572         }
1573         if (i == cancel) {
1574             g_signal_connect(G_OBJECT(window), "key_press_event",
1575                              G_CALLBACK(win_key_press), button);
1576         }
1577         g_signal_connect(G_OBJECT(button), "clicked",
1578                          G_CALLBACK(msgbox_button_clicked), window);
1579         g_object_set_data(G_OBJECT(button), "user-data",
1580                           GINT_TO_POINTER(i));
1581         titles += strlen(titles)+1;
1582         i++;
1583     }
1584     g_object_set_data(G_OBJECT(window), "user-data", &i);
1585     g_signal_connect(G_OBJECT(window), "destroy",
1586                      G_CALLBACK(window_destroy), NULL);
1587     gtk_window_set_modal(GTK_WINDOW(window), TRUE);
1588     gtk_window_set_transient_for(GTK_WINDOW(window), GTK_WINDOW(parent));
1589     /* set_transient_window_pos(parent, window); */
1590     gtk_widget_show(window);
1591     i = -1;
1592     gtk_main();
1593     return (type == MB_YESNO ? i == 1 : TRUE);
1594 }
1595 #endif /* GTK_CHECK_VERSION(3,0,0) */
1596
1597 void error_box(GtkWidget *parent, const char *msg)
1598 {
1599     message_box(parent, "Error", msg, FALSE, MB_OK);
1600 }
1601
1602 static void config_ok_button_clicked(GtkButton *button, gpointer data)
1603 {
1604     frontend *fe = (frontend *)data;
1605     const char *err;
1606
1607     err = midend_set_config(fe->me, fe->cfg_which, fe->cfg);
1608
1609     if (err)
1610         error_box(fe->cfgbox, err);
1611     else {
1612         fe->cfgret = TRUE;
1613         gtk_widget_destroy(fe->cfgbox);
1614         changed_preset(fe);
1615     }
1616 }
1617
1618 static void config_cancel_button_clicked(GtkButton *button, gpointer data)
1619 {
1620     frontend *fe = (frontend *)data;
1621
1622     gtk_widget_destroy(fe->cfgbox);
1623 }
1624
1625 static int editbox_key(GtkWidget *widget, GdkEventKey *event, gpointer data)
1626 {
1627     /*
1628      * GtkEntry has a nasty habit of eating the Return key, which
1629      * is unhelpful since it doesn't actually _do_ anything with it
1630      * (it calls gtk_widget_activate, but our edit boxes never need
1631      * activating). So I catch Return before GtkEntry sees it, and
1632      * pass it straight on to the parent widget. Effect: hitting
1633      * Return in an edit box will now activate the default button
1634      * in the dialog just like it will everywhere else.
1635      */
1636     if (event->keyval == GDK_KEY_Return &&
1637         gtk_widget_get_parent(widget) != NULL) {
1638         gint return_val;
1639         g_signal_stop_emission_by_name(G_OBJECT(widget), "key_press_event");
1640         g_signal_emit_by_name(G_OBJECT(gtk_widget_get_parent(widget)),
1641                               "key_press_event", event, &return_val);
1642         return return_val;
1643     }
1644     return FALSE;
1645 }
1646
1647 static void editbox_changed(GtkEditable *ed, gpointer data)
1648 {
1649     config_item *i = (config_item *)data;
1650
1651     assert(i->type == C_STRING);
1652     sfree(i->u.string.sval);
1653     i->u.string.sval = dupstr(gtk_entry_get_text(GTK_ENTRY(ed)));
1654 }
1655
1656 static void button_toggled(GtkToggleButton *tb, gpointer data)
1657 {
1658     config_item *i = (config_item *)data;
1659
1660     assert(i->type == C_BOOLEAN);
1661     i->u.boolean.bval = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(tb));
1662 }
1663
1664 static void droplist_sel(GtkComboBox *combo, gpointer data)
1665 {
1666     config_item *i = (config_item *)data;
1667
1668     assert(i->type == C_CHOICES);
1669     i->u.choices.selected = gtk_combo_box_get_active(combo);
1670 }
1671
1672 static int get_config(frontend *fe, int which)
1673 {
1674     GtkWidget *w, *table, *cancel;
1675     GtkBox *content_box, *button_box;
1676     char *title;
1677     config_item *i;
1678     int y;
1679
1680     fe->cfg = midend_get_config(fe->me, which, &title);
1681     fe->cfg_which = which;
1682     fe->cfgret = FALSE;
1683
1684 #if GTK_CHECK_VERSION(3,0,0)
1685     /* GtkDialog isn't quite flexible enough */
1686     fe->cfgbox = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1687     content_box = GTK_BOX(gtk_vbox_new(FALSE, 8));
1688     g_object_set(G_OBJECT(content_box), "margin", 8, (const char *)NULL);
1689     gtk_widget_show(GTK_WIDGET(content_box));
1690     gtk_container_add(GTK_CONTAINER(fe->cfgbox), GTK_WIDGET(content_box));
1691     button_box = GTK_BOX(gtk_hbox_new(FALSE, 8));
1692     gtk_widget_show(GTK_WIDGET(button_box));
1693     gtk_box_pack_end(content_box, GTK_WIDGET(button_box), FALSE, FALSE, 0);
1694     {
1695         GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
1696         gtk_widget_show(sep);
1697         gtk_box_pack_end(content_box, sep, FALSE, FALSE, 0);
1698     }
1699 #else
1700     fe->cfgbox = gtk_dialog_new();
1701     content_box = GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(fe->cfgbox)));
1702     button_box = GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(fe->cfgbox)));
1703 #endif
1704     gtk_window_set_title(GTK_WINDOW(fe->cfgbox), title);
1705     sfree(title);
1706
1707     w = gtk_button_new_with_our_label(LABEL_CANCEL);
1708     gtk_box_pack_end(button_box, w, FALSE, FALSE, 0);
1709     gtk_widget_show(w);
1710     g_signal_connect(G_OBJECT(w), "clicked",
1711                      G_CALLBACK(config_cancel_button_clicked), fe);
1712     cancel = w;
1713
1714     w = gtk_button_new_with_our_label(LABEL_OK);
1715     gtk_box_pack_end(button_box, w, FALSE, FALSE, 0);
1716     gtk_widget_show(w);
1717     gtk_widget_set_can_default(w, TRUE);
1718     gtk_window_set_default(GTK_WINDOW(fe->cfgbox), w);
1719     g_signal_connect(G_OBJECT(w), "clicked",
1720                      G_CALLBACK(config_ok_button_clicked), fe);
1721
1722 #if GTK_CHECK_VERSION(3,0,0)
1723     table = gtk_grid_new();
1724 #else
1725     table = gtk_table_new(1, 2, FALSE);
1726 #endif
1727     y = 0;
1728     gtk_box_pack_start(content_box, table, FALSE, FALSE, 0);
1729     gtk_widget_show(table);
1730
1731     for (i = fe->cfg; i->type != C_END; i++) {
1732 #if !GTK_CHECK_VERSION(3,0,0)
1733         gtk_table_resize(GTK_TABLE(table), y+1, 2);
1734 #endif
1735
1736         switch (i->type) {
1737           case C_STRING:
1738             /*
1739              * Edit box with a label beside it.
1740              */
1741
1742             w = gtk_label_new(i->name);
1743             align_label(GTK_LABEL(w), 0.0, 0.5);
1744 #if GTK_CHECK_VERSION(3,0,0)
1745             gtk_grid_attach(GTK_GRID(table), w, 0, y, 1, 1);
1746 #else
1747             gtk_table_attach(GTK_TABLE(table), w, 0, 1, y, y+1,
1748                              GTK_SHRINK | GTK_FILL,
1749                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1750                              3, 3);
1751 #endif
1752             gtk_widget_show(w);
1753
1754             w = gtk_entry_new();
1755 #if GTK_CHECK_VERSION(3,0,0)
1756             gtk_grid_attach(GTK_GRID(table), w, 1, y, 1, 1);
1757             g_object_set(G_OBJECT(w), "hexpand", TRUE, (const char *)NULL);
1758 #else
1759             gtk_table_attach(GTK_TABLE(table), w, 1, 2, y, y+1,
1760                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1761                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1762                              3, 3);
1763 #endif
1764             gtk_entry_set_text(GTK_ENTRY(w), i->u.string.sval);
1765             g_signal_connect(G_OBJECT(w), "changed",
1766                              G_CALLBACK(editbox_changed), i);
1767             g_signal_connect(G_OBJECT(w), "key_press_event",
1768                              G_CALLBACK(editbox_key), NULL);
1769             gtk_widget_show(w);
1770
1771             break;
1772
1773           case C_BOOLEAN:
1774             /*
1775              * Simple checkbox.
1776              */
1777             w = gtk_check_button_new_with_label(i->name);
1778             g_signal_connect(G_OBJECT(w), "toggled",
1779                              G_CALLBACK(button_toggled), i);
1780 #if GTK_CHECK_VERSION(3,0,0)
1781             gtk_grid_attach(GTK_GRID(table), w, 0, y, 2, 1);
1782             g_object_set(G_OBJECT(w), "hexpand", TRUE, (const char *)NULL);
1783 #else
1784             gtk_table_attach(GTK_TABLE(table), w, 0, 2, y, y+1,
1785                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1786                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1787                              3, 3);
1788 #endif
1789             gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w),
1790                                          i->u.boolean.bval);
1791             gtk_widget_show(w);
1792             break;
1793
1794           case C_CHOICES:
1795             /*
1796              * Drop-down list (GtkComboBox).
1797              */
1798
1799             w = gtk_label_new(i->name);
1800             align_label(GTK_LABEL(w), 0.0, 0.5);
1801 #if GTK_CHECK_VERSION(3,0,0)
1802             gtk_grid_attach(GTK_GRID(table), w, 0, y, 1, 1);
1803 #else
1804             gtk_table_attach(GTK_TABLE(table), w, 0, 1, y, y+1,
1805                              GTK_SHRINK | GTK_FILL,
1806                              GTK_EXPAND | GTK_SHRINK | GTK_FILL ,
1807                              3, 3);
1808 #endif
1809             gtk_widget_show(w);
1810
1811             {
1812                 int c;
1813                 const char *p, *q;
1814                 char *name;
1815                 GtkListStore *model;
1816                 GtkCellRenderer *cr;
1817                 GtkTreeIter iter;
1818
1819                 model = gtk_list_store_new(1, G_TYPE_STRING);
1820
1821                 c = *i->u.choices.choicenames;
1822                 p = i->u.choices.choicenames+1;
1823
1824                 while (*p) {
1825                     q = p;
1826                     while (*q && *q != c)
1827                         q++;
1828
1829                     name = snewn(q-p+1, char);
1830                     strncpy(name, p, q-p);
1831                     name[q-p] = '\0';
1832
1833                     if (*q) q++;       /* eat delimiter */
1834
1835                     gtk_list_store_append(model, &iter);
1836                     gtk_list_store_set(model, &iter, 0, name, -1);
1837
1838                     p = q;
1839                 }
1840
1841                 w = gtk_combo_box_new_with_model(GTK_TREE_MODEL(model));
1842
1843                 gtk_combo_box_set_active(GTK_COMBO_BOX(w),
1844                                          i->u.choices.selected);
1845
1846                 cr = gtk_cell_renderer_text_new();
1847                 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(w), cr, TRUE);
1848                 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(w), cr,
1849                                                "text", 0, NULL);
1850
1851                 g_signal_connect(G_OBJECT(w), "changed",
1852                                  G_CALLBACK(droplist_sel), i);
1853             }
1854
1855 #if GTK_CHECK_VERSION(3,0,0)
1856             gtk_grid_attach(GTK_GRID(table), w, 1, y, 1, 1);
1857             g_object_set(G_OBJECT(w), "hexpand", TRUE, (const char *)NULL);
1858 #else
1859             gtk_table_attach(GTK_TABLE(table), w, 1, 2, y, y+1,
1860                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1861                              GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1862                              3, 3);
1863 #endif
1864             gtk_widget_show(w);
1865             break;
1866         }
1867
1868         y++;
1869     }
1870
1871     g_signal_connect(G_OBJECT(fe->cfgbox), "destroy",
1872                      G_CALLBACK(window_destroy), NULL);
1873     g_signal_connect(G_OBJECT(fe->cfgbox), "key_press_event",
1874                      G_CALLBACK(win_key_press), cancel);
1875     gtk_window_set_modal(GTK_WINDOW(fe->cfgbox), TRUE);
1876     gtk_window_set_transient_for(GTK_WINDOW(fe->cfgbox),
1877                                  GTK_WINDOW(fe->window));
1878     /* set_transient_window_pos(fe->window, fe->cfgbox); */
1879     gtk_widget_show(fe->cfgbox);
1880     gtk_main();
1881
1882     free_cfg(fe->cfg);
1883
1884     return fe->cfgret;
1885 }
1886
1887 static void menu_key_event(GtkMenuItem *menuitem, gpointer data)
1888 {
1889     frontend *fe = (frontend *)data;
1890     int key = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem),
1891                                                 "user-data"));
1892     if (!midend_process_key(fe->me, 0, 0, key))
1893         gtk_widget_destroy(fe->window);
1894 }
1895
1896 static void get_size(frontend *fe, int *px, int *py)
1897 {
1898     int x, y;
1899
1900     /*
1901      * Currently I don't want to make the GTK port scale large
1902      * puzzles to fit on the screen. This is because X does permit
1903      * extremely large windows and many window managers provide a
1904      * means of navigating round them, and the users I consulted
1905      * before deciding said that they'd rather have enormous puzzle
1906      * windows spanning multiple screen pages than have them
1907      * shrunk. I could change my mind later or introduce
1908      * configurability; this would be the place to do so, by
1909      * replacing the initial values of x and y with the screen
1910      * dimensions.
1911      */
1912     x = INT_MAX;
1913     y = INT_MAX;
1914     midend_size(fe->me, &x, &y, FALSE);
1915     *px = x;
1916     *py = y;
1917 }
1918
1919 #if !GTK_CHECK_VERSION(2,0,0)
1920 #define gtk_window_resize(win, x, y) \
1921         gdk_window_resize(GTK_WIDGET(win)->window, x, y)
1922 #endif
1923
1924 /*
1925  * Called when any other code in this file has changed the
1926  * selected game parameters.
1927  */
1928 static void changed_preset(frontend *fe)
1929 {
1930     int n = midend_which_preset(fe->me);
1931
1932     fe->preset_threaded = TRUE;
1933     if (n < 0 && fe->preset_custom) {
1934         gtk_check_menu_item_set_active(
1935             GTK_CHECK_MENU_ITEM(fe->preset_custom),
1936             TRUE);
1937     } else {
1938         GSList *gs = fe->preset_radio;
1939         GSList *found = NULL;
1940
1941         for (gs = fe->preset_radio; gs; gs = gs->next) {
1942             struct preset_menu_entry *entry =
1943                 (struct preset_menu_entry *)g_object_get_data(
1944                     G_OBJECT(gs->data), "user-data");
1945             if (!entry || entry->id != n)
1946                 gtk_check_menu_item_set_active(
1947                     GTK_CHECK_MENU_ITEM(gs->data), FALSE);
1948             else
1949                 found = gs;
1950         }
1951         if (found)
1952             gtk_check_menu_item_set_active(
1953                 GTK_CHECK_MENU_ITEM(found->data), TRUE);
1954     }
1955     fe->preset_threaded = FALSE;
1956
1957     /*
1958      * Update the greying on the Copy menu option.
1959      */
1960     if (fe->copy_menu_item) {
1961         int enabled = midend_can_format_as_text_now(fe->me);
1962         gtk_widget_set_sensitive(fe->copy_menu_item, enabled);
1963     }
1964 }
1965
1966 #if !GTK_CHECK_VERSION(3,0,0)
1967 static gboolean not_size_allocated_yet(GtkWidget *w)
1968 {
1969     /*
1970      * This function tests whether a widget has not yet taken up space
1971      * on the screen which it will occupy in future. (Therefore, it
1972      * returns true only if the widget does exist but does not have a
1973      * size allocation. A null widget is already taking up all the
1974      * space it ever will.)
1975      */
1976     if (!w)
1977         return FALSE;        /* nonexistent widgets aren't a problem */
1978
1979 #if GTK_CHECK_VERSION(2,18,0)  /* skip if no gtk_widget_get_allocation */
1980     {
1981         GtkAllocation a;
1982         gtk_widget_get_allocation(w, &a);
1983         if (a.height == 0 || a.width == 0)
1984             return TRUE;       /* widget exists but has no size yet */
1985     }
1986 #endif
1987
1988     return FALSE;
1989 }
1990
1991 static void try_shrink_drawing_area(frontend *fe)
1992 {
1993     if (fe->drawing_area_shrink_pending &&
1994         (!fe->menubar_is_local || !not_size_allocated_yet(fe->menubar)) &&
1995         !not_size_allocated_yet(fe->statusbar)) {
1996         /*
1997          * In order to permit the user to resize the window smaller as
1998          * well as bigger, we call this function after the window size
1999          * has ended up where we want it. This shouldn't shrink the
2000          * window immediately; it just arranges that the next time the
2001          * user tries to shrink it, they can.
2002          *
2003          * However, at puzzle creation time, we defer the first of
2004          * these operations until after the menu bar and status bar
2005          * are actually visible. On Ubuntu 12.04 I've found that these
2006          * can take a while to be displayed, and that it's a mistake
2007          * to reduce the drawing area's size allocation before they've
2008          * turned up or else the drawing area makes room for them by
2009          * shrinking to less than the size we intended.
2010          */
2011         gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), 1, 1);
2012         fe->drawing_area_shrink_pending = FALSE;
2013     }
2014 }
2015 #endif /* !GTK_CHECK_VERSION(3,0,0) */
2016
2017 static gint configure_window(GtkWidget *widget,
2018                              GdkEventConfigure *event, gpointer data)
2019 {
2020 #if !GTK_CHECK_VERSION(3,0,0)
2021     /*
2022      * When the main puzzle window changes size, it might be because
2023      * the menu bar or status bar has turned up after starting off
2024      * absent, in which case we should have another go at enacting a
2025      * pending shrink of the drawing area.
2026      */
2027     frontend *fe = (frontend *)data;
2028     try_shrink_drawing_area(fe);
2029 #endif
2030     return FALSE;
2031 }
2032
2033 #if GTK_CHECK_VERSION(3,0,0)
2034 static int window_extra_height(frontend *fe)
2035 {
2036     int ret = 0;
2037     if (fe->menubar) {
2038         GtkRequisition req;
2039         gtk_widget_get_preferred_size(fe->menubar, &req, NULL);
2040         ret += req.height;
2041     }
2042     if (fe->statusbar) {
2043         GtkRequisition req;
2044         gtk_widget_get_preferred_size(fe->statusbar, &req, NULL);
2045         ret += req.height;
2046     }
2047     return ret;
2048 }
2049 #endif
2050
2051 static void resize_fe(frontend *fe)
2052 {
2053     int x, y;
2054
2055     get_size(fe, &x, &y);
2056
2057 #if GTK_CHECK_VERSION(3,0,0)
2058     gtk_window_resize(GTK_WINDOW(fe->window), x, y + window_extra_height(fe));
2059     fe->awaiting_resize_ack = TRUE;
2060 #else
2061     fe->drawing_area_shrink_pending = FALSE;
2062     gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
2063     {
2064         GtkRequisition req;
2065         gtk_widget_size_request(GTK_WIDGET(fe->window), &req);
2066         gtk_window_resize(GTK_WINDOW(fe->window), req.width, req.height);
2067     }
2068     fe->drawing_area_shrink_pending = TRUE;
2069     try_shrink_drawing_area(fe);
2070 #endif
2071 }
2072
2073 static void menu_preset_event(GtkMenuItem *menuitem, gpointer data)
2074 {
2075     frontend *fe = (frontend *)data;
2076     struct preset_menu_entry *entry =
2077         (struct preset_menu_entry *)g_object_get_data(
2078             G_OBJECT(menuitem), "user-data");
2079
2080     if (fe->preset_threaded ||
2081         (GTK_IS_CHECK_MENU_ITEM(menuitem) &&
2082          !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem))))
2083         return;
2084     midend_set_params(fe->me, entry->params);
2085     midend_new_game(fe->me);
2086     changed_preset(fe);
2087     resize_fe(fe);
2088     midend_redraw(fe->me);
2089 }
2090
2091 GdkAtom compound_text_atom, utf8_string_atom;
2092 int paste_initialised = FALSE;
2093
2094 static void set_selection(frontend *fe, GdkAtom selection)
2095 {
2096     if (!paste_initialised) {
2097         compound_text_atom = gdk_atom_intern("COMPOUND_TEXT", FALSE);
2098         utf8_string_atom = gdk_atom_intern("UTF8_STRING", FALSE);
2099         paste_initialised = TRUE;
2100     }
2101
2102     /*
2103      * For this simple application we can safely assume that the
2104      * data passed to this function is pure ASCII, which means we
2105      * can return precisely the same stuff for types STRING,
2106      * COMPOUND_TEXT or UTF8_STRING.
2107      */
2108
2109     if (gtk_selection_owner_set(fe->area, selection, CurrentTime)) {
2110         gtk_selection_clear_targets(fe->area, selection);
2111         gtk_selection_add_target(fe->area, selection,
2112                                  GDK_SELECTION_TYPE_STRING, 1);
2113         gtk_selection_add_target(fe->area, selection, compound_text_atom, 1);
2114         gtk_selection_add_target(fe->area, selection, utf8_string_atom, 1);
2115     }
2116 }
2117
2118 void write_clip(frontend *fe, char *data)
2119 {
2120     if (fe->paste_data)
2121         sfree(fe->paste_data);
2122
2123     fe->paste_data = data;
2124     fe->paste_data_len = strlen(data);
2125
2126     set_selection(fe, GDK_SELECTION_PRIMARY);
2127     set_selection(fe, GDK_SELECTION_CLIPBOARD);
2128 }
2129
2130 void selection_get(GtkWidget *widget, GtkSelectionData *seldata,
2131                    guint info, guint time_stamp, gpointer data)
2132 {
2133     frontend *fe = (frontend *)data;
2134     gtk_selection_data_set(seldata, gtk_selection_data_get_target(seldata), 8,
2135                            fe->paste_data, fe->paste_data_len);
2136 }
2137
2138 gint selection_clear(GtkWidget *widget, GdkEventSelection *seldata,
2139                      gpointer data)
2140 {
2141     frontend *fe = (frontend *)data;
2142
2143     if (fe->paste_data)
2144         sfree(fe->paste_data);
2145     fe->paste_data = NULL;
2146     fe->paste_data_len = 0;
2147     return TRUE;
2148 }
2149
2150 static void menu_copy_event(GtkMenuItem *menuitem, gpointer data)
2151 {
2152     frontend *fe = (frontend *)data;
2153     char *text;
2154
2155     text = midend_text_format(fe->me);
2156
2157     if (text) {
2158         write_clip(fe, text);
2159     } else {
2160         gdk_beep();
2161     }
2162 }
2163
2164 #ifdef OLD_FILESEL
2165
2166 static void filesel_ok(GtkButton *button, gpointer data)
2167 {
2168     frontend *fe = (frontend *)data;
2169
2170     gpointer filesel = g_object_get_data(G_OBJECT(button), "user-data");
2171
2172     const char *name =
2173         gtk_file_selection_get_filename(GTK_FILE_SELECTION(filesel));
2174
2175     fe->filesel_name = dupstr(name);
2176 }
2177
2178 static char *file_selector(frontend *fe, const char *title, int save)
2179 {
2180     GtkWidget *filesel =
2181         gtk_file_selection_new(title);
2182
2183     fe->filesel_name = NULL;
2184
2185     gtk_window_set_modal(GTK_WINDOW(filesel), TRUE);
2186     g_object_set_data
2187         (G_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "user-data",
2188          (gpointer)filesel);
2189     g_signal_connect
2190         (G_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "clicked",
2191          G_CALLBACK(filesel_ok), fe);
2192     g_signal_connect_swapped
2193         (G_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "clicked",
2194          G_CALLBACK(gtk_widget_destroy), (gpointer)filesel);
2195     g_signal_connect_object
2196         (G_OBJECT(GTK_FILE_SELECTION(filesel)->cancel_button), "clicked",
2197          G_CALLBACK(gtk_widget_destroy), (gpointer)filesel);
2198     g_signal_connect(G_OBJECT(filesel), "destroy",
2199                      G_CALLBACK(window_destroy), NULL);
2200     gtk_widget_show(filesel);
2201     gtk_window_set_transient_for(GTK_WINDOW(filesel), GTK_WINDOW(fe->window));
2202     gtk_main();
2203
2204     return fe->filesel_name;
2205 }
2206
2207 #else
2208
2209 static char *file_selector(frontend *fe, const char *title, int save)
2210 {
2211     char *filesel_name = NULL;
2212
2213     GtkWidget *filesel =
2214         gtk_file_chooser_dialog_new(title,
2215                                     GTK_WINDOW(fe->window),
2216                                     save ? GTK_FILE_CHOOSER_ACTION_SAVE :
2217                                     GTK_FILE_CHOOSER_ACTION_OPEN,
2218                                     LABEL_CANCEL, GTK_RESPONSE_CANCEL,
2219                                     save ? LABEL_SAVE : LABEL_OPEN,
2220                                     GTK_RESPONSE_ACCEPT,
2221                                     NULL);
2222
2223     if (gtk_dialog_run(GTK_DIALOG(filesel)) == GTK_RESPONSE_ACCEPT) {
2224         char *name = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filesel));
2225         filesel_name = dupstr(name);
2226         g_free(name);
2227     }
2228
2229     gtk_widget_destroy(filesel);
2230
2231     return filesel_name;
2232 }
2233
2234 #endif
2235
2236 struct savefile_write_ctx {
2237     FILE *fp;
2238     int error;
2239 };
2240
2241 static void savefile_write(void *wctx, const void *buf, int len)
2242 {
2243     struct savefile_write_ctx *ctx = (struct savefile_write_ctx *)wctx;
2244     if (fwrite(buf, 1, len, ctx->fp) < len)
2245         ctx->error = errno;
2246 }
2247
2248 static int savefile_read(void *wctx, void *buf, int len)
2249 {
2250     FILE *fp = (FILE *)wctx;
2251     int ret;
2252
2253     ret = fread(buf, 1, len, fp);
2254     return (ret == len);
2255 }
2256
2257 static void menu_save_event(GtkMenuItem *menuitem, gpointer data)
2258 {
2259     frontend *fe = (frontend *)data;
2260     char *name;
2261
2262     name = file_selector(fe, "Enter name of game file to save", TRUE);
2263
2264     if (name) {
2265         FILE *fp;
2266
2267         if ((fp = fopen(name, "r")) != NULL) {
2268             char buf[256 + FILENAME_MAX];
2269             fclose(fp);
2270             /* file exists */
2271
2272             sprintf(buf, "Are you sure you want to overwrite the"
2273                     " file \"%.*s\"?",
2274                     FILENAME_MAX, name);
2275             if (!message_box(fe->window, "Question", buf, TRUE, MB_YESNO))
2276                 goto free_and_return;
2277         }
2278
2279         fp = fopen(name, "w");
2280
2281         if (!fp) {
2282             error_box(fe->window, "Unable to open save file");
2283             goto free_and_return;
2284         }
2285
2286         {
2287             struct savefile_write_ctx ctx;
2288             ctx.fp = fp;
2289             ctx.error = 0;
2290             midend_serialise(fe->me, savefile_write, &ctx);
2291             fclose(fp);
2292             if (ctx.error) {
2293                 char boxmsg[512];
2294                 sprintf(boxmsg, "Error writing save file: %.400s",
2295                         strerror(errno));
2296                 error_box(fe->window, boxmsg);
2297                 goto free_and_return;
2298             }
2299         }
2300     free_and_return:
2301         sfree(name);
2302     }
2303 }
2304
2305 static void menu_load_event(GtkMenuItem *menuitem, gpointer data)
2306 {
2307     frontend *fe = (frontend *)data;
2308     char *name;
2309     const char *err;
2310
2311     name = file_selector(fe, "Enter name of saved game file to load", FALSE);
2312
2313     if (name) {
2314         FILE *fp = fopen(name, "r");
2315         sfree(name);
2316
2317         if (!fp) {
2318             error_box(fe->window, "Unable to open saved game file");
2319             return;
2320         }
2321
2322         err = midend_deserialise(fe->me, savefile_read, fp);
2323
2324         fclose(fp);
2325
2326         if (err) {
2327             error_box(fe->window, err);
2328             return;
2329         }
2330
2331         changed_preset(fe);
2332         resize_fe(fe);
2333         midend_redraw(fe->me);
2334     }
2335 }
2336
2337 static void menu_solve_event(GtkMenuItem *menuitem, gpointer data)
2338 {
2339     frontend *fe = (frontend *)data;
2340     const char *msg;
2341
2342     msg = midend_solve(fe->me);
2343
2344     if (msg)
2345         error_box(fe->window, msg);
2346 }
2347
2348 static void menu_restart_event(GtkMenuItem *menuitem, gpointer data)
2349 {
2350     frontend *fe = (frontend *)data;
2351
2352     midend_restart_game(fe->me);
2353 }
2354
2355 static void menu_config_event(GtkMenuItem *menuitem, gpointer data)
2356 {
2357     frontend *fe = (frontend *)data;
2358     int which = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem),
2359                                                   "user-data"));
2360
2361     if (fe->preset_threaded ||
2362         (GTK_IS_CHECK_MENU_ITEM(menuitem) &&
2363          !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem))))
2364         return;
2365     changed_preset(fe);                 /* Put the old preset back! */
2366     if (!get_config(fe, which))
2367         return;
2368
2369     midend_new_game(fe->me);
2370     resize_fe(fe);
2371     midend_redraw(fe->me);
2372 }
2373
2374 #ifndef HELP_BROWSER_PATH
2375 #define HELP_BROWSER_PATH "xdg-open:sensible-browser"
2376 #endif
2377
2378 static void show_help(frontend *fe, const char *topic)
2379 {
2380     const char *list = HELP_BROWSER_PATH;
2381     char path[PATH_MAX + 1];
2382     struct {
2383         const char *s;
2384         int len;
2385     } lang[3];
2386     int i;
2387
2388     /*
2389      * Search for help file, trying:
2390      * 1. Version for this locale, ignoring encoding (HTML browsers
2391      *    must handle multiple encodings)
2392      * 2. Version for this locale, ignoring encoding and country
2393      * 3. English version
2394      */
2395     lang[0].s = setlocale(LC_MESSAGES, NULL);
2396     lang[0].len = strcspn(lang[0].s, ".@");
2397     lang[1].s = lang[0].s;
2398     lang[1].len = strcspn(lang[1].s, "_");
2399     if (lang[1].len > lang[0].len)
2400         lang[1].len = lang[0].len;
2401     lang[2].s = "en";
2402     lang[2].len = 2;
2403     for (i = 0; i < lenof(lang); i++) {
2404         sprintf(path, "%s/sgt-puzzles/help/%.*s/%s.html",
2405                 SHAREDIR, lang[i].len, lang[i].s, topic);
2406         if (access(path, R_OK) == 0)
2407             break;
2408     }
2409     if (i == lenof(lang)) {
2410         error_box(fe->window, "Help file is not installed");
2411         return;
2412     }
2413
2414     for (;;) {
2415         size_t len;
2416         char buf[PATH_MAX + 1];
2417         const char *command;
2418         const char *argv[3];
2419
2420         len = strcspn(list, ":");
2421         if (len <= PATH_MAX) {
2422             memcpy(buf, list, len);
2423             buf[len] = 0;
2424             if (buf[0] == '$')
2425                 command = getenv(buf + 1);
2426             else
2427                 command = buf;
2428             if (command) {
2429                 argv[0] = command;
2430                 argv[1] = path;
2431                 argv[2] = NULL;
2432                 if (g_spawn_async(NULL, (char **)argv, NULL,
2433                                   G_SPAWN_SEARCH_PATH,
2434                                   NULL, NULL, NULL, NULL))
2435                     return;
2436             }
2437         }
2438
2439         if (!list[len])
2440             break;
2441         list += len + 1;
2442     }
2443
2444     error_box(fe->window, "Failed to start a help browser");
2445 }
2446
2447 static void menu_help_contents_event(GtkMenuItem *menuitem, gpointer data)
2448 {
2449     show_help((frontend *)data, "index");
2450 }
2451
2452 static void menu_help_specific_event(GtkMenuItem *menuitem, gpointer data)
2453 {
2454     show_help((frontend *)data, thegame.htmlhelp_topic);
2455 }
2456
2457 static void menu_about_event(GtkMenuItem *menuitem, gpointer data)
2458 {
2459     frontend *fe = (frontend *)data;
2460
2461 #if GTK_CHECK_VERSION(3,0,0)
2462     extern char *const *const xpm_icons[];
2463     extern const int n_xpm_icons;
2464     GdkPixbuf *icon = gdk_pixbuf_new_from_xpm_data
2465         ((const gchar **)xpm_icons[n_xpm_icons-1]);
2466     gtk_show_about_dialog
2467         (GTK_WINDOW(fe->window),
2468          "program-name", thegame.name,
2469          "version", ver,
2470          "comments", "Part of Simon Tatham's Portable Puzzle Collection",
2471          "logo", icon,
2472          (const gchar *)NULL);
2473     g_object_unref(G_OBJECT(icon));
2474 #else
2475     char titlebuf[256];
2476     char textbuf[1024];
2477
2478     sprintf(titlebuf, "About %.200s", thegame.name);
2479     sprintf(textbuf,
2480             "%.200s\n\n"
2481             "from Simon Tatham's Portable Puzzle Collection\n\n"
2482             "%.500s", thegame.name, ver);
2483
2484     message_box(fe->window, titlebuf, textbuf, TRUE, MB_OK);
2485 #endif
2486 }
2487
2488 static GtkWidget *add_menu_ui_item(
2489     frontend *fe, GtkContainer *cont, const char *text, int action,
2490     int accel_key, int accel_keyqual)
2491 {
2492     GtkWidget *menuitem = gtk_menu_item_new_with_label(text);
2493     gtk_container_add(cont, menuitem);
2494     g_object_set_data(G_OBJECT(menuitem), "user-data",
2495                       GINT_TO_POINTER(action));
2496     g_signal_connect(G_OBJECT(menuitem), "activate",
2497                      G_CALLBACK(menu_key_event), fe);
2498
2499     if (accel_key) {
2500         /*
2501          * Display a keyboard accelerator alongside this menu item.
2502          * Actually this won't be processed via the usual GTK
2503          * accelerator system, because we add it to a dummy
2504          * accelerator group which is never actually activated on the
2505          * main window; this permits back ends to override special
2506          * keys like 'n' and 'r' and 'u' in some UI states. So
2507          * whatever keystroke we display here will still go to
2508          * key_event and be handled in the normal way.
2509          */
2510         gtk_widget_add_accelerator(menuitem,
2511                                    "activate", fe->dummy_accelgroup,
2512                                    accel_key, accel_keyqual,
2513                                    GTK_ACCEL_VISIBLE | GTK_ACCEL_LOCKED);
2514     }
2515
2516     gtk_widget_show(menuitem);
2517     return menuitem;
2518 }
2519
2520 static void add_menu_separator(GtkContainer *cont)
2521 {
2522     GtkWidget *menuitem = gtk_menu_item_new();
2523     gtk_container_add(cont, menuitem);
2524     gtk_widget_show(menuitem);
2525 }
2526
2527 static void populate_gtk_preset_menu(frontend *fe, struct preset_menu *menu,
2528                                      GtkWidget *gtkmenu)
2529 {
2530     int i;
2531
2532     for (i = 0; i < menu->n_entries; i++) {
2533         struct preset_menu_entry *entry = &menu->entries[i];
2534         GtkWidget *menuitem;
2535
2536         if (entry->params) {
2537             menuitem = gtk_radio_menu_item_new_with_label(
2538                 fe->preset_radio, entry->title);
2539             fe->preset_radio = gtk_radio_menu_item_get_group(
2540                 GTK_RADIO_MENU_ITEM(menuitem));
2541             g_object_set_data(G_OBJECT(menuitem), "user-data", entry);
2542             g_signal_connect(G_OBJECT(menuitem), "activate",
2543                              G_CALLBACK(menu_preset_event), fe);
2544         } else {
2545             GtkWidget *submenu;
2546             menuitem = gtk_menu_item_new_with_label(entry->title);
2547             submenu = gtk_menu_new();
2548             gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu);
2549             populate_gtk_preset_menu(fe, entry->submenu, submenu);
2550         }
2551
2552         gtk_container_add(GTK_CONTAINER(gtkmenu), menuitem);
2553         gtk_widget_show(menuitem);
2554     }
2555 }
2556
2557 enum { ARG_EITHER, ARG_SAVE, ARG_ID }; /* for argtype */
2558
2559 static frontend *new_window(char *arg, int argtype, char **error)
2560 {
2561     frontend *fe;
2562     GtkBox *vbox, *hbox;
2563     GtkWidget *menu, *menuitem;
2564     GList *iconlist;
2565     int x, y, n;
2566     char errbuf[1024];
2567     extern char *const *const xpm_icons[];
2568     extern const int n_xpm_icons;
2569     struct preset_menu *preset_menu;
2570
2571     fe = snew(frontend);
2572 #if GTK_CHECK_VERSION(3,20,0)
2573     fe->css_provider = NULL;
2574 #endif
2575
2576     fe->timer_active = FALSE;
2577     fe->timer_id = -1;
2578
2579     fe->me = midend_new(fe, &thegame, &gtk_drawing, fe);
2580
2581     if (arg) {
2582         const char *err;
2583         FILE *fp;
2584
2585         errbuf[0] = '\0';
2586
2587         switch (argtype) {
2588           case ARG_ID:
2589             err = midend_game_id(fe->me, arg);
2590             if (!err)
2591                 midend_new_game(fe->me);
2592             else
2593                 sprintf(errbuf, "Invalid game ID: %.800s", err);
2594             break;
2595           case ARG_SAVE:
2596             fp = fopen(arg, "r");
2597             if (!fp) {
2598                 sprintf(errbuf, "Error opening file: %.800s", strerror(errno));
2599             } else {
2600                 err = midend_deserialise(fe->me, savefile_read, fp);
2601                 if (err)
2602                     sprintf(errbuf, "Invalid save file: %.800s", err);
2603                 fclose(fp);
2604             }
2605             break;
2606           default /*case ARG_EITHER*/:
2607             /*
2608              * First try treating the argument as a game ID.
2609              */
2610             err = midend_game_id(fe->me, arg);
2611             if (!err) {
2612                 /*
2613                  * It's a valid game ID.
2614                  */
2615                 midend_new_game(fe->me);
2616             } else {
2617                 FILE *fp = fopen(arg, "r");
2618                 if (!fp) {
2619                     sprintf(errbuf, "Supplied argument is neither a game ID (%.400s)"
2620                             " nor a save file (%.400s)", err, strerror(errno));
2621                 } else {
2622                     err = midend_deserialise(fe->me, savefile_read, fp);
2623                     if (err)
2624                         sprintf(errbuf, "%.800s", err);
2625                     fclose(fp);
2626                 }
2627             }
2628             break;
2629         }
2630         if (*errbuf) {
2631             *error = dupstr(errbuf);
2632             midend_free(fe->me);
2633             sfree(fe);
2634             return NULL;
2635         }
2636
2637     } else {
2638         midend_new_game(fe->me);
2639     }
2640
2641 #if !GTK_CHECK_VERSION(3,0,0)
2642     {
2643         /*
2644          * try_shrink_drawing_area() will do some fiddling with the
2645          * window size request (see comment in that function) after
2646          * all the bits and pieces such as the menu bar and status bar
2647          * have appeared in the puzzle window.
2648          *
2649          * However, on Unity systems, the menu bar _doesn't_ appear in
2650          * the puzzle window, because the Unity shell hijacks it into
2651          * the menu bar at the very top of the screen. We therefore
2652          * try to detect that situation here, so that we don't sit
2653          * here forever waiting for a menu bar.
2654          */
2655         const char prop[] = "gtk-shell-shows-menubar";
2656         GtkSettings *settings = gtk_settings_get_default();
2657         if (!g_object_class_find_property(G_OBJECT_GET_CLASS(settings),
2658                                           prop)) {
2659             fe->menubar_is_local = TRUE;
2660         } else {
2661             int unity_mode;
2662             g_object_get(gtk_settings_get_default(),
2663                          prop, &unity_mode,
2664                          (const gchar *)NULL);
2665             fe->menubar_is_local = !unity_mode;
2666         }
2667     }
2668 #endif
2669
2670 #if GTK_CHECK_VERSION(3,0,0)
2671     fe->awaiting_resize_ack = FALSE;
2672 #endif
2673
2674     fe->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
2675     gtk_window_set_title(GTK_WINDOW(fe->window), thegame.name);
2676
2677     vbox = GTK_BOX(gtk_vbox_new(FALSE, 0));
2678     gtk_container_add(GTK_CONTAINER(fe->window), GTK_WIDGET(vbox));
2679     gtk_widget_show(GTK_WIDGET(vbox));
2680
2681     fe->dummy_accelgroup = gtk_accel_group_new();
2682     /*
2683      * Intentionally _not_ added to the window via
2684      * gtk_window_add_accel_group; see menu_key_event
2685      */
2686
2687     hbox = GTK_BOX(gtk_hbox_new(FALSE, 0));
2688     gtk_box_pack_start(vbox, GTK_WIDGET(hbox), FALSE, FALSE, 0);
2689     gtk_widget_show(GTK_WIDGET(hbox));
2690
2691     fe->menubar = gtk_menu_bar_new();
2692     gtk_box_pack_start(hbox, fe->menubar, TRUE, TRUE, 0);
2693     gtk_widget_show(fe->menubar);
2694
2695     menuitem = gtk_menu_item_new_with_mnemonic("_Game");
2696     gtk_container_add(GTK_CONTAINER(fe->menubar), menuitem);
2697     gtk_widget_show(menuitem);
2698
2699     menu = gtk_menu_new();
2700     gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
2701
2702     add_menu_ui_item(fe, GTK_CONTAINER(menu), "New", UI_NEWGAME, 'n', 0);
2703
2704     menuitem = gtk_menu_item_new_with_label("Restart");
2705     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2706     g_signal_connect(G_OBJECT(menuitem), "activate",
2707                      G_CALLBACK(menu_restart_event), fe);
2708     gtk_widget_show(menuitem);
2709
2710     menuitem = gtk_menu_item_new_with_label("Specific...");
2711     g_object_set_data(G_OBJECT(menuitem), "user-data",
2712                       GINT_TO_POINTER(CFG_DESC));
2713     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2714     g_signal_connect(G_OBJECT(menuitem), "activate",
2715                      G_CALLBACK(menu_config_event), fe);
2716     gtk_widget_show(menuitem);
2717
2718     menuitem = gtk_menu_item_new_with_label("Random Seed...");
2719     g_object_set_data(G_OBJECT(menuitem), "user-data",
2720                       GINT_TO_POINTER(CFG_SEED));
2721     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2722     g_signal_connect(G_OBJECT(menuitem), "activate",
2723                      G_CALLBACK(menu_config_event), fe);
2724     gtk_widget_show(menuitem);
2725
2726     fe->preset_radio = NULL;
2727     fe->preset_custom = NULL;
2728     fe->preset_threaded = FALSE;
2729
2730     preset_menu = midend_get_presets(fe->me, NULL);
2731     if (preset_menu->n_entries > 0 || thegame.can_configure) {
2732         GtkWidget *submenu;
2733
2734         menuitem = gtk_menu_item_new_with_mnemonic("_Type");
2735         gtk_container_add(GTK_CONTAINER(fe->menubar), menuitem);
2736         gtk_widget_show(menuitem);
2737
2738         submenu = gtk_menu_new();
2739         gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu);
2740
2741         populate_gtk_preset_menu(fe, preset_menu, submenu);
2742
2743         if (thegame.can_configure) {
2744             menuitem = fe->preset_custom =
2745                 gtk_radio_menu_item_new_with_label(fe->preset_radio,
2746                                                    "Custom...");
2747             fe->preset_radio =
2748                 gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
2749             gtk_container_add(GTK_CONTAINER(submenu), menuitem);
2750             g_object_set_data(G_OBJECT(menuitem), "user-data",
2751                               GINT_TO_POINTER(CFG_SETTINGS));
2752             g_signal_connect(G_OBJECT(menuitem), "activate",
2753                              G_CALLBACK(menu_config_event), fe);
2754             gtk_widget_show(menuitem);
2755         }
2756
2757     }
2758
2759     add_menu_separator(GTK_CONTAINER(menu));
2760     menuitem = gtk_menu_item_new_with_label("Load...");
2761     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2762     g_signal_connect(G_OBJECT(menuitem), "activate",
2763                      G_CALLBACK(menu_load_event), fe);
2764     gtk_widget_show(menuitem);
2765     menuitem = gtk_menu_item_new_with_label("Save...");
2766     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2767     g_signal_connect(G_OBJECT(menuitem), "activate",
2768                      G_CALLBACK(menu_save_event), fe);
2769     gtk_widget_show(menuitem);
2770 #ifndef STYLUS_BASED
2771     add_menu_separator(GTK_CONTAINER(menu));
2772     add_menu_ui_item(fe, GTK_CONTAINER(menu), "Undo", UI_UNDO, 'u', 0);
2773     add_menu_ui_item(fe, GTK_CONTAINER(menu), "Redo", UI_REDO, 'r', 0);
2774 #endif
2775     if (thegame.can_format_as_text_ever) {
2776         add_menu_separator(GTK_CONTAINER(menu));
2777         menuitem = gtk_menu_item_new_with_label("Copy");
2778         gtk_container_add(GTK_CONTAINER(menu), menuitem);
2779         g_signal_connect(G_OBJECT(menuitem), "activate",
2780                          G_CALLBACK(menu_copy_event), fe);
2781         gtk_widget_show(menuitem);
2782         fe->copy_menu_item = menuitem;
2783     } else {
2784         fe->copy_menu_item = NULL;
2785     }
2786     if (thegame.can_solve) {
2787         add_menu_separator(GTK_CONTAINER(menu));
2788         menuitem = gtk_menu_item_new_with_label("Solve");
2789         gtk_container_add(GTK_CONTAINER(menu), menuitem);
2790         g_signal_connect(G_OBJECT(menuitem), "activate",
2791                          G_CALLBACK(menu_solve_event), fe);
2792         gtk_widget_show(menuitem);
2793     }
2794     add_menu_separator(GTK_CONTAINER(menu));
2795     add_menu_ui_item(fe, GTK_CONTAINER(menu), "Exit", UI_QUIT, 'q', 0);
2796
2797     menuitem = gtk_menu_item_new_with_mnemonic("_Help");
2798     gtk_container_add(GTK_CONTAINER(fe->menubar), menuitem);
2799     gtk_widget_show(menuitem);
2800
2801     menu = gtk_menu_new();
2802     gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
2803
2804     menuitem = gtk_menu_item_new_with_label("Contents");
2805     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2806     g_signal_connect(G_OBJECT(menuitem), "activate",
2807                      G_CALLBACK(menu_help_contents_event), fe);
2808     gtk_widget_show(menuitem);
2809
2810     if (thegame.htmlhelp_topic) {
2811         char *item;
2812         assert(thegame.name);
2813         item = snewn(9+strlen(thegame.name), char); /*ick*/
2814         sprintf(item, "Help on %s", thegame.name);
2815         menuitem = gtk_menu_item_new_with_label(item);
2816         sfree(item);
2817         gtk_container_add(GTK_CONTAINER(menu), menuitem);
2818         g_signal_connect(G_OBJECT(menuitem), "activate",
2819                          G_CALLBACK(menu_help_specific_event), fe);
2820         gtk_widget_show(menuitem);
2821     }
2822
2823     menuitem = gtk_menu_item_new_with_label("About");
2824     gtk_container_add(GTK_CONTAINER(menu), menuitem);
2825     g_signal_connect(G_OBJECT(menuitem), "activate",
2826                      G_CALLBACK(menu_about_event), fe);
2827     gtk_widget_show(menuitem);
2828
2829 #ifdef STYLUS_BASED
2830     menuitem=gtk_button_new_with_mnemonic("_Redo");
2831     g_object_set_data(G_OBJECT(menuitem), "user-data",
2832                       GINT_TO_POINTER(UI_REDO));
2833     g_signal_connect(G_OBJECT(menuitem), "clicked",
2834                      G_CALLBACK(menu_key_event), fe);
2835     gtk_box_pack_end(hbox, menuitem, FALSE, FALSE, 0);
2836     gtk_widget_show(menuitem);
2837
2838     menuitem=gtk_button_new_with_mnemonic("_Undo");
2839     g_object_set_data(G_OBJECT(menuitem), "user-data",
2840                       GINT_TO_POINTER(UI_UNDO));
2841     g_signal_connect(G_OBJECT(menuitem), "clicked",
2842                      G_CALLBACK(menu_key_event), fe);
2843     gtk_box_pack_end(hbox, menuitem, FALSE, FALSE, 0);
2844     gtk_widget_show(menuitem);
2845
2846     if (thegame.flags & REQUIRE_NUMPAD) {
2847         hbox = GTK_BOX(gtk_hbox_new(FALSE, 0));
2848         gtk_box_pack_start(vbox, GTK_WIDGET(hbox), FALSE, FALSE, 0);
2849         gtk_widget_show(GTK_WIDGET(hbox));
2850
2851         *((int*)errbuf)=0;
2852         errbuf[1]='\0';
2853         for(errbuf[0]='0';errbuf[0]<='9';errbuf[0]++) {
2854             menuitem=gtk_button_new_with_label(errbuf);
2855             g_object_set_data(G_OBJECT(menuitem), "user-data",
2856                               GINT_TO_POINTER((int)(errbuf[0])));
2857             g_signal_connect(G_OBJECT(menuitem), "clicked",
2858                              G_CALLBACK(menu_key_event), fe);
2859             gtk_box_pack_start(hbox, menuitem, TRUE, TRUE, 0);
2860             gtk_widget_show(menuitem);
2861         }
2862     }
2863 #endif /* STYLUS_BASED */
2864
2865     changed_preset(fe);
2866
2867     snaffle_colours(fe);
2868
2869     if (midend_wants_statusbar(fe->me)) {
2870         GtkWidget *viewport;
2871         GtkRequisition req;
2872
2873         viewport = gtk_viewport_new(NULL, NULL);
2874         gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
2875         fe->statusbar = gtk_statusbar_new();
2876         gtk_container_add(GTK_CONTAINER(viewport), fe->statusbar);
2877         gtk_widget_show(viewport);
2878         gtk_box_pack_end(vbox, viewport, FALSE, FALSE, 0);
2879         gtk_widget_show(fe->statusbar);
2880         fe->statusctx = gtk_statusbar_get_context_id
2881             (GTK_STATUSBAR(fe->statusbar), "game");
2882         gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx,
2883                            DEFAULT_STATUSBAR_TEXT);
2884 #if GTK_CHECK_VERSION(3,0,0)
2885         gtk_widget_get_preferred_size(fe->statusbar, &req, NULL);
2886 #else
2887         gtk_widget_size_request(fe->statusbar, &req);
2888 #endif
2889         gtk_widget_set_size_request(viewport, -1, req.height);
2890     } else
2891         fe->statusbar = NULL;
2892
2893     fe->area = gtk_drawing_area_new();
2894 #if GTK_CHECK_VERSION(2,0,0) && !GTK_CHECK_VERSION(3,0,0)
2895     gtk_widget_set_double_buffered(fe->area, FALSE);
2896 #endif
2897     {
2898         GdkGeometry geom;
2899         geom.base_width = 0;
2900 #if GTK_CHECK_VERSION(3,0,0)
2901         geom.base_height = window_extra_height(fe);
2902         gtk_window_set_geometry_hints(GTK_WINDOW(fe->window), NULL,
2903                                       &geom, GDK_HINT_BASE_SIZE);
2904 #else
2905         geom.base_height = 0;
2906         gtk_window_set_geometry_hints(GTK_WINDOW(fe->window), fe->area,
2907                                       &geom, GDK_HINT_BASE_SIZE);
2908 #endif
2909     }
2910     fe->w = -1;
2911     fe->h = -1;
2912     get_size(fe, &x, &y);
2913 #if GTK_CHECK_VERSION(3,0,0)
2914     gtk_window_set_default_size(GTK_WINDOW(fe->window),
2915                                 x, y + window_extra_height(fe));
2916 #else
2917     fe->drawing_area_shrink_pending = FALSE;
2918     gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
2919 #endif
2920
2921     gtk_box_pack_end(vbox, fe->area, TRUE, TRUE, 0);
2922
2923     clear_backing_store(fe);
2924     fe->fonts = NULL;
2925     fe->nfonts = fe->fontsize = 0;
2926
2927     fe->paste_data = NULL;
2928     fe->paste_data_len = 0;
2929
2930     g_signal_connect(G_OBJECT(fe->window), "destroy",
2931                      G_CALLBACK(destroy), fe);
2932     g_signal_connect(G_OBJECT(fe->window), "key_press_event",
2933                      G_CALLBACK(key_event), fe);
2934     g_signal_connect(G_OBJECT(fe->area), "button_press_event",
2935                      G_CALLBACK(button_event), fe);
2936     g_signal_connect(G_OBJECT(fe->area), "button_release_event",
2937                      G_CALLBACK(button_event), fe);
2938     g_signal_connect(G_OBJECT(fe->area), "motion_notify_event",
2939                      G_CALLBACK(motion_event), fe);
2940     g_signal_connect(G_OBJECT(fe->area), "selection_get",
2941                      G_CALLBACK(selection_get), fe);
2942     g_signal_connect(G_OBJECT(fe->area), "selection_clear_event",
2943                      G_CALLBACK(selection_clear), fe);
2944 #if GTK_CHECK_VERSION(3,0,0)
2945     g_signal_connect(G_OBJECT(fe->area), "draw",
2946                      G_CALLBACK(draw_area), fe);
2947 #else
2948     g_signal_connect(G_OBJECT(fe->area), "expose_event",
2949                      G_CALLBACK(expose_area), fe);
2950 #endif
2951     g_signal_connect(G_OBJECT(fe->window), "map_event",
2952                      G_CALLBACK(map_window), fe);
2953     g_signal_connect(G_OBJECT(fe->area), "configure_event",
2954                      G_CALLBACK(configure_area), fe);
2955     g_signal_connect(G_OBJECT(fe->window), "configure_event",
2956                      G_CALLBACK(configure_window), fe);
2957 #if GTK_CHECK_VERSION(3,0,0)
2958     g_signal_connect(G_OBJECT(fe->window), "size_allocate",
2959                      G_CALLBACK(window_size_alloc), fe);
2960 #endif
2961
2962     gtk_widget_add_events(GTK_WIDGET(fe->area),
2963                           GDK_BUTTON_PRESS_MASK |
2964                           GDK_BUTTON_RELEASE_MASK |
2965                           GDK_BUTTON_MOTION_MASK |
2966                           GDK_POINTER_MOTION_HINT_MASK);
2967
2968     if (n_xpm_icons) {
2969         gtk_window_set_icon(GTK_WINDOW(fe->window),
2970                             gdk_pixbuf_new_from_xpm_data
2971                             ((const gchar **)xpm_icons[0]));
2972
2973         iconlist = NULL;
2974         for (n = 0; n < n_xpm_icons; n++) {
2975             iconlist =
2976                 g_list_append(iconlist,
2977                               gdk_pixbuf_new_from_xpm_data((const gchar **)
2978                                                            xpm_icons[n]));
2979         }
2980         gtk_window_set_icon_list(GTK_WINDOW(fe->window), iconlist);
2981     }
2982
2983     gtk_widget_show(fe->area);
2984     gtk_widget_show(fe->window);
2985
2986 #if !GTK_CHECK_VERSION(3,0,0)
2987     fe->drawing_area_shrink_pending = TRUE;
2988     try_shrink_drawing_area(fe);
2989 #endif
2990
2991     set_window_background(fe, 0);
2992
2993     return fe;
2994 }
2995
2996 char *fgetline(FILE *fp)
2997 {
2998     char *ret = snewn(512, char);
2999     int size = 512, len = 0;
3000     while (fgets(ret + len, size - len, fp)) {
3001         len += strlen(ret + len);
3002         if (ret[len-1] == '\n')
3003             break;                     /* got a newline, we're done */
3004         size = len + 512;
3005         ret = sresize(ret, size, char);
3006     }
3007     if (len == 0) {                    /* first fgets returned NULL */
3008         sfree(ret);
3009         return NULL;
3010     }
3011     ret[len] = '\0';
3012     return ret;
3013 }
3014
3015 static void list_presets_from_menu(struct preset_menu *menu)
3016 {
3017     int i;
3018
3019     for (i = 0; i < menu->n_entries; i++) {
3020         if (menu->entries[i].params) {
3021             char *paramstr = thegame.encode_params(
3022                 menu->entries[i].params, TRUE);
3023             printf("%s %s\n", paramstr, menu->entries[i].title);
3024             sfree(paramstr);
3025         } else {
3026             list_presets_from_menu(menu->entries[i].submenu);
3027         }
3028     }
3029 }
3030
3031 int main(int argc, char **argv)
3032 {
3033     char *pname = argv[0];
3034     char *error;
3035     int ngenerate = 0, print = FALSE, px = 1, py = 1;
3036     int time_generation = FALSE, test_solve = FALSE, list_presets = FALSE;
3037     int soln = FALSE, colour = FALSE;
3038     float scale = 1.0F;
3039     float redo_proportion = 0.0F;
3040     const char *savefile = NULL, *savesuffix = NULL;
3041     char *arg = NULL;
3042     int argtype = ARG_EITHER;
3043     char *screenshot_file = NULL;
3044     int doing_opts = TRUE;
3045     int ac = argc;
3046     char **av = argv;
3047     char errbuf[500];
3048
3049     /*
3050      * Command line parsing in this function is rather fiddly,
3051      * because GTK wants to have a go at argc/argv _first_ - and
3052      * yet we can't let it, because gtk_init() will bomb out if it
3053      * can't open an X display, whereas in fact we want to permit
3054      * our --generate and --print modes to run without an X
3055      * display.
3056      * 
3057      * So what we do is:
3058      *  - we parse the command line ourselves, without modifying
3059      *    argc/argv
3060      *  - if we encounter an error which might plausibly be the
3061      *    result of a GTK command line (i.e. not detailed errors in
3062      *    particular options of ours) we store the error message
3063      *    and terminate parsing.
3064      *  - if we got enough out of the command line to know it
3065      *    specifies a non-X mode of operation, we either display
3066      *    the stored error and return failure, or if there is no
3067      *    stored error we do the non-X operation and return
3068      *    success.
3069      *  - otherwise, we go straight to gtk_init().
3070      */
3071
3072     errbuf[0] = '\0';
3073     while (--ac > 0) {
3074         char *p = *++av;
3075         if (doing_opts && !strcmp(p, "--version")) {
3076             printf("%s, from Simon Tatham's Portable Puzzle Collection\n%s\n",
3077                    thegame.name, ver);
3078             return 0;
3079         } else if (doing_opts && !strcmp(p, "--generate")) {
3080             if (--ac > 0) {
3081                 ngenerate = atoi(*++av);
3082                 if (!ngenerate) {
3083                     fprintf(stderr, "%s: '--generate' expected a number\n",
3084                             pname);
3085                     return 1;
3086                 }
3087             } else
3088                 ngenerate = 1;
3089         } else if (doing_opts && !strcmp(p, "--time-generation")) {
3090             time_generation = TRUE;
3091         } else if (doing_opts && !strcmp(p, "--test-solve")) {
3092             test_solve = TRUE;
3093         } else if (doing_opts && !strcmp(p, "--list-presets")) {
3094             list_presets = TRUE;
3095         } else if (doing_opts && !strcmp(p, "--save")) {
3096             if (--ac > 0) {
3097                 savefile = *++av;
3098             } else {
3099                 fprintf(stderr, "%s: '--save' expected a filename\n",
3100                         pname);
3101                 return 1;
3102             }
3103         } else if (doing_opts && (!strcmp(p, "--save-suffix") ||
3104                                   !strcmp(p, "--savesuffix"))) {
3105             if (--ac > 0) {
3106                 savesuffix = *++av;
3107             } else {
3108                 fprintf(stderr, "%s: '--save-suffix' expected a filename\n",
3109                         pname);
3110                 return 1;
3111             }
3112         } else if (doing_opts && !strcmp(p, "--print")) {
3113             if (!thegame.can_print) {
3114                 fprintf(stderr, "%s: this game does not support printing\n",
3115                         pname);
3116                 return 1;
3117             }
3118             print = TRUE;
3119             if (--ac > 0) {
3120                 char *dim = *++av;
3121                 if (sscanf(dim, "%dx%d", &px, &py) != 2) {
3122                     fprintf(stderr, "%s: unable to parse argument '%s' to "
3123                             "'--print'\n", pname, dim);
3124                     return 1;
3125                 }
3126             } else {
3127                 px = py = 1;
3128             }
3129         } else if (doing_opts && !strcmp(p, "--scale")) {
3130             if (--ac > 0) {
3131                 scale = atof(*++av);
3132             } else {
3133                 fprintf(stderr, "%s: no argument supplied to '--scale'\n",
3134                         pname);
3135                 return 1;
3136             }
3137         } else if (doing_opts && !strcmp(p, "--redo")) {
3138             /*
3139              * This is an internal option which I don't expect
3140              * users to have any particular use for. The effect of
3141              * --redo is that once the game has been loaded and
3142              * initialised, the next move in the redo chain is
3143              * replayed, and the game screen is redrawn part way
3144              * through the making of the move. This is only
3145              * meaningful if there _is_ a next move in the redo
3146              * chain, which means in turn that this option is only
3147              * useful if you're also passing a save file on the
3148              * command line.
3149              *
3150              * This option is used by the script which generates
3151              * the puzzle icons and website screenshots, and I
3152              * don't imagine it's useful for anything else.
3153              * (Unless, I suppose, users don't like my screenshots
3154              * and want to generate their own in the same way for
3155              * some repackaged version of the puzzles.)
3156              */
3157             if (--ac > 0) {
3158                 redo_proportion = atof(*++av);
3159             } else {
3160                 fprintf(stderr, "%s: no argument supplied to '--redo'\n",
3161                         pname);
3162                 return 1;
3163             }
3164         } else if (doing_opts && !strcmp(p, "--screenshot")) {
3165             /*
3166              * Another internal option for the icon building
3167              * script. This causes a screenshot of the central
3168              * drawing area (i.e. not including the menu bar or
3169              * status bar) to be saved to a PNG file once the
3170              * window has been drawn, and then the application
3171              * quits immediately.
3172              */
3173             if (--ac > 0) {
3174                 screenshot_file = *++av;
3175             } else {
3176                 fprintf(stderr, "%s: no argument supplied to '--screenshot'\n",
3177                         pname);
3178                 return 1;
3179             }
3180         } else if (doing_opts && (!strcmp(p, "--with-solutions") ||
3181                                   !strcmp(p, "--with-solution") ||
3182                                   !strcmp(p, "--with-solns") ||
3183                                   !strcmp(p, "--with-soln") ||
3184                                   !strcmp(p, "--solutions") ||
3185                                   !strcmp(p, "--solution") ||
3186                                   !strcmp(p, "--solns") ||
3187                                   !strcmp(p, "--soln"))) {
3188             soln = TRUE;
3189         } else if (doing_opts && !strcmp(p, "--colour")) {
3190             if (!thegame.can_print_in_colour) {
3191                 fprintf(stderr, "%s: this game does not support colour"
3192                         " printing\n", pname);
3193                 return 1;
3194             }
3195             colour = TRUE;
3196         } else if (doing_opts && !strcmp(p, "--load")) {
3197             argtype = ARG_SAVE;
3198         } else if (doing_opts && !strcmp(p, "--game")) {
3199             argtype = ARG_ID;
3200         } else if (doing_opts && !strcmp(p, "--")) {
3201             doing_opts = FALSE;
3202         } else if (!doing_opts || p[0] != '-') {
3203             if (arg) {
3204                 fprintf(stderr, "%s: more than one argument supplied\n",
3205                         pname);
3206                 return 1;
3207             }
3208             arg = p;
3209         } else {
3210             sprintf(errbuf, "%.100s: unrecognised option '%.100s'\n",
3211                     pname, p);
3212             break;
3213         }
3214     }
3215
3216     /*
3217      * Special standalone mode for generating puzzle IDs on the
3218      * command line. Useful for generating puzzles to be printed
3219      * out and solved offline (for puzzles where that even makes
3220      * sense - Solo, for example, is a lot more pencil-and-paper
3221      * friendly than Twiddle!)
3222      * 
3223      * Usage:
3224      * 
3225      *   <puzzle-name> --generate [<n> [<params>]]
3226      * 
3227      * <n>, if present, is the number of puzzle IDs to generate.
3228      * <params>, if present, is the same type of parameter string
3229      * you would pass to the puzzle when running it in GUI mode,
3230      * including optional extras such as the expansion factor in
3231      * Rectangles and the difficulty level in Solo.
3232      * 
3233      * If you specify <params>, you must also specify <n> (although
3234      * you may specify it to be 1). Sorry; that was the
3235      * simplest-to-parse command-line syntax I came up with.
3236      */
3237     if (ngenerate > 0 || print || savefile || savesuffix) {
3238         int i, n = 1;
3239         midend *me;
3240         char *id;
3241         document *doc = NULL;
3242
3243         /*
3244          * If we're in this branch, we should display any pending
3245          * error message from the command line, since GTK isn't going
3246          * to take another crack at making sense of it.
3247          */
3248         if (*errbuf) {
3249             fputs(errbuf, stderr);
3250             return 1;
3251         }
3252
3253         n = ngenerate;
3254
3255         me = midend_new(NULL, &thegame, NULL, NULL);
3256         i = 0;
3257
3258         if (savefile && !savesuffix)
3259             savesuffix = "";
3260         if (!savefile && savesuffix)
3261             savefile = "";
3262
3263         if (print)
3264             doc = document_new(px, py, scale);
3265
3266         /*
3267          * In this loop, we either generate a game ID or read one
3268          * from stdin depending on whether we're in generate mode;
3269          * then we either write it to stdout or print it, depending
3270          * on whether we're in print mode. Thus, this loop handles
3271          * generate-to-stdout, print-from-stdin and generate-and-
3272          * immediately-print modes.
3273          * 
3274          * (It could also handle a copy-stdin-to-stdout mode,
3275          * although there's currently no combination of options
3276          * which will cause this loop to be activated in that mode.
3277          * It wouldn't be _entirely_ pointless, though, because
3278          * stdin could contain bare params strings or random-seed
3279          * IDs, and stdout would contain nothing but fully
3280          * generated descriptive game IDs.)
3281          */
3282         while (ngenerate == 0 || i < n) {
3283             char *pstr, *seed;
3284             const char *err;
3285             struct rusage before, after;
3286
3287             if (ngenerate == 0) {
3288                 pstr = fgetline(stdin);
3289                 if (!pstr)
3290                     break;
3291                 pstr[strcspn(pstr, "\r\n")] = '\0';
3292             } else {
3293                 if (arg) {
3294                     pstr = snewn(strlen(arg) + 40, char);
3295
3296                     strcpy(pstr, arg);
3297                     if (i > 0 && strchr(arg, '#'))
3298                         sprintf(pstr + strlen(pstr), "-%d", i);
3299                 } else
3300                     pstr = NULL;
3301             }
3302
3303             if (pstr) {
3304                 err = midend_game_id(me, pstr);
3305                 if (err) {
3306                     fprintf(stderr, "%s: error parsing '%s': %s\n",
3307                             pname, pstr, err);
3308                     return 1;
3309                 }
3310             }
3311
3312             if (time_generation)
3313                 getrusage(RUSAGE_SELF, &before);
3314
3315             midend_new_game(me);
3316
3317             seed = midend_get_random_seed(me);
3318
3319             if (time_generation) {
3320                 double elapsed;
3321
3322                 getrusage(RUSAGE_SELF, &after);
3323
3324                 elapsed = (after.ru_utime.tv_sec -
3325                            before.ru_utime.tv_sec);
3326                 elapsed += (after.ru_utime.tv_usec -
3327                             before.ru_utime.tv_usec) / 1000000.0;
3328
3329                 printf("%s %s: %.6f\n", thegame.name, seed, elapsed);
3330             }
3331
3332             if (test_solve && thegame.can_solve) {
3333                 /*
3334                  * Now destroy the aux_info in the midend, by means of
3335                  * re-entering the same game id, and then try to solve
3336                  * it.
3337                  */
3338                 char *game_id;
3339
3340                 game_id = midend_get_game_id(me);
3341                 err = midend_game_id(me, game_id);
3342                 if (err) {
3343                     fprintf(stderr, "%s %s: game id re-entry error: %s\n",
3344                             thegame.name, seed, err);
3345                     return 1;
3346                 }
3347                 midend_new_game(me);
3348                 sfree(game_id);
3349
3350                 err = midend_solve(me);
3351                 /*
3352                  * If the solve operation returned the error "Solution
3353                  * not known for this puzzle", that's OK, because that
3354                  * just means it's a puzzle for which we don't have an
3355                  * algorithmic solver and hence can't solve it without
3356                  * the aux_info, e.g. Netslide. Any other error is a
3357                  * problem, though.
3358                  */
3359                 if (err && strcmp(err, "Solution not known for this puzzle")) {
3360                     fprintf(stderr, "%s %s: solve error: %s\n",
3361                             thegame.name, seed, err);
3362                     return 1;
3363                 }
3364             }
3365
3366             sfree(pstr);
3367             sfree(seed);
3368
3369             if (doc) {
3370                 err = midend_print_puzzle(me, doc, soln);
3371                 if (err) {
3372                     fprintf(stderr, "%s: error in printing: %s\n", pname, err);
3373                     return 1;
3374                 }
3375             }
3376             if (savefile) {
3377                 struct savefile_write_ctx ctx;
3378                 char *realname = snewn(40 + strlen(savefile) +
3379                                        strlen(savesuffix), char);
3380                 sprintf(realname, "%s%d%s", savefile, i, savesuffix);
3381
3382                 if (soln) {
3383                     const char *err = midend_solve(me);
3384                     if (err) {
3385                         fprintf(stderr, "%s: unable to show solution: %s\n",
3386                                 realname, err);
3387                         return 1;
3388                     }
3389                 }
3390
3391                 ctx.fp = fopen(realname, "w");
3392                 if (!ctx.fp) {
3393                     fprintf(stderr, "%s: open: %s\n", realname,
3394                             strerror(errno));
3395                     return 1;
3396                 }
3397                 ctx.error = 0;
3398                 midend_serialise(me, savefile_write, &ctx);
3399                 if (ctx.error) {
3400                     fprintf(stderr, "%s: write: %s\n", realname,
3401                             strerror(ctx.error));
3402                     return 1;
3403                 }
3404                 if (fclose(ctx.fp)) {
3405                     fprintf(stderr, "%s: close: %s\n", realname,
3406                             strerror(errno));
3407                     return 1;
3408                 }
3409                 sfree(realname);
3410             }
3411             if (!doc && !savefile && !time_generation) {
3412                 id = midend_get_game_id(me);
3413                 puts(id);
3414                 sfree(id);
3415             }
3416
3417             i++;
3418         }
3419
3420         if (doc) {
3421             psdata *ps = ps_init(stdout, colour);
3422             document_print(doc, ps_drawing_api(ps));
3423             document_free(doc);
3424             ps_free(ps);
3425         }
3426
3427         midend_free(me);
3428
3429         return 0;
3430     } else if (list_presets) {
3431         /*
3432          * Another specialist mode which causes the puzzle to list the
3433          * game_params strings for all its preset configurations.
3434          */
3435         midend *me;
3436         struct preset_menu *menu;
3437
3438         me = midend_new(NULL, &thegame, NULL, NULL);
3439         menu = midend_get_presets(me, NULL);
3440         list_presets_from_menu(menu);
3441         midend_free(me);
3442         return 0;
3443     } else {
3444         frontend *fe;
3445
3446         gtk_init(&argc, &argv);
3447
3448         fe = new_window(arg, argtype, &error);
3449
3450         if (!fe) {
3451             fprintf(stderr, "%s: %s\n", pname, error);
3452             return 1;
3453         }
3454
3455         if (screenshot_file) {
3456             /*
3457              * Some puzzles will not redraw their entire area if
3458              * given a partially completed animation, which means
3459              * we must redraw now and _then_ redraw again after
3460              * freezing the move timer.
3461              */
3462             midend_force_redraw(fe->me);
3463         }
3464
3465         if (redo_proportion) {
3466             /* Start a redo. */
3467             midend_process_key(fe->me, 0, 0, 'r');
3468             /* And freeze the timer at the specified position. */
3469             midend_freeze_timer(fe->me, redo_proportion);
3470         }
3471
3472         if (screenshot_file) {
3473             save_screenshot_png(fe, screenshot_file);
3474             exit(0);
3475         }
3476
3477         gtk_main();
3478     }
3479
3480     return 0;
3481 }