diff --git a/gtk.c b/gtk.c index 7f44baf..718cc80 100644 --- a/gtk.c +++ b/gtk.c @@ -20,6 +20,9 @@ #endif #include +#include +#include +#include #include #include @@ -141,6 +144,8 @@ void fatal(const char *fmt, ...) */ static void changed_preset(frontend *fe); +static void load_prefs(frontend *fe); +static char *save_prefs(frontend *fe); struct font { #ifdef USE_PANGO @@ -1917,9 +1922,17 @@ static void config_ok_button_clicked(GtkButton *button, gpointer data) if (err) error_box(fe->cfgbox, err); else { + if (fe->cfg_which == CFG_PREFS) { + char *prefs_err = save_prefs(fe); + if (prefs_err) { + error_box(fe->cfgbox, prefs_err); + sfree(prefs_err); + } + } fe->cfgret = true; gtk_widget_destroy(fe->cfgbox); - changed_preset(fe); + if (fe->cfg_which != CFG_PREFS) + changed_preset(fe); } } @@ -2742,6 +2755,8 @@ static void print_begin(GtkPrintOperation *printop, thegame.free_params(params); } + load_prefs(fe); + midend_new_game(nme); err = midend_print_puzzle(nme, fe->doc, fe->printsolns); } @@ -2953,6 +2968,140 @@ static void menu_load_event(GtkMenuItem *menuitem, gpointer data) } } +static char *prefs_dir(void) +{ + const char *var; + if ((var = getenv("SGT_PUZZLES_DIR")) != NULL) + return dupstr(var); + if ((var = getenv("XDG_CONFIG_HOME")) != NULL) { + size_t size = strlen(var) + 20; + char *dir = snewn(size, char); + sprintf(dir, "%s/sgt-puzzles", var); + return dir; + } + if ((var = getenv("HOME")) != NULL) { + size_t size = strlen(var) + 32; + char *dir = snewn(size, char); + sprintf(dir, "%s/.config/sgt-puzzles", var); + return dir; + } + return NULL; +} + +static char *prefs_path_general(const char *suffix) +{ + char *dir, *path; + + dir = prefs_dir(); + if (!dir) + return NULL; + + path = make_prefs_path(dir, "/", &thegame, suffix); + + sfree(dir); + return path; +} + +static char *prefs_path(void) +{ + return prefs_path_general(".conf"); +} + +static char *prefs_tmp_path(void) +{ + return prefs_path_general(".conf.tmp"); +} + +static void load_prefs(frontend *fe) +{ + char *path = prefs_path(); + if (!path) + return; + FILE *fp = fopen(path, "r"); + if (!fp) + return; + const char *err = midend_load_prefs(fe->me, savefile_read, fp); + fclose(fp); + if (err) + fprintf(stderr, "Unable to load preferences file %s:\n%s\n", + path, err); + sfree(path); +} + +static char *save_prefs(frontend *fe) +{ + char *dir_path = prefs_dir(); + char *file_path = prefs_path(); + char *tmp_path = prefs_tmp_path(); + struct savefile_write_ctx wctx[1]; + int fd; + bool cleanup_dir = false, cleanup_tmpfile = false; + char *err = NULL; + + if (!dir_path || !file_path || !tmp_path) { + sprintf(err = snewn(256, char), + "Unable to save preferences:\n" + "Could not determine pathname for configuration files"); + goto out; + } + + if (mkdir(dir_path, 0777) < 0) { + /* Ignore errors while trying to make the directory. It may + * well already exist, and even if we got some error code + * other than EEXIST, it's still worth at least _trying_ to + * make the file inside it, and see if that goes wrong. */ + } else { + cleanup_dir = true; + } + + fd = open(tmp_path, O_CREAT | O_WRONLY | O_TRUNC | O_EXCL, 0666); + if (fd < 0) { + const char *os_err = strerror(errno); + sprintf(err = snewn(256 + strlen(tmp_path) + strlen(os_err), char), + "Unable to save preferences:\n" + "Unable to create file '%s': %s", tmp_path, os_err); + goto out; + } else { + cleanup_tmpfile = true; + } + + wctx->error = 0; + wctx->fp = fdopen(fd, "w"); + midend_save_prefs(fe->me, savefile_write, wctx); + fclose(wctx->fp); + if (wctx->error) { + const char *os_err = strerror(wctx->error); + sprintf(err = snewn(80 + strlen(tmp_path) + strlen(os_err), char), + "Unable to write file '%s': %s", tmp_path, os_err); + goto out; + } + + if (rename(tmp_path, file_path) < 0) { + const char *os_err = strerror(wctx->error); + sprintf(err = snewn(256 + strlen(tmp_path) + strlen(file_path) + + strlen(os_err), char), + "Unable to save preferences:\n" + "Unable to rename '%s' to '%s': %s", tmp_path, file_path, + os_err); + goto out; + } else { + cleanup_dir = false; + cleanup_tmpfile = false; + } + + out: + if (cleanup_tmpfile) { + if (unlink(tmp_path) < 0) { /* can't do anything about this */ } + } + if (cleanup_dir) { + if (rmdir(dir_path) < 0) { /* can't do anything about this */ } + } + sfree(dir_path); + sfree(file_path); + sfree(tmp_path); + return err; +} + #ifdef USE_PRINTING static void menu_print_event(GtkMenuItem *menuitem, gpointer data) { @@ -2994,7 +3143,9 @@ static void menu_config_event(GtkMenuItem *menuitem, gpointer data) if (!get_config(fe, which)) return; - midend_new_game(fe->me); + if (which != CFG_PREFS) + midend_new_game(fe->me); + resize_fe(fe); midend_redraw(fe->me); } @@ -3214,6 +3365,7 @@ static frontend *new_window( fe->timer_id = -1; fe->me = midend_new(fe, &thegame, >k_drawing, fe); + load_prefs(fe); fe->dr_api = &internal_drawing; @@ -3477,6 +3629,16 @@ static frontend *new_window( G_CALLBACK(menu_solve_event), fe); gtk_widget_show(menuitem); } + + add_menu_separator(GTK_CONTAINER(menu)); + menuitem = gtk_menu_item_new_with_label("Preferences..."); + gtk_container_add(GTK_CONTAINER(menu), menuitem); + g_object_set_data(G_OBJECT(menuitem), "user-data", + GINT_TO_POINTER(CFG_PREFS)); + g_signal_connect(G_OBJECT(menuitem), "activate", + G_CALLBACK(menu_config_event), fe); + gtk_widget_show(menuitem); + add_menu_separator(GTK_CONTAINER(menu)); add_menu_ui_item(fe, GTK_CONTAINER(menu), "Exit", UI_QUIT, 'q', 0); diff --git a/misc.c b/misc.c index 6b85533..f0c4abd 100644 --- a/misc.c +++ b/misc.c @@ -3,6 +3,7 @@ */ #include +#include #ifdef NO_TGMATH_H # include #else @@ -500,4 +501,34 @@ char *button2label(int button) return NULL; } +char *make_prefs_path(const char *dir, const char *sep, + const game *game, const char *suffix) +{ + size_t dirlen = strlen(dir); + size_t seplen = strlen(sep); + size_t gamelen = strlen(game->name); + size_t suffixlen = strlen(suffix); + char *path, *p; + const char *q; + + path = snewn(dirlen + seplen + gamelen + suffixlen + 1, char); + p = path; + + memcpy(p, dir, dirlen); + p += dirlen; + + memcpy(p, sep, seplen); + p += seplen; + + for (q = game->name; *q; q++) + if (*q != ' ') + *p++ = tolower((unsigned char)*q); + + memcpy(p, suffix, suffixlen); + p += suffixlen; + + *p = '\0'; + return path; +} + /* vim: set shiftwidth=4 tabstop=8: */ diff --git a/puzzles.but b/puzzles.but index f572f70..ad5d604 100644 --- a/puzzles.but +++ b/puzzles.but @@ -177,6 +177,22 @@ solving it yourself after seeing the answer, you can just press Undo. \dd Closes the application entirely. +\dt \i\e{Preferences} + +\dd Where supported (currently only on Unix), brings up a dialog +allowing you to configure personal preferences about a particular +game. Some of these preferences will be specific to a particular game; +others will be common to all games. + +\lcont{ + +One option common to all games allows you to turn off the one-key +shortcuts like \q{N} for new game or \q{Q} for quit, so that there's +less chance of hitting them by accident. You can still access the same +shortcuts with the Ctrl key. + +} + \H{common-id} Specifying games with the \ii{game ID} There are two ways to save a game specification out of a puzzle and @@ -621,8 +637,9 @@ A left-click with the mouse in the row or column containing the empty space will move as many tiles as necessary to move the space to the mouse pointer. -The arrow keys will move a tile adjacent to the space in the direction -indicated (moving the space in the \e{opposite} direction). +By default, the arrow keys will move a tile adjacent to the space in +the direction indicated (moving the space in the \e{opposite} +direction). Pressing \q{h} will make a suggested move. Pressing \q{h} enough times will solve the game, but it may scramble your progress while @@ -636,6 +653,18 @@ The only options available from the \q{Custom...} option on the \q{Type} menu are \e{Width} and \e{Height}, which are self-explanatory. (Once you've changed these, it's not a \q{15-puzzle} any more, of course!) +\H{fifteen-prefs} \I{preferences, for Fifteen}Fifteen user preferences + +On platforms that support user preferences, the \q{Preferences} option +on the \q{Game} menu will let you configure the sense of the arrow +keys. With the default setting, \q{Move the tile}, the arrow key you +press indicates the direction that you want a tile to move, so that +(for example) if you want to move the tile left of the gap rightwards +into the gap, you'd press Right. With the opposite setting, \q{Move +the gap}, the behaviour of the arrow keys is reversed, and you would +press Left to move the tile left of the gap into the gap, so that the +\e{gap} ends up one square left of where it was. + \C{sixteen} \i{Sixteen} @@ -1768,6 +1797,12 @@ don't yet know what that direction is, and this might enable you to deduce something about still other squares.) Even at Hard level, guesswork and backtracking should never be necessary. +\H{slant-prefs} \I{preferences, for Slant}Slant user preferences + +On platforms that support user preferences, the \q{Preferences} option +on the \q{Game} menu will let you configure which way round the mouse +buttons work. + \C{lightup} \i{Light Up} @@ -1851,6 +1886,12 @@ noticeably.) backtracking or guessing, \q{Hard} means that some guesses will probably be necessary. +\H{lightup-prefs} \I{preferences, for Light Up}Light Up user preferences + +On platforms that support user preferences, the \q{Preferences} option +on the \q{Game} menu will let you configure whether \q{this is not a +light} marks are shown when the square is also lit. + \C{map} \i{Map} @@ -1944,6 +1985,12 @@ Unreasonable puzzles may require guessing and backtracking. } +\H{map-prefs} \I{preferences, for Map}Map user preferences + +On platforms that support user preferences, the \q{Preferences} option +on the \q{Game} menu will let you configure the style of the victory +flash. + \C{loopy} \i{Loopy} @@ -2016,6 +2063,34 @@ same; this makes them the least confusing to play. \#{FIXME: what distinguishes Easy, Medium, and Hard? In particular, when are backtracking/guesswork required, if ever?} +\H{loopy-prefs} \I{preferences, for Loopy}Loopy user preferences + +On platforms that support user preferences, the \q{Preferences} option +on the \q{Game} menu will let you configure the following things: + +\q{Draw excluded grid lines faintly}. This is on by default: when a +line of the grid has been explicitly excluded from the solution by +right-clicking it, the line is still drawn, just in a faint grey +colour. If you turn this option off, excluded lines are not drawn at +all. + +\q{Auto-follow unique paths of edges}. This is off by default. When +it's on, clicking to change the status of a single grid line will +potentially propagate the change along multiple lines, if one or both +ends of the line you clicked connect to only one other line. (The idea +is that if two lines meet at a vertex and no other lines do at all, +then those lines are either both part of the loop or neither, so +there's no reason you should have to click separately to toggle each +one.) + +In the mode \q{Based on grid only}, the effects of a click will only +propagate across vertices that have degree 2 in the underlying grid. +For example, in the square grid, the effect will \e{only} occur at the +four grid corners. + +In the mode \q{Based on grid and game state}, the propagation will +also take account of edges you've already excluded from the solution, +so that it will do even more work for you. \C{inertia} \i{Inertia} @@ -2720,6 +2795,14 @@ level, some backtracking will be required, but the solution should still be unique. The remaining levels require increasingly complex reasoning to avoid having to backtrack. +\H{towers-prefs} \I{preferences, for Towers}Towers user preferences + +On platforms that support user preferences, the \q{Preferences} option +on the \q{Game} menu will let you configure the style of the game +display. If you don't like the three-dimensional mode, selecting +\q{2D} will switch to a simpler display style in which towers are +shown by just writing their height in the square. + \C{singles} \i{Singles} @@ -2926,6 +3009,13 @@ These parameters are available from the \q{Custom...} option on the (the start at the top left, and the end at the bottom right). If false the start and end squares are placed randomly (although always both shown). +\H{signpost-prefs} \I{preferences, for Signpost}Signpost user preferences + +On platforms that support user preferences, the \q{Preferences} option +on the \q{Game} menu will let you configure the style of the victory +effect. + + \C{range} \i{Range} \cfg{winhelp-topic}{games.range} @@ -2988,6 +3078,13 @@ These parameters are available from the \q{Custom...} option on the \dd Size of grid in squares. +\H{range-prefs} \I{preferences, for Range}Range user preferences + +On platforms that support user preferences, the \q{Preferences} option +on the \q{Game} menu will let you configure which way round the mouse +buttons work. + + \C{pearl} \i{Pearl} \cfg{winhelp-topic}{games.pearl} @@ -3078,6 +3175,17 @@ possible to deduce it step by step. } +\H{pearl-prefs} \I{preferences, for Pearl}Pearl user preferences + +On platforms that support user preferences, the \q{Preferences} option +on the \q{Game} menu will let you configure the style of the game +display. \q{Traditional} is the default mode, in which the loop runs +between centres of grid squares, and each clue occupies a square. +\q{Loopy-style} is an alternative mode that looks more like Loopy +(\k{loopy}), in which the loop runs between grid \e{vertices}, and the +clues also occupy vertices. + + \C{undead} \i{Undead} \cfg{winhelp-topic}{games.undead} diff --git a/puzzles.h b/puzzles.h index e429c26..4084aa0 100644 --- a/puzzles.h +++ b/puzzles.h @@ -388,6 +388,8 @@ void free_cfg(config_item *cfg); void free_keys(key_label *keys, int nkeys); void obfuscate_bitmap(unsigned char *bmp, int bits, bool decode); char *fgetline(FILE *fp); +char *make_prefs_path(const char *dir, const char *sep, + const game *game, const char *suffix); /* allocates output each time. len is always in bytes of binary data. * May assert (or just go wrong) if lengths are unchecked. */