Tweaks and more complete documentation for Same Game.
[sgt/puzzles] / gtk.c
diff --git a/gtk.c b/gtk.c
index 24b2cca..37dcf23 100644 (file)
--- a/gtk.c
+++ b/gtk.c
@@ -76,16 +76,19 @@ struct frontend {
     config_item *cfg;
     int cfg_which, cfgret;
     GtkWidget *cfgbox;
-    char *paste_data;
+    void *paste_data;
     int paste_data_len;
+    char *laststatus;
+    int pw, ph;                        /* pixmap size (w, h are area size) */
+    int ox, oy;                        /* offset of pixmap in drawing area */
 };
 
 void get_random_seed(void **randseed, int *randseedsize)
 {
-    time_t *tp = snew(time_t);
-    time(tp);
-    *randseed = (void *)tp;
-    *randseedsize = sizeof(time_t);
+    struct timeval *tvp = snew(struct timeval);
+    gettimeofday(tvp, NULL);
+    *randseed = (void *)tvp;
+    *randseedsize = sizeof(struct timeval);
 }
 
 void frontend_default_colour(frontend *fe, float *output)
@@ -98,10 +101,19 @@ void frontend_default_colour(frontend *fe, float *output)
 
 void status_bar(frontend *fe, char *text)
 {
+    char *rewritten;
+
     assert(fe->statusbar);
 
-    gtk_statusbar_pop(GTK_STATUSBAR(fe->statusbar), fe->statusctx);
-    gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx, text);
+    rewritten = midend_rewrite_statusbar(fe->me, text);
+    if (!fe->laststatus || strcmp(rewritten, fe->laststatus)) {
+       gtk_statusbar_pop(GTK_STATUSBAR(fe->statusbar), fe->statusctx);
+       gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx, rewritten);
+       sfree(fe->laststatus);
+       fe->laststatus = rewritten;
+    } else {
+       sfree(rewritten);
+    }
 }
 
 void start_draw(frontend *fe)
@@ -301,7 +313,7 @@ void end_draw(frontend *fe)
                        fe->area->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
                        fe->pixmap,
                         fe->bbox_l, fe->bbox_u,
-                        fe->bbox_l, fe->bbox_u,
+                        fe->ox + fe->bbox_l, fe->oy + fe->bbox_u,
                         fe->bbox_r - fe->bbox_l, fe->bbox_d - fe->bbox_u);
     }
 }
@@ -317,24 +329,26 @@ static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
 {
     frontend *fe = (frontend *)data;
     int keyval;
+    int shift = (event->state & GDK_SHIFT_MASK) ? MOD_SHFT : 0;
+    int ctrl = (event->state & GDK_CONTROL_MASK) ? MOD_CTRL : 0;
 
     if (!fe->pixmap)
         return TRUE;
 
     if (event->keyval == GDK_Up)
-        keyval = CURSOR_UP;
+        keyval = shift | ctrl | CURSOR_UP;
     else if (event->keyval == GDK_KP_Up || event->keyval == GDK_KP_8)
        keyval = MOD_NUM_KEYPAD | '8';
     else if (event->keyval == GDK_Down)
-        keyval = CURSOR_DOWN;
+        keyval = shift | ctrl | CURSOR_DOWN;
     else if (event->keyval == GDK_KP_Down || event->keyval == GDK_KP_2)
        keyval = MOD_NUM_KEYPAD | '2';
     else if (event->keyval == GDK_Left)
-        keyval = CURSOR_LEFT;
+        keyval = shift | ctrl | CURSOR_LEFT;
     else if (event->keyval == GDK_KP_Left || event->keyval == GDK_KP_4)
        keyval = MOD_NUM_KEYPAD | '4';
     else if (event->keyval == GDK_Right)
-        keyval = CURSOR_RIGHT;
+        keyval = shift | ctrl | CURSOR_RIGHT;
     else if (event->keyval == GDK_KP_Right || event->keyval == GDK_KP_6)
        keyval = MOD_NUM_KEYPAD | '6';
     else if (event->keyval == GDK_KP_Home || event->keyval == GDK_KP_7)
@@ -375,17 +389,18 @@ static gint button_event(GtkWidget *widget, GdkEventButton *event,
 
     if (event->button == 2 || (event->state & GDK_SHIFT_MASK))
        button = MIDDLE_BUTTON;
+    else if (event->button == 3 || (event->state & GDK_MOD1_MASK))
+       button = RIGHT_BUTTON;
     else if (event->button == 1)
        button = LEFT_BUTTON;
-    else if (event->button == 3)
-       button = RIGHT_BUTTON;
     else
        return FALSE;                  /* don't even know what button! */
 
     if (event->type == GDK_BUTTON_RELEASE)
         button += LEFT_RELEASE - LEFT_BUTTON;
 
-    if (!midend_process_key(fe->me, event->x, event->y, button))
+    if (!midend_process_key(fe->me, event->x - fe->ox,
+                            event->y - fe->oy, button))
        gtk_widget_destroy(fe->window);
 
     return TRUE;
@@ -409,7 +424,8 @@ static gint motion_event(GtkWidget *widget, GdkEventMotion *event,
     else
        return FALSE;                  /* don't even know what button! */
 
-    if (!midend_process_key(fe->me, event->x, event->y, button))
+    if (!midend_process_key(fe->me, event->x - fe->ox,
+                            event->y - fe->oy, button))
        gtk_widget_destroy(fe->window);
 
     return TRUE;
@@ -424,7 +440,7 @@ static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
        gdk_draw_pixmap(widget->window,
                        widget->style->fg_gc[GTK_WIDGET_STATE(widget)],
                        fe->pixmap,
-                       event->area.x, event->area.y,
+                       event->area.x - fe->ox, event->area.y - fe->oy,
                        event->area.x, event->area.y,
                        event->area.width, event->area.height);
     }
@@ -451,18 +467,27 @@ static gint configure_area(GtkWidget *widget,
 {
     frontend *fe = (frontend *)data;
     GdkGC *gc;
+    int x, y;
 
     if (fe->pixmap)
         gdk_pixmap_unref(fe->pixmap);
 
-    fe->pixmap = gdk_pixmap_new(widget->window, fe->w, fe->h, -1);
+    x = fe->w = event->width;
+    y = fe->h = event->height;
+    midend_size(fe->me, &x, &y, TRUE);
+    fe->pw = x;
+    fe->ph = y;
+    fe->ox = (fe->w - fe->pw) / 2;
+    fe->oy = (fe->h - fe->ph) / 2;
+
+    fe->pixmap = gdk_pixmap_new(widget->window, fe->pw, fe->ph, -1);
 
     gc = gdk_gc_new(fe->area->window);
     gdk_gc_set_foreground(gc, &fe->colours[0]);
-    gdk_draw_rectangle(fe->pixmap, gc, 1, 0, 0, fe->w, fe->h);
+    gdk_draw_rectangle(fe->pixmap, gc, 1, 0, 0, fe->pw, fe->ph);
     gdk_gc_unref(gc);
 
-    midend_redraw(fe->me);
+    midend_force_redraw(fe->me);
 
     return TRUE;
 }
@@ -554,7 +579,7 @@ void message_box(GtkWidget *parent, char *title, char *msg, int centre)
                       GTK_SIGNAL_FUNC(win_key_press), ok);
     gtk_window_set_modal(GTK_WINDOW(window), TRUE);
     gtk_window_set_transient_for(GTK_WINDOW(window), GTK_WINDOW(parent));
-    //set_transient_window_pos(parent, window);
+    /* set_transient_window_pos(parent, window); */
     gtk_widget_show(window);
     gtk_main();
 }
@@ -786,7 +811,7 @@ static int get_config(frontend *fe, int which)
     gtk_window_set_modal(GTK_WINDOW(fe->cfgbox), TRUE);
     gtk_window_set_transient_for(GTK_WINDOW(fe->cfgbox),
                                 GTK_WINDOW(fe->window));
-    //set_transient_window_pos(fe->window, fe->cfgbox);
+    /* set_transient_window_pos(fe->window, fe->cfgbox); */
     gtk_widget_show(fe->cfgbox);
     gtk_main();
 
@@ -804,6 +829,34 @@ static void menu_key_event(GtkMenuItem *menuitem, gpointer data)
        gtk_widget_destroy(fe->window);
 }
 
+static void get_size(frontend *fe, int *px, int *py)
+{
+    int x, y;
+
+    /*
+     * Currently I don't want to make the GTK port scale large
+     * puzzles to fit on the screen. This is because X does permit
+     * extremely large windows and many window managers provide a
+     * means of navigating round them, and the users I consulted
+     * before deciding said that they'd rather have enormous puzzle
+     * windows spanning multiple screen pages than have them
+     * shrunk. I could change my mind later or introduce
+     * configurability; this would be the place to do so, by
+     * replacing the initial values of x and y with the screen
+     * dimensions.
+     */
+    x = INT_MAX;
+    y = INT_MAX;
+    midend_size(fe->me, &x, &y, FALSE);
+    *px = x;
+    *py = y;
+}
+
+#if !GTK_CHECK_VERSION(2,0,0)
+#define gtk_window_resize(win, x, y) \
+       gdk_window_resize(GTK_WIDGET(win)->window, x, y)
+#endif
+
 static void menu_preset_event(GtkMenuItem *menuitem, gpointer data)
 {
     frontend *fe = (frontend *)data;
@@ -813,10 +866,11 @@ static void menu_preset_event(GtkMenuItem *menuitem, gpointer data)
 
     midend_set_params(fe->me, params);
     midend_new_game(fe->me);
-    midend_size(fe->me, &x, &y);
-    gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
+    get_size(fe, &x, &y);
     fe->w = x;
     fe->h = y;
+    gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
+    gtk_window_resize(GTK_WINDOW(fe->window), 1, 1);
 }
 
 GdkAtom compound_text_atom, utf8_string_atom;
@@ -824,6 +878,8 @@ int paste_initialised = FALSE;
 
 void init_paste()
 {
+    unsigned char empty[] = { 0 };
+
     if (paste_initialised)
        return;
 
@@ -837,21 +893,21 @@ void init_paste()
      * ICCCM, we must do this before we start using cut buffers.
      */
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER0, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER0, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER1, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER1, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER2, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER2, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER3, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER3, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER4, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER4, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER5, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER5, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER6, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER6, XA_STRING, 8, PropModeAppend, empty, 0);
     XChangeProperty(GDK_DISPLAY(), GDK_ROOT_WINDOW(),
-                   XA_CUT_BUFFER7, XA_STRING, 8, PropModeAppend, "", 0);
+                   XA_CUT_BUFFER7, XA_STRING, 8, PropModeAppend, empty, 0);
 }
 
 /* Store data in a cut-buffer. */
@@ -937,6 +993,13 @@ static void menu_solve_event(GtkMenuItem *menuitem, gpointer data)
        error_box(fe->window, msg);
 }
 
+static void menu_restart_event(GtkMenuItem *menuitem, gpointer data)
+{
+    frontend *fe = (frontend *)data;
+
+    midend_restart_game(fe->me);
+}
+
 static void menu_config_event(GtkMenuItem *menuitem, gpointer data)
 {
     frontend *fe = (frontend *)data;
@@ -948,10 +1011,11 @@ static void menu_config_event(GtkMenuItem *menuitem, gpointer data)
        return;
 
     midend_new_game(fe->me);
-    midend_size(fe->me, &x, &y);
-    gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
+    get_size(fe, &x, &y);
     fe->w = x;
     fe->h = y;
+    gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
+    gtk_window_resize(GTK_WINDOW(fe->window), 1, 1);
 }
 
 static void menu_about_event(GtkMenuItem *menuitem, gpointer data)
@@ -998,7 +1062,11 @@ static frontend *new_window(char *game_id, char **error)
 
     fe = snew(frontend);
 
+    fe->timer_active = FALSE;
+    fe->timer_id = -1;
+
     fe->me = midend_new(fe, &thegame);
+
     if (game_id) {
         *error = midend_game_id(fe->me, game_id);
         if (*error) {
@@ -1032,7 +1100,12 @@ static frontend *new_window(char *game_id, char **error)
     gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
 
     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "New", 'n');
-    add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Restart", 'r');
+
+    menuitem = gtk_menu_item_new_with_label("Restart");
+    gtk_container_add(GTK_CONTAINER(menu), menuitem);
+    gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
+                      GTK_SIGNAL_FUNC(menu_restart_event), fe);
+    gtk_widget_show(menuitem);
 
     menuitem = gtk_menu_item_new_with_label("Specific...");
     gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
@@ -1131,24 +1204,9 @@ static frontend *new_window(char *game_id, char **error)
         fe->ncolours = ncolours;
         fe->colours = snewn(ncolours, GdkColor);
         for (i = 0; i < ncolours; i++) {
-            /*
-             * Just for Gareth: if you dislike any of the standard
-             * colours, here's your chance to configure them in a
-             * really hacky way. 
-             */
-            char buf[80], *e;
-            unsigned int r, g, b;
-            sprintf(buf, "PUZZLE_COLOUR_%d", i);
-            if ((e = getenv(buf)) != NULL &&
-                sscanf(e, "%2x%2x%2x", &r, &g, &b) == 3) {
-                fe->colours[i].red = r * 0x101;
-                fe->colours[i].green = g * 0x101;
-                fe->colours[i].blue = b * 0x101;
-            } else {
-                fe->colours[i].red = colours[i*3] * 0xFFFF;
-                fe->colours[i].green = colours[i*3+1] * 0xFFFF;
-                fe->colours[i].blue = colours[i*3+2] * 0xFFFF;
-            }
+            fe->colours[i].red = colours[i*3] * 0xFFFF;
+            fe->colours[i].green = colours[i*3+1] * 0xFFFF;
+            fe->colours[i].blue = colours[i*3+2] * 0xFFFF;
         }
         success = snewn(ncolours, gboolean);
         gdk_colormap_alloc_colors(fe->colmap, fe->colours, ncolours,
@@ -1186,18 +1244,18 @@ static frontend *new_window(char *game_id, char **error)
        fe->statusbar = NULL;
 
     fe->area = gtk_drawing_area_new();
-    midend_size(fe->me, &x, &y);
+    get_size(fe, &x, &y);
     gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
     fe->w = x;
     fe->h = y;
 
-    gtk_box_pack_end(vbox, fe->area, FALSE, FALSE, 0);
+    gtk_box_pack_end(vbox, fe->area, TRUE, TRUE, 0);
 
     fe->pixmap = NULL;
     fe->fonts = NULL;
     fe->nfonts = fe->fontsize = 0;
 
-    fe->timer_active = FALSE;
+    fe->laststatus = NULL;
 
     fe->paste_data = NULL;
     fe->paste_data_len = 0;
@@ -1231,6 +1289,9 @@ static frontend *new_window(char *game_id, char **error)
     gtk_widget_show(fe->area);
     gtk_widget_show(fe->window);
 
+    gdk_window_set_background(fe->area->window, &fe->colours[0]);
+    gdk_window_set_background(fe->window->window, &fe->colours[0]);
+
     return fe;
 }
 
@@ -1239,6 +1300,12 @@ int main(int argc, char **argv)
     char *pname = argv[0];
     char *error;
 
+    if (argc > 1 && !strcmp(argv[1], "--version")) {
+       printf("%s, from Simon Tatham's Portable Puzzle Collection\n%s\n",
+              thegame.name, ver);
+       return 0;
+    }
+
     /*
      * Special standalone mode for generating puzzle IDs on the
      * command line. Useful for generating puzzles to be printed
@@ -1277,7 +1344,10 @@ int main(int argc, char **argv)
             if ( (seed = strchr(params, '#')) != NULL )
                 *seed++ = '\0';
            thegame.decode_params(par, params);
-        } else {
+        }
+        if ((error = thegame.validate_params(par)) != NULL) {
+           fprintf(stderr, "%s: %s\n", pname, error);
+            return 1;
         }
        parstr = thegame.encode_params(par, FALSE);
 
@@ -1295,7 +1365,7 @@ int main(int argc, char **argv)
 
        while (n-- > 0) {
            game_aux_info *aux = NULL;
-           char *desc = thegame.new_desc(par, rs, &aux);
+           char *desc = thegame.new_desc(par, rs, &aux, FALSE);
            printf("%s:%s\n", parstr, desc);
            sfree(desc);
            if (aux)