/*
* gtk.c: GTK front end for my puzzle collection.
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE 1 /* for strcasestr */
#endif
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <time.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#ifdef NO_TGMATH_H
# include <math.h>
#else
# include <tgmath.h>
#endif
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gdk/gdkx.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include "puzzles.h"
#include "gtk.h"
#if GTK_CHECK_VERSION(2,0,0)
# define USE_PANGO
# ifdef PANGO_VERSION_CHECK
# if PANGO_VERSION_CHECK(1,8,0)
# define HAVE_SENSIBLE_ABSOLUTE_SIZE_FUNCTION
# endif
# endif
#endif
#if !GTK_CHECK_VERSION(2,4,0)
# define OLD_FILESEL
#endif
#if GTK_CHECK_VERSION(2,8,0)
# define USE_CAIRO
# if GTK_CHECK_VERSION(3,0,0) || defined(GDK_DISABLE_DEPRECATED)
# define USE_CAIRO_WITHOUT_PIXMAP
# endif
#endif
#if defined USE_CAIRO && GTK_CHECK_VERSION(2,10,0)
/* We can only use printing if we are using Cairo for drawing and we
have a GTK version >= 2.10 (when GtkPrintOperation was added). */
# define USE_PRINTING
# if GTK_CHECK_VERSION(2,18,0)
/* We can embed the page setup. Before 2.18, we needed to have a
separate page setup. */
# define USE_EMBED_PAGE_SETUP
# endif
#endif
#if GTK_CHECK_VERSION(3,0,0)
/* The old names are still more concise! */
#define gtk_hbox_new(x,y) gtk_box_new(GTK_ORIENTATION_HORIZONTAL,y)
#define gtk_vbox_new(x,y) gtk_box_new(GTK_ORIENTATION_VERTICAL,y)
/* GTK 3 has retired stock button labels */
#define LABEL_OK "_OK"
#define LABEL_CANCEL "_Cancel"
#define LABEL_NO "_No"
#define LABEL_YES "_Yes"
#define LABEL_SAVE "_Save"
#define LABEL_OPEN "_Open"
#define gtk_button_new_with_our_label gtk_button_new_with_mnemonic
#else
#define LABEL_OK GTK_STOCK_OK
#define LABEL_CANCEL GTK_STOCK_CANCEL
#define LABEL_NO GTK_STOCK_NO
#define LABEL_YES GTK_STOCK_YES
#define LABEL_SAVE GTK_STOCK_SAVE
#define LABEL_OPEN GTK_STOCK_OPEN
#define gtk_button_new_with_our_label gtk_button_new_from_stock
#endif
/* #undef USE_CAIRO */
/* #define NO_THICK_LINE */
#ifdef DEBUGGING
static FILE *debug_fp = NULL;
static void dputs(const char *buf)
{
if (!debug_fp) {
debug_fp = fopen("debug.log", "w");
}
fputs(buf, stderr);
if (debug_fp) {
fputs(buf, debug_fp);
fflush(debug_fp);
}
}
void debug_printf(const char *fmt, ...)
{
char buf[4096];
va_list ap;
va_start(ap, fmt);
vsprintf(buf, fmt, ap);
dputs(buf);
va_end(ap);
}
#endif
/* ----------------------------------------------------------------------
* Error reporting functions used elsewhere.
*/
void fatal(const char *fmt, ...)
{
va_list ap;
fprintf(stderr, "fatal error: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
exit(1);
}
/* ----------------------------------------------------------------------
* GTK front end to puzzles.
*/
static void changed_preset(frontend *fe);
static void load_prefs(frontend *fe);
static char *save_prefs(frontend *fe);
struct font {
#ifdef USE_PANGO
PangoFontDescription *desc;
#else
GdkFont *font;
#endif
int type;
int size;
};
/*
* An internal API for functions which need to be different for
* printing and drawing.
*/
struct internal_drawing_api {
void (*set_colour)(frontend *fe, int colour);
#ifdef USE_CAIRO
void (*fill)(frontend *fe);
void (*fill_preserve)(frontend *fe);
#endif
};
/*
* This structure holds all the data relevant to a single window.
* In principle this would allow us to open multiple independent
* puzzle windows, although I can't currently see any real point in
* doing so. I'm just coding cleanly because there's no
* particularly good reason not to.
*/
struct frontend {
bool headless; /* true if we're running without GTK, for --screenshot */
GtkWidget *window;
GtkAccelGroup *dummy_accelgroup;
GtkWidget *area;
GtkWidget *statusbar;
GtkWidget *menubar;
#if GTK_CHECK_VERSION(3,20,0)
GtkCssProvider *css_provider;
#endif
guint statusctx;
int w, h;
midend *me;
#ifdef USE_CAIRO
const float *colours;
cairo_t *cr;
cairo_surface_t *image;
#ifndef USE_CAIRO_WITHOUT_PIXMAP
GdkPixmap *pixmap;
#endif
GdkColor background; /* for painting outside puzzle area */
#else
GdkPixmap *pixmap;
GdkGC *gc;
GdkColor *colours;
GdkColormap *colmap;
int backgroundindex; /* which of colours[] is background */
#endif
int ncolours;
int bbox_l, bbox_r, bbox_u, bbox_d;
bool timer_active;
int timer_id;
struct timeval last_time;
struct font *fonts;
int nfonts, fontsize;
config_item *cfg;
int cfg_which;
bool cfgret;
GtkWidget *cfgbox;
void *paste_data;
int paste_data_len;
int pw, ph, ps; /* pixmap size (w, h are area size, s is GDK scale) */
int ox, oy; /* offset of pixmap in drawing area */
#ifdef OLD_FILESEL
char *filesel_name;
#endif
GSList *preset_radio;
bool preset_threaded;
GtkWidget *preset_custom;
GtkWidget *copy_menu_item;
#if !GTK_CHECK_VERSION(3,0,0)
bool drawing_area_shrink_pending;
bool menubar_is_local;
#endif
#if GTK_CHECK_VERSION(3,0,0)
/*
* This is used to get round an annoying lack of GTK notification
* message. If we request a window resize with
* gtk_window_resize(), we normally get back a "configure" event
* on the window and on its drawing area, and we respond to the
* latter by doing an appropriate resize of the puzzle. If the
* window is maximised, so that gtk_window_resize() _doesn't_
* change its size, then that configure event never shows up. But
* if we requested the resize in response to a change of puzzle
* parameters (say, the user selected a differently-sized preset
* from the menu), then we would still like to be _notified_ that
* the window size was staying the same, so that we can respond by
* choosing an appropriate tile size for the new puzzle preset in
* the existing window size.
*
* Fortunately, in GTK 3, we may not get a "configure" event on
* the drawing area in this situation, but we still get a
* "size_allocate" event on the whole window (which, in other
* situations when we _do_ get a "configure" on the area, turns up
* second). So we treat _that_ event as indicating that if the
* "configure" event hasn't already shown up then it's not going
* to arrive.
*
* This flag is where we bookkeep this system. On
* gtk_window_resize we set this flag to true; the area's
* configure handler sets it back to false; then if that doesn't
* happen, the window's size_allocate handler does a fallback
* puzzle resize when it sees this flag still set to true.
*/
bool awaiting_resize_ack;
#endif
#ifdef USE_CAIRO
int printcount, printw, printh;
float printscale;
bool printsolns, printcolour;
int hatch;
float hatchthick, hatchspace;
drawing *print_dr;
document *doc;
#endif
#ifdef USE_PRINTING
GtkPrintOperation *printop;
GtkPrintContext *printcontext;
GtkSpinButton *printcount_spin_button, *printw_spin_button,
*printh_spin_button, *printscale_spin_button;
GtkCheckButton *soln_check_button, *colour_check_button;
#endif
const struct internal_drawing_api *dr_api;
};
struct blitter {
#ifdef USE_CAIRO
cairo_surface_t *image;
#else
GdkPixmap *pixmap;
#endif
int w, h, x, y;
};
void get_random_seed(void **randseed, int *randseedsize)
{
struct timeval *tvp = snew(struct timeval);
gettimeofday(tvp, NULL);
*randseed = (void *)tvp;
*randseedsize = sizeof(struct timeval);
}
void frontend_default_colour(frontend *fe, float *output)
{
#if !GTK_CHECK_VERSION(3,0,0)
if (!fe->headless) {
/*
* If we have a widget and it has a style that specifies a
* default background colour, use that as the background for
* the puzzle drawing area.
*/
GdkColor col = gtk_widget_get_style(fe->window)->bg[GTK_STATE_NORMAL];
output[0] = col.red / 65535.0;
output[1] = col.green / 65535.0;
output[2] = col.blue / 65535.0;
}
#endif
/*
* GTK 3 has decided that there's no such thing as a 'default
* background colour' any more, because widget styles might set
* the background to something more complicated like a background
* image. We don't want to get into overlaying our entire puzzle
* on an arbitrary background image, so we'll just make up a
* reasonable shade of grey.
*
* This is also what we do on GTK 2 in headless mode, where we
* don't have a widget style to query.
*/
output[0] = output[1] = output[2] = 0.9F;
}
static void gtk_status_bar(void *handle, const char *text)
{
frontend *fe = (frontend *)handle;
if (fe->headless)
return;
assert(fe->statusbar);
gtk_statusbar_pop(GTK_STATUSBAR(fe->statusbar), fe->statusctx);
gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx, text);
}
/* ----------------------------------------------------------------------
* Cairo drawing functions.
*/
#ifdef USE_CAIRO
static void setup_drawing(frontend *fe)
{
fe->cr = cairo_create(fe->image);
cairo_scale(fe->cr, fe->ps, fe->ps);
cairo_set_antialias(fe->cr, CAIRO_ANTIALIAS_GRAY);
cairo_set_line_width(fe->cr, 1.0);
cairo_set_line_cap(fe->cr, CAIRO_LINE_CAP_SQUARE);
cairo_set_line_join(fe->cr, CAIRO_LINE_JOIN_ROUND);
}
static void teardown_drawing(frontend *fe)
{
cairo_destroy(fe->cr);
fe->cr = NULL;
#ifndef USE_CAIRO_WITHOUT_PIXMAP
if (!fe->headless) {
cairo_t *cr = gdk_cairo_create(fe->pixmap);
cairo_set_source_surface(cr, fe->image, 0, 0);
cairo_rectangle(cr,
fe->bbox_l - 1,
fe->bbox_u - 1,
fe->bbox_r - fe->bbox_l + 2,
fe->bbox_d - fe->bbox_u + 2);
cairo_fill(cr);
cairo_destroy(cr);
}
#endif
}
static void snaffle_colours(frontend *fe)
{
fe->colours = midend_colours(fe->me, &fe->ncolours);
}
static void draw_set_colour(frontend *fe, int colour)
{
cairo_set_source_rgb(fe->cr,
fe->colours[3*colour + 0],
fe->colours[3*colour + 1],
fe->colours[3*colour + 2]);
}
static void print_set_colour(frontend *fe, int colour)
{
float r, g, b;
print_get_colour(fe->print_dr, colour, fe->printcolour,
&(fe->hatch), &r, &g, &b);
if (fe->hatch < 0)
cairo_set_source_rgb(fe->cr, r, g, b);
}
static void set_window_background(frontend *fe, int colour)
{
#if GTK_CHECK_VERSION(3,0,0)
/* In case the user's chosen theme is dark, we should not override
* the background colour for the whole window as this makes the
* menu and status bars unreadable. This might be visible through
* the gtk-application-prefer-dark-theme flag or else we have to
* work it out from the name. */
gboolean dark_theme = false;
char *theme_name = NULL;
g_object_get(gtk_settings_get_default(),
"gtk-application-prefer-dark-theme", &dark_theme,
"gtk-theme-name", &theme_name,
NULL);
if (theme_name && strcasestr(theme_name, "-dark"))
dark_theme = true;
g_free(theme_name);
#if GTK_CHECK_VERSION(3,20,0)
char css_buf[512];
sprintf(css_buf, ".background { "
"background-color: #%02x%02x%02x; }",
(unsigned)(fe->colours[3*colour + 0] * 255),
(unsigned)(fe->colours[3*colour + 1] * 255),
(unsigned)(fe->colours[3*colour + 2] * 255));
if (!fe->css_provider)
fe->css_provider = gtk_css_provider_new();
if (!gtk_css_provider_load_from_data(
GTK_CSS_PROVIDER(fe->css_provider), css_buf, -1, NULL))
assert(0 && "Couldn't load CSS");
if (!dark_theme) {
gtk_style_context_add_provider(
gtk_widget_get_style_context(fe->window),
GTK_STYLE_PROVIDER(fe->css_provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}
gtk_style_context_add_provider(
gtk_widget_get_style_context(fe->area),
GTK_STYLE_PROVIDER(fe->css_provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
#else // still at least GTK 3.0 but less than 3.20
GdkRGBA rgba;
rgba.red = fe->colours[3*colour + 0];
rgba.green = fe->colours[3*colour + 1];
rgba.blue = fe->colours[3*colour + 2];
rgba.alpha = 1.0;
gdk_window_set_background_rgba(gtk_widget_get_window(fe->area), &rgba);
if (!dark_theme)
gdk_window_set_background_rgba(gtk_widget_get_window(fe->window),
&rgba);
#endif // GTK_CHECK_VERSION(3,20,0)
#else // GTK 2 version comes next
GdkColormap *colmap;
colmap = gdk_colormap_get_system();
fe->background.red = fe->colours[3*colour + 0] * 65535;
fe->background.green = fe->colours[3*colour + 1] * 65535;
fe->background.blue = fe->colours[3*colour + 2] * 65535;
if (!gdk_colormap_alloc_color(colmap, &fe->background, false, false)) {
g_error("couldn't allocate background (#%02x%02x%02x)\n",
fe->background.red >> 8, fe->background.green >> 8,
fe->background.blue >> 8);
}
gdk_window_set_background(gtk_widget_get_window(fe->area),
&fe->background);
gdk_window_set_background(gtk_widget_get_window(fe->window),
&fe->background);
#endif
}
static PangoLayout *make_pango_layout(frontend *fe)
{
return (pango_cairo_create_layout(fe->cr));
}
static void draw_pango_layout(frontend *fe, PangoLayout *layout,
int x, int y)
{
cairo_move_to(fe->cr, x, y);
pango_cairo_show_layout(fe->cr, layout);
}
static void save_screenshot_png(frontend *fe, const char *screenshot_file)
{
cairo_surface_write_to_png(fe->image, screenshot_file);
}
static void do_hatch(frontend *fe)
{
double i, x, y, width, height, maxdim;
/* Get the dimensions of the region to be hatched. */
cairo_path_extents(fe->cr, &x, &y, &width, &height);
maxdim = max(width, height);
cairo_save(fe->cr);
/* Set the line color and width. */
cairo_set_source_rgb(fe->cr, 0, 0, 0);
cairo_set_line_width(fe->cr, fe->hatchthick);
/* Clip to the region. */
cairo_clip(fe->cr);
|