Support user preferences in the Mac frontend.

The low-level load and save routines are basically copy-pasted from
gtk.c, with only minor changes to deal with the different locally
appropriate config file location and the lack of savefile_write_ctx.
This commit is contained in:
Simon Tatham
2023-04-24 08:44:40 +01:00
parent 81680583fd
commit bf453043db
2 changed files with 158 additions and 3 deletions

155
osx.m
View File

@ -83,6 +83,8 @@
#include <ctype.h> #include <ctype.h>
#include <time.h> #include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/time.h> #include <sys/time.h>
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#include "puzzles.h" #include "puzzles.h"
@ -178,6 +180,137 @@ void document_add_puzzle(document *doc, const game *game, game_params *par,
{ {
} }
static char *prefs_dir(void)
{
const char *var;
if ((var = getenv("SGT_PUZZLES_DIR")) != NULL)
return dupstr(var);
if ((var = getenv("HOME")) != NULL) {
size_t size = strlen(var) + 128;
char *dir = snewn(size, char);
sprintf(dir, "%s/Library/Application Support/"
"Simon Tatham's Portable Puzzle Collection", var);
return dir;
}
return NULL;
}
static char *prefs_path_general(const game *game, const char *suffix)
{
char *dir, *path;
dir = prefs_dir();
if (!dir)
return NULL;
path = make_prefs_path(dir, "/", game, suffix);
sfree(dir);
return path;
}
static char *prefs_path(const game *game)
{
return prefs_path_general(game, ".conf");
}
static char *prefs_tmp_path(const game *game)
{
return prefs_path_general(game, ".conf.tmp");
}
static void load_prefs(midend *me)
{
const game *game = midend_which_game(me);
char *path = prefs_path(game);
if (!path)
return;
FILE *fp = fopen(path, "r");
if (!fp)
return;
const char *err = midend_load_prefs(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(midend *me)
{
const game *game = midend_which_game(me);
char *dir_path = prefs_dir();
char *file_path = prefs_path(game);
char *tmp_path = prefs_tmp_path(game);
int fd;
FILE *fp;
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;
}
errno = 0;
fp = fdopen(fd, "w");
midend_save_prefs(me, savefile_write, fp);
fclose(fp);
if (errno) {
const char *os_err = strerror(errno);
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(errno);
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;
}
/* /*
* setAppleMenu isn't listed in the NSApplication header, but an * setAppleMenu isn't listed in the NSApplication header, but an
* NSApp responds to it, so we're adding it here to silence * NSApp responds to it, so we're adding it here to silence
@ -551,6 +684,8 @@ struct frontend {
fe.window = self; fe.window = self;
me = midend_new(&fe, ourgame, &osx_drawing, &fe); me = midend_new(&fe, ourgame, &osx_drawing, &fe);
load_prefs(me);
/* /*
* If we ever need to open a fresh window using a provided game * If we ever need to open a fresh window using a provided game
* ID, I think the right thing is to move most of this method * ID, I think the right thing is to move most of this method
@ -1277,6 +1412,11 @@ struct frontend {
[self startConfigureSheet:CFG_SETTINGS]; [self startConfigureSheet:CFG_SETTINGS];
} }
- (void)preferences:(id)sender
{
[self startConfigureSheet:CFG_PREFS];
}
- (void)sheetEndWithStatus:(bool)update - (void)sheetEndWithStatus:(bool)update
{ {
assert(sheet != NULL); assert(sheet != NULL);
@ -1316,8 +1456,21 @@ struct frontend {
[alert setInformativeText:[NSString stringWithUTF8String:error]]; [alert setInformativeText:[NSString stringWithUTF8String:error]];
[alert beginSheetModalForWindow:self modalDelegate:nil [alert beginSheetModalForWindow:self modalDelegate:nil
didEndSelector:NULL contextInfo:nil]; didEndSelector:NULL contextInfo:nil];
} else {
if (cfg_which == CFG_PREFS) {
char *prefs_err = save_prefs(me);
if (prefs_err) {
NSAlert *alert = [[[NSAlert alloc] init] autorelease];
[alert addButtonWithTitle:@"Bah"];
[alert setInformativeText:[NSString stringWithUTF8String:
prefs_err]];
[alert beginSheetModalForWindow:self modalDelegate:nil
didEndSelector:NULL contextInfo:nil];
sfree(prefs_err);
}
} else { } else {
midend_new_game(me); midend_new_game(me);
}
[self resizeForNewGameParams]; [self resizeForNewGameParams];
[self updateTypeMenuTick]; [self updateTypeMenuTick];
} }
@ -1751,6 +1904,8 @@ int main(int argc, char **argv)
newitem(menu, "Paste", "v", NULL, @selector(paste:)); newitem(menu, "Paste", "v", NULL, @selector(paste:));
[menu addItem:[NSMenuItem separatorItem]]; [menu addItem:[NSMenuItem separatorItem]];
newitem(menu, "Solve", "S-s", NULL, @selector(solveGame:)); newitem(menu, "Solve", "S-s", NULL, @selector(solveGame:));
[menu addItem:[NSMenuItem separatorItem]];
newitem(menu, "Preferences", "", NULL, @selector(preferences:));
menu = newsubmenu([app mainMenu], "Type"); menu = newsubmenu([app mainMenu], "Type");
typemenu = menu; typemenu = menu;

View File

@ -179,8 +179,8 @@ solving it yourself after seeing the answer, you can just press Undo.
\dt \i\e{Preferences} \dt \i\e{Preferences}
\dd Where supported (currently only on Windows and Unix), brings up a \dd Where supported (currently on Windows, Unix and MacOS), brings up
dialog allowing you to configure personal preferences about a a dialog allowing you to configure personal preferences about a
particular game. Some of these preferences will be specific to a particular game. Some of these preferences will be specific to a
particular game; others will be common to all games. particular game; others will be common to all games.