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