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