2 * gtk.c: GTK front end for my puzzle collection.
15 #include <sys/resource.h>
18 #include <gdk/gdkkeysyms.h>
20 #include <gdk-pixbuf/gdk-pixbuf.h>
24 #include <X11/Xutil.h>
25 #include <X11/Xatom.h>
29 #if GTK_CHECK_VERSION(2,0,0)
31 # ifdef PANGO_VERSION_CHECK
32 # if PANGO_VERSION_CHECK(1,8,0)
33 # define HAVE_SENSIBLE_ABSOLUTE_SIZE_FUNCTION
37 #if !GTK_CHECK_VERSION(2,4,0)
40 #if GTK_CHECK_VERSION(2,8,0)
42 # if GTK_CHECK_VERSION(3,0,0) || defined(GDK_DISABLE_DEPRECATED)
43 # define USE_CAIRO_WITHOUT_PIXMAP
47 #if GTK_CHECK_VERSION(3,0,0)
48 /* The old names are still more concise! */
49 #define gtk_hbox_new(x,y) gtk_box_new(GTK_ORIENTATION_HORIZONTAL,y)
50 #define gtk_vbox_new(x,y) gtk_box_new(GTK_ORIENTATION_VERTICAL,y)
51 /* GTK 3 has retired stock button labels */
52 #define LABEL_OK "_OK"
53 #define LABEL_CANCEL "_Cancel"
54 #define LABEL_NO "_No"
55 #define LABEL_YES "_Yes"
56 #define LABEL_SAVE "_Save"
57 #define LABEL_OPEN "_Open"
58 #define gtk_button_new_with_our_label gtk_button_new_with_mnemonic
60 #define LABEL_OK GTK_STOCK_OK
61 #define LABEL_CANCEL GTK_STOCK_CANCEL
62 #define LABEL_NO GTK_STOCK_NO
63 #define LABEL_YES GTK_STOCK_YES
64 #define LABEL_SAVE GTK_STOCK_SAVE
65 #define LABEL_OPEN GTK_STOCK_OPEN
66 #define gtk_button_new_with_our_label gtk_button_new_from_stock
69 /* #undef USE_CAIRO */
70 /* #define NO_THICK_LINE */
72 static FILE *debug_fp = NULL;
77 debug_fp = fopen("debug.log", "w");
88 void debug_printf(char *fmt, ...)
94 vsprintf(buf, fmt, ap);
100 /* ----------------------------------------------------------------------
101 * Error reporting functions used elsewhere.
104 void fatal(char *fmt, ...)
108 fprintf(stderr, "fatal error: ");
111 vfprintf(stderr, fmt, ap);
114 fprintf(stderr, "\n");
118 /* ----------------------------------------------------------------------
119 * GTK front end to puzzles.
122 static void changed_preset(frontend *fe);
126 PangoFontDescription *desc;
135 * This structure holds all the data relevant to a single window.
136 * In principle this would allow us to open multiple independent
137 * puzzle windows, although I can't currently see any real point in
138 * doing so. I'm just coding cleanly because there's no
139 * particularly good reason not to.
143 GtkAccelGroup *accelgroup;
145 GtkWidget *statusbar;
147 #if GTK_CHECK_VERSION(3,20,0)
148 GtkCssProvider *css_provider;
154 const float *colours;
156 cairo_surface_t *image;
157 #ifndef USE_CAIRO_WITHOUT_PIXMAP
160 GdkColor background; /* for painting outside puzzle area */
166 int backgroundindex; /* which of colours[] is background */
169 int bbox_l, bbox_r, bbox_u, bbox_d;
170 int timer_active, timer_id;
171 struct timeval last_time;
173 int nfonts, fontsize;
175 int cfg_which, cfgret;
179 int pw, ph; /* pixmap size (w, h are area size) */
180 int ox, oy; /* offset of pixmap in drawing area */
184 GSList *preset_radio;
185 int n_preset_menu_items;
187 GtkWidget *preset_custom;
188 GtkWidget *copy_menu_item;
189 #if !GTK_CHECK_VERSION(3,0,0)
190 int drawing_area_shrink_pending;
191 int menubar_is_local;
197 cairo_surface_t *image;
204 void get_random_seed(void **randseed, int *randseedsize)
206 struct timeval *tvp = snew(struct timeval);
207 gettimeofday(tvp, NULL);
208 *randseed = (void *)tvp;
209 *randseedsize = sizeof(struct timeval);
212 void frontend_default_colour(frontend *fe, float *output)
214 #if !GTK_CHECK_VERSION(3,0,0)
216 * Use the widget style's default background colour as the
217 * background for the puzzle drawing area.
219 GdkColor col = gtk_widget_get_style(fe->window)->bg[GTK_STATE_NORMAL];
220 output[0] = col.red / 65535.0;
221 output[1] = col.green / 65535.0;
222 output[2] = col.blue / 65535.0;
225 * GTK 3 has decided that there's no such thing as a 'default
226 * background colour' any more, because widget styles might set
227 * the background to something more complicated like a background
228 * image. We don't want to get into overlaying our entire puzzle
229 * on an arbitrary background image, so we'll just make up a
230 * reasonable shade of grey.
232 output[0] = output[1] = output[2] = 0.9F;
236 void gtk_status_bar(void *handle, char *text)
238 frontend *fe = (frontend *)handle;
240 assert(fe->statusbar);
242 gtk_statusbar_pop(GTK_STATUSBAR(fe->statusbar), fe->statusctx);
243 gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx, text);
246 /* ----------------------------------------------------------------------
247 * Cairo drawing functions.
252 static void setup_drawing(frontend *fe)
254 fe->cr = cairo_create(fe->image);
255 cairo_set_antialias(fe->cr, CAIRO_ANTIALIAS_GRAY);
256 cairo_set_line_width(fe->cr, 1.0);
257 cairo_set_line_cap(fe->cr, CAIRO_LINE_CAP_SQUARE);
258 cairo_set_line_join(fe->cr, CAIRO_LINE_JOIN_ROUND);
261 static void teardown_drawing(frontend *fe)
263 cairo_destroy(fe->cr);
266 #ifndef USE_CAIRO_WITHOUT_PIXMAP
268 cairo_t *cr = gdk_cairo_create(fe->pixmap);
269 cairo_set_source_surface(cr, fe->image, 0, 0);
273 fe->bbox_r - fe->bbox_l + 2,
274 fe->bbox_d - fe->bbox_u + 2);
281 static void snaffle_colours(frontend *fe)
283 fe->colours = midend_colours(fe->me, &fe->ncolours);
286 static void set_colour(frontend *fe, int colour)
288 cairo_set_source_rgb(fe->cr,
289 fe->colours[3*colour + 0],
290 fe->colours[3*colour + 1],
291 fe->colours[3*colour + 2]);
294 static void set_window_background(frontend *fe, int colour)
296 #if GTK_CHECK_VERSION(3,20,0)
298 sprintf(css_buf, ".background { "
299 "background-color: #%02x%02x%02x; }",
300 (unsigned)(fe->colours[3*colour + 0] * 255),
301 (unsigned)(fe->colours[3*colour + 1] * 255),
302 (unsigned)(fe->colours[3*colour + 2] * 255));
303 if (!fe->css_provider)
304 fe->css_provider = gtk_css_provider_new();
305 if (!gtk_css_provider_load_from_data(
306 GTK_CSS_PROVIDER(fe->css_provider), css_buf, -1, NULL))
307 assert(0 && "Couldn't load CSS");
308 gtk_style_context_add_provider(
309 gtk_widget_get_style_context(fe->window),
310 GTK_STYLE_PROVIDER(fe->css_provider),
311 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
312 gtk_style_context_add_provider(
313 gtk_widget_get_style_context(fe->area),
314 GTK_STYLE_PROVIDER(fe->css_provider),
315 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
316 #elif GTK_CHECK_VERSION(3,0,0)
318 rgba.red = fe->colours[3*colour + 0];
319 rgba.green = fe->colours[3*colour + 1];
320 rgba.blue = fe->colours[3*colour + 2];
322 gdk_window_set_background_rgba(gtk_widget_get_window(fe->area), &rgba);
323 gdk_window_set_background_rgba(gtk_widget_get_window(fe->window), &rgba);
327 colmap = gdk_colormap_get_system();
328 fe->background.red = fe->colours[3*colour + 0] * 65535;
329 fe->background.green = fe->colours[3*colour + 1] * 65535;
330 fe->background.blue = fe->colours[3*colour + 2] * 65535;
331 if (!gdk_colormap_alloc_color(colmap, &fe->background, FALSE, FALSE)) {
332 g_error("couldn't allocate background (#%02x%02x%02x)\n",
333 fe->background.red >> 8, fe->background.green >> 8,
334 fe->background.blue >> 8);
336 gdk_window_set_background(gtk_widget_get_window(fe->area),
338 gdk_window_set_background(gtk_widget_get_window(fe->window),
343 static PangoLayout *make_pango_layout(frontend *fe)
345 return (pango_cairo_create_layout(fe->cr));
348 static void draw_pango_layout(frontend *fe, PangoLayout *layout,
351 cairo_move_to(fe->cr, x, y);
352 pango_cairo_show_layout(fe->cr, layout);
355 static void save_screenshot_png(frontend *fe, const char *screenshot_file)
357 cairo_surface_write_to_png(fe->image, screenshot_file);
360 static void do_clip(frontend *fe, int x, int y, int w, int h)
362 cairo_new_path(fe->cr);
363 cairo_rectangle(fe->cr, x, y, w, h);
367 static void do_unclip(frontend *fe)
369 cairo_reset_clip(fe->cr);
372 static void do_draw_rect(frontend *fe, int x, int y, int w, int h)
375 cairo_new_path(fe->cr);
376 cairo_set_antialias(fe->cr, CAIRO_ANTIALIAS_NONE);
377 cairo_rectangle(fe->cr, x, y, w, h);
379 cairo_restore(fe->cr);
382 static void do_draw_line(frontend *fe, int x1, int y1, int x2, int y2)
384 cairo_new_path(fe->cr);
385 cairo_move_to(fe->cr, x1 + 0.5, y1 + 0.5);
386 cairo_line_to(fe->cr, x2 + 0.5, y2 + 0.5);
387 cairo_stroke(fe->cr);
390 static void do_draw_thick_line(frontend *fe, float thickness,
391 float x1, float y1, float x2, float y2)
394 cairo_set_line_width(fe->cr, thickness);
395 cairo_new_path(fe->cr);
396 cairo_move_to(fe->cr, x1, y1);
397 cairo_line_to(fe->cr, x2, y2);
398 cairo_stroke(fe->cr);
399 cairo_restore(fe->cr);
402 static void do_draw_poly(frontend *fe, int *coords, int npoints,
403 int fillcolour, int outlinecolour)
407 cairo_new_path(fe->cr);
408 for (i = 0; i < npoints; i++)
409 cairo_line_to(fe->cr, coords[i*2] + 0.5, coords[i*2 + 1] + 0.5);
410 cairo_close_path(fe->cr);
411 if (fillcolour >= 0) {
412 set_colour(fe, fillcolour);
413 cairo_fill_preserve(fe->cr);
415 assert(outlinecolour >= 0);
416 set_colour(fe, outlinecolour);
417 cairo_stroke(fe->cr);
420 static void do_draw_circle(frontend *fe, int cx, int cy, int radius,
421 int fillcolour, int outlinecolour)
423 cairo_new_path(fe->cr);
424 cairo_arc(fe->cr, cx + 0.5, cy + 0.5, radius, 0, 2*PI);
425 cairo_close_path(fe->cr); /* Just in case... */
426 if (fillcolour >= 0) {
427 set_colour(fe, fillcolour);
428 cairo_fill_preserve(fe->cr);
430 assert(outlinecolour >= 0);
431 set_colour(fe, outlinecolour);
432 cairo_stroke(fe->cr);
435 static void setup_blitter(blitter *bl, int w, int h)
437 bl->image = cairo_image_surface_create(CAIRO_FORMAT_RGB24, w, h);
440 static void teardown_blitter(blitter *bl)
442 cairo_surface_destroy(bl->image);
445 static void do_blitter_save(frontend *fe, blitter *bl, int x, int y)
447 cairo_t *cr = cairo_create(bl->image);
449 cairo_set_source_surface(cr, fe->image, -x, -y);
454 static void do_blitter_load(frontend *fe, blitter *bl, int x, int y)
456 cairo_set_source_surface(fe->cr, bl->image, x, y);
460 static void clear_backing_store(frontend *fe)
465 static void wipe_and_destroy_cairo(frontend *fe, cairo_t *cr)
467 cairo_set_source_rgb(cr, fe->colours[0], fe->colours[1], fe->colours[2]);
472 static void setup_backing_store(frontend *fe)
474 #ifndef USE_CAIRO_WITHOUT_PIXMAP
475 fe->pixmap = gdk_pixmap_new(gtk_widget_get_window(fe->area),
478 fe->image = cairo_image_surface_create(CAIRO_FORMAT_RGB24,
481 wipe_and_destroy_cairo(fe, cairo_create(fe->image));
482 #ifndef USE_CAIRO_WITHOUT_PIXMAP
483 wipe_and_destroy_cairo(fe, gdk_cairo_create(fe->pixmap));
485 wipe_and_destroy_cairo(fe, gdk_cairo_create
486 (gtk_widget_get_window(fe->area)));
489 static int backing_store_ok(frontend *fe)
491 return (!!fe->image);
494 static void teardown_backing_store(frontend *fe)
496 cairo_surface_destroy(fe->image);
497 #ifndef USE_CAIRO_WITHOUT_PIXMAP
498 gdk_pixmap_unref(fe->pixmap);
505 /* ----------------------------------------------------------------------
506 * GDK drawing functions.
511 static void setup_drawing(frontend *fe)
513 fe->gc = gdk_gc_new(fe->area->window);
516 static void teardown_drawing(frontend *fe)
518 gdk_gc_unref(fe->gc);
522 static void snaffle_colours(frontend *fe)
528 fe->colmap = gdk_colormap_get_system();
529 colours = midend_colours(fe->me, &ncolours);
530 fe->ncolours = ncolours;
531 fe->colours = snewn(ncolours, GdkColor);
532 for (i = 0; i < ncolours; i++) {
533 fe->colours[i].red = colours[i*3] * 0xFFFF;
534 fe->colours[i].green = colours[i*3+1] * 0xFFFF;
535 fe->colours[i].blue = colours[i*3+2] * 0xFFFF;
537 success = snewn(ncolours, gboolean);
538 gdk_colormap_alloc_colors(fe->colmap, fe->colours, ncolours,
539 FALSE, FALSE, success);
540 for (i = 0; i < ncolours; i++) {
542 g_error("couldn't allocate colour %d (#%02x%02x%02x)\n",
543 i, fe->colours[i].red >> 8,
544 fe->colours[i].green >> 8,
545 fe->colours[i].blue >> 8);
550 static void set_window_background(frontend *fe, int colour)
552 fe->backgroundindex = colour;
553 gdk_window_set_background(fe->area->window, &fe->colours[colour]);
554 gdk_window_set_background(fe->window->window, &fe->colours[colour]);
557 static void set_colour(frontend *fe, int colour)
559 gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
563 static PangoLayout *make_pango_layout(frontend *fe)
565 return (pango_layout_new(gtk_widget_get_pango_context(fe->area)));
568 static void draw_pango_layout(frontend *fe, PangoLayout *layout,
571 gdk_draw_layout(fe->pixmap, fe->gc, x, y, layout);
575 static void save_screenshot_png(frontend *fe, const char *screenshot_file)
578 GError *gerror = NULL;
580 midend_redraw(fe->me);
582 pb = gdk_pixbuf_get_from_drawable(NULL, fe->pixmap,
583 NULL, 0, 0, 0, 0, -1, -1);
584 gdk_pixbuf_save(pb, screenshot_file, "png", &gerror, NULL);
587 static void do_clip(frontend *fe, int x, int y, int w, int h)
595 gdk_gc_set_clip_rectangle(fe->gc, &rect);
598 static void do_unclip(frontend *fe)
606 gdk_gc_set_clip_rectangle(fe->gc, &rect);
609 static void do_draw_rect(frontend *fe, int x, int y, int w, int h)
611 gdk_draw_rectangle(fe->pixmap, fe->gc, 1, x, y, w, h);
614 static void do_draw_line(frontend *fe, int x1, int y1, int x2, int y2)
616 gdk_draw_line(fe->pixmap, fe->gc, x1, y1, x2, y2);
619 static void do_draw_thick_line(frontend *fe, float thickness,
620 float x1, float y1, float x2, float y2)
624 gdk_gc_get_values(fe->gc, &save);
625 gdk_gc_set_line_attributes(fe->gc,
630 gdk_draw_line(fe->pixmap, fe->gc, x1, y1, x2, y2);
631 gdk_gc_set_line_attributes(fe->gc,
638 static void do_draw_poly(frontend *fe, int *coords, int npoints,
639 int fillcolour, int outlinecolour)
641 GdkPoint *points = snewn(npoints, GdkPoint);
644 for (i = 0; i < npoints; i++) {
645 points[i].x = coords[i*2];
646 points[i].y = coords[i*2+1];
649 if (fillcolour >= 0) {
650 set_colour(fe, fillcolour);
651 gdk_draw_polygon(fe->pixmap, fe->gc, TRUE, points, npoints);
653 assert(outlinecolour >= 0);
654 set_colour(fe, outlinecolour);
657 * In principle we ought to be able to use gdk_draw_polygon for
658 * the outline as well. In fact, it turns out to interact badly
659 * with a clipping region, for no terribly obvious reason, so I
660 * draw the outline as a sequence of lines instead.
662 for (i = 0; i < npoints; i++)
663 gdk_draw_line(fe->pixmap, fe->gc,
664 points[i].x, points[i].y,
665 points[(i+1)%npoints].x, points[(i+1)%npoints].y);
670 static void do_draw_circle(frontend *fe, int cx, int cy, int radius,
671 int fillcolour, int outlinecolour)
673 if (fillcolour >= 0) {
674 set_colour(fe, fillcolour);
675 gdk_draw_arc(fe->pixmap, fe->gc, TRUE,
676 cx - radius, cy - radius,
677 2 * radius, 2 * radius, 0, 360 * 64);
680 assert(outlinecolour >= 0);
681 set_colour(fe, outlinecolour);
682 gdk_draw_arc(fe->pixmap, fe->gc, FALSE,
683 cx - radius, cy - radius,
684 2 * radius, 2 * radius, 0, 360 * 64);
687 static void setup_blitter(blitter *bl, int w, int h)
690 * We can't create the pixmap right now, because fe->window
691 * might not yet exist. So we just cache w and h and create it
692 * during the firs call to blitter_save.
697 static void teardown_blitter(blitter *bl)
700 gdk_pixmap_unref(bl->pixmap);
703 static void do_blitter_save(frontend *fe, blitter *bl, int x, int y)
706 bl->pixmap = gdk_pixmap_new(fe->area->window, bl->w, bl->h, -1);
707 gdk_draw_pixmap(bl->pixmap,
708 fe->area->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
710 x, y, 0, 0, bl->w, bl->h);
713 static void do_blitter_load(frontend *fe, blitter *bl, int x, int y)
716 gdk_draw_pixmap(fe->pixmap,
717 fe->area->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
719 0, 0, x, y, bl->w, bl->h);
722 static void clear_backing_store(frontend *fe)
727 static void setup_backing_store(frontend *fe)
731 fe->pixmap = gdk_pixmap_new(fe->area->window, fe->pw, fe->ph, -1);
733 gc = gdk_gc_new(fe->area->window);
734 gdk_gc_set_foreground(gc, &fe->colours[0]);
735 gdk_draw_rectangle(fe->pixmap, gc, 1, 0, 0, fe->pw, fe->ph);
736 gdk_draw_rectangle(fe->area->window, gc, 1, 0, 0, fe->w, fe->h);
740 static int backing_store_ok(frontend *fe)
742 return (!!fe->pixmap);
745 static void teardown_backing_store(frontend *fe)
747 gdk_pixmap_unref(fe->pixmap);
753 #ifndef USE_CAIRO_WITHOUT_PIXMAP
754 static void repaint_rectangle(frontend *fe, GtkWidget *widget,
755 int x, int y, int w, int h)
757 GdkGC *gc = gdk_gc_new(gtk_widget_get_window(widget));
759 gdk_gc_set_foreground(gc, &fe->background);
761 gdk_gc_set_foreground(gc, &fe->colours[fe->backgroundindex]);
764 gdk_draw_rectangle(gtk_widget_get_window(widget), gc,
765 TRUE, x, y, fe->ox - x, h);
770 gdk_draw_rectangle(gtk_widget_get_window(widget), gc,
771 TRUE, x, y, w, fe->oy - y);
776 gdk_draw_rectangle(gtk_widget_get_window(widget), gc,
777 TRUE, x + fe->pw, y, w - fe->pw, h);
781 gdk_draw_rectangle(gtk_widget_get_window(widget), gc,
782 TRUE, x, y + fe->ph, w, h - fe->ph);
785 gdk_draw_pixmap(gtk_widget_get_window(widget), gc, fe->pixmap,
786 x - fe->ox, y - fe->oy, x, y, w, h);
791 /* ----------------------------------------------------------------------
792 * Pango font functions.
797 static void add_font(frontend *fe, int index, int fonttype, int fontsize)
800 * Use Pango to find the closest match to the requested
803 PangoFontDescription *fd;
805 fd = pango_font_description_new();
806 /* `Monospace' and `Sans' are meta-families guaranteed to exist */
807 pango_font_description_set_family(fd, fonttype == FONT_FIXED ?
808 "Monospace" : "Sans");
809 pango_font_description_set_weight(fd, PANGO_WEIGHT_BOLD);
811 * I found some online Pango documentation which
812 * described a function called
813 * pango_font_description_set_absolute_size(), which is
814 * _exactly_ what I want here. Unfortunately, none of
815 * my local Pango installations have it (presumably
816 * they're too old), so I'm going to have to hack round
817 * it by figuring out the point size myself. This
818 * limits me to X and probably also breaks in later
819 * Pango installations, so ideally I should add another
820 * CHECK_VERSION type ifdef and use set_absolute_size
821 * where available. All very annoying.
823 #ifdef HAVE_SENSIBLE_ABSOLUTE_SIZE_FUNCTION
824 pango_font_description_set_absolute_size(fd, PANGO_SCALE*fontsize);
827 Display *d = GDK_DISPLAY();
828 int s = DefaultScreen(d);
830 (PANGO_SCALE * 72.27 / 25.4) *
831 ((double) DisplayWidthMM(d, s) / DisplayWidth (d, s));
832 pango_font_description_set_size(fd, resolution * fontsize);
835 fe->fonts[index].desc = fd;
838 static void align_and_draw_text(frontend *fe,
839 int index, int align, int x, int y,
845 layout = make_pango_layout(fe);
850 pango_layout_set_font_description(layout, fe->fonts[index].desc);
851 pango_layout_set_text(layout, text, strlen(text));
852 pango_layout_get_pixel_extents(layout, NULL, &rect);
854 if (align & ALIGN_VCENTRE)
855 rect.y -= rect.height / 2;
857 rect.y -= rect.height;
859 if (align & ALIGN_HCENTRE)
860 rect.x -= rect.width / 2;
861 else if (align & ALIGN_HRIGHT)
862 rect.x -= rect.width;
864 draw_pango_layout(fe, layout, rect.x + x, rect.y + y);
866 g_object_unref(layout);
871 /* ----------------------------------------------------------------------
872 * Old-fashioned font functions.
877 static void add_font(int index, int fonttype, int fontsize)
880 * In GTK 1.2, I don't know of any plausible way to
881 * pick a suitable font, so I'm just going to be
884 fe->fonts[i].font = gdk_font_load(fonttype == FONT_FIXED ?
885 "fixed" : "variable");
888 static void align_and_draw_text(int index, int align, int x, int y,
891 int lb, rb, wid, asc, desc;
894 * Measure vertical string extents with respect to the same
897 gdk_string_extents(fe->fonts[i].font,
898 "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
899 &lb, &rb, &wid, &asc, &desc);
900 if (align & ALIGN_VCENTRE)
901 y += asc - (asc+desc)/2;
906 * ... but horizontal extents with respect to the provided
907 * string. This means that multiple pieces of text centred
908 * on the same y-coordinate don't have different baselines.
910 gdk_string_extents(fe->fonts[i].font, text,
911 &lb, &rb, &wid, &asc, &desc);
913 if (align & ALIGN_HCENTRE)
915 else if (align & ALIGN_HRIGHT)
919 * Actually draw the text.
921 gdk_draw_string(fe->pixmap, fe->fonts[i].font, fe->gc, x, y, text);
926 /* ----------------------------------------------------------------------
927 * The exported drawing functions.
930 void gtk_start_draw(void *handle)
932 frontend *fe = (frontend *)handle;
940 void gtk_clip(void *handle, int x, int y, int w, int h)
942 frontend *fe = (frontend *)handle;
943 do_clip(fe, x, y, w, h);
946 void gtk_unclip(void *handle)
948 frontend *fe = (frontend *)handle;
952 void gtk_draw_text(void *handle, int x, int y, int fonttype, int fontsize,
953 int align, int colour, char *text)
955 frontend *fe = (frontend *)handle;
959 * Find or create the font.
961 for (i = 0; i < fe->nfonts; i++)
962 if (fe->fonts[i].type == fonttype && fe->fonts[i].size == fontsize)
965 if (i == fe->nfonts) {
966 if (fe->fontsize <= fe->nfonts) {
967 fe->fontsize = fe->nfonts + 10;
968 fe->fonts = sresize(fe->fonts, fe->fontsize, struct font);
973 fe->fonts[i].type = fonttype;
974 fe->fonts[i].size = fontsize;
975 add_font(fe, i, fonttype, fontsize);
981 set_colour(fe, colour);
982 align_and_draw_text(fe, i, align, x, y, text);
985 void gtk_draw_rect(void *handle, int x, int y, int w, int h, int colour)
987 frontend *fe = (frontend *)handle;
988 set_colour(fe, colour);
989 do_draw_rect(fe, x, y, w, h);
992 void gtk_draw_line(void *handle, int x1, int y1, int x2, int y2, int colour)
994 frontend *fe = (frontend *)handle;
995 set_colour(fe, colour);
996 do_draw_line(fe, x1, y1, x2, y2);
999 void gtk_draw_thick_line(void *handle, float thickness,
1000 float x1, float y1, float x2, float y2, int colour)
1002 frontend *fe = (frontend *)handle;
1003 set_colour(fe, colour);
1004 do_draw_thick_line(fe, thickness, x1, y1, x2, y2);
1007 void gtk_draw_poly(void *handle, int *coords, int npoints,
1008 int fillcolour, int outlinecolour)
1010 frontend *fe = (frontend *)handle;
1011 do_draw_poly(fe, coords, npoints, fillcolour, outlinecolour);
1014 void gtk_draw_circle(void *handle, int cx, int cy, int radius,
1015 int fillcolour, int outlinecolour)
1017 frontend *fe = (frontend *)handle;
1018 do_draw_circle(fe, cx, cy, radius, fillcolour, outlinecolour);
1021 blitter *gtk_blitter_new(void *handle, int w, int h)
1023 blitter *bl = snew(blitter);
1024 setup_blitter(bl, w, h);
1030 void gtk_blitter_free(void *handle, blitter *bl)
1032 teardown_blitter(bl);
1036 void gtk_blitter_save(void *handle, blitter *bl, int x, int y)
1038 frontend *fe = (frontend *)handle;
1039 do_blitter_save(fe, bl, x, y);
1044 void gtk_blitter_load(void *handle, blitter *bl, int x, int y)
1046 frontend *fe = (frontend *)handle;
1047 if (x == BLITTER_FROMSAVED && y == BLITTER_FROMSAVED) {
1051 do_blitter_load(fe, bl, x, y);
1054 void gtk_draw_update(void *handle, int x, int y, int w, int h)
1056 frontend *fe = (frontend *)handle;
1057 if (fe->bbox_l > x ) fe->bbox_l = x ;
1058 if (fe->bbox_r < x+w) fe->bbox_r = x+w;
1059 if (fe->bbox_u > y ) fe->bbox_u = y ;
1060 if (fe->bbox_d < y+h) fe->bbox_d = y+h;
1063 void gtk_end_draw(void *handle)
1065 frontend *fe = (frontend *)handle;
1067 teardown_drawing(fe);
1069 if (fe->bbox_l < fe->bbox_r && fe->bbox_u < fe->bbox_d) {
1070 #ifdef USE_CAIRO_WITHOUT_PIXMAP
1071 gtk_widget_queue_draw_area(fe->area,
1072 fe->bbox_l - 1 + fe->ox,
1073 fe->bbox_u - 1 + fe->oy,
1074 fe->bbox_r - fe->bbox_l + 2,
1075 fe->bbox_d - fe->bbox_u + 2);
1077 repaint_rectangle(fe, fe->area,
1078 fe->bbox_l - 1 + fe->ox,
1079 fe->bbox_u - 1 + fe->oy,
1080 fe->bbox_r - fe->bbox_l + 2,
1081 fe->bbox_d - fe->bbox_u + 2);
1087 char *gtk_text_fallback(void *handle, const char *const *strings, int nstrings)
1090 * We assume Pango can cope with any UTF-8 likely to be emitted
1093 return dupstr(strings[0]);
1097 const struct drawing_api gtk_drawing = {
1113 NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */
1114 NULL, NULL, /* line_width, line_dotted */
1120 #ifdef NO_THICK_LINE
1123 gtk_draw_thick_line,
1127 static void destroy(GtkWidget *widget, gpointer data)
1129 frontend *fe = (frontend *)data;
1130 deactivate_timer(fe);
1131 midend_free(fe->me);
1135 static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
1137 frontend *fe = (frontend *)data;
1139 int shift = (event->state & GDK_SHIFT_MASK) ? MOD_SHFT : 0;
1140 int ctrl = (event->state & GDK_CONTROL_MASK) ? MOD_CTRL : 0;
1142 if (!backing_store_ok(fe))
1145 #if !GTK_CHECK_VERSION(2,0,0)
1146 /* Gtk 1.2 passes a key event to this function even if it's also
1147 * defined as an accelerator.
1148 * Gtk 2 doesn't do this, and this function appears not to exist there. */
1149 if (fe->accelgroup &&
1150 gtk_accel_group_get_entry(fe->accelgroup,
1151 event->keyval, event->state))
1155 /* Handle mnemonics. */
1156 if (gtk_window_activate_key(GTK_WINDOW(fe->window), event))
1159 if (event->keyval == GDK_KEY_Up)
1160 keyval = shift | ctrl | CURSOR_UP;
1161 else if (event->keyval == GDK_KEY_KP_Up ||
1162 event->keyval == GDK_KEY_KP_8)
1163 keyval = MOD_NUM_KEYPAD | '8';
1164 else if (event->keyval == GDK_KEY_Down)
1165 keyval = shift | ctrl | CURSOR_DOWN;
1166 else if (event->keyval == GDK_KEY_KP_Down ||
1167 event->keyval == GDK_KEY_KP_2)
1168 keyval = MOD_NUM_KEYPAD | '2';
1169 else if (event->keyval == GDK_KEY_Left)
1170 keyval = shift | ctrl | CURSOR_LEFT;
1171 else if (event->keyval == GDK_KEY_KP_Left ||
1172 event->keyval == GDK_KEY_KP_4)
1173 keyval = MOD_NUM_KEYPAD | '4';
1174 else if (event->keyval == GDK_KEY_Right)
1175 keyval = shift | ctrl | CURSOR_RIGHT;
1176 else if (event->keyval == GDK_KEY_KP_Right ||
1177 event->keyval == GDK_KEY_KP_6)
1178 keyval = MOD_NUM_KEYPAD | '6';
1179 else if (event->keyval == GDK_KEY_KP_Home ||
1180 event->keyval == GDK_KEY_KP_7)
1181 keyval = MOD_NUM_KEYPAD | '7';
1182 else if (event->keyval == GDK_KEY_KP_End ||
1183 event->keyval == GDK_KEY_KP_1)
1184 keyval = MOD_NUM_KEYPAD | '1';
1185 else if (event->keyval == GDK_KEY_KP_Page_Up ||
1186 event->keyval == GDK_KEY_KP_9)
1187 keyval = MOD_NUM_KEYPAD | '9';
1188 else if (event->keyval == GDK_KEY_KP_Page_Down ||
1189 event->keyval == GDK_KEY_KP_3)
1190 keyval = MOD_NUM_KEYPAD | '3';
1191 else if (event->keyval == GDK_KEY_KP_Insert ||
1192 event->keyval == GDK_KEY_KP_0)
1193 keyval = MOD_NUM_KEYPAD | '0';
1194 else if (event->keyval == GDK_KEY_KP_Begin ||
1195 event->keyval == GDK_KEY_KP_5)
1196 keyval = MOD_NUM_KEYPAD | '5';
1197 else if (event->keyval == GDK_KEY_BackSpace ||
1198 event->keyval == GDK_KEY_Delete ||
1199 event->keyval == GDK_KEY_KP_Delete)
1201 else if (event->string[0] && !event->string[1])
1202 keyval = (unsigned char)event->string[0];
1207 !midend_process_key(fe->me, 0, 0, keyval))
1208 gtk_widget_destroy(fe->window);
1213 static gint button_event(GtkWidget *widget, GdkEventButton *event,
1216 frontend *fe = (frontend *)data;
1219 if (!backing_store_ok(fe))
1222 if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE)
1225 if (event->button == 2 || (event->state & GDK_SHIFT_MASK))
1226 button = MIDDLE_BUTTON;
1227 else if (event->button == 3 || (event->state & GDK_MOD1_MASK))
1228 button = RIGHT_BUTTON;
1229 else if (event->button == 1)
1230 button = LEFT_BUTTON;
1231 else if (event->button == 8 && event->type == GDK_BUTTON_PRESS)
1233 else if (event->button == 9 && event->type == GDK_BUTTON_PRESS)
1236 return FALSE; /* don't even know what button! */
1238 if (event->type == GDK_BUTTON_RELEASE && button >= LEFT_BUTTON)
1239 button += LEFT_RELEASE - LEFT_BUTTON;
1241 if (!midend_process_key(fe->me, event->x - fe->ox,
1242 event->y - fe->oy, button))
1243 gtk_widget_destroy(fe->window);
1248 static gint motion_event(GtkWidget *widget, GdkEventMotion *event,
1251 frontend *fe = (frontend *)data;
1254 if (!backing_store_ok(fe))
1257 if (event->state & (GDK_BUTTON2_MASK | GDK_SHIFT_MASK))
1258 button = MIDDLE_DRAG;
1259 else if (event->state & GDK_BUTTON1_MASK)
1261 else if (event->state & GDK_BUTTON3_MASK)
1262 button = RIGHT_DRAG;
1264 return FALSE; /* don't even know what button! */
1266 if (!midend_process_key(fe->me, event->x - fe->ox,
1267 event->y - fe->oy, button))
1268 gtk_widget_destroy(fe->window);
1269 #if GTK_CHECK_VERSION(2,12,0)
1270 gdk_event_request_motions(event);
1272 gdk_window_get_pointer(gtk_widget_get_window(widget), NULL, NULL, NULL);
1278 #if GTK_CHECK_VERSION(3,0,0)
1279 static gint draw_area(GtkWidget *widget, cairo_t *cr, gpointer data)
1281 frontend *fe = (frontend *)data;
1282 GdkRectangle dirtyrect;
1284 gdk_cairo_get_clip_rectangle(cr, &dirtyrect);
1285 cairo_set_source_surface(cr, fe->image, fe->ox, fe->oy);
1286 cairo_rectangle(cr, dirtyrect.x, dirtyrect.y,
1287 dirtyrect.width, dirtyrect.height);
1293 static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
1296 frontend *fe = (frontend *)data;
1298 if (backing_store_ok(fe)) {
1299 #ifdef USE_CAIRO_WITHOUT_PIXMAP
1300 cairo_t *cr = gdk_cairo_create(gtk_widget_get_window(widget));
1301 cairo_set_source_surface(cr, fe->image, fe->ox, fe->oy);
1302 cairo_rectangle(cr, event->area.x, event->area.y,
1303 event->area.width, event->area.height);
1307 repaint_rectangle(fe, widget,
1308 event->area.x, event->area.y,
1309 event->area.width, event->area.height);
1316 static gint map_window(GtkWidget *widget, GdkEvent *event,
1319 frontend *fe = (frontend *)data;
1322 * Apparently we need to do this because otherwise the status
1323 * bar will fail to update immediately. Annoying, but there we
1326 gtk_widget_queue_draw(fe->window);
1331 static gint configure_area(GtkWidget *widget,
1332 GdkEventConfigure *event, gpointer data)
1334 frontend *fe = (frontend *)data;
1336 int oldw = fe->w, oldpw = fe->pw, oldh = fe->h, oldph = fe->ph;
1342 midend_size(fe->me, &x, &y, TRUE);
1345 fe->ox = (fe->w - fe->pw) / 2;
1346 fe->oy = (fe->h - fe->ph) / 2;
1348 if (oldw != fe->w || oldpw != fe->pw ||
1349 oldh != fe->h || oldph != fe->ph || !backing_store_ok(fe)) {
1350 if (backing_store_ok(fe))
1351 teardown_backing_store(fe);
1352 setup_backing_store(fe);
1355 midend_force_redraw(fe->me);
1360 static gint timer_func(gpointer data)
1362 frontend *fe = (frontend *)data;
1364 if (fe->timer_active) {
1367 gettimeofday(&now, NULL);
1368 elapsed = ((now.tv_usec - fe->last_time.tv_usec) * 0.000001F +
1369 (now.tv_sec - fe->last_time.tv_sec));
1370 midend_timer(fe->me, elapsed); /* may clear timer_active */
1371 fe->last_time = now;
1374 return fe->timer_active;
1377 void deactivate_timer(frontend *fe)
1380 return; /* can happen due to --generate */
1381 if (fe->timer_active)
1382 g_source_remove(fe->timer_id);
1383 fe->timer_active = FALSE;
1386 void activate_timer(frontend *fe)
1389 return; /* can happen due to --generate */
1390 if (!fe->timer_active) {
1391 fe->timer_id = g_timeout_add(20, timer_func, fe);
1392 gettimeofday(&fe->last_time, NULL);
1394 fe->timer_active = TRUE;
1397 static void window_destroy(GtkWidget *widget, gpointer data)
1402 static int win_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data)
1404 GObject *cancelbutton = G_OBJECT(data);
1407 * `Escape' effectively clicks the cancel button
1409 if (event->keyval == GDK_KEY_Escape) {
1410 g_signal_emit_by_name(cancelbutton, "clicked");
1417 enum { MB_OK, MB_YESNO };
1419 static void align_label(GtkLabel *label, double x, double y)
1421 #if GTK_CHECK_VERSION(3,16,0)
1422 gtk_label_set_xalign(label, x);
1423 gtk_label_set_yalign(label, y);
1424 #elif GTK_CHECK_VERSION(3,14,0)
1425 gtk_widget_set_halign(GTK_WIDGET(label),
1426 x == 0 ? GTK_ALIGN_START :
1427 x == 1 ? GTK_ALIGN_END : GTK_ALIGN_CENTER);
1428 gtk_widget_set_valign(GTK_WIDGET(label),
1429 y == 0 ? GTK_ALIGN_START :
1430 y == 1 ? GTK_ALIGN_END : GTK_ALIGN_CENTER);
1432 gtk_misc_set_alignment(GTK_MISC(label), x, y);
1436 #if GTK_CHECK_VERSION(3,0,0)
1437 int message_box(GtkWidget *parent, char *title, char *msg, int centre,
1443 window = gtk_message_dialog_new
1444 (GTK_WINDOW(parent),
1445 (GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
1446 (type == MB_OK ? GTK_MESSAGE_INFO : GTK_MESSAGE_QUESTION),
1447 (type == MB_OK ? GTK_BUTTONS_OK : GTK_BUTTONS_YES_NO),
1449 gtk_window_set_title(GTK_WINDOW(window), title);
1450 ret = gtk_dialog_run(GTK_DIALOG(window));
1451 gtk_widget_destroy(window);
1452 return (type == MB_OK ? TRUE : (ret == GTK_RESPONSE_YES));
1454 #else /* GTK_CHECK_VERSION(3,0,0) */
1455 static void msgbox_button_clicked(GtkButton *button, gpointer data)
1457 GtkWidget *window = GTK_WIDGET(data);
1460 ip = (int *)g_object_get_data(G_OBJECT(window), "user-data");
1461 v = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "user-data"));
1464 gtk_widget_destroy(GTK_WIDGET(data));
1467 int message_box(GtkWidget *parent, char *title, char *msg, int centre,
1470 GtkWidget *window, *hbox, *text, *button;
1474 window = gtk_dialog_new();
1475 text = gtk_label_new(msg);
1476 align_label(GTK_LABEL(text), 0.0, 0.0);
1477 hbox = gtk_hbox_new(FALSE, 0);
1478 gtk_box_pack_start(GTK_BOX(hbox), text, FALSE, FALSE, 20);
1480 (GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(window))),
1481 hbox, FALSE, FALSE, 20);
1482 gtk_widget_show(text);
1483 gtk_widget_show(hbox);
1484 gtk_window_set_title(GTK_WINDOW(window), title);
1485 gtk_label_set_line_wrap(GTK_LABEL(text), TRUE);
1487 if (type == MB_OK) {
1488 titles = LABEL_OK "\0";
1491 assert(type == MB_YESNO);
1492 titles = LABEL_NO "\0" LABEL_YES "\0";
1499 button = gtk_button_new_with_our_label(titles);
1501 (GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(window))),
1502 button, FALSE, FALSE, 0);
1503 gtk_widget_show(button);
1505 gtk_widget_set_can_default(button, TRUE);
1506 gtk_window_set_default(GTK_WINDOW(window), button);
1509 g_signal_connect(G_OBJECT(window), "key_press_event",
1510 G_CALLBACK(win_key_press), button);
1512 g_signal_connect(G_OBJECT(button), "clicked",
1513 G_CALLBACK(msgbox_button_clicked), window);
1514 g_object_set_data(G_OBJECT(button), "user-data",
1515 GINT_TO_POINTER(i));
1516 titles += strlen(titles)+1;
1519 g_object_set_data(G_OBJECT(window), "user-data", &i);
1520 g_signal_connect(G_OBJECT(window), "destroy",
1521 G_CALLBACK(window_destroy), NULL);
1522 gtk_window_set_modal(GTK_WINDOW(window), TRUE);
1523 gtk_window_set_transient_for(GTK_WINDOW(window), GTK_WINDOW(parent));
1524 /* set_transient_window_pos(parent, window); */
1525 gtk_widget_show(window);
1528 return (type == MB_YESNO ? i == 1 : TRUE);
1530 #endif /* GTK_CHECK_VERSION(3,0,0) */
1532 void error_box(GtkWidget *parent, char *msg)
1534 message_box(parent, "Error", msg, FALSE, MB_OK);
1537 static void config_ok_button_clicked(GtkButton *button, gpointer data)
1539 frontend *fe = (frontend *)data;
1542 err = midend_set_config(fe->me, fe->cfg_which, fe->cfg);
1545 error_box(fe->cfgbox, err);
1548 gtk_widget_destroy(fe->cfgbox);
1553 static void config_cancel_button_clicked(GtkButton *button, gpointer data)
1555 frontend *fe = (frontend *)data;
1557 gtk_widget_destroy(fe->cfgbox);
1560 static int editbox_key(GtkWidget *widget, GdkEventKey *event, gpointer data)
1563 * GtkEntry has a nasty habit of eating the Return key, which
1564 * is unhelpful since it doesn't actually _do_ anything with it
1565 * (it calls gtk_widget_activate, but our edit boxes never need
1566 * activating). So I catch Return before GtkEntry sees it, and
1567 * pass it straight on to the parent widget. Effect: hitting
1568 * Return in an edit box will now activate the default button
1569 * in the dialog just like it will everywhere else.
1571 if (event->keyval == GDK_KEY_Return &&
1572 gtk_widget_get_parent(widget) != NULL) {
1574 g_signal_stop_emission_by_name(G_OBJECT(widget), "key_press_event");
1575 g_signal_emit_by_name(G_OBJECT(gtk_widget_get_parent(widget)),
1576 "key_press_event", event, &return_val);
1582 static void editbox_changed(GtkEditable *ed, gpointer data)
1584 config_item *i = (config_item *)data;
1587 i->sval = dupstr(gtk_entry_get_text(GTK_ENTRY(ed)));
1590 static void button_toggled(GtkToggleButton *tb, gpointer data)
1592 config_item *i = (config_item *)data;
1594 i->ival = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(tb));
1597 static void droplist_sel(GtkComboBox *combo, gpointer data)
1599 config_item *i = (config_item *)data;
1601 i->ival = gtk_combo_box_get_active(combo);
1604 static int get_config(frontend *fe, int which)
1606 GtkWidget *w, *table, *cancel;
1607 GtkBox *content_box, *button_box;
1612 fe->cfg = midend_get_config(fe->me, which, &title);
1613 fe->cfg_which = which;
1616 #if GTK_CHECK_VERSION(3,0,0)
1617 /* GtkDialog isn't quite flexible enough */
1618 fe->cfgbox = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1619 content_box = GTK_BOX(gtk_vbox_new(FALSE, 8));
1620 g_object_set(G_OBJECT(content_box), "margin", 8, (const char *)NULL);
1621 gtk_widget_show(GTK_WIDGET(content_box));
1622 gtk_container_add(GTK_CONTAINER(fe->cfgbox), GTK_WIDGET(content_box));
1623 button_box = GTK_BOX(gtk_hbox_new(FALSE, 8));
1624 gtk_widget_show(GTK_WIDGET(button_box));
1625 gtk_box_pack_end(content_box, GTK_WIDGET(button_box), FALSE, FALSE, 0);
1627 GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
1628 gtk_widget_show(sep);
1629 gtk_box_pack_end(content_box, sep, FALSE, FALSE, 0);
1632 fe->cfgbox = gtk_dialog_new();
1633 content_box = GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(fe->cfgbox)));
1634 button_box = GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(fe->cfgbox)));
1636 gtk_window_set_title(GTK_WINDOW(fe->cfgbox), title);
1639 w = gtk_button_new_with_our_label(LABEL_CANCEL);
1640 gtk_box_pack_end(button_box, w, FALSE, FALSE, 0);
1642 g_signal_connect(G_OBJECT(w), "clicked",
1643 G_CALLBACK(config_cancel_button_clicked), fe);
1646 w = gtk_button_new_with_our_label(LABEL_OK);
1647 gtk_box_pack_end(button_box, w, FALSE, FALSE, 0);
1649 gtk_widget_set_can_default(w, TRUE);
1650 gtk_window_set_default(GTK_WINDOW(fe->cfgbox), w);
1651 g_signal_connect(G_OBJECT(w), "clicked",
1652 G_CALLBACK(config_ok_button_clicked), fe);
1654 #if GTK_CHECK_VERSION(3,0,0)
1655 table = gtk_grid_new();
1657 table = gtk_table_new(1, 2, FALSE);
1660 gtk_box_pack_start(content_box, table, FALSE, FALSE, 0);
1661 gtk_widget_show(table);
1663 for (i = fe->cfg; i->type != C_END; i++) {
1664 #if !GTK_CHECK_VERSION(3,0,0)
1665 gtk_table_resize(GTK_TABLE(table), y+1, 2);
1671 * Edit box with a label beside it.
1674 w = gtk_label_new(i->name);
1675 align_label(GTK_LABEL(w), 0.0, 0.5);
1676 #if GTK_CHECK_VERSION(3,0,0)
1677 gtk_grid_attach(GTK_GRID(table), w, 0, y, 1, 1);
1679 gtk_table_attach(GTK_TABLE(table), w, 0, 1, y, y+1,
1680 GTK_SHRINK | GTK_FILL,
1681 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1686 w = gtk_entry_new();
1687 #if GTK_CHECK_VERSION(3,0,0)
1688 gtk_grid_attach(GTK_GRID(table), w, 1, y, 1, 1);
1689 g_object_set(G_OBJECT(w), "hexpand", TRUE, (const char *)NULL);
1691 gtk_table_attach(GTK_TABLE(table), w, 1, 2, y, y+1,
1692 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1693 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1696 gtk_entry_set_text(GTK_ENTRY(w), i->sval);
1697 g_signal_connect(G_OBJECT(w), "changed",
1698 G_CALLBACK(editbox_changed), i);
1699 g_signal_connect(G_OBJECT(w), "key_press_event",
1700 G_CALLBACK(editbox_key), NULL);
1709 w = gtk_check_button_new_with_label(i->name);
1710 g_signal_connect(G_OBJECT(w), "toggled",
1711 G_CALLBACK(button_toggled), i);
1712 #if GTK_CHECK_VERSION(3,0,0)
1713 gtk_grid_attach(GTK_GRID(table), w, 0, y, 2, 1);
1714 g_object_set(G_OBJECT(w), "hexpand", TRUE, (const char *)NULL);
1716 gtk_table_attach(GTK_TABLE(table), w, 0, 2, y, y+1,
1717 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1718 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1721 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), i->ival);
1727 * Drop-down list (GtkComboBox).
1730 w = gtk_label_new(i->name);
1731 align_label(GTK_LABEL(w), 0.0, 0.5);
1732 #if GTK_CHECK_VERSION(3,0,0)
1733 gtk_grid_attach(GTK_GRID(table), w, 0, y, 1, 1);
1735 gtk_table_attach(GTK_TABLE(table), w, 0, 1, y, y+1,
1736 GTK_SHRINK | GTK_FILL,
1737 GTK_EXPAND | GTK_SHRINK | GTK_FILL ,
1745 GtkListStore *model;
1746 GtkCellRenderer *cr;
1749 model = gtk_list_store_new(1, G_TYPE_STRING);
1756 while (*q && *q != c)
1759 name = snewn(q-p+1, char);
1760 strncpy(name, p, q-p);
1763 if (*q) q++; /* eat delimiter */
1765 gtk_list_store_append(model, &iter);
1766 gtk_list_store_set(model, &iter, 0, name, -1);
1771 w = gtk_combo_box_new_with_model(GTK_TREE_MODEL(model));
1773 gtk_combo_box_set_active(GTK_COMBO_BOX(w), i->ival);
1775 cr = gtk_cell_renderer_text_new();
1776 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(w), cr, TRUE);
1777 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(w), cr,
1780 g_signal_connect(G_OBJECT(w), "changed",
1781 G_CALLBACK(droplist_sel), i);
1784 #if GTK_CHECK_VERSION(3,0,0)
1785 gtk_grid_attach(GTK_GRID(table), w, 1, y, 1, 1);
1786 g_object_set(G_OBJECT(w), "hexpand", TRUE, (const char *)NULL);
1788 gtk_table_attach(GTK_TABLE(table), w, 1, 2, y, y+1,
1789 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1790 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
1800 g_signal_connect(G_OBJECT(fe->cfgbox), "destroy",
1801 G_CALLBACK(window_destroy), NULL);
1802 g_signal_connect(G_OBJECT(fe->cfgbox), "key_press_event",
1803 G_CALLBACK(win_key_press), cancel);
1804 gtk_window_set_modal(GTK_WINDOW(fe->cfgbox), TRUE);
1805 gtk_window_set_transient_for(GTK_WINDOW(fe->cfgbox),
1806 GTK_WINDOW(fe->window));
1807 /* set_transient_window_pos(fe->window, fe->cfgbox); */
1808 gtk_widget_show(fe->cfgbox);
1816 static void menu_key_event(GtkMenuItem *menuitem, gpointer data)
1818 frontend *fe = (frontend *)data;
1819 int key = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem),
1821 if (!midend_process_key(fe->me, 0, 0, key))
1822 gtk_widget_destroy(fe->window);
1825 static void get_size(frontend *fe, int *px, int *py)
1830 * Currently I don't want to make the GTK port scale large
1831 * puzzles to fit on the screen. This is because X does permit
1832 * extremely large windows and many window managers provide a
1833 * means of navigating round them, and the users I consulted
1834 * before deciding said that they'd rather have enormous puzzle
1835 * windows spanning multiple screen pages than have them
1836 * shrunk. I could change my mind later or introduce
1837 * configurability; this would be the place to do so, by
1838 * replacing the initial values of x and y with the screen
1843 midend_size(fe->me, &x, &y, FALSE);
1848 #if !GTK_CHECK_VERSION(2,0,0)
1849 #define gtk_window_resize(win, x, y) \
1850 gdk_window_resize(GTK_WIDGET(win)->window, x, y)
1854 * Called when any other code in this file has changed the
1855 * selected game parameters.
1857 static void changed_preset(frontend *fe)
1859 int n = midend_which_preset(fe->me);
1861 fe->preset_threaded = TRUE;
1862 if (n < 0 && fe->preset_custom) {
1863 gtk_check_menu_item_set_active(
1864 GTK_CHECK_MENU_ITEM(fe->preset_custom),
1867 GSList *gs = fe->preset_radio;
1868 int i = fe->n_preset_menu_items - 1 - n;
1869 if (fe->preset_custom)
1876 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(gs->data),
1878 } else for (gs = fe->preset_radio; gs; gs = gs->next) {
1879 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(gs->data),
1883 fe->preset_threaded = FALSE;
1886 * Update the greying on the Copy menu option.
1888 if (fe->copy_menu_item) {
1889 int enabled = midend_can_format_as_text_now(fe->me);
1890 gtk_widget_set_sensitive(fe->copy_menu_item, enabled);
1894 #if !GTK_CHECK_VERSION(3,0,0)
1895 static gboolean not_size_allocated_yet(GtkWidget *w)
1898 * This function tests whether a widget has not yet taken up space
1899 * on the screen which it will occupy in future. (Therefore, it
1900 * returns true only if the widget does exist but does not have a
1901 * size allocation. A null widget is already taking up all the
1902 * space it ever will.)
1905 return FALSE; /* nonexistent widgets aren't a problem */
1907 #if GTK_CHECK_VERSION(2,18,0) /* skip if no gtk_widget_get_allocation */
1910 gtk_widget_get_allocation(w, &a);
1911 if (a.height == 0 || a.width == 0)
1912 return TRUE; /* widget exists but has no size yet */
1919 static void try_shrink_drawing_area(frontend *fe)
1921 if (fe->drawing_area_shrink_pending &&
1922 (!fe->menubar_is_local || !not_size_allocated_yet(fe->menubar)) &&
1923 !not_size_allocated_yet(fe->statusbar)) {
1925 * In order to permit the user to resize the window smaller as
1926 * well as bigger, we call this function after the window size
1927 * has ended up where we want it. This shouldn't shrink the
1928 * window immediately; it just arranges that the next time the
1929 * user tries to shrink it, they can.
1931 * However, at puzzle creation time, we defer the first of
1932 * these operations until after the menu bar and status bar
1933 * are actually visible. On Ubuntu 12.04 I've found that these
1934 * can take a while to be displayed, and that it's a mistake
1935 * to reduce the drawing area's size allocation before they've
1936 * turned up or else the drawing area makes room for them by
1937 * shrinking to less than the size we intended.
1939 gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), 1, 1);
1940 fe->drawing_area_shrink_pending = FALSE;
1943 #endif /* !GTK_CHECK_VERSION(3,0,0) */
1945 static gint configure_window(GtkWidget *widget,
1946 GdkEventConfigure *event, gpointer data)
1948 #if !GTK_CHECK_VERSION(3,0,0)
1950 * When the main puzzle window changes size, it might be because
1951 * the menu bar or status bar has turned up after starting off
1952 * absent, in which case we should have another go at enacting a
1953 * pending shrink of the drawing area.
1955 frontend *fe = (frontend *)data;
1956 try_shrink_drawing_area(fe);
1961 #if GTK_CHECK_VERSION(3,0,0)
1962 static int window_extra_height(frontend *fe)
1967 gtk_widget_get_preferred_size(fe->menubar, &req, NULL);
1970 if (fe->statusbar) {
1972 gtk_widget_get_preferred_size(fe->statusbar, &req, NULL);
1979 static void resize_fe(frontend *fe)
1983 get_size(fe, &x, &y);
1985 #if GTK_CHECK_VERSION(3,0,0)
1986 gtk_window_resize(GTK_WINDOW(fe->window), x, y + window_extra_height(fe));
1988 fe->drawing_area_shrink_pending = FALSE;
1989 gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
1992 gtk_widget_size_request(GTK_WIDGET(fe->window), &req);
1993 gtk_window_resize(GTK_WINDOW(fe->window), req.width, req.height);
1995 fe->drawing_area_shrink_pending = TRUE;
1996 try_shrink_drawing_area(fe);
2000 static void menu_preset_event(GtkMenuItem *menuitem, gpointer data)
2002 frontend *fe = (frontend *)data;
2003 game_params *params =
2004 (game_params *)g_object_get_data(G_OBJECT(menuitem), "user-data");
2006 if (fe->preset_threaded ||
2007 (GTK_IS_CHECK_MENU_ITEM(menuitem) &&
2008 !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem))))
2010 midend_set_params(fe->me, params);
2011 midend_new_game(fe->me);
2014 midend_redraw(fe->me);
2017 GdkAtom compound_text_atom, utf8_string_atom;
2018 int paste_initialised = FALSE;
2020 static void set_selection(frontend *fe, GdkAtom selection)
2022 if (!paste_initialised) {
2023 compound_text_atom = gdk_atom_intern("COMPOUND_TEXT", FALSE);
2024 utf8_string_atom = gdk_atom_intern("UTF8_STRING", FALSE);
2025 paste_initialised = TRUE;
2029 * For this simple application we can safely assume that the
2030 * data passed to this function is pure ASCII, which means we
2031 * can return precisely the same stuff for types STRING,
2032 * COMPOUND_TEXT or UTF8_STRING.
2035 if (gtk_selection_owner_set(fe->area, selection, CurrentTime)) {
2036 gtk_selection_clear_targets(fe->area, selection);
2037 gtk_selection_add_target(fe->area, selection,
2038 GDK_SELECTION_TYPE_STRING, 1);
2039 gtk_selection_add_target(fe->area, selection, compound_text_atom, 1);
2040 gtk_selection_add_target(fe->area, selection, utf8_string_atom, 1);
2044 void write_clip(frontend *fe, char *data)
2047 sfree(fe->paste_data);
2049 fe->paste_data = data;
2050 fe->paste_data_len = strlen(data);
2052 set_selection(fe, GDK_SELECTION_PRIMARY);
2053 set_selection(fe, GDK_SELECTION_CLIPBOARD);
2056 void selection_get(GtkWidget *widget, GtkSelectionData *seldata,
2057 guint info, guint time_stamp, gpointer data)
2059 frontend *fe = (frontend *)data;
2060 gtk_selection_data_set(seldata, gtk_selection_data_get_target(seldata), 8,
2061 fe->paste_data, fe->paste_data_len);
2064 gint selection_clear(GtkWidget *widget, GdkEventSelection *seldata,
2067 frontend *fe = (frontend *)data;
2070 sfree(fe->paste_data);
2071 fe->paste_data = NULL;
2072 fe->paste_data_len = 0;
2076 static void menu_copy_event(GtkMenuItem *menuitem, gpointer data)
2078 frontend *fe = (frontend *)data;
2081 text = midend_text_format(fe->me);
2084 write_clip(fe, text);
2092 static void filesel_ok(GtkButton *button, gpointer data)
2094 frontend *fe = (frontend *)data;
2096 gpointer filesel = g_object_get_data(G_OBJECT(button), "user-data");
2099 gtk_file_selection_get_filename(GTK_FILE_SELECTION(filesel));
2101 fe->filesel_name = dupstr(name);
2104 static char *file_selector(frontend *fe, char *title, int save)
2106 GtkWidget *filesel =
2107 gtk_file_selection_new(title);
2109 fe->filesel_name = NULL;
2111 gtk_window_set_modal(GTK_WINDOW(filesel), TRUE);
2113 (G_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "user-data",
2116 (G_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "clicked",
2117 G_CALLBACK(filesel_ok), fe);
2118 g_signal_connect_swapped
2119 (G_OBJECT(GTK_FILE_SELECTION(filesel)->ok_button), "clicked",
2120 G_CALLBACK(gtk_widget_destroy), (gpointer)filesel);
2121 g_signal_connect_object
2122 (G_OBJECT(GTK_FILE_SELECTION(filesel)->cancel_button), "clicked",
2123 G_CALLBACK(gtk_widget_destroy), (gpointer)filesel);
2124 g_signal_connect(G_OBJECT(filesel), "destroy",
2125 G_CALLBACK(window_destroy), NULL);
2126 gtk_widget_show(filesel);
2127 gtk_window_set_transient_for(GTK_WINDOW(filesel), GTK_WINDOW(fe->window));
2130 return fe->filesel_name;
2135 static char *file_selector(frontend *fe, char *title, int save)
2137 char *filesel_name = NULL;
2139 GtkWidget *filesel =
2140 gtk_file_chooser_dialog_new(title,
2141 GTK_WINDOW(fe->window),
2142 save ? GTK_FILE_CHOOSER_ACTION_SAVE :
2143 GTK_FILE_CHOOSER_ACTION_OPEN,
2144 LABEL_CANCEL, GTK_RESPONSE_CANCEL,
2145 save ? LABEL_SAVE : LABEL_OPEN,
2146 GTK_RESPONSE_ACCEPT,
2149 if (gtk_dialog_run(GTK_DIALOG(filesel)) == GTK_RESPONSE_ACCEPT) {
2150 char *name = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filesel));
2151 filesel_name = dupstr(name);
2155 gtk_widget_destroy(filesel);
2157 return filesel_name;
2162 struct savefile_write_ctx {
2167 static void savefile_write(void *wctx, void *buf, int len)
2169 struct savefile_write_ctx *ctx = (struct savefile_write_ctx *)wctx;
2170 if (fwrite(buf, 1, len, ctx->fp) < len)
2174 static int savefile_read(void *wctx, void *buf, int len)
2176 FILE *fp = (FILE *)wctx;
2179 ret = fread(buf, 1, len, fp);
2180 return (ret == len);
2183 static void menu_save_event(GtkMenuItem *menuitem, gpointer data)
2185 frontend *fe = (frontend *)data;
2188 name = file_selector(fe, "Enter name of game file to save", TRUE);
2193 if ((fp = fopen(name, "r")) != NULL) {
2194 char buf[256 + FILENAME_MAX];
2198 sprintf(buf, "Are you sure you want to overwrite the"
2200 FILENAME_MAX, name);
2201 if (!message_box(fe->window, "Question", buf, TRUE, MB_YESNO))
2202 goto free_and_return;
2205 fp = fopen(name, "w");
2208 error_box(fe->window, "Unable to open save file");
2209 goto free_and_return;
2213 struct savefile_write_ctx ctx;
2216 midend_serialise(fe->me, savefile_write, &ctx);
2220 sprintf(boxmsg, "Error writing save file: %.400s",
2222 error_box(fe->window, boxmsg);
2223 goto free_and_return;
2231 static void menu_load_event(GtkMenuItem *menuitem, gpointer data)
2233 frontend *fe = (frontend *)data;
2236 name = file_selector(fe, "Enter name of saved game file to load", FALSE);
2239 FILE *fp = fopen(name, "r");
2243 error_box(fe->window, "Unable to open saved game file");
2247 err = midend_deserialise(fe->me, savefile_read, fp);
2252 error_box(fe->window, err);
2258 midend_redraw(fe->me);
2262 static void menu_solve_event(GtkMenuItem *menuitem, gpointer data)
2264 frontend *fe = (frontend *)data;
2267 msg = midend_solve(fe->me);
2270 error_box(fe->window, msg);
2273 static void menu_restart_event(GtkMenuItem *menuitem, gpointer data)
2275 frontend *fe = (frontend *)data;
2277 midend_restart_game(fe->me);
2280 static void menu_config_event(GtkMenuItem *menuitem, gpointer data)
2282 frontend *fe = (frontend *)data;
2283 int which = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem),
2286 if (fe->preset_threaded ||
2287 (GTK_IS_CHECK_MENU_ITEM(menuitem) &&
2288 !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem))))
2290 changed_preset(fe); /* Put the old preset back! */
2291 if (!get_config(fe, which))
2294 midend_new_game(fe->me);
2296 midend_redraw(fe->me);
2299 static void menu_about_event(GtkMenuItem *menuitem, gpointer data)
2301 frontend *fe = (frontend *)data;
2303 #if GTK_CHECK_VERSION(3,0,0)
2304 extern char *const *const xpm_icons[];
2305 extern const int n_xpm_icons;
2306 GdkPixbuf *icon = gdk_pixbuf_new_from_xpm_data
2307 ((const gchar **)xpm_icons[n_xpm_icons-1]);
2308 gtk_show_about_dialog
2309 (GTK_WINDOW(fe->window),
2310 "program-name", thegame.name,
2312 "comments", "Part of Simon Tatham's Portable Puzzle Collection",
2314 (const gchar *)NULL);
2315 g_object_unref(G_OBJECT(icon));
2320 sprintf(titlebuf, "About %.200s", thegame.name);
2323 "from Simon Tatham's Portable Puzzle Collection\n\n"
2324 "%.500s", thegame.name, ver);
2326 message_box(fe->window, titlebuf, textbuf, TRUE, MB_OK);
2330 static GtkWidget *add_menu_item_with_key(frontend *fe, GtkContainer *cont,
2331 char *text, int key)
2333 GtkWidget *menuitem = gtk_menu_item_new_with_label(text);
2335 gtk_container_add(cont, menuitem);
2336 g_object_set_data(G_OBJECT(menuitem), "user-data", GINT_TO_POINTER(key));
2337 g_signal_connect(G_OBJECT(menuitem), "activate",
2338 G_CALLBACK(menu_key_event), fe);
2339 switch (key & ~0x1F) {
2342 keyqual = GDK_CONTROL_MASK;
2346 keyqual = GDK_SHIFT_MASK;
2352 gtk_widget_add_accelerator(menuitem,
2353 "activate", fe->accelgroup,
2356 gtk_widget_show(menuitem);
2360 static void add_menu_separator(GtkContainer *cont)
2362 GtkWidget *menuitem = gtk_menu_item_new();
2363 gtk_container_add(cont, menuitem);
2364 gtk_widget_show(menuitem);
2367 enum { ARG_EITHER, ARG_SAVE, ARG_ID }; /* for argtype */
2369 static frontend *new_window(char *arg, int argtype, char **error)
2372 GtkBox *vbox, *hbox;
2373 GtkWidget *menu, *menuitem;
2377 extern char *const *const xpm_icons[];
2378 extern const int n_xpm_icons;
2380 fe = snew(frontend);
2381 #if GTK_CHECK_VERSION(3,20,0)
2382 fe->css_provider = NULL;
2385 fe->timer_active = FALSE;
2388 fe->me = midend_new(fe, &thegame, >k_drawing, fe);
2398 err = midend_game_id(fe->me, arg);
2400 midend_new_game(fe->me);
2402 sprintf(errbuf, "Invalid game ID: %.800s", err);
2405 fp = fopen(arg, "r");
2407 sprintf(errbuf, "Error opening file: %.800s", strerror(errno));
2409 err = midend_deserialise(fe->me, savefile_read, fp);
2411 sprintf(errbuf, "Invalid save file: %.800s", err);
2415 default /*case ARG_EITHER*/:
2417 * First try treating the argument as a game ID.
2419 err = midend_game_id(fe->me, arg);
2422 * It's a valid game ID.
2424 midend_new_game(fe->me);
2426 FILE *fp = fopen(arg, "r");
2428 sprintf(errbuf, "Supplied argument is neither a game ID (%.400s)"
2429 " nor a save file (%.400s)", err, strerror(errno));
2431 err = midend_deserialise(fe->me, savefile_read, fp);
2433 sprintf(errbuf, "%.800s", err);
2440 *error = dupstr(errbuf);
2441 midend_free(fe->me);
2447 midend_new_game(fe->me);
2450 #if !GTK_CHECK_VERSION(3,0,0)
2453 * try_shrink_drawing_area() will do some fiddling with the
2454 * window size request (see comment in that function) after
2455 * all the bits and pieces such as the menu bar and status bar
2456 * have appeared in the puzzle window.
2458 * However, on Unity systems, the menu bar _doesn't_ appear in
2459 * the puzzle window, because the Unity shell hijacks it into
2460 * the menu bar at the very top of the screen. We therefore
2461 * try to detect that situation here, so that we don't sit
2462 * here forever waiting for a menu bar.
2464 const char prop[] = "gtk-shell-shows-menubar";
2465 GtkSettings *settings = gtk_settings_get_default();
2466 if (!g_object_class_find_property(G_OBJECT_GET_CLASS(settings),
2468 fe->menubar_is_local = TRUE;
2471 g_object_get(gtk_settings_get_default(),
2473 (const gchar *)NULL);
2474 fe->menubar_is_local = !unity_mode;
2479 fe->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
2480 gtk_window_set_title(GTK_WINDOW(fe->window), thegame.name);
2482 vbox = GTK_BOX(gtk_vbox_new(FALSE, 0));
2483 gtk_container_add(GTK_CONTAINER(fe->window), GTK_WIDGET(vbox));
2484 gtk_widget_show(GTK_WIDGET(vbox));
2486 fe->accelgroup = gtk_accel_group_new();
2487 gtk_window_add_accel_group(GTK_WINDOW(fe->window), fe->accelgroup);
2489 hbox = GTK_BOX(gtk_hbox_new(FALSE, 0));
2490 gtk_box_pack_start(vbox, GTK_WIDGET(hbox), FALSE, FALSE, 0);
2491 gtk_widget_show(GTK_WIDGET(hbox));
2493 fe->menubar = gtk_menu_bar_new();
2494 gtk_box_pack_start(hbox, fe->menubar, TRUE, TRUE, 0);
2495 gtk_widget_show(fe->menubar);
2497 menuitem = gtk_menu_item_new_with_mnemonic("_Game");
2498 gtk_container_add(GTK_CONTAINER(fe->menubar), menuitem);
2499 gtk_widget_show(menuitem);
2501 menu = gtk_menu_new();
2502 gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
2504 add_menu_item_with_key(fe, GTK_CONTAINER(menu), "New", 'n');
2506 menuitem = gtk_menu_item_new_with_label("Restart");
2507 gtk_container_add(GTK_CONTAINER(menu), menuitem);
2508 g_signal_connect(G_OBJECT(menuitem), "activate",
2509 G_CALLBACK(menu_restart_event), fe);
2510 gtk_widget_show(menuitem);
2512 menuitem = gtk_menu_item_new_with_label("Specific...");
2513 g_object_set_data(G_OBJECT(menuitem), "user-data",
2514 GINT_TO_POINTER(CFG_DESC));
2515 gtk_container_add(GTK_CONTAINER(menu), menuitem);
2516 g_signal_connect(G_OBJECT(menuitem), "activate",
2517 G_CALLBACK(menu_config_event), fe);
2518 gtk_widget_show(menuitem);
2520 menuitem = gtk_menu_item_new_with_label("Random Seed...");
2521 g_object_set_data(G_OBJECT(menuitem), "user-data",
2522 GINT_TO_POINTER(CFG_SEED));
2523 gtk_container_add(GTK_CONTAINER(menu), menuitem);
2524 g_signal_connect(G_OBJECT(menuitem), "activate",
2525 G_CALLBACK(menu_config_event), fe);
2526 gtk_widget_show(menuitem);
2528 fe->preset_radio = NULL;
2529 fe->preset_custom = NULL;
2530 fe->n_preset_menu_items = 0;
2531 fe->preset_threaded = FALSE;
2532 if ((n = midend_num_presets(fe->me)) > 0 || thegame.can_configure) {
2536 menuitem = gtk_menu_item_new_with_mnemonic("_Type");
2537 gtk_container_add(GTK_CONTAINER(fe->menubar), menuitem);
2538 gtk_widget_show(menuitem);
2540 submenu = gtk_menu_new();
2541 gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu);
2543 for (i = 0; i < n; i++) {
2545 game_params *params;
2547 midend_fetch_preset(fe->me, i, &name, ¶ms);
2550 gtk_radio_menu_item_new_with_label(fe->preset_radio, name);
2552 gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
2553 fe->n_preset_menu_items++;
2554 gtk_container_add(GTK_CONTAINER(submenu), menuitem);
2555 g_object_set_data(G_OBJECT(menuitem), "user-data", params);
2556 g_signal_connect(G_OBJECT(menuitem), "activate",
2557 G_CALLBACK(menu_preset_event), fe);
2558 gtk_widget_show(menuitem);
2561 if (thegame.can_configure) {
2562 menuitem = fe->preset_custom =
2563 gtk_radio_menu_item_new_with_label(fe->preset_radio,
2566 gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
2567 gtk_container_add(GTK_CONTAINER(submenu), menuitem);
2568 g_object_set_data(G_OBJECT(menuitem), "user-data",
2569 GINT_TO_POINTER(CFG_SETTINGS));
2570 g_signal_connect(G_OBJECT(menuitem), "activate",
2571 G_CALLBACK(menu_config_event), fe);
2572 gtk_widget_show(menuitem);
2577 add_menu_separator(GTK_CONTAINER(menu));
2578 menuitem = gtk_menu_item_new_with_label("Load...");
2579 gtk_container_add(GTK_CONTAINER(menu), menuitem);
2580 g_signal_connect(G_OBJECT(menuitem), "activate",
2581 G_CALLBACK(menu_load_event), fe);
2582 gtk_widget_show(menuitem);
2583 menuitem = gtk_menu_item_new_with_label("Save...");
2584 gtk_container_add(GTK_CONTAINER(menu), menuitem);
2585 g_signal_connect(G_OBJECT(menuitem), "activate",
2586 G_CALLBACK(menu_save_event), fe);
2587 gtk_widget_show(menuitem);
2588 #ifndef STYLUS_BASED
2589 add_menu_separator(GTK_CONTAINER(menu));
2590 add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Undo", 'u');
2591 add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Redo", 'r');
2593 if (thegame.can_format_as_text_ever) {
2594 add_menu_separator(GTK_CONTAINER(menu));
2595 menuitem = gtk_menu_item_new_with_label("Copy");
2596 gtk_container_add(GTK_CONTAINER(menu), menuitem);
2597 g_signal_connect(G_OBJECT(menuitem), "activate",
2598 G_CALLBACK(menu_copy_event), fe);
2599 gtk_widget_show(menuitem);
2600 fe->copy_menu_item = menuitem;
2602 fe->copy_menu_item = NULL;
2604 if (thegame.can_solve) {
2605 add_menu_separator(GTK_CONTAINER(menu));
2606 menuitem = gtk_menu_item_new_with_label("Solve");
2607 gtk_container_add(GTK_CONTAINER(menu), menuitem);
2608 g_signal_connect(G_OBJECT(menuitem), "activate",
2609 G_CALLBACK(menu_solve_event), fe);
2610 gtk_widget_show(menuitem);
2612 add_menu_separator(GTK_CONTAINER(menu));
2613 add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Exit", 'q');
2615 menuitem = gtk_menu_item_new_with_mnemonic("_Help");
2616 gtk_container_add(GTK_CONTAINER(fe->menubar), menuitem);
2617 gtk_widget_show(menuitem);
2619 menu = gtk_menu_new();
2620 gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
2622 menuitem = gtk_menu_item_new_with_label("About");
2623 gtk_container_add(GTK_CONTAINER(menu), menuitem);
2624 g_signal_connect(G_OBJECT(menuitem), "activate",
2625 G_CALLBACK(menu_about_event), fe);
2626 gtk_widget_show(menuitem);
2629 menuitem=gtk_button_new_with_mnemonic("_Redo");
2630 g_object_set_data(G_OBJECT(menuitem), "user-data",
2631 GINT_TO_POINTER((int)('r')));
2632 g_signal_connect(G_OBJECT(menuitem), "clicked",
2633 G_CALLBACK(menu_key_event), fe);
2634 gtk_box_pack_end(hbox, menuitem, FALSE, FALSE, 0);
2635 gtk_widget_show(menuitem);
2637 menuitem=gtk_button_new_with_mnemonic("_Undo");
2638 g_object_set_data(G_OBJECT(menuitem), "user-data",
2639 GINT_TO_POINTER((int)('u')));
2640 g_signal_connect(G_OBJECT(menuitem), "clicked",
2641 G_CALLBACK(menu_key_event), fe);
2642 gtk_box_pack_end(hbox, menuitem, FALSE, FALSE, 0);
2643 gtk_widget_show(menuitem);
2645 if (thegame.flags & REQUIRE_NUMPAD) {
2646 hbox = GTK_BOX(gtk_hbox_new(FALSE, 0));
2647 gtk_box_pack_start(vbox, GTK_WIDGET(hbox), FALSE, FALSE, 0);
2648 gtk_widget_show(GTK_WIDGET(hbox));
2652 for(errbuf[0]='0';errbuf[0]<='9';errbuf[0]++) {
2653 menuitem=gtk_button_new_with_label(errbuf);
2654 g_object_set_data(G_OBJECT(menuitem), "user-data",
2655 GINT_TO_POINTER((int)(errbuf[0])));
2656 g_signal_connect(G_OBJECT(menuitem), "clicked",
2657 G_CALLBACK(menu_key_event), fe);
2658 gtk_box_pack_start(hbox, menuitem, TRUE, TRUE, 0);
2659 gtk_widget_show(menuitem);
2662 #endif /* STYLUS_BASED */
2666 snaffle_colours(fe);
2668 if (midend_wants_statusbar(fe->me)) {
2669 GtkWidget *viewport;
2672 viewport = gtk_viewport_new(NULL, NULL);
2673 gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
2674 fe->statusbar = gtk_statusbar_new();
2675 gtk_container_add(GTK_CONTAINER(viewport), fe->statusbar);
2676 gtk_widget_show(viewport);
2677 gtk_box_pack_end(vbox, viewport, FALSE, FALSE, 0);
2678 gtk_widget_show(fe->statusbar);
2679 fe->statusctx = gtk_statusbar_get_context_id
2680 (GTK_STATUSBAR(fe->statusbar), "game");
2681 gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx,
2682 DEFAULT_STATUSBAR_TEXT);
2683 #if GTK_CHECK_VERSION(3,0,0)
2684 gtk_widget_get_preferred_size(fe->statusbar, &req, NULL);
2686 gtk_widget_size_request(fe->statusbar, &req);
2688 gtk_widget_set_size_request(viewport, -1, req.height);
2690 fe->statusbar = NULL;
2692 fe->area = gtk_drawing_area_new();
2693 #if GTK_CHECK_VERSION(2,0,0) && !GTK_CHECK_VERSION(3,0,0)
2694 gtk_widget_set_double_buffered(fe->area, FALSE);
2698 geom.base_width = 0;
2699 #if GTK_CHECK_VERSION(3,0,0)
2700 geom.base_height = window_extra_height(fe);
2701 gtk_window_set_geometry_hints(GTK_WINDOW(fe->window), NULL,
2702 &geom, GDK_HINT_BASE_SIZE);
2704 geom.base_height = 0;
2705 gtk_window_set_geometry_hints(GTK_WINDOW(fe->window), fe->area,
2706 &geom, GDK_HINT_BASE_SIZE);
2711 get_size(fe, &x, &y);
2712 #if GTK_CHECK_VERSION(3,0,0)
2713 gtk_window_set_default_size(GTK_WINDOW(fe->window),
2714 x, y + window_extra_height(fe));
2716 fe->drawing_area_shrink_pending = FALSE;
2717 gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
2720 gtk_box_pack_end(vbox, fe->area, TRUE, TRUE, 0);
2722 clear_backing_store(fe);
2724 fe->nfonts = fe->fontsize = 0;
2726 fe->paste_data = NULL;
2727 fe->paste_data_len = 0;
2729 g_signal_connect(G_OBJECT(fe->window), "destroy",
2730 G_CALLBACK(destroy), fe);
2731 g_signal_connect(G_OBJECT(fe->window), "key_press_event",
2732 G_CALLBACK(key_event), fe);
2733 g_signal_connect(G_OBJECT(fe->area), "button_press_event",
2734 G_CALLBACK(button_event), fe);
2735 g_signal_connect(G_OBJECT(fe->area), "button_release_event",
2736 G_CALLBACK(button_event), fe);
2737 g_signal_connect(G_OBJECT(fe->area), "motion_notify_event",
2738 G_CALLBACK(motion_event), fe);
2739 g_signal_connect(G_OBJECT(fe->area), "selection_get",
2740 G_CALLBACK(selection_get), fe);
2741 g_signal_connect(G_OBJECT(fe->area), "selection_clear_event",
2742 G_CALLBACK(selection_clear), fe);
2743 #if GTK_CHECK_VERSION(3,0,0)
2744 g_signal_connect(G_OBJECT(fe->area), "draw",
2745 G_CALLBACK(draw_area), fe);
2747 g_signal_connect(G_OBJECT(fe->area), "expose_event",
2748 G_CALLBACK(expose_area), fe);
2750 g_signal_connect(G_OBJECT(fe->window), "map_event",
2751 G_CALLBACK(map_window), fe);
2752 g_signal_connect(G_OBJECT(fe->area), "configure_event",
2753 G_CALLBACK(configure_area), fe);
2754 g_signal_connect(G_OBJECT(fe->window), "configure_event",
2755 G_CALLBACK(configure_window), fe);
2757 gtk_widget_add_events(GTK_WIDGET(fe->area),
2758 GDK_BUTTON_PRESS_MASK |
2759 GDK_BUTTON_RELEASE_MASK |
2760 GDK_BUTTON_MOTION_MASK |
2761 GDK_POINTER_MOTION_HINT_MASK);
2764 gtk_window_set_icon(GTK_WINDOW(fe->window),
2765 gdk_pixbuf_new_from_xpm_data
2766 ((const gchar **)xpm_icons[0]));
2769 for (n = 0; n < n_xpm_icons; n++) {
2771 g_list_append(iconlist,
2772 gdk_pixbuf_new_from_xpm_data((const gchar **)
2775 gtk_window_set_icon_list(GTK_WINDOW(fe->window), iconlist);
2778 gtk_widget_show(fe->area);
2779 gtk_widget_show(fe->window);
2781 #if !GTK_CHECK_VERSION(3,0,0)
2782 fe->drawing_area_shrink_pending = TRUE;
2783 try_shrink_drawing_area(fe);
2786 set_window_background(fe, 0);
2791 char *fgetline(FILE *fp)
2793 char *ret = snewn(512, char);
2794 int size = 512, len = 0;
2795 while (fgets(ret + len, size - len, fp)) {
2796 len += strlen(ret + len);
2797 if (ret[len-1] == '\n')
2798 break; /* got a newline, we're done */
2800 ret = sresize(ret, size, char);
2802 if (len == 0) { /* first fgets returned NULL */
2810 int main(int argc, char **argv)
2812 char *pname = argv[0];
2814 int ngenerate = 0, print = FALSE, px = 1, py = 1;
2815 int time_generation = FALSE, test_solve = FALSE, list_presets = FALSE;
2816 int soln = FALSE, colour = FALSE;
2818 float redo_proportion = 0.0F;
2819 char *savefile = NULL, *savesuffix = NULL;
2821 int argtype = ARG_EITHER;
2822 char *screenshot_file = NULL;
2823 int doing_opts = TRUE;
2829 * Command line parsing in this function is rather fiddly,
2830 * because GTK wants to have a go at argc/argv _first_ - and
2831 * yet we can't let it, because gtk_init() will bomb out if it
2832 * can't open an X display, whereas in fact we want to permit
2833 * our --generate and --print modes to run without an X
2837 * - we parse the command line ourselves, without modifying
2839 * - if we encounter an error which might plausibly be the
2840 * result of a GTK command line (i.e. not detailed errors in
2841 * particular options of ours) we store the error message
2842 * and terminate parsing.
2843 * - if we got enough out of the command line to know it
2844 * specifies a non-X mode of operation, we either display
2845 * the stored error and return failure, or if there is no
2846 * stored error we do the non-X operation and return
2848 * - otherwise, we go straight to gtk_init().
2854 if (doing_opts && !strcmp(p, "--version")) {
2855 printf("%s, from Simon Tatham's Portable Puzzle Collection\n%s\n",
2858 } else if (doing_opts && !strcmp(p, "--generate")) {
2860 ngenerate = atoi(*++av);
2862 fprintf(stderr, "%s: '--generate' expected a number\n",
2868 } else if (doing_opts && !strcmp(p, "--time-generation")) {
2869 time_generation = TRUE;
2870 } else if (doing_opts && !strcmp(p, "--test-solve")) {
2872 } else if (doing_opts && !strcmp(p, "--list-presets")) {
2873 list_presets = TRUE;
2874 } else if (doing_opts && !strcmp(p, "--save")) {
2878 fprintf(stderr, "%s: '--save' expected a filename\n",
2882 } else if (doing_opts && (!strcmp(p, "--save-suffix") ||
2883 !strcmp(p, "--savesuffix"))) {
2887 fprintf(stderr, "%s: '--save-suffix' expected a filename\n",
2891 } else if (doing_opts && !strcmp(p, "--print")) {
2892 if (!thegame.can_print) {
2893 fprintf(stderr, "%s: this game does not support printing\n",
2900 if (sscanf(dim, "%dx%d", &px, &py) != 2) {
2901 fprintf(stderr, "%s: unable to parse argument '%s' to "
2902 "'--print'\n", pname, dim);
2908 } else if (doing_opts && !strcmp(p, "--scale")) {
2910 scale = atof(*++av);
2912 fprintf(stderr, "%s: no argument supplied to '--scale'\n",
2916 } else if (doing_opts && !strcmp(p, "--redo")) {
2918 * This is an internal option which I don't expect
2919 * users to have any particular use for. The effect of
2920 * --redo is that once the game has been loaded and
2921 * initialised, the next move in the redo chain is
2922 * replayed, and the game screen is redrawn part way
2923 * through the making of the move. This is only
2924 * meaningful if there _is_ a next move in the redo
2925 * chain, which means in turn that this option is only
2926 * useful if you're also passing a save file on the
2929 * This option is used by the script which generates
2930 * the puzzle icons and website screenshots, and I
2931 * don't imagine it's useful for anything else.
2932 * (Unless, I suppose, users don't like my screenshots
2933 * and want to generate their own in the same way for
2934 * some repackaged version of the puzzles.)
2937 redo_proportion = atof(*++av);
2939 fprintf(stderr, "%s: no argument supplied to '--redo'\n",
2943 } else if (doing_opts && !strcmp(p, "--screenshot")) {
2945 * Another internal option for the icon building
2946 * script. This causes a screenshot of the central
2947 * drawing area (i.e. not including the menu bar or
2948 * status bar) to be saved to a PNG file once the
2949 * window has been drawn, and then the application
2950 * quits immediately.
2953 screenshot_file = *++av;
2955 fprintf(stderr, "%s: no argument supplied to '--screenshot'\n",
2959 } else if (doing_opts && (!strcmp(p, "--with-solutions") ||
2960 !strcmp(p, "--with-solution") ||
2961 !strcmp(p, "--with-solns") ||
2962 !strcmp(p, "--with-soln") ||
2963 !strcmp(p, "--solutions") ||
2964 !strcmp(p, "--solution") ||
2965 !strcmp(p, "--solns") ||
2966 !strcmp(p, "--soln"))) {
2968 } else if (doing_opts && !strcmp(p, "--colour")) {
2969 if (!thegame.can_print_in_colour) {
2970 fprintf(stderr, "%s: this game does not support colour"
2971 " printing\n", pname);
2975 } else if (doing_opts && !strcmp(p, "--load")) {
2977 } else if (doing_opts && !strcmp(p, "--game")) {
2979 } else if (doing_opts && !strcmp(p, "--")) {
2981 } else if (!doing_opts || p[0] != '-') {
2983 fprintf(stderr, "%s: more than one argument supplied\n",
2989 sprintf(errbuf, "%.100s: unrecognised option '%.100s'\n",
2996 * Special standalone mode for generating puzzle IDs on the
2997 * command line. Useful for generating puzzles to be printed
2998 * out and solved offline (for puzzles where that even makes
2999 * sense - Solo, for example, is a lot more pencil-and-paper
3000 * friendly than Twiddle!)
3004 * <puzzle-name> --generate [<n> [<params>]]
3006 * <n>, if present, is the number of puzzle IDs to generate.
3007 * <params>, if present, is the same type of parameter string
3008 * you would pass to the puzzle when running it in GUI mode,
3009 * including optional extras such as the expansion factor in
3010 * Rectangles and the difficulty level in Solo.
3012 * If you specify <params>, you must also specify <n> (although
3013 * you may specify it to be 1). Sorry; that was the
3014 * simplest-to-parse command-line syntax I came up with.
3016 if (ngenerate > 0 || print || savefile || savesuffix) {
3020 document *doc = NULL;
3023 * If we're in this branch, we should display any pending
3024 * error message from the command line, since GTK isn't going
3025 * to take another crack at making sense of it.
3028 fputs(errbuf, stderr);
3034 me = midend_new(NULL, &thegame, NULL, NULL);
3037 if (savefile && !savesuffix)
3039 if (!savefile && savesuffix)
3043 doc = document_new(px, py, scale);
3046 * In this loop, we either generate a game ID or read one
3047 * from stdin depending on whether we're in generate mode;
3048 * then we either write it to stdout or print it, depending
3049 * on whether we're in print mode. Thus, this loop handles
3050 * generate-to-stdout, print-from-stdin and generate-and-
3051 * immediately-print modes.
3053 * (It could also handle a copy-stdin-to-stdout mode,
3054 * although there's currently no combination of options
3055 * which will cause this loop to be activated in that mode.
3056 * It wouldn't be _entirely_ pointless, though, because
3057 * stdin could contain bare params strings or random-seed
3058 * IDs, and stdout would contain nothing but fully
3059 * generated descriptive game IDs.)
3061 while (ngenerate == 0 || i < n) {
3062 char *pstr, *err, *seed;
3063 struct rusage before, after;
3065 if (ngenerate == 0) {
3066 pstr = fgetline(stdin);
3069 pstr[strcspn(pstr, "\r\n")] = '\0';
3072 pstr = snewn(strlen(arg) + 40, char);
3075 if (i > 0 && strchr(arg, '#'))
3076 sprintf(pstr + strlen(pstr), "-%d", i);
3082 err = midend_game_id(me, pstr);
3084 fprintf(stderr, "%s: error parsing '%s': %s\n",
3090 if (time_generation)
3091 getrusage(RUSAGE_SELF, &before);
3093 midend_new_game(me);
3095 seed = midend_get_random_seed(me);
3097 if (time_generation) {
3100 getrusage(RUSAGE_SELF, &after);
3102 elapsed = (after.ru_utime.tv_sec -
3103 before.ru_utime.tv_sec);
3104 elapsed += (after.ru_utime.tv_usec -
3105 before.ru_utime.tv_usec) / 1000000.0;
3107 printf("%s %s: %.6f\n", thegame.name, seed, elapsed);
3110 if (test_solve && thegame.can_solve) {
3112 * Now destroy the aux_info in the midend, by means of
3113 * re-entering the same game id, and then try to solve
3116 char *game_id, *err;
3118 game_id = midend_get_game_id(me);
3119 err = midend_game_id(me, game_id);
3121 fprintf(stderr, "%s %s: game id re-entry error: %s\n",
3122 thegame.name, seed, err);
3125 midend_new_game(me);
3128 err = midend_solve(me);
3130 * If the solve operation returned the error "Solution
3131 * not known for this puzzle", that's OK, because that
3132 * just means it's a puzzle for which we don't have an
3133 * algorithmic solver and hence can't solve it without
3134 * the aux_info, e.g. Netslide. Any other error is a
3137 if (err && strcmp(err, "Solution not known for this puzzle")) {
3138 fprintf(stderr, "%s %s: solve error: %s\n",
3139 thegame.name, seed, err);
3148 err = midend_print_puzzle(me, doc, soln);
3150 fprintf(stderr, "%s: error in printing: %s\n", pname, err);
3155 struct savefile_write_ctx ctx;
3156 char *realname = snewn(40 + strlen(savefile) +
3157 strlen(savesuffix), char);
3158 sprintf(realname, "%s%d%s", savefile, i, savesuffix);
3161 char *err = midend_solve(me);
3163 fprintf(stderr, "%s: unable to show solution: %s\n",
3169 ctx.fp = fopen(realname, "w");
3171 fprintf(stderr, "%s: open: %s\n", realname,
3176 midend_serialise(me, savefile_write, &ctx);
3178 fprintf(stderr, "%s: write: %s\n", realname,
3179 strerror(ctx.error));
3182 if (fclose(ctx.fp)) {
3183 fprintf(stderr, "%s: close: %s\n", realname,
3189 if (!doc && !savefile && !time_generation) {
3190 id = midend_get_game_id(me);
3199 psdata *ps = ps_init(stdout, colour);
3200 document_print(doc, ps_drawing_api(ps));
3208 } else if (list_presets) {
3210 * Another specialist mode which causes the puzzle to list the
3211 * game_params strings for all its preset configurations.
3216 me = midend_new(NULL, &thegame, NULL, NULL);
3217 npresets = midend_num_presets(me);
3219 for (i = 0; i < npresets; i++) {
3220 game_params *params;
3221 char *name, *paramstr;
3223 midend_fetch_preset(me, i, &name, ¶ms);
3224 paramstr = thegame.encode_params(params, TRUE);
3226 printf("%s %s\n", paramstr, name);
3235 gtk_init(&argc, &argv);
3237 fe = new_window(arg, argtype, &error);
3240 fprintf(stderr, "%s: %s\n", pname, error);
3244 if (screenshot_file) {
3246 * Some puzzles will not redraw their entire area if
3247 * given a partially completed animation, which means
3248 * we must redraw now and _then_ redraw again after
3249 * freezing the move timer.
3251 midend_force_redraw(fe->me);
3254 if (redo_proportion) {
3256 midend_process_key(fe->me, 0, 0, 'r');
3257 /* And freeze the timer at the specified position. */
3258 midend_freeze_timer(fe->me, redo_proportion);
3261 if (screenshot_file) {
3262 save_screenshot_png(fe, screenshot_file);