ref: bf453043db68342de85028c7a44cb75262e02ad9
parent: 81680583fd5af8a1fd9b1ee30d5fa3dfc073832a
author: Simon Tatham <anakin@pobox.com>
date: Mon Apr 24 04:44:40 EDT 2023
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.
--- a/osx.m
+++ b/osx.m
@@ -83,6 +83,8 @@
#include <ctype.h>
#include <time.h>
+#include <fcntl.h>
+#include <sys/stat.h>
#include <sys/time.h>
#import <Cocoa/Cocoa.h>
#include "puzzles.h"
@@ -178,6 +180,137 @@
{
}
+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
* NSApp responds to it, so we're adding it here to silence
@@ -551,6 +684,8 @@
fe.window = self;
me = midend_new(&fe, ourgame, &osx_drawing, &fe);
+ load_prefs(me);
+
/*
* 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
@@ -1277,6 +1412,11 @@
[self startConfigureSheet:CFG_SETTINGS];
}
+- (void)preferences:(id)sender
+{
+ [self startConfigureSheet:CFG_PREFS];
+}
+
- (void)sheetEndWithStatus:(bool)update
{
assert(sheet != NULL);
@@ -1317,7 +1457,20 @@
[alert beginSheetModalForWindow:self modalDelegate:nil
didEndSelector:NULL contextInfo:nil];
} else {
- midend_new_game(me);
+ 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 {
+ midend_new_game(me);
+ }
[self resizeForNewGameParams];
[self updateTypeMenuTick];
}
@@ -1751,6 +1904,8 @@
newitem(menu, "Paste", "v", NULL, @selector(paste:));
[menu addItem:[NSMenuItem separatorItem]];
newitem(menu, "Solve", "S-s", NULL, @selector(solveGame:));
+ [menu addItem:[NSMenuItem separatorItem]];
+ newitem(menu, "Preferences", "", NULL, @selector(preferences:));
menu = newsubmenu([app mainMenu], "Type");
typemenu = menu;
--- a/puzzles.but
+++ b/puzzles.but
@@ -179,8 +179,8 @@
\dt \i\e{Preferences}
-\dd Where supported (currently only on Windows and Unix), brings up a
-dialog allowing you to configure personal preferences about a
+\dd Where supported (currently on Windows, Unix and MacOS), 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.