#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdarg.h>
#include <errno.h>
#include <limits.h>

#include <unistd.h>
#include <dirent.h>
#include <pwd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>

#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <glib.h>

#include "gtklife.h"
#include "life.h"
#include "ewmh.h"
#include "util.h"
#include "icons.h"

/*** Constants ***/

static point  null_point = {-1, -1};
static rect   null_rect  = {{-1, -1}, {-1, -1}};

char* file_format_names[NUM_FORMATS] = {"GLF (GtkLife)", "RLE", "Life 1.05", "Life 1.06", "XLife"};
char* default_file_extensions[]      = {"glf", "rle", "lif", "l", NULL};
char* color_names[NUM_COLORS]        = {"background", "live cells", "grid", "selection"};

static command_type  component_toggle_commands[NUM_COMPONENTS] = {
    CMD_VIEW_SHOW_TOOLBAR,
    CMD_VIEW_SHOW_SIDEBAR,
    CMD_VIEW_SHOW_SCROLLBARS,
    CMD_VIEW_SHOW_STATUSBAR
};

static char*  component_short_names[NUM_COMPONENTS] = {
    "toolbar",
    "sidebar",
    "scrollbars",
    "statusbar"
};

static char*  component_full_names[NUM_COMPONENTS] = {
    "Toolbar",
    "Sidebar",
    "Scrollbars",
    "Status Bar"
};

pattern_coll  pattern_collections[NUM_COLLECTIONS] = {
    {"lpa",     "Life Pattern Archive - Alan Hensel"},
    {"jslife",  "JSLife - Jason Summers"},
};


help_browser  help_browsers[NUM_HELP_BROWSERS] = {
    {"Firefox", "firefox -remote 'openURL($f, new-window)' || firefox '$f'"},
    {"Mozilla", "mozilla -remote 'openURL($f, new-window)' || mozilla '$f'"}
};

/* Steps when adding a new menu item:
 *
 * Add an item to the command_type enum
 * Add an entry in command_info[] (below)
 * Add an entry in menu_items[]
 * Add an entry in toolbar_buttons[], if desired
 * Add a prototype and body for the handler function
 */

/* Information on menu/toolbar commands, used by handle_menu_item_activate() in dispatching commands.
 * Fields: type, handler
 */
command_info_type command_info[NUM_COMMANDS] = {
    { ITEM_NORMAL, file_new },
    { ITEM_NORMAL, file_open },
    { ITEM_NORMAL, file_reopen },
    { ITEM_NORMAL, file_save },
    { ITEM_NORMAL, file_save_as },
    { ITEM_NORMAL, file_description },
    { ITEM_NORMAL, file_change_collection },
    { ITEM_NORMAL, file_quit },

    { ITEM_NORMAL, view_zoom_in },
    { ITEM_NORMAL, view_zoom_out },
    { ITEM_RADIO,  view_zoom_1 },
    { ITEM_RADIO,  view_zoom_2 },
    { ITEM_RADIO,  view_zoom_4 },
    { ITEM_RADIO,  view_zoom_8 },
    { ITEM_RADIO,  view_zoom_16 },
    { ITEM_NORMAL, view_scroll_left },
    { ITEM_NORMAL, view_scroll_right },
    { ITEM_NORMAL, view_scroll_up },
    { ITEM_NORMAL, view_scroll_down },
    { ITEM_NORMAL, view_scroll_nw },
    { ITEM_NORMAL, view_scroll_ne },
    { ITEM_NORMAL, view_scroll_sw },
    { ITEM_NORMAL, view_scroll_se },
    { ITEM_NORMAL, view_scroll_page_left },
    { ITEM_NORMAL, view_scroll_page_right },
    { ITEM_NORMAL, view_scroll_page_up },
    { ITEM_NORMAL, view_scroll_page_down },
    { ITEM_NORMAL, view_scroll_page_nw },
    { ITEM_NORMAL, view_scroll_page_ne },
    { ITEM_NORMAL, view_scroll_page_sw },
    { ITEM_NORMAL, view_scroll_page_se },
    { ITEM_NORMAL, view_recenter },
    { ITEM_NORMAL, view_goto },
    { ITEM_NORMAL, view_find_active_cells },
    { ITEM_CHECK,  view_show_toolbar },
    { ITEM_CHECK,  view_show_sidebar },
    { ITEM_CHECK,  view_show_scrollbars },
    { ITEM_CHECK,  view_show_statusbar },
    { ITEM_CHECK,  view_fullscreen },

    { ITEM_RADIO,  edit_drawing_tool },
    { ITEM_RADIO,  edit_selection_tool },
    { ITEM_NORMAL, edit_cut },
    { ITEM_NORMAL, edit_copy },
    { ITEM_NORMAL, edit_clear },
    { ITEM_NORMAL, edit_paste },
    { ITEM_NORMAL, edit_move },
    { ITEM_NORMAL, edit_cancel_paste },
    { ITEM_NORMAL, edit_preferences },

    { ITEM_NORMAL, run_start_stop },
    { ITEM_NORMAL, run_step },
    { ITEM_NORMAL, run_jump },
    { ITEM_NORMAL, run_faster },
    { ITEM_NORMAL, run_slower },
    { ITEM_NORMAL, run_speed },

    { ITEM_NORMAL, help_help },
    { ITEM_NORMAL, help_pattern_archive },
    { ITEM_NORMAL, help_glf_file_format },
    { ITEM_NORMAL, help_about }
};

/* Menubar
 * Fields: Item, Keybinding, Handler function (H or NULL), Command ID, Item type
 * The same handler is used for everything here; the real bound function is stored in command_info[].
 */
#define H handle_menu_item_activate
static GtkItemFactoryEntry menu_items[] = {
    { "/_File",                 0,               0,  0,                         "<Branch>"       },
    { "/File/tearoff",          0,               0,  0,                         "<Tearoff>"      },
    { "/File/_New",             "<control>N",    H,  CMD_FILE_NEW,              0                },
    { "/File/_Open...",         "<control>O",    H,  CMD_FILE_OPEN,             0                },
    { "/File/_Reopen",          "R",             H,  CMD_FILE_REOPEN,           0                },
    { "/File/_Save",            "<control>S",    H,  CMD_FILE_SAVE,             0                },
    { "/File/Save _As...",      "<control>A",    H,  CMD_FILE_SAVE_AS,          0                },
    { "/File/sep1",             0,               0,  0,                         "<Separator>"    },
    { "/File/_Description...",  "D",             H,  CMD_FILE_DESCRIPTION,      0                },
    { "/File/sep2",             0,               0,  0,                         "<Separator>"    },
    { "/File/_Change Pattern Collection...", "C",H,  CMD_FILE_CHANGE_COLLECTION,0                },
    { "/File/sep3",             0,               0,  0,                         "<Separator>"    },
    { "/File/_Quit",            "<control>Q",    H,  CMD_FILE_QUIT,             0                },

    { "/_View",                 0,               0,  0,                         "<Branch>"       },
    { "/View/tearoff",          0,               0,  0,                         "<Tearoff>"      },
    { "/View/Zoom _In",         "equal",         H,  CMD_VIEW_ZOOM_IN,          0                },
    { "/View/Zoom _Out",        "minus",         H,  CMD_VIEW_ZOOM_OUT,         0                },
    { "/View/_Zoom",            0,               0,  0,                         "<Branch>"       },
    { "/View/Zoom/tearoff",     0,               0,  0,                         "<Tearoff>"      },
    { "/View/Zoom/_1:1",        "1",             H,  CMD_VIEW_ZOOM_1,           "<RadioItem>"    },
    { "/View/Zoom/_2:1",        "2",             H,  CMD_VIEW_ZOOM_2,           "/View/Zoom/1:1" },
    { "/View/Zoom/_4:1",        "4",             H,  CMD_VIEW_ZOOM_4,           "/View/Zoom/1:1" },
    { "/View/Zoom/_8:1",        "8",             H,  CMD_VIEW_ZOOM_8,           "/View/Zoom/1:1" },
    { "/View/Zoom/1_6:1",       "Z",             H,  CMD_VIEW_ZOOM_16,          "/View/Zoom/1:1" },
    { "/View/sep1",             0,               0,  0,                         "<Separator>"    },
    { "/View/_Scroll",          0,               0,  0,                         "<Branch>"       },
    { "/View/Scroll/Left",      "KP_4",          H,  CMD_VIEW_SCROLL_LEFT,      0                },
    { "/View/Scroll/Right",     "KP_6",          H,  CMD_VIEW_SCROLL_RIGHT,     0                },
    { "/View/Scroll/Up",        "KP_8",          H,  CMD_VIEW_SCROLL_UP,        0                },
    { "/View/Scroll/Down",      "KP_2",          H,  CMD_VIEW_SCROLL_DOWN,      0                },
    { "/View/Scroll/NW",        "KP_7",          H,  CMD_VIEW_SCROLL_NW,        0                },
    { "/View/Scroll/NE",        "KP_9",          H,  CMD_VIEW_SCROLL_NE,        0                },
    { "/View/Scroll/SW",        "KP_1",          H,  CMD_VIEW_SCROLL_SW,        0                },
    { "/View/Scroll/SE",        "KP_3",          H,  CMD_VIEW_SCROLL_SE,        0                },
    { "/View/Scroll/Page Left", "<control>KP_4", H,  CMD_VIEW_SCROLL_PAGE_LEFT, 0                },
    { "/View/Scroll/Page Right","<control>KP_6", H,  CMD_VIEW_SCROLL_PAGE_RIGHT,0                },
    { "/View/Scroll/Page Up",   "<control>KP_8", H,  CMD_VIEW_SCROLL_PAGE_UP,   0                },
    { "/View/Scroll/Page Down", "<control>KP_2", H,  CMD_VIEW_SCROLL_PAGE_DOWN, 0                },
    { "/View/Scroll/Page NW",   "<control>KP_7", H,  CMD_VIEW_SCROLL_PAGE_NW,   0                },
    { "/View/Scroll/Page NE",   "<control>KP_9", H,  CMD_VIEW_SCROLL_PAGE_NE,   0                },
    { "/View/Scroll/Page SW",   "<control>KP_1", H,  CMD_VIEW_SCROLL_PAGE_SW,   0                },
    { "/View/Scroll/Page SE",   "<control>KP_3", H,  CMD_VIEW_SCROLL_PAGE_SE,   0                },
    { "/View/_Recenter",        "KP_5",          H,  CMD_VIEW_RECENTER,         0                },
    { "/View/_Goto...",         "G",             H,  CMD_VIEW_GOTO,             0                },
    { "/View/_Find Active Cells", 0,             H,  CMD_VIEW_FIND_ACTIVE_CELLS,0                },
    { "/View/sep2",             0,               0,  0,                         "<Separator>"    },
    { "/View/Show Toolbar",     0,               H,  CMD_VIEW_SHOW_TOOLBAR,     "<CheckItem>"    },
    { "/View/Show Sidebar",     0,               H,  CMD_VIEW_SHOW_SIDEBAR,     "<CheckItem>"    },
    { "/View/Show Scrollbars",  0,               H,  CMD_VIEW_SHOW_SCROLLBARS,  "<CheckItem>"    },
    { "/View/Show Status Bar",  0,               H,  CMD_VIEW_SHOW_STATUSBAR,   "<CheckItem>"    },
    { "/View/F_ullscreen",      "F",             H,  CMD_VIEW_FULLSCREEN,       "<CheckItem>"    },

    { "/_Edit",                 0,               0,  0,                         "<Branch>"       },
    { "/Edit/tearoff",          0,               0,  0,                         "<Tearoff>"      },
    { "/Edit/_Drawing Tool",    0,               H,  CMD_EDIT_DRAWING_TOOL,     "<RadioItem>"    },
    { "/Edit/_Selection Tool",  0,               H,  CMD_EDIT_SELECTION_TOOL,   "/Edit/Drawing Tool" },
    { "/Edit/sep1",             0,               0,  0,                         "<Separator>"    },
    { "/Edit/_Cut",             "<control>X",    H,  CMD_EDIT_CUT,              0                },
    { "/Edit/C_opy",            "<control>C",    H,  CMD_EDIT_COPY,             0                },
    { "/Edit/C_lear",           "<control>D",    H,  CMD_EDIT_CLEAR,            0                },
    { "/Edit/_Paste",           "<control>V",    H,  CMD_EDIT_PASTE,            0                },
    { "/Edit/_Move",            "M",             H,  CMD_EDIT_MOVE,             0                },
    { "/Edit/C_ancel Paste",    "<control>G",    H,  CMD_EDIT_CANCEL_PASTE,     0                },
    { "/Edit/sep2",             0,               0,  0,                         "<Separator>"    },
    { "/Edit/Pre_ferences...",  "P",             H,  CMD_EDIT_PREFERENCES,      0                },

    { "/_Run",                  0,               0,  0,                         "<Branch>"       },
    { "/Run/tearoff",           0,               0,  0,                         "<Tearoff>"      },
    { "/Run/_Start",            "S",             H,  CMD_RUN_START_STOP,        0                },
    { "/Run/S_tep",             "T",             H,  CMD_RUN_STEP,              0                },
    { "/Run/_Jump",             "J",             H,  CMD_RUN_JUMP,              0                },
    { "/Run/sep1",              0,               0,  0,                         "<Separator>"    },
    { "/Run/_Faster",           "period",        H,  CMD_RUN_FASTER,            0                },
    { "/Run/Slo_wer",           "comma",         H,  CMD_RUN_SLOWER,            0                },
    { "/Run/S_peed...",         "<alt>S",        H,  CMD_RUN_SPEED,             0                },

    { "/_Help",                 0,               0,  0,                         "<LastBranch>"   },
    { "/Help/tearoff",          0,               0,  0,                         "<Tearoff>"      },
    { "/Help/_Help",            "F1",            H,  CMD_HELP_HELP,             0                },
    { "/Help/_Pattern Archive", 0,               H,  CMD_HELP_PATTERN_ARCHIVE,  0                },
    { "/Help/_GLF File Format", 0,               H,  CMD_HELP_GLF_FILE_FORMAT,  0                },
    { "/Help/_About GtkLife",   0,               H,  CMD_HELP_ABOUT,            0                }
};
#undef H

/* Toolbar
 * Fields: Command ID, Tooltip, Icon data
 */
static toolbar_button toolbar_buttons[] = {
    { CMD_FILE_NEW,            "Clear the grid",                new_icon         },
    { CMD_FILE_REOPEN,         "Reopen the current file",       reopen_icon      },
    { CMD_FILE_OPEN,           "Open a pattern file",           open_icon        },
    { CMD_FILE_SAVE,           "Save pattern",                  save_icon        },
    { CMD_FILE_DESCRIPTION,    "View/edit pattern description", description_icon },

    {-1,NULL,NULL},

    { CMD_VIEW_ZOOM_IN,        "Zoom in",                       zoom_in_icon     },
    { CMD_VIEW_ZOOM_OUT,       "Zoom out",                      zoom_out_icon    },
    { CMD_VIEW_ZOOM_16,        "Zoom all the way in",           zoom_16_icon     },
    { CMD_VIEW_ZOOM_1,         "Zoom 1:1",                      zoom_1_icon      },

    {-1,NULL,NULL},

    { CMD_EDIT_DRAWING_TOOL,   "Drawing tool",                  draw_icon        },
    { CMD_EDIT_SELECTION_TOOL, "Selection tool",                select_icon      },
    { CMD_EDIT_CUT,            "Cut",                           cut_icon         },
    { CMD_EDIT_COPY,           "Copy",                          copy_icon        },
    { CMD_EDIT_PASTE,          "Paste",                         paste_icon       },

    {-1,NULL,NULL},

    { CMD_RUN_START_STOP,   "Start/stop",                    start_icon       },
    { CMD_RUN_STEP,         "Step ahead one generation",     step_icon        }
};

/*** Globals ***/

static char*  user_dir = NULL;

/* Preferences */
static struct {
    char*            last_version;
    char*            default_collection;
    int32            default_window_width;
    int32            default_window_height;
    int32            default_zoom;
    boolean          default_visible_components[NUM_COMPONENTS];
    boolean          default_fullscreen;
    uint32           colors[NUM_COLORS];
    int32            default_speed;
    int32            speed_slider_min;
    int32            speed_slider_max;
    int32            speed_increment;
    boolean          default_skip_frames;
    char*            help_browser;
} config = {
    NULL,
    NULL,
    DEFAULT_WINDOW_WIDTH,
    DEFAULT_WINDOW_HEIGHT,
    DEFAULT_ZOOM,
    {DEFAULT_SHOW_TOOLBAR, DEFAULT_SHOW_SIDEBAR, DEFAULT_SHOW_SCROLLBARS, DEFAULT_SHOW_STATUSBAR},
    DEFAULT_FULLSCREEN,
    {DEFAULT_BG_COLOR, DEFAULT_CELL_COLOR, DEFAULT_GRID_COLOR, DEFAULT_SELECT_COLOR},
    DEFAULT_SPEED,
    DEFAULT_SPEED_SLIDER_MIN,
    DEFAULT_SPEED_SLIDER_MAX,
    DEFAULT_SPEED_INCREMENT,
    DEFAULT_SKIP_FRAMES,
    NULL
};

/* Some general state globals */
static struct {
    /* Directories and pattern paths */
    char*             home_dir;
    char*             pattern_path;
    file_format_type  file_format;
    char*             current_collection;
    coll_status_type  collection_status;
    char*             current_dir;
    pattern_file*     sidebar_files;
    int32             num_sidebar_files;
    pattern_file*     sub_sidebar_files;
    int32             num_sub_sidebar_files;
    recent_file       recent_files[MAX_RECENT_FILES];

    /* Running pattern stats */
    boolean  pattern_running;
    int32    speed;
    boolean  skip_frames;
    uint32   start_tick;
    uint64   start_time;
    int32    skipped_frames;
    boolean  jump_cancelled;

    /* Data related to mouse tracking, mouse dragging and cut/paste */
    tracking_mouse_type  tracking_mouse;
    boolean     select_mode;
    point       last_drawn;
    rect        selection;
    uint64      selection_start_time;
    cage_type*  copy_buffer;
    rect        copy_rect;
    rect        paste_box;
    boolean     moving;

    /* Data related to statusbar messages */
    boolean     temp_message_active;
    uint64      temp_message_start_time;
    char*       original_message;
} state;

/* Globals related to the GUI display state */
static struct {
    /* Zoom level, where [zoom]:1 is the display ratio */
    int32        zoom;

    /* Physical canvas and logical viewport. Canvas is measured in pixels,
     * viewport in cells */
    dimension    canvas_size;
    dimension    eff_canvas_size;   /* "effective" canvas size--minus any unused space */
    dimension    viewport_sizes[MAX_ZOOM+1];
    dimension    viewport_size;
    rect         viewport;

    /* Clipping rectangle, in pixels */
    rect         update;

    /* Offscreen pixmap data */
    uint8*       life_pixmap;
    uint32       life_pixmap_alloc_len;
    GdkRgbCmap*  life_pixmap_colormap;

    /* Which parts of the GUI are visible */
    boolean      visible_components[NUM_COMPONENTS];
    boolean      sub_sidebar_visible;
    boolean      fullscreen;
} dstate;

/* GUI objects that need to be global */
static struct {
    GtkWidget*   window;
    GtkWidget*   menubar;
    GtkTooltips* menu_tooltips;
    GtkWidget*   toolbar;
    GtkWidget*   sidebar;
    GtkWidget*   main_sidebar;
    GtkWidget*   sub_sidebar;
    GtkWidget*   patterns_clist;
    GtkWidget*   sub_patterns_clist;
    GtkWidget*   canvas;
    GtkWidget*   vscrollbar;
    GtkWidget*   hscrollbar;
    GtkWidget*   statusbar;
    GtkWidget*   recent_files_separator;
    GtkWidget*   start_stop_menu_label;
    GtkWidget*   start_stop_pixmap;
    GtkWidget*   start_stop_alt_pixmap;
    GtkWidget*   speed_label;
    GtkWidget*   speed_slider;
    GtkWidget*   hover_point_label;
    GtkWidget*   status_message_label;
    GtkWidget*   tick_label;
    GtkWidget*   population_label;
    command_widgets_type   command_widgets[NUM_COMMANDS];

    GtkWidget*   description_dialog;
    GtkWidget*   description_textbox;
} gui;

/*** Main ***/

int main(int argc, char *argv[])
{
    struct stat  statbuf;
    char*        resolved_path;

    set_prog_name(PROG);
    if (argc > 2)
        usage_abort();
    state.home_dir = get_home_directory();
    create_user_dir();
    load_preferences();
    apply_defaults();
    load_recent_files_list();
    gtk_init(&argc, &argv);
    init_gui();
    load_keybindings();

    if (argc > 1) {
        resolved_path = get_canonical_path(argv[1]);
        if (!resolved_path)
            error_dialog("Bad path on command line:\n\"%s\" does not exist", argv[1]);
        else if (stat(resolved_path, &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) {
            validate_and_set_collection_dir(resolved_path, TRUE);
            state.collection_status = COLL_OKAY;
        } else {
            attempt_load_pattern(resolved_path);
            set_current_dir_from_file(resolved_path);
        }
        free(resolved_path);
    }

    if (state.collection_status == COLL_BAD)
        error_dialog("Warning: the configured pattern collection is no\n"
                     "longer valid, restoring default collection.");
    else if (state.collection_status == COLL_DEFAULT_BAD)
        error_dialog("Warning: The default pattern collection is unavailable.\n"
                     "(Has %s been properly installed?)", TITLE);

    main_loop();

    return 0;
}

void usage_abort(void)
{
    fprintf(stderr, "Usage: %s <pattern file> or %s <pattern directory>\n", PROG, PROG);
    exit(1);
}

/* Main loop functions */

/* Handle events and run the Life pattern
 */
void main_loop(void)
{
    while (1) {
        /* Block if no pattern is running and we don't need to track mouse movement, else poll */
        while (gtk_events_pending())
            gtk_main_iteration_do(FALSE);
        handle_mouse_movement();
        if (state.temp_message_active &&
            get_time_milliseconds() - state.temp_message_start_time >= TEMP_MESSAGE_INTERVAL)
            restore_status_message();
        if (state.pattern_running)
            tick_and_update();
        else
            usleep(10000);   /* don't eat up all the processor just for mouse tracking */
    }
}

/* If we're in sync with the requested speed (or the pattern is being stepped through manually),
 * process the next Life tick, update the pixmap and redraw the screen. If going too fast, don't do
 * anything. If too slow and state.skip_frames is TRUE, and state.skipped_frames is not maxed out
 * yet, process the next tick and update the pixmap, but don't redraw the screen.
 */
void tick_and_update(void)
{
    int32  running_fps = 0;
    uint64 t;

    if (state.pattern_running) {
        t = get_time_milliseconds();
        if (t == state.start_time)
            running_fps = MAX_SPEED + 1;
        else
            running_fps = ROUND((double)(tick - state.start_tick) * 1000.0 /
                                (double)(t - state.start_time));
        if (tick > state.start_tick && running_fps > state.speed)
            return;
    }
    if (!state.skipped_frames) {
        dstate.update.start.x = dstate.canvas_size.width  - 1;
        dstate.update.start.y = dstate.canvas_size.height - 1;
        dstate.update.end.x = dstate.update.end.y = 0;
    }

    next_tick(TRUE);

    if (state.pattern_running && running_fps < state.speed && state.skip_frames &&
        state.skipped_frames < MAX_SKIPPED_FRAMES)
        state.skipped_frames++;
    else {
        trigger_canvas_update();
        update_tick_label();
        update_population_label();
    }
}

/*** Functions for reading/saving user settings ***/

/* Create the user directory (if necessary), and record its full path.
 */
void create_user_dir(void)
{
    user_dir = dsprintf("%s/%s", state.home_dir, USER_DIR);
    if (mkdir(user_dir, 0777) < 0 && errno != EEXIST) {
        sys_warn("can't create user directory %s", user_dir);
        return;
    }
}

/* Read and apply saved user preferences
 */
void load_preferences(void)
{
    FILE*  f;
    char*  path;
    char*  line;
    char*  key;
    char*  val;
    char*  equal_pos;
    int32  component;

    config.last_version = safe_strdup("0.0");
    config.default_collection = dsprintf("%s/patterns/%s/", DATADIR,
                                         pattern_collections[DEFAULT_COLLECTION].dir);

#ifdef GTK2
    path = dsprintf("%s/preferences-gtk2", user_dir);
#else
    path = dsprintf("%s/preferences", user_dir);
#endif
    f = fopen(path, "r");
    free(path);
    if (!f)
        return;

    while ((line = dgets(f)) != NULL) {
        equal_pos = strchr(line, '=');
        if (!equal_pos) {
            warn("invalid line '%s' found in preferences file, ignoring", line);
            free(line);
            continue;
        }
        *equal_pos = '\0';
        key = line;
        val = equal_pos+1;

        if (strstr(key, "last_version")) {
            free(config.last_version);
            config.last_version = safe_strdup(val);
        } else if (strstr(key, "default_collection")) {
            free(config.default_collection);
            config.default_collection = safe_strdup(val);
        } else if (strstr(key, "window_width"))
            config.default_window_width = atoi(val);
        else if (strstr(key, "window_height"))
            config.default_window_height = atoi(val);
        else if (STR_EQUAL(key, "default_zoom"))
            config.default_zoom = atoi(val);
        else if (STR_STARTS_WITH(key, "default_show_") &&
                 (component = get_component_by_short_name(key+13)) >= 0)
            config.default_visible_components[component] = atoi(val);
        else if (STR_EQUAL(key, "default_fullscreen"))
            config.default_fullscreen = atoi(val);
        else if (STR_STARTS_WITH(key, "color_") && is_numeric(key+6) && atoi(key+6) < NUM_COLORS)
            config.colors[atoi(key+6)] = strtoul(val, NULL, 16);
        else if (STR_EQUAL(key, "default_speed"))
            config.default_speed = atoi(val);
        else if (STR_EQUAL(key, "speed_slider_min"))
            config.speed_slider_min = atoi(val);
        else if (STR_EQUAL(key, "speed_slider_max"))
            config.speed_slider_max = atoi(val);
        else if (STR_EQUAL(key, "speed_increment"))
            config.speed_increment = atoi(val);
        else if (STR_EQUAL(key, "default_skip_frames"))
            config.default_skip_frames = atoi(val);
        else if (STR_EQUAL(key, "help_browser")) {
            free(config.help_browser);
            config.help_browser = (IS_EMPTY_STRING(val) ? NULL : safe_strdup(val));
        } else
            warn("invalid key '%s' found in preferences file, ignoring", key);

        free(line);
    }

    /* We only added the "please choose a help browser" prompt as of 5.1, so make sure they
     * see it: */
    if (!last_version_atleast(5.1)) {
        free(config.help_browser);
        config.help_browser = NULL;
    }

    fclose(f);
}

/* Apply default values to various globals, based on preferences and/or system defaults.
 */
void apply_defaults(void)
{
    char*  default_dir;

    if (!validate_and_set_collection_dir(config.default_collection, FALSE)) {
        state.collection_status = COLL_BAD;
        default_dir = dsprintf("%s/patterns/%s/", DATADIR,
                               pattern_collections[DEFAULT_COLLECTION].dir);
        if (!validate_and_set_collection_dir(default_dir, FALSE)) {
            state.collection_status = COLL_DEFAULT_BAD;
            set_collection_dir(default_dir, FALSE);
        }
        free(default_dir);
    }

    state.speed       = config.default_speed;
    state.skip_frames = config.default_skip_frames;
    dstate.zoom       = config.default_zoom;

    memcpy(dstate.visible_components, config.default_visible_components,
           NUM_COMPONENTS * sizeof(boolean));

    state.selection = state.copy_rect = state.paste_box = null_rect;
    state.last_drawn = null_point;
}

/* Read and apply saved user keybindings
 */
void load_keybindings(void)
{
    char*  path;

    /* Menus changed in 5.1, so discard old keybindings to be safe */
    if (!last_version_atleast(5.1))
        return;

#ifdef GTK2
    path = dsprintf("%s/keybindings-gtk2", user_dir);
    gtk_accel_map_load(path);
#else
    path = dsprintf("%s/keybindings", user_dir);
    gtk_item_factory_parse_rc(path);
#endif
    free(path);
}

/* Read the recent file list to put in the "File" menu
 */
void load_recent_files_list(void)
{
    FILE*  f;
    char*  rf_path;
    char*  full_path;
    char*  filename;
    char*  slash_pos;
    int32  i;

    memset(state.recent_files, 0, sizeof(recent_file) * MAX_RECENT_FILES);
    rf_path = dsprintf("%s/recent", user_dir);
    f = fopen(rf_path, "r");
    free(rf_path);
    if (!f)
        return;
    i = 0;
    while (i < MAX_RECENT_FILES && (full_path = dgets(f)) != NULL) {
        slash_pos = strrchr(full_path, '/');
        filename = safe_strdup(slash_pos ? slash_pos+1 : full_path);
        state.recent_files[i].full_path = full_path;
        state.recent_files[i].filename  = filename;
        i++;
    }
    fclose(f);
}

/* Save current preferences to a file in the user directory.
 */
void save_preferences(void)
{
    FILE*  f;
    char*  path;
    int32  i;

#ifdef GTK2
    path = dsprintf("%s/preferences-gtk2", user_dir);
#else
    path = dsprintf("%s/preferences", user_dir);
#endif
    f = fopen(path, "w");
    free(path);
    if (!f) {
        sys_warn("can't open preferences file for writing");
        return;
    }

    fprintf(f, "last_version=%s\n",             VERSION);
    fprintf(f, "default_collection=%s\n",       config.default_collection);
    fprintf(f, "default_window_width=%d\n",     config.default_window_width);
    fprintf(f, "default_window_height=%d\n",    config.default_window_height);
    fprintf(f, "default_zoom=%d\n",             config.default_zoom);
    for (i=0; i < NUM_COMPONENTS; i++)
        fprintf(f, "default_show_%s=%d\n", component_short_names[i],
                config.default_visible_components[i]);
    fprintf(f, "default_fullscreen=%d\n",       config.default_fullscreen);
    for (i=0; i < NUM_COLORS; i++)
        fprintf(f, "color_%d=%06X\n", i, config.colors[i]);
    fprintf(f, "default_speed=%d\n",            config.default_speed);
    fprintf(f, "speed_slider_min=%d\n",         config.speed_slider_min);
    fprintf(f, "speed_slider_max=%d\n",         config.speed_slider_max);
    fprintf(f, "speed_increment=%d\n",          config.speed_increment);
    fprintf(f, "default_skip_frames=%d\n",      config.default_skip_frames);
    fprintf(f, "help_browser=%s\n",             (config.help_browser ? config.help_browser : ""));

    if (fclose(f) != 0)
        sys_warn("close failed after writing to ~/%s/preferences", USER_DIR);
}

/* Save current keybindings to a file in the user directory.
 */
void save_keybindings(void)
{
    char*  path;

#ifdef GTK2
    path = dsprintf("%s/keybindings-gtk2", user_dir);
    gtk_accel_map_save(path);
#else
    path = dsprintf("%s/keybindings", user_dir);
    gtk_item_factory_dump_rc(path, NULL, TRUE);
#endif
    free(path);
}

/* Save the list of recently accessed pattern files
 */
void save_recent_files_list(void)
{
    FILE*  f;
    char*  path;
    int32  i;

    path = dsprintf("%s/recent", user_dir);
    f = fopen(path, "w");
    free(path);
    if (!f) {
        sys_warn("can't open %s/recent for writing", user_dir);
        return;
    }
    for (i=0; i < MAX_RECENT_FILES && state.recent_files[i].full_path; i++)
        fprintf(f, "%s\n", state.recent_files[i].full_path);
    if (fclose(f) != 0)
        sys_warn("close failed after writing to ~/%s/recent", USER_DIR);
}

/*** Bound Functions ***/

void file_new(void)
{
    if (state.pattern_running)
        start_stop();
    free(state.pattern_path);
    state.pattern_path = NULL;
    gtk_window_set_title(GTK_WINDOW(gui.window), TITLE);
    set_command_sensitivity(CMD_FILE_REOPEN, FALSE);
    state.last_drawn = null_point;
    deactivate_selection(FALSE);
    deactivate_paste(FALSE);
    clear_world();
    update_tick_label();
    update_population_label();
    update_description_textbox(FALSE);
    sidebar_unselect();
    view_recenter();    /* will trigger a canvas redraw */
}

void file_open(void)
{
    GtkWidget*  file_selector;

    file_selector = gtk_file_selection_new("Load Pattern");
    setup_child_window(file_selector);
    gtk_file_selection_set_filename(GTK_FILE_SELECTION(file_selector), state.current_dir);
    gtk_signal_connect(GTK_OBJECT(GTK_FILE_SELECTION(file_selector)->ok_button), "clicked",
                       GTK_SIGNAL_FUNC(file_open_ok), file_selector);
    gtk_signal_connect_object(GTK_OBJECT(GTK_FILE_SELECTION(file_selector)->cancel_button),
                              "clicked", GTK_SIGNAL_FUNC(gtk_widget_destroy), GTK_OBJECT(file_selector));
    gtk_widget_show(file_selector);
}

void file_reopen(void)
{
    attempt_load_pattern(state.pattern_path);
}

void file_save(void)
{
    if (state.pattern_path)
        attempt_save_pattern(state.pattern_path, state.file_format);
    else
        file_save_as();
}

void file_save_as(void)
{
    GtkWidget*  file_selector;
    GtkWidget*  format_combo;
    GtkWidget*  alignment;
    GtkWidget*  hbox;
    GList*      format_list = NULL;
    char*       home_dir;
    int32       i;

    /* Create file selector and set default save path */
    file_selector = gtk_file_selection_new("Save Pattern");
    if (state.pattern_path)
        gtk_file_selection_set_filename(GTK_FILE_SELECTION(file_selector), state.pattern_path);
    else {
        home_dir = append_trailing_slash(state.home_dir);
        gtk_file_selection_set_filename(GTK_FILE_SELECTION(file_selector), home_dir);
        free(home_dir);
    }

    /* Connect buttons */
    gtk_signal_connect(GTK_OBJECT(GTK_FILE_SELECTION(file_selector)->ok_button), "clicked",
                       GTK_SIGNAL_FUNC(file_save_as_ok), file_selector);
    gtk_signal_connect_object(GTK_OBJECT(GTK_FILE_SELECTION(file_selector)->cancel_button),
                              "clicked", GTK_SIGNAL_FUNC(gtk_widget_destroy), GTK_OBJECT(file_selector));

    /* Create and add the format-selection combo box */
    format_combo = gtk_combo_new();
    gtk_object_set_data(GTK_OBJECT(file_selector), "format", format_combo);
    for (i=0; i < NUM_FORMATS; i++)
        format_list = g_list_append(format_list, file_format_names[i]);
    gtk_combo_set_popdown_strings(GTK_COMBO(format_combo), format_list);
    g_list_free(format_list);
    hbox = gtk_hbox_new(FALSE, 5);
    gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new("File Format:"), FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(hbox), format_combo, FALSE, FALSE, 0);
    CENTER_ALIGN(hbox);
    gtk_box_pack_start(GTK_BOX(GTK_FILE_SELECTION(file_selector)->main_vbox), alignment, FALSE,
                       FALSE, 0);
    gtk_widget_show_all(alignment);

    /* Center and display the dialog */
    setup_child_window(file_selector);
    gtk_widget_show(file_selector);
}

void file_description(void)
{
    GtkWidget*  dialog;
    GtkWidget*  hbox;
    GtkWidget*  textbox;
    GtkWidget*  scrollbar;

    /* If the dialog is already active, close it */
    if (gui.description_dialog) {
        gtk_signal_handler_block_by_func(GTK_OBJECT(gui.description_dialog),
                                         GTK_SIGNAL_FUNC(file_description_destroy),
                                         gui.description_dialog);
        gtk_widget_destroy(gui.description_dialog);
    }

    /* Setup the dialog window */
    dialog = gui.description_dialog =
        create_dialog("Pattern Description", NULL, GTK_SIGNAL_FUNC(file_description_destroy), TRUE, 3,
                     "Okay", GTK_SIGNAL_FUNC(file_description_ok), "Apply",
                      GTK_SIGNAL_FUNC(file_description_apply), "Cancel", NULL);

    /* Set up the textbox with scrollbars */
    hbox = gtk_hbox_new(FALSE, 0);
    textbox = gui.description_textbox = gtk_text_new(NULL, NULL);
    gtk_text_set_editable(GTK_TEXT(textbox), TRUE);
#ifdef GTK2
    gtk_signal_connect(GTK_OBJECT(textbox), "scroll_event",
                       GTK_SIGNAL_FUNC(handle_desc_box_mouse_scroll), NULL);
#else
    gtk_signal_connect(GTK_OBJECT(textbox), "button_press_event",
                       GTK_SIGNAL_FUNC(handle_desc_box_mouse_scroll), NULL);
#endif
    gtk_box_pack_start(GTK_BOX(hbox), textbox, TRUE, TRUE, 0);
    scrollbar = gtk_vscrollbar_new(GTK_TEXT(textbox)->vadj);
    gtk_box_pack_start(GTK_BOX(hbox), scrollbar, FALSE, FALSE, 0);
    gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox), hbox);

    /* Add the text */
    update_description_textbox(TRUE);

    /* Show the dialog */
    gtk_widget_show_all(dialog);
}

void file_change_collection(void)
{
    GtkWidget*  dialog;
    GtkWidget*  vbox;
    GtkWidget*  hbox;
    GtkWidget*  entry;
    GtkWidget*  button;
    GtkWidget*  radio;
    GtkWidget*  label;
    collection_dialog_info*  info;
    boolean  found_collection = FALSE;
    char*    buf;
    int32    i;

    info = safe_malloc(sizeof(collection_dialog_info));
    dialog = create_dialog("Choose Pattern Collection", info, GTK_SIGNAL_FUNC(file_change_collection_destroy),
                           TRUE, 3, "Okay", GTK_SIGNAL_FUNC(file_change_collection_ok), "Apply",
                           GTK_SIGNAL_FUNC(file_change_collection_apply), "Cancel", NULL);
    info->dialog = dialog;

    vbox = gtk_vbox_new(FALSE, 5);
    gtk_container_set_border_width(GTK_CONTAINER(vbox), 5);
    label = gtk_label_new("Collection:");
    gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
    gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);

    for (i=0; i < NUM_COLLECTIONS; i++) {
        hbox = gtk_hbox_new(FALSE, 3);
        gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new("     "), FALSE, FALSE, 0);
        if (i == 0)
            radio = gtk_radio_button_new_with_label(NULL, pattern_collections[i].title);
        else
            radio = gtk_radio_button_new_with_label_from_widget(
                GTK_RADIO_BUTTON(info->radio_buttons[0]), pattern_collections[i].title);
        if (!found_collection) {
            buf = dsprintf("%s/patterns/%s/", DATADIR, pattern_collections[i].dir);
            if (STR_EQUAL(state.current_collection, buf)) {
                found_collection = TRUE;
                gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
            }
            free(buf);
        }
        gtk_box_pack_start(GTK_BOX(hbox), radio, FALSE, FALSE, 0);
        gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
        info->radio_buttons[i] = radio;
    }

    hbox = gtk_hbox_new(FALSE, 3);
    gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new("     "), FALSE, FALSE, 0);
    radio = gtk_radio_button_new_with_label_from_widget(
        GTK_RADIO_BUTTON(info->radio_buttons[0]), "Custom:");
    if (!found_collection)
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
    gtk_signal_connect(GTK_OBJECT(radio), "toggled",
                       GTK_SIGNAL_FUNC(file_change_coll_toggle_custom), info);
    gtk_box_pack_start(GTK_BOX(hbox), radio, FALSE, FALSE, 0);
    info->radio_buttons[i] = radio;
    entry = gtk_entry_new();
    gtk_widget_set_usize(entry, 250, 0);
    if (found_collection)
        gtk_widget_set_sensitive(entry, FALSE);
    else
        gtk_entry_set_text(GTK_ENTRY(entry), state.current_collection);
    gtk_signal_connect(GTK_OBJECT(entry), "activate",
                       GTK_SIGNAL_FUNC(file_change_collection_enter), info);
    gtk_box_pack_start(GTK_BOX(hbox), entry, FALSE, FALSE, 0);
    info->custom_path_entry = entry;
    button = gtk_button_new_with_label("....");
    if (found_collection)
        gtk_widget_set_sensitive(button, FALSE);
    gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(file_change_coll_select_custom), info);
    gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
    info->custom_path_button = button;
    gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);

    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), vbox, TRUE, TRUE, 0);
    gtk_widget_show_all(dialog);
}

void file_recent(GtkMenuItem* menu_item, gpointer user_data)
{
    int32  num;

    num = *((int32*)user_data);
    if (state.recent_files[num].full_path) {
        if (attempt_load_pattern(state.recent_files[num].full_path))
            sidebar_unselect();
    }
}

void file_quit(void)
{
    save_preferences();
    save_keybindings();
    save_recent_files_list();
    exit(0);
}

void view_zoom_in(void)
{
    int32  new_zoom;

    if (dstate.zoom < MAX_ZOOM) {
        new_zoom = dstate.zoom * 2;
        gtk_check_menu_item_set_active(
            GTK_CHECK_MENU_ITEM(gui.command_widgets[ZOOM_CMD(new_zoom)].menu_item), TRUE);
    }
}

void view_zoom_out(void)
{
    int32  new_zoom;

    if (dstate.zoom > MIN_ZOOM) {
        new_zoom = dstate.zoom / 2;
        gtk_check_menu_item_set_active(
            GTK_CHECK_MENU_ITEM(gui.command_widgets[ZOOM_CMD(new_zoom)].menu_item), TRUE);
    }
}

void view_zoom_1(void)
{
    view_zoom(1);
}

void view_zoom_2(void)
{
    view_zoom(2);
}

void view_zoom_4(void)
{
    view_zoom(4);
}

void view_zoom_8(void)
{
    view_zoom(8);
}

void view_zoom_16(void)
{
    view_zoom(16);
}

void view_scroll_left(void)
{
    view_scroll_generic(-1, 0, SCROLL_STEP);
}

void view_scroll_right(void)
{
    view_scroll_generic(1, 0, SCROLL_STEP);
}

void view_scroll_up(void)
{
    view_scroll_generic(0, -1, SCROLL_STEP);
}

void view_scroll_down(void)
{
    view_scroll_generic(0, 1, SCROLL_STEP);
}

void view_scroll_nw(void)
{
    view_scroll_generic(-1, -1, SCROLL_STEP);
}

void view_scroll_ne(void)
{
    view_scroll_generic(1, -1, SCROLL_STEP);
}

void view_scroll_sw(void)
{
    view_scroll_generic(-1, 1, SCROLL_STEP);
}

void view_scroll_se(void)
{
    view_scroll_generic(1, 1, SCROLL_STEP);
}

void view_scroll_page_left(void)
{
    view_scroll_generic(-1, 0, SCROLL_PAGE);
}

void view_scroll_page_right(void)
{
    view_scroll_generic(1, 0, SCROLL_PAGE);
}

void view_scroll_page_up(void)
{
    view_scroll_generic(0, -1, SCROLL_PAGE);
}

void view_scroll_page_down(void)
{
    view_scroll_generic(0, 1, SCROLL_PAGE);
}

void view_scroll_page_nw(void)
{
    view_scroll_generic(-1, -1, SCROLL_PAGE);
}

void view_scroll_page_ne(void)
{
    view_scroll_generic(1, -1, SCROLL_PAGE);
}

void view_scroll_page_sw(void)
{
    view_scroll_generic(-1, 1, SCROLL_PAGE);
}

void view_scroll_page_se(void)
{
    view_scroll_generic(1, 1, SCROLL_PAGE);
}

void view_recenter(void)
{
    point  pt;

    pt.x = pt.y = WORLD_SIZE/2;
    set_viewport_position(&pt, TRUE);
    adjust_scrollbar_values();
    full_canvas_redraw();
}

void view_goto(void)
{
    GtkWidget*  dialog;
    GtkWidget*  hbox;
    GtkWidget*  alignment;
    GtkWidget*  x_entry;
    GtkWidget*  y_entry;
    goto_dialog_info*  info;

    /* Setup the dialog window */
    info = safe_malloc(sizeof(goto_dialog_info));
    dialog = info->dialog =
        create_dialog("Set Viewport", info, GTK_SIGNAL_FUNC(view_goto_destroy), TRUE, 3,
                      "Okay", GTK_SIGNAL_FUNC(view_goto_ok), "Apply", GTK_SIGNAL_FUNC(view_goto_apply),
                      "Cancel", NULL);

    /* Setup the X and Y text entries */
    hbox = gtk_hbox_new(FALSE, 0);
    gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new("X = "), FALSE, FALSE, 0);
    x_entry = info->x_entry = gtk_entry_new_with_max_length(11);
    gtk_widget_set_usize(x_entry, 75, 0);
    gtk_signal_connect(GTK_OBJECT(x_entry), "activate", GTK_SIGNAL_FUNC(view_goto_enter), info);
    gtk_box_pack_start(GTK_BOX(hbox), x_entry, FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new(" , Y = "), FALSE, FALSE, 0);
    y_entry = info->y_entry = gtk_entry_new_with_max_length(11);
    gtk_widget_set_usize(y_entry, 75, 0);
    gtk_signal_connect(GTK_OBJECT(y_entry), "activate", GTK_SIGNAL_FUNC(view_goto_enter), info);
    gtk_box_pack_start(GTK_BOX(hbox), y_entry, FALSE, FALSE, 0);
    CENTER_ALIGN(hbox);
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), alignment, TRUE, TRUE, 0);

    /* Setup the radio buttons */
    hbox = gtk_hbox_new(FALSE, 10);
    info->ul_corner_radio = gtk_radio_button_new_with_label(NULL, "Upper-left corner");
    gtk_box_pack_start(GTK_BOX(hbox), info->ul_corner_radio, FALSE, FALSE, 0);
    info->center_radio =
        gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(info->ul_corner_radio),
                                                    "Center");
    gtk_box_pack_start(GTK_BOX(hbox), info->center_radio, FALSE, FALSE, 0);
    CENTER_ALIGN(hbox);
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), alignment, TRUE, TRUE, 0);

    /* Put keyboard focus on the x entry */
    gtk_widget_grab_focus(x_entry);

    /* Show the dialog */
    gtk_widget_show_all(dialog);
}

void view_find_active_cells(void)
{
    point  pt;

    if (!find_active_cell(&pt.x, &pt.y)) {
        error_dialog("The grid is empty!");
        return;
    }
    set_viewport_position(&pt, TRUE);
    adjust_scrollbar_values();
}

void view_show_toolbar(void)
{
    view_show(COMPONENT_TOOLBAR);
}

void view_show_sidebar(void)
{
    view_show(COMPONENT_SIDEBAR);
}

void view_show_scrollbars(void)
{
    view_show(COMPONENT_SCROLLBARS);
}

void view_show_statusbar(void)
{
    view_show(COMPONENT_STATUSBAR);
}

void view_fullscreen(void)
{
    if (GTK_CHECK_MENU_ITEM(gui.command_widgets[CMD_VIEW_FULLSCREEN].menu_item)->active ==
        dstate.fullscreen)
        return;

    dstate.fullscreen = !dstate.fullscreen;
    if (dstate.fullscreen) {
        gtk_widget_hide_all(gui.toolbar);
        gtk_widget_hide_all(gui.sidebar);
        gtk_widget_hide(gui.vscrollbar);
        gtk_widget_hide(gui.hscrollbar);
    } else {
        if (dstate.visible_components[COMPONENT_TOOLBAR])
            gtk_widget_show_all(gui.toolbar);
        if (dstate.visible_components[COMPONENT_SIDEBAR]) {
            gtk_widget_show_all(gui.sidebar);
            if (!dstate.sub_sidebar_visible)
                gtk_widget_hide(gui.sub_sidebar);
        } if (dstate.visible_components[COMPONENT_SCROLLBARS]) {
            gtk_widget_show(gui.vscrollbar);
            gtk_widget_show(gui.hscrollbar);
        }
    }
    ewmh_toggle_fullscreen(gui.window);
}

void edit_drawing_tool(void)
{
    state.select_mode = FALSE;
    gtk_widget_set_sensitive(gui.command_widgets[CMD_EDIT_DRAWING_TOOL].toolbar_button, FALSE);
    gtk_widget_set_sensitive(gui.command_widgets[CMD_EDIT_SELECTION_TOOL].toolbar_button, TRUE);
    set_cursor(GDK_PENCIL);
}

void edit_selection_tool(void)
{
    state.select_mode = TRUE;
    gtk_widget_set_sensitive(gui.command_widgets[CMD_EDIT_DRAWING_TOOL].toolbar_button, TRUE);
    gtk_widget_set_sensitive(gui.command_widgets[CMD_EDIT_SELECTION_TOOL].toolbar_button, FALSE);
    set_cursor(GDK_CROSSHAIR);
}

void edit_cut(void)
{
    selection_copy_clear(TRUE, TRUE);
    deactivate_selection(FALSE);
    full_canvas_redraw();
    update_population_label();
}

void edit_copy(void)
{
    selection_copy_clear(TRUE, FALSE);
    deactivate_selection(TRUE);
}

void edit_clear(void)
{
    selection_copy_clear(FALSE, TRUE);
    deactivate_selection(FALSE);
    full_canvas_redraw();
    update_population_label();
}

void edit_paste(void)
{
    point  pt;
    rect   r;

    if (state.tracking_mouse)
        return;

    if (SELECTION_ACTIVE())
        selection_copy_clear(TRUE, FALSE);
    state.tracking_mouse = PASTING;
    set_command_sensitivity(CMD_EDIT_CANCEL_PASTE, TRUE);
    set_status_message((state.moving ? MOVING_MESSAGE : PASTING_MESSAGE), FALSE);
    gtk_widget_get_pointer(gui.canvas, &pt.x, &pt.y);
    get_logical_coords(&pt, &r.start);
    r.end.x = r.start.x + (state.copy_rect.end.x - state.copy_rect.start.x);
    r.end.y = r.start.y + (state.copy_rect.end.y - state.copy_rect.start.y);
    if (r.end.x < WORLD_SIZE && r.end.y < WORLD_SIZE)
        screen_box_update(&state.paste_box, &r, TRUE);
}

void edit_move(void)
{
    if (state.tracking_mouse)
        return;

    state.moving = TRUE;
    edit_paste();
}

void edit_cancel_paste(void)
{
    deactivate_paste(TRUE);
}

void edit_preferences(void)
{
    GtkWidget*  dialog;
    GtkWidget*  tabbed_frame;
    GtkWidget*  table;
    GtkWidget*  vbox;
    GtkWidget*  vbox2;
    GtkWidget*  hbox;
    GtkWidget*  frame;
    GtkWidget*  alignment;
    GtkWidget*  button;
    GtkWidget*  toggle;
    GtkWidget*  radio;
    GtkWidget*  entry;
    GtkWidget*  label;
    prefs_dialog_info*  info;
    color_dialog_info*  color_info;
    GList*      zoom_list = NULL;
    GList*      pos;
    char*       title_text;
    char*       label_text;
    char*       default_zoom_str;
    char*       buf;
    boolean     found_collection = FALSE;
    uint32      default_value;
    int32       i;

    info = safe_malloc(sizeof(prefs_dialog_info));

    /* Setup the dialog window */
    dialog = info->dialog =
        create_dialog(TITLE " Preferences", info, GTK_SIGNAL_FUNC(edit_preferences_destroy), TRUE, 3,
                     "Okay", GTK_SIGNAL_FUNC(edit_preferences_ok), "Apply",
                      GTK_SIGNAL_FUNC(edit_preferences_apply), "Cancel", NULL);
    gtk_container_set_border_width(GTK_CONTAINER(dialog), 10);
    tabbed_frame = gtk_notebook_new();

    /*** Setup "File" preferences ***/

    frame = gtk_frame_new("Collections");
    gtk_container_set_border_width(GTK_CONTAINER(frame), 5);

    /* Default pattern collection */

    vbox = gtk_vbox_new(FALSE, 5);
    gtk_container_set_border_width(GTK_CONTAINER(vbox), 3);
    label = gtk_label_new("Default pattern collection:");
    gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
    gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);

    for (i=0; i < NUM_COLLECTIONS; i++) {
        hbox = gtk_hbox_new(FALSE, 3);
        gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new("     "), FALSE, FALSE, 0);
        if (i == 0)
            radio = gtk_radio_button_new_with_label(NULL, pattern_collections[i].title);
        else
            radio = gtk_radio_button_new_with_label_from_widget(
                GTK_RADIO_BUTTON(info->collection_radio_buttons[0]), pattern_collections[i].title);
        if (!found_collection) {
            buf = dsprintf("%s/patterns/%s/", DATADIR, pattern_collections[i].dir);
            if (STR_EQUAL(config.default_collection, buf)) {
                found_collection = TRUE;
                gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
            }
            free(buf);
        }
        gtk_box_pack_start(GTK_BOX(hbox), radio, FALSE, FALSE, 0);
        gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
        info->collection_radio_buttons[i] = radio;
    }

    hbox = gtk_hbox_new(FALSE, 3);
    gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new("     "), FALSE, FALSE, 0);
    radio = gtk_radio_button_new_with_label_from_widget(
        GTK_RADIO_BUTTON(info->collection_radio_buttons[0]), "Custom:");
    if (!found_collection)
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
    gtk_signal_connect(GTK_OBJECT(radio), "toggled",
                       GTK_SIGNAL_FUNC(prefs_toggle_custom_collection), info);
    gtk_box_pack_start(GTK_BOX(hbox), radio, FALSE, FALSE, 0);
    info->collection_radio_buttons[i] = radio;
    entry = gtk_entry_new();
    gtk_widget_set_usize(entry, 250, 0);
    if (found_collection)
        gtk_widget_set_sensitive(entry, FALSE);
    else
        gtk_entry_set_text(GTK_ENTRY(entry), config.default_collection);
    gtk_box_pack_start(GTK_BOX(hbox), entry, FALSE, FALSE, 0);
    info->custom_collection_path_entry = entry;
    button = gtk_button_new_with_label("....");
    if (found_collection)
        gtk_widget_set_sensitive(button, FALSE);
    gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(prefs_select_custom_collection), info);
    gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
    info->custom_collection_path_button = button;
    gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);

    /* Add "File" prefs to the tabbed frame */
    gtk_container_add(GTK_CONTAINER(frame), vbox);
    FRAME_ALIGN(frame);
    gtk_notebook_append_page(GTK_NOTEBOOK(tabbed_frame), alignment, gtk_label_new("File"));

    /*** Setup "View" preferences ***/

    vbox = gtk_vbox_new(FALSE, 0);

    /** "General" View Prefs **/

    frame = gtk_frame_new("General");
    gtk_container_set_border_width(GTK_CONTAINER(frame), 5);
    vbox2 = gtk_vbox_new(FALSE, 10);
    gtk_container_set_border_width(GTK_CONTAINER(vbox2), 3);
    table = gtk_table_new(2 + NUM_COMPONENTS, 2, FALSE);
    gtk_table_set_row_spacings(GTK_TABLE(table), 5);
    gtk_table_set_col_spacings(GTK_TABLE(table), 10);

    /* Initial window size */
    LEFT_ALIGN(gtk_label_new("Initial window size:"));
    gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, 0, 1, TABLE_OPTS, TABLE_OPTS, 0, 0);
    hbox = gtk_hbox_new(FALSE, 0);
    info->default_window_width_spinbox = create_spinbox(config.default_window_width, 0, 10000);
    gtk_box_pack_start(GTK_BOX(hbox), info->default_window_width_spinbox, FALSE, FALSE, 0);
    label = gtk_label_new("X");
    gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 5);
    info->default_window_height_spinbox = create_spinbox(config.default_window_height, 0, 10000);
    gtk_box_pack_start(GTK_BOX(hbox), info->default_window_height_spinbox, FALSE, FALSE, 0);
    LEFT_ALIGN(hbox);
    gtk_table_attach(GTK_TABLE(table), alignment, 1, 2, 0, 1, TABLE_OPTS, TABLE_OPTS, 0, 0);

    /* Default zoom level */
    LEFT_ALIGN(gtk_label_new("Default zoom level:"));
    gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, 1, 2, TABLE_OPTS, TABLE_OPTS, 0, 0);
    info->default_zoom_combo = gtk_combo_new();
    gtk_widget_set_usize(info->default_zoom_combo, 75, 0);
    default_zoom_str = "1:1";
    for (i=MIN_ZOOM; i <= MAX_ZOOM; i*=2) {
        buf = dsprintf("%d:1", i);
        zoom_list = g_list_append(zoom_list, buf);
        if (i == config.default_zoom)
            default_zoom_str = buf;
    }
    gtk_combo_set_popdown_strings(GTK_COMBO(info->default_zoom_combo), zoom_list);
    gtk_entry_set_text(GTK_ENTRY(GTK_COMBO(info->default_zoom_combo)->entry), default_zoom_str);
    for (pos=zoom_list; pos; pos=g_list_next(pos))
        free(pos->data);
    g_list_free(zoom_list);
    LEFT_ALIGN(info->default_zoom_combo);
    gtk_table_attach(GTK_TABLE(table), alignment, 1, 2, 1, 2, TABLE_OPTS, TABLE_OPTS, 0, 0);

    /* Visible component toggles */
    LEFT_ALIGN(gtk_label_new("Show by default:"));
    gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, 2, 3, TABLE_OPTS, TABLE_OPTS, 0, 0);
    for (i=0; i < NUM_COMPONENTS; i++) {
        hbox = gtk_hbox_new(FALSE, 0);
        toggle = info->visible_component_toggles[i] = gtk_check_button_new();
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle),
                                     config.default_visible_components[i]);
        gtk_box_pack_start(GTK_BOX(hbox), toggle, FALSE, FALSE, 0);
        label = gtk_label_new(component_full_names[i]);
        gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 3);
        LEFT_ALIGN(hbox);
        gtk_table_attach(GTK_TABLE(table), alignment, 1, 2, 2+i, 3+i, TABLE_OPTS, TABLE_OPTS,
                         0, 0);
    }

    gtk_box_pack_start(GTK_BOX(vbox2), table, FALSE, FALSE, 0);

    /* Default fullscreen */
    hbox = gtk_hbox_new(FALSE, 0);
    info->default_fullscreen_toggle = gtk_check_button_new();
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(info->default_fullscreen_toggle),
                                 config.default_fullscreen);
    gtk_box_pack_start(GTK_BOX(hbox), info->default_fullscreen_toggle, FALSE, FALSE, 0);
    label = gtk_label_new("Start in fullscreen mode");
    gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 3);
    LEFT_ALIGN(hbox);
    gtk_box_pack_start(GTK_BOX(vbox2), alignment, FALSE, FALSE, 0);

    /* Add "General" view prefs */
    gtk_container_add(GTK_CONTAINER(frame), vbox2);
    FRAME_ALIGN(frame);
    gtk_box_pack_start(GTK_BOX(vbox), alignment, FALSE, FALSE, 3);

    /** "Colors" View Prefs **/

    frame = gtk_frame_new("Colors");
    gtk_container_set_border_width(GTK_CONTAINER(frame), 5);
    table = gtk_table_new(NUM_COLORS, 4, FALSE);
    gtk_container_set_border_width(GTK_CONTAINER(table), 3);
    gtk_table_set_row_spacings(GTK_TABLE(table), 5);
    gtk_table_set_col_spacings(GTK_TABLE(table), 10);

    /* Loop through colors */
    for (i=0; i < NUM_COLORS; i++) {
        if (i == BG_COLOR_INDEX) {
            title_text    = "Choose Background Color";
            default_value = DEFAULT_BG_COLOR;
        } else if (i == CELL_COLOR_INDEX) {
            title_text  = "Choose Cell Color";
            default_value = DEFAULT_CELL_COLOR;
        } else if (i == GRID_COLOR_INDEX) {
            title_text  = "Choose Grid Color";
            default_value = DEFAULT_GRID_COLOR;
        } else {
            title_text  = "Choose Selection Color";
            default_value = DEFAULT_SELECT_COLOR;
        }
        color_info = info->color_dialog_infos[i] = safe_malloc(sizeof(color_dialog_info));

        label_text = dsprintf("%s:", color_names[i]);
        label_text[0] = toupper(label_text[0]);
        LEFT_ALIGN(gtk_label_new(label_text));
        free(label_text);
        gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, i, i+1, TABLE_OPTS, TABLE_OPTS, 0, 0);
        info->color_entries[i] = gtk_entry_new_with_max_length(7);
        gtk_widget_set_usize(info->color_entries[i], 75, 0);
        buf = dsprintf("#%06X", config.colors[i]);
        gtk_entry_set_text(GTK_ENTRY(info->color_entries[i]), buf);
        free(buf);
        LEFT_ALIGN(info->color_entries[i]);
        gtk_table_attach(GTK_TABLE(table), alignment, 1, 2, i, i+1, TABLE_OPTS, TABLE_OPTS, 0, 0);
        color_info->dialog_title  = title_text;
        color_info->prefs_entry   = info->color_entries[i];
        color_info->cur_value     = config.colors[i];
        color_info->default_value = default_value;
        button = gtk_button_new_with_label(" Select ");
        gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(prefs_select_color), color_info);
        LEFT_ALIGN(button);
        gtk_table_attach(GTK_TABLE(table), alignment, 2, 3, i, i+1, TABLE_OPTS, TABLE_OPTS, 0, 0);

        button = gtk_button_new_with_label(" Default ");
        gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(prefs_default_color), color_info);
        LEFT_ALIGN(button);
        gtk_table_attach(GTK_TABLE(table), alignment, 3, 4, i, i+1, TABLE_OPTS, TABLE_OPTS, 0, 0);
    }

    /* Add "Colors" view prefs */
    gtk_container_add(GTK_CONTAINER(frame), table);
    FRAME_ALIGN(frame);
    gtk_box_pack_start(GTK_BOX(vbox), alignment, FALSE, FALSE, 3);

    /* Add "View" prefs to the tabbed frame */
    gtk_notebook_append_page(GTK_NOTEBOOK(tabbed_frame), vbox, gtk_label_new("View"));

    /*** Setup "Run" preferences ***/

    frame = gtk_frame_new("Speed");
    gtk_container_set_border_width(GTK_CONTAINER(frame), 5);
    table = gtk_table_new(4, 2, FALSE);
    gtk_container_set_border_width(GTK_CONTAINER(table), 3);
    gtk_table_set_row_spacings(GTK_TABLE(table), 5);
    gtk_table_set_col_spacings(GTK_TABLE(table), 10);

    /* Default speed */
    LEFT_ALIGN(gtk_label_new("Default speed (Gen/s):"));
    gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, 0, 1, TABLE_OPTS, TABLE_OPTS, 0, 0);
    info->default_speed_spinbox = create_spinbox(config.default_speed, 1, MAX_SPEED);
    LEFT_ALIGN(info->default_speed_spinbox);
    gtk_table_attach(GTK_TABLE(table), alignment, 1, 2, 0, 1, TABLE_OPTS, TABLE_OPTS, 0, 0);

    /* Speed slider range */
    LEFT_ALIGN(gtk_label_new("Speed slider range:"));
    gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, 1, 2, TABLE_OPTS, TABLE_OPTS, 0, 0);
    hbox = gtk_hbox_new(FALSE, 0);
    info->speed_slider_min_spinbox = create_spinbox(config.speed_slider_min, 1, MAX_SPEED);
    gtk_box_pack_start(GTK_BOX(hbox), info->speed_slider_min_spinbox, FALSE, FALSE, 0);
    label = gtk_label_new("-");
    gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 5);
    info->speed_slider_max_spinbox = create_spinbox(config.speed_slider_max, 1, MAX_SPEED);
    gtk_box_pack_start(GTK_BOX(hbox), info->speed_slider_max_spinbox, FALSE, FALSE, 3);
    LEFT_ALIGN(hbox);
    gtk_table_attach(GTK_TABLE(table), alignment, 1, 2, 1, 2, TABLE_OPTS, TABLE_OPTS, 0, 0);

    /* Speed increment */
    LEFT_ALIGN(gtk_label_new("Speed increment:"));
    gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, 2, 3, TABLE_OPTS, TABLE_OPTS, 0, 0);
    info->speed_increment_spinbox = create_spinbox(config.speed_increment, 1, 10000);
    LEFT_ALIGN(info->speed_increment_spinbox);
    gtk_table_attach(GTK_TABLE(table), alignment, 1, 2, 2, 3, TABLE_OPTS, TABLE_OPTS, 0, 0);

    /* Default skip frames */
    hbox = gtk_hbox_new(FALSE, 0);
    info->default_skip_toggle = gtk_check_button_new();
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(info->default_skip_toggle),
                                 config.default_skip_frames);
    gtk_box_pack_start(GTK_BOX(hbox), info->default_skip_toggle, FALSE, FALSE, 0);
    label = gtk_label_new("Skip frames by default");
    gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 3);
    LEFT_ALIGN(hbox);
    gtk_table_attach(GTK_TABLE(table), alignment, 0, 2, 3, 4, TABLE_OPTS, TABLE_OPTS, 0, 0);

    /* Add "Run" prefs to the tabbed frame */
    gtk_container_add(GTK_CONTAINER(frame), table);
    FRAME_ALIGN(frame);
    gtk_notebook_append_page(GTK_NOTEBOOK(tabbed_frame), alignment, gtk_label_new("Run"));

    /*** Setup "Help Browser" preferences ***/
    frame = gtk_frame_new(NULL);
    gtk_container_set_border_width(GTK_CONTAINER(frame), 5);
    vbox = gtk_vbox_new(FALSE, 0);
    label = gtk_label_new("Web browser for viewing help files:");
    gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
    gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 5);
    table = create_help_browser_table(info->browser_radio_buttons, &(info->custom_browser_entry));
    gtk_signal_connect(GTK_OBJECT(info->browser_radio_buttons[NUM_HELP_BROWSERS+1]), "toggled",
                       GTK_SIGNAL_FUNC(prefs_toggle_custom_browser), info);
    gtk_box_pack_start(GTK_BOX(vbox), table, TRUE, TRUE, 5);

    /* Add "Help Browser" prefs to the tabbed frame */
    gtk_container_add(GTK_CONTAINER(frame), vbox);
    FRAME_ALIGN(frame);
    gtk_notebook_append_page(GTK_NOTEBOOK(tabbed_frame), alignment, gtk_label_new("Help Browser"));

    /* Add tabbed frame to dialog vbox */
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), tabbed_frame, FALSE, FALSE, 5);

    /* Show the dialog */
    gtk_widget_show_all(dialog);
}

void run_start_stop(void)
{
    if (state.pattern_running) {
        if (state.skipped_frames) {
            trigger_canvas_update();
            update_tick_label();
            update_population_label();
        }
    }
    start_stop();
}

void run_step(void)
{
    if (!state.pattern_running)
        tick_and_update();
}

void run_jump(void)
{
    GtkWidget*  dialog;
    GtkWidget*  hbox;
    GtkWidget*  tick_entry;

    /* Setup the dialog window */
    dialog = create_dialog("Jump", NULL, NULL, TRUE, 2, "Okay", GTK_SIGNAL_FUNC(run_jump_ok), "Cancel", NULL);
    gtk_container_set_border_width(GTK_CONTAINER(dialog), 2);

    /* Setup the tick entry */
    hbox = gtk_hbox_new(FALSE, 0);
    gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new("Jump to Tick:"), FALSE, FALSE, 5);
    tick_entry = gtk_entry_new_with_max_length(10);
    gtk_object_set_user_data(GTK_OBJECT(dialog), tick_entry);
    gtk_widget_set_usize(tick_entry, 100, 0);
    gtk_signal_connect(GTK_OBJECT(tick_entry), "activate", GTK_SIGNAL_FUNC(run_jump_enter),
                       dialog);
    gtk_box_pack_start(GTK_BOX(hbox), tick_entry, FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), hbox, FALSE, FALSE, 5);

    /* Put keyboard focus on the tick entry */
    gtk_widget_grab_focus(tick_entry);

    /* Show the dialog */
    gtk_widget_show_all(dialog);
}

void run_faster(void)
{
    set_speed(state.speed + config.speed_increment);
}

void run_slower(void)
{
    set_speed(state.speed - config.speed_increment);
}

void run_speed(void)
{
    GtkWidget*  dialog;
    GtkWidget*  hbox;
    GtkWidget*  label;
    speed_dialog_info*  info;

    info = safe_malloc(sizeof(speed_dialog_info));

    /* Setup the dialog window */
    dialog = info->dialog =
        create_dialog("Set Speed", info, GTK_SIGNAL_FUNC(run_speed_destroy), TRUE, 3,
                      "Okay", GTK_SIGNAL_FUNC(run_speed_ok), "Apply", GTK_SIGNAL_FUNC(run_speed_apply),
                      "Cancel", NULL);
    gtk_container_set_border_width(GTK_CONTAINER(dialog), 3);

    /* Setup the Gen/s speed spinbox */
    hbox = gtk_hbox_new(FALSE, 0);
    label = gtk_label_new("Speed in generations/sec:");
    gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 5);
    info->speed_spinbox = create_spinbox(state.speed, 1, MAX_SPEED);
    gtk_signal_connect(GTK_OBJECT(info->speed_spinbox), "activate",
                       GTK_SIGNAL_FUNC(run_speed_enter), info);
    gtk_box_pack_start(GTK_BOX(hbox), info->speed_spinbox, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), hbox, FALSE, FALSE, 5);

    /* Setup the "skip frames" toggle */
    hbox = gtk_hbox_new(FALSE, 0);
    info->skip_toggle = gtk_check_button_new();
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(info->skip_toggle), state.skip_frames);
    gtk_box_pack_start(GTK_BOX(hbox), info->skip_toggle, FALSE, FALSE, 3);
    label = gtk_label_new("Skip frames to achieve speed");
    gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 3);
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), hbox, FALSE, FALSE, 5);

    /* Put keyboard focus on the speed spinbox */
    gtk_editable_select_region(GTK_EDITABLE(info->speed_spinbox), 0, -1);
    gtk_widget_grab_focus(info->speed_spinbox);

    /* Show the dialog */
    gtk_widget_show_all(dialog);
}

void help_help(void)
{
    help_view_page("index.html");
}

void help_pattern_archive(void)
{
    help_view_page("patterns.html");
}

void help_glf_file_format(void)
{
    help_view_page("glf_format.html");
}

void help_about(void)
{
    GtkWidget*  dialog;
    GtkWidget*  vbox;
    GtkWidget*  hbox;
    GtkWidget*  alignment;
    GtkWidget*  pixmap;

    /* Set up the dialog window */
    dialog = create_dialog("About " TITLE, NULL, NULL, TRUE, 1, "Okay", NULL);

    /* Add the banner */
    pixmap = load_pixmap_from_xpm_file(DATADIR "/graphics/banner.xpm");
    if (pixmap)
        gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), pixmap, FALSE, FALSE, 5);

    hbox = gtk_hbox_new(FALSE, 0);

    /* Add the logo */
    pixmap = load_pixmap_from_xpm_file(DATADIR "/graphics/logo.xpm");
    if (pixmap)
        gtk_box_pack_start(GTK_BOX(hbox), pixmap, FALSE, FALSE, 5);

    /* Add the text */
    vbox = gtk_vbox_new(FALSE, 0);
    gtk_box_pack_start(GTK_BOX(vbox), gtk_label_new(TITLE " Version " VERSION), FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(vbox), gtk_label_new("Suzanne Britton"), FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(vbox), gtk_label_new("Copyright 2006"), FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(vbox), gtk_label_new("GNU General Public License"), FALSE, FALSE,
                       0);
    CENTER_ALIGN(vbox);
    gtk_box_pack_start(GTK_BOX(hbox), alignment, TRUE, TRUE, 0);

    /* Show the dialog */
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), hbox, FALSE, FALSE, 5);
    gtk_widget_show_all(dialog);
}

/*** Bound Function Helpers ***/

/* Called to generate an "are you sure" dialog when the user is about to overwrite
 * an existing file.
 */
void file_save_as_confirm_overwrite(GtkWidget* file_selector) {
    GtkWidget*  dialog;
    GtkWidget*  label;
    const char* path;
    char*       message;

    dialog = create_dialog("File Exists", file_selector, NULL, TRUE, 2,
                           "Okay", GTK_SIGNAL_FUNC(file_save_as_confirm_ok), "Cancel", NULL);
    gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
    gtk_object_set_data(GTK_OBJECT(file_selector), "confirm", dialog);

    /* Set up the confirmation question */
    path = gtk_file_selection_get_filename(GTK_FILE_SELECTION(file_selector));
    message = dsprintf("%s exists, overwrite?", path);
    label = gtk_label_new(message);
    free(message);
    gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox), label);

    /* Show the dialog */
    gtk_widget_show_all(dialog);
}

/* Set the pattern description based on the given dialog results.
 */
void file_description_perform(void)
{
    char*  desc;

    desc = gtk_editable_get_chars(GTK_EDITABLE(gui.description_textbox), 0, -1);
    set_description(desc);
    free(desc);
}

/* Set the pattern collection based on the given dialog results, returning FALSE if there were
 * validation errors, TRUE otherwise.
 */
boolean file_change_collection_perform(collection_dialog_info* info)
{
    char*    dir;
    boolean  succeeded;
    int32    i;

    for (i=0; i < NUM_COLLECTIONS; i++) {
        if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(info->radio_buttons[i])))
            break;
    }
    if (i < NUM_COLLECTIONS)
        dir = dsprintf("%s/patterns/%s", DATADIR, pattern_collections[i].dir);
    else {
        dir = safe_strdup(gtk_entry_get_text(GTK_ENTRY(info->custom_path_entry)));
        if (IS_EMPTY_STRING(dir)) {
            error_dialog("You must specify a custom patterns directory.");
            free(dir);
            return FALSE;
        }
    }

    succeeded = validate_and_set_collection_dir(dir, TRUE);
    free(dir);
    if (succeeded) {
        if (dstate.sub_sidebar_visible) {
            dstate.sub_sidebar_visible = FALSE;
            if (dstate.visible_components[COMPONENT_SIDEBAR])
                gtk_widget_hide_all(gui.sub_sidebar);
        }
        sidebar_unselect();
        force_sidebar_resize();
    }
    return succeeded;
}

/* Change to the given zoom level. */
void view_zoom(int32 new_zoom)
{
    point  center;

    if (new_zoom == dstate.zoom)
        return;

    /* Set the new zoom level */
    dstate.zoom = new_zoom;

    /* Determine the original viewport center point */
    center.x = dstate.viewport.start.x + dstate.viewport_size.width/2;
    center.y = dstate.viewport.start.y + dstate.viewport_size.height/2;

    /* Record new viewport dimensions at the current zoom level */
    dstate.viewport_size.width  = dstate.viewport_sizes[new_zoom].width;
    dstate.viewport_size.height = dstate.viewport_sizes[new_zoom].height;

    /* Determine the effective canvas size for the new zoom level */
    set_effective_canvas_size();

    /* Set new viewport start to center around the same point as before */
    set_viewport_position(&center, TRUE);

    /* Adjust scrollbars for the new viewport size and position */
    adjust_scrollbars();

    /* Draw the pixmap and generate a canvas expose */
    full_canvas_redraw();

    /* Set whether "zoom in" and "zoom out" menu items are grayed */
    set_zoom_sensitivities();
}

/* Toggle display of the given main window component. */
void view_show(component_type component)
{
    GtkWidget*  widget1;
    GtkWidget*  widget2;
    boolean     toggle_cmd;
    boolean     is_visible;

    toggle_cmd = component_toggle_commands[component];
    is_visible = GTK_CHECK_MENU_ITEM(gui.command_widgets[toggle_cmd].menu_item)->active;
    if (is_visible == dstate.visible_components[component])
        return;

    dstate.visible_components[component] = is_visible;

    get_component_widgets(component, &widget1, &widget2);
    if (is_visible) {
        gtk_widget_show_all(widget1);
        if (widget2)
            gtk_widget_show_all(widget2);
        if (component == COMPONENT_SIDEBAR && !dstate.sub_sidebar_visible)
            gtk_widget_hide_all(gui.sub_sidebar);
    } else {
        gtk_widget_hide_all(widget1);
        if (widget2)
            gtk_widget_hide_all(widget2);
    }
}

/* A helper for the various view_scroll_ functions. xdir indicates how to scroll along the x axis (-1
 * for left, 0 for none, 1 for right), ydir indicates the y axis scrolling, and how_far indicates
 * how much to scroll (this parameter may take the special values SCROLL_STEP and SCROLL_PAGE, to
 * use the scrollbar step and page increments). This function just adjusts the scrollbar(s), and
 * lets the scrollbar event handlers take care of the rest.
 */
void view_scroll_generic(int32 xdir, int32 ydir, int32 how_far)
{
    GtkWidget*      scrollbar;
    GtkAdjustment*  sb_state;
    int32           dir;
    int32           i;

    for (i=0; i < 2; i++) {
        if (i == 0) {
            dir = xdir;
            scrollbar = gui.hscrollbar;
        } else {
            dir = ydir;
            scrollbar = gui.vscrollbar;
        }
        if (!dir)
            continue;
        sb_state = gtk_range_get_adjustment(GTK_RANGE(scrollbar));
        if (how_far == SCROLL_STEP)
            how_far = sb_state->step_increment;
        else if (how_far == SCROLL_PAGE)
            how_far = sb_state->page_increment;
        gtk_adjustment_set_value(sb_state, ROUND(sb_state->value) + dir * how_far);
    }
}

/* Called when the user chooses where they want to paste after typing "ctrl-v".
 */
void edit_paste_win_style(const point* pt)
{
    if (!edit_paste_verify_target(pt))
        return;
    if (state.moving)
        selection_copy_clear(FALSE, TRUE);
    edit_paste_perform(pt);
    deactivate_selection(FALSE);
    deactivate_paste(FALSE);
    full_canvas_redraw();
    update_population_label();
}

/* Called when the user middle-clicks on a cell to paste.
 */
void edit_paste_unix_style(const point* pt)
{
    if (state.tracking_mouse)
        return;

    if (SELECTION_ACTIVE())
        selection_copy_clear(TRUE, FALSE);
    else if (rects_identical(&state.copy_rect, &null_rect))
        return;
    if (!edit_paste_verify_target(pt))
        return;
    edit_paste_perform(pt);
    deactivate_selection(FALSE);
    full_canvas_redraw();
    update_population_label();
}

/* Verify that the current copy buffer can be pasted to the given point (i.e., it won't go off
 * the end of the world). Display an error dialog and return FALSE if not, otherwise return TRUE.
 */
boolean edit_paste_verify_target(const point* pt)
{
    if (pt->x + (state.copy_rect.end.x - state.copy_rect.start.x) < WORLD_SIZE &&
        pt->y + (state.copy_rect.end.y - state.copy_rect.start.y) < WORLD_SIZE)
        return TRUE;
    else {
        error_dialog("There's not enough room to paste there.");
        return FALSE;
    }
}

/* Paste the current copy buffer at the given location.
 */
void edit_paste_perform(const point* pt)
{
    int32       xstart, ystart;
    int32       xoff, yoff;
    cage_type*  c;

    for (c=state.copy_buffer; c; c=c->next) {
        xstart = pt->x + (c->x * CAGE_SIZE) - state.copy_rect.start.x;
        ystart = pt->y + (c->y * CAGE_SIZE) - state.copy_rect.start.y;
        for (yoff=0; yoff < 4; yoff++) {
            for (xoff=0; xoff < 4; xoff++) {
                if (c->bnw[0] & (1 << (yoff*4 + xoff)))
                    draw_cell(xstart+xoff, ystart+yoff, DRAW_SET);
            }
        }
        for (yoff=0; yoff < 4; yoff++) {
            for (xoff=0; xoff < 4; xoff++) {
                if (c->bne[0] & (1 << (yoff*4 + xoff)))
                    draw_cell(xstart+4+xoff, ystart+yoff, DRAW_SET);
            }
        }
        for (yoff=0; yoff < 4; yoff++) {
            for (xoff=0; xoff < 4; xoff++) {
                if (c->bsw[0] & (1 << (yoff*4 + xoff)))
                    draw_cell(xstart+xoff, ystart+4+yoff, DRAW_SET);
            }
        }
        for (yoff=0; yoff < 4; yoff++) {
            for (xoff=0; xoff < 4; xoff++) {
                if (c->bse[0] & (1 << (yoff*4 + xoff)))
                    draw_cell(xstart+4+xoff, ystart+4+yoff, DRAW_SET);
            }
        }
    }
}

/* Apply the given preferences. Return true if the preferences were applied successfully, false if
 * there was a validation error (in which case an error dialog will be popped up.)
 */
boolean edit_preferences_perform(prefs_dialog_info* info)
{
    static int32 powers_of_2[] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 0};
    GtkAdjustment*  slider_state;
    char*           collection_dir;
    const char*     color_str;
    const char*     command_line;
    char*           buf;
    boolean         colors_changed;
    int32           default_speed, speed_slider_min, speed_slider_max;
    int32           i;

    /* Validate "File" prefs */
    for (i=0; i < NUM_COLLECTIONS; i++) {
        if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(info->collection_radio_buttons[i])))
            break;
    }
    if (i < NUM_COLLECTIONS)
        buf = dsprintf("%s/patterns/%s", DATADIR, pattern_collections[i].dir);
    else {
        buf = safe_strdup(gtk_entry_get_text(GTK_ENTRY(info->custom_collection_path_entry)));
        if (IS_EMPTY_STRING(buf)) {
            error_dialog("You must specify a custom patterns directory.");
            free(buf);
            return FALSE;
        }
    }
    collection_dir = validate_collection_dir(buf, TRUE);
    free(buf);
    if (!collection_dir)
        return FALSE;

    /* Validate "View" prefs */
    for (i=0; i < NUM_COLORS; i++) {
        color_str = gtk_entry_get_text(GTK_ENTRY(info->color_entries[i]));
        if (color_str[0] == '#')
            color_str++;
        if (strlen(color_str) > 6 || !is_hexadecimal(color_str)) {
            error_dialog("Invalid %s color: expecting a 6-digit hexadecimal value.",
                         color_names[i]);
            return FALSE;
        }
    }

    /* Validate "Run" prefs */
    default_speed =
        gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(info->default_speed_spinbox));
    speed_slider_min =
        gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(info->speed_slider_min_spinbox));
    speed_slider_max =
        gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(info->speed_slider_max_spinbox));
    if (speed_slider_min > speed_slider_max) {
        error_dialog("Invalid speed slider range (minimum greater than maximum!)");
        return FALSE;
    }
    if (default_speed < speed_slider_min || default_speed > speed_slider_max) {
        error_dialog("The default speed must fall inside the speed slider range.");
        return FALSE;
    }

    /* Validate "Help Browser" prefs */
    if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(info->browser_radio_buttons[NUM_HELP_BROWSERS+1]))) {
        command_line = gtk_entry_get_text(GTK_ENTRY(info->custom_browser_entry));
        if (!command_line || is_blank(command_line)) {
            error_dialog("You must enter a command line for the custom help browser");
            return FALSE;
        }
    }

    /* Apply "File" preferences */
    if (!STR_EQUAL(collection_dir, config.default_collection)) {
        free(config.default_collection);
        config.default_collection = collection_dir;
    } else
        free(collection_dir);

    /* Apply General "View" preferences */
    config.default_window_width =
        gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(info->default_window_width_spinbox));
    config.default_window_height =
        gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(info->default_window_height_spinbox));
    config.default_zoom =
        atoi(gtk_entry_get_text(GTK_ENTRY(GTK_COMBO(info->default_zoom_combo)->entry)));
    if (config.default_zoom < MIN_ZOOM)
        config.default_zoom = MIN_ZOOM;
    else if (config.default_zoom > MAX_ZOOM)
        config.default_zoom = MAX_ZOOM;
    else {
        for (i=0; powers_of_2[i]; i++) {
            if (powers_of_2[i] >= config.default_zoom) {
                config.default_zoom = powers_of_2[i];
                break;
            }
        }
    }
    for (i=0; i < NUM_COMPONENTS; i++)
        config.default_visible_components[i] =
            gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(info->visible_component_toggles[i]));
    config.default_fullscreen =
        gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(info->default_fullscreen_toggle));

    /* Apply Color "View" preferences */
    for (i=0, colors_changed = FALSE; i < NUM_COLORS; i++) {
        color_str = gtk_entry_get_text(GTK_ENTRY(info->color_entries[i]));
        if (color_str[0] == '#')
            color_str++;
        if (config.colors[i] != strtoul(color_str, NULL, 16)) {
            config.colors[i] = strtoul(color_str, NULL, 16);
            colors_changed = TRUE;
        }
    }
    if (colors_changed) {
        dstate.life_pixmap_colormap = gdk_rgb_cmap_new(config.colors, NUM_COLORS);
        state.skipped_frames = 0;
        gtk_widget_queue_draw(gui.canvas);
    }

    /* Apply "Run" preferences */
    config.default_speed    = default_speed;
    config.speed_slider_min = speed_slider_min;
    config.speed_slider_max = speed_slider_max;
    config.speed_increment =
        gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(info->speed_increment_spinbox));;
    slider_state = gtk_range_get_adjustment(GTK_RANGE(gui.speed_slider));
    slider_state->lower = MIN(config.speed_slider_min, state.speed);
    slider_state->upper = MAX(config.speed_slider_max, state.speed);
    slider_state->page_increment = config.speed_increment;
    gtk_adjustment_changed(slider_state);
    set_speed_label_size(slider_state->upper);
    config.default_skip_frames = gtk_toggle_button_get_active(
        GTK_TOGGLE_BUTTON(info->default_skip_toggle));

    /* Apply "Help Browser" preferences */
    free(config.help_browser);
    config.help_browser = NULL;
    for (i=0; i < NUM_HELP_BROWSERS+1; i++) {
        if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(info->browser_radio_buttons[i+1]))) {
            config.help_browser = safe_strdup(
                i < NUM_HELP_BROWSERS ? help_browsers[i].command_line :
                                        gtk_entry_get_text(GTK_ENTRY(info->custom_browser_entry)));
            break;
        }
    }

    save_preferences();
    return TRUE;
}

/* View the given file in the configured help browser. If the user hasn't set up a browser yet,
 * have them choose one, then come back to this. */
void help_view_page(const char* filename)
{
    char*  url;
    char*  command;
    char*  old_command;
    char*  p;

    if (!config.help_browser) {
        pick_help_browser(filename);
        return;
    }

    url = dsprintf("file://%s/%s", DOCDIR, filename);
    command = dsprintf("%s &", config.help_browser);
    while ((p = strstr(command, HELP_BROWSER_PARAM)) != NULL) {
        *p = '\0';
        old_command = command;
        command = dsprintf("%s%s%s", command, url, p + strlen(HELP_BROWSER_PARAM));
        free(old_command);
    }
    system(command);
    free(command);
    free(url);
}

/* Popup a dialog which will let the user choose a help browser. After they've done so, run it
 * to view the indicated file.
 */
void pick_help_browser(const char *helpfile)
{
    GtkWidget* dialog;
    GtkWidget* table;
    GtkWidget* frame;
    pick_browser_dialog_info* info;

    info = safe_malloc(sizeof(pick_browser_dialog_info));
    info->helpfile = helpfile;

    dialog = info->dialog =
        create_dialog("Choose Help Browser", info, GTK_SIGNAL_FUNC(pick_browser_destroy), FALSE, 2,
                      "Okay", GTK_SIGNAL_FUNC(pick_browser_ok), "Cancel", NULL);
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox),
                       gtk_label_new("Please pick a help browser"),
                       TRUE, TRUE, 0);
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox),
                       gtk_label_new("(You can change this later in Preferences)"),
                       TRUE, TRUE, 0);
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), gtk_label_new("   "), TRUE, TRUE, 0);

    table = create_help_browser_table(info->radio_buttons, &(info->custom_browser_entry));
    gtk_signal_connect(GTK_OBJECT(info->radio_buttons[NUM_HELP_BROWSERS+1]), "toggled",
                       GTK_SIGNAL_FUNC(pick_browser_toggle_custom), info);
    frame = gtk_frame_new(NULL);
    gtk_container_set_border_width(GTK_CONTAINER(frame), 5);
    gtk_container_add(GTK_CONTAINER(frame), table);
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), frame, TRUE, TRUE, 0);

    gtk_widget_show_all(dialog);
}

/* Create and return a table of radio buttons allowing the user to choose a help browser, with
 * the appropriate button (if any) pre-selected. This function is shared between pick_help_browser()
 * and edit_preferences().
 */
GtkWidget* create_help_browser_table(GtkWidget* radio_buttons[], GtkWidget** custom_browser_entry)
{
    GtkWidget* table;
    GtkWidget* entry;
    GtkWidget* radio;
    GtkWidget* label;
    GtkWidget* alignment;
    boolean  found_browser = FALSE;
    char*    buf;
    int      i;

    /* Create the table */
    table = gtk_table_new(NUM_HELP_BROWSERS+1, 2, FALSE);
    gtk_container_set_border_width(GTK_CONTAINER(table), 3);
    gtk_table_set_row_spacings(GTK_TABLE(table), 5);
    gtk_table_set_col_spacings(GTK_TABLE(table), 5);

    /* Row for no help browser */
    radio = radio_buttons[0] = gtk_radio_button_new_with_label(NULL, "None");
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
    LEFT_ALIGN(radio);
    gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, 0, 1, TABLE_OPTS, TABLE_OPTS, 0, 0);
    label = gtk_label_new("");
    LEFT_ALIGN(label);
    gtk_table_attach(GTK_TABLE(table), alignment, 1, 2, 0, 1, TABLE_OPTS, TABLE_OPTS, 0, 0);

    /* One row per builtin help browser: */
    for (i=0; i < NUM_HELP_BROWSERS; i++) {
        radio = gtk_radio_button_new_with_label_from_widget(
            GTK_RADIO_BUTTON(radio_buttons[0]), help_browsers[i].name);
        if (config.help_browser && !found_browser) {
            if (STR_EQUAL(config.help_browser, help_browsers[i].command_line)) {
                found_browser = TRUE;
                gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
            }
        }
        radio_buttons[i+1] = radio;
        LEFT_ALIGN(radio);
        gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, i+1, i+2, TABLE_OPTS, TABLE_OPTS, 0, 0);
        buf = dsprintf("(%s)", help_browsers[i].command_line);
        label = gtk_label_new(buf);
        LEFT_ALIGN(label);
        gtk_table_attach(GTK_TABLE(table), alignment, 1, 2, i+1, i+2, TABLE_OPTS, TABLE_OPTS, 0, 0);
    }

    /* Extra row for custom help browser, with a text entry: */
    radio = radio_buttons[i+1] = gtk_radio_button_new_with_label_from_widget(
        GTK_RADIO_BUTTON(radio_buttons[0]), "Custom:");
    if (config.help_browser && !found_browser)
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
    LEFT_ALIGN(radio);
    gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, i+1, i+2, TABLE_OPTS, TABLE_OPTS, 0, 0);
    entry = *custom_browser_entry = gtk_entry_new();
    if (!config.help_browser || found_browser)
        gtk_widget_set_sensitive(entry, FALSE);
    else if (config.help_browser)
        gtk_entry_set_text(GTK_ENTRY(entry), config.help_browser);
    gtk_table_attach(GTK_TABLE(table), entry, 1, 2, i+1, i+2, TABLE_OPTS, TABLE_OPTS, 0, 0);

    return table;
}

/*** GUI initialization functions ***/

/* Setup the gtklife window and widgets
 */
void init_gui(void)
{
    GtkWidget*  vbox;
    GtkWidget*  hbox;
    GtkWidget*  widget1;
    GtkWidget*  widget2;
    int32       i;

    init_rgb();
    init_window();

    vbox = gtk_vbox_new(FALSE, 0);
    init_menubar(vbox);
    init_toolbar(vbox);
    init_sensitivities();
    hbox = gtk_hbox_new(FALSE, 0);
    init_sidebar(hbox);
    init_canvas(hbox);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
    init_statusbar(vbox);
    gtk_container_add(GTK_CONTAINER(gui.window), vbox);
    gtk_widget_show_all(gui.window);

    /* Hide any GUI components the user doesn't want to see */
    for (i=0; i < NUM_COMPONENTS; i++) {
        if (!dstate.visible_components[i]) {
            get_component_widgets(i, &widget1, &widget2);
            gtk_widget_hide_all(widget1);
            if (widget2)
                gtk_widget_hide_all(widget2);
        }
    }

    /* Hide the sub-patterns sidebar initially */
    if (dstate.visible_components[COMPONENT_SIDEBAR])
        gtk_widget_hide_all(gui.sub_sidebar);

    /* Hide unused recent-file entries. If there are no entries, hide the separator too. */
    for (i=0; i < MAX_RECENT_FILES; i++) {
        if (!state.recent_files[i].filename)
            gtk_widget_hide(state.recent_files[i].menu_item);
    }
    if (!state.recent_files[0].filename)
        gtk_widget_hide(gui.recent_files_separator);

    /* Put keyboard focus on the speed slider */
    gtk_widget_grab_focus(gui.speed_slider);

    /* Go to fullscreen if config says to */
    if (config.default_fullscreen)
        gtk_check_menu_item_set_active(
            GTK_CHECK_MENU_ITEM(gui.command_widgets[CMD_VIEW_FULLSCREEN].menu_item), TRUE);

    /* Setup some stuff based on the default tool (draw or select) */
    if (state.select_mode)
        edit_selection_tool();
    else
        edit_drawing_tool();
}

/* Initialize the GdkRGB subsystem for image handling. Also create the colormap we'll be using.
 */
void init_rgb(void)
{
    gdk_rgb_init();
    gtk_widget_set_default_colormap(gdk_rgb_get_cmap());
    gtk_widget_set_default_visual(gdk_rgb_get_visual());
    dstate.life_pixmap_colormap = gdk_rgb_cmap_new(config.colors, NUM_COLORS);
}

/* Initialize the main application window
 */
void init_window(void)
{
    gui.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_signal_connect(GTK_OBJECT(gui.window), "destroy",
                       GTK_SIGNAL_FUNC(handle_main_window_destroy), NULL);
    gtk_widget_set_name(GTK_WIDGET(gui.window), PROG);
    gtk_window_set_title(GTK_WINDOW(gui.window), TITLE);
    gtk_window_set_default_size(GTK_WINDOW(gui.window), config.default_window_width,
                                config.default_window_height);
    gtk_widget_realize(gui.window);
}

/* Setup the top menubar
 */
void init_menubar(GtkWidget* containing_box)
{
    GtkItemFactory*       menu_factory;
    GtkItemFactoryEntry*  entry;
    GtkAccelGroup*        accelerators;
    GtkWidget*            file_menu;
    GtkWidget*            item;
    char*                 str;
    int32                 num_items;
    int32*                num;
    int32                 i, j;

    gui.menu_tooltips = gtk_tooltips_new();

    /* Setup menu items */
    accelerators = gtk_accel_group_new();
    menu_factory = gtk_item_factory_new(GTK_TYPE_MENU_BAR, "<main>", accelerators);
    num_items = sizeof(menu_items) / sizeof(menu_items[0]);
    gtk_item_factory_create_items(menu_factory, num_items, menu_items, NULL);
    gtk_window_add_accel_group(GTK_WINDOW(gui.window), accelerators);
    gui.menubar = gtk_item_factory_get_widget(menu_factory, "<main>");

    /* Extract menu commands to gui.command_widgets[].menu_item */
    for (i=0, j=0, entry=menu_items; i < num_items; i++, entry++) {
        if (entry->callback) {
            str = remove_underscore(entry->path);
            gui.command_widgets[j++].menu_item = gtk_item_factory_get_item(menu_factory, str);
            free(str);
        }
    }

    /* Extract any other menu details that we'll need to access */
    gui.start_stop_menu_label =
        GTK_BIN(gtk_item_factory_get_item(menu_factory, "/Run/Start"))->child;

    /* Set the initial state of check and radio menu items */
    gtk_check_menu_item_set_active(
        GTK_CHECK_MENU_ITEM(gui.command_widgets[ZOOM_CMD(dstate.zoom)].menu_item), TRUE);
    for (i=0; i < NUM_COMPONENTS; i++)
        gtk_check_menu_item_set_active(
            GTK_CHECK_MENU_ITEM(gui.command_widgets[component_toggle_commands[i]].menu_item),
            dstate.visible_components[i]);

    /* Setup recent files list */
    file_menu = gtk_item_factory_get_widget(menu_factory, "/File");
    for (i=0; i < MAX_RECENT_FILES; i++) {
#ifdef GTK2
        if (state.recent_files[i].filename)
            str = dsprintf("_%d. %s", i+1, state.recent_files[i].filename);
        else
            str = dsprintf("_%d.", i+1);
        item = gtk_menu_item_new_with_mnemonic(str);
#else
        if (state.recent_files[i].filename)
            str = dsprintf("%d. %s", i+1, state.recent_files[i].filename);
        else
            str = dsprintf("%d.", i+1);
        item = gtk_menu_item_new_with_label(str);
        gtk_label_set_pattern(GTK_LABEL(GTK_BIN(item)->child), "_");
        gtk_widget_add_accelerator(item, "activate_item",
                                   gtk_menu_get_uline_accel_group(GTK_MENU(file_menu)),
                                   (i+1) + 0x30, 0, GTK_ACCEL_LOCKED);
#endif
        free(str);
        if (state.recent_files[i].full_path)
            gtk_tooltips_set_tip(gui.menu_tooltips, item, state.recent_files[i].full_path, NULL);
        state.recent_files[i].menu_item = item;
        state.recent_files[i].label     = GTK_BIN(item)->child;
        gtk_widget_add_accelerator(item, "activate", accelerators, (i+1) + 0x30,
                                   GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
        num = safe_malloc(sizeof(int32));
        *num = i;
        gtk_signal_connect(GTK_OBJECT(item), "activate", GTK_SIGNAL_FUNC(file_recent), num);
        gtk_menu_shell_insert(GTK_MENU_SHELL(file_menu), item, RECENT_FILES_INSERT_POS+i);
    }
    /* Insert separator after recent files */
    gui.recent_files_separator = gtk_menu_item_new();
    gtk_menu_shell_insert(GTK_MENU_SHELL(file_menu), gui.recent_files_separator,
                          RECENT_FILES_INSERT_POS+i);

    /* Add menubar to the window */
    gtk_box_pack_start(GTK_BOX(containing_box), gui.menubar, FALSE, FALSE, 0);
}

/* Setup the toolbar, including speed slider to the right of the buttons. Place the toolbar button
 * widgets in gui.command_widgets[].toolbar_button.
 */
void init_toolbar(GtkWidget* containing_box)
{
    GtkWidget*       toolbar;
    GtkWidget*       icon;
    GtkWidget*       toolbar_frame;
    GtkWidget*       speed_frame;
    GtkWidget*       hbox;
    toolbar_button*  button;
    int32            num_buttons;
    int32            i;

    /* Create the button bar */
#ifdef GTK2
    toolbar = gtk_toolbar_new();
    gtk_toolbar_set_orientation(GTK_TOOLBAR(toolbar), GTK_ORIENTATION_HORIZONTAL);
    gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_ICONS);
#else
    toolbar = gtk_toolbar_new(GTK_ORIENTATION_HORIZONTAL, GTK_TOOLBAR_ICONS);
    gtk_toolbar_set_space_size(GTK_TOOLBAR(toolbar), 10);
#endif

    /* Add toolbar buttons, also recording them in gui.command_widgets[].toolbar_button */
    num_buttons = sizeof(toolbar_buttons) / sizeof(toolbar_buttons[0]);
    for (i=0, button=toolbar_buttons; i < num_buttons; i++, button++) {
        if (button->cmd < 0)
            gtk_toolbar_append_space(GTK_TOOLBAR(toolbar));
        else {
            icon = load_pixmap_from_rgba_array(button->icon, ICON_WIDTH, ICON_HEIGHT,
                                               &(gui.window->style->bg[GTK_STATE_NORMAL]));
            gui.command_widgets[button->cmd].toolbar_button =
                gtk_toolbar_append_item(GTK_TOOLBAR(toolbar), NULL, button->description, NULL,
                                        icon, GTK_SIGNAL_FUNC(handle_toolbar_click),
                                        gui.command_widgets[button->cmd].menu_item);
            if (button->icon == start_icon)
                gui.start_stop_pixmap = icon;
        }
    }
    gui.start_stop_alt_pixmap = load_pixmap_from_rgba_array(stop_icon, ICON_WIDTH, ICON_HEIGHT,
                                          &(gui.window->style->bg[GTK_STATE_NORMAL]));
    gtk_widget_ref(gui.start_stop_alt_pixmap);

    /* Frame the button bar */
    toolbar_frame = gtk_frame_new(NULL);
    gtk_container_add(GTK_CONTAINER(toolbar_frame), toolbar);

    /* Create and frame the Gen/s slider */
    hbox = gtk_hbox_new(FALSE, 0);
    init_speed_box(hbox);
    speed_frame = gtk_frame_new(NULL);
    gtk_container_add(GTK_CONTAINER(speed_frame), hbox);

    /* Pack the button bar and speed control into gui.toolbar */
    gui.toolbar = gtk_hbox_new(FALSE, 0);
    gtk_box_pack_start(GTK_BOX(gui.toolbar), toolbar_frame, FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(gui.toolbar), speed_frame, TRUE, TRUE, 5);

    /* Add the toolbar box to the container */
    gtk_box_pack_start(GTK_BOX(containing_box), gui.toolbar, FALSE, FALSE, 0);
}

/* Set whether various menu items and toolbar buttons are initially enabled.
 */
void init_sensitivities(void)
{
    command_type  i;

    set_command_sensitivity(CMD_FILE_REOPEN, FALSE);
    set_zoom_sensitivities();
    for (i=CMD_EDIT_CUT; i <= CMD_EDIT_CANCEL_PASTE; i++)
        set_command_sensitivity(i, FALSE);
}

/* Setup the speed slider box
 */
void init_speed_box(GtkWidget* containing_box)
{
    GtkWidget*  label;
    GtkObject*  slider_params;
    char*       speed_str;

    /* Setup label */
    label = gtk_label_new(" Speed: ");
    gtk_box_pack_start(GTK_BOX(containing_box), label, FALSE, FALSE, 0);
    speed_str = dsprintf("%d", state.speed);
    gui.speed_label = gtk_label_new(speed_str);
    free(speed_str);
    set_speed_label_size(config.speed_slider_max);
    gtk_misc_set_alignment(GTK_MISC(gui.speed_label), 1, 0.5);
    gtk_box_pack_start(GTK_BOX(containing_box), gui.speed_label, FALSE, FALSE, 0);
    label = gtk_label_new(" Gen/s  ");
    gtk_box_pack_start(GTK_BOX(containing_box), label, FALSE, FALSE, 0);

    /* Setup slider */
    slider_params = gtk_adjustment_new(state.speed, config.speed_slider_min,
                                       config.speed_slider_max, 1, config.speed_increment, 0);
    gui.speed_slider = gtk_hscale_new(GTK_ADJUSTMENT(slider_params));
    gtk_scale_set_draw_value(GTK_SCALE(gui.speed_slider), FALSE);
    gtk_signal_connect(GTK_OBJECT(slider_params), "value_changed",
                       GTK_SIGNAL_FUNC(handle_speed_slider_change), NULL);
    gtk_box_pack_start(GTK_BOX(containing_box), gui.speed_slider, TRUE, TRUE, 0);

    /* Extra space at the end */
    label = gtk_label_new("  ");
    gtk_box_pack_start(GTK_BOX(containing_box), label, FALSE, FALSE, 0);
}

/* Setup the pattern loader sidebar and sub-patterns sidebar
 */
void init_sidebar(GtkWidget* containing_box)
{
    GtkWidget**  sb;
    GtkWidget**  clist;
    char*        list_header;
    boolean      is_main_sidebar;
    int32        i;

    gui.sidebar = gtk_vbox_new(FALSE, 0);

    for (i=0; i < 2; i++) {
        if (i == 0) {
            is_main_sidebar = TRUE;
            sb = &gui.main_sidebar;
            clist = &gui.patterns_clist;
            list_header = "Patterns";
        } else {
            is_main_sidebar = FALSE;
            sb = &gui.sub_sidebar;
            clist = &gui.sub_patterns_clist;
            list_header = "[None]";
        }

        /* Create the list, its contents and its selection signal handler */
        *clist = gtk_clist_new_with_titles(1, &list_header);
        if (is_main_sidebar)
            update_sidebar_generic(TRUE, state.current_collection);
        gtk_clist_set_selection_mode(GTK_CLIST(*clist), GTK_SELECTION_SINGLE);
        gtk_signal_connect(GTK_OBJECT(*clist), "select_row",
                           GTK_SIGNAL_FUNC(handle_sidebar_select), (void*)is_main_sidebar);

        /* For the main sidebar, set the event handler for clicking the column header */
        if (is_main_sidebar) {
            gtk_clist_column_titles_active(GTK_CLIST(*clist));
            gtk_signal_connect(GTK_OBJECT(*clist), "click-column",
                               GTK_SIGNAL_FUNC(handle_sidebar_header_click), NULL);
        } else
            gtk_clist_column_titles_passive(GTK_CLIST(*clist));

        /* Give it scrollbars */
        *sb = gtk_scrolled_window_new(NULL, NULL);
        gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(*sb), GTK_POLICY_AUTOMATIC,
                                       GTK_POLICY_AUTOMATIC);
        gtk_container_add(GTK_CONTAINER(*sb), *clist);

        /* Pack it into the sidebar vbox */
        gtk_box_pack_start(GTK_BOX(gui.sidebar), *sb, TRUE, TRUE, 0);
    }

    gtk_box_pack_start(GTK_BOX(containing_box), gui.sidebar, FALSE, FALSE, 0);
}

/* Setup the Life pattern drawing area, including scrollbars
 */
void init_canvas(GtkWidget* containing_box)
{
    GtkWidget*  vbox;
    GtkWidget*  hbox;
    GtkObject*  scroll_params;

    vbox = gtk_vbox_new(FALSE, 0);
    hbox = gtk_hbox_new(FALSE, 0);

    /* Create the canvas */
    gui.canvas = gtk_drawing_area_new();

    /* Trap some events */
    gtk_signal_connect(GTK_OBJECT(gui.canvas), "configure_event",
                       GTK_SIGNAL_FUNC(handle_canvas_resize), NULL);
    gtk_signal_connect(GTK_OBJECT(gui.canvas), "expose_event",
                       GTK_SIGNAL_FUNC(handle_canvas_expose), NULL);
    gtk_signal_connect(GTK_OBJECT(gui.canvas), "button_press_event",
                       GTK_SIGNAL_FUNC(handle_mouse_press), NULL);
    gtk_signal_connect(GTK_OBJECT(gui.canvas), "button_release_event",
                       GTK_SIGNAL_FUNC(handle_mouse_release), NULL);
   /* these two don't work if set on the canvas, for some reason: */
    gtk_signal_connect(GTK_OBJECT(gui.window), "key_press_event",
                       GTK_SIGNAL_FUNC(handle_key_press), NULL);
    gtk_signal_connect(GTK_OBJECT(gui.window), "key_release_event",
                       GTK_SIGNAL_FUNC(handle_key_release), NULL);
#ifdef GTK2
    gtk_signal_connect(GTK_OBJECT(gui.canvas), "scroll_event",
                       GTK_SIGNAL_FUNC(handle_mouse_scroll), NULL);
#endif
    gtk_widget_add_events(gui.canvas, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
    gtk_widget_add_events(gui.window, GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK);
    gtk_box_pack_start(GTK_BOX(hbox), gui.canvas, TRUE, TRUE, 0);

    /* Create a vertical scrollbar for the canvas. A number of these settings won't get properly
     * set until handle_canvas_resize is called, so use dummy values for now.
     */
    scroll_params = gtk_adjustment_new(INIT_Y_CENTER, 0, WORLD_SIZE-1, 1, 1, 0);
    gui.vscrollbar = gtk_vscrollbar_new(GTK_ADJUSTMENT(scroll_params));
    gtk_box_pack_start(GTK_BOX(hbox), gui.vscrollbar, FALSE, FALSE, 0);

    /* add the hbox to the containing box */
    gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);

    /* Create and add a horizontal scrollbar for the canvas. */
    scroll_params = gtk_adjustment_new(INIT_X_CENTER, 0, WORLD_SIZE-1, 1, 1, 0);
    gui.hscrollbar = gtk_hscrollbar_new(GTK_ADJUSTMENT(scroll_params));
    gtk_box_pack_start(GTK_BOX(vbox), gui.hscrollbar, FALSE, FALSE, 0);

    /* Put the canvas+scrollbars into the box */
    gtk_box_pack_start(GTK_BOX(containing_box), vbox, TRUE, TRUE, 0);
}

/* Initialize the bottom statusbar, showing current cell, tick, and population.
 */
void init_statusbar(GtkWidget* containing_box)
{
    GtkWidget*  hbox;
    GtkWidget*  hbox2;
    GtkWidget*  hseparator;
    GtkWidget*  alignment;

    gui.statusbar = gtk_vbox_new(FALSE, 0);
    hseparator = gtk_hseparator_new();
    gtk_box_pack_start(GTK_BOX(gui.statusbar), hseparator, FALSE, FALSE, 0);

    hbox = gtk_hbox_new(FALSE, 0);

    hbox2 = gtk_hbox_new(FALSE, 0);
    gtk_box_pack_start(GTK_BOX(hbox2), gtk_label_new("  "), FALSE, FALSE, 0);
    gui.hover_point_label = gtk_label_new("");
    gtk_box_pack_start(GTK_BOX(hbox2), gui.hover_point_label, FALSE, FALSE, 0);
    LEFT_ALIGN(hbox2);
    gtk_box_pack_start(GTK_BOX(hbox), alignment, FALSE, FALSE, 0);

    gui.status_message_label = gtk_label_new("");
    CENTER_ALIGN(gui.status_message_label);
    gtk_box_pack_start(GTK_BOX(hbox), alignment, TRUE, TRUE, 0);

    hbox2 = gtk_hbox_new(FALSE, 0);
    init_tick_display(hbox2);
    init_population_display(hbox2);
    RIGHT_ALIGN(hbox2);
    gtk_box_pack_start(GTK_BOX(hbox), alignment, FALSE, FALSE, 0);

    gtk_box_pack_start(GTK_BOX(gui.statusbar), hbox, FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(containing_box), gui.statusbar, FALSE, FALSE, 0);
}

/* Set up the tick display
 */
void init_tick_display(GtkWidget* containing_box)
{
    gtk_box_pack_start(GTK_BOX(containing_box), gtk_label_new(" Tick:  "), FALSE, FALSE, 0);
    gui.tick_label = gtk_label_new("0");
    gtk_box_pack_start(GTK_BOX(containing_box), gui.tick_label, FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(containing_box), gtk_label_new("   "), FALSE, FALSE, 0);
}

/* Set up the population display
 */
void init_population_display(GtkWidget* containing_box)
{
    gtk_box_pack_start(GTK_BOX(containing_box), gtk_label_new("Population:  "), FALSE, FALSE, 0);
    gui.population_label = gtk_label_new("0");
    gtk_box_pack_start(GTK_BOX(containing_box), gui.population_label, FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(containing_box), gtk_label_new("  "), FALSE, FALSE, 0);
}

/*** Event Handlers ***/

/* Called when the user activates a menu item. Dispatch the command to the appropriate bound
 * function.
 */
void handle_menu_item_activate(gpointer callback_data, guint callback_action, GtkWidget *widget)
{
    command_type    cmd;
    bound_function  handler;

    cmd = callback_action;
    /* no action for deselecting a radio-button menu item: */
    if (command_info[cmd].type == ITEM_RADIO && !(GTK_CHECK_MENU_ITEM(gui.command_widgets[cmd].menu_item)->active))
        return;
    handler = command_info[cmd].handler;
    (*handler)();
}

/* Called when the user clicks a button on the toolbar. Execute the action by activating the
 * corresponding menu item.
 */
void handle_toolbar_click(GtkButton* button, gpointer user_data)
{
    gtk_widget_activate((GtkWidget*)user_data);
}

/* Called when all or some of the Life display needs to be drawn onscreen. Redraw the exposed
 * rectangle from the offscreen pixmap.
 */
gboolean handle_canvas_expose(GtkWidget *widget, GdkEventExpose *event, gpointer user_data)
{
    gdk_draw_indexed_image(gui.canvas->window,
                     gui.canvas->style->fg_gc[GTK_WIDGET_STATE(gui.canvas)],
                     event->area.x, event->area.y, event->area.width, event->area.height,
                     GDK_RGB_DITHER_NONE,
                     dstate.life_pixmap + event->area.y*dstate.canvas_size.width + event->area.x,
                     dstate.canvas_size.width, dstate.life_pixmap_colormap);
    return TRUE;
}

/* Called when the window dimensions are changed (including at startup): Set up various globals
 * dependent on window size, and recreate and redraw the canvas pixmap. Since an expose event will
 * already have been generated, we don't generate one here.
 */
gboolean handle_canvas_resize(GtkWidget* widget, GdkEventConfigure* event, gpointer user_data)
{
    point  center;
    int32  i;

    /* Get canvas dimensions (in pixels) */
    dstate.canvas_size.width  = gui.canvas->allocation.width;
    dstate.canvas_size.height = gui.canvas->allocation.height;

    /* Determine viewport dimensions (in cells) at all zoom levels for the new window size */
    for (i=MIN_ZOOM; i <= MAX_ZOOM; i *= 2) {
        if (DRAW_GRID(i)) {
            dstate.viewport_sizes[i].width  = (dstate.canvas_size.width  - 1) / (i + 1);
            dstate.viewport_sizes[i].height = (dstate.canvas_size.height - 1) / (i + 1);
        }
        else {
            dstate.viewport_sizes[i].width  = dstate.canvas_size.width  / i;
            dstate.viewport_sizes[i].height = dstate.canvas_size.height / i;
        }
    }

    /* Determine the original viewport center point. If the program has just started up, pick the
     * middle of the world.
     */
    if (!dstate.life_pixmap) {   /* program just started */
        center.x = INIT_X_CENTER;
        center.y = INIT_Y_CENTER;
    } else {
        center.x = dstate.viewport.start.x + dstate.viewport_size.width/2;
        center.y = dstate.viewport.start.y + dstate.viewport_size.height/2;
    }

    /* Record new viewport dimensions at the current zoom level */
    dstate.viewport_size.width  = dstate.viewport_sizes[dstate.zoom].width;
    dstate.viewport_size.height = dstate.viewport_sizes[dstate.zoom].height;

    /* Determine the new effective canvas size */
    set_effective_canvas_size();

    /* Set new viewport start to center around the same point as before */
    set_viewport_position(&center, TRUE);

    /* Adjust scrollbars for the new viewport size and position */
    adjust_scrollbars();

    /* Create and draw a new pixmap for the new viewport */
    if (dstate.canvas_size.width * dstate.canvas_size.height > dstate.life_pixmap_alloc_len) {
        dstate.life_pixmap_alloc_len = dstate.canvas_size.width * dstate.canvas_size.height;
        dstate.life_pixmap = safe_realloc(dstate.life_pixmap, dstate.life_pixmap_alloc_len);
    }
    draw_life_pixmap();

    return TRUE;
}

/* Called when a dialog is about to be mapped to the screen in gtk1, in order to center it on the
 * main application window.
 */
void handle_child_window_realize(GtkWidget* child, gpointer user_data)
{
    int32  x, y, w, h;
    int32  cw, ch;

    gdk_window_get_root_origin(gui.window->window, &x, &y);
    gdk_window_get_geometry(gui.window->window, NULL, NULL, &w, &h, NULL);
    cw = child->allocation.width;
    ch = child->allocation.height;

    x += w/2 - cw/2;
    y += h/2 - ch/2;
    if (x < 0)
        x = 0;
    else if (x + cw >= gdk_screen_width())
        x = gdk_screen_width() - cw - 1;
    if (y < 0)
        y = 0;
    else if (y + ch >= gdk_screen_height())
        y = gdk_screen_height() - ch - 1;

    gtk_widget_set_uposition(child, x, y);
}

/* Scroll the description text box when they move the mouse wheel.
 */
#ifdef GTK2
gboolean handle_desc_box_mouse_scroll(GtkWidget* widget, GdkEventScroll* event, gpointer user_data)
{
    GtkAdjustment*  adj;

    adj = GTK_TEXT(widget)->vadj;
    if (event->direction == GDK_SCROLL_UP)
        gtk_adjustment_set_value(adj, adj->value - adj->page_increment/2);
    else if (event->direction == GDK_SCROLL_DOWN)
        gtk_adjustment_set_value(adj,
            MIN(adj->upper - adj->page_increment, adj->value + adj->page_increment/2));
    return TRUE;
}
#else
gboolean handle_desc_box_mouse_scroll(GtkWidget* widget, GdkEventButton* event, gpointer user_data)
{
    GtkAdjustment*  adj;

    adj = GTK_TEXT(widget)->vadj;
    if (event->button == 4) {
        gtk_adjustment_set_value(adj, adj->value - adj->page_increment/2);
        return TRUE;
    } else if (event->button == 5) {
        gtk_adjustment_set_value(adj, adj->value + adj->page_increment/2);
        return TRUE;
    } else
        return FALSE;
}
#endif

/* Called when the user closes the main application window.
 */
void handle_main_window_destroy(GtkObject* object, gpointer user_data)
{
    file_quit();
}

/* Handle mouse movement over the Life canvas. This is not actually a signal handler, but is called
 * manually from the main loop.
 */
void handle_mouse_movement(void)
{
    GdkModifierType  mouse_mask;
    point  pt, lpt;
    rect   r;
    int32  scroll_increment;

    gdk_window_get_pointer(gui.canvas->window, &pt.x, &pt.y, &mouse_mask);
    if (pt.x >= 0 && pt.x < dstate.eff_canvas_size.width &&
        pt.y >= 0 && pt.y < dstate.eff_canvas_size.height)
        get_logical_coords(&pt, &lpt);
    else
        lpt = null_point;
    update_hover_point_label(&lpt);

    if (!state.tracking_mouse)
        return;

    if (state.tracking_mouse != PASTING && !(mouse_mask & GDK_BUTTON1_MASK)) {
        /* They released the mouse button while we weren't looking */
        state.tracking_mouse = NORMAL;
        state.last_drawn = null_point;
        return;
    }

    /* If they're dragging the mouse and are at or off the end of the canvas, scroll with them */
    if (state.tracking_mouse != PASTING) {
        scroll_increment = MAX_ZOOM / dstate.zoom;
        if (state.tracking_mouse == SELECTING)   /* scroll faster if selecting */
            scroll_increment *= 2;
        if (pt.x <= 0)
            view_scroll_generic(-1, 0, scroll_increment);
        else if (pt.x >= dstate.eff_canvas_size.width-1)
            view_scroll_generic(1, 0, scroll_increment);
        if (pt.y <= 0)
            view_scroll_generic(0, -1, scroll_increment);
        else if (pt.y >= dstate.eff_canvas_size.height-1)
            view_scroll_generic(0, 1, scroll_increment);
        get_logical_coords(&pt, &lpt);
    }

    if (state.tracking_mouse == DRAWING) {
        if (points_identical(&state.last_drawn, &null_point))
            user_draw(&lpt);
        else if (!points_identical(&lpt, &state.last_drawn))
            draw_from_to(&state.last_drawn, &lpt);
    } else if (state.tracking_mouse == SELECTING) {
        r.start = state.selection.start;
        r.end   = lpt;
        screen_box_update(&state.selection, &r, TRUE);
    } else {  /* pasting */
        if (points_identical(&lpt, &null_point))
            r = null_rect;
        else {
            r.start = lpt;
            r.end.x = r.start.x + (state.copy_rect.end.x - state.copy_rect.start.x);
            r.end.y = r.start.y + (state.copy_rect.end.y - state.copy_rect.start.y);
            if (r.end.x >= WORLD_SIZE || r.end.y >= WORLD_SIZE)
                r = null_rect;
        }
        screen_box_update(&state.paste_box, &r, TRUE);
    }
}

/* Handle a mouse button click on the Life canvas.
 */
gboolean handle_mouse_press(GtkWidget* widget, GdkEventButton* event, gpointer user_data)
{
    point  pt, lpt;

    pt.x = event->x;
    pt.y = event->y;
    get_logical_coords(&pt, &lpt);
    if (state.tracking_mouse == PASTING && event->button < 3)
        /* They've chosen where to paste a copied region */
        edit_paste_win_style(&lpt);
    else if (event->button == 1) {
        /* Start drawing or, if select mode is on, start selecting. If they are holding down the shift key, do the
         * opposite action. */
        boolean shift_pressed;
        shift_pressed = ((event->state & GDK_SHIFT_MASK) ? TRUE : FALSE);
        if (state.select_mode ^ shift_pressed)
            activate_selection(&lpt);
        else
            user_draw(&lpt);
    } else if (event->button == 2)
        /* They're pasting a region unix-style by clicking the middle mouse button */
        edit_paste_unix_style(&lpt);
    else if (event->button == 3) {
        /* Recenter at this point */
        set_viewport_position(&lpt, TRUE);
        adjust_scrollbar_values();
    } else if (event->button == 4) {   /* mouse wheel up */
        if (event->state & GDK_SHIFT_MASK)
            view_scroll_left();
        else if (event->state & GDK_CONTROL_MASK)
            view_zoom_out();
        else
            view_scroll_up();
    } else if (event->button == 5) {   /* mouse wheel down */
        if (event->state & GDK_SHIFT_MASK)
            view_scroll_right();
        else if (event->state & GDK_CONTROL_MASK)
            view_zoom_in();
        else
            view_scroll_down();
    }

    return TRUE;
}

/* Handle a mouse button release on the Life canvas.
 */
gboolean handle_mouse_release(GtkWidget* widget, GdkEventButton* event, gpointer user_data)
{
    if (event->button == 1 &&
        (state.tracking_mouse == DRAWING || state.tracking_mouse == SELECTING)) {
        /* If they just quickly shift-clicked the left mouse button, deactivate the current
         * selection. */
        if (state.tracking_mouse == SELECTING &&
            state.selection.start.x == state.selection.end.x &&
            state.selection.start.y == state.selection.end.y &&
            get_time_milliseconds() - state.selection_start_time < 250) {
            deactivate_selection(TRUE);
        }
        state.tracking_mouse = NORMAL;
        state.last_drawn = null_point;
    }

    return TRUE;
}

/* Handle key press on the Life canvas. Used to detect shift key presses and change cursor accordingly.
 */
gboolean handle_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
{
    if (event->keyval == GDK_Shift_L || event->keyval == GDK_Shift_R)
        set_cursor(state.select_mode ? GDK_PENCIL : GDK_CROSSHAIR);
    return FALSE;
}

/* Handle key release on the Life canvas. Used to detect shift key releases and change cursor accordingly.
 */
gboolean handle_key_release(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
{
    if (event->keyval == GDK_Shift_L || event->keyval == GDK_Shift_R)
        set_cursor(state.select_mode ? GDK_CROSSHAIR : GDK_PENCIL);
    return FALSE;
}

#ifdef GTK2
gboolean handle_mouse_scroll(GtkWidget* widget, GdkEventScroll* event, gpointer user_data)
{
    boolean shift_pressed, control_pressed;

    shift_pressed   = ((event->state & GDK_SHIFT_MASK) != 0);
    control_pressed = ((event->state & GDK_CONTROL_MASK) != 0);
    switch (event->direction) {
    case GDK_SCROLL_UP:
        if (control_pressed)
            view_zoom_in();
        else if (shift_pressed)
            view_scroll_left();
        else
            view_scroll_up();
        break;
    case GDK_SCROLL_DOWN:
        if (control_pressed)
            view_zoom_out();
        else if (shift_pressed)
            view_scroll_right();
        else
            view_scroll_down();
        break;
    case GDK_SCROLL_LEFT:
        view_scroll_left();
        break;
    case GDK_SCROLL_RIGHT:
        view_scroll_right();
        break;
    }

    return TRUE;
}
#endif

/* Called when the horizontal scrollbar is adjusted (by the user or by our program). Adjust the
 * viewport start, and do a full redraw.
 */
void handle_hscrollbar_change(GtkAdjustment* adjustment, gpointer user_data)
{
    point  pt;

    pt.x = ROUND(adjustment->value);
    pt.y = dstate.viewport.start.y;
    set_viewport_position(&pt, FALSE);
    full_canvas_redraw();
}

/* Called when the vertical scrollbar is adjusted (by the user or by our program). Adjust the
 * viewport start, and do a full redraw.
 */
void handle_vscrollbar_change(GtkAdjustment* adjustment, gpointer user_data)
{
    point  pt;

    pt.x = dstate.viewport.start.x;
    pt.y = ROUND(adjustment->value);
    set_viewport_position(&pt, FALSE);
    full_canvas_redraw();
}

/* Called when the user selects a pattern file from the sidebar or sub-sidebar. */
void handle_sidebar_select(GtkCList* clist, gint row, gint column, GdkEventButton* event,
                           gpointer user_data)
{
    boolean        is_main_sidebar;
    pattern_file*  pattern_list;
    int32          num_patterns;

    is_main_sidebar = (boolean)user_data;
    if (is_main_sidebar) {
        pattern_list = state.sidebar_files;
        num_patterns = state.num_sidebar_files;
    } else {
        pattern_list = state.sub_sidebar_files;
        num_patterns = state.num_sub_sidebar_files;
    }

    if (row >= 0 && row < num_patterns) {
        if (pattern_list[row].is_directory)
            update_sub_sidebar_contents(row);
        else {
            if (is_main_sidebar && dstate.sub_sidebar_visible) {
                /* If they selected a non-directory in the main sidebar, and the sub-sidebar
                 * is currently visible, hide it. */
                gtk_widget_hide_all(gui.sub_sidebar);
                dstate.sub_sidebar_visible = FALSE;
                force_sidebar_resize();
            }
            attempt_load_pattern(pattern_list[row].path);
        }
    }
}

/* Called when the user clicks the header on the main patterns sidebar: initiate the menu command
 * File->Change Pattern Collection.
 */
void handle_sidebar_header_click(GtkCList* clist, gint column, gpointer user_data)
{
    file_change_collection();
}

/* Called when the speed slider has been adjusted (by the user or by our program). Set the new
 * speed, update the speed label, and reset the FPS measurement if the pattern is running.
 */
void handle_speed_slider_change(GtkAdjustment* adjustment, gpointer user_data)
{
    char*  speed_str;

    state.speed = ROUND(adjustment->value);
    speed_str = dsprintf("%d", state.speed);
    gtk_label_set_text(GTK_LABEL(gui.speed_label), speed_str);
    free(speed_str);
    reset_fps_measure();
}

/* Called when the user selects a file from a file open dialog */
void file_open_ok(GtkButton* button, gpointer user_data)
{
    GtkWidget*  file_selector;
    const char* path;

    file_selector = (GtkWidget*)user_data;
    path = gtk_file_selection_get_filename(GTK_FILE_SELECTION(file_selector));
    if (attempt_load_pattern(path)) {
        sidebar_unselect();
        set_current_dir_from_file(state.pattern_path);
        gtk_widget_destroy(file_selector);
    }
}

/* Called when the user selects a file from a "save as" dialog */
void file_save_as_ok(GtkButton* button, gpointer user_data)
{
    GtkWidget*   file_selector;
    struct stat  statbuf;
    const char*  path;

    file_selector = (GtkWidget*)user_data;
    path = gtk_file_selection_get_filename(GTK_FILE_SELECTION(file_selector));
    if (stat(path, &statbuf) == 0)   /* file exists */
        file_save_as_confirm_overwrite(file_selector);
    else {
        gtk_object_set_data(GTK_OBJECT(file_selector), "confirm", NULL);
        file_save_as_confirm_ok(NULL, file_selector);
    }
}

/* Called when it's time to actually save to a file--any user confirmation is done. */
void file_save_as_confirm_ok(GtkButton* button, gpointer user_data)
{
    GtkWidget*        file_selector;
    GtkWidget*        confirm_dialog;
    GtkWidget*        format_combo;
    const char*       path;
    const char*       format_str;
    file_format_type  format;
    int32             i;

    file_selector = (GtkWidget*)user_data;
    confirm_dialog = gtk_object_get_data(GTK_OBJECT(file_selector), "confirm");
    if (confirm_dialog)
        gtk_widget_destroy(confirm_dialog);
    path = gtk_file_selection_get_filename(GTK_FILE_SELECTION(file_selector));

    format_combo = gtk_object_get_data(GTK_OBJECT(file_selector), "format");
    format_str = gtk_entry_get_text(GTK_ENTRY(GTK_COMBO(format_combo)->entry));
    for (i=0; i < NUM_FORMATS; i++) {
        if (STR_EQUAL(format_str, file_format_names[i]))
            break;
    }
    format = ((i < NUM_FORMATS) ? i : FORMAT_GLF);

    set_current_dir_from_file(path);
    if (attempt_save_pattern(path, format)) {
        sidebar_unselect();
        gtk_widget_destroy(file_selector);
    }
}

/* Called when the user clicks "Okay" from the pattern description dialog */
void file_description_ok(GtkButton* button, gpointer user_data)
{
    file_description_perform();
    gtk_widget_destroy(gui.description_dialog);
}

/* Called when the user clicks "Apply" from the pattern description dialog.
 */
void file_description_apply(GtkButton* button, gpointer user_data)
{
    file_description_perform();
}

/* Called when the description dialog is closed */
void file_description_destroy(GtkObject* object, gpointer user_data)
{
    gui.description_dialog = gui.description_textbox = NULL;
}

/* Called when the user clicks "Okay" in the change collection dialog. */
void file_change_collection_ok(GtkButton* button, gpointer user_data)
{
    collection_dialog_info* info;

    info = (collection_dialog_info*)user_data;
    if (file_change_collection_perform(info))
        gtk_widget_destroy(info->dialog);
}

/* Called when the user hits "enter" while the custom-dir entry is focused in the Change-Collection
 * dialog. Treat this like an "Okay" click.
 */
void file_change_collection_enter(GtkEditable* editable, gpointer user_data)
{
    file_change_collection_ok(NULL, user_data);
}

/* Called when the user clicks "Apply" in the change collection dialog. */
void file_change_collection_apply(GtkButton* button, gpointer user_data)
{
    file_change_collection_perform((collection_dialog_info*)user_data);
}

/* Called when the change collection dialog is completed or otherwise closed. */
void file_change_collection_destroy(GtkObject* object, gpointer user_data)
{
    free(user_data);
}

/* Called when the user toggles the "Custom" pattern collection option in the
 * File->Change-Pattern-Collection dialog.
 */
void file_change_coll_toggle_custom(GtkToggleButton* toggle, gpointer user_data)
{
    collection_dialog_info*  info;
    boolean sensitive;

    info = (collection_dialog_info*)user_data;
    sensitive = gtk_toggle_button_get_active(toggle);
    gtk_widget_set_sensitive(info->custom_path_entry,  sensitive);
    gtk_widget_set_sensitive(info->custom_path_button, sensitive);
}

/* Called when the user clicks the "..." button to select a custom patterns dir in the
 * File->Change-Pattern-Collection dialog.
 */
void file_change_coll_select_custom(GtkButton* button, gpointer user_data)
{
    GtkWidget*               file_selector;
    collection_dialog_info*  info;
    const char*              dir;

    info = (collection_dialog_info*)user_data;

    /* Setup the file selector */
    file_selector = gtk_file_selection_new("Choose Patterns Directory");
    setup_child_window(file_selector);
    gtk_window_set_modal(GTK_WINDOW(file_selector), TRUE);
    gtk_object_set_user_data(GTK_OBJECT(file_selector), info);
    dir = gtk_entry_get_text(GTK_ENTRY(info->custom_path_entry));
    if (!dir || IS_EMPTY_STRING(dir))
        dir = state.home_dir;
    dir = append_trailing_slash(dir);
    gtk_file_selection_set_filename(GTK_FILE_SELECTION(file_selector), dir);
    free((char*)dir);

    /* Link the buttons to handlers */
    gtk_signal_connect(GTK_OBJECT(GTK_FILE_SELECTION(file_selector)->ok_button), "clicked",
                       GTK_SIGNAL_FUNC(file_change_coll_select_custom_ok), file_selector);
    gtk_signal_connect_object(GTK_OBJECT(GTK_FILE_SELECTION(file_selector)->cancel_button),
                              "clicked", GTK_SIGNAL_FUNC(gtk_widget_destroy), GTK_OBJECT(file_selector));

    /* Show the dialog */
    gtk_widget_show(file_selector);
}

/* Called when the user finishes selecting a custom patterns dir for
 * File->Change-Pattern-Collection*/
void file_change_coll_select_custom_ok(GtkButton* button, gpointer user_data) {
    GtkWidget*               file_selector;
    collection_dialog_info*  info;

    file_selector = (GtkWidget*)user_data;
    info = gtk_object_get_user_data(GTK_OBJECT(file_selector));
    gtk_entry_set_text(GTK_ENTRY(info->custom_path_entry),
                       gtk_file_selection_get_filename(GTK_FILE_SELECTION(file_selector)));
    gtk_widget_destroy(file_selector);
}

/* Called when the user clicks "Okay" in the View->Goto dialog. */
void view_goto_ok(GtkButton* button, gpointer user_data)
{
    view_goto_apply(NULL, user_data);
    gtk_widget_destroy(((goto_dialog_info*)user_data)->dialog);
}

/* Called when the user hits "enter" while the X or Y text entry is focused in the View->Goto
 * dialog. Treat this like an "Okay" click.
 */
void view_goto_enter(GtkEditable* editable, gpointer user_data)
{
    view_goto_ok(NULL, user_data);
}

/* Called when the user clicks "Apply" in the View->Goto dialog. */
void view_goto_apply(GtkButton* button, gpointer user_data)
{
    goto_dialog_info*  info;
    const char*  numstr;
    point    pt;
    boolean  center_around;

    info = (goto_dialog_info*)user_data;
    numstr = gtk_entry_get_text(GTK_ENTRY(info->x_entry));
    pt.x = atoi(numstr) + WORLD_SIZE/2;
    numstr = gtk_entry_get_text(GTK_ENTRY(info->y_entry));
    pt.y = atoi(numstr) + WORLD_SIZE/2;
    center_around = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(info->center_radio));

    set_viewport_position(&pt, center_around);
    adjust_scrollbar_values();
}

/* Called when the View->Goto dialog is closed. */
void view_goto_destroy(GtkObject* object, gpointer user_data)
{
    free(user_data);
}

/* Called when the user clicks "Okay" in the preferences dialog */
void edit_preferences_ok(GtkButton* button, gpointer user_data)
{
    prefs_dialog_info* info;

    info = (prefs_dialog_info*)user_data;
    if (edit_preferences_perform(info))
        gtk_widget_destroy(info->dialog);
}

/* Called when the user clicks "Apply" in the preferences dialog. */
void edit_preferences_apply(GtkButton* button, gpointer user_data)
{
    edit_preferences_perform((prefs_dialog_info*)user_data);
}

/* Called when the preferences dialog is completed or otherwise closed. */
void edit_preferences_destroy(GtkObject* object, gpointer user_data)
{
    prefs_dialog_info* info;
    int32  i;

    info = (prefs_dialog_info*)user_data;
    for (i=0; i < NUM_COLORS; i++)
        free(info->color_dialog_infos[i]);
    free(info);
}

/* Called when the user toggles the "Custom" pattern collection option in the preferences dialog.
 */
void prefs_toggle_custom_collection(GtkToggleButton* toggle, gpointer user_data)
{
    prefs_dialog_info*  info;
    boolean sensitive;

    info = (prefs_dialog_info*)user_data;
    sensitive = gtk_toggle_button_get_active(toggle);
    gtk_widget_set_sensitive(info->custom_collection_path_entry,  sensitive);
    gtk_widget_set_sensitive(info->custom_collection_path_button, sensitive);
}

/* Called when the user clicks the "..." button to select a custom patterns dir in preferences */
void prefs_select_custom_collection(GtkButton* button, gpointer user_data)
{
    GtkWidget*          file_selector;
    prefs_dialog_info*  info;
    const char*         dir;

    info = (prefs_dialog_info*)user_data;

    /* Setup the file selector */
    file_selector = gtk_file_selection_new("Choose Patterns Directory");
    setup_child_window(file_selector);
    gtk_window_set_modal(GTK_WINDOW(file_selector), TRUE);
    gtk_object_set_user_data(GTK_OBJECT(file_selector), info);
    dir = gtk_entry_get_text(GTK_ENTRY(info->custom_collection_path_entry));
    if (!dir || IS_EMPTY_STRING(dir))
        dir = state.home_dir;
    dir = append_trailing_slash(dir);
    gtk_file_selection_set_filename(GTK_FILE_SELECTION(file_selector), dir);
    free((char*)dir);

    /* Link the buttons to handlers */
    gtk_signal_connect(GTK_OBJECT(GTK_FILE_SELECTION(file_selector)->ok_button), "clicked",
                       GTK_SIGNAL_FUNC(prefs_select_custom_collection_ok), file_selector);
    gtk_signal_connect_object(GTK_OBJECT(GTK_FILE_SELECTION(file_selector)->cancel_button),
                              "clicked", GTK_SIGNAL_FUNC(gtk_widget_destroy), GTK_OBJECT(file_selector));

    /* Show the dialog */
    gtk_widget_show(file_selector);
}

/* Called when the user finishes selecting a custom patterns dir for preferences. */
void prefs_select_custom_collection_ok(GtkButton* button, gpointer user_data)
{
    prefs_dialog_info*  info;
    GtkWidget*          file_selector;

    file_selector = (GtkWidget*)user_data;
    info = gtk_object_get_user_data(GTK_OBJECT(file_selector));
    gtk_entry_set_text(GTK_ENTRY(info->custom_collection_path_entry),
                       gtk_file_selection_get_filename(GTK_FILE_SELECTION(file_selector)));
    gtk_widget_destroy(file_selector);
}

/* Called when the user clicks a "select" button to change a color in preferences */
void prefs_select_color(GtkButton* button, gpointer user_data)
{
    color_dialog_info*  info;
    GtkWidget*          color_dialog;
    double              color_array[4];
    const char*         color_str;
    uint32              color_val;
    int32               i;

    info = (color_dialog_info*)user_data;

    /* Setup the color selector */
    color_dialog = gtk_color_selection_dialog_new(info->dialog_title);
    setup_child_window(color_dialog);
    gtk_window_set_modal(GTK_WINDOW(color_dialog), TRUE);
    gtk_object_set_user_data(GTK_OBJECT(color_dialog), info);
    color_str = gtk_entry_get_text(GTK_ENTRY(info->prefs_entry));
    if (color_str[0] == '#')
        color_str++;
    if (strlen(color_str) <= 6 && is_hexadecimal(color_str))
        color_val = strtoul(color_str, NULL, 16);
    else
        color_val = info->cur_value;
    color_array[0] = (double)(color_val >> 16)         / 255.0;
    color_array[1] = (double)((color_val >> 8) & 0xFF) / 255.0;
    color_array[2] = (double)(color_val & 0xFF)        / 255.0;
    for (i=0; i < 2; i++)
        gtk_color_selection_set_color(
            GTK_COLOR_SELECTION(GTK_COLOR_SELECTION_DIALOG(color_dialog)->colorsel), color_array);

    /* Link the buttons to handlers */
    gtk_signal_connect(GTK_OBJECT(GTK_COLOR_SELECTION_DIALOG(color_dialog)->ok_button), "clicked",
                       GTK_SIGNAL_FUNC(prefs_select_color_ok), color_dialog);
    gtk_signal_connect_object(GTK_OBJECT(GTK_COLOR_SELECTION_DIALOG(color_dialog)->cancel_button),
                              "clicked", GTK_SIGNAL_FUNC(gtk_widget_destroy), GTK_OBJECT(color_dialog));

    /* Show the dialog */
    gtk_widget_hide(GTK_COLOR_SELECTION_DIALOG(color_dialog)->help_button);
    gtk_widget_show(color_dialog);
}

/* Called when the user finishes selecting a new color in preferences */
void prefs_select_color_ok(GtkButton* button, gpointer user_data)
{
    GtkWidget*          color_dialog;
    color_dialog_info*  info;
    double              color_array[4];
    char*               color_str;

    color_dialog = (GtkWidget*)user_data;
    info = gtk_object_get_user_data(GTK_OBJECT(color_dialog));
    gtk_color_selection_get_color(
        GTK_COLOR_SELECTION(GTK_COLOR_SELECTION_DIALOG(color_dialog)->colorsel), color_array);
    info->cur_value = ((int)(color_array[0] * 255.0) << 16) |
                      ((int)(color_array[1] * 255.0) << 8)  |
                      ((int)(color_array[2] * 255.0));
    color_str = dsprintf("#%06X", info->cur_value);
    gtk_entry_set_text(GTK_ENTRY(info->prefs_entry), color_str);
    free(color_str);
    gtk_widget_destroy(color_dialog);
}

/* Called when the user clicks "default" to reset a color in preferences */
void prefs_default_color(GtkButton* button, gpointer user_data)
{
    color_dialog_info* info;
    char*              color_str;

    info = (color_dialog_info*)user_data;
    info->cur_value = info->default_value;
    color_str = dsprintf("#%06X", info->cur_value);
    gtk_entry_set_text(GTK_ENTRY(info->prefs_entry), color_str);
    free(color_str);
}

/* Called when the user toggles the "Custom" help browser option in the preferences dialog. */
void prefs_toggle_custom_browser(GtkToggleButton* toggle, gpointer user_data)
{
    prefs_dialog_info* info;
    boolean sensitive;

    info = (prefs_dialog_info*)user_data;
    sensitive = gtk_toggle_button_get_active(toggle);
    gtk_widget_set_sensitive(info->custom_browser_entry, sensitive);
}

/* Called when the user clicks "Okay" from the jump-to-tick dialog. */
void run_jump_ok(GtkButton* widget, gpointer user_data)
{
    GtkWidget*  jump_dialog;
    GtkWidget*  jumping_dialog;
    GtkWidget*  table;
    GtkWidget*  tick_entry;
    GtkWidget*  tick_label;
    GtkWidget*  pop_label;
    GtkWidget*  label;
    GtkWidget*  hbox;
    GtkWidget*  alignment;
    const char* tick_str;
    char*       str;
    uint32      target_tick;
    uint32      tick_offset;

    jump_dialog = (GtkWidget*)user_data;

    /* Determine the tick to jump to */
    tick_entry = gtk_object_get_user_data(GTK_OBJECT(jump_dialog));
    tick_str = gtk_entry_get_text(GTK_ENTRY(tick_entry));
    if (!is_numeric(tick_str)) {
        error_dialog("Please enter the tick that you want to jump to.");
        return;
    }
    target_tick = strtoul(tick_str, NULL, 10);
    if (target_tick < tick) {
        error_dialog("You can't jump backwards.");
        return;
    }
    gtk_widget_destroy(jump_dialog);

    /* Setup and display the modal progress dialog */

    jumping_dialog = create_dialog("Jumping...", NULL, GTK_SIGNAL_FUNC(run_jump_stop), FALSE, 1, "Stop", NULL);
    gtk_window_set_default_size(GTK_WINDOW(jumping_dialog), 200, 0);
    gtk_window_set_modal(GTK_WINDOW(jumping_dialog), TRUE);

    table = gtk_table_new(2, 2, FALSE);
    gtk_container_set_border_width(GTK_CONTAINER(table), 5);
    gtk_table_set_row_spacings(GTK_TABLE(table), 5);
    gtk_table_set_col_spacings(GTK_TABLE(table), 10);

    label = gtk_label_new("Tick:");
    LEFT_ALIGN(label);
    gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, 0, 1, TABLE_OPTS, TABLE_OPTS, 0, 0);
    hbox = gtk_hbox_new(FALSE, 0);
    tick_label = gtk_label_new("");
    gtk_box_pack_start(GTK_BOX(hbox), tick_label, FALSE, FALSE, 0);
    str = dsprintf(" / %u", target_tick);
    gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new(str), FALSE, FALSE, 0);
    free(str);
    LEFT_ALIGN(hbox);
    gtk_table_attach(GTK_TABLE(table), alignment, 1, 2, 0, 1, TABLE_OPTS, TABLE_OPTS, 0, 0);

    label = gtk_label_new("Population:");
    LEFT_ALIGN(label);
    gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, 1, 2, TABLE_OPTS, TABLE_OPTS, 0, 0);
    pop_label = gtk_label_new("");
    LEFT_ALIGN(pop_label);
    gtk_table_attach(GTK_TABLE(table), alignment, 1, 2, 1, 2, TABLE_OPTS, TABLE_OPTS, 0, 0);

    CENTER_ALIGN(table);
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(jumping_dialog)->vbox), alignment, TRUE, TRUE, 0);
    gtk_widget_show_all(jumping_dialog);

    state.jump_cancelled = FALSE;
    while (gtk_events_pending())
        gtk_main_iteration_do(FALSE);

    /* Perform the jump */
    for (tick_offset = 0;
         tick < target_tick && !state.jump_cancelled;
         tick_offset++) {
        if (tick_offset % 100 == 0) {
            str = dsprintf("%u", tick);
            gtk_label_set_text(GTK_LABEL(tick_label), str);
            free(str);
            str = dsprintf("%u", population);
            gtk_label_set_text(GTK_LABEL(pop_label), str);
            free(str);
        }
        next_tick(FALSE);
        gtk_main_iteration_do(FALSE);
    }

    /* Jump complete: update the screen */
    if (!state.jump_cancelled)
        gtk_widget_destroy(jumping_dialog);
    full_canvas_redraw();
    update_tick_label();
    update_population_label();
    reset_fps_measure();
}

/* Called when the user hits "enter" on the tick entry in the Run->Jump dialog. Treat this like an
 * "Okay" click.
 */
void run_jump_enter(GtkEditable* editable, gpointer user_data)
{
    run_jump_ok(NULL, user_data);
}

/* Called when the user halts a jump-in-progress by hitting the "Stop" button or closing the
 * dialog.
 */
void run_jump_stop(GtkObject* object, gpointer user_data)
{
    state.jump_cancelled = TRUE;
}

/* Called when the user clicks "Okay" from the set speed dialog.
 */
void run_speed_ok(GtkButton* button, gpointer user_data)
{
    run_speed_apply(NULL, user_data);
    gtk_widget_destroy(((speed_dialog_info*)user_data)->dialog);
}

/* Called when the user exits the speed dialog by hitting "enter" on the text entry. Treat this
 * like an "Okay" click.
 */
void run_speed_enter(GtkEditable* editable, gpointer user_data)
{
    gtk_spin_button_update(GTK_SPIN_BUTTON(((speed_dialog_info*)user_data)->speed_spinbox));
    run_speed_ok(NULL, user_data);
}

/* called when the "Apply" button is clicked in the set speed dialog */
void run_speed_apply(GtkButton* button, gpointer user_data)
{
    speed_dialog_info*  info;

    info = (speed_dialog_info*)user_data;
    set_speed(gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(info->speed_spinbox)));
    state.skip_frames = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(info->skip_toggle));
}

/* Called when the speed dialog is closed */
void run_speed_destroy(GtkObject* object, gpointer user_data)
{
    free(user_data);
}

/* Called when the user clicks "Okay" in the pick browser dialog */
void pick_browser_ok(GtkButton* button, gpointer user_data)
{
    pick_browser_dialog_info* info;
    const char* command_line;
    int i;

    info = (pick_browser_dialog_info*)user_data;
    if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(info->radio_buttons[0]))) {
        error_dialog("Web browser required to view help");
        gtk_widget_destroy(info->dialog);
        return;
    }
    for (i=0; i < NUM_HELP_BROWSERS+1; i++) {
        if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(info->radio_buttons[i+1])))
            break;
    }
    if (i == NUM_HELP_BROWSERS+1) {
        error_dialog("Web browser required to view help");
        gtk_widget_destroy(info->dialog);
        return;
    }

    if (i < NUM_HELP_BROWSERS)
        config.help_browser = safe_strdup(help_browsers[i].command_line);
    else {
        command_line = gtk_entry_get_text(GTK_ENTRY(info->custom_browser_entry));
        if (!command_line || is_blank(command_line)) {
            error_dialog("You must enter a command line for the custom browser.");
            return;
        }
        config.help_browser = safe_strdup(command_line);
    }

    save_preferences();
    help_view_page(info->helpfile);
    gtk_widget_destroy(info->dialog);
}

/* Called when the pick browser dialog is destroyed */
void pick_browser_destroy(GtkObject* object, gpointer user_data)
{
    free(user_data);
}

/* Called when the user toggles the "Custom" help browser option in the pick-browser dialog. */
void pick_browser_toggle_custom(GtkToggleButton* toggle, gpointer user_data)
{
    pick_browser_dialog_info* info;
    boolean sensitive;

    info = (pick_browser_dialog_info*)user_data;
    sensitive = gtk_toggle_button_get_active(toggle);
    gtk_widget_set_sensitive(info->custom_browser_entry, sensitive);
}

/*** Functions for drawing the offscreen pixmap ***/

/* Fully redraw the Life pixmap: backdrop, grid, selection box, and live cells.
 */
void draw_life_pixmap(void)
{
    cage_type*  c;
    int32       x, y;

    /* Draw the backdrop */
    memset(dstate.life_pixmap, BG_COLOR_INDEX, dstate.canvas_size.width*dstate.canvas_size.height);

    /* Draw the live cells */
    while ((c = loop_cages_onscreen()) != NULL) {
        x = c->x * CAGE_SIZE;
        y = c->y * CAGE_SIZE;
        if (c->bnw[parity])
            draw_life_block(c->bnw[parity], x, y, TRUE);
        if (c->bne[parity])
            draw_life_block(c->bne[parity], x + BLOCK_SIZE, y, TRUE);
        if (c->bsw[parity])
            draw_life_block(c->bsw[parity], x, y + BLOCK_SIZE, TRUE);
        if (c->bse[parity])
            draw_life_block(c->bse[parity], x + BLOCK_SIZE, y + BLOCK_SIZE, TRUE);
    }

    draw_grid_and_boxes();
}

/* Redraw just the zoom grid and boxes (selection, paste) on the life pixmap, if applicable.
 */
void draw_grid_and_boxes(void)
{
    int32  x, y;

    /* Draw the grid, if necessary */
    if (DRAW_GRID(dstate.zoom)) {
        /* Draw the horizontal lines */
        for (y=0; y < dstate.canvas_size.height; y += (dstate.zoom+1))
            memset(dstate.life_pixmap + y*dstate.canvas_size.width, GRID_COLOR_INDEX,
                   dstate.eff_canvas_size.width);
        /* Draw the vertical lines */
        for (x=0; x < dstate.canvas_size.width; x += (dstate.zoom+1)) {
            for (y=0; y < dstate.eff_canvas_size.height; y++)
                dstate.life_pixmap[y*dstate.canvas_size.width + x] = GRID_COLOR_INDEX;
        }
    }

    /* Draw the selection box and/or paste-indicator box */
    draw_screen_box(&state.selection);
    draw_screen_box(&state.paste_box);
}

/* Draw the given box (selection or paste indicator) to the pixmap.
 */
void draw_screen_box(const rect* box)
{
    dimension  boxsize;
    rect       nbox;
    rect       r, cr;
    int32      y;

    if (box->start.x < 0)
        return;
    nbox = *box;
    normalize_rectangle(&nbox);
    if (nbox.start.x > dstate.viewport.end.x || nbox.start.y > dstate.viewport.end.y ||
        nbox.end.x < dstate.viewport.start.x || nbox.end.y < dstate.viewport.start.y) {
        return;
    }

    get_screen_rectangle(&nbox, &r);
    r.end.x += dstate.zoom-1;
    r.end.y += dstate.zoom-1;
    if (DRAW_GRID(dstate.zoom)) {
        r.start.x--;
        r.start.y--;
        r.end.x++;
        r.end.y++;
    }

    cr.start.x = MAX(0, r.start.x);
    cr.start.y = MAX(0, r.start.y);
    cr.end.x   = MIN(dstate.eff_canvas_size.width-1,  r.end.x);
    cr.end.y   = MIN(dstate.eff_canvas_size.height-1, r.end.y);
    get_rect_dimensions(&cr, &boxsize);

    if (r.start.y >= 0)
        memset(dstate.life_pixmap + r.start.y*dstate.canvas_size.width + cr.start.x,
               SELECT_COLOR_INDEX, boxsize.width);
    if (r.end.y < dstate.eff_canvas_size.height)
        memset(dstate.life_pixmap + r.end.y*dstate.canvas_size.width + cr.start.x,
               SELECT_COLOR_INDEX, boxsize.width);
    if (r.start.x >= 0) {
        for (y=cr.start.y; y <= cr.end.y; y++)
            dstate.life_pixmap[y*dstate.canvas_size.width + r.start.x] = SELECT_COLOR_INDEX;
    }
    if (r.end.x < dstate.eff_canvas_size.width) {
        for (y=cr.start.y; y <= cr.end.y; y++)
            dstate.life_pixmap[y*dstate.canvas_size.width + r.end.x] = SELECT_COLOR_INDEX;
    }
}

/* Draw the given 4x4 cell block onto the Life pixmap, at the given starting coordinates.
 * If full_update is TRUE, draw only live cells (because the screen has been cleared beforehand),
 * otherwise draw all cells. If full_update is FALSE, update the current clipping rectangle
 * (dstate.update.start.x, etc.) to include this block. This function may be called by the
 * backend.
 */
void draw_life_block(uint16 block, int32 xstart, int32 ystart, boolean full_update)
{
    int32     xend, yend;
    int32     minx, miny, maxx, maxy;
    int32     real_minx, real_miny, real_maxx, real_maxy;
    int32     realx, realy;
    int32     grid_block_size;
    int32     start_bit;
    int32     bit;
    int32     x, y, i;
    uint8     color;
    uint8*    spos;
    uint8*    pos;

    grid_block_size = (DRAW_GRID(dstate.zoom) ? dstate.zoom+1 : dstate.zoom);

    /* Figure out the screen offset, in blocks */
    xstart -= dstate.viewport.start.x;
    ystart -= dstate.viewport.start.y;
    xend = xstart + BLOCK_SIZE - 1;
    yend = ystart + BLOCK_SIZE - 1;
    minx = MAX(xstart, 0);
    miny = MAX(ystart, 0);
    maxx = MIN(xend, dstate.viewport_size.width-1);
    maxy = MIN(yend, dstate.viewport_size.height-1);
    if (minx > maxx || miny > maxy)
        return;

    /* Update the clipping rectangle, if necessary */
    if (!full_update) {
        real_minx = minx * grid_block_size;
        real_miny = miny * grid_block_size;
        real_maxx = maxx * grid_block_size + (dstate.zoom - 1);
        real_maxy = maxy * grid_block_size + (dstate.zoom - 1);
        if (DRAW_GRID(dstate.zoom)) {
            real_minx++;
            real_miny++;
            real_maxx++;
            real_maxy++;
        }
        if (real_minx < dstate.update.start.x)
            dstate.update.start.x = real_minx;
        if (real_miny < dstate.update.start.y)
            dstate.update.start.y = real_miny;
        if (real_maxx > dstate.update.end.x)
            dstate.update.end.x = real_maxx;
        if (real_maxy > dstate.update.end.y)
            dstate.update.end.y = real_maxy;
    }

    start_bit = (miny - ystart) * BLOCK_SIZE + (minx - xstart);
    /* Draw the cells */
    if (dstate.zoom > 1) {
        for (y=miny; y <= maxy; y++, start_bit += BLOCK_SIZE) {
            for (x=minx, bit=start_bit; x <= maxx; x++, bit++) {
                color = ((block & (1 << bit)) ? CELL_COLOR_INDEX : BG_COLOR_INDEX);
                if (full_update && color == BG_COLOR_INDEX)
                    continue;
                realx = x * grid_block_size;
                realy = y * grid_block_size;
                if (DRAW_GRID(dstate.zoom)) {
                    realx++;
                    realy++;
                }
                for (i=0, pos=dstate.life_pixmap + realy*dstate.canvas_size.width + realx;
                     i < dstate.zoom;
                     i++, pos += dstate.canvas_size.width)
                    memset(pos, color, dstate.zoom);
            }
        }
    } else {
        spos = dstate.life_pixmap + miny * dstate.canvas_size.width + minx;
        for (y=miny; y <= maxy; y++, spos += dstate.canvas_size.width, start_bit += BLOCK_SIZE) {
            for (x=minx, bit=start_bit, pos=spos; x <= maxx; x++, pos++, bit++) {
                color = ((block & (1 << bit)) ? CELL_COLOR_INDEX : BG_COLOR_INDEX);
                *pos = color;
            }
        }
    }
}

/* Set/unset the cell at the given absolute (logical) coordinates, then update the Life state and
 * the screen. If the coordinate are outside of the viewport, adjust them. If the user just clicked
 * the mouse, check the color of the cell to determine whether to set or erase. If they've been
 * dragging the mouse, continue either setting or erasing.
 */
void user_draw(const point* pt)
{
    static uint8  color;
    point   npt;
    point   spt;
    uint8*  pos;
    uint8   cur_color;
    int32   i;

    npt = *pt;
    if (npt.x < dstate.viewport.start.x)
        npt.x = dstate.viewport.start.x;
    else if (npt.x > dstate.viewport.end.x)
        npt.x = dstate.viewport.end.x;
    if (npt.y < dstate.viewport.start.y)
        npt.y = dstate.viewport.start.y;
    else if (npt.y > dstate.viewport.end.y)
        npt.y = dstate.viewport.end.y;
    state.last_drawn = npt;
    get_screen_coords(&npt, &spt);

    cur_color = dstate.life_pixmap[spt.y*dstate.canvas_size.width + spt.x];
    if (state.tracking_mouse == DRAWING) {
        if (cur_color == color)
            return;
    } else
        color = ((cur_color == CELL_COLOR_INDEX) ? BG_COLOR_INDEX : CELL_COLOR_INDEX);

    draw_cell(npt.x, npt.y, (color == BG_COLOR_INDEX ? DRAW_UNSET : DRAW_SET));
    if (dstate.zoom == 1)
        dstate.life_pixmap[spt.y*dstate.canvas_size.width + spt.x] = color;
    else {
        for (i=0, pos = dstate.life_pixmap + spt.y*dstate.canvas_size.width + spt.x;
             i < dstate.zoom;
             i++, pos += dstate.canvas_size.width)
            memset(pos, color, dstate.zoom);
    }

    gtk_widget_queue_draw_area(gui.canvas, spt.x, spt.y, dstate.zoom, dstate.zoom);
    state.tracking_mouse = DRAWING;

    update_population_label();
}

/* Call user_draw repeatedly to draw/erase a line between the points start and end.
 */
void draw_from_to(const point* start, const point* end)
{
    point   s, e, pt;
    point   temp;
    double  slope, realx, realy;

    s = *start;
    e = *end;

    if (abs(e.x - s.x) > abs(e.y - s.y)) {
        if (s.x > e.x)
            SWAP(s, e);
        slope = (double)(e.y - s.y) / (double)(e.x - s.x);
        pt = s;
        while (pt.x <= e.x) {
            user_draw(&pt);
            pt.x++;
            realy = (double)s.y + slope * (double)(pt.x - s.x);
            if (abs(pt.y - realy) >= 0.5)
                (slope < 0) ? pt.y-- : pt.y++;
        }
    } else {
        if (s.y > e.y)
            SWAP(s, e);
        slope = (double)(e.x - s.x) / (double)(e.y - s.y);
        pt = s;
        while (pt.y <= e.y) {
            user_draw(&pt);
            pt.y++;
            realx = (double)s.x + slope * (double)(pt.y - s.y);
            if (abs(pt.x - realx) >= 0.5)
                (slope < 0) ? pt.x-- : pt.x++;
        }
    }

    state.last_drawn = *end;
}

/*** Misc. GUI functions ***/

/* Adjust horizontal and vertical scrollbars for the current viewport start and size.
 */
void adjust_scrollbars(void)
{
    GtkObject*  scroll_params;

    /* Necessary to say WORLD_SIZE instead of WORLD_SIZE-1 to get full range. Why? */

    scroll_params = gtk_adjustment_new(dstate.viewport.start.x, 0, WORLD_SIZE,
                                       dstate.viewport_size.width/8, dstate.viewport_size.width,
                                       dstate.viewport_size.width);
    gtk_range_set_adjustment(GTK_RANGE(gui.hscrollbar), GTK_ADJUSTMENT(scroll_params));
    gtk_signal_connect(GTK_OBJECT(scroll_params), "value_changed",
                       GTK_SIGNAL_FUNC(handle_hscrollbar_change), NULL);

    scroll_params = gtk_adjustment_new(dstate.viewport.start.y, 0, WORLD_SIZE,
                                       dstate.viewport_size.height/8, dstate.viewport_size.height,
                                       dstate.viewport_size.height);
    gtk_range_set_adjustment(GTK_RANGE(gui.vscrollbar), GTK_ADJUSTMENT(scroll_params));
    gtk_signal_connect(GTK_OBJECT(scroll_params), "value_changed",
                       GTK_SIGNAL_FUNC(handle_vscrollbar_change), NULL);
    gtk_widget_queue_draw(gui.hscrollbar);
    gtk_widget_queue_draw(gui.vscrollbar);
}

/* Adjust horizontal and vertical scrollbars for the current viewport start */
void adjust_scrollbar_values(void)
{
    GtkAdjustment*  sb_state;

    sb_state = gtk_range_get_adjustment(GTK_RANGE(gui.hscrollbar));
    gtk_adjustment_set_value(sb_state, dstate.viewport.start.x);
    sb_state = gtk_range_get_adjustment(GTK_RANGE(gui.vscrollbar));
    gtk_adjustment_set_value(sb_state, dstate.viewport.start.y);
}

/* Setup a child window, making it transient to the main application window, and insuring that
 * it will be centered thereon. In Gtk1, set up a signal handler to do centering. In Gtk2, just use
 * gtk_window_set_position().
 */
void setup_child_window(GtkWidget* child)
{
    gtk_window_set_transient_for(GTK_WINDOW(child), GTK_WINDOW(gui.window));
#ifdef GTK2
    gtk_window_set_position(GTK_WINDOW(child), GTK_WIN_POS_CENTER_ON_PARENT);
#else
    gtk_signal_connect(GTK_OBJECT(child), "realize", GTK_SIGNAL_FUNC(handle_child_window_realize),
                       NULL);
#endif
}

/* Create a dialog centered on the main window.
 *
 * Arguments:
 *   title                - the title for the dialog
 *   info                 - dialog info to pass to signal handlers. If NULL, the dialog itself
 *                          will be passed.
 *   destroy_handler      - function to be called when the dialog is destroyed, NULL if none
 *   first_button_default - TRUE if the first button should be designated a default button
 *   num_buttons          - the number of buttons the dialog has at the bottom
 *
 *   The remaining arguments should have a label and a handler for each of the buttons. If a button
 *   handler should simply destroy the dialog, pass NULL for it.
 */
GtkWidget* create_dialog(const char* title, gpointer info, GtkSignalFunc destroy_handler,
                         boolean first_button_default, int32 num_buttons, ...)
{
    va_list        args;
    GtkWidget*     dialog;
    GtkWidget*     alignment;
    GtkWidget*     button;
    GtkSignalFunc  handler;
    char*          button_label;
    int32          i;

    dialog = gtk_dialog_new();
    if (!info)
        info = dialog;
    gtk_window_set_title(GTK_WINDOW(dialog), title);
    setup_child_window(dialog);
    if (destroy_handler)
        gtk_signal_connect(GTK_OBJECT(dialog), "destroy", GTK_SIGNAL_FUNC(destroy_handler), info);
    gtk_container_set_border_width(GTK_CONTAINER(dialog), 5);
    gtk_container_set_border_width(GTK_CONTAINER(GTK_DIALOG(dialog)->action_area), 5);

    va_start(args, num_buttons);
    for (i=0; i < num_buttons; i++) {
        button_label = dsprintf(" %s ", va_arg(args, char*));
        button = gtk_button_new_with_label(button_label);
        free(button_label);
        CENTER_ALIGN(button);
        gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->action_area), alignment, FALSE, FALSE, 0);
        if (i == 0 && first_button_default) {
            GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
            gtk_widget_grab_default(button);
        }
        handler = va_arg(args, GtkSignalFunc);
        if (handler)
            gtk_signal_connect(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(handler), info);
        else
            gtk_signal_connect_object(GTK_OBJECT(button), "clicked", GTK_SIGNAL_FUNC(gtk_widget_destroy),
                                      GTK_OBJECT(dialog));
    }
    va_end(args);

    return dialog;
}

/* Display an error/warning dialog with the given message and an "Okay" button. The message may be
 * specified in printf-style fashion, with format string and arguments.
 */
void error_dialog(const char* format, ...)
{
    va_list     args;
    GtkWidget*  dialog;
    GtkWidget*  hbox;
    GtkWidget*  error_pixmap;
    GtkWidget*  message;
    char*       str;

    va_start(args, format);
    str = vdsprintf(format, args);
    va_end(args);

    dialog = create_dialog(TITLE " Error", NULL, NULL, TRUE, 1, "Okay", NULL);
    hbox = gtk_hbox_new(FALSE, 0);
    error_pixmap = load_pixmap_from_rgba_array(error_icon, ICON_WIDTH, ICON_HEIGHT,
                                               &(gui.window->style->bg[GTK_STATE_NORMAL]));
    gtk_box_pack_start(GTK_BOX(hbox), error_pixmap, FALSE, FALSE, 5);
    message = gtk_label_new(str);
    free(str);
    gtk_label_set_justify(GTK_LABEL(message), GTK_JUSTIFY_LEFT);
    gtk_box_pack_start(GTK_BOX(hbox), message, FALSE, FALSE, 0);
    gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox), hbox);
    gtk_widget_show_all(dialog);
}

/* Force the hidebar to shrink down to minimum via a hide/show shuffle.
 */
void force_sidebar_resize(void)
{
    if (dstate.fullscreen || !dstate.visible_components[COMPONENT_SIDEBAR])
        return;

    if (dstate.sub_sidebar_visible) {
        gtk_widget_hide_all(gui.sidebar);
        gtk_widget_show_all(gui.sidebar);
    } else {
        gtk_widget_hide_all(gui.main_sidebar);
        gtk_widget_show_all(gui.main_sidebar);
        gtk_widget_hide(gui.sidebar);
        gtk_widget_show(gui.sidebar);
    }
}

/* Redraw the entire Life pixmap, then trigger an expose event on the canvas. Also set
 * state.skipped_frames back to 0.
 */
void full_canvas_redraw(void)
{
    state.skipped_frames = 0;
    draw_life_pixmap();
    gtk_widget_queue_draw(gui.canvas);
}

/* Put the current filename (taken from state.recent_files[0]) into the main window title */
void put_filename_in_window_title(void)
{
    char*  title;

    title = dsprintf("%s: %s", TITLE, state.recent_files[0].filename);
    gtk_window_set_title(GTK_WINDOW(gui.window), title);
    free(title);
}

/* Erase a temporary status message and restore the original one (if any).
 */
void restore_status_message(void)
{
    state.temp_message_active = FALSE;
    gtk_label_set_text(GTK_LABEL(gui.status_message_label), state.original_message);
    free(state.original_message);
    state.original_message = NULL;
}

/* Set whether the given command is to be grayed out, for both the menu item and (if it exists)
 * the toolbar button.
 */
void set_command_sensitivity(command_type cmd, boolean sensitive)
{
    gtk_widget_set_sensitive(gui.command_widgets[cmd].menu_item, sensitive);
    if (gui.command_widgets[cmd].toolbar_button)
        gtk_widget_set_sensitive(gui.command_widgets[cmd].toolbar_button, sensitive);
}

/* Set the current cursor to the given type.
 */
void set_cursor(GdkCursorType type)
{
    GdkCursor* cursor;

    cursor = gdk_cursor_new(type);
    gdk_window_set_cursor(gui.canvas->window, cursor);
#ifdef GTK2
    gdk_cursor_unref(cursor);
#else
    gdk_cursor_destroy(cursor);
#endif
}

/* Record the current effective canvas size (canvas size minus any unused space) */
void set_effective_canvas_size(void)
{
    if (DRAW_GRID(dstate.zoom)) {
        dstate.eff_canvas_size.width  = dstate.viewport_size.width  * (dstate.zoom+1) + 1;
        dstate.eff_canvas_size.height = dstate.viewport_size.height * (dstate.zoom+1) + 1;
    } else {
        dstate.eff_canvas_size.width  = dstate.viewport_size.width  * dstate.zoom;
        dstate.eff_canvas_size.height = dstate.viewport_size.height * dstate.zoom;
    }
}

/* Adjust horizontal widget size for the speed label, based on max speed */
void set_speed_label_size(int32 max_speed)
{
    GdkFont* font;
    char* speed_str;
    int extra;

#ifdef GTK2
    font = gtk_style_get_font(gui.window->style);
    extra = 15;
#else
    font = gui.window->style->font;
    extra = 2;
#endif
    speed_str = dsprintf("%d", max_speed);
    gtk_widget_set_usize(GTK_WIDGET(gui.speed_label),
                         gdk_string_measure(font, speed_str) + extra, 0);
    free(speed_str);
}

/* Put a message in the statusbar. If is_temporary is TRUE, the message will only be shown for
 * TEMP_MESSAGE_INTERVAL seconds, then the original message will be restored. New temporary
 * messages permanently supplant old ones, while leaving the original non-temporary intact.
 */
void set_status_message(char* msg, boolean is_temporary)
{
    char*  org_msg;

    if (is_temporary) {
        if (!state.temp_message_active) {
            gtk_label_get(GTK_LABEL(gui.status_message_label), &org_msg);
            state.original_message = safe_strdup(org_msg);
            state.temp_message_active = TRUE;
        }
        state.temp_message_start_time = get_time_milliseconds();
    } else {     /* dump any current temporary message */
        state.temp_message_active = FALSE;
        free(state.original_message);
        state.original_message = NULL;
    }

    gtk_label_set_text(GTK_LABEL(gui.status_message_label), msg);
}

/* Set the global variables for viewport location, and let the backend know about the new
 * viewport. If center_around is true, attempt to center around the given point, otherwise try to
 * set the given point at the upper left corner. If the viewport would go off the edge of the
 * world, readjust it.
 */
void set_viewport_position(const point* pt, boolean center_around)
{
    /* Set viewport start */
    dstate.viewport.start.x = pt->x;
    dstate.viewport.start.y = pt->y;
    if (center_around) {
        dstate.viewport.start.x -= dstate.viewport_size.width/2;
        dstate.viewport.start.y -= dstate.viewport_size.height/2;
    }

    /* Readjust viewport start if the current viewport goes off the edge of the world */
    if (dstate.viewport.start.x < 0)
        dstate.viewport.start.x = 0;
    else if (dstate.viewport.start.x + dstate.viewport_size.width > WORLD_SIZE)
        dstate.viewport.start.x = WORLD_SIZE - dstate.viewport_size.width;
    if (dstate.viewport.start.y < 0)
        dstate.viewport.start.y = 0;
    else if (dstate.viewport.start.y + dstate.viewport_size.height > WORLD_SIZE)
        dstate.viewport.start.y = WORLD_SIZE - dstate.viewport_size.height;

    /* Set viewport end */
    dstate.viewport.end.x = dstate.viewport.start.x + dstate.viewport_size.width  - 1;
    dstate.viewport.end.y = dstate.viewport.start.y + dstate.viewport_size.height - 1;

    /* Let the backend know about the new viewport */
    set_viewport(dstate.viewport.start.x, dstate.viewport.start.y, dstate.viewport.end.x,
                 dstate.viewport.end.y);
}

/* Set the sensitivity of zoom menu items and buttons based on current zoom level.
 */
void set_zoom_sensitivities(void)
{
    set_command_sensitivity(CMD_VIEW_ZOOM_IN,  (dstate.zoom < MAX_ZOOM));
    set_command_sensitivity(CMD_VIEW_ZOOM_OUT, (dstate.zoom > MIN_ZOOM));
    gtk_widget_set_sensitive(gui.command_widgets[CMD_VIEW_ZOOM_1].toolbar_button,
                             (dstate.zoom > MIN_ZOOM));
    gtk_widget_set_sensitive(gui.command_widgets[CMD_VIEW_ZOOM_16].toolbar_button,
                             (dstate.zoom < MAX_ZOOM));
}

/* Trigger an expose event on the portion of the Life canvas which has changed over the last tick,
 * setting state.skipped_frames back to 0.
 */
void trigger_canvas_update(void)
{
    dimension  size;

    state.skipped_frames = 0;
    get_rect_dimensions(&dstate.update, &size);
    if (size.width > 0 && size.height > 0)
        gtk_widget_queue_draw_area(gui.canvas, dstate.update.start.x, dstate.update.start.y,
                                   size.width, size.height);
}

/* Update the description box if the File->Description dialog is open. If first_time is TRUE, the
 * dialog has just been created.
 */
void update_description_textbox(boolean first_time)
{
    GdkFont* font;
    int32  desc_width = 0;
    int32  desc_height;
    int32  extra;
    int32  i;

    if (!gui.description_dialog)
        return;

    /* Determine the width and height of the current description text, and set default size of
     * description dialog accordingly */
#ifdef GTK2
    font = gtk_style_get_font(gui.window->style);
    extra = 50;
#else
    font = gui.window->style->font;
    extra = 40;
#endif
    for (i=0; i < desc_num_lines; i++) {
        int32 w;
        w = gdk_string_measure(font, pattern_description[i]) + extra;
        if (w > desc_width)
            desc_width = w;
    }
    if (desc_width == 0) {
        desc_width = 510;
        desc_height = 300;
    } else {
        if (desc_width < 300)
            desc_width = 300;
        desc_height = MIN(400, 20 * desc_num_lines);
    }
    gtk_window_set_default_size(GTK_WINDOW(gui.description_dialog), desc_width, desc_height);

    /* Set the description text */
    gtk_text_freeze(GTK_TEXT(gui.description_textbox));
    gtk_editable_delete_text(GTK_EDITABLE(gui.description_textbox), 0, -1);
    for (i=0; i < desc_num_lines; i++) {
        gtk_text_insert(GTK_TEXT(gui.description_textbox), NULL, NULL, NULL,
                        pattern_description[i], -1);
        gtk_text_insert(GTK_TEXT(gui.description_textbox), NULL, NULL, NULL, "\n", -1);
    }
    gtk_text_thaw(GTK_TEXT(gui.description_textbox));
}

/* Update the onscreen label indicating where the mouse is hovering. If the mouse pointer is not
 * within the canvas (*mouse_pointer == null_point), show the coordinates of the upper-left corner.
 */
void update_hover_point_label(const point* mouse_pointer)
{
    static point  showing = {-1,-1};
    point  pt;
    char*  hover_point_str;

    pt = (points_identical(mouse_pointer, &null_point) ? dstate.viewport.start : *mouse_pointer);
    if (points_identical(&pt, &showing))
        return;
    showing = pt;

    hover_point_str = dsprintf("(%d, %d)", pt.x - WORLD_SIZE/2, pt.y - WORLD_SIZE/2);
    gtk_label_set_text(GTK_LABEL(gui.hover_point_label), hover_point_str);
    free(hover_point_str);
}

/* Update the onscreen population label to match the current population. */
void update_population_label(void)
{
    char  str[20];

    sprintf(str, "%u", population);
    gtk_label_set_text(GTK_LABEL(gui.population_label), str);
}

/* Regenerate the list of patterns for the sidebar. */
void update_sidebar_contents(void)
{
    update_sidebar_generic(TRUE, state.current_collection);
}

/* Regenerate the list of patterns for the sub-sidebar, based on the given selected row in the
 * main sidebar. Also set the new sub-sidebar header. If the sub-sidebar is not currently
 * visible, show it.*/
void update_sub_sidebar_contents(int32 sidebar_selection)
{
    char*  header;

    update_sidebar_generic(FALSE, state.sidebar_files[sidebar_selection].path);
    header = safe_strdup(state.sidebar_files[sidebar_selection].title);
    if (header[strlen(header)-1] == '/')
        header[strlen(header)-1] = '\0';
    gtk_clist_set_column_title(GTK_CLIST(gui.sub_patterns_clist), 0, header);
    free(header);
    if (!dstate.sub_sidebar_visible) {
        gtk_widget_show_all(gui.sub_sidebar);
        dstate.sub_sidebar_visible = TRUE;
    }
}

/* Update either the main or sub-sidebar, loading patterns from the given directory. If updating
 * the sub-sidebar and it's currently hidden, unhide it.
 */
void update_sidebar_generic(boolean is_main_sidebar, const char* new_dir)
{
    GtkWidget*      clist;
    pattern_file**  patterns;
    int32*          num_patterns;
    boolean         include_subdirs;
    int32           width_request;
    int32           i;

    if (is_main_sidebar) {
        clist           = gui.patterns_clist;
        patterns        = &state.sidebar_files;
        num_patterns    = &state.num_sidebar_files;
        include_subdirs = TRUE;
    } else {
        clist           = gui.sub_patterns_clist;
        patterns        = &state.sub_sidebar_files;
        num_patterns    = &state.num_sub_sidebar_files;
        include_subdirs = FALSE;
    }

    /* Load the new file list */
    load_pattern_directory(new_dir, include_subdirs, patterns, num_patterns);

    /* Clear the list display */
    gtk_clist_freeze(GTK_CLIST(clist));
    gtk_clist_clear(GTK_CLIST(clist));

    /* Add the list items */
    for (i=0; i < *num_patterns; i++)
        gtk_clist_append(GTK_CLIST(clist), &((*patterns)[i].title));

    /* Set a reasonable list width request */
    width_request = gtk_clist_optimal_column_width(GTK_CLIST(clist), 0) + 30;
    gtk_widget_set_usize(clist, width_request, -1);

    /* Display the new list */
    gtk_clist_thaw(GTK_CLIST(clist));
}

/* Update the onscreen tick label to match the current tick. */
void update_tick_label(void)
{
    char  str[20];

    sprintf(str, "%u", tick);
    gtk_label_set_text(GTK_LABEL(gui.tick_label), str);
}

/* Create an integral, snap-to-ticks spinbutton with the given initial, minimum and maximum values.
 */
GtkWidget* create_spinbox(int32 init_value, int32 min, int32 max)
{
    GtkWidget*  spinbox;
    GtkObject*  adjustment;
    char*       max_str;

    adjustment = gtk_adjustment_new(init_value, min, max, 1, 1, 0);
    spinbox = gtk_spin_button_new(GTK_ADJUSTMENT(adjustment), 1, 0);
    gtk_spin_button_set_numeric(GTK_SPIN_BUTTON(spinbox), TRUE);
    gtk_spin_button_set_snap_to_ticks(GTK_SPIN_BUTTON(spinbox), TRUE);
    max_str = dsprintf("%d", max);
#ifndef GTK2
    gtk_widget_set_usize(spinbox, gdk_string_measure(gui.window->style->font, max_str) + 25, 0);
#endif
    free(max_str);

    return spinbox;
}

/*** Miscellaneous Functions ***/

/* Add the current file to the top of the recent files list, and update the File menu accordingly.
 * If it is already in the list, move it to the top.
 */
void add_to_recent_files(void)
{
    char*    filename;
    char*    label_text;
    boolean  maxed_out = FALSE;
    int32    num_recent;
    int32    old_pos;
    int32    start;
    int32    i;

    /* Is it already at the top of recent files? */
    if (state.recent_files[0].full_path &&
        STR_EQUAL(state.pattern_path, state.recent_files[0].full_path))
        return;

    /* Determine filename from full_path */
    split_path(state.pattern_path, NULL, &filename);

    /* Determine the current # of recent files, and the position of this one if it's already in
     * the list.
     */
    for (i=0, old_pos=0; i < MAX_RECENT_FILES && state.recent_files[i].full_path; i++) {
        if (STR_EQUAL(state.recent_files[i].full_path, state.pattern_path))
            old_pos = i;
    }
    num_recent = i;

    /* If the recent files list is at maximum and this is a new entry, drop off the last item */
    if (num_recent == MAX_RECENT_FILES && !old_pos) {
        maxed_out = TRUE;
        num_recent--;
    }

    /* Make room for the new item at the top of the list */
    start = (old_pos ? old_pos : num_recent);
    free(state.recent_files[start].full_path);
    free(state.recent_files[start].filename);
    for (i=start; i > 0; i--) {
        state.recent_files[i].full_path = state.recent_files[i-1].full_path;
        state.recent_files[i].filename  = state.recent_files[i-1].filename;
        gtk_tooltips_set_tip(gui.menu_tooltips, state.recent_files[i].menu_item,
                             state.recent_files[i].full_path, NULL);
#ifdef GTK2
        label_text = dsprintf("_%d. %s", i+1, state.recent_files[i].filename);
        gtk_label_set_text_with_mnemonic(GTK_LABEL(state.recent_files[i].label), label_text);
#else
        label_text = dsprintf("%d. %s", i+1, state.recent_files[i].filename);
        gtk_label_set_text(GTK_LABEL(state.recent_files[i].label), label_text);
#endif
        free(label_text);
    }

    /* Put the new item at the top */
    state.recent_files[0].full_path = safe_strdup(state.pattern_path);
    state.recent_files[0].filename  = filename;
    gtk_tooltips_set_tip(gui.menu_tooltips, state.recent_files[0].menu_item,
                         state.recent_files[0].full_path, NULL);
#ifdef GTK2
    label_text = dsprintf("_1. %s", filename);
    gtk_label_set_text_with_mnemonic(GTK_LABEL(state.recent_files[0].label), label_text);
#else
    label_text = dsprintf("1. %s", filename);
    gtk_label_set_text(GTK_LABEL(state.recent_files[0].label), label_text);
#endif
    free(label_text);

    /* If we've increased the length of the list, show the menu item which was hidden before */
    if (!maxed_out && !old_pos)
        gtk_widget_show(state.recent_files[num_recent].menu_item);
    /* If the list was empty before this, show the menu separator which was hidden before */
    if (!num_recent)
        gtk_widget_show(gui.recent_files_separator);
}

/* Attempt to load a pattern file from the given path (making it canonical first), displaying an
 * error dialog and returning FALSE on failure, otherwise returning TRUE. Extensions from
 * default_file_extensions[] will be tried if the original path doesn't exist. If the load was
 * successful, this function will stop any running pattern, update the tick label, and recenter and
 * redraw the canvas.
 */
boolean attempt_load_pattern(const char* path)
{
    char*             resolved_path;
    char*             final_path;
    load_result_type  load_result;
    struct stat       statbuf;

    resolved_path = get_canonical_path(path);
    if (!resolved_path) {
        error_dialog("Open failed: \"%s\" does not exist", path);
        return FALSE;
    }
    final_path = find_life_file(resolved_path);
    free(resolved_path);
    if (!final_path) {
        error_dialog("Open failed: \"%s\" does not exist", path);
        return FALSE;
    }
    if (stat(final_path, &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) {
        error_dialog("Open failed: \"%s\" is a directory", path);
        free(final_path);
        return FALSE;
    }

    load_result = load_pattern(final_path, &state.file_format);
    if (load_result != LOAD_SUCCESS) {
        switch (load_result) {
        case LOAD_SYS_ERROR:
            error_dialog("Open failed: %s", strerror(errno));
            break;
        case LOAD_UNRECOGNIZED_FORMAT:
            error_dialog("Open failed: unrecognized file format");
            break;
        case LOAD_BAD_GLF_VERSION:
            error_dialog("Open failed: invalid GLF version in file");
            break;
        case LOAD_BAD_LIF_VERSION:
            error_dialog("Open failed: invalid LIF version in file\n"
                         "(neither 1.05 nor 1.06)");
            break;
        default:   /* LOAD_STRUCTURED_XLIFE */
            error_dialog("This appears to be a structured-format XLife file\n"
                         "(containing includes and/or sub-blocks). This type\n"
                         "of XLife file is not currently supported.");
            break;
        }
        free(final_path);
        return FALSE;
    }

    if (!state.pattern_path || !STR_EQUAL(final_path, state.pattern_path)) {
        free(state.pattern_path);
        state.pattern_path = final_path;
        add_to_recent_files();
        put_filename_in_window_title();
    } else
        free(final_path);

    if (state.pattern_running)
        start_stop();
    state.last_drawn = null_point;
    deactivate_selection(FALSE);
    deactivate_paste(FALSE);
    update_tick_label();
    update_population_label();
    update_description_textbox(FALSE);
    set_command_sensitivity(CMD_FILE_REOPEN, TRUE);
    view_recenter();   /* will trigger a canvas redraw */
    set_status_message(FILE_LOADED_MESSAGE, TRUE);

    return TRUE;
}

/* Attempt to save a pattern file to the given path (first canonicalizing it), displaying an error
 * dialog and returning FALSE on failure, otherwise returning TRUE.
 */
boolean attempt_save_pattern(const char* path, file_format_type format)
{
    save_result_type  save_result;
    char*  resolved_path;

    resolved_path = get_canonical_path(path);
    if (!resolved_path) {
        error_dialog("Save failed: invalid path \"%s\"", path);
        return FALSE;
    }

    save_result = save_pattern(resolved_path, format);
    if (save_result == SAVE_SUCCESS) {
        if (!state.pattern_path || !STR_EQUAL(resolved_path, state.pattern_path)) {
            free(state.pattern_path);
            state.pattern_path = resolved_path;
            add_to_recent_files();
            put_filename_in_window_title();
            set_command_sensitivity(CMD_FILE_REOPEN, TRUE);
        } else
            free(resolved_path);
        state.file_format = format;
        set_status_message(FILE_SAVED_MESSAGE, TRUE);
        return TRUE;
    }
    else {
        free(resolved_path);
        if (save_result == SAVE_SYS_ERROR)
            error_dialog("Save failed: %s", strerror(errno));
        else if (format == FORMAT_RLE)
            error_dialog("The description for an RLE-type file must be\n"
                         "no more than %d characters wide. Please fix\n"
                         "and re-save, or choose a different file format.",
                         RLE_DESC_MAX_COLS);
        else
            error_dialog("The description for a LIF-type file must be no\n"
                         "more than %d lines long and %d characters wide.\n"
                         "Please fix and re-save, or choose a different\n"
                         "file format.",
                         LIF_DESC_MAX_LINES, LIF_DESC_MAX_COLS);
        return FALSE;
    }
}

/* Begin a selection box starting at the given point, and redraw the canvas.
 */
void activate_selection(const point* pt)
{
    rect          r;
    command_type  i;

    state.tracking_mouse = SELECTING;
    state.selection_start_time = get_time_milliseconds();
    r.start = r.end = *pt;
    screen_box_update(&state.selection, &r, TRUE);
    for (i=CMD_EDIT_CUT; i <= CMD_EDIT_MOVE; i++)
        set_command_sensitivity(i, TRUE);
}

/* Clear all memory associated with the given pattern list, setting list to NULL and num_patterns
 * to 0.
 */
void clear_pattern_list(pattern_file** patterns, int32* num_patterns)
{
    int32  i;

    for (i=0; i < *num_patterns; i++) {
        free((*patterns)[i].filename);
        free((*patterns)[i].path);
        free((*patterns)[i].title);
    }
    free(*patterns);
    *patterns = NULL;
    *num_patterns = 0;
}

/* Deactivate any existing selection box. If redraw is TRUE, update the screen accordingly.
 */
void deactivate_selection(boolean redraw)
{
    command_type  i;

    if (rects_identical(&state.selection, &null_rect))
        return;

    if (state.tracking_mouse == SELECTING)
        state.tracking_mouse = NORMAL;
    screen_box_update(&state.selection, &null_rect, redraw);
    for (i=CMD_EDIT_CUT; i <= CMD_EDIT_MOVE; i++) {
        if (!(i == CMD_EDIT_PASTE && COPY_BUFFER_ACTIVE()))
            set_command_sensitivity(i, FALSE);
    }
}

/* If we are in paste-mode or move-mode, deactivate it. If redraw is TRUE, update the screen
 * accordingly.
 */
void deactivate_paste(boolean redraw)
{
    if (state.tracking_mouse != PASTING)
        return;

    state.tracking_mouse = NORMAL;
    state.moving = FALSE;
    screen_box_update(&state.paste_box, &null_rect, redraw);
    set_status_message((dstate.fullscreen ? FULLSCREEN_MESSAGE : ""), FALSE);
    set_command_sensitivity(CMD_EDIT_CANCEL_PASTE, FALSE);
}

/* Attempt to locate a life file based on the given path. Try the path by itself, then try
 * appending the extensions in default_file_extensions[]. Return the first match found
 * (dynamically allocated), or NULL.
 */
char* find_life_file(const char* path)
{
    struct stat  statbuf;
    char*  newpath;
    int32  i;

    if (stat(path, &statbuf) == 0)
        return safe_strdup(path);
    for (i=0; default_file_extensions[i]; i++) {
        newpath = dsprintf("%s.%s", path, default_file_extensions[i]);
        if (stat(newpath, &statbuf) == 0)
            return newpath;
        free(newpath);
    }

    return NULL;
}

/* Return the number that cooresponds to the given main window component. If there is no such
 * component, return -1.
 */
int32 get_component_by_short_name(const char* name)
{
    int32  i;

    for (i=0; i < NUM_COMPONENTS; i++) {
        if (STR_EQUAL(component_short_names[i], name))
            return i;
    }
    return -1;
}

void get_component_widgets(component_type component, GtkWidget** widget1, GtkWidget** widget2)
{
    *widget1 = ((component == COMPONENT_TOOLBAR)    ? gui.toolbar :
                (component == COMPONENT_SIDEBAR)    ? gui.sidebar :
                (component == COMPONENT_SCROLLBARS) ? gui.hscrollbar :
                                                      gui.statusbar);
    *widget2 = ((component == COMPONENT_SCROLLBARS) ? gui.vscrollbar : NULL);
}

/* Determine the Life coordinates (between 0 and WORLD_SIZE-1) that correspond to the given screen
 * coordinates (relative to the canvas). If the given coordinates are out of bounds, use the
 * nearest point on the canvas.
 */
void get_logical_coords(const point* p, point* logical_p)
{
    point  pt;
    int32  grid_block_size;

    pt = *p;
    if (pt.x < 0)
        pt.x = 0;
    else if (pt.x >= dstate.eff_canvas_size.width)
        pt.x = dstate.eff_canvas_size.width - 1;
    if (pt.y < 0)
        pt.y = 0;
    else if (pt.y >= dstate.eff_canvas_size.height)
        pt.y = dstate.eff_canvas_size.height - 1;

    grid_block_size = (DRAW_GRID(dstate.zoom) ? dstate.zoom+1 : dstate.zoom);
    logical_p->x = MIN(dstate.viewport.end.x, dstate.viewport.start.x + pt.x / grid_block_size);
    logical_p->y = MIN(dstate.viewport.end.y, dstate.viewport.start.y + pt.y / grid_block_size);
}

/* Determine the screen coordinates that correspond to the given Life coordinates. Specifically,
 * determine the location of the upper left hand pixel of the onscreen block (past any grid
 * lines).
 */
void get_screen_coords(const point* p, point* screen_p)
{
    int32  grid_block_size;

    grid_block_size = (DRAW_GRID(dstate.zoom) ? dstate.zoom+1 : dstate.zoom);
    screen_p->x = (p->x - dstate.viewport.start.x) * grid_block_size;
    screen_p->y = (p->y - dstate.viewport.start.y) * grid_block_size;
    if (DRAW_GRID(dstate.zoom)) {
        screen_p->x++;
        screen_p->y++;
    }
}

/* Call get_screen_coords to get screen coordinates for the Life coordinates of the given
 * rectangle.
 */
void get_screen_rectangle(const rect* r, rect* screen_r)
{
    get_screen_coords(&(r->start), &(screen_r->start));
    get_screen_coords(&(r->end),   &(screen_r->end));
}

/* Read a directory of patterns into a sorted, dynamically-allocated array of pattern_file's,
 * including any files with extensions in default_file_extensions[]. If include_subdirs is true,
 * also include subdirectories of dir in the array. The number of items loaded is placed in
 * num_patterns.
 *
 * *patterns and *num_patterns should reflect the original state of the list. If *patterns is
 * non-NULL, the old list will be cleared via clear_pattern_list() before loading the new one.
 */
void load_pattern_directory(const char* dir, boolean include_subdirs, pattern_file** patterns,
                            int32* num_patterns)
{
    pattern_file*   list;
    pattern_file    new_file;
    char*           dir_prefix;
    char*           path;
    char*           filename;
    char*           dot_pos;
    int32           num;
    int32           alloc_num;
    DIR*            dir_reader;
    struct dirent*  file_ent;
    struct stat     statbuf;
    int32           i;

    if (*patterns)
        clear_pattern_list(patterns, num_patterns);
    list = *patterns;
    num  = *num_patterns;

    dir_reader = opendir(dir);
    if (!dir_reader)
        return;
    if (dir[strlen(dir)-1] == '/')
        dir_prefix = safe_strdup(dir);
    else
        dir_prefix = dsprintf("%s/", dir);

    alloc_num = 50;
    list = safe_malloc(alloc_num * sizeof(pattern_file));
    while ((file_ent = readdir(dir_reader)) != NULL) {
        filename = file_ent->d_name;
        if (filename[0] == '.')
            continue;
        new_file.filename = NULL;
        path = dsprintf("%s%s", dir_prefix, filename);
        if (stat(path, &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) {
            if (include_subdirs) {
                new_file.filename = safe_strdup(filename);
                new_file.is_directory = TRUE;
            }
        } else {
            for (i=0; default_file_extensions[i]; i++) {
                if ((dot_pos = strrchr(filename, '.')) != NULL &&
                    STR_EQUAL_I(dot_pos+1, default_file_extensions[i])) {
                    new_file.filename = safe_strdup(filename);
                    new_file.is_directory = FALSE;
                    break;
                }
            }
        }
        if (new_file.filename) {
            new_file.path = path;
            set_pattern_title(&new_file);
            if (num == alloc_num) {
                alloc_num += 50;
                list = safe_realloc(list, alloc_num * sizeof(pattern_file));
            }
            memcpy(&list[num++], &new_file, sizeof(pattern_file));
        } else
            free(path);
    }
    closedir(dir_reader);
    qsort(list, num, sizeof(pattern_file), qsort_pattern_files);
    free(dir_prefix);

    *patterns = list;
    *num_patterns = num;
}

/* If the pattern is running, reset the frames-per-second  measurement.
 */
void reset_fps_measure(void)
{
    if (state.pattern_running) {
        state.start_tick = tick;
        state.start_time = get_time_milliseconds();
    }
}

/* Process an update of an onscreen rectangle (selection or paste). If redraw is true, redraw
 * the offscreen pixmap and invalidate portions of the screen to account for the visible change.
 * Pass &null_rect for newr to indicate that the rectangle has been deactivated.
 */
void screen_box_update(rect* oldr, const rect* newr, boolean redraw)
{
    rect       r, or, sr, cr;
    dimension  dim;
    int32      i;

    if (rects_identical(oldr, newr))
        return;
    or = *oldr;
    *oldr = *newr;
    if (!redraw)
        return;

    if (DRAW_GRID(dstate.zoom))
        draw_grid_and_boxes();
    else
        draw_life_pixmap();

    for (i=0; i < 2; i++) {
        if (i == 0) {
            if (or.start.x < 0)
                continue;
            r = or;
        } else {
            if (newr->start.x < 0)
                continue;
            r = *newr;
        }

        normalize_rectangle(&r);
        get_screen_rectangle(&r, &sr);
        sr.end.x += dstate.zoom-1;
        sr.end.y += dstate.zoom-1;
        if (DRAW_GRID(dstate.zoom)) {
            sr.start.x--;
            sr.start.y--;
            sr.end.x++;
            sr.end.y++;
        }
        cr.start.x = MAX(0, sr.start.x);
        cr.start.y = MAX(0, sr.start.y);
        cr.end.x   = MIN(dstate.eff_canvas_size.width-1, sr.end.x);
        cr.end.y   = MIN(dstate.eff_canvas_size.height-1, sr.end.y);
        get_rect_dimensions(&cr, &dim);

        if (sr.start.x >= 0)
            gtk_widget_queue_draw_area(gui.canvas, sr.start.x, cr.start.y, 1, dim.height);
        if (sr.end.x < dstate.eff_canvas_size.width)
            gtk_widget_queue_draw_area(gui.canvas, sr.end.x, cr.start.y, 1, dim.height);
        if (sr.start.y >= 0)
            gtk_widget_queue_draw_area(gui.canvas, cr.start.x, sr.start.y, dim.width, 1);
        if (sr.end.y < dstate.eff_canvas_size.height)
            gtk_widget_queue_draw_area(gui.canvas, cr.start.x, sr.end.y, dim.width, 1);
    }
}

/* Clear and/or copy the current selection. */
void selection_copy_clear(boolean copy, boolean clear)
{
    static uint16 left_col_masks[8]   = {0xFFFF, 0xEEEE, 0xCCCC, 0x8888, 0, 0, 0, 0};
    static uint16 right_col_masks[8]  = {0xFFFF, 0x7777, 0x3333, 0x1111, 0, 0, 0, 0};
    static uint16 top_col_masks[8]    = {0xFFFF, 0xFFF0, 0xFF00, 0xF000, 0, 0, 0, 0};
    static uint16 bottom_col_masks[8] = {0xFFFF, 0x0FFF, 0x00FF, 0x000F, 0, 0, 0, 0};
    cage_type*  c;
    cage_type*  newcage;
    rect        r;
    int32       left_offset, right_offset, top_offset, bottom_offset;
    uint16      nw_mask, ne_mask, sw_mask, se_mask;
    int32       x, y;

    r = state.selection;
    normalize_rectangle(&r);
    if (copy) {
        state.copy_rect = r;
        clear_cage_list(&state.copy_buffer);
        set_command_sensitivity(CMD_EDIT_PASTE, TRUE);
    }

    while ((c = loop_cages()) != NULL) {
        if (IS_EMPTY(c))
            continue;
        x = c->x * CAGE_SIZE;
        y = c->y * CAGE_SIZE;
        if (r.end.x >= x && r.start.x < x+CAGE_SIZE &&
            r.end.y >= y && r.start.y < y+CAGE_SIZE) {
            nw_mask = ne_mask = sw_mask = se_mask = 0xFFFF;
            if (r.start.x > x || r.start.y > y ||
                r.end.x < x+CAGE_SIZE-1 || r.end.y < y+CAGE_SIZE-1) {

                left_offset   = MAX(0, r.start.x - x);
                right_offset  = MAX(0, x + CAGE_SIZE - 1 - r.end.x);
                top_offset    = MAX(0, r.start.y - y);
                bottom_offset = MAX(0, y + CAGE_SIZE - 1 - r.end.y);

                nw_mask &= left_col_masks[left_offset];
                sw_mask &= left_col_masks[left_offset];
                ne_mask &= left_col_masks[MAX(left_offset-4, 0)];
                se_mask &= left_col_masks[MAX(left_offset-4, 0)];
                ne_mask &= right_col_masks[right_offset];
                se_mask &= right_col_masks[right_offset];
                nw_mask &= right_col_masks[MAX(right_offset-4, 0)];
                sw_mask &= right_col_masks[MAX(right_offset-4, 0)];
                nw_mask &= top_col_masks[top_offset];
                ne_mask &= top_col_masks[top_offset];
                sw_mask &= top_col_masks[MAX(top_offset-4, 0)];
                se_mask &= top_col_masks[MAX(top_offset-4, 0)];
                sw_mask &= bottom_col_masks[bottom_offset];
                se_mask &= bottom_col_masks[bottom_offset];
                nw_mask &= bottom_col_masks[MAX(bottom_offset-4, 0)];
                ne_mask &= bottom_col_masks[MAX(bottom_offset-4, 0)];
            }
            if (copy) {
                newcage = safe_malloc(sizeof(cage_type));
                newcage->x = c->x;
                newcage->y = c->y;
                newcage->bnw[0] = c->bnw[parity] & nw_mask;
                newcage->bne[0] = c->bne[parity] & ne_mask;
                newcage->bsw[0] = c->bsw[parity] & sw_mask;
                newcage->bse[0] = c->bse[parity] & se_mask;
                newcage->next = state.copy_buffer;
                state.copy_buffer = newcage;
            }
            if (clear)
                mask_cage(c, ~nw_mask, ~ne_mask, ~sw_mask, ~se_mask);
        }
    }
}

/* Set the pattern collection directory to the given path. If have_gui is TRUE, update the onscreen
 * sidebar accordingly.
 */
void set_collection_dir(const char* path, boolean have_gui)
{
    if (!state.current_collection || !STR_EQUAL(path, state.current_collection)) {
        free(state.current_collection);
        state.current_collection = safe_strdup(path);
        if (have_gui)
            update_sidebar_contents();
    }
    free(state.current_dir);
    state.current_dir = safe_strdup(state.current_collection);
}

/* Set state.current_dir (default load directory) to the parent directory of the given file
 * (specified by absolute path).
 */
void set_current_dir_from_file(const char* filepath)
{
    char*  parent_dir;
    char*  slash_pos;

    parent_dir = safe_strdup(filepath);
    slash_pos = strrchr(parent_dir, '/');
    if (!slash_pos) {
        free(parent_dir);
        return;
    }
    *(slash_pos+1) = '\0';
    free(state.current_dir);
    state.current_dir = parent_dir;
}

/* Set the display title (file->title) for the given pattern file structure. Title is derived from
 * filename as follows: For a regular file, strip off the extension. For a directory, append a '/'
 * character. In all cases, replace underscores with spaces and capitalize words.
 */
void set_pattern_title(pattern_file* file)
{
    char*  p;

    if (file->is_directory)
        file->title = dsprintf("%s/", file->filename);
    else {
        file->title = safe_strdup(file->filename);
        if ((p = strrchr(file->title, '.')) != NULL)
            *p = '\0';
    }

    file->title[0] = toupper(file->title[0]);
    for (p=file->title; *p; p++) {
        if (*p == '_') {
            *p = ' ';
            *(p+1) = toupper(*(p+1));
        }
    }
}

/* Set the pattern run speed, by some means other than the speed slider. This function will
 * readjust the slider, thus triggering an event, so handle_speed_slider_change will take care of
 * everything else.
 */
void set_speed(int32 new_speed)
{
    GtkAdjustment*  slider_state;

    state.speed = new_speed;
    if (state.speed < 1)
        state.speed = 1;
    else if (state.speed > MAX_SPEED)
        state.speed = MAX_SPEED;

    slider_state = gtk_range_get_adjustment(GTK_RANGE(gui.speed_slider));
    if (state.speed < slider_state->lower) {
        slider_state->lower = state.speed;
        gtk_adjustment_changed(slider_state);
    } else if (state.speed > slider_state->upper) {
        slider_state->upper = state.speed;
        gtk_adjustment_changed(slider_state);
        set_speed_label_size(state.speed);
    }
    gtk_adjustment_set_value(slider_state, state.speed);
}

/* Deselect any selected item in the sidebar (or the sub-sidebar, if it is active).
 */
void sidebar_unselect(void)
{
    GtkWidget*  clist;

    clist = (dstate.sub_sidebar_visible ? gui.sub_patterns_clist : gui.patterns_clist);
    if (GTK_CLIST(clist)->selection || GTK_CLIST(clist)->focus_row > -1) {
        gtk_clist_unselect_all(GTK_CLIST(clist));
        GTK_CLIST(clist)->focus_row = -1;
        gtk_widget_queue_draw(clist);
    }
}

/* Toggle state.pattern_running, and update menus, toolbar, etc. accordingly, but do *not* update
 * the canvas or the tick label.
 */
void start_stop(void)
{
    const char*  new_text;
    GdkPixmap*   old_gdk_pixmap;
    GdkPixmap*   new_gdk_pixmap;

    state.pattern_running = !state.pattern_running;
    if (state.pattern_running) {
        state.skipped_frames = 0;
        state.start_tick = tick;
        state.start_time = get_time_milliseconds();
        new_text = "Stop";
    } else {
        state.skipped_frames = 0;
        new_text = "Start";
    }

    gtk_label_set_text(GTK_LABEL(gui.start_stop_menu_label), new_text);
    gtk_pixmap_get(GTK_PIXMAP(gui.start_stop_pixmap), &old_gdk_pixmap, NULL);
    gtk_pixmap_get(GTK_PIXMAP(gui.start_stop_alt_pixmap), &new_gdk_pixmap, NULL);
    gtk_pixmap_set(GTK_PIXMAP(gui.start_stop_pixmap), new_gdk_pixmap, NULL);
    gtk_pixmap_set(GTK_PIXMAP(gui.start_stop_alt_pixmap), old_gdk_pixmap, NULL);
}

/* Return true if the last_version string from preferences indicates a version at least as
 * recent as req_version.
 */
boolean last_version_atleast(double req_version)
{
    return (atof(config.last_version) >= req_version);
}

/* Validate the given pattern collection directory, returning a canonical form (dynamically
 * allocated) if valid, and NULL otherwise. If validation fails and have_gui is TRUE, display an
 * error dialog.
 */
char* validate_collection_dir(const char* path, boolean have_gui)
{
    DIR*   dir;
    char*  resolved_path;
    char*  final_path;;

    resolved_path = get_canonical_path(path);
    if (!resolved_path || !(dir = opendir(resolved_path))) {
        if (have_gui) {
            if (!resolved_path || errno == ENOENT)
                error_dialog("Invalid pattern collection directory:\n%s does not exist", path);
            else
                error_dialog("Invalid pattern collection directory\n(%s)", strerror(errno));
        }
        free(resolved_path);
        return NULL;
    } else {
        closedir(dir);
        final_path = append_trailing_slash(resolved_path);
        free(resolved_path);
        return final_path;
    }
}

/* Canonicalize, validate and, if valid, set the given pattern collection dir. If have_gui is
 * true, display an error dialog for a validation error, and update the sidebar display after
 * setting the collection. Otherwise do everything quietly.
 *
 * Return TRUE if validation succeeded, FALSE otherwise
 */
boolean validate_and_set_collection_dir(const char* path, boolean have_gui)
{
    char*  resolved_path;

    if ((resolved_path = validate_collection_dir(path, have_gui)) != NULL) {
        set_collection_dir(resolved_path, have_gui);
        free(resolved_path);
        return TRUE;
    } else
        return FALSE;
}

/*** Utility Functions ***/

/* Blend the pixel rgb2 (an array of 3 unsigned chars represented red, green,
 * blue) onto the pixel rgb1, where rgb2 has the given opacity (0-255).
 */
void alpha_blend(uint8* rgb1, const uint8* rgb2, int32 opacity)
{
    *rgb1 = ((int)(*rgb1) * (255 - opacity) + (int)(*rgb2++) * opacity) / 255;
    rgb1++;
    *rgb1 = ((int)(*rgb1) * (255 - opacity) + (int)(*rgb2++) * opacity) / 255;
    rgb1++;
    *rgb1 = ((int)(*rgb1) * (255 - opacity) + (int)(*rgb2++) * opacity) / 255;
}

/* Return a dynamically allocated copy of the given path, with a trailing slash appended unless
 * it already has one.
 */
char* append_trailing_slash(const char* path)
{
    return (path[strlen(path)-1] == '/') ? safe_strdup(path) : dsprintf("%s/", path);
}

/* Return an absolute, canonical pathname for the given path, dynamically allocated. All symlinks
 * and references to . and .. will be resolved. References to ~ will be resolved by referring to
 * state.home_dir; references to ~user will be resolved via getpwnam. If the path cannot be
 * resolved, return NULL.
 *
 * This fuction will resolve a file that does not exist, as long as its parent directory exists.
 * Trailing symlinks will not be translated, but intermediate directory symlinks will.
 */
char* get_canonical_path(const char* path)
{
    struct  passwd*  passwd_entry;
    char   path_buf[PATH_MAX];
    const char*  endptr;
    char*  p;
    char*  user;
    char*  dir;
    char*  file;
    char*  realpath_result;
    int32  username_len;

    if (!path || IS_EMPTY_STRING(path))
        return NULL;

    /* First, render the past absolute */
    if (STR_STARTS_WITH(path, "/"))                /* already an absolute path */
        p = safe_strdup(path);
    else if (STR_STARTS_WITH(path, "~")) {         /* home-relative path */
        if (path[1] == '/' || path[1] == '\0')
            p = dsprintf("%s%s", state.home_dir, path+1);
        else {
            endptr = strchr(path+1, '/');
            if (!endptr)
                endptr = path + strlen(path);
            username_len = endptr - (path+1);
            user = safe_malloc((username_len + 1) * sizeof(char));
            strncpy(user, path+1, username_len);
            user[username_len] = '\0';
            passwd_entry = getpwnam(user);
            free(user);
            if (!passwd_entry || !(passwd_entry->pw_dir) || !strlen(passwd_entry->pw_dir))
                return NULL;
            p = dsprintf("%s%s", safe_strdup(passwd_entry->pw_dir), endptr);
        }
    } else {                                       /* cwd-relative path */
        if (!getcwd(path_buf, PATH_MAX))
            return NULL;
        p = dsprintf("%s/%s", path_buf, path);
    }
    if (p[0] != '/') {  /* sanity check */
        free(p);
        return NULL;
    }

    /* Cut off trailing slashes */
    while (!STR_EQUAL(p, "/") && p[strlen(p)-1] == '/')
        p[strlen(p)-1] = '\0';

    /* Usually we want to resolve the parent directory instead of the whole path */
    split_path(p, &dir, &file);
    if (!dir) {}
    else if (STR_EQUAL(file, ".") || STR_EQUAL(file, "..")) {
        realpath_result = realpath(p, path_buf);
        free(p);
        if (realpath_result)
            p = safe_strdup(path_buf);
        else
            p = NULL;
    } else {
        realpath_result = realpath(dir, path_buf);
        free(p);
        if (realpath_result)
            p = join_path(path_buf, file);
        else
            p = NULL;
    }

    free(dir);
    free(file);
    return p;
}

/* Return the user's home directory, dynamically allocated. If no home directory is found, "/" is
 * assumed.
 */
char* get_home_directory(void)
{
    const char* home_dir;

    home_dir = g_get_home_dir();
    if (home_dir && strlen(home_dir))
        return ((home_dir[strlen(home_dir)-1] == '/') ? safe_strdup(home_dir) :
                                                        dsprintf("%s/", home_dir));
    else {
        warn("No home directory found! Assuming '/'.");
        return safe_strdup("/");
    }
}

/* Return a dimension structure representing the width and height of the given rectangle.
 */
void get_rect_dimensions(const rect* r, dimension* dim)
{
    dim->width  = r->end.x - r->start.x + 1;
    dim->height = r->end.y - r->start.y + 1;
}

/* Get the current time in milliseconds, as a 64-bit uint
 */
uint64 get_time_milliseconds(void)
{
    struct timeval   t;
    struct timezone  tz;

    gettimeofday(&t, &tz);
    return (uint64)(t.tv_sec) * 1000ULL + (uint64)(t.tv_usec) / 1000ULL;
}

/* Join the given parent directory and filename into a path.
 */
char* join_path(const char* dir, const char* file)
{
    char*  divider;

    divider = ((dir[strlen(dir)-1] == '/') ? "" : "/");
    return dsprintf("%s%s%s", dir, divider, file);
}

/* Load a GTK pixmap from an xpm file, returning NULL on failure.
 */
GtkWidget* load_pixmap_from_xpm_file(const char* path)
{
    GdkPixmap*  gdk_pixmap;

    gdk_pixmap = gdk_pixmap_create_from_xpm(gui.window->window, NULL,
                                            &(gui.window->style->bg[GTK_STATE_NORMAL]), path);
    if (!gdk_pixmap)
        return NULL;
    return gtk_pixmap_new(gdk_pixmap, NULL);
}

/* Load a GTK Pixmap from an array of RGBA pixel values, alpha blending onto the given
 * background color.
 */
GtkWidget* load_pixmap_from_rgba_array(uint8* pixels, int32 width, int32 height, GdkColor* bg)
{
    GdkPixmap*  gdk_pixmap;
    uint8       bg_rgb[3];
    uint8*      rgb_pixels;
    int32       i;

    bg_rgb[0] = bg->red   / 257;
    bg_rgb[1] = bg->green / 257;
    bg_rgb[2] = bg->blue  / 257;
    rgb_pixels = safe_malloc(width * height * 3);
    for (i=0; i < width*height; i++) {
        memcpy(&rgb_pixels[i*3], bg_rgb, 3);
        alpha_blend(&rgb_pixels[i*3], &pixels[i*4], pixels[i*4+3]);
    }
    gdk_pixmap = gdk_pixmap_new(gui.window->window, width, height, -1);
    gdk_draw_rgb_image(gdk_pixmap, gui.window->style->fg_gc[GTK_WIDGET_STATE(gui.window)], 0, 0,
                       width, height, GDK_RGB_DITHER_NONE, rgb_pixels, width*3);
    free(rgb_pixels);
    return gtk_pixmap_new(gdk_pixmap, NULL);
}

/* Insure that r->start is the upper-left-hand corner of the rectangle.
 */
void normalize_rectangle(rect* r)
{
    int32  temp;

    if (r->start.x > r->end.x)
        SWAP(r->start.x, r->end.x);
    if (r->start.y > r->end.y)
        SWAP(r->start.y, r->end.y);
}

/* Return TRUE if the two point structures are identical.
 */
boolean points_identical(const point* p1, const point* p2)
{
    return (p1->x == p2->x && p1->y == p2->y);
}

/* A qsort helper to sort an array of pattern files by title, ignoring case.
 */
int32 qsort_pattern_files(const void* pat1, const void* pat2)
{
    return strcasecmp(((const pattern_file*)pat1)->title, ((const pattern_file*)pat2)->title);
}

/* Return TRUE if the two rect structures are identical.
 */
boolean rects_identical(const rect* p1, const rect* p2)
{
    return (points_identical(&(p1->start), &(p2->start)) &&
            points_identical(&(p1->end), &(p2->end)));
}

/* Return a dynamically allocated string which is a copy of str with any underscore character
 * removed.
 */
char* remove_underscore(const char* str)
{
    char*  newstr;
    char*  us_pos;

    newstr = safe_strdup(str);
    us_pos = strchr(newstr, '_');
    if (us_pos)
        memmove(us_pos, us_pos+1, strlen(us_pos+1)+1);

    return newstr;
}

/* Split the given absolute path into parent directory and file, which will be dynamically
 * allocated and placed in the the corresponding paramters. If path is "/", the return values are
 * NULL and "/".
 *
 * If you don't need dir or file, pass the unneeded parameter as NULL.
 */
void split_path(const char* path, char** dir, char** file)
{
    char*  slash_pos;
    char*  p;
    char*  d;
    char*  f;

    p = safe_strdup(path);

    if (STR_EQUAL(p, "/")) {
        d = NULL;
        f = "/";
    } else {
        slash_pos = strrchr(p, '/');
        if (slash_pos == p) {
            d = "/";
            f = p+1;
        } else {
            *slash_pos = '\0';
            d = p;
            f = slash_pos+1;
        }
    }

    if (dir)
        *dir  = (d ? safe_strdup(d) : NULL);
    if (file)
        *file = safe_strdup(f);
    free(p);
}
