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.
This commit is contained in:
Simon Tatham
2023-04-23 10:58:53 +01:00
parent 4752c7a2d9
commit 6c66e2b2de
4 changed files with 307 additions and 4 deletions

166
gtk.c
View File

@ -20,6 +20,9 @@
#endif #endif
#include <unistd.h> #include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h> #include <sys/time.h>
#include <sys/resource.h> #include <sys/resource.h>
@ -141,6 +144,8 @@ void fatal(const char *fmt, ...)
*/ */
static void changed_preset(frontend *fe); static void changed_preset(frontend *fe);
static void load_prefs(frontend *fe);
static char *save_prefs(frontend *fe);
struct font { struct font {
#ifdef USE_PANGO #ifdef USE_PANGO
@ -1917,9 +1922,17 @@ static void config_ok_button_clicked(GtkButton *button, gpointer data)
if (err) if (err)
error_box(fe->cfgbox, err); error_box(fe->cfgbox, err);
else { 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; fe->cfgret = true;
gtk_widget_destroy(fe->cfgbox); 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); thegame.free_params(params);
} }
load_prefs(fe);
midend_new_game(nme); midend_new_game(nme);
err = midend_print_puzzle(nme, fe->doc, fe->printsolns); 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 #ifdef USE_PRINTING
static void menu_print_event(GtkMenuItem *menuitem, gpointer data) 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)) if (!get_config(fe, which))
return; return;
midend_new_game(fe->me); if (which != CFG_PREFS)
midend_new_game(fe->me);
resize_fe(fe); resize_fe(fe);
midend_redraw(fe->me); midend_redraw(fe->me);
} }
@ -3214,6 +3365,7 @@ static frontend *new_window(
fe->timer_id = -1; fe->timer_id = -1;
fe->me = midend_new(fe, &thegame, &gtk_drawing, fe); fe->me = midend_new(fe, &thegame, &gtk_drawing, fe);
load_prefs(fe);
fe->dr_api = &internal_drawing; fe->dr_api = &internal_drawing;
@ -3477,6 +3629,16 @@ static frontend *new_window(
G_CALLBACK(menu_solve_event), fe); G_CALLBACK(menu_solve_event), fe);
gtk_widget_show(menuitem); 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_separator(GTK_CONTAINER(menu));
add_menu_ui_item(fe, GTK_CONTAINER(menu), "Exit", UI_QUIT, 'q', 0); add_menu_ui_item(fe, GTK_CONTAINER(menu), "Exit", UI_QUIT, 'q', 0);

31
misc.c
View File

@ -3,6 +3,7 @@
*/ */
#include <assert.h> #include <assert.h>
#include <ctype.h>
#ifdef NO_TGMATH_H #ifdef NO_TGMATH_H
# include <math.h> # include <math.h>
#else #else
@ -500,4 +501,34 @@ char *button2label(int button)
return NULL; 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: */ /* vim: set shiftwidth=4 tabstop=8: */

View File

@ -177,6 +177,22 @@ solving it yourself after seeing the answer, you can just press Undo.
\dd Closes the application entirely. \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} \H{common-id} Specifying games with the \ii{game ID}
There are two ways to save a game specification out of a puzzle and 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 space will move as many tiles as necessary to move the space to the
mouse pointer. mouse pointer.
The arrow keys will move a tile adjacent to the space in the direction By default, the arrow keys will move a tile adjacent to the space in
indicated (moving the space in the \e{opposite} direction). the direction indicated (moving the space in the \e{opposite}
direction).
Pressing \q{h} will make a suggested move. Pressing \q{h} enough Pressing \q{h} will make a suggested move. Pressing \q{h} enough
times will solve the game, but it may scramble your progress while 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 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!) 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} \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, deduce something about still other squares.) Even at Hard level,
guesswork and backtracking should never be necessary. 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} \C{lightup} \i{Light Up}
@ -1851,6 +1886,12 @@ noticeably.)
backtracking or guessing, \q{Hard} means that some guesses will backtracking or guessing, \q{Hard} means that some guesses will
probably be necessary. 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} \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} \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, \#{FIXME: what distinguishes Easy, Medium, and Hard? In particular,
when are backtracking/guesswork required, if ever?} 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} \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 still be unique. The remaining levels require increasingly complex
reasoning to avoid having to backtrack. 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} \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 (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). 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} \C{range} \i{Range}
\cfg{winhelp-topic}{games.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. \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} \C{pearl} \i{Pearl}
\cfg{winhelp-topic}{games.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} \C{undead} \i{Undead}
\cfg{winhelp-topic}{games.undead} \cfg{winhelp-topic}{games.undead}

View File

@ -388,6 +388,8 @@ void free_cfg(config_item *cfg);
void free_keys(key_label *keys, int nkeys); void free_keys(key_label *keys, int nkeys);
void obfuscate_bitmap(unsigned char *bmp, int bits, bool decode); void obfuscate_bitmap(unsigned char *bmp, int bits, bool decode);
char *fgetline(FILE *fp); 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. /* allocates output each time. len is always in bytes of binary data.
* May assert (or just go wrong) if lengths are unchecked. */ * May assert (or just go wrong) if lengths are unchecked. */