ref: 6c66e2b2de63b6a8159835516ee229bda3253bc2
parent: 4752c7a2d9bd83d41d418b31b931a9bb9af219fa
author: Simon Tatham <anakin@pobox.com>
date: Sun Apr 23 06:58:53 EDT 2023
Support preferences in the GTK frontend. Finally, some user-visible behaviour changes as a payoff for all that preparation work! In this commit, the GTK puzzles get a 'Preferences' option in the menu, which presents a dialog box to configure the preference settings. On closing that dialog box, the puzzle preferences are enacted immediately, and also saved to a configuration file where the next run of the same puzzle will reload them. The default file location is ~/.config/sgt-puzzles/<puzzlename>.conf, although you can override the .config dir via $XDG_CONFIG_HOME or override the Puzzles-specific subdir with $SGT_PUZZLES_DIR. This is the first commit that actually exposes all the new preferences work to the user, and therefore, I've also added documentation of all the current preference options.
--- a/gtk.c
+++ b/gtk.c
@@ -20,6 +20,9 @@
#endif
#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
@@ -141,6 +144,8 @@
*/
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 @@
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 @@
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 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 @@
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 @@
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 @@
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);
--- a/misc.c
+++ b/misc.c
@@ -3,6 +3,7 @@
*/
#include <assert.h>
+#include <ctype.h>
#ifdef NO_TGMATH_H
# include <math.h>
#else
@@ -498,6 +499,36 @@
/* should never get here */
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: */
--- a/puzzles.but
+++ b/puzzles.but
@@ -177,6 +177,22 @@
\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 @@
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,7 +653,19 @@
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}
\cfg{winhelp-topic}{games.sixteen}
@@ -1768,7 +1797,13 @@
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}
\cfg{winhelp-topic}{games.lightup}
@@ -1851,7 +1886,13 @@
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}
\cfg{winhelp-topic}{games.map}
@@ -1944,7 +1985,13 @@
}
+\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}
\cfg{winhelp-topic}{games.loopy}
@@ -2016,7 +2063,35 @@
\#{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}
\cfg{winhelp-topic}{games.inertia}
@@ -2720,7 +2795,15 @@
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}
\cfg{winhelp-topic}{games.singles}
@@ -2926,6 +3009,13 @@
(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 @@
\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}
@@ -3077,6 +3174,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}
--- a/puzzles.h
+++ b/puzzles.h
@@ -388,6 +388,8 @@
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. */