chiark / gitweb /
Merge branch 'master' of https://git.tartarus.org/simon/puzzles into widelines
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Tue, 3 Oct 2017 19:48:34 +0000 (20:48 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Tue, 3 Oct 2017 19:48:34 +0000 (20:48 +0100)
1  2 
devel.but
gtk.c
pearl.c
slant.c
towers.c

diff --combined devel.but
index 361add8446b18b6e19ec77e356c2141de50f58f8,131678c17b85e99273a125ddcd95016b28038d4f..bf1c74fd435246d90744933ebd5852e7ece076bf
+++ b/devel.but
@@@ -555,16 -555,15 +555,15 @@@ The \cw{config_item} structure contain
  
  \c char *name;
  \c int type;
- \c char *sval;
- \c int ival;
+ \c union { /* type-specific fields */ } u;
+ \e         iiiiiiiiiiiiiiiiiiiiiiiiii
  
  \c{name} is an ASCII string giving the textual label for a GUI
  control. It is \e{not} expected to be dynamically allocated.
  
  \c{type} contains one of a small number of \c{enum} values defining
- what type of control is being described. The meaning of the \c{sval}
- and \c{ival} fields depends on the value in \c{type}. The valid
- values are:
+ what type of control is being described. The usable member of the
+ union field \c{u} depends on \c{type}. The valid type values are:
  
  \dt \c{C_STRING}
  
  input. The back end does not bother informing the front end that the
  box is numeric rather than textual; some front ends do have the
  capacity to take this into account, but I decided it wasn't worth
- the extra complexity in the interface.) For this type, \c{ival} is
- unused, and \c{sval} contains a dynamically allocated string
- representing the contents of the input box.
+ the extra complexity in the interface.)
+ \lcont{
+ For controls of this type, \c{u.string} contains a single field
+ \c char *sval;
+ which stores a dynamically allocated string representing the contents
+ of the input box.
+ }
  
  \dt \c{C_BOOLEAN}
  
- \dd Describes a simple checkbox. For this type, \c{sval} is unused,
- and \c{ival} is \cw{TRUE} or \cw{FALSE}.
+ \dd Describes a simple checkbox.
+ \lcont{
+ For controls of this type, \c{u.boolean} contains a single field
+ \c int bval;
+ which is either \cw{TRUE} or \cw{FALSE}.
+ }
  
  \dt \c{C_CHOICES}
  
  \dd Describes a drop-down list presenting one of a small number of
- fixed choices. For this type, \c{sval} contains a list of strings
- describing the choices; the very first character of \c{sval} is used
- as a delimiter when processing the rest (so that the strings
- \cq{:zero:one:two}, \cq{!zero!one!two} and \cq{xzeroxonextwo} all
- define a three-element list containing \cq{zero}, \cq{one} and
- \cq{two}). \c{ival} contains the index of the currently selected
- element, numbering from zero (so that in the above example, 0 would
- mean \cq{zero} and 2 would mean \cq{two}).
+ fixed choices.
  
  \lcont{
  
- Note that for this control type, \c{sval} is \e{not} dynamically
- allocated, whereas it was for \c{C_STRING}.
+ For controls of this type, \c{u.choices} contains two fields:
+ \c const char *choicenames;
+ \c int selected;
+ \c{choicenames} contains a list of strings describing the choices. The
+ very first character of \c{sval} is used as a delimiter when
+ processing the rest (so that the strings \cq{:zero:one:two},
+ \cq{!zero!one!two} and \cq{xzeroxonextwo} all define a three-element
+ list containing \cq{zero}, \cq{one} and \cq{two}).
+ \c{selected} contains the index of the currently selected element,
+ numbering from zero (so that in the above example, 0 would mean
+ \cq{zero} and 2 would mean \cq{two}).
+ Note that \c{u.choices.choicenames} is \e{not} dynamically allocated,
+ unlike \c{u.string.sval}.
  
  }
  
  \dt \c{C_END}
  
- \dd Marks the end of the array of \c{config_item}s. All other fields
- are unused.
+ \dd Marks the end of the array of \c{config_item}s. There is no
+ associated member of the union field \c{u} for this type.
  
  The array returned from this function is expected to have filled in
  the initial values of all the controls according to the input
@@@ -639,7 -664,8 +664,8 @@@ function is never called and need not d
  
  \S{backend-validate-params} \cw{validate_params()}
  
- \c char *(*validate_params)(const game_params *params, int full);
+ \c const char *(*validate_params)(const game_params *params,
+ \c                                int full);
  
  This function takes a \c{game_params} structure as input, and checks
  that the parameters described in it fall within sensible limits. (At
@@@ -724,7 -750,8 +750,8 @@@ again in the game description
  
  \S{backend-validate-desc} \cw{validate_desc()}
  
- \c char *(*validate_desc)(const game_params *params, const char *desc);
+ \c const char *(*validate_desc)(const game_params *params,
+ \c                              const char *desc);
  
  This function is given a game description, and its job is to
  validate that it describes a puzzle which makes sense.
@@@ -907,10 -934,10 +934,10 @@@ divide mouse coordinates by it.
  in response to the input event; the puzzle was not interested in it
  at all.
  
- \b Returning the empty string (\cw{""}) indicates that the input
+ \b Returning the special value \cw{UI_UPDATE} indicates that the input
  event has resulted in a change being made to the \c{game_ui} which
- will require a redraw of the game window, but that no actual
\e{move} was made (i.e. no new \c{game_state} needs to be created).
+ will require a redraw of the game window, but that no actual \e{move}
+ was made (i.e. no new \c{game_state} needs to be created).
  
  \b Returning anything else indicates that a move was made and that a
  new \c{game_state} must be created. However, instead of actually
@@@ -925,7 -952,7 +952,7 @@@ strings can be written to disk when sav
  
  The return value from \cw{interpret_move()} is expected to be
  dynamically allocated if and only if it is not either \cw{NULL}
- \e{or} the empty string.
+ \e{or} the special string constant \c{UI_UPDATE}.
  
  After this function is called, the back end is permitted to rely on
  some subsequent operations happening in sequence:
@@@ -1028,7 -1055,7 +1055,7 @@@ not even offer the \q{Solve} menu optio
  \S{backend-solve} \cw{solve()}
  
  \c char *(*solve)(const game_state *orig, const game_state *curr,
- \c                const char *aux, char **error);
+ \c                const char *aux, const char **error);
  
  This function is called when the user selects the \q{Solve} option
  from the menu.
@@@ -1934,7 -1961,8 +1961,8 @@@ This ensures that thin lines are visibl
  \S{drawing-draw-text} \cw{draw_text()}
  
  \c void draw_text(drawing *dr, int x, int y, int fonttype,
- \c                int fontsize, int align, int colour, char *text);
+ \c                int fontsize, int align, int colour,
+ \c                const char *text);
  
  Draws text in the puzzle window.
  
@@@ -2095,7 -2123,7 +2123,7 @@@ printing routines, that code may safel
  
  \S{drawing-status-bar} \cw{status_bar()}
  
- \c void status_bar(drawing *dr, char *text);
+ \c void status_bar(drawing *dr, const char *text);
  
  Sets the text in the game's status bar to \c{text}. The text is copied
  from the supplied buffer, so the caller is free to deallocate or
@@@ -2366,7 -2394,8 +2394,8 @@@ function \cw{drawing_new()} (see \k{dra
  \S{drawingapi-draw-text} \cw{draw_text()}
  
  \c void (*draw_text)(void *handle, int x, int y, int fonttype,
- \c                   int fontsize, int align, int colour, char *text);
+ \c                   int fontsize, int align, int colour,
+ \c                   const char *text);
  
  This function behaves exactly like the back end \cw{draw_text()}
  function; see \k{drawing-draw-text}.
@@@ -2469,7 -2498,7 +2498,7 @@@ called unless drawing is attempted
  
  \S{drawingapi-status-bar} \cw{status_bar()}
  
- \c void (*status_bar)(void *handle, char *text);
+ \c void (*status_bar)(void *handle, const char *text);
  
  This function behaves exactly like the back end \cw{status_bar()}
  function; see \k{drawing-status-bar}.
@@@ -3129,8 -3158,8 +3158,8 @@@ will probably need to pass it to \cw{mi
  
  \H{midend-set-config} \cw{midend_set_config()}
  
- \c char *midend_set_config(midend *me, int which,
- \c                         config_item *cfg);
+ \c const char *midend_set_config(midend *me, int which,
+ \c                               config_item *cfg);
  
  Passes the mid-end the results of a configuration dialog box.
  \c{which} should have the same value which it had when
@@@ -3151,7 -3180,7 +3180,7 @@@ using \cw{midend_size()} and eventuall
  
  \H{midend-game-id} \cw{midend_game_id()}
  
- \c char *midend_game_id(midend *me, char *id);
+ \c const char *midend_game_id(midend *me, const char *id);
  
  Passes the mid-end a string game ID (of any of the valid forms
  \cq{params}, \cq{params:description} or \cq{params#seed}) which the
@@@ -3219,7 -3248,7 +3248,7 @@@ conversion
  
  \H{midend-solve} \cw{midend_solve()}
  
- \c char *midend_solve(midend *me);
+ \c const char *midend_solve(midend *me);
  
  Requests the mid-end to perform a Solve operation.
  
@@@ -3267,8 -3296,7 +3296,7 @@@ visually activate and deactivate a red
  \H{midend-serialise} \cw{midend_serialise()}
  
  \c void midend_serialise(midend *me,
- \c                       void (*write)(void *ctx, void *buf, int len),
- \c                       void *wctx);
+ \c     void (*write)(void *ctx, const void *buf, int len), void *wctx);
  
  Calling this function causes the mid-end to convert its entire
  internal state into a long ASCII text string, and to pass that
@@@ -3291,9 -3319,8 +3319,8 @@@ output string
  
  \H{midend-deserialise} \cw{midend_deserialise()}
  
- \c char *midend_deserialise(midend *me,
- \c                          int (*read)(void *ctx, void *buf, int len),
- \c                          void *rctx);
+ \c const char *midend_deserialise(midend *me,
+ \c     int (*read)(void *ctx, void *buf, int len), void *rctx);
  
  This function is the counterpart to \cw{midend_serialise()}. It
  calls the supplied \cw{read} function repeatedly to read a quantity
@@@ -3330,9 -3357,8 +3357,8 @@@ place
  
  \H{identify-game} \cw{identify_game()}
  
- \c char *identify_game(char **name,
- \c                     int (*read)(void *ctx, void *buf, int len),
- \c                     void *rctx);
+ \c const char *identify_game(char **name,
+ \c     int (*read)(void *ctx, void *buf, int len), void *rctx);
  
  This function examines a serialised midend stream, of the same kind
  used by \cw{midend_serialise()} and \cw{midend_deserialise()}, and
@@@ -3482,7 -3508,7 +3508,7 @@@ calling \cw{midend_timer()}
  
  \H{frontend-fatal} \cw{fatal()}
  
- \c void fatal(char *fmt, ...);
+ \c void fatal(const char *fmt, ...);
  
  This is called by some utility functions if they encounter a
  genuinely fatal error such as running out of memory. It is a
@@@ -3737,10 -3763,10 +3763,10 @@@ quite everywhere.
  
  \c void free_cfg(config_item *cfg);
  
- This function correctly frees an array of \c{config_item}s,
- including walking the array until it gets to the end and freeing
- precisely those \c{sval} fields which are expected to be dynamically
- allocated.
+ This function correctly frees an array of \c{config_item}s, including
+ walking the array until it gets to the end and freeing any subsidiary
+ data items in each \c{u} sub-union which are expected to be
dynamically allocated.
  
  (See \k{backend-configure} for details of the \c{config_item}
  structure.)
@@@ -4896,5 -4922,3 +4922,5 @@@ that the redraw and the animations (if 
  to animate undo properly seems to be a common error.
  
  Other than that, just use your common sense.
 +
 +\versionid Simon Tatham's Portable Puzzle Collection, version 20161228.7cae89f
diff --combined gtk.c
index dd2ebe6389665ee9f5669c875b2f65262f1fe27a,37ba8078e20d5858eb25659cf34336cb2f57cece..30257cbfbc3802e691118cdb3964e409519aabf7
--- 1/gtk.c
--- 2/gtk.c
+++ b/gtk.c
@@@ -2,10 -2,6 +2,10 @@@
   * gtk.c: GTK front end for my puzzle collection.
   */
  
 +#ifndef _POSIX_C_SOURCE
 +#define _POSIX_C_SOURCE 1 /* for PATH_MAX */
 +#endif
 +
  #include <stdio.h>
  #include <assert.h>
  #include <stdlib.h>
@@@ -14,9 -10,6 +14,9 @@@
  #include <string.h>
  #include <errno.h>
  #include <math.h>
 +#include <limits.h>
 +#include <unistd.h>
 +#include <locale.h>
  
  #include <sys/time.h>
  #include <sys/resource.h>
@@@ -78,7 -71,7 +78,7 @@@
  #ifdef DEBUGGING
  static FILE *debug_fp = NULL;
  
- void dputs(char *buf)
+ void dputs(const char *buf)
  {
      if (!debug_fp) {
          debug_fp = fopen("debug.log", "w");
@@@ -92,7 -85,7 +92,7 @@@
      }
  }
  
- void debug_printf(char *fmt, ...)
+ void debug_printf(const char *fmt, ...)
  {
      char buf[4096];
      va_list ap;
   * Error reporting functions used elsewhere.
   */
  
- void fatal(char *fmt, ...)
+ void fatal(const char *fmt, ...)
  {
      va_list ap;
  
@@@ -196,6 -189,38 +196,38 @@@ struct frontend 
      int drawing_area_shrink_pending;
      int 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.
+      */
+     int awaiting_resize_ack;
+ #endif
  };
  
  struct blitter {
@@@ -239,7 -264,7 +271,7 @@@ void frontend_default_colour(frontend *
  #endif
  }
  
- void gtk_status_bar(void *handle, char *text)
+ void gtk_status_bar(void *handle, const char *text)
  {
      frontend *fe = (frontend *)handle;
  
@@@ -975,7 -1000,7 +1007,7 @@@ void gtk_unclip(void *handle
  }
  
  void gtk_draw_text(void *handle, int x, int y, int fonttype, int fontsize,
-                  int align, int colour, char *text)
+                  int align, int colour, const char *text)
  {
      frontend *fe = (frontend *)handle;
      int i;
@@@ -1345,15 -1370,10 +1377,10 @@@ static gint map_window(GtkWidget *widge
      return TRUE;
  }
  
- static gint configure_area(GtkWidget *widget,
-                            GdkEventConfigure *event, gpointer data)
+ static void resize_puzzle_to_area(frontend *fe, int x, int y)
  {
-     frontend *fe = (frontend *)data;
-     int x, y;
      int oldw = fe->w, oldpw = fe->pw, oldh = fe->h, oldph = fe->ph;
  
-     x = event->width;
-     y = event->height;
      fe->w = x;
      fe->h = y;
      midend_size(fe->me, &x, &y, TRUE);
      }
  
      midend_force_redraw(fe->me);
+ }
  
+ static gint configure_area(GtkWidget *widget,
+                            GdkEventConfigure *event, gpointer data)
+ {
+     frontend *fe = (frontend *)data;
+     resize_puzzle_to_area(fe, event->width, event->height);
+     fe->awaiting_resize_ack = FALSE;
      return TRUE;
  }
  
+ #if GTK_CHECK_VERSION(3,0,0)
+ static void window_size_alloc(GtkWidget *widget, GtkAllocation *allocation,
+                               gpointer data)
+ {
+     frontend *fe = (frontend *)data;
+     if (fe->awaiting_resize_ack) {
+         GtkAllocation a;
+         gtk_widget_get_allocation(fe->area, &a);
+         resize_puzzle_to_area(fe, a.width, a.height);
+         fe->awaiting_resize_ack = FALSE;
+     }
+ }
+ #endif
  static gint timer_func(gpointer data)
  {
      frontend *fe = (frontend *)data;
@@@ -1451,8 -1492,8 +1499,8 @@@ static void align_label(GtkLabel *label
  }
  
  #if GTK_CHECK_VERSION(3,0,0)
- int message_box(GtkWidget *parent, char *title, char *msg, int centre,
-               int type)
+ int message_box(GtkWidget *parent, const char *title, const char *msg,
+                 int centre, int type)
  {
      GtkWidget *window;
      gint ret;
@@@ -1546,7 -1587,7 +1594,7 @@@ int message_box(GtkWidget *parent, cha
  }
  #endif /* GTK_CHECK_VERSION(3,0,0) */
  
- void error_box(GtkWidget *parent, char *msg)
+ void error_box(GtkWidget *parent, const char *msg)
  {
      message_box(parent, "Error", msg, FALSE, MB_OK);
  }
  static void config_ok_button_clicked(GtkButton *button, gpointer data)
  {
      frontend *fe = (frontend *)data;
-     char *err;
+     const char *err;
  
      err = midend_set_config(fe->me, fe->cfg_which, fe->cfg);
  
@@@ -1600,22 -1641,25 +1648,25 @@@ static void editbox_changed(GtkEditabl
  {
      config_item *i = (config_item *)data;
  
-     sfree(i->sval);
-     i->sval = dupstr(gtk_entry_get_text(GTK_ENTRY(ed)));
+     assert(i->type == C_STRING);
+     sfree(i->u.string.sval);
+     i->u.string.sval = dupstr(gtk_entry_get_text(GTK_ENTRY(ed)));
  }
  
  static void button_toggled(GtkToggleButton *tb, gpointer data)
  {
      config_item *i = (config_item *)data;
  
-     i->ival = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(tb));
+     assert(i->type == C_BOOLEAN);
+     i->u.boolean.bval = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(tb));
  }
  
  static void droplist_sel(GtkComboBox *combo, gpointer data)
  {
      config_item *i = (config_item *)data;
  
-     i->ival = gtk_combo_box_get_active(combo);
+     assert(i->type == C_CHOICES);
+     i->u.choices.selected = gtk_combo_box_get_active(combo);
  }
  
  static int get_config(frontend *fe, int which)
                             GTK_EXPAND | GTK_SHRINK | GTK_FILL,
                             3, 3);
  #endif
-           gtk_entry_set_text(GTK_ENTRY(w), i->sval);
+           gtk_entry_set_text(GTK_ENTRY(w), i->u.string.sval);
            g_signal_connect(G_OBJECT(w), "changed",
                               G_CALLBACK(editbox_changed), i);
            g_signal_connect(G_OBJECT(w), "key_press_event",
                             GTK_EXPAND | GTK_SHRINK | GTK_FILL,
                             3, 3);
  #endif
-           gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), i->ival);
+           gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w),
+                                          i->u.boolean.bval);
            gtk_widget_show(w);
            break;
  
  
              {
                int c;
-               char *p, *q, *name;
+               const char *p, *q;
+                 char *name;
                  GtkListStore *model;
                GtkCellRenderer *cr;
                  GtkTreeIter iter;
  
                  model = gtk_list_store_new(1, G_TYPE_STRING);
  
-               c = *i->sval;
-               p = i->sval+1;
+               c = *i->u.choices.choicenames;
+               p = i->u.choices.choicenames+1;
  
                while (*p) {
                    q = p;
  
                  w = gtk_combo_box_new_with_model(GTK_TREE_MODEL(model));
  
-               gtk_combo_box_set_active(GTK_COMBO_BOX(w), i->ival);
+               gtk_combo_box_set_active(GTK_COMBO_BOX(w),
+                                          i->u.choices.selected);
  
                cr = gtk_cell_renderer_text_new();
                gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(w), cr, TRUE);
@@@ -1888,8 -1935,7 +1942,7 @@@ static void changed_preset(frontend *fe
              struct preset_menu_entry *entry =
                  (struct preset_menu_entry *)g_object_get_data(
                      G_OBJECT(gs->data), "user-data");
-             if (entry && entry->id != n)
+             if (!entry || entry->id != n)
                  gtk_check_menu_item_set_active(
                      GTK_CHECK_MENU_ITEM(gs->data), FALSE);
              else
          }
          if (found)
              gtk_check_menu_item_set_active(
-                 GTK_CHECK_MENU_ITEM(found->data), FALSE);
+                 GTK_CHECK_MENU_ITEM(found->data), TRUE);
      }
      fe->preset_threaded = FALSE;
  
@@@ -2003,6 -2049,7 +2056,7 @@@ static void resize_fe(frontend *fe
  
  #if GTK_CHECK_VERSION(3,0,0)
      gtk_window_resize(GTK_WINDOW(fe->window), x, y + window_extra_height(fe));
+     fe->awaiting_resize_ack = TRUE;
  #else
      fe->drawing_area_shrink_pending = FALSE;
      gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
@@@ -2121,7 -2168,7 +2175,7 @@@ static void filesel_ok(GtkButton *butto
      fe->filesel_name = dupstr(name);
  }
  
- static char *file_selector(frontend *fe, char *title, int save)
+ static char *file_selector(frontend *fe, const char *title, int save)
  {
      GtkWidget *filesel =
          gtk_file_selection_new(title);
  
  #else
  
- static char *file_selector(frontend *fe, char *title, int save)
+ static char *file_selector(frontend *fe, const char *title, int save)
  {
      char *filesel_name = NULL;
  
@@@ -2184,7 -2231,7 +2238,7 @@@ struct savefile_write_ctx 
      int error;
  };
  
- static void savefile_write(void *wctx, void *buf, int len)
+ static void savefile_write(void *wctx, const void *buf, int len)
  {
      struct savefile_write_ctx *ctx = (struct savefile_write_ctx *)wctx;
      if (fwrite(buf, 1, len, ctx->fp) < len)
@@@ -2251,7 -2298,8 +2305,8 @@@ static void menu_save_event(GtkMenuIte
  static void menu_load_event(GtkMenuItem *menuitem, gpointer data)
  {
      frontend *fe = (frontend *)data;
-     char *name, *err;
+     char *name;
+     const char *err;
  
      name = file_selector(fe, "Enter name of saved game file to load", FALSE);
  
  static void menu_solve_event(GtkMenuItem *menuitem, gpointer data)
  {
      frontend *fe = (frontend *)data;
-     char *msg;
+     const char *msg;
  
      msg = midend_solve(fe->me);
  
@@@ -2316,89 -2364,6 +2371,89 @@@ static void menu_config_event(GtkMenuIt
      midend_redraw(fe->me);
  }
  
 +#ifndef HELP_BROWSER_PATH
 +#define HELP_BROWSER_PATH "xdg-open:sensible-browser"
 +#endif
 +
 +static void show_help(frontend *fe, const char *topic)
 +{
 +    const char *list = HELP_BROWSER_PATH;
 +    char path[PATH_MAX + 1];
 +    struct {
 +      const char *s;
 +      int len;
 +    } lang[3];
 +    int i;
 +
 +    /*
 +     * Search for help file, trying:
 +     * 1. Version for this locale, ignoring encoding (HTML browsers
 +     *    must handle multiple encodings)
 +     * 2. Version for this locale, ignoring encoding and country
 +     * 3. English version
 +     */
 +    lang[0].s = setlocale(LC_MESSAGES, NULL);
 +    lang[0].len = strcspn(lang[0].s, ".@");
 +    lang[1].s = lang[0].s;
 +    lang[1].len = strcspn(lang[1].s, "_");
 +    if (lang[1].len > lang[0].len)
 +      lang[1].len = lang[0].len;
 +    lang[2].s = "en";
 +    lang[2].len = 2;
 +    for (i = 0; i < lenof(lang); i++) {
 +      sprintf(path, "%s/sgt-puzzles/help/%.*s/%s.html",
 +              SHAREDIR, lang[i].len, lang[i].s, topic);
 +      if (access(path, R_OK) == 0)
 +          break;
 +    }
 +    if (i == lenof(lang)) {
 +      error_box(fe->window, "Help file is not installed");
 +      return;
 +    }
 +
 +    for (;;) {
 +      size_t len;
 +      char buf[PATH_MAX + 1];
 +      const char *command;
 +      const char *argv[3];
 +
 +      len = strcspn(list, ":");
 +      if (len <= PATH_MAX) {
 +          memcpy(buf, list, len);
 +          buf[len] = 0;
 +          if (buf[0] == '$')
 +              command = getenv(buf + 1);
 +          else
 +              command = buf;
 +          if (command) {
 +              argv[0] = command;
 +              argv[1] = path;
 +              argv[2] = NULL;
 +              if (g_spawn_async(NULL, (char **)argv, NULL,
 +                                G_SPAWN_SEARCH_PATH,
 +                                NULL, NULL, NULL, NULL))
 +                  return;
 +          }
 +      }
 +
 +      if (!list[len])
 +          break;
 +      list += len + 1;
 +    }
 +
 +    error_box(fe->window, "Failed to start a help browser");
 +}
 +
 +static void menu_help_contents_event(GtkMenuItem *menuitem, gpointer data)
 +{
 +    show_help((frontend *)data, "index");
 +}
 +
 +static void menu_help_specific_event(GtkMenuItem *menuitem, gpointer data)
 +{
 +    show_help((frontend *)data, thegame.htmlhelp_topic);
 +}
 +
  static void menu_about_event(GtkMenuItem *menuitem, gpointer data)
  {
      frontend *fe = (frontend *)data;
  }
  
  static GtkWidget *add_menu_ui_item(
-     frontend *fe, GtkContainer *cont, char *text, int action,
+     frontend *fe, GtkContainer *cont, const char *text, int action,
      int accel_key, int accel_keyqual)
  {
      GtkWidget *menuitem = gtk_menu_item_new_with_label(text);
@@@ -2524,7 -2489,7 +2579,7 @@@ static frontend *new_window(char *arg, 
      fe->me = midend_new(fe, &thegame, &gtk_drawing, fe);
  
      if (arg) {
-       char *err;
+       const char *err;
        FILE *fp;
  
        errbuf[0] = '\0';
      }
  #endif
  
+ #if GTK_CHECK_VERSION(3,0,0)
+     fe->awaiting_resize_ack = FALSE;
+ #endif
      fe->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
      gtk_window_set_title(GTK_WINDOW(fe->window), thegame.name);
  
      menu = gtk_menu_new();
      gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
  
 +    menuitem = gtk_menu_item_new_with_label("Contents");
 +    gtk_container_add(GTK_CONTAINER(menu), menuitem);
 +    g_signal_connect(G_OBJECT(menuitem), "activate",
 +                   G_CALLBACK(menu_help_contents_event), fe);
 +    gtk_widget_show(menuitem);
 +
 +    if (thegame.htmlhelp_topic) {
 +      char *item;
 +      assert(thegame.name);
 +      item = snewn(9+strlen(thegame.name), char); /*ick*/
 +      sprintf(item, "Help on %s", thegame.name);
 +      menuitem = gtk_menu_item_new_with_label(item);
 +      sfree(item);
 +      gtk_container_add(GTK_CONTAINER(menu), menuitem);
 +      g_signal_connect(G_OBJECT(menuitem), "activate",
 +                       G_CALLBACK(menu_help_specific_event), fe);
 +      gtk_widget_show(menuitem);
 +    }
 +
      menuitem = gtk_menu_item_new_with_label("About");
      gtk_container_add(GTK_CONTAINER(menu), menuitem);
      g_signal_connect(G_OBJECT(menuitem), "activate",
                       G_CALLBACK(configure_area), fe);
      g_signal_connect(G_OBJECT(fe->window), "configure_event",
                       G_CALLBACK(configure_window), fe);
+ #if GTK_CHECK_VERSION(3,0,0)
+     g_signal_connect(G_OBJECT(fe->window), "size_allocate",
+                      G_CALLBACK(window_size_alloc), fe);
+ #endif
  
      gtk_widget_add_events(GTK_WIDGET(fe->area),
                            GDK_BUTTON_PRESS_MASK |
@@@ -2974,7 -2928,7 +3037,7 @@@ int main(int argc, char **argv
      int soln = FALSE, colour = FALSE;
      float scale = 1.0F;
      float redo_proportion = 0.0F;
-     char *savefile = NULL, *savesuffix = NULL;
+     const char *savefile = NULL, *savesuffix = NULL;
      char *arg = NULL;
      int argtype = ARG_EITHER;
      char *screenshot_file = NULL;
         * generated descriptive game IDs.)
         */
        while (ngenerate == 0 || i < n) {
-           char *pstr, *err, *seed;
+           char *pstr, *seed;
+             const char *err;
              struct rusage before, after;
  
            if (ngenerate == 0) {
                   * re-entering the same game id, and then try to solve
                   * it.
                   */
-                 char *game_id, *err;
+                 char *game_id;
  
                  game_id = midend_get_game_id(me);
                  err = midend_game_id(me, game_id);
                sprintf(realname, "%s%d%s", savefile, i, savesuffix);
  
                  if (soln) {
-                     char *err = midend_solve(me);
+                     const char *err = midend_solve(me);
                      if (err) {
                          fprintf(stderr, "%s: unable to show solution: %s\n",
                                  realname, err);
diff --combined pearl.c
index ce04154a617532cb58a4b21491bbb4eace2e643b,4f3be50275b1cc32e0dd2d785e56c1d6ca0f3d6f..164ff482aa4192c9fbb94683af913d3286e2ae06
+++ b/pearl.c
@@@ -234,29 -234,24 +234,24 @@@ static config_item *game_configure(cons
      ret[0].name = "Width";
      ret[0].type = C_STRING;
      sprintf(buf, "%d", params->w);
-     ret[0].sval = dupstr(buf);
-     ret[0].ival = 0;
+     ret[0].u.string.sval = dupstr(buf);
  
      ret[1].name = "Height";
      ret[1].type = C_STRING;
      sprintf(buf, "%d", params->h);
-     ret[1].sval = dupstr(buf);
-     ret[1].ival = 0;
+     ret[1].u.string.sval = dupstr(buf);
  
      ret[2].name = "Difficulty";
      ret[2].type = C_CHOICES;
-     ret[2].sval = DIFFCONFIG;
-     ret[2].ival = params->difficulty;
+     ret[2].u.choices.choicenames = DIFFCONFIG;
+     ret[2].u.choices.selected = params->difficulty;
  
      ret[3].name = "Allow unsoluble";
      ret[3].type = C_BOOLEAN;
-     ret[3].sval = NULL;
-     ret[3].ival = params->nosolve;
+     ret[3].u.boolean.bval = params->nosolve;
  
      ret[4].name = NULL;
      ret[4].type = C_END;
-     ret[4].sval = NULL;
-     ret[4].ival = 0;
  
      return ret;
  }
@@@ -265,22 -260,20 +260,22 @@@ static game_params *custom_params(cons
  {
      game_params *ret = snew(game_params);
  
-     ret->w = atoi(cfg[0].sval);
-     ret->h = atoi(cfg[1].sval);
-     ret->difficulty = cfg[2].ival;
-     ret->nosolve = cfg[3].ival;
+     ret->w = atoi(cfg[0].u.string.sval);
+     ret->h = atoi(cfg[1].u.string.sval);
+     ret->difficulty = cfg[2].u.choices.selected;
+     ret->nosolve = cfg[3].u.boolean.bval;
  
      return ret;
  }
  
- static char *validate_params(const game_params *params, int full)
+ static const char *validate_params(const game_params *params, int full)
  {
      if (params->w < 5) return "Width must be at least five";
      if (params->h < 5) return "Height must be at least five";
      if (params->difficulty < 0 || params->difficulty >= DIFFCOUNT)
          return "Unknown difficulty level";
 +    if (params->difficulty >= DIFF_TRICKY && params->w + params->h < 11)
 +      return "Width or height must be at least six for Tricky";
  
      return NULL;
  }
@@@ -1394,7 -1387,7 +1389,7 @@@ static char *new_game_desc(const game_p
      return desc;
  }
  
- static char *validate_desc(const game_params *params, const char *desc)
+ static const char *validate_desc(const game_params *params, const char *desc)
  {
      int i, sizesofar;
      const int totalsize = params->w * params->h;
@@@ -1728,7 -1721,7 +1723,7 @@@ static char *solve_for_diff(game_state 
  }
  
  static char *solve_game(const game_state *state, const game_state *currstate,
-                         const char *aux, char **error)
+                         const char *aux, const char **error)
  {
      game_state *solved = dup_game(state);
      int i, ret, sz = state->shared->sz;
@@@ -2024,11 -2017,11 +2019,11 @@@ static char *mark_in_direction(const ga
  
      char ch = primary ? 'F' : 'M', *other;
  
-     if (!INGRID(state, x, y) || !INGRID(state, x2, y2)) return "";
+     if (!INGRID(state, x, y) || !INGRID(state, x2, y2)) return UI_UPDATE;
  
      /* disallow laying a mark over a line, or vice versa. */
      other = primary ? state->marks : state->lines;
-     if (other[y*w+x] & dir || other[y2*w+x2] & dir2) return "";
+     if (other[y*w+x] & dir || other[y2*w+x2] & dir2) return UI_UPDATE;
      
      sprintf(buf, "%c%d,%d,%d;%c%d,%d,%d", ch, dir, x, y, ch, dir2, x2, y2);
      return dupstr(buf);
@@@ -2062,12 -2055,12 +2057,12 @@@ static char *interpret_move(const game_
          ui->dragcoords[0] = gy * w + gx;
          ui->ndragcoords = 0;           /* will be 1 once drag is confirmed */
  
-         return "";
+         return UI_UPDATE;
      }
  
      if (button == LEFT_DRAG && ui->ndragcoords >= 0) {
          update_ui_drag(state, ui, gx, gy);
-         return "";
+         return UI_UPDATE;
      }
  
      if (IS_MOUSE_RELEASE(button)) release = TRUE;
            if (ui->ndragcoords >= 0)
                update_ui_drag(state, ui, ui->curx, ui->cury);
        }
-       return "";
+       return UI_UPDATE;
      }
  
      if (IS_CURSOR_SELECT(button)) {
        if (!ui->cursor_active) {
            ui->cursor_active = TRUE;
-           return "";
+           return UI_UPDATE;
        } else if (button == CURSOR_SELECT) {
            if (ui->ndragcoords == -1) {
                ui->ndragcoords = 0;
                ui->dragcoords[0] = ui->cury * w + ui->curx;
                ui->clickx = CENTERED_COORD(ui->curx);
                ui->clicky = CENTERED_COORD(ui->cury);
-               return "";
+               return UI_UPDATE;
            } else release = TRUE;
        } else if (button == CURSOR_SELECT2 && ui->ndragcoords >= 0) {
            ui->ndragcoords = -1;
-           return "";
+           return UI_UPDATE;
        }
      }
  
      if (button == 27 || button == '\b') {
          ui->ndragcoords = -1;
-         return "";
+         return UI_UPDATE;
      }
  
      if (release) {
  
              ui->ndragcoords = -1;
  
-             return buf ? buf : "";
+             return buf ? buf : UI_UPDATE;
          } else if (ui->ndragcoords == 0) {
              /* Click (or tiny drag). Work out which edge we were
               * closest to. */
              cx = CENTERED_COORD(gx);
              cy = CENTERED_COORD(gy);
  
-             if (!INGRID(state, gx, gy)) return "";
+             if (!INGRID(state, gx, gy)) return UI_UPDATE;
  
              if (max(abs(x-cx),abs(y-cy)) < TILE_SIZE/4) {
                  /* TODO closer to centre of grid: process as a cell click not an edge click. */
  
-                 return "";
+                 return UI_UPDATE;
              } else {
                int direction;
                  if (abs(x-cx) < abs(y-cy)) {
@@@ -2714,7 -2707,8 +2709,8 @@@ int main(int argc, const char *argv[]
      game_params *p = NULL;
      random_state *rs = NULL;
      time_t seed = time(NULL);
-     char *id = NULL, *err;
+     char *id = NULL;
+     const char *err;
  
      setvbuf(stdout, NULL, _IONBF, 0);
  
diff --combined slant.c
index e45a2ea851c46f0fdd31641578f1b9ff0b19361c,3fd66115b15f36aeeaedb30a9c8950c0f0575ea7..c9b6b40ac65b764620205a5529bce6f4b9c0e528
+++ b/slant.c
@@@ -184,24 -184,20 +184,20 @@@ static config_item *game_configure(cons
      ret[0].name = "Width";
      ret[0].type = C_STRING;
      sprintf(buf, "%d", params->w);
-     ret[0].sval = dupstr(buf);
-     ret[0].ival = 0;
+     ret[0].u.string.sval = dupstr(buf);
  
      ret[1].name = "Height";
      ret[1].type = C_STRING;
      sprintf(buf, "%d", params->h);
-     ret[1].sval = dupstr(buf);
-     ret[1].ival = 0;
+     ret[1].u.string.sval = dupstr(buf);
  
      ret[2].name = "Difficulty";
      ret[2].type = C_CHOICES;
-     ret[2].sval = DIFFCONFIG;
-     ret[2].ival = params->diff;
+     ret[2].u.choices.choicenames = DIFFCONFIG;
+     ret[2].u.choices.selected = params->diff;
  
      ret[3].name = NULL;
      ret[3].type = C_END;
-     ret[3].sval = NULL;
-     ret[3].ival = 0;
  
      return ret;
  }
@@@ -210,14 -206,14 +206,14 @@@ static game_params *custom_params(cons
  {
      game_params *ret = snew(game_params);
  
-     ret->w = atoi(cfg[0].sval);
-     ret->h = atoi(cfg[1].sval);
-     ret->diff = cfg[2].ival;
+     ret->w = atoi(cfg[0].u.string.sval);
+     ret->h = atoi(cfg[1].u.string.sval);
+     ret->diff = cfg[2].u.choices.selected;
  
      return ret;
  }
  
- static char *validate_params(const game_params *params, int full)
+ static const char *validate_params(const game_params *params, int full)
  {
      /*
       * (At least at the time of writing this comment) The grid
@@@ -417,7 -413,7 +413,7 @@@ static void fill_square(int w, int h, i
  }
  
  static int vbitmap_clear(int w, int h, struct solver_scratch *sc,
-                          int x, int y, int vbits, char *reason, ...)
+                          int x, int y, int vbits, const char *reason, ...)
  {
      int done_something = FALSE;
      int vbit;
@@@ -738,7 -734,7 +734,7 @@@ static int slant_solve(int w, int h, co
                int fs, bs, v;
                int c1, c2;
  #ifdef SOLVER_DIAGNOSTICS
-               char *reason = "<internal error>";
+               const char *reason = "<internal error>";
  #endif
  
                if (soln[y*w+x])
@@@ -1216,7 -1212,7 +1212,7 @@@ static char *new_game_desc(const game_p
      return desc;
  }
  
- static char *validate_desc(const game_params *params, const char *desc)
+ static const char *validate_desc(const game_params *params, const char *desc)
  {
      int w = params->w, h = params->h, W = w+1, H = h+1;
      int area = W*H;
@@@ -1460,7 -1456,7 +1456,7 @@@ static int check_completion(game_state 
  }
  
  static char *solve_game(const game_state *state, const game_state *currstate,
-                         const char *aux, char **error)
+                         const char *aux, const char **error)
  {
      int w = state->p.w, h = state->p.h;
      signed char *soln;
@@@ -1683,7 -1679,7 +1679,7 @@@ static char *interpret_move(const game_
      } else if (IS_CURSOR_SELECT(button)) {
          if (!ui->cur_visible) {
              ui->cur_visible = 1;
-             return "";
+             return UI_UPDATE;
          }
          x = ui->cur_x;
          y = ui->cur_y;
      } else if (IS_CURSOR_MOVE(button)) {
          move_cursor(button, &ui->cur_x, &ui->cur_y, w, h, 0);
          ui->cur_visible = 1;
-         return "";
+         return UI_UPDATE;
      } else if (button == '\\' || button == '\b' || button == '/') {
        int x = ui->cur_x, y = ui->cur_y;
        if (button == ("\\" "\b" "/")[state->soln[y*w + x] + 1]) return NULL;
@@@ -1793,7 -1789,11 +1789,7 @@@ static float *game_colours(frontend *fe
      float *ret = snewn(3 * NCOLOURS, float);
  
      /* CURSOR colour is a background highlight. */
 -    game_mkhighlight(fe, ret, COL_BACKGROUND, COL_CURSOR, -1);
 -
 -    ret[COL_FILLEDSQUARE * 3 + 0] = ret[COL_BACKGROUND * 3 + 0];
 -    ret[COL_FILLEDSQUARE * 3 + 1] = ret[COL_BACKGROUND * 3 + 1];
 -    ret[COL_FILLEDSQUARE * 3 + 2] = ret[COL_BACKGROUND * 3 + 2];
 +    game_mkhighlight(fe, ret, COL_BACKGROUND, COL_CURSOR, COL_FILLEDSQUARE);
  
      ret[COL_GRID * 3 + 0] = ret[COL_BACKGROUND * 3 + 0] * 0.7F;
      ret[COL_GRID * 3 + 1] = ret[COL_BACKGROUND * 3 + 1] * 0.7F;
@@@ -2189,7 -2189,8 +2185,8 @@@ int main(int argc, char **argv
  {
      game_params *p;
      game_state *s;
-     char *id = NULL, *desc, *err;
+     char *id = NULL, *desc;
+     const char *err;
      int grade = FALSE;
      int ret, diff, really_verbose = FALSE;
      struct solver_scratch *sc;
diff --combined towers.c
index 656b8ad338532be40e82a919812a9bd719ee7ee0,9ccc6ae1857707ddcba795a30d8b3024c73b97de..3862639c73c626224f8c9bc32beb35c2603e50df
+++ b/towers.c
@@@ -212,18 -212,15 +212,15 @@@ static config_item *game_configure(cons
      ret[0].name = "Grid size";
      ret[0].type = C_STRING;
      sprintf(buf, "%d", params->w);
-     ret[0].sval = dupstr(buf);
-     ret[0].ival = 0;
+     ret[0].u.string.sval = dupstr(buf);
  
      ret[1].name = "Difficulty";
      ret[1].type = C_CHOICES;
-     ret[1].sval = DIFFCONFIG;
-     ret[1].ival = params->diff;
+     ret[1].u.choices.choicenames = DIFFCONFIG;
+     ret[1].u.choices.selected = params->diff;
  
      ret[2].name = NULL;
      ret[2].type = C_END;
-     ret[2].sval = NULL;
-     ret[2].ival = 0;
  
      return ret;
  }
@@@ -232,13 -229,13 +229,13 @@@ static game_params *custom_params(cons
  {
      game_params *ret = snew(game_params);
  
-     ret->w = atoi(cfg[0].sval);
-     ret->diff = cfg[1].ival;
+     ret->w = atoi(cfg[0].u.string.sval);
+     ret->diff = cfg[1].u.choices.selected;
  
      return ret;
  }
  
- static char *validate_params(const game_params *params, int full)
+ static const char *validate_params(const game_params *params, int full)
  {
      if (params->w < 3 || params->w > 9)
          return "Grid size must be between 3 and 9";
@@@ -388,12 -385,12 +385,12 @@@ static int solver_easy(struct latin_sol
            return ret;
  
  #ifdef STANDALONE_SOLVER
 -          if (solver_show_working)
 -              sprintf(prefix, "%*slower bounds for clue %s %d:\n",
 -                      solver_recurse_depth*4, "",
 -                      cluepos[c/w], c%w+1);
 -          else
 -              prefix[0] = '\0';              /* placate optimiser */
 +      if (solver_show_working)
 +          sprintf(prefix, "%*slower bounds for clue %s %d:\n",
 +                  solver_recurse_depth*4, "",
 +                  cluepos[c/w], c%w+1);
 +      else
 +          prefix[0] = '\0';          /* placate optimiser */
  #endif
  
        i = 0;
@@@ -802,7 -799,7 +799,7 @@@ don
   * Gameplay.
   */
  
- static char *validate_desc(const game_params *params, const char *desc)
+ static const char *validate_desc(const game_params *params, const char *desc)
  {
      int w = params->w, a = w*w;
      const char *p = desc;
@@@ -970,7 -967,7 +967,7 @@@ static void free_game(game_state *state
  }
  
  static char *solve_game(const game_state *state, const game_state *currstate,
-                         const char *aux, char **error)
+                         const char *aux, const char **error)
  {
      int w = state->par.w, a = w*w;
      int i, ret;
@@@ -1349,7 -1346,7 +1346,7 @@@ static char *interpret_move(const game_
                  ui->hpencil = 0;
              }
              ui->hcursor = 0;
-             return "";                       /* UI activity occurred */
+             return UI_UPDATE;
          }
          if (button == RIGHT_BUTTON) {
              /*
                  ui->hshow = 0;
              }
              ui->hcursor = 0;
-             return "";                       /* UI activity occurred */
+             return UI_UPDATE;
          }
      } else if (button == LEFT_BUTTON) {
          if (is_clue(state, tx, ty)) {
          }
          move_cursor(button, &ui->hx, &ui->hy, w, w, 0);
          ui->hshow = ui->hcursor = 1;
-         return "";
+         return UI_UPDATE;
      }
      if (ui->hshow &&
          (button == CURSOR_SELECT)) {
          ui->hpencil = 1 - ui->hpencil;
          ui->hcursor = 1;
-         return "";
+         return UI_UPDATE;
      }
  
      if (ui->hshow &&
@@@ -2021,7 -2018,8 +2018,8 @@@ int main(int argc, char **argv
  {
      game_params *p;
      game_state *s;
-     char *id = NULL, *desc, *err;
+     char *id = NULL, *desc;
+     const char *err;
      int grade = FALSE;
      int ret, diff, really_show_working = FALSE;