summaryrefslogtreecommitdiff
path: root/apps/plugins/puzzles/src/gtk.c
diff options
context:
space:
mode:
Diffstat (limited to 'apps/plugins/puzzles/src/gtk.c')
-rw-r--r--apps/plugins/puzzles/src/gtk.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}