/* demo-Gtk.c --- implements the interactive demo-mode and options dialogs.
 * xscreensaver, Copyright © 1993-2024 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 */

#ifdef ENABLE_NLS
# include <locale.h>
#endif /* ENABLE_NLS */

#ifdef HAVE_UNAME
# include <sys/utsname.h>	/* for uname() */
#endif /* HAVE_UNAME */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <ctype.h>
#include <time.h>
#include <pwd.h>		/* for getpwuid() */
#include <sys/stat.h>
#include <sys/time.h>

#include <signal.h>
#include <errno.h>
#ifdef HAVE_SYS_WAIT_H
# include <sys/wait.h>		/* for waitpid() and associated macros */
#endif

#include <X11/Xatom.h>		/* for XA_INTEGER */
#include <X11/Shell.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>
#include <gdk/gdkx.h>		/* For gdk_x11_get_default_xdisplay(), etc. */

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

#include "blurb.h"
#include "xscreensaver-intl.h"
#include "version.h"
#include "types.h"
#include "resources.h"		/* for parse_time() */
#include "remote.h"		/* for xscreensaver_command() */
#include "screens.h"
#include "visual.h"
#include "atoms.h"
#include "usleep.h"
#include "atoms.h"
#include "screenshot.h"
#include "xmu.h"

#include "demo-Gtk-conf.h"


/* from exec.c */
extern void exec_command (const char *shell, const char *command, int nice);
extern int on_path_p (const char *program);


#undef countof
#define countof(x) (sizeof((x))/sizeof((*x)))

const char *progclass = "XScreenSaver";

#ifdef __GNUC__
  __extension__   /* don't warn about "string length is greater than the
                     length ISO C89 compilers are required to support". */
#endif
static char *defaults[] = {
#include "XScreenSaver_ad.h"
 0
};

/* The order of the items in the mode menu. */
static int mode_menu_order[] = {
  DONT_BLANK, BLANK_ONLY, ONE_HACK, RANDOM_HACKS, RANDOM_HACKS_SAME };
enum { COL_ENABLED, COL_NAME, COL_LAST };
typedef enum { D_NONE, D_LAUNCH, D_GNOME, D_KDE } dialog_button;


typedef struct {

  char *short_version;		/* version number of this xscreensaver build */

  GtkWindow *window;
  GtkWindow *dialog;

  Display *dpy;
  Bool wayland_p;
  Pixmap screenshot;
  Visual *gl_visual;

  conf_data *cdata;		/* private data for per-hack configuration */

  Bool debug_p;			/* whether to print diagnostics */
  Bool initializing_p;		/* flag for breaking recursion loops */
  Bool flushing_p;		/* flag for breaking recursion loops */
  Bool saving_p;		/* flag for breaking recursion loops */
  Bool dpms_supported_p;	/* Whether XDPMS is available */

  char *desired_preview_cmd;	/* subprocess we intend to run */
  char *running_preview_cmd;	/* subprocess we are currently running */
  pid_t running_preview_pid;	/* pid of forked subproc (might be dead) */
  Bool running_preview_error_p;	/* whether the pid died abnormally */

  Bool preview_suppressed_p;	/* flag meaning "don't launch subproc" */
  int subproc_timer_id;		/* timer to delay subproc launch */
  int subproc_check_timer_id;	/* timer to check whether it started up */
  int subproc_check_countdown;  /* how many more checks left */

  int *list_elt_to_hack_number;	/* table for sorting the hack list */
  int *hack_number_to_list_elt;	/* the inverse table */
  Bool *hacks_available_p;	/* whether hacks are on $PATH */
  int total_available;		/* how many are on $PATH */
  int list_count;		/* how many items are in the list: this may be
                                   less than p->screenhacks_count, if some are
                                   suppressed. */

  int _selected_list_element;	/* don't use this: call
                                   selected_list_element() instead */

  Bool multi_screen_p;		/* Is there more than one monitor */

  saver_preferences prefs;

} state;


/* Class definitions for the application and two windows.  The classes are:

   XScreenSaverApp    -- The invisible GtkApplication main-loop framework.
   XScreenSaverWindow -- The main window with the scrolling list of hacks.
   XScreenSaverDialog -- The per-hack settings window.
 */
#define XSCREENSAVER_APP_TYPE (xscreensaver_app_get_type())
G_DECLARE_FINAL_TYPE (XScreenSaverApp, xscreensaver_app, XSCREENSAVER, APP,
                      GtkApplication)

struct _XScreenSaverApp {
  GtkApplication parent;
  Bool cmdline_debug_p;
};


G_DEFINE_TYPE (XScreenSaverApp, xscreensaver_app, GTK_TYPE_APPLICATION)

/* The widgets we reference from the demo.ui file.
 */
#define ALL_WINDOW_WIDGETS	\
  W(activate_menuitem)		\
  W(lock_menuitem)		\
  W(kill_menuitem)		\
  W(restart_menuitem)		\
  W(list)			\
  W(scroller)			\
  W(preview_frame)		\
  W(short_preview_label)	\
  W(preview_author_label)	\
  W(timeout_spinbutton)		\
  W(cycle_spinbutton)		\
  W(lock_spinbutton)		\
  W(dpms_standby_spinbutton)	\
  W(dpms_suspend_spinbutton)	\
  W(dpms_off_spinbutton)	\
  W(fade_spinbutton)		\
  W(lock_button)		\
  W(dpms_button)		\
  W(dpms_quickoff_button)	\
  W(grab_desk_button)		\
  W(grab_video_button)		\
  W(grab_image_button)		\
  W(fade_button)		\
  W(unfade_button)		\
  W(preview)			\
  W(preview_notebook)		\
  W(text_radio)			\
  W(text_file_radio)		\
  W(text_file_browse)		\
  W(text_program_radio)		\
  W(text_url_radio)		\
  W(text_host_radio)		\
  W(image_text)			\
  W(image_browse_button)	\
  W(text_entry)			\
  W(text_file_entry)		\
  W(text_program_entry)		\
  W(text_url_entry)		\
  W(text_program_browse)	\
  W(theme_menu)			\
  W(mode_menu)			\
  W(next_prev_hbox)		\
  W(blanking_table)		\
  W(lock_mlabel)		\
  W(dpms_frame)			\
  W(dpms_standby_label)		\
  W(dpms_standby_mlabel)	\
  W(dpms_suspend_label)		\
  W(dpms_suspend_mlabel)	\
  W(dpms_off_label)		\
  W(dpms_off_mlabel)		\
  W(fade_label)			\
  W(demo)			\
  W(settings)			\

/* The widgets we reference from the prefs.ui file.
 */
#define ALL_DIALOG_WIDGETS	\
  W(opt_notebook)		\
  W(doc)			\
  W(settings_vbox)		\
  W(cmd_text)			\
  W(opt_frame)			\
  W(dialog_vbox)		\
  W(adv_button)			\
  W(std_button)			\
  W(cmd_label)			\
  W(manual)			\
  W(visual)			\
  W(visual_combo)		\
  W(reset_button)		\
  W(ok_button)			\

#define XSCREENSAVER_WINDOW_TYPE (xscreensaver_window_get_type())
G_DECLARE_FINAL_TYPE (XScreenSaverWindow, xscreensaver_window,
                      XSCREENSAVER, WINDOW, GtkApplicationWindow)

struct _XScreenSaverWindow {
  GtkApplicationWindow parent;
  state state;

  GtkWidget
#  undef W
#  define W(NAME) * NAME,
   ALL_WINDOW_WIDGETS
   *_dummy;
#  undef W
};

G_DEFINE_TYPE (XScreenSaverWindow, xscreensaver_window,
               GTK_TYPE_APPLICATION_WINDOW)


#define XSCREENSAVER_DIALOG_TYPE (xscreensaver_dialog_get_type())
G_DECLARE_FINAL_TYPE (XScreenSaverDialog, xscreensaver_dialog,
                      XSCREENSAVER, DIALOG, GtkDialog)

struct _XScreenSaverDialog {
  GtkApplicationWindow parent;
  XScreenSaverWindow *main;
  char *unedited_cmdline;   /* Current hack command line before saving */

  GtkWidget
#  undef W
#  define W(NAME) * NAME,
   ALL_DIALOG_WIDGETS
   *_dummy;
#  undef W
};

G_DEFINE_TYPE (XScreenSaverDialog, xscreensaver_dialog,
               GTK_TYPE_DIALOG)


static void hack_subproc_environment (Window preview_window_id, Bool debug_p);

static void populate_demo_window (state *, int list_elt);
static void populate_prefs_page (state *);
static void populate_popup_window (state *);

static Bool flush_dialog_changes_and_save (state *);
static Bool flush_popup_changes_and_save (state *);
static Bool validate_image_directory (state *, const char *path);

static int maybe_reload_init_file (state *);
static void await_xscreensaver (state *);
static Bool xscreensaver_running_p (state *);
static void sensitize_menu_items (state *s, Bool force_p);

static void schedule_preview (state *, const char *cmd);
static void kill_preview_subproc (state *, Bool reset_p);
static void schedule_preview_check (state *);
static void sensitize_demo_widgets (state *, Bool sensitive_p);
static void kill_gnome_screensaver (state *);
static void kill_kde_screensaver (state *);

/* Some pathname utilities */

/* Removed redundant . and .. components from the pathname.
   Strip leading and trailing spaces.
   Make it have a trailing slash if it should be a directory.
 */
static char *
normalize_pathname (const char *path, gboolean dir_p)
{
  int L;
  char *p2, *s;
  if (!path) return 0;
  if (!*path) return strdup ("");

  /* Strip leading spaces */
  while (isspace (*path)) path++;

  L = strlen (path);
  p2 = (char *) malloc (L + 3);
  strcpy (p2, path);

  /* Strip trailing spaces and slashes */
  while (L > 0 && (isspace (p2[L-1]) || p2[L-1] == '/'))
    p2[--L] = 0;

  for (s = p2; s && *s; s++)
    {
      if (*s == '/' &&
          (!strncmp (s, "/../", 4) ||			/* delete "XYZ/../" */
           !strncmp (s, "/..\000", 4)))			/* delete "XYZ/..$" */
        {
          char *s0 = s;
          while (s0 > p2 && s0[-1] != '/')
            s0--;
          if (s0 > p2)
            {
              s0--;
              s += 3;
              /* strcpy (s0, s); */
              memmove(s0, s, strlen(s) + 1);
              s = s0-1;
            }
        }
      else if (*s == '/' && !strncmp (s, "/./", 3)) {	/* delete "/./" */
        /* strcpy (s, s+2), s--; */
        memmove(s, s+2, strlen(s+2) + 1);
        s--;
       }
      else if (*s == '/' && !strncmp (s, "/.\000", 3))	/* delete "/.$" */
        *s = 0, s--;
    }

  /*
    Normalize consecutive slashes.
    Ignore doubled slashes after ":" to avoid mangling URLs.
  */

  for (s = p2; s && *s; s++){
    if (*s == ':') continue;
    if (!s[1] || !s[2]) continue;
    while (s[1] == '/' && s[2] == '/')
      /* strcpy (s+1, s+2); */
      memmove (s+1, s+2, strlen(s+2) + 1);
  }

  /* and strip trailing whitespace for good measure. */
  L = strlen(p2);
  while (isspace(p2[L-1]))
    p2[--L] = 0;

  if (dir_p)
    {
      p2[L++] = '/';	/* Add trailing slash */
      p2[L] = 0;
    }

  return p2;
}


/* Expand or un-expand ~/ to $HOME in a pathname, as requested.
   Strip leading and trailing spaces.
   Make it have a trailing slash if it should be a directory.
 */
static char *
pathname_tilde (const char *p, gboolean add_p, gboolean dir_p)
{
  char *p2;
  if (!p) return 0;

  if (dir_p &&
      (!strncasecmp (p, "http://",  7) ||
       !strncasecmp (p, "https://", 8)))
    dir_p = FALSE;

  p2 = normalize_pathname (p, dir_p);
  p = p2;

  if (add_p)
    {
      const char *home = getenv("HOME");
      int L = strlen(home);
      if (!strncmp (home, p, L) && p[L] == '/')
        {
          char *p3 = (char *) malloc (strlen (p) + 4);
          strcpy (p3, "~");
          strcat (p3, p + L);
          free (p2);
          p2 = p3;
        }
    }
  else if (!strncmp (p, "~/", 2))
    {
      const char *home = getenv("HOME");
      char *p3 = (char *) malloc (strlen (p) + strlen (home) + 4);
      strcpy (p3, home);
      strcat (p3, p + 1);
      free (p2);
      p2 = p3;
    }

  return p2;
}


/* Is the path a directory that exists? */
static gboolean
directory_p (const char *path)
{
  char *p2 = pathname_tilde (path, FALSE, FALSE);   /* no slash on dir */
  struct stat st;
  gboolean ok = FALSE;

  if (!p2 || !*p2)
    ok = FALSE;
  else if (stat (p2, &st))
    ok = FALSE;
  else if (!S_ISDIR (st.st_mode))
    ok = FALSE;
  else
    ok = TRUE;
  free (p2);
  return ok;
}


/* Is the path a file (not directory) that exists? */
static gboolean
file_p (const char *path)
{
  char *p2 = pathname_tilde (path, FALSE, FALSE);
  struct stat st;
  gboolean ok = FALSE;
  if (!p2 || !*p2)
    ok = FALSE;
  else if (stat (p2, &st))
    ok = FALSE;
  else if (S_ISDIR (st.st_mode))
    ok = FALSE;
  else
    ok = TRUE;
  free (p2);
  return ok;
}



/* See if the directory has at least one image file under it.
   Recurses to at most the given depth, chasing symlinks.
   To do this properly would mean running "xscreensaver-getimage-file"
   and seeing if it found anything, but that might take a long time to
   build the cache the first time, so this is close enough.
 */
static Bool
image_files_p (const char *path, int max_depth)
{
  const char * const exts[] = {
    "jpg", "jpeg", "pjpeg", "pjpg", "png", "gif", 
    "tif", "tiff", "xbm", "xpm", "svg",
  };
  struct dirent *de;
  Bool ok = FALSE;
  char *p2 = pathname_tilde (path, FALSE, FALSE);   /* no slash on dir */
  DIR *dir = opendir (p2);
  if (!dir) goto DONE;

  while (!ok && (de = readdir (dir)))
    {
      struct stat st;
      const char *f = de->d_name;
      char *f2;
      if (*f == '.') continue;

      f2 = (char *) malloc (strlen(p2) + strlen(f) + 10);
      strcpy (f2, p2);
      strcat (f2, "/");
      strcat (f2, f);

      if (!stat (f2, &st))
        {
          if (S_ISDIR (st.st_mode))
            {
              if (max_depth > 0 && image_files_p (f2, max_depth - 1))
                ok = TRUE;
            }
          else
            {
              int i;
              const char *ext = strrchr (f, '.');
              if (ext)
                for (i = 0; i < countof(exts); i++)
                  if (!strcasecmp (ext+1, exts[i]))
                    {
                      /* fprintf (stderr, "%s: found %s\n", blurb(), f2); */
                      ok = TRUE;
                      break;
                    }
            }
        }

      free (f2);
    }

  closedir (dir);
 DONE:
  free (p2);
  return ok;
}


/* Some random utility functions
 */

/* Why this behavior isn't automatic, I'll never understand.
 */
static void
ensure_selected_item_visible (state *s, GtkWidget *widget)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);

  /* Find the path of the selected row in the list.
   */
  GtkTreeView *list_widget = GTK_TREE_VIEW (win->list);
  GtkTreeSelection *selection = gtk_tree_view_get_selection (list_widget);
  GtkTreeModel *model;
  GtkTreeIter iter;
  GtkTreePath *path;

  if (!gtk_tree_selection_get_selected (selection, &model, &iter))
    path = gtk_tree_path_new_first ();
  else
    path = gtk_tree_model_get_path (model, &iter);

  /* Make this item be visible and selected. */
  gtk_tree_view_set_cursor (list_widget, path, NULL, FALSE);

  /* Make the scroller show that item at the center of the viewport.
     The set_cursor() call, above, makes the item be visible, but it hugs
     the top or bottom edge of the viewport, instead of providing more
     surrounding context.

     Doing the following in list_select_changed_cb() instead of here makes
     the list vertically re-center when using the cursor keys instead of
     hugging the top or bottom (good) but also makes it re-center when
     clicking on a new item with the mouse (bad).
   */
  if (gtk_widget_get_realized (GTK_WIDGET (list_widget)))
    {
      GdkWindow *bin = gtk_tree_view_get_bin_window (list_widget);
      int binh = gdk_window_get_height (bin);
      GdkRectangle r;
      gtk_tree_view_get_cell_area (list_widget, path, NULL, &r);
      gtk_tree_view_convert_widget_to_tree_coords (list_widget,
                                                   r.x, r.y, &r.x, &r.y);
      gtk_tree_view_scroll_to_point (list_widget, r.x, r.y - binh / 2);
    }

  gtk_tree_path_free (path);
}


static void
warning_dialog_cb (GtkDialog *dialog, gint response_id, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  if (s->debug_p)
    fprintf (stderr, "%s: dialog response %d\n", blurb(), response_id);
  switch (response_id) {
  case D_LAUNCH: restart_menu_cb (GTK_WIDGET (dialog), user_data); break;
  case D_GNOME:  kill_gnome_screensaver (s); break;
  case D_KDE:    kill_kde_screensaver (s); break;
  default:   /* D_NONE or GTK_RESPONSE_DELETE_EVENT */
    break;
  }
  gtk_widget_destroy (GTK_WIDGET (dialog));
}


static Bool
warning_dialog_1 (GtkWindow *win,
                  const char *title,
                  const char *message,
                  dialog_button button_type)
{
  GtkWidget *dialog =
    (button_type == D_NONE
     ? gtk_dialog_new_with_buttons (title, win,
                                    GTK_DIALOG_DESTROY_WITH_PARENT,
                                    _("_OK"), D_NONE,
                                    NULL)
     : gtk_dialog_new_with_buttons (title, win,
                                    GTK_DIALOG_DESTROY_WITH_PARENT,
                                    (button_type == D_LAUNCH ? _("Launch") :
                                     button_type == D_GNOME  ? _("Kill") :
                                     button_type == D_KDE    ? _("Kill") :
                                     _("_OK")),
                                    button_type,
                                    _("_Cancel"), D_NONE,
                                    NULL));
  GtkWidget *content_area =
    gtk_dialog_get_content_area (GTK_DIALOG (dialog));
  GtkWidget *hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
  GtkWidget *vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
  GtkWidget *im = gtk_image_new_from_icon_name ("dialog-warning",
                                                GTK_ICON_SIZE_DIALOG);
  GtkWidget *label = gtk_label_new (message);
  int margin = 32;

  gtk_box_pack_start (GTK_BOX (hbox), im,    FALSE, FALSE, 0);
  gtk_box_pack_start (GTK_BOX (hbox), vbox,  FALSE, FALSE, 0);
  gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0);
  gtk_container_add (GTK_CONTAINER (content_area), hbox);

  gtk_widget_set_margin_start  (hbox, margin);
  gtk_widget_set_margin_end    (hbox, margin);
  gtk_widget_set_margin_top    (hbox, margin);
  gtk_widget_set_margin_bottom (hbox, margin / 2);

  gtk_widget_set_margin_start (label, margin / 2);
  gtk_widget_set_valign (im, GTK_ALIGN_START);

  g_signal_connect (dialog, "response",
                    G_CALLBACK (warning_dialog_cb),
                    win);

  gtk_dialog_set_default_response (GTK_DIALOG (dialog), button_type);
  gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);
  gtk_window_set_transient_for (GTK_WINDOW (dialog), win);
  gtk_widget_show_all (dialog);

  return TRUE;
}


void
warning_dialog (GtkWindow *win, const char *title, const char *message)
{
  warning_dialog_1 (win, title, message, D_NONE);
}


static void
run_cmd (state *s, Atom command, int arg)
{
  char *err = 0;
  int status;

  if (!s->dpy) return;
  flush_dialog_changes_and_save (s);

  if (s->debug_p)
    fprintf (stderr, "%s: command: %s %d\n", blurb(),
             XGetAtomName (s->dpy, command), arg);
  status = xscreensaver_command (s->dpy, command, arg, FALSE, &err);

  /* Kludge: ignore the spurious "window unexpectedly deleted" errors... */
  if (status < 0 && err && strstr (err, "unexpectedly deleted"))
    status = 0;

  if (status < 0)
    {
      char buf [255];
      sprintf (buf, "%.100s", (err ? err : _("Unknown error!")));
      warning_dialog (s->window, _("Error"), buf);
    }
  if (err) free (err);

  sensitize_menu_items (s, TRUE);
}


static void
run_hack (state *s, int list_elt, Bool report_errors_p)
{
  int hack_number;
  char *err = 0;
  int status;

  if (!s->dpy) return;
  if (list_elt < 0) return;
  hack_number = s->list_elt_to_hack_number[list_elt];

  flush_dialog_changes_and_save (s);
  schedule_preview (s, 0);

  if (s->debug_p)
    fprintf (stderr, "%s: command: DEMO %d\n", blurb(), hack_number + 1);
  status = xscreensaver_command (s->dpy, XA_DEMO, hack_number + 1,
                                 FALSE, &err);

  if (status < 0 && report_errors_p)
    {
      if (xscreensaver_running_p (s))
        {
          /* Kludge: ignore the spurious "window unexpectedly deleted"
             errors... */
          if (err && strstr (err, "unexpectedly deleted"))
            status = 0;

          if (status < 0)
            {
              char buf [255];
              sprintf (buf, "%.100s", err ? err : _("Unknown error!"));
              warning_dialog (s->window, _("Error"), buf);
            }
        }
      else
        {
          /* The error is that the daemon isn't running;
             offer to restart it.
           */
          const char *d = DisplayString (s->dpy);
          char msg [1024];
          sprintf (msg,
                   _("The XScreenSaver daemon doesn't seem to be running\n"
                     "on display \"%.25s\".  Launch it now?"),
                   d);
          warning_dialog_1 (s->window, _("Warning"), msg, D_LAUNCH);
        }
    }

  if (err) free (err);

  sensitize_menu_items (s, FALSE);
}


static pid_t
fork_and_exec (state *s, int argc, char **argv)
{
  char buf [255];
  pid_t forked = fork();
  switch ((int) forked) {
  case -1:
    sprintf (buf, "%s: couldn't fork", blurb());
    perror (buf);
    break;

  case 0:
    if (s->dpy) close (ConnectionNumber (s->dpy));
    execvp (argv[0], argv);		/* shouldn't return. */

    sprintf (buf, "%s: pid %lu: couldn't exec %s", blurb(),
             (unsigned long) getpid(), argv[0]);
    perror (buf);
    exit (1);				/* exits child fork */
    break;

  default:				/* parent fork */

    /* Put it in its own process group so that this process getting SIGTERM
       does not propagate to the forked process. */
    if (setpgid (forked, 0))
      {
        char buf [255];
        sprintf (buf, "%s: setpgid %d", blurb(), forked);
        perror (buf);
      }

    if (s->debug_p)
      {
        int i;
        fprintf (stderr, "%s: pid %lu: forked:", blurb(),
                 (unsigned long) forked);
        for (i = 0; i < argc; i++)
          if (strchr (argv[i], ' '))
            fprintf (stderr, " \"%s\"", argv[i]);
          else
            fprintf (stderr, " %s", argv[i]);
        fprintf (stderr, "\n");
      }

    break;
  }

  return forked;
}



/****************************************************************************

 XScreenSaverWindow callbacks, referenced by demo.ui.

 ****************************************************************************/

/* File menu / Quit */
G_MODULE_EXPORT void
quit_menu_cb (GtkAction *menu_action, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  if (s->debug_p) fprintf (stderr, "%s: quit menu\n", blurb());
  flush_dialog_changes_and_save (s);
  kill_preview_subproc (s, FALSE);
  g_application_quit (G_APPLICATION (
    gtk_window_get_application (GTK_WINDOW (win))));
}


/* Help menu / About */
G_MODULE_EXPORT void
about_menu_cb (GtkAction *menu_action, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  if (s->debug_p) fprintf (stderr, "%s: about menu\n", blurb());
  preview_theme_cb (NULL, user_data);
}


/* Help menu / Documentation */
G_MODULE_EXPORT void
doc_menu_cb (GtkAction *menu_action, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  saver_preferences *p = &s->prefs;
  char *help_command;
  int ac = 0;
  char *av[10];

  if (s->debug_p) fprintf (stderr, "%s: doc menu\n", blurb());

  if (!p->help_url || !*p->help_url)
    {
      warning_dialog (GTK_WINDOW (win), _("Error"),
                      _("No Help URL has been specified.\n"));
      return;
    }

  help_command = (char *) malloc (strlen (p->load_url_command) +
				  (strlen (p->help_url) * 5) + 20);
  sprintf (help_command, p->load_url_command,
           p->help_url, p->help_url, p->help_url, p->help_url, p->help_url);

  av[ac++] = "/bin/sh";
  av[ac++] = "-c";
  av[ac++] = help_command;
  av[ac] = 0;
  fork_and_exec (s, ac, av);
  free (help_command);
}


/* File menu opened */
G_MODULE_EXPORT void
file_menu_cb (GtkAction *menu_action, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  if (s->debug_p) fprintf (stderr, "%s: file menu post\n", blurb());
  sensitize_menu_items (s, FALSE);
}


/* File menu / Activate */
G_MODULE_EXPORT void
activate_menu_cb (GtkAction *menu_action, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  if (s->debug_p) fprintf (stderr, "%s: activate menu\n", blurb());
  run_cmd (s, XA_ACTIVATE, 0);
}


/* File menu / Lock */
G_MODULE_EXPORT void
lock_menu_cb (GtkAction *menu_action, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  if (s->debug_p) fprintf (stderr, "%s: lock menu\n", blurb());
  run_cmd (s, XA_LOCK, 0);
}


/* File menu / Kill daemon */
G_MODULE_EXPORT void
kill_menu_cb (GtkAction *menu_action, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  if (s->debug_p) fprintf (stderr, "%s: kill menu\n", blurb());
  run_cmd (s, XA_EXIT, 0);
}


/* File menu / Restart */
G_MODULE_EXPORT void
restart_menu_cb (GtkWidget *widget, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  int ac = 0;
  char *av[10];
  if (s->debug_p) fprintf (stderr, "%s: restart menu\n", blurb());
  if (!s->dpy) return;
  flush_dialog_changes_and_save (s);
  xscreensaver_command (s->dpy, XA_EXIT, 0, FALSE, NULL);
  sleep (1);

  av[ac++] = "xscreensaver";
  av[ac++] = "--splash";
  if (s->debug_p) av[ac++] = "--verbose";
  av[ac] = 0;
  fork_and_exec (s, ac, av);
  await_xscreensaver (s);
}


static Bool
xscreensaver_running_p (state *s)
{
  char *rversion = 0;
  if (!s->dpy) return FALSE;
  server_xscreensaver_version (s->dpy, &rversion, 0, 0);
  if (!rversion)
    return FALSE;
  free (rversion);
  return TRUE;
}

static void
await_xscreensaver (state *s)
{
  int countdown = 5;
  Bool ok = FALSE;

  while (!ok && (--countdown > 0))
    if (xscreensaver_running_p (s))
      ok = TRUE;
    else
      sleep (1);    /* If it's not there yet, wait a second... */

  sensitize_menu_items (s, TRUE);

  if (! ok)
    {
      /* Timed out, no screensaver running. */

      char buf [1024];
      Bool root_p = (geteuid () == 0);
      
      strcpy (buf, 
              _("The xscreensaver daemon did not start up properly.\n"
		"\n"));

      if (root_p)
        strcat (buf,
          _("You are running as root.  Don't do that.  Instead, you should\n"
            "log in as a normal user and use `sudo' as necessary.")
          );
      else
        strcat (buf, _("Please check your $PATH and permissions."));

      warning_dialog (s->window, _("Error"), buf);
    }
}


static int
selected_list_element (state *s)
{
  return s->_selected_list_element;
}


/* Write the settings to disk; call this only when changes have been made.
 */
static int
demo_write_init_file (state *s, saver_preferences *p)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);

  if (!write_init_file (s->dpy, p, s->short_version, FALSE))
    {
      if (s->debug_p)
        fprintf (stderr, "%s: wrote %s\n", blurb(), init_file_name());
      return 0;
    }
  else
    {
      const char *f = init_file_name();
      if (!f || !*f)
        warning_dialog (GTK_WINDOW (win), _("Error"), 
                        _("Couldn't determine init file name!\n"));
      else
        {
          char *b = (char *) malloc (strlen(f) + 1024);
          sprintf (b, _("Couldn't write %s\n"), f);
          warning_dialog (GTK_WINDOW (win), _("Error"), b);
          free (b);
        }
      return -1;
    }
}


/* The "Preview" button on the main page. */
G_MODULE_EXPORT void
run_this_cb (GtkButton *button, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  int list_elt = selected_list_element (s);
  if (list_elt < 0) return;
  if (s->debug_p) fprintf (stderr, "%s: preview button\n", blurb());
  flush_dialog_changes_and_save (s);
  run_hack (s, list_elt, TRUE);
}


static void
force_list_select_item (state *s, GtkWidget *list, int list_elt, Bool scroll_p)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  GtkWidget *parent = win->scroller;
  gboolean was = gtk_widget_get_sensitive (parent);
  GtkTreeIter iter;
  GtkTreeModel *model;
  GtkTreeSelection *selection;

  if (!was) gtk_widget_set_sensitive (parent, TRUE);
  model = gtk_tree_view_get_model (GTK_TREE_VIEW (list));
  if (!model) abort();
  if (gtk_tree_model_iter_nth_child (model, &iter, NULL, list_elt))
    {
      selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (list));
      gtk_tree_selection_select_iter (selection, &iter);
      if (s->debug_p)
        fprintf (stderr, "%s: select list elt %d\n", blurb(), list_elt);
    }
  if (scroll_p) ensure_selected_item_visible (s, GTK_WIDGET (list));
  if (!was) gtk_widget_set_sensitive (parent, FALSE);
}


/* The down arrow */
G_MODULE_EXPORT void
run_next_cb (GtkButton *button, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  Bool ops = s->preview_suppressed_p;
  GtkWidget *list_widget = win->list;
  int list_elt = selected_list_element (s);

  if (s->debug_p) fprintf (stderr, "%s: down arrow\n", blurb());

  if (list_elt < 0)
    list_elt = 0;
  else
    list_elt++;

  if (list_elt >= s->list_count)
    list_elt = 0;

  s->preview_suppressed_p = TRUE;

  flush_dialog_changes_and_save (s);
  force_list_select_item (s, list_widget, list_elt, TRUE);
  populate_demo_window (s, list_elt);
  populate_popup_window (s);
  run_hack (s, list_elt, FALSE);

  s->preview_suppressed_p = ops;
}


/* The up arrow */
G_MODULE_EXPORT void
run_prev_cb (GtkButton *button, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  Bool ops = s->preview_suppressed_p;
  GtkWidget *list_widget = win->list;
  int list_elt = selected_list_element (s);

  if (s->debug_p) fprintf (stderr, "%s: up arrow\n", blurb());

  if (list_elt < 0)
    list_elt = s->list_count - 1;
  else
    list_elt--;

  if (list_elt < 0)
    list_elt = s->list_count - 1;

  s->preview_suppressed_p = TRUE;

  flush_dialog_changes_and_save (s);
  force_list_select_item (s, list_widget, list_elt, TRUE);
  populate_demo_window (s, list_elt);
  populate_popup_window (s);
  run_hack (s, list_elt, FALSE);

  s->preview_suppressed_p = ops;
}


/* Writes the settings of the given hack into prefs.
   Returns true if there was a change, FALSE otherwise.
   command and/or visual may be 0, or enabled_p may be -1, meaning "no change".
 */
static Bool
flush_changes (state *s,
               int list_elt,
               int enabled_p,
               const char *command,
               const char *visual)
{
  saver_preferences *p = &s->prefs;
  Bool changed = FALSE;
  screenhack *hack;
  int hack_number;
  if (list_elt < 0 || list_elt >= s->list_count)
    abort();

  hack_number = s->list_elt_to_hack_number[list_elt];
  hack = p->screenhacks[hack_number];

  if (enabled_p != -1 &&
      enabled_p != hack->enabled_p)
    {
      hack->enabled_p = enabled_p;
      changed = TRUE;
      if (s->debug_p)
        fprintf (stderr, "%s: \"%s\": enabled => %d\n",
                 blurb(), hack->name, enabled_p);
    }

  if (command)
    {
      if (!hack->command || !!strcmp (command, hack->command))
        {
          if (hack->command) free (hack->command);
          hack->command = strdup (command);
          changed = TRUE;
          if (s->debug_p)
            fprintf (stderr, "%s: \"%s\": command => \"%s\"\n",
                     blurb(), hack->name, command);
        }
    }

  if (visual)
    {
      const char *ov = hack->visual;
      if (!ov || !*ov) ov = "any";
      if (!*visual) visual = "any";
      if (!!strcasecmp (visual, ov))
        {
          if (hack->visual) free (hack->visual);
          hack->visual = strdup (visual);
          changed = TRUE;
          if (s->debug_p)
            fprintf (stderr, "%s: \"%s\": visual => \"%s\"\n",
                     blurb(), hack->name, visual);
        }
    }

  return changed;
}


/* Helper for the text fields that contain time specifications:
   this parses the text, and does error checking.
 */
static void 
hack_time_text (state *s, const char *line, Time *store, Bool sec_p)
{
  if (*line)
    {
      int value;
      if (!sec_p || strchr (line, ':'))
        value = parse_time ((char *) line, sec_p, TRUE);
      else
        {
          char c;
          if (sscanf (line, "%d%c", &value, &c) != 1)
            value = -1;
          if (!sec_p)
            value *= 60;
        }

      value *= 1000;	/* Time measures in microseconds */
      if (value < 0)
	{
	  char b[255];
	  sprintf (b, _("Unparsable time format: \"%.100s\"\n"),
		   line);
	  warning_dialog (s->window, _("Error"), b);
	}
      else
	*store = value;
    }
}


typedef struct {
  state *s;
  int i;
  Bool *changed;
} FlushForeachClosure;

static gboolean
flush_checkbox (GtkTreeModel *model,
		GtkTreePath *path,
		GtkTreeIter *iter,
		gpointer data)
{
  FlushForeachClosure *closure = data;
  gboolean checked;

  gtk_tree_model_get (model, iter,
		      COL_ENABLED, &checked,
		      -1);

  if (flush_changes (closure->s, closure->i,
		     checked, 0, 0))
    *closure->changed = TRUE;
  
  closure->i++;

  /* don't remove row */
  return FALSE;
}


static char *
theme_name_strip (const char *s)
{
  const char *in = s;
  char *s2 = strdup(s);
  char *out = s2;
  for (; *in; in++)
    if (*in >= 'A' && *in <= 'Z')
      *out++ = *in + ('a'-'A');
    else if (*in == ' ' || *in == '\t')
      ;
    else
      *out++ = *in;
  *out = 0;
  return s2;
}


/* Flush out any changes made in the main dialog window (where changes
   take place immediately: clicking on a checkbox causes the init file
   to be written right away.)
 */
static Bool
flush_dialog_changes_and_save (state *s)
{
  saver_preferences *p = &s->prefs;
  saver_preferences P2, *p2 = &P2;
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  GtkTreeView *list_widget = GTK_TREE_VIEW (win->list);
  GtkTreeModel *model = gtk_tree_view_get_model (list_widget);
  FlushForeachClosure closure;
  Bool changed = FALSE;

  if (s->initializing_p) return FALSE;
  if (s->saving_p) return FALSE;
  s->saving_p = TRUE;

  *p2 = *p;

  /* Flush any checkbox changes in the list down into the s2 prefs struct.
   */
  closure.s = s;
  closure.changed = &changed;
  closure.i = 0;
  gtk_tree_model_foreach (model, flush_checkbox, &closure);

  /* Flush the non-hack-specific settings down into the prefs struct.
   */

# define SECONDS(PREF,WIDGET) \
    hack_time_text (s, gtk_entry_get_text (GTK_ENTRY (win->WIDGET)), \
                    &(PREF), TRUE)
# define MINUTES(PREF,WIDGET) \
    hack_time_text (s, gtk_entry_get_text (GTK_ENTRY (win->WIDGET)), \
                    &(PREF), FALSE)
# define CHECKBOX(PREF,WIDGET) \
    (PREF) = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (win->WIDGET))
# define PATHNAME(PREF,WIDGET,DIRP) \
    (PREF) = pathname_tilde ( \
      gtk_entry_get_text (GTK_ENTRY (win->WIDGET)), TRUE, (DIRP))
# define TEXT(PREF,WIDGET) \
    (PREF) = (char *) g_strdup (gtk_entry_get_text (GTK_ENTRY (win->WIDGET)))

  MINUTES  (p2->timeout,         timeout_spinbutton);
  MINUTES  (p2->cycle,           cycle_spinbutton);
  CHECKBOX (p2->lock_p,          lock_button);
  MINUTES  (p2->lock_timeout,    lock_spinbutton);

  CHECKBOX (p2->dpms_enabled_p,  dpms_button);
  CHECKBOX (p2->dpms_quickoff_p, dpms_quickoff_button);
  MINUTES  (p2->dpms_standby,    dpms_standby_spinbutton);
  MINUTES  (p2->dpms_suspend,    dpms_suspend_spinbutton);
  MINUTES  (p2->dpms_off,        dpms_off_spinbutton);

  CHECKBOX (p2->grab_desktop_p,  grab_desk_button);
  CHECKBOX (p2->grab_video_p,    grab_video_button);
  CHECKBOX (p2->random_image_p,  grab_image_button);
  PATHNAME (p2->image_directory, image_text, TRUE);

  {
    Bool v = FALSE;
    Bool od = s->debug_p;
    s->debug_p = FALSE;
    CHECKBOX (v, text_host_radio);    if (v) p2->tmode = TEXT_DATE;
    CHECKBOX (v, text_radio);         if (v) p2->tmode = TEXT_LITERAL;
    CHECKBOX (v, text_file_radio);    if (v) p2->tmode = TEXT_FILE;
    CHECKBOX (v, text_program_radio); if (v) p2->tmode = TEXT_PROGRAM;
    CHECKBOX (v, text_url_radio);     if (v) p2->tmode = TEXT_URL;
    s->debug_p = od;
  }

  TEXT     (p2->text_literal, text_entry);
  PATHNAME (p2->text_file,    text_file_entry,    FALSE);
  PATHNAME (p2->text_program, text_program_entry, FALSE);
  TEXT     (p2->text_url,     text_url_entry);

  CHECKBOX (p2->fade_p,       fade_button);
  CHECKBOX (p2->unfade_p,     unfade_button);
  SECONDS  (p2->fade_seconds, fade_spinbutton);

# undef SECONDS
# undef MINUTES
# undef CHECKBOX
# undef PATHNAME
# undef TEXT

  /* Map the mode menu to `saver_mode' enum values. */
  {
    GtkComboBox *opt = GTK_COMBO_BOX (win->mode_menu);
    int menu_elt = gtk_combo_box_get_active (opt);
    if (menu_elt < 0 || menu_elt >= countof(mode_menu_order)) abort();
    p2->mode = mode_menu_order[menu_elt];
  }

  if (p2->mode == ONE_HACK)
    {
      int list_elt = selected_list_element (s);
      p2->selected_hack = (list_elt >= 0
                           ? s->list_elt_to_hack_number[list_elt]
                           : -1);
    }

  /* Theme menu. */
  {
    GtkComboBox *cbox = GTK_COMBO_BOX (win->theme_menu);
    char *themes = get_string_resource (s->dpy, "themeNames", "ThemeNames");
    int menu_index = gtk_combo_box_get_active (cbox);
    char *token = themes ? themes : "default";
    char *name, *last;
    int i = 0;
    while ((name = strtok_r (token, ",", &last)))
      {
        token = 0;
        if (i == menu_index)
          {
            char *name2 = theme_name_strip (name);
            if (p->dialog_theme && !!strcmp (p->dialog_theme, name2))
              {
                free (p->dialog_theme);
                p2->dialog_theme = name2;
                if (s->debug_p)
                  fprintf (stderr, "%s:   theme => \"%s\"\n", blurb(),
                           p2->dialog_theme);
              }
            else
              {
                free (name2);
              }
          }
        i++;
      }
  }

  /* It is difficult to get "editing completed" events out of GtkEntry.
     I want something that fires on RET or focus-out, but I can't seem
     to find a consistent way to get that.  So let's fake it here.
   */
  if (!s->initializing_p &&
      !!strcmp (p->image_directory, p2->image_directory))
    {
      if (s->debug_p)
        fprintf (stderr, "%s: imagedir validating \"%s\" -> \"%s\"\n",
                 blurb(), p->image_directory, p2->image_directory);
      if (! validate_image_directory (s, p2->image_directory))
        {
          /* Don't save the bad new value into the preferences. */
          if (p2->image_directory != p->image_directory)
            free (p2->image_directory);
          p2->image_directory = strdup (p->image_directory);
        }
    }


  /* Copy any changes from p2 into p, and log them.
   */
# undef STR
# define STR(S) #S
# define COPY(FIELD)                                     \
  if (p->FIELD != p2->FIELD) {                           \
    changed = TRUE;                                      \
    if (s->debug_p)                                      \
      fprintf (stderr, "%s:   %s: %ld => %ld\n", blurb(),\
               STR(FIELD), (unsigned long) p->FIELD,     \
               (unsigned long) p2->FIELD);               \
  }                                                      \
  p->FIELD = p2->FIELD

  COPY(mode);
  COPY(selected_hack);

  COPY(timeout);
  COPY(cycle);
  COPY(lock_p);
  COPY(lock_timeout);

  COPY(dpms_enabled_p);
  COPY(dpms_quickoff_p);
  COPY(dpms_standby);
  COPY(dpms_suspend);
  COPY(dpms_off);

  COPY(tmode);

  COPY(install_cmap_p);
  COPY(fade_p);
  COPY(unfade_p);
  COPY(fade_seconds);

  COPY(grab_desktop_p);
  COPY(grab_video_p);
  COPY(random_image_p);

  COPY(dialog_theme);
# undef COPY

# define COPYSTR(FIELD)                                   \
  if (!p->FIELD ||                                        \
      !p2->FIELD ||                                       \
      strcmp(p->FIELD, p2->FIELD))                        \
    {                                                     \
      changed = TRUE;                                     \
      if (s->debug_p)                                     \
        fprintf (stderr, "%s:   %s => \"%s\"\n", blurb(), \
                 STR(FIELD), p2->FIELD);                  \
    }                                                     \
  if (p->FIELD && p->FIELD != p2->FIELD)                  \
    free (p->FIELD);                                      \
  p->FIELD = p2->FIELD;                                   \
  p2->FIELD = 0

  COPYSTR(image_directory);
  COPYSTR(text_literal);
  COPYSTR(text_file);
  COPYSTR(text_program);
  COPYSTR(text_url);
# undef COPYSTR

  populate_prefs_page (s);

  if (changed)
    {
      if (s->dpy)
        sync_server_dpms_settings (s->dpy, p);
      demo_write_init_file (s, p);

      /* Tell the xscreensaver daemon to wake up and reload the init file,
         in case the timeout has changed.  Without this, it would wait
         until the *old* timeout had expired before reloading. */
      if (s->debug_p)
        fprintf (stderr, "%s: command: DEACTIVATE\n", blurb());
      if (s->dpy)
        xscreensaver_command (s->dpy, XA_DEACTIVATE, 0, 0, 0);
    }

  s->saving_p = FALSE;

  return changed;
}


/* Called when any field in the prefs dialog may have been changed.
   Referenced by many items in demo.ui. */
G_MODULE_EXPORT gboolean
pref_changed_cb (GtkWidget *widget, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;

  if (s->debug_p)
    {
      if (s->flushing_p)
        fprintf (stderr, "%s:   (pref changed: %s)\n", blurb(),
                 gtk_widget_get_name (widget));
      else
        fprintf (stderr, "%s: pref changed: %s\n", blurb(),
                 gtk_widget_get_name (widget));
    }

  if (! s->flushing_p)
    {
      s->flushing_p = TRUE;
      flush_dialog_changes_and_save (s);
      s->flushing_p = FALSE;
    }
  return GDK_EVENT_PROPAGATE;
}


/* Same as pref_changed_cb but different. */
G_MODULE_EXPORT gboolean
pref_changed_event_cb (GtkWidget *widget, GdkEvent *event, gpointer user_data)
{
  pref_changed_cb (widget, user_data);
  return GDK_EVENT_PROPAGATE;
}


/* Called when the timeout or DPMS spinbuttons are changed, by demo.ui.
 */
G_MODULE_EXPORT gboolean
dpms_sanity_cb (GtkWidget *widget, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  Time timeout, standby, suspend, off;

  if (s->flushing_p) return GDK_EVENT_PROPAGATE;
  if (s->initializing_p) return GDK_EVENT_PROPAGATE;
  if (! s->dpms_supported_p) return GDK_EVENT_PROPAGATE;

  /* Read the current values from the four spinbuttons. */
# define MINUTES(V,WIDGET) \
    hack_time_text (s, gtk_entry_get_text (GTK_ENTRY (win->WIDGET)), \
                    &(V), FALSE)
  MINUTES (timeout, timeout_spinbutton);
  MINUTES (standby, dpms_standby_spinbutton);
  MINUTES (suspend, dpms_suspend_spinbutton);
  MINUTES (off,     dpms_off_spinbutton);
# undef MINUTES

  /* If the DPMS settings are non-zero, they must not go backwards:
     standby >= timeout (screen saver activation)
     suspend >= standby
     off     >= suspend
   */
# define MINUTES(V,LOWER,WIDGET) \
  if ((V) != 0 && (V) < LOWER) \
    gtk_spin_button_set_value (GTK_SPIN_BUTTON (win->WIDGET), \
                               (double) ((LOWER) + 59) / (60 * 1000))
  MINUTES (standby, timeout, dpms_standby_spinbutton);
  MINUTES (suspend, standby, dpms_suspend_spinbutton);
  MINUTES (off,     standby, dpms_off_spinbutton);
  MINUTES (off,     suspend, dpms_off_spinbutton);
# undef MINUTES

  return GDK_EVENT_PROPAGATE;
}


/* Same as dpms_sanity_cb but different. */
G_MODULE_EXPORT gboolean
dpms_sanity_event_cb (GtkWidget *widget, GdkEvent *event, gpointer user_data)
{
  dpms_sanity_cb (widget, user_data);
  return GDK_EVENT_PROPAGATE;
}



/* Callback on menu items in the "mode" options menu.
 */
G_MODULE_EXPORT void
mode_menu_item_cb (GtkWidget *widget, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  saver_preferences *p = &s->prefs;
  GtkWidget *list = win->list;
  int list_elt;

  int menu_index = gtk_combo_box_get_active (GTK_COMBO_BOX (widget));
  saver_mode new_mode = mode_menu_order[menu_index];

  if (s->flushing_p) return;  /* Called as a spurious side-effect */
  if (s->initializing_p) return;

  if (s->debug_p) fprintf (stderr, "%s: mode menu\n", blurb());

  /* Keep the same list element displayed as before; except if we're
     switching *to* "one screensaver" mode from any other mode, set
     "the one" to be that which is currently selected.
   */
  list_elt = selected_list_element (s);
  if (new_mode == ONE_HACK)
    p->selected_hack = s->list_elt_to_hack_number[list_elt];

  {
    saver_mode old_mode = p->mode;
    p->mode = new_mode;
    populate_demo_window (s, list_elt);
    populate_popup_window (s);
    force_list_select_item (s, list, list_elt, TRUE);
    p->mode = old_mode;  /* put it back, so the init file gets written */
  }

  pref_changed_cb (widget, user_data);
}


/* Remove the "random-same" item from the screen saver mode menu
   (we don't display that unless there are multiple screens.)
 */
static void
hide_mode_menu_random_same (state *s)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  GtkComboBox *opt = GTK_COMBO_BOX (win->mode_menu);
  GtkTreeModel *list = gtk_combo_box_get_model (opt);
  unsigned int i;
  for (i = 0; i < countof(mode_menu_order); i++)
    {
      if (mode_menu_order[i] == RANDOM_HACKS_SAME)
        {
          GtkTreeIter iter;
          gtk_tree_model_iter_nth_child (list, &iter, NULL, i);
          gtk_list_store_remove (GTK_LIST_STORE (list), &iter);
          break;
        }
    }

  /* recompute option-menu size */
  gtk_widget_unrealize (GTK_WIDGET (opt));
  gtk_widget_realize (GTK_WIDGET (opt));
}


/* Called when a new tab is selected. */
G_MODULE_EXPORT void
switch_page_cb (GtkNotebook *notebook, GtkWidget *page,
                gint page_num, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;

  if (s->debug_p) fprintf (stderr, "%s: tab changed\n", blurb());
  populate_prefs_page (s);
  pref_changed_cb (GTK_WIDGET (notebook), user_data);

  /* If we're switching to page 0, schedule the current hack to be run.
     Otherwise, schedule it to stop. */
  if (page_num == 0)
    {
      populate_demo_window (s, selected_list_element (s));
      populate_popup_window (s);
    }
  else
    schedule_preview (s, 0);
}


/* Called when a line is double-clicked in the saver list. */
static void
list_activated_cb (GtkTreeView *list, GtkTreePath *path,
		   GtkTreeViewColumn *column, gpointer data)
{
  state *s = data;
  char *str;
  int list_elt;

  if (s->debug_p) fprintf (stderr, "%s: list activated\n", blurb());

  /* I did this in Gtk 2 and I don't remember why:
     if (gdk_pointer_is_grabbed()) return;
     I don't understand how to use gdk_display_device_is_grabbed().
   */

  str = gtk_tree_path_to_string (path);
  list_elt = strtol (str, NULL, 10);
  g_free (str);

  if (list_elt >= 0)
    run_hack (s, list_elt, TRUE);
}

/* Called when a line is selected/highlighted in the saver list. */
static void
list_select_changed_cb (GtkTreeSelection *selection, gpointer data)
{
  state *s = (state *) data;
  GtkTreeModel *model;
  GtkTreeIter iter;
  GtkTreePath *path;
  char *str;
  int list_elt;
 
  if (s->debug_p) fprintf (stderr, "%s: list selection changed\n", blurb());

  if (!gtk_tree_selection_get_selected (selection, &model, &iter))
    return;

  path = gtk_tree_model_get_path (model, &iter);
  str = gtk_tree_path_to_string (path);
  list_elt = strtol (str, NULL, 10);

  gtk_tree_path_free (path);
  g_free (str);

  populate_demo_window (s, list_elt);

  /* Re-populate the Settings window any time a new item is selected
     in the list, in case both windows are currently visible. */
  populate_popup_window (s);

  flush_dialog_changes_and_save (s);
}


/* Called when the checkboxes that are in the left column of the
   scrolling list are clicked.  This both populates the right pane
   (just as clicking on the label (really, listitem) does) and
   also syncs this checkbox with  the right pane Enabled checkbox.
 */
static void
list_checkbox_cb (GtkCellRendererToggle *toggle,
		  gchar *path_string, gpointer data)
{
  state *s = (state *) data;

  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  GtkScrolledWindow *scroller = GTK_SCROLLED_WINDOW (win->scroller);
  GtkTreeView *list = GTK_TREE_VIEW (win->list);
  GtkTreeModel *model = gtk_tree_view_get_model (list);
  GtkTreePath *path = gtk_tree_path_new_from_string (path_string);
  GtkTreeIter iter;
  gboolean active;
  GtkAdjustment *adj;
  double scroll_top;

  int list_elt;

  if (s->debug_p) fprintf (stderr, "%s: list checkbox\n", blurb());

  if (!gtk_tree_model_get_iter (model, &iter, path))
    {
      g_warning ("bad path: %s", path_string);
      return;
    }
  gtk_tree_path_free (path);

  gtk_tree_model_get (model, &iter,
		      COL_ENABLED, &active,
		      -1);

  gtk_list_store_set (GTK_LIST_STORE (model), &iter,
		      COL_ENABLED, !active,
		      -1);

  list_elt = strtol (path_string, NULL, 10);  

  /* remember previous scroll position of the top of the list */
  adj = gtk_scrolled_window_get_vadjustment (scroller);
  scroll_top = gtk_adjustment_get_value (adj);

  flush_dialog_changes_and_save (s);
  force_list_select_item (s, GTK_WIDGET (list), list_elt, FALSE);
  populate_demo_window (s, list_elt);
  populate_popup_window (s);
  
  /* restore the previous scroll position of the top of the list.
     this is weak, but I don't really know why it's moving... */
  gtk_adjustment_set_value (adj, scroll_top);
}


/* If the directory or URL does not have images in it, pop up a warning
   dialog.  This happens at startup, and is fast.
 */
static void
validate_image_directory_quick (state *s)
{
  saver_preferences *p = &s->prefs;
  char *warn = 0;
  char buf[10240];

  if (!p->random_image_p) return;

  if (!p->image_directory || !*p->image_directory)
    warn = _("Image directory is unset");
  else if (!strncmp (p->image_directory, "http://", 7) ||
           !strncmp (p->image_directory, "https://", 8))
    warn = 0;
  else if (!directory_p (p->image_directory))
    warn = _("Image directory does not exist");
  else if (!image_files_p (p->image_directory, 10))
    warn = _("Image directory is empty");
  
  if (!warn) return;

  sprintf (buf,
    _("%.100s:\n\n"
      "        %.100s\n\n"
      "Select the 'Advanced' tab and choose a directory with some\n"
      "pictures in it, or you're going to see a lot of boring colorbars!"),
           warn,
           (p->image_directory ? p->image_directory : ""));
  warning_dialog (s->window, _("Warning"), buf);
}


/* "Cancel" button on the validate image directory progress dialog. */
static void
validate_image_directory_cancel_cb (GtkDialog *dialog, gint response_id,
                                    gpointer user_data)
{
  Bool *closed = (Bool *) user_data;
  *closed = TRUE;
  gtk_widget_destroy (GTK_WIDGET (dialog));
}


typedef struct {
  GtkWidget *dialog;
  int timer_id;
} validate_timer_closure;

static int
validate_timer_show (gpointer data)
{
  validate_timer_closure *vtc = (validate_timer_closure *) data;
  gtk_widget_show_all (vtc->dialog);
  vtc->timer_id = 0;
  return FALSE;
}


/* If the directory or URL does not have images in it, pop up a warning
   dialog and return false. This happens when the imageDirectory preference
   is edited, and might be slow.

   It does this by running "xscreensaver-getimage-file", which has the side
   effect of populating the image cache for that directory.  Since that will
   take a while if there are a lot of files, this also pops up a progress
   dialog with a spinner in it, and a cancel button.  That progress dialog
   only pops up if the validation has already been running for a little
   while, so that it doesn't flicker for small or pre-cached directories.
 */
static Bool
validate_image_directory (state *s, const char *path)
{
  validate_timer_closure vtc;
  char buf[1024];
  char err[1024];
  GtkWidget *dialog, *content_area, *label, *spinner;
  int margin = 32;
  Bool closed_p = FALSE;

  dialog = gtk_dialog_new_with_buttons (_("XScreenSaver Image Cache"),
                                        s->window,
                                        GTK_DIALOG_DESTROY_WITH_PARENT,
                                        _("_Cancel"), GTK_RESPONSE_CLOSE,
                                        NULL);

  sprintf (buf, _("Populating image cache for \"%.100s\"..."), path);
  content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
  label = gtk_label_new (buf);

  gtk_widget_set_margin_start (label, margin);
  gtk_widget_set_margin_end (label, margin);
  gtk_widget_set_margin_top (label, margin);
  gtk_widget_set_margin_bottom (label, margin / 2);
  gtk_container_add (GTK_CONTAINER (content_area), label);

  spinner = gtk_spinner_new();
  gtk_spinner_start (GTK_SPINNER (spinner));
  gtk_container_add (GTK_CONTAINER (content_area), spinner);
  gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);

  g_signal_connect (dialog, "response",
                    G_CALLBACK (validate_image_directory_cancel_cb),
                    &closed_p);
  g_signal_connect (dialog, "close",
                    G_CALLBACK (validate_image_directory_cancel_cb),
                    &closed_p);

  /* Only pop up the dialog box with the spinner if this has already taken
     a little while, so that if it completes immediately, we don't flicker.
   */
  vtc.dialog = dialog;
  vtc.timer_id = g_timeout_add (1000, validate_timer_show, &vtc);

  while (gtk_events_pending ())  /* Paint the window now. */
    gtk_main_iteration ();

  {
    pid_t forked;
    int fds [2];
    int in, out;

    char *av[10];
    int ac = 0;

    *err = 0;
    av[ac++] = "xscreensaver-getimage-file";
    av[ac++] = (char *) path;
    av[ac] = 0;

    if (pipe (fds))
      {
        strcpy (err, "error creating pipe");
        goto FAIL;
      }

    in = fds [0];
    out = fds [1];

    switch ((int) (forked = fork ()))
      {
      case -1:
        {
          strcpy (err, "couldn't fork");
          goto FAIL;
        }
      case 0:						/* Child fork */
        {
          int stderr_fd = 2;

          close (in);  /* don't need this one */
          if (! s->debug_p)
            close (fileno (stdout));
          close (ConnectionNumber (s->dpy));		/* close display fd */

          if (dup2 (out, stderr_fd) < 0)		/* pipe stdout */
            {
              perror ("could not dup() a new stderr:");
              exit (1);
            }

          execvp (av[0], av);			/* shouldn't return. */

          sprintf (buf, "%s: running %s", blurb(), av[0]);
          perror (buf);

          /* Note that one must use _exit() instead of exit() in procs forked
             off of Gtk programs -- Gtk installs an atexit handler that has a
             copy of the X connection (which we've already closed, for safety.)
             If one uses exit() instead of _exit(), then one sometimes gets a
             spurious "Gdk-ERROR: Fatal IO error on X server" error message.
          */
          _exit (1);                              /* exits fork */
          break;
        }
      default:						/* Parent fork */
        {
          char *ss = err;
          int bufsiz = sizeof(err);

          close (out);  /* don't need this one */

          if (s->debug_p)
            fprintf (stderr, "%s: forked %s\n", blurb(), av[0]);

          while (1)
            {
              fd_set rset;
              struct timeval tv;
              tv.tv_sec  = 0;
              tv.tv_usec = 1000000 / 10;    /* Repaint widgets at 10 fps */
              FD_ZERO (&rset);
              FD_SET (in, &rset);
              if (0 < select (in+1, &rset, 0, 0, &tv))
                {
                  int n = read (in, (void *) ss, bufsiz);
                  if (n <= 0)
                    {
                      if (s->debug_p)
                        fprintf (stderr, "%s: %s: read EOF\n", blurb(), av[0]);
                      break;
                    }
                  else
                    {
                      ss += n;
                      bufsiz -= n;
                      *ss = 0;

                      if (s->debug_p)
                        fprintf (stderr, "%s: %s: read: \"%s\"\n", blurb(),
                                 av[0], ss - n);
                    }
                }

              /* Service Gtk events and timers */
              while (gtk_events_pending ())
                gtk_main_iteration ();

              if (closed_p)
                {
                  kill (forked, SIGTERM);

                  if (s->debug_p)
                    fprintf (stderr, "%s: cancel\n", blurb());
                  break;
                }
            }

          *ss = 0;
          close (in);

          if (s->debug_p)
            fprintf (stderr, "%s: %s exited\n", blurb(), av[0]);

          /* Wait for the child to die. */
          {
            int wait_status = 0;
            waitpid (-1, &wait_status, 0);
          }
        }
      }
  }

  if (vtc.timer_id)   /* Remove the popup timer if it hasn't fired. */
    g_source_remove (vtc.timer_id);

  if (s->debug_p)
    fprintf (stderr, "%s: dismiss\n", blurb());

  if (! closed_p)
    gtk_widget_destroy (dialog);

 FAIL:
  if (*err)
    {
      warning_dialog (s->window, _("Warning"), err);
      return FALSE;
    }

  return TRUE;
}


/* Called when the imageDirectory text field is edited directly (focus-out).
 */
G_MODULE_EXPORT gboolean
image_text_pref_changed_event_cb (GtkWidget *widget, GdkEvent *event,
                                  gpointer user_data)
{
#if 0  /* This is handled in flush_dialog_changes_and_save now */
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  saver_preferences *p = &s->prefs;
  GtkEntry *w = GTK_ENTRY (win->image_text);
  const char *str = gtk_entry_get_text (w);
  char *path = pathname_tilde (str, TRUE, TRUE);

  if (s->debug_p) fprintf (stderr, "%s: imagedir text edited\n", blurb());

  if (p->image_directory && strcmp(p->image_directory, path))
    {
      if (s->debug_p)
        fprintf (stderr, "%s: imagedir validating \"%s\" -> \"%s\"\n", blurb(),
                 p->image_directory, path);
      if (! validate_image_directory (s, path))
        {
          /* Don't save the bad new value into the preferences. */
          free (path);
          return  GDK_EVENT_PROPAGATE;
        }
    }

  free (path);
# endif
  pref_changed_event_cb (widget, event, user_data);
  return GDK_EVENT_PROPAGATE;
}


/* Run a modal file selector dialog.
   Select a file, directory, or program.
   Normalize the resultant path and store it into the string pointer.
   Also update the text field with the new path.
   Returns true if any changes made.
 */
gboolean
file_chooser (GtkWindow *parent, GtkEntry *entry, char **retP,
              const char *title, gboolean verbose_p,
              gboolean dir_p, gboolean program_p)
{
  gint res;
  gboolean changed_p = FALSE;
  GtkWidget *dialog =
    gtk_file_chooser_dialog_new (title, parent,
                                 (dir_p
                                  ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER
                                  : GTK_FILE_CHOOSER_ACTION_OPEN),
                                 _("_Cancel"), GTK_RESPONSE_CANCEL,
                                 _("_Select"), GTK_RESPONSE_ACCEPT,
                                 NULL);
  const char *old = gtk_entry_get_text (entry);  /* not *retP */

  if (*old)
    {
      char *p2 = pathname_tilde (old, FALSE, dir_p);
      GFile *gf;

      /* If it's a command line and it begins with an absolute path,
         default to that file and its directory. */
      if (program_p && (*p2 == '/' || *p2 == '~'))
        {
          char *s = strpbrk (p2, " \t\r\n");
          if (s) *s = 0;
          program_p = FALSE;
        }

      gf = g_file_new_for_path (p2);
      if (! program_p)
        {
          gtk_file_chooser_set_file (GTK_FILE_CHOOSER (dialog), gf, NULL);
          if (verbose_p)
            fprintf (stderr, "%s:   chooser: default \"%s\"\n", blurb(), p2);
        }
      free (p2);
      g_object_unref (gf);
    }

  res = gtk_dialog_run (GTK_DIALOG (dialog));
  if (res == GTK_RESPONSE_ACCEPT)
    {
      GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
      char *str = gtk_file_chooser_get_filename (chooser);
      char *path = pathname_tilde (str, TRUE, dir_p);
      g_free (str);

      if (*retP && !strcmp (*retP, path))
        {
          if (verbose_p)
            fprintf (stderr, "%s:   chooser: unchanged\n", blurb());
          free (path);	/* no change */
        }
      else if (dir_p && !directory_p (path))
        {
          char b[255];
          sprintf (b, _("Directory does not exist: \"%.100s\"\n"), path);
          warning_dialog (parent, _("Error"), b);
          free (path);	/* no change */
        }
      else if (!dir_p && !file_p (path))
        {
          char b[255];
          sprintf (b, _("File does not exist: \"%.100s\"\n"), path);
          warning_dialog (parent, _("Error"), b);
          free (path);	/* no change */
        }
      else
        {
          if (verbose_p)
            fprintf (stderr, "%s:   chooser: \"%s\" -> \"%s\n",
                     blurb(), *retP, path);
          if (*retP) free (*retP);
          *retP = path;
          gtk_entry_set_text (entry, path);
          changed_p = TRUE;
        }
    }
  else if (verbose_p)
    fprintf (stderr, "%s:   chooser: cancelled\n", blurb());

  gtk_widget_destroy (dialog);
  return changed_p;
}


/* The "Browse" button next to the imageDirectory text field. */
G_MODULE_EXPORT void
browse_image_dir_cb (GtkButton *button, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  saver_preferences *p = &s->prefs;
  char *old = strdup (p->image_directory);

  if (s->debug_p) fprintf (stderr, "%s: imagedir browse button\n", blurb());
  if (file_chooser (GTK_WINDOW (win),
                    GTK_ENTRY (win->image_text),
                    &p->image_directory,
                    _("Please select the image directory."),
                    s->debug_p, TRUE, FALSE))
    {
      if (validate_image_directory (s, p->image_directory))
        demo_write_init_file (s, p);
      else
        {
          /* Don't save the bad new value into the preferences. */
          free (p->image_directory);
          p->image_directory = old;
          old = 0;
        }
    }

  if (old) free (old);
}


/* The "Browse" button next to the textFile text field. */
G_MODULE_EXPORT void
browse_text_file_cb (GtkButton *button, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  saver_preferences *p = &s->prefs;

  if (s->debug_p) fprintf (stderr, "%s: textfile browse button\n", blurb());
  if (file_chooser (GTK_WINDOW (win),
                    GTK_ENTRY (win->text_file_entry),
                    &p->text_file,
                    _("Please select a text file."),
                    s->debug_p, FALSE, FALSE))
    demo_write_init_file (s, p);
}


/* The "Browse" button next to the textProgram text field. */
G_MODULE_EXPORT void
browse_text_program_cb (GtkButton *button, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  saver_preferences *p = &s->prefs;

  if (s->debug_p) fprintf (stderr, "%s: textprogram browse button\n", blurb());
  if (file_chooser (GTK_WINDOW (win),
                    GTK_ENTRY (win->text_program_entry),
                    &p->text_program,
                    _("Please select a text-generating program."),
                    s->debug_p, FALSE, TRUE))
    demo_write_init_file (s, p);
}


/* The "Preview" button next to the Theme option menu. */
G_MODULE_EXPORT void
preview_theme_cb (GtkWidget *w, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  int ac = 0;
  char *av[10];

  if (s->debug_p) fprintf (stderr, "%s: preview theme button\n", blurb());

  /* Settings button is disabled with --splash --splash so that we don't
     end up with two copies of xscreensaver-settings running. */
  av[ac++] = "xscreensaver-auth";
  av[ac++] = "--splash";
  av[ac++] = "--splash";
  av[ac] = 0;
  fork_and_exec (s, ac, av);
}


/* The "Settings" button on the main page. */
G_MODULE_EXPORT void
settings_cb (GtkButton *button, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (user_data);
  state *s = &win->state;
  saver_preferences *p = &s->prefs;
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (s->dialog);
  int list_elt = selected_list_element (s);

  if (s->debug_p) fprintf (stderr, "%s: settings button\n", blurb());

  populate_demo_window (s, list_elt);   /* reset the widget */
  populate_popup_window (s);		/* create UI on popup window */

  /* Pre-select the "Standard" page. */
  settings_std_cb (NULL, s->dialog);
  settings_switch_page_cb (GTK_NOTEBOOK (dialog->opt_notebook), NULL, 0,
                           s->dialog);

  /* If there is no saved position for the dialog, position it to the
     right of the main window.  See also restore_window_position(),
     which already ran at startup. */
  {
    int win_x, win_y, dialog_x, dialog_y;
    char dummy;
    char *old = p->settings_geom;

    if (!old || !*old ||
        4 != sscanf (old, " %d , %d %d , %d %c",
                     &win_x, &win_y, &dialog_x, &dialog_y, &dummy))
      win_x = win_y = dialog_x = dialog_y = -1;
    
    if (dialog_x <= 0 && dialog_y <= 0)
      {
        int win_w, win_h;
        gtk_window_get_position (GTK_WINDOW (s->window), &win_x, &win_y);
        gtk_window_get_size (GTK_WINDOW (s->window), &win_w, &win_h);
        dialog_x = win_x + win_w + 8;
        dialog_y = win_y;
        gtk_window_move (GTK_WINDOW (s->dialog), dialog_x, dialog_y);
      }
  }

  gtk_widget_show (GTK_WIDGET (s->dialog));
}


/* Populating the various widgets
 */


/* Returns the number of the last hack run by the server.
 */
static int
server_current_hack (state *s)
{
  Atom type;
  int format;
  unsigned long nitems, bytesafter;
  unsigned char *dataP = 0;
  int hack_number = -1;

  if (!s->dpy) return hack_number;
  if (XGetWindowProperty (s->dpy, RootWindow(s->dpy, 0), /* always screen #0 */
                          XA_SCREENSAVER_STATUS,
                          0, 3, FALSE, XA_INTEGER,
                          &type, &format, &nitems, &bytesafter,
                          &dataP)
      == Success
      && type == XA_INTEGER
      && nitems >= 3
      && dataP)
    {
      PROP32 *data = (PROP32 *) dataP;
      hack_number = (int) data[2] - 1;
    }

  if (dataP) XFree (dataP);

  return hack_number;
}


/* Finds the number of the last hack that was run, and makes that item be
   selected by default.
 */
static void
scroll_to_current_hack (state *s)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  saver_preferences *p = &s->prefs;
  int hack_number = -1;

  if (p->mode == ONE_HACK)		   /* in "one" mode, use the one */
    hack_number = p->selected_hack;
  if (hack_number < 0)			   /* otherwise, use the last-run */
    hack_number = server_current_hack (s);
  if (hack_number < 0)			   /* failing that, last "one mode" */
    hack_number = p->selected_hack;
  if (hack_number < 0)			   /* failing that, newest hack. */
    {
      /* We should only get here if the user does not have a .xscreensaver
         file, and the screen has not been blanked with a hack since X
         started up: in other words, this is probably a fresh install.

         Instead of just defaulting to hack #0 (in either "programs" or
         "alphabetical" order) let's try to default to the last runnable
         hack in the "programs" list: this is probably the hack that was
         most recently added to the xscreensaver distribution (and so
         it's probably the currently-coolest one!)
       */
      hack_number = p->screenhacks_count-1;
      while (hack_number > 0 &&
             ! (s->hacks_available_p[hack_number] &&
                p->screenhacks[hack_number]->enabled_p))
        hack_number--;
    }

  if (hack_number >= 0 && hack_number < p->screenhacks_count)
    {
      int list_elt = s->hack_number_to_list_elt[hack_number];
      GtkWidget *list = win->list;
      force_list_select_item (s, list, list_elt, TRUE);
      populate_demo_window (s, list_elt);
      populate_popup_window (s);
    }
}


static void
populate_hack_list (state *s)
{
  saver_preferences *p = &s->prefs;
  GtkTreeView *list = GTK_TREE_VIEW (XSCREENSAVER_WINDOW (s->window)->list);
  GtkListStore *model;
  GtkTreeSelection *selection;
  GtkCellRenderer *ren;
  GtkTreeIter iter;
  int i;

  g_object_get (G_OBJECT (list),
		"model", &model,
		NULL);
  if (!model)
    {
      model = gtk_list_store_new (COL_LAST, G_TYPE_BOOLEAN, G_TYPE_STRING);
      g_object_set (G_OBJECT (list), "model", model, NULL);
      g_object_unref (model);

      ren = gtk_cell_renderer_toggle_new ();
      gtk_tree_view_insert_column_with_attributes (list, COL_ENABLED,
						   _("Use"), ren,
						   "active", COL_ENABLED,
						   NULL);

      g_signal_connect (ren, "toggled",
			G_CALLBACK (list_checkbox_cb),
			s);

      ren = gtk_cell_renderer_text_new ();
      gtk_tree_view_insert_column_with_attributes (list, COL_NAME,
						   _("Screen Saver"), ren,
						   "markup", COL_NAME,
						   NULL);

      g_signal_connect_after (list, "row_activated",
			      G_CALLBACK (list_activated_cb),
			      s);

      selection = gtk_tree_view_get_selection (list);
      g_signal_connect (selection, "changed",
			G_CALLBACK (list_select_changed_cb),
			s);

    }

  for (i = 0; i < s->list_count; i++)
    {
      int hack_number = s->list_elt_to_hack_number[i];
      screenhack *hack = (hack_number < 0 ? 0 : p->screenhacks[hack_number]);
      char *pretty_name;
      Bool available_p = (hack && s->hacks_available_p [hack_number]);

      if (!hack) continue;

      /* If we're to suppress uninstalled hacks, check $PATH now. */
      if (p->ignore_uninstalled_p && !available_p)
        continue;

      pretty_name = (hack->name
                     ? strdup (hack->name)
                     : make_hack_name (s->dpy, hack->command));

      if (!available_p)
        {
          /* Make the text foreground be the color of insensitive widgets
             (but don't actually make it be insensitive, since we still
             want to be able to click on it.)
           */
          GtkStyleContext *c =
            gtk_widget_get_style_context (GTK_WIDGET (list));
          GdkRGBA fg;
          char *buf = (char *) malloc (strlen (pretty_name) + 100);
          gtk_style_context_get_color (c, GTK_STATE_FLAG_INSENSITIVE, &fg);
          sprintf (buf, "<span foreground=\"#%02X%02X%02X\">%s</span>",
                   (unsigned int) (0xFF * fg.red),
                   (unsigned int) (0xFF * fg.green),
                   (unsigned int) (0xFF * fg.blue),
                   pretty_name);
          free (pretty_name);
          pretty_name = buf;
        }

      gtk_list_store_append (model, &iter);
      gtk_list_store_set (model, &iter,
			  COL_ENABLED, hack->enabled_p,
			  COL_NAME, pretty_name,
			  -1);
      free (pretty_name);
    }
}


static void
update_list_sensitivity (state *s)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  saver_preferences *p = &s->prefs;
  Bool sensitive = (p->mode == RANDOM_HACKS ||
                    p->mode == RANDOM_HACKS_SAME ||
                    p->mode == ONE_HACK);
  Bool checkable = (p->mode == RANDOM_HACKS ||
                    p->mode == RANDOM_HACKS_SAME);
  Bool blankable = (p->mode != DONT_BLANK);

  GtkTreeView *list      = GTK_TREE_VIEW (win->list);
  GtkTreeViewColumn *use = gtk_tree_view_get_column (list, COL_ENABLED);

  gtk_widget_set_sensitive (GTK_WIDGET (win->scroller), sensitive);
  gtk_widget_set_sensitive (GTK_WIDGET (win->next_prev_hbox),  sensitive);
  gtk_widget_set_sensitive (GTK_WIDGET (win->blanking_table),  blankable);
  gtk_tree_view_column_set_visible (use, checkable);
}


static void
populate_prefs_page (state *s)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  saver_preferences *p = &s->prefs;

  Bool can_lock_p = TRUE;

# ifdef NO_LOCKING
  can_lock_p = FALSE;
# endif

  /* If there is only one screen, the mode menu contains
     "random" but not "random-same".
   */
  if (!s->multi_screen_p && p->mode == RANDOM_HACKS_SAME)
    p->mode = RANDOM_HACKS;


  /* The file supports timeouts of less than a minute, but the GUI does
     not, so throttle the values to be at least one minute (since "0" is
     a bad rounding choice...)
   */
# define THROTTLE(NAME) if (p->NAME != 0 && p->NAME < 60000) p->NAME = 60000
  THROTTLE (timeout);
  THROTTLE (cycle);
  /* THROTTLE (passwd_timeout); */  /* GUI doesn't set this; leave it alone */
# undef THROTTLE

# define FMT_MINUTES(NAME,N) \
    gtk_spin_button_set_value (GTK_SPIN_BUTTON (win->NAME), \
                               (double) ((N) + 59) / (60 * 1000))
# define FMT_SECONDS(NAME,N) \
    gtk_spin_button_set_value (GTK_SPIN_BUTTON (win->NAME), \
                               (double) ((N) / 1000))

  FMT_MINUTES (timeout_spinbutton,      p->timeout);
  FMT_MINUTES (cycle_spinbutton,        p->cycle);
  FMT_MINUTES (lock_spinbutton,         p->lock_timeout);
  FMT_MINUTES (dpms_standby_spinbutton, p->dpms_standby);
  FMT_MINUTES (dpms_suspend_spinbutton, p->dpms_suspend);
  FMT_MINUTES (dpms_off_spinbutton,     p->dpms_off);
  FMT_SECONDS (fade_spinbutton,         p->fade_seconds);
# undef FMT_MINUTES
# undef FMT_SECONDS

# define TOGGLE_ACTIVE(NAME,ACTIVEP) \
    gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (win->NAME), (ACTIVEP))

  TOGGLE_ACTIVE (lock_button,       p->lock_p);
  TOGGLE_ACTIVE (dpms_button,       p->dpms_enabled_p && s->dpms_supported_p);
  TOGGLE_ACTIVE (dpms_quickoff_button, (p->dpms_quickoff_p &&
                                        s->dpms_supported_p));
  TOGGLE_ACTIVE (grab_desk_button,  p->grab_desktop_p);
  TOGGLE_ACTIVE (grab_video_button, p->grab_video_p);
  TOGGLE_ACTIVE (grab_image_button, p->random_image_p);
  TOGGLE_ACTIVE (fade_button,       p->fade_p);
  TOGGLE_ACTIVE (unfade_button,     p->unfade_p);

  switch (p->tmode)
    {
    case TEXT_LITERAL: TOGGLE_ACTIVE (text_radio,         TRUE); break;
    case TEXT_FILE:    TOGGLE_ACTIVE (text_file_radio,    TRUE); break;
    case TEXT_PROGRAM: TOGGLE_ACTIVE (text_program_radio, TRUE); break;
    case TEXT_URL:     TOGGLE_ACTIVE (text_url_radio,     TRUE); break;
    default:           TOGGLE_ACTIVE (text_host_radio,    TRUE); break;
    }

# undef TOGGLE_ACTIVE

  gtk_entry_set_text (GTK_ENTRY (win->image_text),
                      (p->image_directory ? p->image_directory : ""));
  gtk_widget_set_sensitive (win->image_text, p->random_image_p);
  gtk_widget_set_sensitive (win->image_browse_button,
                            p->random_image_p);

  gtk_entry_set_text (GTK_ENTRY (win->text_entry),
                      (p->text_literal ? p->text_literal : ""));
  gtk_entry_set_text (GTK_ENTRY (win->text_file_entry),
                      (p->text_file ? p->text_file : ""));
  gtk_entry_set_text (GTK_ENTRY (win->text_program_entry),
                      (p->text_program ? p->text_program : ""));
  gtk_entry_set_text (GTK_ENTRY (win->text_url_entry),
                      (p->text_url ? p->text_url : ""));

  gtk_widget_set_sensitive (win->text_entry,
                            p->tmode == TEXT_LITERAL);
  gtk_widget_set_sensitive (win->text_file_entry,
                            p->tmode == TEXT_FILE);
  gtk_widget_set_sensitive (win->text_file_browse,
                            p->tmode == TEXT_FILE);
  gtk_widget_set_sensitive (win->text_program_entry,
                            p->tmode == TEXT_PROGRAM);
  gtk_widget_set_sensitive (win->text_program_browse,
                            p->tmode == TEXT_PROGRAM);
  gtk_widget_set_sensitive (win->text_url_entry,
                            p->tmode == TEXT_URL);

  /* Theme menu */
  {
    GtkComboBox *cbox = GTK_COMBO_BOX (win->theme_menu);

    if (cbox)
      {
        char *themes = get_string_resource(s->dpy, "themeNames", "ThemeNames");
        char *token = themes ? themes : strdup ("default");
        char *name, *last = 0;
        GtkListStore *model;
        GtkTreeIter iter;
        int i = 0;
        /* Bad things happen if we do these things more than once. */
        static Bool model_built_p = FALSE;
        static Bool signal_connected_p = FALSE;

        if (! model_built_p)
          {
            g_object_get (G_OBJECT (cbox), "model", &model, NULL);
            if (!model) abort();
            gtk_list_store_clear (model);
          }

        while ((name = strtok_r (token, ",", &last)))
          {
            char *name2;
            int L;
            token = 0;

            /* Strip leading and trailing whitespace */
            while (*name == ' ' || *name == '\t' || *name == '\n')
              name++;
            L = strlen(name);
            while (L && (name[L-1] == ' ' || name[L-1] == '\t' ||
                         name[L-1] == '\n'))
              name[--L] = 0;

            if (! model_built_p)
              {
                gtk_list_store_append (model, &iter);
                gtk_list_store_set (model, &iter, 0, name, -1);
              }

            name2 = theme_name_strip (name);
            if (p->dialog_theme && name2 && !strcmp (p->dialog_theme, name2))
              gtk_combo_box_set_active (cbox, i);
            free (name2);
            i++;
          }

        model_built_p = TRUE;

        if (! signal_connected_p)
          {
            g_signal_connect (G_OBJECT (cbox), "changed",
                              G_CALLBACK (pref_changed_cb), (gpointer) win);
            signal_connected_p = TRUE;
          } 
      }
  }

  /* Map the `saver_mode' enum to mode menu to values. */
  {
    GtkComboBox *opt = GTK_COMBO_BOX (win->mode_menu);

    int i;
    for (i = 0; i < countof(mode_menu_order); i++)
      if (mode_menu_order[i] == p->mode)
        break;
    gtk_combo_box_set_active (opt, i);
    update_list_sensitivity (s);
  }

# define SENSITIZE(NAME,SENSITIVEP) \
  gtk_widget_set_sensitive (win->NAME, (SENSITIVEP))

  /* Blanking and Locking
   */
  SENSITIZE (lock_button,     can_lock_p);
  SENSITIZE (lock_spinbutton, can_lock_p && p->lock_p);
  SENSITIZE (lock_mlabel,     can_lock_p && p->lock_p);

  /* DPMS
   */
  SENSITIZE (dpms_button,            s->dpms_supported_p);
  SENSITIZE (dpms_standby_label,     s->dpms_supported_p && p->dpms_enabled_p);
  SENSITIZE (dpms_standby_mlabel,    s->dpms_supported_p && p->dpms_enabled_p);
  SENSITIZE (dpms_standby_spinbutton,s->dpms_supported_p && p->dpms_enabled_p);
  SENSITIZE (dpms_suspend_label,     s->dpms_supported_p && p->dpms_enabled_p);
  SENSITIZE (dpms_suspend_mlabel,    s->dpms_supported_p && p->dpms_enabled_p);
  SENSITIZE (dpms_suspend_spinbutton,s->dpms_supported_p && p->dpms_enabled_p);
  SENSITIZE (dpms_off_label,         s->dpms_supported_p && p->dpms_enabled_p);
  SENSITIZE (dpms_off_mlabel,        s->dpms_supported_p && p->dpms_enabled_p);
  SENSITIZE (dpms_off_spinbutton,    s->dpms_supported_p && p->dpms_enabled_p);
  SENSITIZE (dpms_quickoff_button,   s->dpms_supported_p);

  SENSITIZE (fade_label,      (p->fade_p || p->unfade_p));
  SENSITIZE (fade_spinbutton, (p->fade_p || p->unfade_p));

# undef SENSITIZE

  if (!s->dpms_supported_p)
    gtk_frame_set_label (GTK_FRAME (win->dpms_frame),
      _("Display Power Management (not supported by this display)"));
}


/* Creates a human-readable anchor to put on a URL.
 */
static char *
anchorize (const char *url)
{
  const char *wiki1 =  "http://en.wikipedia.org/wiki/";
  const char *wiki2 = "https://en.wikipedia.org/wiki/";
  const char *math1 =  "http://mathworld.wolfram.com/";
  const char *math2 = "https://mathworld.wolfram.com/";
  if (!strncmp (wiki1, url, strlen(wiki1)) ||
      !strncmp (wiki2, url, strlen(wiki2))) {
    char *anchor = (char *) malloc (strlen(url) * 3 + 10);
    const char *in;
    char *out;
    strcpy (anchor, "Wikipedia: \"");
    in = url + strlen(!strncmp (wiki1, url, strlen(wiki1)) ? wiki1 : wiki2);
    out = anchor + strlen(anchor);
    while (*in) {
      if (*in == '_') {
        *out++ = ' ';
      } else if (*in == '#') {
        *out++ = ':';
        *out++ = ' ';
      } else if (*in == '%') {
        char hex[3];
        unsigned int n = 0;
        hex[0] = in[1];
        hex[1] = in[2];
        hex[2] = 0;
        sscanf (hex, "%x", &n);
        *out++ = (char) n;
        in += 2;
      } else {
        *out++ = *in;
      }
      in++;
    }
    *out++ = '"';
    *out = 0;
    return anchor;

  } else if (!strncmp (math1, url, strlen(math1)) ||
             !strncmp (math2, url, strlen(math2))) {
    char *anchor = (char *) malloc (strlen(url) * 3 + 10);
    const char *start, *in;
    char *out;
    strcpy (anchor, "MathWorld: \"");
    start = url + strlen(!strncmp (math1, url, strlen(math1)) ? math1 : math2);
    in = start;
    out = anchor + strlen(anchor);
    while (*in) {
      if (*in == '_') {
        *out++ = ' ';
      } else if (in != start && *in >= 'A' && *in <= 'Z') {
        *out++ = ' ';
        *out++ = *in;
      } else if (!strncmp (in, ".htm", 4)) {
        break;
      } else {
        *out++ = *in;
      }
      in++;
    }
    *out++ = '"';
    *out = 0;
    return anchor;

  } else {
    return strdup (url);
  }
}


/* Quote the text as HTML and make URLs be clickable links. 
 */
static char *
hreffify (const char *in)
{
  char *ret, *out;
  if (!in) return 0;

  ret = out = malloc (strlen(in) * 3);
  while (*in)
    {
      if (!strncmp (in, "http://", 7) ||
          !strncmp (in, "https://", 8))
        {
          char *url, *anchor;
          const char *end = in;
          while (*end &&
                 *end != ' ' && *end != '\t' && *end != '\r' && *end != '\n')
            end++;

          url = (char *) malloc (end - in + 1);
          strncpy (url, in, end-in);
          url [end-in] = 0;

          anchor = anchorize (url);

          strcpy (out, "<a href=\""); out += strlen (out);
          strcpy (out, url);          out += strlen (out);
          strcpy (out, "\">");        out += strlen (out);
          strcpy (out, anchor);       out += strlen (out);
          strcpy (out, "</a>"); out += strlen (out);
          free (url);
          free (anchor);
          in = end;
        }
      else if (*in == '<')
        {
          strcpy (out, "&lt;");
          out += strlen (out);
          in++;
        }
      else if (*in == '>')
        {
          strcpy (out, "&gt;");
          out += strlen (out);
          in++;
        }
      else if (*in == '&')
        {
          strcpy (out, "&amp;");
          out += strlen (out);
          in++;
        }
      else
        {
          *out++ = *in++;
        }
    }
  *out = 0;
  return ret;
}


static void
sensitize_menu_items (state *s, Bool force_p)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  static Bool running_p = FALSE;
  static time_t last_checked = 0;
  time_t now = time ((time_t *) 0);

  if (force_p || now > last_checked + 10)   /* check every 10 seconds */
    {
      running_p = xscreensaver_running_p (s);
      last_checked = time ((time_t *) 0);
    }

  gtk_widget_set_sensitive (win->activate_menuitem, running_p);
  gtk_widget_set_sensitive (win->lock_menuitem, running_p);
  gtk_widget_set_sensitive (win->kill_menuitem, running_p);

  gtk_menu_item_set_label (GTK_MENU_ITEM (win->restart_menuitem),
                           (running_p
                            ? _("Restart Daemon")
                            : _("Launch Daemon")));
}


/* Fill in the contents of the main window, and a few things on the
   settings dialog.
 */
static void
populate_demo_window (state *s, int list_elt)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (s->dialog);
  saver_preferences *p = &s->prefs;
  screenhack *hack;
  char *pretty_name;
  GtkFrame *frame1 = GTK_FRAME (win->preview_frame);
  GtkFrame *frame2 = dialog ? GTK_FRAME (dialog->opt_frame) : 0;
  GtkEntry *cmd    = dialog ? GTK_ENTRY (dialog->cmd_text)  : 0;
  GtkComboBox *vis = dialog ? GTK_COMBO_BOX (dialog->visual_combo) : 0;

  /* Enforce a minimum size on the preview pane. */
  if (s->dpy)
    {
      int dw = DisplayWidth (s->dpy, 0);
      int dh = DisplayHeight (s->dpy, 0);
      int minw, minh;
 # define TRY(W) do { \
        minw = (W); minh = minw * 9/16 + 48;    \
        if (dw > minw * 1.5 && dh > minh * 1.5) \
          gtk_widget_set_size_request (GTK_WIDGET (frame1), minw, minh); \
        } while(0)
      TRY (300);
      TRY (400);
      TRY (480);
      TRY (640);
      TRY (800);
    /*  TRY (960); */
# undef TRY
    }

  if (p->mode == BLANK_ONLY)
    {
      hack = 0;
      pretty_name = strdup (_("Blank Screen"));
      schedule_preview (s, 0);
    }
  else if (p->mode == DONT_BLANK)
    {
      hack = 0;
      pretty_name = strdup (_("Screen Saver Disabled"));
      schedule_preview (s, 0);
    }
  else
    {
      int hack_number = (list_elt >= 0 && list_elt < s->list_count
                         ? s->list_elt_to_hack_number[list_elt]
                         : -1);
      hack = (hack_number >= 0 ? p->screenhacks[hack_number] : 0);

      pretty_name = (hack
                     ? (hack->name
                        ? strdup (hack->name)
                        : make_hack_name (s->dpy, hack->command))
                     : 0);

      if (hack)
        schedule_preview (s, hack->command);
      else
        schedule_preview (s, 0);
    }

  if (!pretty_name)
    pretty_name = strdup (_("Preview"));

  if (dialog->unedited_cmdline) free (dialog->unedited_cmdline);
  dialog->unedited_cmdline = strdup (hack ? hack->command : "");

  gtk_frame_set_label (frame1, _(pretty_name));
  if (frame2)
    gtk_frame_set_label (frame2, _(pretty_name));
  if (cmd)
    gtk_entry_set_text (cmd, dialog->unedited_cmdline);

  {
    char title[255];
    sprintf (title, _("%s: %.100s Settings"),
             progclass, (pretty_name ? pretty_name : "???"));
    gtk_window_set_title (GTK_WINDOW (s->window), title);
    if (s->dialog)
      gtk_window_set_title (GTK_WINDOW (s->dialog), title);
  }

  /* Fill in the Visual combo-box */
  if (vis)
    gtk_entry_set_text (GTK_ENTRY (gtk_bin_get_child (GTK_BIN (vis))),
                        (hack
                         ? (hack->visual && *hack->visual
                            ? hack->visual
                            : _("Any"))
                         : ""));

  sensitize_demo_widgets (s, (hack ? TRUE : FALSE));

  if (pretty_name) free (pretty_name);

  /* This causes the window to scroll out from under the mouse when 
     clicking on an item, vertically centering it. That's annoying. */
  /* ensure_selected_item_visible (s, list); */

  s->_selected_list_element = list_elt;
}


static void
widget_deleter (GtkWidget *widget, gpointer data)
{
  gtk_widget_destroy (widget);
}


static char **sort_hack_cmp_names_kludge;
static int
sort_hack_cmp (const void *a, const void *b)
{
  if (a == b)
    return 0;
  else
    {
      int aa = *(int *) a;
      int bb = *(int *) b;
      const char last[] = "\377\377\377\377\377\377\377\377\377\377\377";
      return strcmp ((aa < 0 ? last : sort_hack_cmp_names_kludge[aa]),
                     (bb < 0 ? last : sort_hack_cmp_names_kludge[bb]));
    }
}


static void
initialize_sort_map (state *s)
{
  saver_preferences *p = &s->prefs;
  int i, j;

  if (s->list_elt_to_hack_number) free (s->list_elt_to_hack_number);
  if (s->hack_number_to_list_elt) free (s->hack_number_to_list_elt);
  if (s->hacks_available_p) free (s->hacks_available_p);

  s->list_elt_to_hack_number = (int *)
    calloc (sizeof(int), p->screenhacks_count + 1);
  s->hack_number_to_list_elt = (int *)
    calloc (sizeof(int), p->screenhacks_count + 1);
  s->hacks_available_p = (Bool *)
    calloc (sizeof(Bool), p->screenhacks_count + 1);
  s->total_available = 0;

  /* Check which hacks actually exist on $PATH
   */
  for (i = 0; i < p->screenhacks_count; i++)
    {
      screenhack *hack = p->screenhacks[i];
      int on = on_path_p (hack->command) ? 1 : 0;
      s->hacks_available_p[i] = on;
      s->total_available += on;
    }

  /* Initialize list->hack table to unsorted mapping, omitting nonexistent
     hacks, if desired.
   */
  j = 0;
  for (i = 0; i < p->screenhacks_count; i++)
    {
      if (!p->ignore_uninstalled_p ||
          s->hacks_available_p[i])
        s->list_elt_to_hack_number[j++] = i;
    }
  s->list_count = j;

  for (; j < p->screenhacks_count; j++)
    s->list_elt_to_hack_number[j] = -1;


  /* Generate list of sortable names (once)
   */
  sort_hack_cmp_names_kludge = (char **)
    calloc (sizeof(char *), p->screenhacks_count);
  for (i = 0; i < p->screenhacks_count; i++)
    {
      screenhack *hack = p->screenhacks[i];
      char *name = (hack->name && *hack->name
                    ? strdup (hack->name)
                    : make_hack_name (s->dpy, hack->command));
      gchar *s2 = g_str_to_ascii (name, 0);  /* Sort "Möbius" properly */
      gchar *s3 = g_ascii_strdown (s2, -1);
      free (name);
      free (s2);
      sort_hack_cmp_names_kludge[i] = s3;
    }

  /* Sort list->hack map alphabetically
   */
  qsort (s->list_elt_to_hack_number,
         p->screenhacks_count,
         sizeof(*s->list_elt_to_hack_number),
         sort_hack_cmp);

  /* Free names
   */
  for (i = 0; i < p->screenhacks_count; i++)
    free (sort_hack_cmp_names_kludge[i]);
  free (sort_hack_cmp_names_kludge);
  sort_hack_cmp_names_kludge = 0;

  /* Build inverse table */
  for (i = 0; i < p->screenhacks_count; i++)
    {
      int n = s->list_elt_to_hack_number[i];
      if (n != -1)
        s->hack_number_to_list_elt[n] = i;
    }
}


static int
maybe_reload_init_file (state *s)
{
  saver_preferences *p = &s->prefs;
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  int status = 0;

  static Bool reentrant_lock = FALSE;
  if (reentrant_lock) return 0;
  reentrant_lock = TRUE;

  if (init_file_changed_p (p))
    {
      const char *f = init_file_name();
      char *b;
      int list_elt;
      GtkWidget *list;

      if (!f || !*f) return 0;
      b = (char *) malloc (strlen(f) + 1024);
      sprintf (b, _("file \"%s\" has changed, reloading.\n"), f);
      warning_dialog (s->window, _("Warning"), b);
      free (b);

      load_init_file (s->dpy, p);
      initialize_sort_map (s);

      list_elt = selected_list_element (s);
      list = win->list;
      gtk_container_foreach (GTK_CONTAINER (list), widget_deleter, NULL);
      populate_hack_list (s);
      force_list_select_item (s, list, list_elt, TRUE);
      populate_prefs_page (s);
      populate_demo_window (s, list_elt);
      populate_popup_window (s);
      ensure_selected_item_visible (s, list);

      status = 1;
    }

  reentrant_lock = FALSE;
  return status;
}


/* Making the preview window have the right X visual (so that GL works.)
 */

static Visual *get_best_gl_visual (state *);

static GdkVisual *
x_visual_to_gdk_visual (GdkWindow *win, Visual *xv)
{
  if (xv)
    {
      GdkScreen *screen = gdk_window_get_screen (win);
      GList *gvs = gdk_screen_list_visuals (screen);
      /* This list is sometimes NULL, not even the default visual! */
      for (; gvs; gvs = gvs->next)
        {
          GdkVisual *gv = (GdkVisual *) gvs->data;
          if (xv == GDK_VISUAL_XVISUAL (gv))
            return gv;
        }
    }
  return 0;
}


static void
clear_preview_window (state *s)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  int list_elt = selected_list_element (s);
  int hack_number = (list_elt >= 0
                     ? s->list_elt_to_hack_number[list_elt]
                     : -1);
  Bool available_p = (hack_number >= 0
                      ? s->hacks_available_p [hack_number]
                      : TRUE);
  Bool nothing_p = (s->total_available < 5);

  GtkWidget *notebook = win->preview_notebook;
  gtk_notebook_set_current_page (GTK_NOTEBOOK (notebook),
                                 (!s->running_preview_error_p ? 0 :  /* ok */
                                  nothing_p    ? 3 : /* no hacks installed */
                                  !available_p ? 2 : /* hack not installed */
                                  s->wayland_p ? 4 : /* fucking wayland */
                                  1));		     /* preview failed */
}


static gboolean
preview_resize_cb (GtkWidget *self, GdkEvent *event, gpointer data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (data);
  state *s = &win->state;

  /* If a subproc is running, clear the window to black when we resize.
     Without this, sometimes turds get left behind. */
  if (s->dpy && !s->wayland_p && s->running_preview_cmd)
    {
      GdkWindow *window = gtk_widget_get_window (self);
      Window id;
      XWindowAttributes xgwa;
      XGCValues gcv;
      GC gc;

      if (! window) return TRUE;
      id = gdk_x11_window_get_xid (window);
      if (! id) return TRUE;

      /* Not sure why XClearWindow is insufficient here, but it is. */
      XGetWindowAttributes (s->dpy, id, &xgwa);
      gcv.foreground = BlackPixelOfScreen (xgwa.screen);
      gc = XCreateGC (s->dpy, id, GCForeground, &gcv);
      XFillRectangle (s->dpy, id, gc, 0, 0, xgwa.width, xgwa.height);
      XFreeGC (s->dpy, gc);
    }
  return FALSE;
}


static void
reset_preview_window (state *s)
{
  /* On some systems (most recently, MacOS X) OpenGL programs get confused
     when you kill one and re-start another on the same window.  So maybe
     it's best to just always destroy and recreate the preview window
     when changing hacks, instead of always trying to reuse the same one?
   */
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  GtkWidget *pr = win->preview;
  if (s->dpy && !s->wayland_p && gtk_widget_get_realized (pr))
    {
      GdkWindow *window = gtk_widget_get_window (pr);
      Window oid = (window ? gdk_x11_window_get_xid (window) : 0);
      Window id;
      gtk_widget_hide (pr);
      gtk_widget_unrealize (pr);
      gtk_widget_set_has_window (pr, TRUE);
      gtk_widget_realize (pr);
      gtk_widget_show (pr);
      id = (window ? gdk_x11_window_get_xid (window) : 0);
      if (s->debug_p && oid != id)
        fprintf (stderr, "%s: window id 0x%X -> 0x%X\n", blurb(),
                 (unsigned int) oid,
                 (unsigned int) id);
    }
}


/* Make the preview widget use the best GL visual.
   We just always use that one rather than switching.
 */
static void
fix_preview_visual (state *s)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  Visual    *xvisual = s->gl_visual;
  GtkWidget *widget  = win->preview;
  GdkWindow *gwindow = gtk_widget_get_window (GTK_WIDGET (win));
  GdkScreen *gscreen = gdk_window_get_screen (gwindow);
  GdkVisual *gvisual1 = gdk_screen_get_system_visual (gscreen);
  GdkVisual *gvisual2 = x_visual_to_gdk_visual (gwindow, xvisual);

  if (! gvisual2)
    {
      gvisual2 = gvisual1;
      if (s->debug_p)
        fprintf (stderr, "%s: couldn't convert X Visual 0x%lx to a GdkVisual;"
                 " winging it.\n", blurb(),
             (xvisual ? (unsigned long) xvisual->visualid : 0L));
    }

  if (s->debug_p)
    fprintf (stderr, "%s: using %s visual 0x%lx\n", blurb(),
             (gvisual1 == gvisual2 ? "default" : "non-default"),
             (xvisual ? (unsigned long) xvisual->visualid : 0L));

  if (!gtk_widget_get_realized (widget) ||
      gtk_widget_get_visual (widget) != gvisual2)
    {
      gtk_widget_unrealize (widget);
      gtk_widget_set_has_window (widget, TRUE);
      gtk_widget_set_visual (widget, gvisual2);
      gtk_widget_realize (widget);
    }

  gtk_widget_show (widget);
}


/* Subprocesses
 */

static char *
subproc_pretty_name (state *s)
{
  if (s->running_preview_cmd)
    {
      char *ps = strdup (s->running_preview_cmd);
      char *ss = strchr (ps, ' ');
      if (ss) *ss = 0;
      ss = strrchr (ps, '/');
      if (!ss)
        ss = ps;
      else
        {
          ss = strdup (ss+1);
          free (ps);
        }
      return ss;
    }
  else
    return strdup ("???");
}


static void
reap_zombies (state *s)
{
  int wait_status = 0;
  pid_t pid;
  while ((pid = waitpid (-1, &wait_status, WNOHANG|WUNTRACED)) > 0)
    {
      if (s->debug_p)
        {
          if (pid == s->running_preview_pid)
            {
              char *ss = subproc_pretty_name (s);
              fprintf (stderr, "%s: pid %lu (%s) died\n", blurb(),
                       (unsigned long) pid, ss);
              free (ss);
            }
          else
            fprintf (stderr, "%s: pid %lu died\n", blurb(),
                     (unsigned long) pid);
        }
    }
}


#define EXEC_FAILED_EXIT_STATUS -33

/* Mostly lifted from driver/subprocs.c */
static Visual *
get_best_gl_visual (state *s)
{
  pid_t forked;
  int fds [2];
  int in, out;
  char buf[1024];

  char *av[10];
  int ac = 0;

  av[ac++] = "xscreensaver-gl-visual";
  av[ac] = 0;

  if (pipe (fds))
    {
      perror ("error creating pipe:");
      return 0;
    }

  in = fds [0];
  out = fds [1];

  switch ((int) (forked = fork ()))
    {
    case -1:
      {
        sprintf (buf, "%s: couldn't fork", blurb());
        perror (buf);
        exit (1);
      }
    case 0:
      {
        close (in);  /* don't need this one */
        close (ConnectionNumber (s->dpy));	/* close display fd */

        if (dup2 (out, STDOUT_FILENO) < 0)	/* pipe stdout */
          {
            perror ("could not dup() a new stdout:");
            return 0;
          }

        execvp (av[0], av);			/* shouldn't return. */

        if (errno != ENOENT)
          {
            /* Ignore "no such file or directory" errors, unless verbose.
               Issue all other exec errors, though. */
            sprintf (buf, "%s: running %s", blurb(), av[0]);
            perror (buf);
          }

        /* Note that one must use _exit() instead of exit() in procs forked
           off of Gtk programs -- Gtk installs an atexit handler that has a
           copy of the X connection (which we've already closed, for safety.)
           If one uses exit() instead of _exit(), then one sometimes gets a
           spurious "Gdk-ERROR: Fatal IO error on X server" error message.
        */
        _exit (EXEC_FAILED_EXIT_STATUS);	/* exits fork */
        break;
      }
    default:
      {
        int result = 0;
        int wait_status = 0;
        int exit_status = EXEC_FAILED_EXIT_STATUS;

        FILE *f = fdopen (in, "r");
        unsigned int v = 0;
        char c;
        int i = 0;

        close (out);  /* don't need this one */

        *buf = 0;
        do {
          errno = 0;
          if (! fgets (buf, sizeof(buf)-1, f))
            *buf = 0;
        } while (errno == EINTR &&	/* fgets might fail due to SIGCHLD. */
                 i++ < 1000);		/* And just in case. */

        fclose (f);

        /* Wait for the child to die. */
        waitpid (forked, &wait_status, 0);

        exit_status = WEXITSTATUS (wait_status);
        /* Treat exit code as a signed 8-bit quantity. */
        if (exit_status & 0x80) exit_status |= ~0xFF;

        if (exit_status == EXEC_FAILED_EXIT_STATUS)
          {
            fprintf (stderr, "%s: %s is not installed\n", blurb(), av[0]);
            return 0;
          }

        if (1 == sscanf (buf, "0x%x %c", &v, &c))
          result = (int) v;

        if (result == 0)
          {
            if (s->debug_p)
              fprintf (stderr, "%s: %s did not report a GL visual!\n",
                       blurb(), av[0]);
            return 0;
          }
        else
          {
            Visual *v = id_to_visual (DefaultScreenOfDisplay (s->dpy), result);
            if (s->debug_p)
              fprintf (stderr, "%s: GL visual is 0x%X\n", blurb(), result);
            if (!v) abort();
            return v;
          }
      }
    }

  abort();
}


static void
kill_preview_subproc (state *s, Bool reset_p)
{
  s->running_preview_error_p = FALSE;

  reap_zombies (s);
  clear_preview_window (s);

  if (s->subproc_check_timer_id)
    {
      g_source_remove (s->subproc_check_timer_id);
      s->subproc_check_timer_id = 0;
      s->subproc_check_countdown = 0;
    }

  if (s->running_preview_pid)
    {
      int status = kill (s->running_preview_pid, SIGTERM);
      char *ss = subproc_pretty_name (s);

      if (status < 0)
        {
          if (errno == ESRCH)
            {
              if (s->debug_p)
                fprintf (stderr, "%s: pid %lu (%s) was already dead.\n",
                         blurb(), (unsigned long) s->running_preview_pid, ss);
            }
          else
            {
              char buf [1024];
              sprintf (buf, "%s: couldn't kill pid %lu (%s)",
                       blurb(), (unsigned long) s->running_preview_pid, ss);
              perror (buf);
            }
        }
      else {
	int endstatus;
	waitpid(s->running_preview_pid, &endstatus, 0);
	if (s->debug_p)
	  fprintf (stderr, "%s: killed pid %lu (%s)\n", blurb(),
		   (unsigned long) s->running_preview_pid, ss);
      }

      free (ss);
      s->running_preview_pid = 0;
      if (s->running_preview_cmd) free (s->running_preview_cmd);
      s->running_preview_cmd = 0;
    }

  reap_zombies (s);

  if (reset_p)
    {
      reset_preview_window (s);
      clear_preview_window (s);
    }
}


/* Immediately and unconditionally launches the given process,
   after appending the -window-id option; sets running_preview_pid.
 */
static void
launch_preview_subproc (state *s)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  saver_preferences *p = &s->prefs;
  Window id;
  char *new_cmd = 0;
  pid_t forked;
  const char *cmd = s->desired_preview_cmd;

  GtkWidget *pr = win->preview;
  GdkWindow *window;

  reset_preview_window (s);

  window = gtk_widget_get_window (pr);

  s->running_preview_error_p = FALSE;

  if (s->preview_suppressed_p || !s->gl_visual)
    {
      kill_preview_subproc (s, FALSE);
      goto DONE;
    }

  new_cmd = malloc (strlen (cmd) + 40);

  id = (window && !s->wayland_p
        ? gdk_x11_window_get_xid (window)
        : 0);
  if (id == 0)
    {
      /* No window id?  No command to run. */
      free (new_cmd);
      new_cmd = 0;
    }
  else
    {
      /* We do this instead of relying on $XSCREENSAVER_WINDOW specifically
         so that third-party savers that don't implement --window-id will fail:
         otherwise we might have full-screen windows popping up when we were
         just trying to get a preview thumbnail.
       */
      strcpy (new_cmd, cmd);
      sprintf (new_cmd + strlen (new_cmd), " --window-id 0x%X",
               (unsigned int) id);
    }

  if (id && s->screenshot)
    screenshot_save (s->dpy, id, s->screenshot);

  kill_preview_subproc (s, FALSE);
  if (! new_cmd)
    {
      s->running_preview_error_p = TRUE;
      clear_preview_window (s);
      goto DONE;
    }

  switch ((int) (forked = fork ()))
    {
    case -1:
      {
        char buf[255];
        sprintf (buf, "%s: couldn't fork", blurb());
        perror (buf);
        s->running_preview_error_p = TRUE;
        goto DONE;
        break;
      }
    case 0:
      {
        close (ConnectionNumber (s->dpy));

        hack_subproc_environment (id, s->debug_p);

        usleep (250000);  /* pause for 1/4th second before launching, to give
                             the previous program time to die and flush its X
                             buffer, so we don't get leftover turds on the
                             window. */

        exec_command (p->shell, new_cmd, p->nice_inferior);
        /* Don't bother printing an error message when we are unable to
           exec subprocesses; we handle that by polling the pid later.

           Note that one must use _exit() instead of exit() in procs forked
           off of Gtk programs -- Gtk installs an atexit handler that has a
           copy of the X connection (which we've already closed, for safety.)
           If one uses exit() instead of _exit(), then one sometimes gets a
           spurious "Gdk-ERROR: Fatal IO error on X server" error message.
        */
        _exit (1);  /* exits child fork */
        break;

      default:

        if (s->running_preview_cmd) free (s->running_preview_cmd);
        s->running_preview_cmd = strdup (s->desired_preview_cmd);
        s->running_preview_pid = forked;

        if (s->debug_p)
          {
            char *ss = subproc_pretty_name (s);
            fprintf (stderr, "%s: forked %lu (%s)\n", blurb(),
                     (unsigned long) forked, ss);
            free (ss);
          }
        break;
      }
    }

  /* Put some props on the embedded preview window, for debugging. */
  XStoreName (s->dpy, id, "XScreenSaver Settings Preview");
  XChangeProperty (s->dpy, id, XA_WM_COMMAND,
                   XA_STRING, 8, PropModeReplace,
                   (unsigned char *) new_cmd,
                   strlen (new_cmd));
  XChangeProperty (s->dpy, id, XA_NET_WM_PID,
                   XA_CARDINAL, 32, PropModeReplace,
                   (unsigned char *) &forked, 1);

  schedule_preview_check (s);

 DONE:
  if (new_cmd) free (new_cmd);
  new_cmd = 0;
}


/* Modify $DISPLAY and $PATH for the benefit of subprocesses.
 */
static void
hack_environment (state *s)
{
  static const char *def_path =
# ifdef DEFAULT_PATH_PREFIX
    DEFAULT_PATH_PREFIX;
# else
    "";
# endif

  const char *odpy = s->dpy ? DisplayString (s->dpy) : ":0.0";
  char *ndpy = (char *) malloc(strlen(odpy) + 20);
  strcpy (ndpy, "DISPLAY=");
  strcat (ndpy, odpy);
  if (putenv (ndpy))
    abort ();

  if (s->debug_p)
    fprintf (stderr, "%s: %s\n", blurb(), ndpy);

  /* don't free(ndpy) -- some implementations of putenv (BSD 4.4, glibc
     2.0) copy the argument, but some (libc4,5, glibc 2.1.2) do not.
     So we must leak it (and/or the previous setting).  Yay.
   */

  if (def_path && *def_path)
    {
      const char *opath = getenv("PATH");
      char *npath = (char *) malloc(strlen(def_path) + strlen(opath) + 20);
      strcpy (npath, "PATH=");
      strcat (npath, def_path);
      strcat (npath, ":");
      strcat (npath, opath);

      if (putenv (npath))
	abort ();
      /* do not free(npath) -- see above */

      if (s->debug_p)
        fprintf (stderr, "%s: added \"%s\" to $PATH\n", blurb(), def_path);
    }
}


static void
hack_subproc_environment (Window preview_window_id, Bool debug_p)
{
  /* Store a window ID in $XSCREENSAVER_WINDOW -- this isn't strictly
     necessary yet, but it will make programs work if we had invoked
     them with "--root" and not with "--window-id" -- which, of course,
     doesn't happen.
   */
  char *nssw = (char *) malloc (40);
  sprintf (nssw, "XSCREENSAVER_WINDOW=0x%X", (unsigned int) preview_window_id);

  /* Allegedly, BSD 4.3 didn't have putenv(), but nobody runs such systems
     any more, right?  It's not Posix, but everyone seems to have it. */
  if (putenv (nssw))
    abort ();

  if (debug_p)
    fprintf (stderr, "%s: %s\n", blurb(), nssw);

  /* do not free(nssw) -- see above */
}



/* Called from a timer:
   Launches the currently-chosen subprocess, if it's not already running.
   If there's a different process running, kills it.
 */
static int
update_subproc_timer (gpointer data)
{
  state *s = (state *) data;
  if (! s->desired_preview_cmd)
    kill_preview_subproc (s, TRUE);
  else if (!s->running_preview_cmd ||
           !!strcmp (s->desired_preview_cmd, s->running_preview_cmd))
    launch_preview_subproc (s);

  s->subproc_timer_id = 0;
  return FALSE;  /* do not re-execute timer */
}


/* Call this when you think you might want a preview process running.
   It will set a timer that will actually launch that program a second
   from now, if you haven't changed your mind (to avoid double-click
   spazzing, etc.)  `cmd' may be null meaning "no process".
 */
static void
schedule_preview (state *s, const char *cmd)
{
  int delay = 1000 * 0.5;   /* 1/2 second hysteresis */

  if (s->debug_p)
    {
      if (cmd)
        fprintf (stderr, "%s: scheduling preview \"%s\"\n", blurb(), cmd);
      else
        fprintf (stderr, "%s: scheduling preview death\n", blurb());
    }

  if (s->desired_preview_cmd) free (s->desired_preview_cmd);
  s->desired_preview_cmd = (cmd ? strdup (cmd) : 0);

  if (s->subproc_timer_id)
    g_source_remove (s->subproc_timer_id);
  s->subproc_timer_id = g_timeout_add (delay, update_subproc_timer, s);
}


/* Called from a timer:
   Checks to see if the subproc that should be running, actually is.
 */
static int
check_subproc_timer (gpointer data)
{
  state *s = (state *) data;
  Bool again_p = TRUE;

  if (s->running_preview_error_p ||   /* already dead */
      s->running_preview_pid <= 0)
    {
      again_p = FALSE;
    }
  else
    {
      int status;
      reap_zombies (s);
      status = kill (s->running_preview_pid, 0);
      if (status < 0 && errno == ESRCH)
        s->running_preview_error_p = TRUE;

      if (s->debug_p && s->running_preview_error_p)
        {
          char *ss = subproc_pretty_name (s);
          fprintf (stderr, "%s: timer: pid %lu (%s) is %s\n", blurb(),
                   (unsigned long) s->running_preview_pid, ss,
                   (s->running_preview_error_p ? "dead" : "alive"));
          free (ss);
        }

      if (s->running_preview_error_p)
        {
          clear_preview_window (s);
          again_p = FALSE;
        }
    }

  /* Otherwise, it's currently alive.  We might be checking again, or we
     might be satisfied. */

  if (--s->subproc_check_countdown <= 0)
    again_p = FALSE;

  if (again_p)
    return TRUE;     /* re-execute timer */
  else
    {
      s->subproc_check_timer_id = 0;
      s->subproc_check_countdown = 0;
      return FALSE;  /* do not re-execute timer */
    }
}


/* Call this just after launching a subprocess.
   This sets a timer that will, five times a second for two seconds,
   check whether the program is still running.  The assumption here
   is that if the process didn't stay up for more than a couple of
   seconds, then either the program doesn't exist, or it doesn't
   take a --window-id argument.
 */
static void
schedule_preview_check (state *s)
{
  int seconds = 2;
  int ticks = 5;

  if (s->debug_p)
    fprintf (stderr, "%s: scheduling check\n", blurb());

  if (s->subproc_check_timer_id)
    g_source_remove (s->subproc_check_timer_id);
  s->subproc_check_timer_id =
   g_timeout_add (1000 / ticks,
                  check_subproc_timer, (gpointer) s);
  s->subproc_check_countdown = ticks * seconds;
}


static Bool
screen_blanked_p (state *s)
{
  Atom type;
  int format;
  unsigned long nitems, bytesafter;
  unsigned char *dataP = 0;
  Bool blanked_p = FALSE;

  if (!s->dpy) return FALSE;
  if (XGetWindowProperty (s->dpy, RootWindow (s->dpy, 0), /* always screen 0 */
                          XA_SCREENSAVER_STATUS,
                          0, 3, FALSE, XA_INTEGER,
                          &type, &format, &nitems, &bytesafter,
                          &dataP)
      == Success
      && type == XA_INTEGER
      && nitems >= 3
      && dataP)
    {
      Atom *data = (Atom *) dataP;
      blanked_p = (data[0] == XA_BLANK || data[0] == XA_LOCK);
    }

  if (dataP) XFree (dataP);

  return blanked_p;
}

/* Wake up every now and then and see if the screen is blanked.
   If it is, kill off the small-window demo -- no point in wasting
   cycles by running two screensavers at once...
 */
static int
check_blanked_timer (gpointer data)
{
  state *s = (state *) data;
  Bool blanked_p = screen_blanked_p (s);
  if (blanked_p && s->running_preview_pid)
    {
      if (s->debug_p)
        fprintf (stderr, "%s: screen is blanked: killing preview\n", blurb());
      kill_preview_subproc (s, TRUE);
    }

  return TRUE;  /* re-execute timer */
}


/* Is there more than one active monitor? */
static Bool
multi_screen_p (Display *dpy)
{
  monitor **monitors = dpy ? scan_monitors (dpy) : NULL;
  Bool ret = FALSE;
  if (monitors)
    {
      int count = 0;
      int good_count = 0;
      while (monitors[count])
        {
          if (monitors[count]->sanity == S_SANE)
            good_count++;
          count++;
        }
      free_monitors (monitors);
      ret = (good_count > 1);
    }
  return ret;
}


#if 0
static Bool
xrm_mapper (XrmDatabase *db, XrmBindingList bindings, XrmQuarkList quarks,
            XrmRepresentation *type, XrmValue *value, XPointer closure)
{
  int i;
  for (i = 0; quarks[i]; i++)
    {
      if (bindings[i] == XrmBindTightly)
	fprintf (stderr, (i == 0 ? "" : "."));
      else if (bindings[i] == XrmBindLoosely)
	fprintf (stderr, "*");
      else
	fprintf (stderr, " ??? ");
      fprintf(stderr, "%s", XrmQuarkToString (quarks[i]));
    }

  fprintf (stderr, ": %s\n", (char *) value->addr);

  return FALSE;
}
#endif /* 0 */


static int
ignore_all_errors_ehandler (Display *dpy, XErrorEvent *error)
{
  return 0;
}


static Window
gnome_screensaver_window (Display *dpy, char **name_ret)
{
  int nscreens;
  int i, screen;
  Window gnome_window = 0;
  XErrorHandler old_handler;

  if (!dpy) return 0;
  XSync (dpy, FALSE);
  old_handler = XSetErrorHandler (ignore_all_errors_ehandler);

  nscreens = ScreenCount (dpy);
  for (screen = 0; screen < nscreens; screen++)
    {
      Window root = RootWindow (dpy, screen);
      Window parent, *kids;
      unsigned int nkids;

      if (! XQueryTree (dpy, root, &root, &parent, &kids, &nkids))
        abort ();
      if (name_ret)
        *name_ret = 0;
      for (i = 0; i < nkids; i++)
        {
          Atom type;
          int format;
          unsigned long nitems, bytesafter;
          unsigned char *name;
          if (XGetWindowProperty (dpy, kids[i], XA_WM_COMMAND, 0, 128,
                                  FALSE, XA_STRING, &type, &format, &nitems,
                                  &bytesafter, &name)
              == Success
              && type != None
              && (!strcmp ((char *) name, "gnome-screensaver") ||
                  !strcmp ((char *) name, "mate-screensaver") ||
                  !strcmp ((char *) name, "cinnamon-screensaver") ||
                  !strcmp ((char *) name, "xfce4-screensaver") ||
                  !strcmp ((char *) name, "light-locker")))
            {
              gnome_window = kids[i];
              if (name_ret)
                *name_ret = strdup ((char *) name);
              break;
            }
        }
      if (kids) XFree ((char *) kids);
      if (gnome_window)
        break;
    }

  XSync (dpy, FALSE);
  XSetErrorHandler (old_handler);
  return gnome_window;
}


static Bool
gnome_screensaver_active_p (state *s, char **name_ret)
{
  Window w = gnome_screensaver_window (s->dpy, name_ret);
  return (w ? TRUE : FALSE);
}


static void
kill_gnome_screensaver (state *s)
{
  Window w = gnome_screensaver_window (s->dpy, NULL);
  if (w) XKillClient (s->dpy, (XID) w);
}


static Bool
kde_screensaver_active_p (void)
{
  /* Apparently this worked in KDE 3, but not 4 or 5.
     Maybe parsing the output of this would work in KDE 5:
     kreadconfig5 --file kscreenlockerrc --group Daemon --key Autolock
     but there's probably no way to kill the KDE saver.
     Fuck it. */
  FILE *p = popen ("dcop kdesktop KScreensaverIface isEnabled 2>/dev/null",
                   "r");
  char buf[255];
  int i = 0;
  if (!p) return FALSE;

  *buf = 0;
  do {
    errno = 0;
    if (! fgets (buf, sizeof(buf)-1, p))
      *buf = 0;
  } while (errno == EINTR &&	/* fgets might fail due to SIGCHLD. */
           i++ < 1000);		/* And just in case. */

  pclose (p);
  if (!strcmp (buf, "true\n"))
    return TRUE;
  else
    return FALSE;
}


static void
kill_kde_screensaver (state *s)
{
  int ac = 0;
  char *av[10];
  av[ac++] = "dcop";
  av[ac++] = "kdesktop";
  av[ac++] = "KScreensaverIface";
  av[ac++] = "enable";
  av[ac++] = "false";
  av[ac] = 0;
  fork_and_exec (s, ac, av);
}


static int
the_network_is_not_the_computer (gpointer data)
{
  state *s = (state *) data;
  char *rversion = 0, *ruser = 0, *rhost = 0;
  char *luser, *lhost;
  char *msg = 0;
  char *oname = 0;
  struct passwd *p = getpwuid (getuid ());
  const char *d = s->dpy ? DisplayString (s->dpy) : ":0.0";

# if defined(HAVE_UNAME)
  struct utsname uts;
  if (uname (&uts) < 0)
    lhost = "<UNKNOWN>";
  else
    lhost = uts.nodename;
# else  /* !HAVE_UNAME */
  strcat (lhost, "<UNKNOWN>");
# endif /* !HAVE_UNAME */

  if (p && p->pw_name)
    luser = p->pw_name;
  else
    luser = "???";

  if (s->dpy)
    server_xscreensaver_version (s->dpy, &rversion, &ruser, &rhost);

  /* Make a buffer that's big enough for a number of copies of all the
     strings, plus some. */
  msg = (char *) malloc (10 * ((rversion ? strlen(rversion) : 0) +
			       (ruser ? strlen(ruser) : 0) +
			       (rhost ? strlen(rhost) : 0) +
			       strlen(lhost) +
			       strlen(luser) +
			       strlen(d) +
			       1024));
  *msg = 0;

  if ((!rversion || !*rversion) && !s->debug_p)
    {
# ifndef __APPLE__
      sprintf (msg,
	       _("The XScreenSaver daemon doesn't seem to be running\n"
		 "on display \"%.25s\".  Launch it now?"),
	       d);
# endif
    }
  else if (p && ruser && *ruser && !!strcmp (ruser, p->pw_name))
    {
      /* Warn that the two processes are running as different users.
       */
      sprintf(msg,
	    _("%s is running as user \"%s\" on host \"%s\".\n"
	      "But the xscreensaver managing display \"%.25s\"\n"
	      "is running as user \"%s\" on host \"%s\".\n"
	      "\n"
	      "Since they are different users, they won't be reading/writing\n"
	      "the same ~/.xscreensaver file, so %s isn't\n"
	      "going to work right.\n"
	      "\n"
	      "You should either re-run %s as \"%s\", or re-run\n"
	      "xscreensaver as \"%s\".\n"
              "\n"
              "Restart the xscreensaver daemon now?\n"),
	      progname, luser, lhost,
	      d,
	      (ruser ? ruser : "???"), (rhost ? rhost : "???"),
	      progname,
	      progname, (ruser ? ruser : "???"),
	      luser);
    }
  else if (rhost && *rhost && !!strcmp (rhost, lhost))
    {
      /* Warn that the two processes are running on different hosts.
       */
      sprintf (msg,
	      _("%s is running as user \"%s\" on host \"%s\".\n"
	       "But the xscreensaver managing display \"%s\"\n"
	       "is running as user \"%s\" on host \"%s\".\n"
	       "\n"
	       "If those two machines don't share a file system (that is,\n"
	       "if they don't see the same ~%s/.xscreensaver file) then\n"
	       "%s won't work right.\n"
               "\n"
               "Restart the daemon on \"%s\" as \"%s\" now?\n"),
	       progname, luser, lhost,
	       d,
	       (ruser ? ruser : "???"), (rhost ? rhost : "???"),
	       luser,
	       progname,
               lhost, luser);
    }
  else if (rversion && *rversion && !!strcmp (rversion, s->short_version))
    {
      /* Warn that the version numbers don't match.
       */
      sprintf (msg,
	     _("This is %s version %s.\n"
	       "But the xscreensaver managing display \"%s\"\n"
	       "is version %s.  This could cause problems.\n"
	       "\n"
	       "Restart the xscreensaver daemon now?\n"),
	       progname, s->short_version,
	       d,
	       rversion);
    }

  validate_image_directory_quick (s);

  if (*msg)
    warning_dialog_1 (s->window, _("Warning"), msg, D_LAUNCH);

  if (rversion) free (rversion);
  if (ruser) free (ruser);
  if (rhost) free (rhost);
  free (msg);
  msg = 0;

  /* Note: since these dialogs are not modal, they will stack up.
     So we do this check *after* popping up the "xscreensaver is not
     running" dialog so that these are on top.  Good enough.
   */

  if (gnome_screensaver_active_p (s, &oname))
    {
      char msg [1024];
      sprintf (msg,
               _("The GNOME screen saver daemon (%s) appears to be running.\n"
                 "It must be stopped for XScreenSaver to work properly.\n"
                 "\n"
                 "Stop the \"%s\" daemon now?\n"),
               oname, oname);
      warning_dialog_1 (s->window, _("Warning"), msg, D_GNOME);
    }

  if (kde_screensaver_active_p())
    warning_dialog_1 (s->window, _("Warning"),
                    _("The KDE screen saver daemon appears to be running.\n"
                      "It must be stopped for XScreenSaver to work properly.\n"
                      "\n"
                      "Stop the KDE screen saver daemon now?\n"),
                    D_KDE);

  if (! s->gl_visual)
    warning_dialog (s->window, _("Error"),
      _("No GL visuals: the xscreensaver-gl* packages are required."));

  if (s->wayland_p)
    warning_dialog (s->window, _("Warning"),
                _("You are running Wayland rather than the X Window System.\n"
                  "\n"
                  "Under Wayland, idle-detection fails when non-X11 programs\n"
                  "are selected, meaning the screen may blank prematurely.\n"
                  "Also, locking is impossible.\n"
                  "\n"
                  "See the XScreenSaver manual for instructions on\n"
                  "configuring your system to use X11 instead of Wayland.\n"));

  return FALSE;  /* Only run timer once */
}


/* We use this error handler so that X errors are preceeded by the name
   of the program that generated them.
 */
static int
x_error (Display *dpy, XErrorEvent *error)
{
  fprintf (stderr, "\n%s: X error:\n", blurb());
  XmuPrintDefaultErrorMessage (dpy, error, stderr);
  /* No way to get 'state' in here... */
  /* kill_preview_subproc (s, FALSE); */
  exit (-1);   /* Likewise, no way to call g_application_quit(). */
  return 0;
}


static void
g_logger (const gchar *domain, GLogLevelFlags log_level,
          const gchar *message, gpointer data)
{
  if (log_level & G_LOG_LEVEL_DEBUG) return;
  if (log_level & G_LOG_LEVEL_INFO)  return;
  fprintf (stderr, "%s: %s: %s\n", blurb(), domain, message);
  if (log_level & G_LOG_LEVEL_CRITICAL) exit (-1);
}

/* Why are there two of these hooks and why does this one suck so hard?? */
static GLogWriterOutput
g_other_logger (GLogLevelFlags log_level, const GLogField *fields,
                gsize n_fields, gpointer data)
{
  int i;
  GLogWriterOutput ret = G_LOG_WRITER_UNHANDLED;
  if (log_level & G_LOG_LEVEL_DEBUG) return ret;
  if (log_level & G_LOG_LEVEL_INFO)  return ret;
  for (i = 0; i < n_fields; i++)
    {
      const GLogField *field = &fields[i];
      if (strcmp (field->key, "MESSAGE")) continue;
      fprintf (stderr, "%s: %s\n", blurb(), (char *) field->value);
      ret = G_LOG_WRITER_HANDLED;
    }
  if (log_level & G_LOG_LEVEL_CRITICAL) exit (-1);
  return ret;
}


/****************************************************************************

 XScreenSaverDialog callbacks, referenced by prefs.ui.

 ****************************************************************************/

/* The "Documentation" button on the Settings dialog */
G_MODULE_EXPORT void
manual_cb (GtkButton *button, gpointer user_data)
{
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (user_data);
  XScreenSaverWindow *win = dialog->main;
  state *s = &win->state;
  saver_preferences *p = &s->prefs;
  GtkWidget *list_widget = win->list;
  int list_elt = selected_list_element (s);
  int hack_number;
  char *name, *name2, *cmd, *str;
  char *oname = 0;
  if (s->debug_p) fprintf (stderr, "%s: documentation button\n", blurb());
  if (list_elt < 0) return;
  hack_number = s->list_elt_to_hack_number[list_elt];

  flush_dialog_changes_and_save (s);
  ensure_selected_item_visible (s, list_widget);

  name = strdup (p->screenhacks[hack_number]->command);
  name2 = name;
  oname = name;
  while (isspace (*name2)) name2++;
  str = name2;
  while (*str && !isspace (*str)) str++;
  *str = 0;
  str = strrchr (name2, '/');
  if (str) name2 = str+1;

  cmd = get_string_resource (s->dpy, "manualCommand", "ManualCommand");
  if (cmd)
    {
      int ac = 0;
      char *av[10];
      char *cmd2 = (char *) malloc (strlen (cmd) + (strlen (name2) * 4) + 100);
      sprintf (cmd2, cmd, name2, name2, name2, name2);
      av[ac++] = "/bin/sh";
      av[ac++] = "-c";
      av[ac++] = cmd2;
      av[ac] = 0;
      fork_and_exec (s, ac, av);
      free (cmd2);
    }
  else
    {
      warning_dialog (s->window, _("Error"),
                      _("no `manualCommand' resource set."));
    }

  free (oname);
}


static void
settings_sync_cmd_text (state *s)
{
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (s->dialog);
  GtkWidget *cmd = GTK_WIDGET (dialog->cmd_text);
  char *cmd_line;
  if (! s->cdata) return;
  cmd_line = get_configurator_command_line (s->cdata, FALSE);
  gtk_entry_set_text (GTK_ENTRY (cmd), cmd_line);
  free (cmd_line);
}


/* The "Advanced" button on the settings dialog. */
G_MODULE_EXPORT void
settings_adv_cb (GtkButton *button, gpointer user_data)
{
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (user_data);
  XScreenSaverWindow *win = dialog->main;
  state *s = &win->state;
  GtkNotebook *notebook = GTK_NOTEBOOK (dialog->opt_notebook);
  if (s->debug_p) fprintf (stderr, "%s: settings advanced button\n", blurb());
  settings_sync_cmd_text (s);
  gtk_notebook_set_current_page (notebook, 1);
}


/* The "Standard" button on the settings dialog. */
G_MODULE_EXPORT void
settings_std_cb (GtkButton *button, gpointer user_data)
{
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (user_data);
  XScreenSaverWindow *win = dialog->main;
  state *s = &win->state;
  GtkNotebook *notebook = GTK_NOTEBOOK (dialog->opt_notebook);
  if (s->debug_p) fprintf (stderr, "%s: settings standard button\n", blurb());
  settings_sync_cmd_text (s);
  gtk_notebook_set_current_page (notebook, 0);
}


/* The "Reset to Defaults" button on the settings dialog. */
G_MODULE_EXPORT void
settings_reset_cb (GtkButton *button, gpointer user_data)
{
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (user_data);
  XScreenSaverWindow *win = dialog->main;
  GtkWidget *cmd = GTK_WIDGET (dialog->cmd_text);
  state *s = &win->state;
  char *cmd_line;
  if (s->debug_p) fprintf (stderr, "%s: settings reset button\n", blurb());
  if (! s->cdata) return;
  cmd_line = get_configurator_command_line (s->cdata, TRUE);
  gtk_entry_set_text (GTK_ENTRY (cmd), cmd_line);
  free (cmd_line);
  populate_popup_window (s);
}


/* Called when "Advanced/Standard" buttons change the displayed page. */
G_MODULE_EXPORT void
settings_switch_page_cb (GtkNotebook *notebook, GtkWidget *page,
                         gint page_num, gpointer user_data)
{
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (user_data);
  XScreenSaverWindow *win = dialog->main;
  state *s = &win->state;
  GtkWidget *adv = dialog->adv_button;
  GtkWidget *std = dialog->std_button;
  if (s->debug_p) fprintf (stderr, "%s: settings page changed\n", blurb());

  if (page_num == 0)
    {
      gtk_widget_show (adv);
      gtk_widget_hide (std);
    }
  else if (page_num == 1)
    {
      gtk_widget_hide (adv);
      gtk_widget_show (std);
    }
  else
    abort();

  /* Nobody uses the "Advanced" tab.  Let's just hide it. 
     (The tab still needs to be there, since the 'cmd_text' widget is
     what gets stored into the .xscreensaver file.) */
  gtk_widget_hide (adv);
  gtk_widget_hide (std);
}


/* The "Cancel" button on the Settings dialog. */
G_MODULE_EXPORT void
settings_cancel_cb (GtkWidget *button, gpointer user_data)
{
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (user_data);
  XScreenSaverWindow *win = dialog->main;
  state *s = &win->state;
  if (s->debug_p) fprintf (stderr, "%s: settings cancel button\n", blurb());
  gtk_widget_hide (GTK_WIDGET (dialog));
  gtk_widget_unrealize (GTK_WIDGET (dialog));

  /* Restart the hack running in the Preview window with the reset options. */
  schedule_preview (s, dialog->unedited_cmdline);
}


/* The "Ok" button on the Settings dialog. */
G_MODULE_EXPORT void
settings_ok_cb (GtkWidget *button, gpointer user_data)
{
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (user_data);
  XScreenSaverWindow *win = dialog->main;
  state *s = &win->state;
  GtkNotebook *notebook = GTK_NOTEBOOK (dialog->opt_notebook);
  int page = gtk_notebook_get_current_page (notebook);
  if (s->debug_p) fprintf (stderr, "%s: settings ok button\n", blurb());

  if (page == 0)
    /* Regenerate the command-line from the widget contents before saving.
       But don't do this if we're looking at the command-line page already,
       or we will blow away what they typed... */
    settings_sync_cmd_text (s);

  flush_popup_changes_and_save (s);
  gtk_widget_hide (GTK_WIDGET (dialog));
  gtk_widget_unrealize (GTK_WIDGET (dialog));
}


/* Called when any widget value is changed in the Settings dialog. */
static void
dialog_change_cb (GtkWidget *widget, gpointer user_data)
{
  state *s = (state *) user_data;
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (s->dialog);
  char *cur_cmd, *def_cmd;
  const char *prev_cmd;
  if (!dialog || !s->cdata) return;
  settings_sync_cmd_text (s);
  cur_cmd  = get_configurator_command_line (s->cdata, FALSE);
  def_cmd  = get_configurator_command_line (s->cdata, TRUE);
  prev_cmd = dialog->unedited_cmdline;

  /* "Reset to Defaults" button enabled only if current cmd is not default. */
  gtk_widget_set_sensitive (dialog->reset_button,
                            !!strcmp (cur_cmd, def_cmd));

  /* "Save" button enabled only if current cmd is edited. */
  gtk_widget_set_sensitive (dialog->ok_button,
                            !!strcmp (cur_cmd, prev_cmd));

  /* Restart the hack running in the Preview window with the prevailing,
     un-saved set of options, for a realtime preview of what they do. */
  schedule_preview (s, cur_cmd);

  free (cur_cmd);
  free (def_cmd);
}


/* Fill in the contents of the "Settings" dialog for the current hack.
   It may or may not currently be visible. 
 */
static void
populate_popup_window (state *s)
{
  saver_preferences *p = &s->prefs;
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (s->dialog);

  GtkLabel *doc = dialog ? GTK_LABEL (dialog->doc) : 0;
  char *doc_string = 0;

  if (s->cdata)
    {
      free_conf_data (s->cdata);
      s->cdata = 0;
    }

  {
    int list_elt = selected_list_element (s);
    int hack_number = (list_elt >= 0 && list_elt < s->list_count
                       ? s->list_elt_to_hack_number[list_elt]
                       : -1);
    screenhack *hack = (hack_number >= 0 ? p->screenhacks[hack_number] : 0);

    if (p->mode == BLANK_ONLY || p->mode == DONT_BLANK)
      hack = 0;

    if (hack && dialog)
      {
        GtkWidget *parent = dialog->settings_vbox;
        GtkWidget *cmd = GTK_WIDGET (dialog->cmd_text);
        const char *cmd_line = gtk_entry_get_text (GTK_ENTRY (cmd));
        if (!cmd_line) abort();
        s->cdata = load_configurator (cmd_line, dialog_change_cb, s, 
                                      s->debug_p);
        dialog_change_cb (NULL, s);
        if (s->cdata && s->cdata->widget)
          gtk_box_pack_start (GTK_BOX (parent), s->cdata->widget,
                              TRUE, TRUE, 0);

        /* Make the pretty name on the tab boxes include the year and be bold.
         */
        if (s->cdata && s->cdata->year)
          {
            XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
            GtkFrame *frame1 = GTK_FRAME (win->preview_frame);
            GtkFrame *frame2 = GTK_FRAME (dialog->opt_frame);
            char *pretty_name = (hack->name
                                 ? strdup (hack->name)
                                 : make_hack_name (s->dpy, hack->command));
            char *s2 = (char *) malloc (strlen (pretty_name) + 20);
            sprintf (s2, "<b>%s (%d)</b>", pretty_name, s->cdata->year);
            free (pretty_name);
            pretty_name = s2;

            gtk_frame_set_label (frame1, _(pretty_name));
            gtk_frame_set_label (frame2, _(pretty_name));
            gtk_label_set_use_markup (  /* Must be after set_label */
              GTK_LABEL (gtk_frame_get_label_widget (frame1)), TRUE);
            gtk_label_set_use_markup (
              GTK_LABEL (gtk_frame_get_label_widget (frame2)), TRUE);
            free (pretty_name);
          }
      }
  }

  doc_string = (s->cdata && s->cdata->description && *s->cdata->description
                ? _(s->cdata->description)
                : 0);
  doc_string = hreffify (doc_string);
  if (doc)
    {
      GtkWidget *w = dialog->dialog_vbox;
      gtk_label_set_text (doc, (doc_string
                                ? doc_string
                                : _("No description available.")));
      gtk_label_set_use_markup (doc, TRUE);

      /* Shrink the dialog to the minimum viable size. */
      gtk_window_resize (GTK_WINDOW (dialog), 1, 1);

      gtk_widget_hide (w);
      gtk_widget_unrealize (w);
      gtk_widget_realize (w);
      gtk_widget_show (w);
    }

  /* Also set the documentation on the main window, below the preview. */
  {
    GtkLabel *doc2 = GTK_LABEL (win->short_preview_label);
    GtkLabel *doc3 = GTK_LABEL (win->preview_author_label);
    char *s2 = 0;
    char *s3 = 0;

    if (doc_string)
      {
        /* Keep only the first paragraph, and the last line.
           Omit everything in between. */
        const char *second_para = strstr (doc_string, "\n\n");
        const char *last_line = strrchr (doc_string, '\n');
        s2 = strdup (doc_string);
        if (second_para)
          s2[second_para - doc_string] = 0;
        if (last_line)
          s3 = strdup (last_line + 1);
      }

    gtk_label_set_text (doc2, 
                        (s2
                         ? _(s2)
                         : (p->mode == BLANK_ONLY || p->mode == DONT_BLANK)
                         ? ""
                         : _("No description available.")));
    gtk_label_set_text (doc3, (s3 ? _(s3) : ""));
    if (s2) free (s2);
    if (s3) free (s3);
  }

  if (doc_string)
    free (doc_string);
}


static void
sensitize_demo_widgets (state *s, Bool sensitive_p)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (s->dialog);
  gtk_widget_set_sensitive (win->demo, sensitive_p);
  gtk_widget_set_sensitive (win->settings, sensitive_p);
  if (dialog)
    {
      gtk_widget_set_sensitive (dialog->cmd_label, sensitive_p);
      gtk_widget_set_sensitive (dialog->cmd_text, sensitive_p);
      gtk_widget_set_sensitive (dialog->manual, sensitive_p);
      gtk_widget_set_sensitive (dialog->visual, sensitive_p);
      gtk_widget_set_sensitive (dialog->visual_combo, sensitive_p);
    }
}


/* Flush out any changes made in the popup dialog box (where changes
   take place only when the OK button is clicked.)
 */
static Bool
flush_popup_changes_and_save (state *s)
{
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (s->dialog);

  Bool changed = FALSE;
  saver_preferences *p = &s->prefs;
  int list_elt = selected_list_element (s);

  GtkEntry *cmd = GTK_ENTRY (dialog->cmd_text);
  GtkWidget *vis = GTK_WIDGET (dialog->visual_combo);
  GtkEntry *visent = GTK_ENTRY (gtk_bin_get_child (GTK_BIN (vis)));

  const char *visual = gtk_entry_get_text (visent);
  const char *command = gtk_entry_get_text (cmd);

  char c;
  unsigned long id;

  if (s->saving_p) return FALSE;
  s->saving_p = TRUE;

  if (list_elt < 0)
    goto DONE;

  if (maybe_reload_init_file (s) != 0)
    {
      changed = TRUE;
      goto DONE;
    }

  /* Sanity-check and canonicalize whatever the user typed into the combo box.
   */
  if      (!strcasecmp (visual, ""))                   visual = "";
  else if (!strcasecmp (visual, "any"))                visual = "";
  else if (!strcasecmp (visual, "default"))            visual = "Default";
  else if (!strcasecmp (visual, "default-n"))          visual = "Default-N";
  else if (!strcasecmp (visual, "default-i"))          visual = "Default-I";
  else if (!strcasecmp (visual, "best"))               visual = "Best";
  else if (!strcasecmp (visual, "mono"))               visual = "Mono";
  else if (!strcasecmp (visual, "monochrome"))         visual = "Mono";
  else if (!strcasecmp (visual, "gray"))               visual = "Gray";
  else if (!strcasecmp (visual, "grey"))               visual = "Gray";
  else if (!strcasecmp (visual, "color"))              visual = "Color";
  else if (!strcasecmp (visual, "gl"))                 visual = "GL";
  else if (!strcasecmp (visual, "staticgray"))         visual = "StaticGray";
  else if (!strcasecmp (visual, "staticcolor"))        visual = "StaticColor";
  else if (!strcasecmp (visual, "truecolor"))          visual = "TRUEColor";
  else if (!strcasecmp (visual, "grayscale"))          visual = "GrayScale";
  else if (!strcasecmp (visual, "greyscale"))          visual = "GrayScale";
  else if (!strcasecmp (visual, "pseudocolor"))        visual = "PseudoColor";
  else if (!strcasecmp (visual, "directcolor"))        visual = "DirectColor";
  else if (1 == sscanf (visual, " %lu %c", &id, &c))   ;
  else if (1 == sscanf (visual, " 0x%lx %c", &id, &c)) ;
  else visual = "";

  changed = flush_changes (s, list_elt, -1, command, visual);
  if (changed)
    {
      demo_write_init_file (s, p);

      /* Do this to re-launch the hack if (and only if) the command line
         has changed. */
      populate_demo_window (s, selected_list_element (s));
    }

 DONE:
  s->saving_p = FALSE;
  return changed;
}


/****************************************************************************

 XScreenSaverWindow

 ****************************************************************************/


static void
xscreensaver_window_destroy (GObject *object)
{
  /* Called by WM close box, but not by File / Quit */
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (object);
  quit_menu_cb (NULL, win);  /* Shouldn't return? */
  G_OBJECT_CLASS (xscreensaver_window_parent_class)->dispose (object);
}


/* gtk_window_move() sets the origin of the window's WM decorations, but
   GTK's "configure-event" returns the root-relative origin of the window
   within the decorations, so the "configure-event" numbers are too large by
   the size of the decorations (title bar and border).  Without compensating
   for this, the window would move down and slightly to the right every time
   we saved and restored.  GDK provides no way to find those numbers, so we
   have to hack it out X11 style...
 */
static void
wm_decoration_origin (GtkWindow *gtkw, int *x, int *y)
{
  Display *dpy = gdk_x11_get_default_xdisplay();
  GdkWindow *gdkw = gtk_widget_get_window (GTK_WIDGET (gtkw));
  Window xw = gdk_x11_window_get_xid (gdkw);

  Window root, parent, *kids;
  unsigned int nkids;

  Atom type = None;
  int format;
  unsigned long nitems, bytesafter;
  unsigned char *data;

  static Atom swm_vroot = 0;
  XWindowAttributes xgwa;

  if (!dpy || !xw) return;
  if (! XQueryTree (dpy, xw, &root, &parent, &kids, &nkids))
    abort ();
  if (kids) XFree ((char *) kids);

  if (parent == root)	/* No window above us at all */
    return;

  if (! swm_vroot)
    swm_vroot = XInternAtom (dpy, "__SWM_VROOT", FALSE);

  /* If parent is a virtual root, there is no intervening WM decoration. */
  if (XGetWindowProperty (dpy, parent, swm_vroot,
                          0, 0, FALSE, AnyPropertyType,
                          &type, &format, &nitems, &bytesafter,
                          (unsigned char **) &data)
      == Success
      && type != None)
    return;

  /* If we have a parent, it is the WM decoration, so use its origin. */
  if (! XGetWindowAttributes (dpy, parent, &xgwa))
    abort();
  *x = xgwa.x;
  *y = xgwa.y;
}


static void
save_window_position (state *s, GtkWindow *win, int x, int y, Bool dialog_p)
{
  saver_preferences *p = &s->prefs;
  int win_x, win_y, dialog_x, dialog_y;
  char dummy;
  char *old = p->settings_geom;
  char str[100];

  if (!s->dpy || s->wayland_p) return;
  wm_decoration_origin (win, &x, &y);

  if (!old || !*old ||
      4 != sscanf (old, " %d , %d %d , %d %c",
                   &win_x, &win_y, &dialog_x, &dialog_y, &dummy))
    win_x = win_y = dialog_x = dialog_y = -1;

  if (dialog_p)
    dialog_x = x, dialog_y = y;
  else
    win_x = x, win_y = y;

  sprintf (str, "%d,%d %d,%d", win_x, win_y, dialog_x, dialog_y);
           
  if (old && !strcmp (old, str)) return;  /* unchanged */

  p->settings_geom = strdup (str);

  if (s->debug_p)
    fprintf (stderr, "%s: geom: %s => %s\n", blurb(),
             (old ? old : "null"), str);

  /* This writes the .xscreensaver file repeatedly as the window is dragged,
     which is too much.  We could defer it with a timer.  But instead let's
     just not save it upon resize, and only save the positions once the
     file is written due to some other change.
   */
  /* demo_write_init_file (s, p); */
  if (old) free (old);
}


static void
restore_window_position (state *s, GtkWindow *window, Bool dialog_p)
{
  saver_preferences *p = &s->prefs;
  int win_x, win_y, dialog_x, dialog_y, x, y;
  char dummy;
  char *old = p->settings_geom;

  if (!old || !*old ||
      4 != sscanf (old, " %d , %d %d , %d %c",
                   &win_x, &win_y, &dialog_x, &dialog_y, &dummy))
    win_x = win_y = dialog_x = dialog_y = -1;

  if (dialog_p)
    x = dialog_x, y = dialog_y;
  else
    x = win_x, y = win_y;

  if (x <= 0 || y <= 0) return;

  if (s->debug_p)
    fprintf (stderr, "%s: restore origin: %d,%d\n", blurb(), x, y);
  gtk_window_move (window, x, y);
}


/* When the window is moved, save the new origin in .xscreensaver. */
static gboolean
xscreensaver_window_resize_cb (GtkWindow *window, GdkEvent *event,
                               gpointer data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (window);
  state *s = &win->state;
  save_window_position (s, GTK_WINDOW (win), 
                        event->configure.x, event->configure.y, FALSE);
  return FALSE;
}


static int
delayed_scroll_kludge (gpointer data)
{
  state *s = (state *) data;
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (s->window);
  GtkWidget *list_widget = win->list;
  ensure_selected_item_visible (s, list_widget);
  return FALSE;  /* do not re-execute timer */
}


static void
quit_action_cb (GSimpleAction *action, GVariant *parameter, gpointer user_data)
{
  quit_menu_cb (NULL, user_data);
}


static GActionEntry app_entries[] = {
  { "quit",   quit_action_cb,   NULL, NULL, NULL },
/*
  { "undo",   undo_action_cb,   NULL, NULL, NULL },
  { "redo",   redo_action_cb,   NULL, NULL, NULL },
  { "cut",    cut_action_cb,    NULL, NULL, NULL },
  { "copy",   copy_action_cb,   NULL, NULL, NULL },
  { "paste",  paste_action_cb,  NULL, NULL, NULL },
  { "delete", delete_action_cb, NULL, NULL, NULL },
*/
};

/* #### I don't know how to make the accelerators show up on the menu items,
   and I don't understand the difference between "callbacks" and "actions".
   I see examples in other .ui files that do things like:

       <ui>
         <menubar name="menubar">
           <menu action="file">
             <menuitem name="activate_menu" action="activate_action"/>
   or
       <menu id="menubar">
         <submenu>
           <attribute name="label">File</attribute>
           <section>
             <item>
               <attribute name="label">Activate</attribute>
               <attribute name="action">app.activate</attribute>

   but when I put variants of that in demo.ui, nothing shows up.

   It would be nice to have an "Edit" menu with working text-editing commands
   on it, for use with our various text fields.  One would think that a GUI
   toolkit would provide boilerplate for such things, but nooooo...
 */
const gchar *accels[][2] = {
  { "app.quit",   "<Ctrl>Q" },
/*
  { "app.undo",   "<Ctrl>Z" },
  { "app.redo",   "<Ctrl>Y" },
  { "app.cut",    "<Ctrl>X" },
  { "app.copy",   "<Ctrl>C" },
  { "app.paste",  "<Ctrl>V" },
*/
};


static void
xscreensaver_window_realize (GtkWidget *self, gpointer user_data)
{
  XScreenSaverWindow *win = XSCREENSAVER_WINDOW (self);
  state *s = &win->state;
  saver_preferences *p = &s->prefs;

  s->initializing_p = TRUE;
  s->short_version = XSCREENSAVER_VERSION;
  s->window = GTK_WINDOW (win);

  s->dpy = gdk_x11_get_default_xdisplay();

  /* Debian 11.4, Gtk 3.24.24, 2022: under Wayland, get_default_xdisplay is
     returning uninitialized data!  However, gdk_x11_window_get_xid prints a
     warning and returns NULL.  So let's try that, and as a fallback, also try
     and sanity check the contents of the Display structure...
   */
  if (! gdk_x11_window_get_xid (gtk_widget_get_window (self)))
    {
      s->dpy = NULL;
      s->wayland_p = TRUE;
    }

  if (s->dpy)
    {
      if (ProtocolVersion (s->dpy) != 11 ||
          ProtocolRevision (s->dpy) != 0)
        {
          fprintf (stderr, "%s: uninitialized data in Display: "
                   "protocol version %d.%d!\n", blurb(),
                   ProtocolVersion(s->dpy), ProtocolRevision(s->dpy));
          s->dpy = NULL;
          s->wayland_p = TRUE;
        }
    }

  /* If we don't have a display connection, then we are surely under Wayland
     even if the environment variable is not set.
   */
  if (!s->dpy &&
      !getenv ("WAYLAND_DISPLAY") &&
      !getenv ("WAYLAND_SOCKET"))
    putenv ("WAYLAND_DISPLAY=probably");

  if (getenv ("WAYLAND_DISPLAY") ||
      getenv ("WAYLAND_SOCKET"))
    s->wayland_p = TRUE;

  /* If GTK is running directly under Wayland, try to open an X11 connection
     to XWayland anyway, so that get_string_resource and load_init_file work.
   */
  if (! s->dpy)
    {
      s->dpy = XOpenDisplay (NULL);
      if (s->debug_p)
        {
          if (s->dpy)
            fprintf (stderr, "%s: opened secondary XWayland connection\n",
                     blurb());
          else
            fprintf (stderr, "%s: failed to open XWayland connection\n",
                     blurb());
        }
    }

  /* Teach Xt to use the Display that Gtk/Gdk have already opened.
   */
  if (s->dpy)
    {
      XtAppContext app;
      int argc = 0;
      char *argv[2];
      const char *oprogname = progname;

      progname = "xscreensaver";  /* For X resources */
      argv[argc++] = (char *) progname;
      argv[argc] = 0;

      XtToolkitInitialize ();
      app = XtCreateApplicationContext ();
      XtAppSetFallbackResources (app, defaults);
      XtDisplayInitialize (app, s->dpy, progname, progclass, 0, 0, &argc, argv);

      if (s->debug_p)
        {
          XSync (s->dpy, False);
          XSynchronize (s->dpy, True);	/* Must be after XtDisplayInitialize */
        }

      /* Result discarded and leaked */
      XtAppCreateShell (progname, progclass, applicationShellWidgetClass,
                        s->dpy, 0, 0);
      p->db = XtDatabase (s->dpy);
      XSetErrorHandler (x_error);

      signal (SIGPIPE, SIG_IGN);  /* Is this necessary? */

      progname = oprogname;

# if 0
      /* Print out all the Xrm resources we read. */
      {
        XrmName name = { 0 };
        XrmClass class = { 0 };
        int count = 0;
        XrmEnumerateDatabase (db, &name, &class, XrmEnumAllLevels, xrm_mapper,
                              (void *) &count);
      }
# endif
    }

  s->multi_screen_p = multi_screen_p (s->dpy);

  /* Let's see if the server supports DPMS.
   */
  s->dpms_supported_p = FALSE;
# ifdef HAVE_DPMS_EXTENSION
  {
    int op = 0, event = 0, error = 0;
    if (s->dpy && XQueryExtension (s->dpy, "DPMS", &op, &event, &error))
      /* Technically this should also check DPMSCapable(), but this is
         almost certainly close enough. */
      s->dpms_supported_p = TRUE;
    else if (s->debug_p)
      fprintf (stderr, "%s: server does not support power management\n",
               blurb());
  }
# else  /* !HAVE_DPMS_EXTENSION */
  if (s->debug_p)
    fprintf (stderr, "%s: DPMS not supported at compile time\n", blurb());
# endif /* !HAVE_DPMS_EXTENSION */

# if defined(__APPLE__) && !defined(__OPTIMIZE__)
  s->dpms_supported_p = TRUE;  /* macOS X11: debugging kludge */
# endif

  if (s->dpy)
    init_xscreensaver_atoms (s->dpy);

  hack_environment (s);  /* must be before initialize_sort_map() */
  load_init_file (s->dpy, p);
  initialize_sort_map (s);

  s->gl_visual = get_best_gl_visual (s);
  s->dialog = g_object_new (XSCREENSAVER_DIALOG_TYPE, NULL);
  XSCREENSAVER_DIALOG (s->dialog)->main = win;

  gtk_window_set_transient_for (GTK_WINDOW (s->dialog), GTK_WINDOW (win));

  sensitize_menu_items (s, TRUE);
  populate_hack_list (s);
  populate_prefs_page (s);
  sensitize_demo_widgets (s, FALSE);
  scroll_to_current_hack (s);
  if (s->dpy && !s->wayland_p)
    fix_preview_visual (s);
  if (! s->multi_screen_p)
    hide_mode_menu_random_same (s);

  restore_window_position (s, GTK_WINDOW (self), FALSE);

  g_timeout_add (60 * 1000, check_blanked_timer, s);

  /* Attach the actions and their keybindings. */
  {
    int i;
    GtkApplication *app = gtk_window_get_application (GTK_WINDOW (win));
    g_action_map_add_action_entries (G_ACTION_MAP (app),
                                     app_entries, countof (app_entries),
                                     win);
    for (i = 0; i < countof (accels); i++)
      {
        const gchar *a[2];
        a[0] = accels[i][1];
        a[1] = 0;
        gtk_application_set_accels_for_action (GTK_APPLICATION (app),
                                               accels[i][0], a);
      }
  }

# if 0
  /* Load every configurator in turn, to scan them for errors all at once. */
  if (s->debug_p)
    {
      int i;
      for (i = 0; i < p->screenhacks_count; i++)
        {
          screenhack *hack = p->screenhacks[i];
          conf_data *d = load_configurator (hack->command, s->debug_p);
          if (d) free_conf_data (d);
        }
    }
# endif

  /* Grab the screenshot pixmap before mapping the window. */
  if (s->dpy && !s->wayland_p)
    {
      GdkWindow *gw = gtk_widget_get_window (win->preview);
      Window xw = gdk_x11_window_get_xid (gw);
      s->screenshot = screenshot_grab (s->dpy, xw, TRUE, s->debug_p);
    }

  /* Issue any warnings about the running xscreensaver daemon.
     Wait a few seconds, in case things are still starting up. */
  g_timeout_add (5 * 1000, the_network_is_not_the_computer, s);

  /* This totally sucks -- set a timer that whacks the scrollbar 0.5 seconds
     after we start up.  Otherwise, it always appears scrolled to the top. */
  g_timeout_add (500, delayed_scroll_kludge, s);

  s->initializing_p = FALSE;
}


static void
xscreensaver_window_init (XScreenSaverWindow *win)
{
  gtk_widget_init_template (GTK_WIDGET (win));
  g_signal_connect (win, "destroy",
                    G_CALLBACK (xscreensaver_window_destroy), win);
  g_signal_connect (win, "realize",
                    G_CALLBACK (xscreensaver_window_realize), win);
  g_signal_connect (win, "configure-event",
                    G_CALLBACK (xscreensaver_window_resize_cb),win);
  g_signal_connect (win->preview, "configure-event",
                    G_CALLBACK (preview_resize_cb),win);
}


static void
xscreensaver_window_class_init (XScreenSaverWindowClass *class)
{
  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
                                     "/org/jwz/xscreensaver/demo.ui");

  /* Fill in the widget fields in XScreenSaverWindow with the corresponding
     objects created from demo.ui. */
# undef W
# define W(NAME) \
    gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), \
                                          XScreenSaverWindow, NAME);
   ALL_WINDOW_WIDGETS
#  undef W
}


/****************************************************************************

 XScreenSaverDialog

 ****************************************************************************/

static void
xscreensaver_dialog_destroy (GObject *object)
{
  /* Called by WM close box, but not by File / Quit */
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (object);
  XScreenSaverWindow *win = dialog->main;
  flush_dialog_changes_and_save (&win->state);
  G_OBJECT_CLASS (xscreensaver_dialog_parent_class)->dispose (object);
}


static void
xscreensaver_dialog_realize (GtkWidget *self, gpointer user_data)
{
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (self);
  XScreenSaverWindow *win = dialog->main;
  state *s = &win->state;
  restore_window_position (s, GTK_WINDOW (self), TRUE);
}


/* When the window is moved, save the new origin in .xscreensaver. */
static gboolean
xscreensaver_dialog_resize_cb (GtkWindow *window, GdkEvent *event,
                               gpointer data)
{
  XScreenSaverDialog *dialog = XSCREENSAVER_DIALOG (window);
  XScreenSaverWindow *win = dialog->main;
  state *s = &win->state;
  save_window_position (s, GTK_WINDOW (dialog),
                        event->configure.x, event->configure.y, TRUE);
  return FALSE;
}


/* The WM close box. */
static gboolean
xscreensaver_dialog_delete_cb (GtkWidget *self, GdkEvent *event,
                               gpointer user_data)
{
  settings_cancel_cb (GTK_WIDGET (self), user_data);
  return TRUE;  /* Do not run other handlers */
}


static void
xscreensaver_dialog_init (XScreenSaverDialog *win)
{
  gtk_widget_init_template (GTK_WIDGET (win));
  g_signal_connect (win, "destroy",
                    G_CALLBACK (xscreensaver_dialog_destroy), win);
  g_signal_connect (win, "realize",
                    G_CALLBACK (xscreensaver_dialog_realize), win);
  g_signal_connect (win, "configure-event",
                    G_CALLBACK (xscreensaver_dialog_resize_cb), win);
  g_signal_connect (win, "delete-event",
                    G_CALLBACK (xscreensaver_dialog_delete_cb), win);
}


static void
xscreensaver_dialog_class_init (XScreenSaverDialogClass *class)
{
  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
                                     "/org/jwz/xscreensaver/prefs.ui");

  /* Fill in the widget fields in XScreenSaverDialog with the corresponding
     objects created from prefs.ui. */
# undef W
# define W(NAME) \
    gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), \
                                          XScreenSaverDialog, NAME);
   ALL_DIALOG_WIDGETS
#  undef W
}


/****************************************************************************

 XScreenSaverApp

 ****************************************************************************/


static void
xscreensaver_app_init (XScreenSaverApp *app)
{
}


static void
xscreensaver_app_startup (GApplication *app)
{
  G_APPLICATION_CLASS (xscreensaver_app_parent_class)->startup (app);

  /* Without this, the floating point numbers in the XML files are not
     parsed properly in locales that use commas instead of periods in
     floats: sscanf %f expects "1.0" to be "1,0" and returns 0.

     This must be called later than main() because something beneath
     g_application_run() calls setlocale(LC_ALL, "") and would override it.
  */
# ifdef ENABLE_NLS
  if (!setlocale (LC_NUMERIC, "C"))
    fprintf (stderr, "%s: warning: could not set LC_NUMERIC=C\n", blurb());
# endif /* ENABLE_NLS */
}


static void
xscreensaver_app_activate (GApplication *app)
{
  XScreenSaverWindow *win =
    g_object_new (XSCREENSAVER_WINDOW_TYPE, "application", app, NULL);
  win->state.debug_p = XSCREENSAVER_APP (app)->cmdline_debug_p;
  gtk_widget_show_all (GTK_WIDGET (win));
  gtk_window_present (GTK_WINDOW (win));
}


static void
xscreensaver_app_open (GApplication *app,
                       GFile **files, gint n_files,
                       const gchar *hint)
{
  GList *windows = gtk_application_get_windows (GTK_APPLICATION (app));
  if (windows)
    gtk_window_present (GTK_WINDOW (windows->data));
  else
    xscreensaver_app_activate (app);
}


static int
opts_cb (GApplication *app, GVariantDict *opts, gpointer data)
{
  if (g_variant_dict_contains (opts, "version")) {
    fprintf (stderr, "%s\n", screensaver_id+4);
    return 0;
  } else if (g_variant_dict_contains (opts, "debug")) {
    XSCREENSAVER_APP (app)->cmdline_debug_p = TRUE;
    return -1;
  } else {
    return -1;
  }
}


static void
xscreensaver_app_class_init (XScreenSaverAppClass *class)
{
  G_APPLICATION_CLASS (class)->startup  = xscreensaver_app_startup;
  G_APPLICATION_CLASS (class)->activate = xscreensaver_app_activate;
  G_APPLICATION_CLASS (class)->open     = xscreensaver_app_open;
}

static XScreenSaverApp *
xscreensaver_app_new (void)
{
  XScreenSaverApp *app = g_object_new (XSCREENSAVER_APP_TYPE,
                                       "application-id",
                                       "org.jwz.xscreensaver.settings",
                                       NULL);

  g_application_add_main_option (G_APPLICATION (app), "version", 'v', 
                                 G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
                                 "Print the version number",
                                 NULL);
  g_application_add_main_option (G_APPLICATION (app), "debug", 0, 
                                 G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
                                 "Print diagnostics to stderr",
                                 NULL);
  g_signal_connect (app, "handle-local-options", G_CALLBACK (opts_cb), app);
  return app;
}


int
main (int argc, char *argv[])
{
  char *s;
  progname = argv[0];
  s = strrchr (progname, '/');
  if (s) progname = s+1;
  g_log_set_default_handler (g_logger, NULL);
  g_log_set_writer_func (g_other_logger, NULL, NULL);

# ifdef ENABLE_NLS
  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
  textdomain (GETTEXT_PACKAGE);
  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
# endif /* ENABLE_NLS */

  return g_application_run (G_APPLICATION (xscreensaver_app_new()),
                            argc, argv);
}



#endif /* HAVE_GTK -- whole file */
