chiark / gitweb /
Magnets: you can now mark clues as done
[sgt-puzzles.git] / gtk.c
diff --git a/gtk.c b/gtk.c
index d45a76d29bd19107dddd261cf19b165d8872a586..3e8df06eb01bab665bd1e747673707ccec79dc27 100644 (file)
--- a/gtk.c
+++ b/gtk.c
@@ -12,6 +12,7 @@
 #include <math.h>
 
 #include <sys/time.h>
+#include <sys/resource.h>
 
 #include <gtk/gtk.h>
 #include <gdk/gdkkeysyms.h>
@@ -117,6 +118,7 @@ struct frontend {
     GtkAccelGroup *accelgroup;
     GtkWidget *area;
     GtkWidget *statusbar;
+    GtkWidget *menubar;
     guint statusctx;
     int w, h;
     midend *me;
@@ -149,11 +151,13 @@ struct frontend {
 #ifdef OLD_FILESEL
     char *filesel_name;
 #endif
+    int drawing_area_shrink_pending;
     GSList *preset_radio;
     int n_preset_menu_items;
     int preset_threaded;
     GtkWidget *preset_custom;
     GtkWidget *copy_menu_item;
+    int menubar_is_local;
 };
 
 struct blitter {
@@ -1333,8 +1337,7 @@ int message_box(GtkWidget *parent, char *title, char *msg, int centre,
        titles += strlen(titles)+1;
        i++;
     }
-    gtk_object_set_data(GTK_OBJECT(window), "user-data",
-                       GINT_TO_POINTER(&i));
+    gtk_object_set_data(GTK_OBJECT(window), "user-data", &i);
     gtk_signal_connect(GTK_OBJECT(window), "destroy",
                        GTK_SIGNAL_FUNC(window_destroy), NULL);
     gtk_window_set_modal(GTK_WINDOW(window), TRUE);
@@ -1661,6 +1664,69 @@ static void changed_preset(frontend *fe)
     }
 }
 
+static gboolean not_size_allocated_yet(GtkWidget *w)
+{
+    /*
+     * This function tests whether a widget has not yet taken up space
+     * on the screen which it will occupy in future. (Therefore, it
+     * returns true only if the widget does exist but does not have a
+     * size allocation. A null widget is already taking up all the
+     * space it ever will.)
+     */
+    if (!w)
+        return FALSE;        /* nonexistent widgets aren't a problem */
+
+#if GTK_CHECK_VERSION(2,18,0)  /* skip if no gtk_widget_get_allocation */
+    {
+        GtkAllocation a;
+        gtk_widget_get_allocation(w, &a);
+        if (a.height == 0 || a.width == 0)
+            return TRUE;       /* widget exists but has no size yet */
+    }
+#endif
+
+    return FALSE;
+}
+
+static void try_shrink_drawing_area(frontend *fe)
+{
+    if (fe->drawing_area_shrink_pending &&
+        (!fe->menubar_is_local || !not_size_allocated_yet(fe->menubar)) &&
+        !not_size_allocated_yet(fe->statusbar)) {
+        /*
+         * In order to permit the user to resize the window smaller as
+         * well as bigger, we call this function after the window size
+         * has ended up where we want it. This shouldn't shrink the
+         * window immediately; it just arranges that the next time the
+         * user tries to shrink it, they can.
+         *
+         * However, at puzzle creation time, we defer the first of
+         * these operations until after the menu bar and status bar
+         * are actually visible. On Ubuntu 12.04 I've found that these
+         * can take a while to be displayed, and that it's a mistake
+         * to reduce the drawing area's size allocation before they've
+         * turned up or else the drawing area makes room for them by
+         * shrinking to less than the size we intended.
+         */
+        gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), 1, 1);
+        fe->drawing_area_shrink_pending = FALSE;
+    }
+}
+
+static gint configure_window(GtkWidget *widget,
+                             GdkEventConfigure *event, gpointer data)
+{
+    frontend *fe = (frontend *)data;
+    /*
+     * When the main puzzle window changes size, it might be because
+     * the menu bar or status bar has turned up after starting off
+     * absent, in which case we should have another go at enacting a
+     * pending shrink of the drawing area.
+     */
+    try_shrink_drawing_area(fe);
+    return FALSE;
+}
+
 static void resize_fe(frontend *fe)
 {
     int x, y;
@@ -1668,18 +1734,15 @@ static void resize_fe(frontend *fe)
     get_size(fe, &x, &y);
     fe->w = x;
     fe->h = y;
+    fe->drawing_area_shrink_pending = FALSE;
     gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
     {
         GtkRequisition req;
         gtk_widget_size_request(GTK_WIDGET(fe->window), &req);
         gtk_window_resize(GTK_WINDOW(fe->window), req.width, req.height);
     }
-    /*
-     * Now that we've established the preferred size of the window,
-     * reduce the drawing area's size request so the user can shrink
-     * the window.
-     */
-    gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), 1, 1);
+    fe->drawing_area_shrink_pending = TRUE;
+    try_shrink_drawing_area(fe);
 }
 
 static void menu_preset_event(GtkMenuItem *menuitem, gpointer data)
@@ -2036,7 +2099,7 @@ static frontend *new_window(char *arg, int argtype, char **error)
 {
     frontend *fe;
     GtkBox *vbox, *hbox;
-    GtkWidget *menubar, *menu, *menuitem;
+    GtkWidget *menu, *menuitem;
     GdkPixmap *iconpm;
     GList *iconlist;
     int x, y, n;
@@ -2111,6 +2174,33 @@ static frontend *new_window(char *arg, int argtype, char **error)
        midend_new_game(fe->me);
     }
 
+    {
+        /*
+         * try_shrink_drawing_area() will do some fiddling with the
+         * window size request (see comment in that function) after
+         * all the bits and pieces such as the menu bar and status bar
+         * have appeared in the puzzle window.
+         *
+         * However, on Unity systems, the menu bar _doesn't_ appear in
+         * the puzzle window, because the Unity shell hijacks it into
+         * the menu bar at the very top of the screen. We therefore
+         * try to detect that situation here, so that we don't sit
+         * here forever waiting for a menu bar.
+         */
+        const char prop[] = "gtk-shell-shows-menubar";
+        GtkSettings *settings = gtk_settings_get_default();
+        if (!g_object_class_find_property(G_OBJECT_GET_CLASS(settings),
+                                          prop)) {
+            fe->menubar_is_local = TRUE;
+        } else {
+            int unity_mode;
+            g_object_get(gtk_settings_get_default(),
+                         prop, &unity_mode,
+                         (const gchar *)NULL);
+            fe->menubar_is_local = !unity_mode;
+        }
+    }
+
     fe->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
     gtk_window_set_title(GTK_WINDOW(fe->window), thegame.name);
 
@@ -2125,12 +2215,12 @@ static frontend *new_window(char *arg, int argtype, char **error)
     gtk_box_pack_start(vbox, GTK_WIDGET(hbox), FALSE, FALSE, 0);
     gtk_widget_show(GTK_WIDGET(hbox));
 
-    menubar = gtk_menu_bar_new();
-    gtk_box_pack_start(hbox, menubar, TRUE, TRUE, 0);
-    gtk_widget_show(menubar);
+    fe->menubar = gtk_menu_bar_new();
+    gtk_box_pack_start(hbox, fe->menubar, TRUE, TRUE, 0);
+    gtk_widget_show(fe->menubar);
 
     menuitem = gtk_menu_item_new_with_mnemonic("_Game");
-    gtk_container_add(GTK_CONTAINER(menubar), menuitem);
+    gtk_container_add(GTK_CONTAINER(fe->menubar), menuitem);
     gtk_widget_show(menuitem);
 
     menu = gtk_menu_new();
@@ -2169,7 +2259,7 @@ static frontend *new_window(char *arg, int argtype, char **error)
         int i;
 
         menuitem = gtk_menu_item_new_with_mnemonic("_Type");
-        gtk_container_add(GTK_CONTAINER(menubar), menuitem);
+        gtk_container_add(GTK_CONTAINER(fe->menubar), menuitem);
         gtk_widget_show(menuitem);
 
         submenu = gtk_menu_new();
@@ -2201,7 +2291,7 @@ static frontend *new_window(char *arg, int argtype, char **error)
                gtk_radio_menu_item_group(GTK_RADIO_MENU_ITEM(menuitem));
             gtk_container_add(GTK_CONTAINER(submenu), menuitem);
             gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
-                               GPOINTER_TO_INT(CFG_SETTINGS));
+                               GINT_TO_POINTER(CFG_SETTINGS));
             gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
                                GTK_SIGNAL_FUNC(menu_config_event), fe);
             gtk_widget_show(menuitem);
@@ -2248,7 +2338,7 @@ static frontend *new_window(char *arg, int argtype, char **error)
     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Exit", 'q');
 
     menuitem = gtk_menu_item_new_with_mnemonic("_Help");
-    gtk_container_add(GTK_CONTAINER(menubar), menuitem);
+    gtk_container_add(GTK_CONTAINER(fe->menubar), menuitem);
     gtk_widget_show(menuitem);
 
     menu = gtk_menu_new();
@@ -2328,6 +2418,7 @@ static frontend *new_window(char *arg, int argtype, char **error)
     GTK_WIDGET_UNSET_FLAGS(fe->area, GTK_DOUBLE_BUFFERED);
 #endif
     get_size(fe, &x, &y);
+    fe->drawing_area_shrink_pending = FALSE;
     gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
     fe->w = x;
     fe->h = y;
@@ -2361,6 +2452,8 @@ static frontend *new_window(char *arg, int argtype, char **error)
                       GTK_SIGNAL_FUNC(map_window), fe);
     gtk_signal_connect(GTK_OBJECT(fe->area), "configure_event",
                       GTK_SIGNAL_FUNC(configure_area), fe);
+    gtk_signal_connect(GTK_OBJECT(fe->window), "configure_event",
+                      GTK_SIGNAL_FUNC(configure_window), fe);
 
     gtk_widget_add_events(GTK_WIDGET(fe->area),
                           GDK_BUTTON_PRESS_MASK |
@@ -2386,12 +2479,8 @@ static frontend *new_window(char *arg, int argtype, char **error)
     gtk_widget_show(fe->area);
     gtk_widget_show(fe->window);
 
-    /*
-     * Now that we've established the preferred size of the window,
-     * reduce the drawing area's size request so the user can shrink
-     * the window.
-     */
-    gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), 1, 1);
+    fe->drawing_area_shrink_pending = TRUE;
+    try_shrink_drawing_area(fe);
     set_window_background(fe, 0);
 
     return fe;
@@ -2421,6 +2510,7 @@ int main(int argc, char **argv)
     char *pname = argv[0];
     char *error;
     int ngenerate = 0, print = FALSE, px = 1, py = 1;
+    int time_generation = FALSE, test_solve = FALSE, list_presets = FALSE;
     int soln = FALSE, colour = FALSE;
     float scale = 1.0F;
     float redo_proportion = 0.0F;
@@ -2473,6 +2563,12 @@ int main(int argc, char **argv)
                }
            } else
                ngenerate = 1;
+       } else if (doing_opts && !strcmp(p, "--time-generation")) {
+            time_generation = TRUE;
+       } else if (doing_opts && !strcmp(p, "--test-solve")) {
+            test_solve = TRUE;
+       } else if (doing_opts && !strcmp(p, "--list-presets")) {
+            list_presets = TRUE;
        } else if (doing_opts && !strcmp(p, "--save")) {
            if (--ac > 0) {
                savefile = *++av;
@@ -2594,11 +2690,6 @@ int main(int argc, char **argv)
        }
     }
 
-    if (*errbuf) {
-       fputs(errbuf, stderr);
-       return 1;
-    }
-
     /*
      * Special standalone mode for generating puzzle IDs on the
      * command line. Useful for generating puzzles to be printed
@@ -2626,6 +2717,16 @@ int main(int argc, char **argv)
        char *id;
        document *doc = NULL;
 
+        /*
+         * If we're in this branch, we should display any pending
+         * error message from the command line, since GTK isn't going
+         * to take another crack at making sense of it.
+         */
+        if (*errbuf) {
+            fputs(errbuf, stderr);
+            return 1;
+        }
+
        n = ngenerate;
 
        me = midend_new(NULL, &thegame, NULL, NULL);
@@ -2656,7 +2757,8 @@ int main(int argc, char **argv)
         * generated descriptive game IDs.)
         */
        while (ngenerate == 0 || i < n) {
-           char *pstr, *err;
+           char *pstr, *err, *seed;
+            struct rusage before, after;
 
            if (ngenerate == 0) {
                pstr = fgetline(stdin);
@@ -2682,9 +2784,63 @@ int main(int argc, char **argv)
                    return 1;
                }
            }
-           sfree(pstr);
 
-           midend_new_game(me);
+            if (time_generation)
+                getrusage(RUSAGE_SELF, &before);
+
+            midend_new_game(me);
+
+            seed = midend_get_random_seed(me);
+
+            if (time_generation) {
+                double elapsed;
+
+                getrusage(RUSAGE_SELF, &after);
+
+                elapsed = (after.ru_utime.tv_sec -
+                           before.ru_utime.tv_sec);
+                elapsed += (after.ru_utime.tv_usec -
+                            before.ru_utime.tv_usec) / 1000000.0;
+
+                printf("%s %s: %.6f\n", thegame.name, seed, elapsed);
+            }
+
+            if (test_solve && thegame.can_solve) {
+                /*
+                 * Now destroy the aux_info in the midend, by means of
+                 * re-entering the same game id, and then try to solve
+                 * it.
+                 */
+                char *game_id, *err;
+
+                game_id = midend_get_game_id(me);
+                err = midend_game_id(me, game_id);
+                if (err) {
+                    fprintf(stderr, "%s %s: game id re-entry error: %s\n",
+                            thegame.name, seed, err);
+                    return 1;
+                }
+                midend_new_game(me);
+                sfree(game_id);
+
+                err = midend_solve(me);
+                /*
+                 * If the solve operation returned the error "Solution
+                 * not known for this puzzle", that's OK, because that
+                 * just means it's a puzzle for which we don't have an
+                 * algorithmic solver and hence can't solve it without
+                 * the aux_info, e.g. Netslide. Any other error is a
+                 * problem, though.
+                 */
+                if (err && strcmp(err, "Solution not known for this puzzle")) {
+                    fprintf(stderr, "%s %s: solve error: %s\n",
+                            thegame.name, seed, err);
+                    return 1;
+                }
+            }
+
+           sfree(pstr);
+            sfree(seed);
 
            if (doc) {
                err = midend_print_puzzle(me, doc, soln);
@@ -2698,6 +2854,16 @@ int main(int argc, char **argv)
                char *realname = snewn(40 + strlen(savefile) +
                                       strlen(savesuffix), char);
                sprintf(realname, "%s%d%s", savefile, i, savesuffix);
+
+                if (soln) {
+                    char *err = midend_solve(me);
+                    if (err) {
+                        fprintf(stderr, "%s: unable to show solution: %s\n",
+                                realname, err);
+                        return 1;
+                    }
+                }
+
                ctx.fp = fopen(realname, "w");
                if (!ctx.fp) {
                    fprintf(stderr, "%s: open: %s\n", realname,
@@ -2718,7 +2884,7 @@ int main(int argc, char **argv)
                }
                sfree(realname);
            }
-           if (!doc && !savefile) {
+           if (!doc && !savefile && !time_generation) {
                id = midend_get_game_id(me);
                puts(id);
                sfree(id);
@@ -2737,6 +2903,30 @@ int main(int argc, char **argv)
        midend_free(me);
 
        return 0;
+    } else if (list_presets) {
+        /*
+         * Another specialist mode which causes the puzzle to list the
+         * game_params strings for all its preset configurations.
+         */
+        int i, npresets;
+        midend *me;
+
+       me = midend_new(NULL, &thegame, NULL, NULL);
+        npresets = midend_num_presets(me);
+
+        for (i = 0; i < npresets; i++) {
+            game_params *params;
+            char *name, *paramstr;
+
+            midend_fetch_preset(me, i, &name, &params);
+            paramstr = thegame.encode_params(params, TRUE);
+
+            printf("%s %s\n", paramstr, name);
+            sfree(paramstr);
+        }
+
+       midend_free(me);
+        return 0;
     } else {
        frontend *fe;