/* demo-Gtk-conf.c --- implements the dynamic configuration dialogs.
 * xscreensaver, Copyright © 2001-2022 Jamie Zawinski <jwz@jwz.org>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or 
 * implied warranty.
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#ifdef HAVE_GTK   /* whole file */

#include <xscreensaver-intl.h>

#include <stdlib.h>

#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif

#include <stdio.h>
#include <string.h>
#include <ctype.h>

#include <libxml/parser.h> 

#if (__GNUC__ >= 4)	/* Ignore useless warnings generated by gtk.h */
# undef inline
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wstrict-prototypes"
# pragma GCC diagnostic ignored "-Wlong-long"
# pragma GCC diagnostic ignored "-Wvariadic-macros"
# pragma GCC diagnostic ignored "-Wpedantic"
# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif

#include <gtk/gtk.h>

#if (__GNUC__ >= 4)
# pragma GCC diagnostic pop
#endif

#include "blurb.h"
#include "demo-Gtk-conf.h"

/* Deal with deprecation of direct access to struct fields on the way to GTK3
   See http://live.gnome.org/GnomeGoals/UseGseal
 */
#if GTK_CHECK_VERSION(2,14,0)
# define GET_PARENT(w)          gtk_widget_get_parent (w)
# define GET_ADJ_VALUE(a)       gtk_adjustment_get_value (a)
# define GET_ADJ_UPPER(a)       gtk_adjustment_get_upper (a)
# define GET_ADJ_LOWER(a)       gtk_adjustment_get_lower (a)
#else
# define GET_PARENT(w)          ((w)->parent)
# define GET_ADJ_VALUE(a)       ((a)->value)
# define GET_ADJ_UPPER(a)       ((a)->upper)
# define GET_ADJ_LOWER(a)       ((a)->lower)
#endif


static gboolean debug_p = FALSE;  /* Copied from s->debug_p in parent */


#define MIN_SLIDER_WIDTH     150
#define MIN_SPINBUTTON_WIDTH  48
#define MIN_LABEL_WIDTH       70


typedef enum {
  COMMAND,
  FAKE,
  DESCRIPTION,
  FAKEPREVIEW,
  STRING,
  FILENAME,
  SLIDER,
  SPINBUTTON,
  BOOLEAN,
  SELECT,
  SELECT_OPTION
} parameter_type;


typedef enum {
  NONE, INVERT, RATIO
} convert_t;


typedef struct {

  parameter_type type;

  xmlChar *id;		/* widget name */
  xmlChar *label;	/* heading label, or null */

  /* command, fake, description, fakepreview, string, file
   */
  xmlChar *string;	/* file name, description, whatever. */

  /* slider, spinbutton
   */
  xmlChar *low_label;	/* label for the left side */
  xmlChar *high_label;	/* label for the right side */
  float low;		/* minimum value */
  float high;		/* maximum value */
  float value;		/* default value */
  gboolean integer_p;	/* whether the range is integral, or real */
  xmlChar *arg;		/* command-line option to set (substitute "%") */
  convert_t convert;	/* invert: whether to flip the value and pretend the
                           range goes from hi-low instead of low-hi.
                           ratio: position 1.0 at the center. */

  /* boolean, select-option
   */
  xmlChar *arg_set;	/* command-line option to set for "yes", or null */
  xmlChar *arg_unset;	/* command-line option to set for "no", or null */

  /* select
   */
  GList *options;

  /* select_option
   */
  GtkWidget *widget;

} parameter;


static parameter *make_select_option (const char *file, xmlNodePtr);
static void make_parameter_widget (const char *filename, 
                                   parameter *, GtkWidget *,
                                   void (*changed_cb) (GtkWidget *, gpointer),
                                   gpointer changed_data);
static void browse_button_cb (GtkButton *button, gpointer user_data);


/* Frees the parameter object and all strings and sub-parameters.
   Does not destroy the widget, if any.
 */
static void
free_parameter (parameter *p)
{
  GList *rest;
  if (p->id)         free (p->id);
  if (p->label)      free (p->label);
  if (p->string)     free (p->string);
  if (p->low_label)  free (p->low_label);
  if (p->high_label) free (p->high_label);
  if (p->arg)        free (p->arg);
  if (p->arg_set)    free (p->arg_set);
  if (p->arg_unset)  free (p->arg_unset);

  for (rest = p->options; rest; rest = rest->next)
    if (rest->data)
      free_parameter ((parameter *) rest->data);

  memset (p, ~0, sizeof(*p));
  free (p);
}


/* Debugging: dumps out a `parameter' structure.
 */
#if 0
void
describe_parameter (FILE *out, parameter *p)
{
  fprintf (out, "<");
  switch (p->type)
    {
    case COMMAND:     fprintf (out, "command");      break;
    case FAKE:        fprintf (out, "fake");         break;
    case DESCRIPTION: fprintf (out, "_description"); break;
    case FAKEPREVIEW: fprintf (out, "fakepreview");  break;
    case STRING:      fprintf (out, "string");       break;
    case FILENAME:    fprintf (out, "filename");     break;
    case SLIDER:      fprintf (out, "number type=\"slider\"");     break;
    case SPINBUTTON:  fprintf (out, "number type=\"spinbutton\""); break;
    case BOOLEAN:     fprintf (out, "boolean");      break;
    case SELECT:      fprintf (out, "select");       break;
    default: abort(); break;
    }
  if (p->id)         fprintf (out, " id=\"%s\"",            p->id);
  if (p->label)      fprintf (out, " _label=\"%s\"",        p->label);
  if (p->string && p->type != DESCRIPTION)
                     fprintf (out, " string=\"%s\"",        p->string);
  if (p->low_label)  fprintf (out, " _low-label=\"%s\"",    p->low_label);
  if (p->high_label) fprintf (out, " _high-label=\"%s\"",   p->high_label);
  if (p->low)        fprintf (out, " low=\"%.2f\"",         p->low);
  if (p->high)       fprintf (out, " high=\"%.2f\"",        p->high);
  if (p->value)      fprintf (out, " default=\"%.2f\"",     p->value);
  if (p->arg)        fprintf (out, " arg=\"%s\"",           p->arg);

  switch (p->convert) {
  case NONE: break;
  case INVERT: fprintf (out, " convert=\"invert\""); break;
  case RATIO:  fprintf (out, " convert=\"ratio\"");  break;
  default: abort(); break;
  }

  if (p->arg_set)    fprintf (out, " arg-set=\"%s\"",       p->arg_set);
  if (p->arg_unset)  fprintf (out, " arg-unset=\"%s\"",     p->arg_unset);
  fprintf (out, ">\n");

  if (p->type == SELECT)
    {
      GList *opt;
      for (opt = p->options; opt; opt = opt->next)
        {
          parameter *o = (parameter *) opt->data;
          if (o->type != SELECT_OPTION) abort();
          fprintf (out, "  <option");
          if (o->id)        fprintf (out, " id=\"%s\"",        o->id);
          if (o->label)     fprintf (out, " _label=\"%s\"",    o->label);
          if (o->arg_set)   fprintf (out, " arg-set=\"%s\"",   o->arg_set);
          if (o->arg_unset) fprintf (out, " arg-unset=\"%s\"", o->arg_unset);
          fprintf (out, ">\n");
        }
      fprintf (out, "</select>\n");
    }
  else if (p->type == DESCRIPTION)
    {
      if (p->string)
        fprintf (out, "  %s\n", p->string);
      fprintf (out, "</_description>\n");
    }
}
#endif /* 0 */


/* Like xmlGetProp() but parses a float out of the string.
   If the number was expressed as a float and not an integer
   (that is, the string contained a decimal point) then
   `floatp' is set to TRUE.  Otherwise, it is unchanged.
 */
static float
xml_get_float (xmlNodePtr node, const xmlChar *name, gboolean *floatpP)
{
  const char *s = (char *) xmlGetProp (node, name);
  float f;
  char c;
  if (!s || 1 != sscanf (s, "%f %c", &f, &c))
    return 0;
  else
    {
      if (strchr (s, '.')) *floatpP = TRUE;
      return f;
    }
}


static void sanity_check_parameter (const char *filename,
                                    const xmlChar *node_name,
                                    parameter *p);
static void sanity_check_text_node (const char *filename,
                                    const xmlNodePtr node);
static void sanity_check_menu_options (const char *filename,
                                       const xmlChar *node_name,
                                       parameter *p);

/* Allocates and returns a new `parameter' object based on the
   properties in the given XML node.  Returns 0 if there's nothing
   to create (comment, or unknown tag.)
 */
static parameter *
make_parameter (const char *filename, xmlNodePtr node)
{
  parameter *p;
  const char *name = (char *) node->name;
  const char *convert;
  gboolean floatp = FALSE;

  if (node->type == XML_COMMENT_NODE)
    return 0;

  p = calloc (1, sizeof(*p));

  if (!name) abort();
  else if (!strcmp (name, "command"))      p->type = COMMAND;
  else if (!strcmp (name, "fullcommand"))  p->type = COMMAND;
  else if (!strcmp (name, "_description")) p->type = DESCRIPTION;
  else if (!strcmp (name, "fakepreview"))  p->type = FAKEPREVIEW;
  else if (!strcmp (name, "fake"))         p->type = FAKE;
  else if (!strcmp (name, "boolean"))      p->type = BOOLEAN;
  else if (!strcmp (name, "string"))       p->type = STRING;
  else if (!strcmp (name, "file"))         p->type = FILENAME;
  else if (!strcmp (name, "number"))       p->type = SPINBUTTON;
  else if (!strcmp (name, "select"))       p->type = SELECT;

  else if (!strcmp (name, "xscreensaver-text") ||   /* ignored in X11; */
           !strcmp (name, "xscreensaver-image") ||  /* used in Cocoa. */
           !strcmp (name, "xscreensaver-updater") ||
           !strcmp (name, "video"))
    {
      free (p);
      return 0;
    }
  else if (node->type == XML_TEXT_NODE)
    {
      sanity_check_text_node (filename, node);
      free (p);
      return 0;
    }
  else
    {
      if (debug_p)
        fprintf (stderr, "%s: WARNING: %s: unknown tag: \"%s\"\n",
                 blurb(), filename, name);
      free (p);
      return 0;
    }

  if (p->type == SPINBUTTON)
    {
      const char *type = (char *) xmlGetProp (node, (xmlChar *) "type");
      if (!type || !strcmp (type, "spinbutton")) p->type = SPINBUTTON;
      else if (!strcmp (type, "slider"))         p->type = SLIDER;
      else
        {
          if (debug_p)
            fprintf (stderr, "%s: WARNING: %s: unknown %s type: \"%s\"\n",
                     blurb(), filename, name, type);
          free (p);
          return 0;
        }
    }
  else if (p->type == DESCRIPTION)
    {
      if (node->xmlChildrenNode &&
          node->xmlChildrenNode->type == XML_TEXT_NODE &&
          !node->xmlChildrenNode->next)
        p->string = (xmlChar *)
          strdup ((char *) node->xmlChildrenNode->content);
    }

  p->id         = xmlGetProp (node, (xmlChar *) "id");
  p->label      = xmlGetProp (node, (xmlChar *) "_label");
  p->low_label  = xmlGetProp (node, (xmlChar *) "_low-label");
  p->high_label = xmlGetProp (node, (xmlChar *) "_high-label");
  p->low        = xml_get_float (node, (xmlChar *) "low",     &floatp);
  p->high       = xml_get_float (node, (xmlChar *) "high",    &floatp);
  p->value      = xml_get_float (node, (xmlChar *) "default", &floatp);
  p->integer_p  = !floatp;
  convert       = (char *) xmlGetProp (node, (xmlChar *) "convert");

  if      (convert && !strcmp (convert, "invert")) p->convert = INVERT;
  else if (convert && !strcmp (convert, "ratio"))  p->convert = RATIO;
  else p->convert = NONE;

  p->arg        = xmlGetProp (node, (xmlChar *) "arg");
  p->arg_set    = xmlGetProp (node, (xmlChar *) "arg-set");
  p->arg_unset  = xmlGetProp (node, (xmlChar *) "arg-unset");

  /* Check for missing decimal point */
  if (debug_p &&
      p->integer_p &&
      (p->high != p->low) &&
      (p->high - p->low) <= 1)
    fprintf (stderr,
            "%s: WARNING: %s: %s: range [%.1f, %.1f] shouldn't be integral!\n",
             blurb(), filename, p->id,
             p->low, p->high);

  if (p->type == SELECT)
    {
      xmlNodePtr kids;
      for (kids = node->xmlChildrenNode; kids; kids = kids->next)
        {
          parameter *s = make_select_option (filename, kids);
          if (s)
            p->options = g_list_append (p->options, s);
        }
    }

  sanity_check_parameter (filename, (const xmlChar *) name, p);

  return p;
}


/* Allocates and returns a new SELECT_OPTION `parameter' object based
   on the properties in the given XML node.  Returns 0 if there's nothing
   to create (comment, or unknown tag.)
 */
static parameter *
make_select_option (const char *filename, xmlNodePtr node)
{
  if (node->type == XML_COMMENT_NODE)
    return 0;
  else if (node->type == XML_TEXT_NODE)
    {
      sanity_check_text_node (filename, node);
      return 0;
    }
  else if (node->type != XML_ELEMENT_NODE)
    {
      if (debug_p)
        fprintf (stderr,
                 "%s: WARNING: %s: %s: unexpected child tag type %d\n",
                 blurb(), filename, node->name, (int)node->type);
      return 0;
    }
  else if (strcmp ((char *) node->name, "option"))
    {
      if (debug_p)
        fprintf (stderr,
                 "%s: WARNING: %s: %s: child not an option tag: \"%s\"\n",
                 blurb(), filename, node->name, node->name);
      return 0;
    }
  else
    {
      parameter *s = calloc (1, sizeof(*s));

      s->type       = SELECT_OPTION;
      s->id         = xmlGetProp (node, (xmlChar *) "id");
      s->label      = xmlGetProp (node, (xmlChar *) "_label");
      s->arg_set    = xmlGetProp (node, (xmlChar *) "arg-set");
      s->arg_unset  = xmlGetProp (node, (xmlChar *) "arg-unset");

      sanity_check_parameter (filename, node->name, s);
      return s;
    }
}


/* Rudimentary check to make sure someone hasn't typed "arg-set="
   when they should have typed "arg=", etc.
 */
static void
sanity_check_parameter (const char *filename, const xmlChar *node_name,
                        parameter *p)
{
  struct {
    gboolean id;
    gboolean label;
    gboolean string;
    gboolean low_label;
    gboolean high_label;
    gboolean low;
    gboolean high;
    gboolean value;
    gboolean arg;
    gboolean convert;
    gboolean arg_set;
    gboolean arg_unset;
  } allowed, require;

  memset (&allowed, 0, sizeof (allowed));
  memset (&require, 0, sizeof (require));

  switch (p->type)
    {
    case COMMAND:
      allowed.arg = TRUE;
      require.arg = TRUE;
      break;
    case FAKE:
      break;
    case DESCRIPTION:
      allowed.string = TRUE;
      break;
    case FAKEPREVIEW:
      break;
    case STRING:
      allowed.id = TRUE;
      require.id = TRUE;
      allowed.label = TRUE;
      require.label = TRUE;
      allowed.arg = TRUE;
      require.arg = TRUE;
      break;
    case FILENAME:
      allowed.id = TRUE;
      require.id = TRUE;
      allowed.label = TRUE;
      allowed.arg = TRUE;
      require.arg = TRUE;
      break;
    case SLIDER:
      allowed.id = TRUE;
      require.id = TRUE;
      allowed.label = TRUE;
      allowed.low_label = TRUE;
      allowed.high_label = TRUE;
      allowed.arg = TRUE;
      require.arg = TRUE;
      allowed.low = TRUE;
      /* require.low = TRUE; -- may be 0 */
      allowed.high = TRUE;
      /* require.high = TRUE; -- may be 0 */
      allowed.value = TRUE;
      /* require.value = TRUE; -- may be 0 */
      allowed.convert = TRUE;
      break;
    case SPINBUTTON:
      allowed.id = TRUE;
      require.id = TRUE;
      allowed.label = TRUE;
      allowed.arg = TRUE;
      require.arg = TRUE;
      allowed.low = TRUE;
      /* require.low = TRUE; -- may be 0 */
      allowed.high = TRUE;
      /* require.high = TRUE; -- may be 0 */
      allowed.value = TRUE;
      /* require.value = TRUE; -- may be 0 */
      allowed.convert = FALSE;
      break;
    case BOOLEAN:
      allowed.id = TRUE;
      require.id = TRUE;
      allowed.label = TRUE;
      allowed.arg_set = TRUE;
      allowed.arg_unset = TRUE;
      break;
    case SELECT:
      allowed.id = TRUE;
      require.id = TRUE;
      break;
    case SELECT_OPTION:
      allowed.id = TRUE;
      allowed.label = TRUE;
      require.label = TRUE;
      allowed.arg_set = TRUE;
      break;
    default:
      abort();
      break;
    }

# define WARN(STR) \
   fprintf (stderr, "%s: %s: " STR " in <%s%s id=\"%s\">\n", \
              blurb(), filename, node_name, \
              (!strcmp((char *) node_name, "number") \
               ? (p->type == SPINBUTTON ? " type=spinbutton" : " type=slider")\
               : ""), \
              (p->id ? (char *) p->id : ""))
# define CHECK(SLOT,NAME) \
   if (p->SLOT && !allowed.SLOT) \
     WARN ("\"" NAME "\" is not a valid option"); \
   if (!p->SLOT && require.SLOT) \
     WARN ("\"" NAME "\" is required")

  CHECK (id,         "id");
  CHECK (label,      "_label");
  CHECK (string,     "(body text)");
  CHECK (low_label,  "_low-label");
  CHECK (high_label, "_high-label");
  CHECK (low,        "low");
  CHECK (high,       "high");
  CHECK (value,      "default");
  CHECK (arg,        "arg");
  CHECK (convert,    "convert");
  CHECK (arg_set,    "arg-set");
  CHECK (arg_unset,  "arg-unset");

  if (p->convert == RATIO)
    {
      if (p->low  <= 0) WARN ("low must be > 0");
      if (p->high <= 1) WARN ("high must be > 1");
    }
# undef CHECK
# undef WARN

  if (p->type == SELECT)
    sanity_check_menu_options (filename, node_name, p);
}


static void
sanity_check_menu_options (const char *filename, const xmlChar *node_name,
                           parameter *p)
{
  GList *opts;
  int nulls = 0;
  char *prefix = 0;

  for (opts = p->options; opts; opts = opts->next)
    {
      parameter *s = (parameter *) opts->data;
      if (!s->arg_set) nulls++;

      if (s->arg_set)
        {
          char *a = strdup ((char *) s->arg_set);
          char *spc = strchr (a, ' ');
          if (spc) *spc = 0;
          if (prefix)
            {
# if 0
              if (strcmp (a, prefix))
                fprintf (stderr,
                      "%s: %s: both \"%s\" and \"%s\" used in <select id=\"%s\">\n",
                         blurb(), filename, prefix, a, p->id);
# endif
              free (prefix);
            }
          prefix = a;
        }

/*      fprintf (stderr, "\n   %s\n", s->arg_set);*/
    }

  if (prefix) free (prefix);
  prefix = 0;
  if (nulls > 1)
    fprintf (stderr, 
             "%s: %s: more than one menu with no arg-set in <select id=\"%s\">\n",
             blurb(), filename, p->id);
}


/* "text" nodes show up for all the non-tag text in the file, including
   all the newlines between tags.  Warn if there is text there that
   is not whitespace.
 */
static void
sanity_check_text_node (const char *filename, const xmlNodePtr node)
{
  const char *body = (const char *) node->content;
  if (node->type != XML_TEXT_NODE) abort();
  while (isspace (*body)) body++;
  if (*body)
    fprintf (stderr, "%s: WARNING: %s: random text present: \"%s\"\n",
             blurb(), filename, body);
}


/* Returns a list of strings, every switch mentioned in the parameters.
   The strings must be freed.
 */
static GList *
get_all_switches (const char *filename, GList *parms)
{
  GList *switches = 0;
  GList *p;
  for (p = parms; p; p = p->next)
    {
      parameter *pp = (parameter *) p->data;

      if (pp->type == SELECT)
        {
          GList *list2 = get_all_switches (filename, pp->options);
          switches = g_list_concat (switches, list2);
        }
      if (pp->arg && *pp->arg)
        switches = g_list_append (switches, strdup ((char *) pp->arg));
      if (pp->arg_set && *pp->arg_set)
        switches = g_list_append (switches, strdup ((char *) pp->arg_set));
      if (pp->arg_unset && *pp->arg_unset)
        switches = g_list_append (switches, strdup ((char *) pp->arg_unset));
    }
  return switches;
}


/* Ensures that no switch is mentioned more than once.
 */
static void
sanity_check_parameters (const char *filename, GList *parms)
{
  GList *list = get_all_switches (filename, parms);
  GList *p;
  for (p = list; p; p = p->next)
    {
      char *sw = (char *) p->data;
      GList *p2;

      if (*sw != '-' && *sw != '+')
        fprintf (stderr, "%s: %s: switch does not begin with hyphen \"%s\"\n",
                 blurb(), filename, sw);

      for (p2 = p->next; p2; p2 = p2->next)
        {
          const char *sw2 = (const char *) p2->data;
          if (!strcmp (sw, sw2))
            fprintf (stderr, "%s: %s: duplicate switch \"%s\"\n",
                     blurb(), filename, sw);
        }

      free (sw);
    }
  g_list_free (list);
}


/* Helper for make_parameters()
 */
static GList *
make_parameters_1 (const char *filename, xmlNodePtr node, GtkWidget *parent,
                   void (*changed_cb) (GtkWidget *, gpointer),
                   gpointer changed_data)
{
  GList *list = 0;

  for (; node; node = node->next)
    {
      const char *name = (char *) node->name;
      if (!strcmp (name, "hgroup") ||
          !strcmp (name, "vgroup"))
        {
          GtkWidget *box = gtk_box_new (*name == 'h'
                                        ? GTK_ORIENTATION_HORIZONTAL
                                        : GTK_ORIENTATION_VERTICAL,
                                        0);
          GList *list2;
          gtk_widget_show (box);
          gtk_box_pack_start (GTK_BOX (parent), box, FALSE, FALSE, 0);

          list2 = make_parameters_1 (filename, node->xmlChildrenNode, box,
                                     changed_cb, changed_data);
          if (list2)
            list = g_list_concat (list, list2);
        }
      else
        {
          parameter *p = make_parameter (filename, node);
          if (p)
            {
              list = g_list_append (list, p);
              make_parameter_widget (filename, p, parent,
                                     changed_cb, changed_data);
            }
        }
    }
  return list;
}


/* Calls make_parameter() and make_parameter_widget() on each relevant
   tag in the XML tree.  Also handles the "hgroup" and "vgroup" flags.
   Returns a GList of `parameter' objects.
 */
static GList *
make_parameters (const char *filename, xmlNodePtr node, GtkWidget *parent,
                 void (*changed_cb) (GtkWidget *, gpointer),
                 gpointer changed_data)
{
  for (; node; node = node->next)
    {
      if (node->type == XML_ELEMENT_NODE &&
          !strcmp ((char *) node->name, "screensaver"))
        return make_parameters_1 (filename, node->xmlChildrenNode, parent,
                                  changed_cb, changed_data);
    }
  return 0;
}


static gfloat
invert_range (gfloat low, gfloat high, gfloat value)
{
  gfloat range = high-low;
  gfloat off = value-low;
  return (low + (range - off));
}

/* In: 0-1; Out: low-high. */
static gfloat
ratio_to_range (gfloat low, gfloat high, gfloat ratio)
{
  return (ratio > 0.5
          ? (1   + (2 * (ratio - 0.5) * (high - 1)))
          : (low + (2 * ratio         * (1 - low))));
}

/* In: low-high; Out: 0-1. */
static gfloat
range_to_ratio (gfloat low, gfloat high, gfloat value)
{
  return (value > 1
          ? ((value - 1)   / (2 * (high - 1))) + 0.5
          : ((value - low) / (2 * (1 - low))));
}


static GtkAdjustment *
make_adjustment (const char *filename, parameter *p)
{
  gfloat low = p->low;
  gfloat high = p->high;
  float range = (high - low);
  float value = (p->convert == INVERT
                 ? invert_range (low, high, p->value)
                 : p->value);
  gfloat si = (high - low) / 100;
  gfloat pi = (high - low) / 10;
  gfloat page_size = ((p->type == SLIDER) ? 1 : 0);

  if (p->value < low || p->value > high)
    {
      if (debug_p && p->integer_p)
        fprintf (stderr, "%s: WARNING: %s: %d is not in range [%d, %d]\n",
                 blurb(), filename,
                 (int) p->value, (int) low, (int) high);
      else if (debug_p)
        fprintf (stderr,
                 "%s: WARNING: %s: %.2f is not in range [%.2f, %.2f]\n",
                 blurb(), filename, p->value, low, high);
      value = (value < low ? low : high);
    }
#if 0
  else if (debug_p && p->value < 1000 && high >= 10000)
    {
      if (p->integer_p)
        fprintf (stderr,
                 "%s: WARNING: %s: %d is suspicious for range [%d, %d]\n",
                 blurb(), filename,
                 (int) p->value, (int) low, (int) high);
      else
        fprintf (stderr,
               "%s: WARNING: %s: %.2f is suspicious for range [%.2f, %.2f]\n",
                 blurb(), filename, p->value, low, high);
    }
#endif /* 0 */

  si = (int) (si + 0.5);
  pi = (int) (pi + 0.5);
  if (si < 1) si = 1;
  if (pi < 1) pi = 1;

  if (range <= 500) si = 1;

  /* For simplicity, "ratio" sliders in the UI all run from 0.0 to 1.0. */
  if (p->convert == RATIO)
    {
      value = range_to_ratio (low, high, value);
      low = 0;
      high = 1;
      page_size = 0;
    }

  return GTK_ADJUSTMENT (gtk_adjustment_new (value, low, high + page_size,
                                             si, pi, page_size));
}


static void
set_widget_min_width (GtkWidget *w, int width)
{
  GtkRequisition min, nat;
  gtk_widget_get_preferred_size (w, &min, &nat);
  if (min.width < width)
    gtk_widget_set_size_request (GTK_WIDGET (w), width, -1);
}


/* If we're inside a vbox, we need to put an hbox in it, or labels appear 
   on top instead of to the left, and things stretch to the full width of
   the window.
 */
static GtkWidget *
insert_fake_hbox (GtkWidget *parent)
{
  if (gtk_orientable_get_orientation (GTK_ORIENTABLE (parent)) ==
      GTK_ORIENTATION_VERTICAL)
    {
      GtkWidget *hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
      gtk_box_pack_start (GTK_BOX (parent), hbox, FALSE, FALSE, 4);
      gtk_widget_show (hbox);
      return hbox;
    }
  return parent;
}


static void
link_atk_label_to_widget (GtkWidget *label, GtkWidget *widget)
{
  AtkObject *atk_label = gtk_widget_get_accessible (label);
  AtkObject *atk_widget = gtk_widget_get_accessible (widget);

  atk_object_add_relationship (atk_label, ATK_RELATION_LABEL_FOR,
                               atk_widget);
  atk_object_add_relationship (atk_widget, ATK_RELATION_LABELLED_BY,
                               atk_label);
}

/* Given a `parameter' struct, allocates an appropriate GtkWidget for it,
   and stores it in `p->widget'.
   `parent' must be a GtkBox.
 */
static void
make_parameter_widget (const char *filename, parameter *p, GtkWidget *parent,
                       void (*changed_cb) (GtkWidget *, gpointer data),
                       gpointer changed_data)
{
  const char *label = (char *) p->label;
  if (p->widget) return;

  switch (p->type)
    {
    case STRING:
      {
        GtkWidget *entry = gtk_entry_new ();
        parent = insert_fake_hbox (parent);
        if (label)
          {
            GtkWidget *w = gtk_label_new (_(label));
            link_atk_label_to_widget (w, entry);
            gtk_label_set_justify (GTK_LABEL (w), GTK_JUSTIFY_RIGHT);
            gtk_label_set_xalign (GTK_LABEL (w), 1.0);
            gtk_label_set_yalign (GTK_LABEL (w), 0.5);
            set_widget_min_width (GTK_WIDGET (w), MIN_LABEL_WIDTH);
            gtk_widget_show (w);
            gtk_box_pack_start (GTK_BOX (parent), w, FALSE, FALSE, 4);
          }

        p->widget = entry;
        if (p->string)
          gtk_entry_set_text (GTK_ENTRY (p->widget), (char *) p->string);
        gtk_box_pack_start (GTK_BOX (parent), p->widget, FALSE, FALSE, 4);
        g_signal_connect (p->widget, "changed", G_CALLBACK (changed_cb),
                          changed_data);
        break;
      }
    case FILENAME:
      {
        GtkWidget *L = gtk_label_new (label ? _(label) : "");
        GtkWidget *entry = gtk_entry_new ();
        GtkWidget *button = gtk_button_new_with_label (_("Browse..."));
        link_atk_label_to_widget (L, entry);
        gtk_widget_show (entry);
        gtk_widget_show (button);
        p->widget = entry;

        g_signal_connect (button, "clicked", G_CALLBACK (browse_button_cb),
                          (gpointer) entry);

        gtk_label_set_justify (GTK_LABEL (L), GTK_JUSTIFY_RIGHT);
        gtk_label_set_xalign (GTK_LABEL (L), 1.0);
        gtk_label_set_yalign (GTK_LABEL (L), 0.5);
        set_widget_min_width (GTK_WIDGET (L), MIN_LABEL_WIDTH);
        gtk_widget_show (L);

        if (p->string)
          gtk_entry_set_text (GTK_ENTRY (entry), (char *) p->string);

        parent = insert_fake_hbox (parent);
        gtk_box_pack_start (GTK_BOX (parent), L,      FALSE, FALSE, 4);
        gtk_box_pack_start (GTK_BOX (parent), entry,  TRUE,  TRUE,  4);
        gtk_box_pack_start (GTK_BOX (parent), button, FALSE, FALSE, 4);
        g_signal_connect (p->widget, "changed",
                          G_CALLBACK (changed_cb), changed_data);
        break;
      }
    case SLIDER:
      {
        GtkAdjustment *adj = make_adjustment (filename, p);
        GtkWidget *scale = gtk_scale_new (GTK_ORIENTATION_HORIZONTAL, adj);
        GtkWidget *labelw = 0;

        if (label)
          {
            labelw = gtk_label_new (_(label));
            link_atk_label_to_widget (labelw, scale);
            gtk_label_set_justify (GTK_LABEL (labelw), GTK_JUSTIFY_LEFT);
            gtk_label_set_xalign (GTK_LABEL (labelw), 0.0);
            gtk_label_set_yalign (GTK_LABEL (labelw), 0.5);
            set_widget_min_width (GTK_WIDGET (labelw), MIN_LABEL_WIDTH);
            gtk_widget_show (labelw);
            gtk_box_pack_start (GTK_BOX (parent), labelw, FALSE, FALSE, 2);
          }

        /* Do this after 'labelw' so that it appears above, not to left. */
        parent = insert_fake_hbox (parent);

        if (p->low_label)
          {
            GtkWidget *w = gtk_label_new (_((char *) p->low_label));
            link_atk_label_to_widget (w, scale);
            gtk_label_set_justify (GTK_LABEL (w), GTK_JUSTIFY_RIGHT);
            gtk_label_set_xalign (GTK_LABEL (w), 1.0);
            gtk_label_set_yalign (GTK_LABEL (w), 0.5);
            set_widget_min_width (GTK_WIDGET (w), MIN_LABEL_WIDTH);
            gtk_widget_show (w);
            gtk_box_pack_start (GTK_BOX (parent), w, FALSE, FALSE, 4);
          }

        gtk_scale_set_value_pos (GTK_SCALE (scale), GTK_POS_BOTTOM);
        gtk_scale_set_digits (GTK_SCALE (scale), (p->integer_p ? 0 : 2));
        set_widget_min_width (GTK_WIDGET (scale), MIN_SLIDER_WIDTH);

        /* We can't show the value as it is wrong for ratio and inverted
           sliders. */
        gtk_scale_set_draw_value (GTK_SCALE (scale), FALSE);

        gtk_box_pack_start (GTK_BOX (parent), scale, FALSE, FALSE, 4);

        gtk_widget_show (scale);

        if (p->high_label)
          {
            GtkWidget *w = gtk_label_new (_((char *) p->high_label));
            link_atk_label_to_widget (w, scale);
            gtk_label_set_justify (GTK_LABEL (w), GTK_JUSTIFY_LEFT);
            gtk_label_set_xalign (GTK_LABEL (w), 0.0);
            gtk_label_set_yalign (GTK_LABEL (w), 0.5);
            set_widget_min_width (GTK_WIDGET (w), MIN_LABEL_WIDTH);
            gtk_widget_show (w);
            gtk_box_pack_start (GTK_BOX (parent), w, FALSE, FALSE, 4);
          }

        p->widget = scale;
        g_signal_connect (adj, "value-changed",
                          G_CALLBACK (changed_cb), changed_data);
        break;
      }
    case SPINBUTTON:
      {
        GtkAdjustment *adj = make_adjustment (filename, p);
        GtkWidget *spin = gtk_spin_button_new (adj, 15, 0);
        gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (spin), TRUE);
        gtk_spin_button_set_snap_to_ticks (GTK_SPIN_BUTTON (spin), TRUE);
        gtk_spin_button_set_value (GTK_SPIN_BUTTON (spin), GET_ADJ_VALUE(adj));
        set_widget_min_width (GTK_WIDGET (spin), MIN_SPINBUTTON_WIDTH);

        if (label)
          {
            GtkWidget *w = gtk_label_new (_(label));
            link_atk_label_to_widget (w, spin);
            gtk_label_set_justify (GTK_LABEL (w), GTK_JUSTIFY_RIGHT);
            gtk_label_set_xalign (GTK_LABEL (w), 1.0);
            gtk_label_set_yalign (GTK_LABEL (w), 0.5);
            set_widget_min_width (GTK_WIDGET (w), MIN_LABEL_WIDTH);
            gtk_widget_show (w);
            parent = insert_fake_hbox (parent);
            gtk_box_pack_start (GTK_BOX (parent), w, FALSE, FALSE, 4);
          }

        gtk_widget_show (spin);
        gtk_box_pack_start (GTK_BOX (parent), spin, FALSE, FALSE, 4);

        p->widget = spin;
        g_signal_connect (adj, "value-changed",
                          G_CALLBACK (changed_cb), changed_data);
        break;
      }
    case BOOLEAN:
      {
        p->widget = gtk_check_button_new_with_label (_(label));
        /* Let these stretch -- doesn't hurt. 
           parent = insert_fake_hbox (parent);
         */
        gtk_box_pack_start (GTK_BOX (parent), p->widget, FALSE, FALSE, 4);
        g_signal_connect (p->widget, "clicked",
                          G_CALLBACK (changed_cb), changed_data);
        break;
      }
    case SELECT:
      {
        GtkComboBox *cbox = GTK_COMBO_BOX (gtk_combo_box_new());
        GtkListStore *model = gtk_list_store_new (1, G_TYPE_STRING);
        GtkCellRenderer *view = gtk_cell_renderer_text_new();
        GtkTreeIter iter;
        GList *opts;

        g_object_set (G_OBJECT (cbox), "model", model, NULL);
        g_object_unref (model);
        gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (cbox), view, TRUE);
        gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (cbox), view,
                                        "text", 0, NULL);

        for (opts = p->options; opts; opts = opts->next)
          {
            parameter *s = (parameter *) opts->data;
            const char *name = _((char *) s->label);
            gtk_list_store_append (model, &iter);
            gtk_list_store_set (model, &iter, 0, name, -1);
          }

        p->widget = GTK_WIDGET (cbox);
        parent = insert_fake_hbox (parent);
        gtk_box_pack_start (GTK_BOX (parent), p->widget, FALSE, FALSE, 4);
        g_signal_connect (p->widget, "changed",
                          G_CALLBACK (changed_cb), changed_data);
        break;
      }

    case COMMAND:
    case FAKE:
    case DESCRIPTION:
    case FAKEPREVIEW:
      break;
    default:
      abort();
    }

  if (p->widget)
    {
      gtk_widget_set_name (p->widget, (char *) p->id);
      gtk_widget_show (p->widget);
    }
}


/* "Browse" button: user_data is the corresponding GtkEntry */
static void
browse_button_cb (GtkButton *button, gpointer user_data)
{
  GtkEntry *entry = GTK_ENTRY (user_data);
  GtkWindow *win = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (button)));
  const char *ofile = gtk_entry_get_text (entry);
  char *file = strdup (ofile);

  if (debug_p) fprintf (stderr, "%s: settings browse button\n", blurb());
  if (file_chooser (win, entry, &file, _("Select file."),
                    debug_p, FALSE, FALSE))
    {
      if (debug_p)
        fprintf (stderr, "%s:   file: \"%s\" -> \"%s\n", blurb(), ofile, file);
      gtk_entry_set_text (entry, file);
    }
  free (file);
}


/* Converting to and from command-lines
 */


/* Returns a copy of string that has been quoted according to shell rules:
   it may have been wrapped in "" and had some characters backslashed; or
   it may be unchanged.
 */
static char *
shell_quotify (const char *string)
{
  char *string2 = (char *) malloc ((strlen (string) * 2) + 10);
  const char *in;
  char *out;
  int need_quotes = 0;
  int in_length = 0;

  out = string2;
  *out++ = '"';
  for (in = string; *in; in++)
    {
      in_length++;
      if (*in == '!' ||
          *in == '"' ||
          *in == '$')
        {
          need_quotes = 1;
          *out++ = '\\';
          *out++ = *in;
        }
      else if (*in <= ' ' ||
               *in >= 127 ||
               *in == '\'' ||
               *in == '#' ||
               *in == '%' ||
               *in == '&' ||
               *in == '(' ||
               *in == ')' ||
               *in == '*')
        {
          need_quotes = 1;
          *out++ = *in;
        }
      else
        *out++ = *in;
    }
  *out++ = '"';
  *out = 0;

  if (in_length == 0)
    need_quotes = 1;

  if (need_quotes)
    return (string2);

  free (string2);
  return strdup (string);
}

/* Modify the string in place to remove wrapping double-quotes
   and interior backslashes. 
 */
static void
de_stringify (char *s)
{
  char q = s[0];
  if (q != '\'' && q != '\"' && q != '`')
    abort();
  memmove (s, s+1, strlen (s));
  while (*s && *s != q)
    {
      if (*s == '\\')
        memmove (s, s+1, strlen (s)+1);
      s++;
    }
  if (*s != q) abort();
  *s = 0;
}


/* Substitutes a shell-quotified version of `value' into `p->arg' at
   the place where the `%' character appeared.
 */
static char *
format_switch (parameter *p, const char *value)
{
  char *fmt = (char *) p->arg;
  char *v2;
  char *result, *s;
  if (!fmt || !value) return 0;
  v2 = shell_quotify (value);
  result = (char *) malloc (strlen (fmt) + strlen (v2) + 10);
  s = result;
  for (; *fmt; fmt++)
    if (*fmt != '%')
      *s++ = *fmt;
    else
      {
        strcpy (s, v2);
        s += strlen (s);
      }
  *s = 0;

  free (v2);
  return result;
}


static void
pretty_float (char *out, gfloat val)
{
  char *s1;
  sprintf (out, "%.3f", val);
  s1 = strchr (out, '.');
  if (s1)
    {
      char *s2 = s1 + strlen(s1) - 1;
      while (s2 > s1 && *s2 == '0')	/* lose trailing zeroes */
        *s2-- = 0;
      if (s2 >= s1 && *s2 == '.')	/* lose trailing decimal */
        *s2-- = 0;
    }
}


/* Maps a `parameter' to a command-line switch.
   Returns 0 if it can't, or if the parameter has the default value.
 */
static char *
parameter_to_switch (parameter *p)
{
  switch (p->type)
    {
    case COMMAND:
      if (p->arg)
        return strdup ((char *) p->arg);
      else
        return 0;
      break;
    case STRING:
    case FILENAME:
      if (!p->widget) return 0;
      {
        const char *s = gtk_entry_get_text (GTK_ENTRY (p->widget));
        char *v;
        if (!strcmp ((s ? s : ""),
                     (p->string ? (char *) p->string : "")))
          v = 0;  /* same as default */
        else
          v = format_switch (p, s);

        /* don't free `s' */
        return v;
      }
    case SLIDER:
    case SPINBUTTON:
      if (!p->widget) return 0;
      {
        GtkAdjustment *adj =
          (p->type == SLIDER
           ? gtk_range_get_adjustment (GTK_RANGE (p->widget))
           : gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (p->widget)));
        char buf[255];
        float value;
        switch (p->convert) {
        case INVERT:
          value = invert_range (GET_ADJ_LOWER(adj), GET_ADJ_UPPER(adj),
                                GET_ADJ_VALUE(adj)) - 1;
          break;
        case RATIO:
          value = ratio_to_range (p->low, p->high, GET_ADJ_VALUE(adj));
          break;
        default:
          value = GET_ADJ_VALUE(adj);
          break;
        }

        if (value == p->value)  /* same as default */
          return 0;

        if (p->integer_p)
          sprintf (buf, "%d", (int) (value + (value > 0 ? 0.5 : -0.5)));
        else
          pretty_float (buf, value);

        return format_switch (p, buf);
      }
    case BOOLEAN:
      if (!p->widget) return 0;
      {
        GtkToggleButton *b = GTK_TOGGLE_BUTTON (p->widget);
        const char *s = (gtk_toggle_button_get_active (b)
                         ? (char *) p->arg_set
                         : (char *) p->arg_unset);
        if (s)
          return strdup (s);
        else
          return 0;
      }
    case SELECT:
      if (!p->widget) return 0;
      {
        GtkComboBox *cbox = GTK_COMBO_BOX (p->widget);
        int menu_elt = gtk_combo_box_get_active (cbox);
        GList *ol = (menu_elt >= 0 ? g_list_nth (p->options, menu_elt) : NULL);
        parameter *o = (ol ? (parameter *) ol->data : 0);
        const char *s;
        if (o)
          {
            if (o->type != SELECT_OPTION) abort();
            s = (char *) o->arg_set;
            if (s)
              return strdup (s);
          }
        return 0;
      }
    default:
      if (p->widget)
        abort();
      else
        return 0;
    }
}

/* Maps a GList of `parameter' objects to a complete command-line string.
   All arguments will be properly quoted.
 */
static char *
parameters_to_cmd_line (GList *parms, gboolean default_p)
{
  int L = g_list_length (parms);
  int LL = 0;
  char **strs = (char **) calloc (sizeof (*strs), L);
  char *result;
  char *out;
  int i, j;

  for (i = 0, j = 0; parms; parms = parms->next, i++)
    {
      parameter *p = (parameter *) parms->data;
      if (!default_p || p->type == COMMAND)
        {
          char *s = parameter_to_switch (p);
          strs[j++] = s;
          LL += (s ? strlen(s) : 0) + 1;
        }
    }

  result = (char *) malloc (LL + 10);
  out = result;
  for (i = 0; i < j; i++)
    if (strs[i])
      {
        strcpy (out, strs[i]);
        out += strlen (out);
        *out++ = ' ';
        free (strs[i]);
      }
  *out = 0;
  while (out > result && out[-1] == ' ')  /* strip trailing spaces */
    *(--out) = 0;
  free (strs);

  return result;
}


/* Returns a GList of the tokens the string, using shell syntax;
   Quoted strings are handled as a single token.
 */
static GList *
tokenize_command_line (const char *cmd)
{
  GList *result = 0;
  const char *s = cmd;
  while (*s)
    {
      const char *start;
      char *ss;
      for (; isspace(*s); s++);		/* skip whitespace */

      start = s;
      if (*s == '\'' || *s == '\"' || *s == '`')
        {
          char q = *s;
          s++;
          while (*s && *s != q)		/* skip to matching quote */
            {
              if (*s == '\\' && s[1])	/* allowing backslash quoting */
                s++;
              s++;
            }
          s++;
        }
      else
        {
          while (*s &&
                 (! (isspace(*s) ||
                     *s == '\'' ||
                     *s == '\"' ||
                     *s == '`')))
            s++;
        }

      if (s > start)
        {
          ss = (char *) malloc ((s - start) + 1);
          strncpy (ss, start, s-start);
          ss[s-start] = 0;
          if (*ss == '\'' || *ss == '\"' || *ss == '`')
            de_stringify (ss);
          result = g_list_append (result, ss);
        }
    }

  return result;
}

static void parameter_set_switch (parameter *, gpointer value);
static gboolean parse_command_line_into_parameters_1 (const char *filename,
                                                      GList *parms,
                                                      const char *option,
                                                      const char *value,
                                                      parameter *parent);


/* Parses the command line, and flushes those options down into
   the `parameter' structs in the list.
 */
static void
parse_command_line_into_parameters (const char *filename,
                                    const char *cmd, GList *parms)
{
  GList *tokens = tokenize_command_line (cmd);
  GList *rest;
  for (rest = tokens; rest; rest = rest->next)
    {
      char *option = rest->data;
      rest->data = 0;

      if (option && option[0] != '-' && option[0] != '+')
        {
          if (debug_p)
            fprintf (stderr, "%s: WARNING: %s: not a switch: \"%s\"\n",
                     blurb(), filename, option);
        }
      else
        {
          char *value = 0;

          if (rest->next)   /* pop off the arg to this option */
            {
              char *s = (char *) rest->next->data;
              /* the next token is the next switch if it matches ^-[-a-z]
                 (To avoid losing on "-x -3.1".)
               */
              if (! (s &&
                     s[0] == '-' &&
                     (s[1] == '-' || isalpha(s[1]))))
                {
                  value = s;
                  rest->next->data = 0;
                  rest = rest->next;
                }
            }

          parse_command_line_into_parameters_1 (filename, parms,
                                                option, value, 0);
          if (value) free (value);
          free (option);
        }
    }
  g_list_free (tokens);
}


static gboolean
compare_opts (const char *option, const char *value,
              const char *template)
{
  int ol;
  char *c;

  /* -arg and --arg are the same. */
  if (option[0]   == '-' && option[1]   == '-') option++;
  if (template[0] == '-' && template[1] == '-') template++;
  ol = strlen (option);

  if (strncmp (option, template, ol))
    return FALSE;

  if (template[ol] != (value ? ' ' : 0))
    return FALSE;

  /* At this point, we have a match against "option".
     If template contains a %, we're done.
     Else, compare against "value" too.
   */
  c = strchr (template, '%');
  if (c)
    return TRUE;

  if (!value)
    return (template[ol] == 0);
  if (strcmp (template + ol + 1, value))
    return FALSE;

  return TRUE;
}


static gboolean
parse_command_line_into_parameters_1 (const char *filename,
                                      GList *parms,
                                      const char *option,
                                      const char *value,
                                      parameter *parent)
{
  GList *p;
  parameter *match = 0;
  gint which = -1;
  gint index = 0;

  for (p = parms; p; p = p->next)
    {
      parameter *pp = (parameter *) p->data;
      which = -99;

      if (pp->type == SELECT)
        {
          if (parse_command_line_into_parameters_1 (filename,
                                                    pp->options,
                                                    option, value,
                                                    pp))
            {
              which = -2;
              match = pp;
            }
        }
      else if (pp->arg)
        {
          if (compare_opts (option, value, (char *) pp->arg))
            {
              which = -1;
              match = pp;
            }
        }
      else if (pp->arg_set)
        {
          if (compare_opts (option, value, (char *) pp->arg_set))
            {
              which = 1;
              match = pp;
            }
        }
      else if (pp->arg_unset)
        {
          if (compare_opts (option, value, (char *) pp->arg_unset))
            {
              which = 0;
              match = pp;
            }
        }

      if (match)
        break;

      index++;
    }

  if (!match)
    {
      if (debug_p && !parent)
        fprintf (stderr, "%s: WARNING: %s: no match for %s %s\n",
                 blurb(), filename, option, (value ? value : ""));
      return FALSE;
    }

  switch (match->type)
    {
    case STRING:
    case FILENAME:
    case SLIDER:
    case SPINBUTTON:
      if (which != -1) abort();
      parameter_set_switch (match, (gpointer) value);
      break;
    case BOOLEAN:
      if (which != 0 && which != 1) abort();
      parameter_set_switch (match, GINT_TO_POINTER(which));
      break;
    case SELECT_OPTION:
      if (which != 1) abort();
      parameter_set_switch (parent, GINT_TO_POINTER(index));
      break;
    default:
      break;
    }
  return TRUE;
}


/* Set the parameter's value.
   For STRING, FILENAME, SLIDER, and SPINBUTTON, `value' is a char*.
   For BOOLEAN and SELECT, `value' is an int.
 */
static void
parameter_set_switch (parameter *p, gpointer value)
{
  if (p->type == SELECT_OPTION) abort();
  if (!p->widget) return;
  switch (p->type)
    {
    case STRING:
    case FILENAME:
      {
        gtk_entry_set_text (GTK_ENTRY (p->widget), (char *) value);
        break;
      }
    case SLIDER:
    case SPINBUTTON:
      {
        GtkAdjustment *adj =
          (p->type == SLIDER
           ? gtk_range_get_adjustment (GTK_RANGE (p->widget))
           : gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (p->widget)));
        float f;
        char c;

        if (1 == sscanf ((char *) value, "%f %c", &f, &c))
          {
            switch (p->convert) {
            case INVERT:
              f = invert_range (GET_ADJ_LOWER(adj), GET_ADJ_UPPER(adj), f) - 1;
              break;
            case RATIO:
              f = range_to_ratio (p->low, p->high, f);
              break;
            default: break;
            }
            gtk_adjustment_set_value (adj, f);
          }
        break;
      }
    case BOOLEAN:
      {
        GtkToggleButton *b = GTK_TOGGLE_BUTTON (p->widget);
        gtk_toggle_button_set_active (b, GPOINTER_TO_INT(value));
        break;
      }
    case SELECT:
      {
        GtkComboBox *cbox = GTK_COMBO_BOX (p->widget);
        gtk_combo_box_set_active (cbox, GPOINTER_TO_INT(value));
        break;
      }
    default:
      abort();
    }
}


static void
restore_defaults (const char *progname, GList *parms)
{
  for (; parms; parms = parms->next)
    {
      parameter *p = (parameter *) parms->data;
      if (!p->widget) continue;
      switch (p->type)
        {
        case STRING:
        case FILENAME:
          {
            gtk_entry_set_text (GTK_ENTRY (p->widget),
                                (p->string ? (char *) p->string : ""));
            break;
          }
        case SLIDER:
          {
            GtkAdjustment *adj =
              (p->type == SLIDER
               ? gtk_range_get_adjustment (GTK_RANGE (p->widget))
               : gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (p->widget)));
            gfloat value;
            switch (p->convert) {
            case INVERT:
              value = invert_range (p->low, p->high, p->value);
              break;
            case RATIO:
              value = range_to_ratio (p->low, p->high, p->value);
              break;
            default:
              value = p->value;
              break;
            }
            gtk_adjustment_set_value (adj, value);
            break;
          }
        case SPINBUTTON:
          {
            GtkAdjustment *adj =
              gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (p->widget));
            gtk_adjustment_set_value (adj, p->value);
            break;
          }
        case BOOLEAN:
          {
            /* A toggle button should be on by default if it inserts
               nothing into the command line when on.  E.g., it should
               be on if `arg_set' is null.
             */
            gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (p->widget),
                                          (!p->arg_set || !*p->arg_set));
            break;
          }
        case SELECT:
          {
            GtkComboBox *cbox = GTK_COMBO_BOX (p->widget);
            GList *opts;
            int selected = 0;
            int index;

            for (opts = p->options, index = 0; opts;
                 opts = opts->next, index++)
              {
                parameter *s = (parameter *) opts->data;
                /* The default menu item is the first one with
                   no `arg_set' field. */
                if (!s->arg_set)
                  {
                    selected = index;
                    break;
                  }
              }

            gtk_combo_box_set_active (cbox, selected);
            break;
          }
        default:
          abort();
        }
    }
}



/* Documentation strings
 */

static char *
get_description (GList *parms, gboolean verbose_p)
{
  parameter *doc = 0;
  for (; parms; parms = parms->next)
    {
      parameter *p = (parameter *) parms->data;
      if (p->type == DESCRIPTION)
        {
          doc = p;
          break;
        }
    }

  if (!doc || !doc->string)
    return 0;
  else
    {
      char *d = strdup ((char *) doc->string);
      char *s;
      char *p;
      for (s = d; *s; s++)
        if (s[0] == '\n')
          {
            if (s[1] == '\n')      /* blank line: leave it */
              s++;
            else if (s[1] == ' ' || s[1] == '\t')
              s++;                 /* next line is indented: leave newline */
            else if (!strncmp(s+1, "http:", 5) ||
                     !strncmp(s+1, "https:", 5))
              s++;                 /* next line begins a URL: leave newline */
            else
              s[0] = ' ';          /* delete newline to un-fold this line */
          }

      /* strip off leading whitespace on first line only */
      for (s = d; *s && (*s == ' ' || *s == '\t'); s++)
        ;
      while (*s == '\n')   /* strip leading newlines */
        s++;
      if (s != d)
        memmove (d, s, strlen(s)+1);

      /* strip off trailing whitespace and newlines */
      {
        int L = strlen(d);
        while (L && isspace(d[L-1]))
          d[--L] = 0;
      }

      /* strip off duplicated whitespaces */
      for (s = d; *s; s++)
	  if (s[0] == ' ')
	  {
	    p = s+1;
	    while (*s == ' ')
	      s++;
            if (*p && (s != p))
              memmove (p, s, strlen(s)+1);
	  }

#if 0
      /*if (verbose_p)*/
        {
          fprintf (stderr, "%s: text read   is \"%s\"\n", blurb(),doc->string);
          fprintf (stderr, "%s: description is \"%s\"\n", blurb(), d);
          fprintf (stderr, "%s: translation is \"%s\"\n", blurb(), _(d));
        }
#endif /* 0 */

      return (d);
    }
}

static int
get_year (const char *desc)
{
  /* Find the last 4-digit year-like number on the last line of the string. */
  const char *s = strrchr (desc, '\n');
  int year = 0;
  if (!s) return 0;
  while (*s)
    {
      if ((s[0] == ' ' || s[0] == '\t' || s[0] == '\r' || s[0] == '\n' ||
           s[0] == '-' || s[0] == '.') &&
          s[1] >= '0' && s[1] <= '9' &&
          s[2] >= '0' && s[2] <= '9' &&
          s[3] >= '0' && s[3] <= '9' &&
          s[4] >= '0' && s[4] <= '9' &&
          (s[5] == ' ' || s[5] == '\t' || s[5] == '\r' || s[5] == '\n' ||
           s[5] == '-' || s[5] == '.'))
        {
          int n = (((s[1]-'0') * 1000) +
                   ((s[2]-'0') * 100) +
                   ((s[3]-'0') * 10) +
                   ((s[4]-'0') * 1));
          if (n > 1970 && n < 2100)
            year = n;
          s += 4;
        }
      s++;
    }
  return year;
}



/* External interface.
 */

static conf_data *
load_configurator_1 (const char *program, const char *arguments,
                     void (*changed_cb) (GtkWidget *, gpointer),
                     gpointer changed_data,
                     gboolean verbose_p)
{
  const char *dir = HACK_CONFIGURATION_PATH;
  char *base_program;
  int L = strlen (dir);
  char *file;
  char *s;
  FILE *f;
  conf_data *data;

  if (L == 0) return 0;

  base_program = strrchr(program, '/');
  if (base_program) base_program++;
  if (!base_program) base_program = (char *) program;

  file = (char *) malloc (L + strlen (base_program) + 10);
  data = (conf_data *) calloc (1, sizeof(*data));

  strcpy (file, dir);
  if (file[L-1] != '/')
    file[L++] = '/';
  strcpy (file+L, base_program);

  for (s = file+L; *s; s++)
    if (*s == '/' || *s == ' ')
      *s = '_';
    else if (isupper (*s))
      *s = tolower (*s);

  strcat (file+L, ".xml");

  f = fopen (file, "r");
  if (f)
    {
      int res, size = 1024;
      char chars[1024];
      xmlParserCtxtPtr ctxt;
      xmlDocPtr doc = 0;
      GtkWidget *vbox0;
      GList *parms;

      if (verbose_p)
        fprintf (stderr, "%s: reading %s...\n", blurb(), file);

      res = fread (chars, 1, 4, f);
      if (res <= 0)
        {
          free (data);
          data = 0;
          goto DONE;
        }

      ctxt = xmlCreatePushParserCtxt(NULL, NULL, chars, res, file);
      while ((res = fread(chars, 1, size, f)) > 0)
        xmlParseChunk (ctxt, chars, res, 0);
      xmlParseChunk (ctxt, chars, 0, 1);
      doc = ctxt->myDoc;
      xmlFreeParserCtxt (ctxt);
      fclose (f);

      /* Parsed the XML file.  Now make some widgets. */

      vbox0 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
      gtk_widget_show (vbox0);

      parms = make_parameters (file, doc->xmlRootNode, vbox0,
                               changed_cb, changed_data);
      sanity_check_parameters (file, parms);

      xmlFreeDoc (doc);

      restore_defaults (program, parms);
      if (arguments && *arguments)
        parse_command_line_into_parameters (program, arguments, parms);

      data->widget = vbox0;
      data->parameters = parms;
      data->description = get_description (parms, verbose_p);
      data->year = get_year (data->description);
    }
  else
    {
      parameter *p;

      if (verbose_p)
        fprintf (stderr, "%s: %s does not exist.\n", blurb(), file);

      p = calloc (1, sizeof(*p));
      p->type = COMMAND;
      p->arg = (xmlChar *) strdup (arguments);

      data->parameters = g_list_append (0, (gpointer) p);
    }

  data->progname = strdup (program);

 DONE:
  free (file);
  return data;
}

static void
split_command_line (const char *full_command_line,
                    char **prog_ret, char **args_ret)
{
  char *line = strdup (full_command_line);
  char *prog;
  char *args;
  char *s;

  prog = line;
  s = line;
  while (*s)
    {
      if (isspace (*s))
        {
          *s = 0;
          s++;
          while (isspace (*s)) s++;
          break;
        }
      else if (*s == '=')  /* if the leading word contains an "=", skip it. */
        {
          while (*s && !isspace (*s)) s++;
          while (isspace (*s)) s++;
          prog = s;
        }
      s++;
    }
  args = s;

  *prog_ret = strdup (prog);
  *args_ret = strdup (args);
  free (line);
}


conf_data *
load_configurator (const char *full_command_line,
                   void (*changed_cb) (GtkWidget *, gpointer),
                   gpointer changed_data,
                   gboolean verbose_p)
{
  char *prog;
  char *args;
  conf_data *cd;
  debug_p = verbose_p;
  split_command_line (full_command_line, &prog, &args);
  cd = load_configurator_1 (prog, args, changed_cb, changed_data, verbose_p);
  free (prog);
  free (args);
  return cd;
}



char *
get_configurator_command_line (conf_data *data, gboolean default_p)
{
  char *args = parameters_to_cmd_line (data->parameters, default_p);
  char *result = (char *) malloc (strlen (data->progname) +
                                  strlen (args) + 2);
  strcpy (result, data->progname);
  strcat (result, " ");
  strcat (result, args);
  free (args);
  return result;
}


void
set_configurator_command_line (conf_data *data, const char *full_command_line)
{
  char *prog;
  char *args;
  split_command_line (full_command_line, &prog, &args);
  if (data->progname) free (data->progname);
  data->progname = prog;
  restore_defaults (prog, data->parameters);
  parse_command_line_into_parameters (prog, args, data->parameters);
  free (args);
}

void
free_conf_data (conf_data *data)
{
  if (data->parameters)
    {
      GList *rest;
      for (rest = data->parameters; rest; rest = rest->next)
        {
          free_parameter ((parameter *) rest->data);
          rest->data = 0;
        }
      g_list_free (data->parameters);
      data->parameters = 0;
    }

  if (data->widget)
    gtk_widget_destroy (data->widget);

  if (data->progname)
    free (data->progname);
  if (data->description)
    free (data->description);

  memset (data, ~0, sizeof(*data));
  free (data);
}


#endif /* HAVE_GTK -- whole file */
