SameBoy/gtk3/settings.c
Maximilian Mader 1d7034fb88
[GTK3] Add GtkRadioMenuItems at runtime
Defining them in the UI definition file was buggy in Unity
and MATE (Mutiny layout). Somehow creating them manually
via the API works around that bug.

The only problem is that Unity fails to update the
marker for the active menu item on the *first* click.
It then lags one item update behind, i.e.
1) CGB is active
2) Click on AGB, CGB is still rendered as active
3) Click on any (including AGB) of the options, now AGB is rendered as active

Also: The Gnome 3 style hamburger menu has been removed.
2020-05-19 02:33:25 +02:00

552 lines
18 KiB
C

#include "settings.h"
#define get_object(id) gtk_builder_get_object(builder, id)
#define builder_get(type, id) type(get_object(id))
gchar* settings_file_path;
GKeyFile *key_file;
static void print_config_error(GError *error) {
if (error == NULL) return;
if (!g_error_matches(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND) && !g_error_matches(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)) {
g_warning("Config error: %s", error->message);
}
}
void _print_config(config_t *config, GLogLevelFlags log_level) {
#define EXPAND_GROUP(name, members) { \
g_log(G_LOG_DOMAIN, log_level, "[%s]", #name); \
struct config_ ## name ## _t *group = &config->name; \
members \
}
#define EXPAND_GROUP_MEMBER(member, key_type, default_value) \
g_log(G_LOG_DOMAIN, log_level, "%s="FORMAT_FOR_KEY_TYPE(key_type)"", #member, group->member);
EXPAND_CONFIG
#undef EXPAND_GROUP
#undef EXPAND_GROUP_MEMBER
}
// Duplicates the input and converts the first character to uppercase
gchar* ascii_ucfirst(gchar *str) {
gchar *out = g_strdup(str);
out[0] = g_ascii_toupper(str[0]);
return out;
}
void load_config_from_key_file(config_t *config, GKeyFile *key_file) {
g_message("Loading config from key file");
GError *error = NULL;
#define EXPAND_GROUP(name_arg, members) { \
struct config_ ## name_arg ## _t *group = &config->name_arg; \
g_autofree gchar *group_name = ascii_ucfirst(#name_arg); \
members \
}
#define EXPAND_GROUP_MEMBER(member, key_type, default_value) \
group->member = g_key_file_get_##key_type(key_file, group_name, #member, &error); \
if (error != NULL) { \
group->member = default_value; \
print_config_error(error); \
g_clear_error(&error); \
}
EXPAND_CONFIG
if (config->emulation.rewind_duration > 600) {
g_warning("Setting Emulation.rewind_duration too high might affect performance.");
}
#undef EXPAND_GROUP
#undef EXPAND_GROUP_MEMBER
}
void print_config(config_t *config) {
_print_config(config, G_LOG_LEVEL_MESSAGE);
}
void save_config_to_key_file(config_t *config, GKeyFile *key_file) {
g_message("Saving config to key file");
GError *error = NULL;
gchar *group_name;
#define EXPAND_GROUP(name_arg, members) { \
struct config_ ## name_arg ## _t *group = &config->name_arg; \
g_autofree gchar *group_name = ascii_ucfirst(#name_arg); \
members \
}
#define EXPAND_GROUP_MEMBER_IF_0(member, key_type, default_value) \
g_key_file_set_##key_type(key_file, group_name, #member, group->member);
#define EXPAND_GROUP_MEMBER_IF_1(member, key_type, default_value) \
if (group->member != NULL) { \
g_key_file_set_##key_type(key_file, group_name, #member, group->member); \
} \
else if (g_key_file_has_key(key_file, group_name, #member, &error)) { \
if (error != NULL) { \
g_warning("%s", error->message); \
g_clear_error(&error); \
} \
g_key_file_remove_key(key_file, group_name, #member, &error); \
if (error != NULL) { \
g_warning("%s", error->message); \
g_clear_error(&error); \
} \
}
#define EXPAND_GROUP_MEMBER_IF_EVAL(y, member, key_type, default_value) EXPAND_GROUP_MEMBER_IF_ ## y(member, key_type, default_value)
#define EXPAND_GROUP_MEMBER_IF(member, key_type, is_pointer, default_value) EXPAND_GROUP_MEMBER_IF_EVAL(is_pointer, member, key_type, default_value)
#define EXPAND_GROUP_MEMBER(member, key_type, default_value) EXPAND_GROUP_MEMBER_IF(member, key_type, GTYPE_IS_POINTER(key_type), default_value)
EXPAND_CONFIG
#undef EXPAND_GROUP
#undef EXPAND_GROUP_MEMBER
#undef EXPAND_GROUP_MEMBER_IF
#undef EXPAND_GROUP_MEMBER_IF_EVAL
#undef EXPAND_GROUP_MEMBER_IF_0
#undef EXPAND_GROUP_MEMBER_IF_1
// Save config to disk
if (!g_key_file_save_to_file(key_file, settings_file_path, &error)) {
g_warning ("Failed to save %s: %s", settings_file_path, error->message);
g_error_free(error);
return;
}
}
void on_preferences_realize(GtkWidget *w, gpointer builder_ptr) {
GtkWindow *preferences = GTK_WINDOW(w);
GtkBuilder *builder = (GtkBuilder *) builder_ptr;
GApplication *app = G_APPLICATION(gtk_builder_get_application(builder));
update_boot_rom_selector(builder);
// Hook up the static preferences
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "rewind_duration_selector"), g_strdup_printf("%i", config.emulation.rewind_duration));
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "dmg_revision_selector"), config.emulation.dmg_revision_name);
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "sgb_revision_selector"), config.emulation.sgb_revision_name);
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "cgb_revision_selector"), config.emulation.cgb_revision_name);
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "shader_selector"), config.video.shader);
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "color_correction_selector"), config.video.color_correction_id);
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "frame_blending_selector"), config.video.frame_blending_mode);
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "display_border_selector"), config.video.display_border_mode);
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "monochrome_palette_selector"), config.video.monochrome_palette_id);
gtk_toggle_button_set_active(builder_get(GTK_TOGGLE_BUTTON, "integer_scaling_toggle"), config.video.use_integer_scaling);
gtk_toggle_button_set_active(builder_get(GTK_TOGGLE_BUTTON, "aspect_ratio_toggle"), config.video.keep_aspect_ratio);
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "highpass_filter_selector"), config.audio.high_pass_filter_id);
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "sample_rate_selector"), g_strdup_printf("%i", config.audio.sample_rate));
gtk_toggle_button_set_active(builder_get(GTK_TOGGLE_BUTTON, "analog_speed_controls_toggle"), config.controls.analog_speed_controls);
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "rumble_mode_selector"), config.controls.rumble_mode);
}
void init_settings(GApplication *app, gchar *path, GDateTime **modification_date, GtkWindow *preferences) {
free_settings();
key_file = g_key_file_new();
if (path != NULL) {
settings_file_path = path;
}
else {
settings_file_path = g_build_filename(g_get_user_config_dir(), SETTINGS_FILE, NULL);
}
load_settings(app, modification_date);
}
int load_settings(GApplication *app, GDateTime **modification_date) {
GError *error = NULL;
g_message("Trying to load settings from %s", settings_file_path);
g_autoptr(GFile) file = g_file_new_for_path(settings_file_path);
g_autoptr(GFileInfo) file_info = g_file_query_info(file, "time::*", G_FILE_QUERY_INFO_NONE, NULL, NULL);
*modification_date = g_file_info_get_modification_date_time(file_info);
if (!g_key_file_load_from_file(key_file, settings_file_path, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, &error)) {
if (error->domain == G_FILE_ERROR) {
g_warning("Unable to load %s: %s", settings_file_path, error->message);
}
else if (error->domain == G_KEY_FILE_ERROR) {
g_warning("Failed to parse %s: %s", settings_file_path, error->message);
}
g_error_free(error);
}
load_config_from_key_file(&config, key_file);
_print_config(&config, G_LOG_LEVEL_DEBUG);
// Update GAction states
g_action_group_change_action_state(G_ACTION_GROUP(app), "toggle_mute", g_variant_new_boolean(config.audio.muted));
return 0;
}
void save_settings(GtkWindow *main_window, GDateTime *saved_modification_date) {
GError *error = NULL;
g_message("Trying to save settings to %s", settings_file_path);
g_autoptr(GFile) file = g_file_new_for_path(settings_file_path);
g_autoptr(GFileInfo) file_info = g_file_query_info(file, "time::*", G_FILE_QUERY_INFO_NONE, NULL, NULL);
GDateTime *modification_date = g_file_info_get_modification_date_time(file_info);
if (!g_date_time_equal(saved_modification_date, modification_date)) {
GtkMessageDialog *dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new(
main_window,
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_QUESTION,
GTK_BUTTONS_YES_NO,
"It looks like the configuration has changed on disk. Overwrite?"
));
gtk_window_set_title(GTK_WINDOW(dialog), "SameBoy");
gint result = gtk_dialog_run(GTK_DIALOG(dialog));
switch (result) {
case GTK_RESPONSE_YES:
save_config_to_key_file(&config, key_file);
break;
default:
// Action has been canceled
break;
}
}
else {
save_config_to_key_file(&config, key_file);
}
}
void free_settings(void) {
if (key_file != NULL) {
g_key_file_free(key_file);
key_file = NULL;
}
}
void update_boot_rom_selector(GtkBuilder *builder) {
GtkComboBoxText *combo_box = builder_get(GTK_COMBO_BOX_TEXT, "boot_rom_selector");
gtk_combo_box_text_remove_all(combo_box);
gtk_combo_box_text_append(combo_box, "auto", "Use Built-in Boot ROMs");
if (config.emulation.boot_rom_path != NULL && !g_str_equal(config.emulation.boot_rom_path, "auto") && !g_str_equal(config.emulation.boot_rom_path, "other")) {
gtk_combo_box_text_append(combo_box, config.emulation.boot_rom_path, config.emulation.boot_rom_path);
gtk_combo_box_set_active_id(GTK_COMBO_BOX(combo_box), config.emulation.boot_rom_path);
}
else {
gtk_combo_box_set_active_id(GTK_COMBO_BOX(combo_box), "auto");
}
gtk_combo_box_text_append_text(combo_box, "<separator>");
gtk_combo_box_text_append(combo_box, "other", "Other");
}
GB_color_correction_mode_t get_color_correction_mode(void) {
if (config.video.color_correction_id == NULL) goto default_value;
if (g_strcmp0(config.video.color_correction_id, "disabled") == 0) {
return GB_COLOR_CORRECTION_DISABLED;
}
else if (g_strcmp0(config.video.color_correction_id, "correct_color_curves") == 0) {
return GB_COLOR_CORRECTION_CORRECT_CURVES;
}
else if (g_strcmp0(config.video.color_correction_id, "emulate_hardware") == 0) {
return GB_COLOR_CORRECTION_EMULATE_HARDWARE;
}
else if (g_strcmp0(config.video.color_correction_id, "preserve_brightness") == 0) {
return GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS;
}
else if (g_strcmp0(config.video.color_correction_id, "reduce_contrast") == 0) {
return GB_COLOR_CORRECTION_REDUCE_CONTRAST;
}
// This should not happen
g_warning("Unknown color correction mode: %s\nFalling back to “Emulate Hardware”", config.video.color_correction_id);
default_value: return GB_COLOR_CORRECTION_EMULATE_HARDWARE;
}
void set_color_correction_mode(GB_color_correction_mode_t mode) {
switch (mode) {
case GB_COLOR_CORRECTION_DISABLED:
config.video.color_correction_id = "disabled";
break;
case GB_COLOR_CORRECTION_CORRECT_CURVES:
config.video.color_correction_id = "correct_color_curves";
break;
case GB_COLOR_CORRECTION_EMULATE_HARDWARE:
config.video.color_correction_id = "emulate_hardware";
break;
case GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS:
config.video.color_correction_id = "preserve_brightness";
break;
case GB_COLOR_CORRECTION_REDUCE_CONTRAST:
config.video.color_correction_id = "reduce_contrast";
break;
}
}
GB_frame_blending_mode_t get_frame_blending_mode(void) {
if (config.video.frame_blending_mode == NULL) goto default_value;
if (g_strcmp0(config.video.frame_blending_mode, "disabled") == 0) {
return GB_FRAME_BLENDING_MODE_DISABLED;
}
else if (g_strcmp0(config.video.frame_blending_mode, "simple") == 0) {
return GB_FRAME_BLENDING_MODE_SIMPLE;
}
else if (g_strcmp0(config.video.frame_blending_mode, "accurate") == 0) {
return GB_FRAME_BLENDING_MODE_ACCURATE;
}
// This should not happen
g_warning("Unknown frame blending mode: %s\nFalling back to “Disabled”", config.video.frame_blending_mode);
default_value: return GB_FRAME_BLENDING_MODE_DISABLED;
}
void set_frame_blending_mode(GB_frame_blending_mode_t mode) {
switch (mode) {
case GB_FRAME_BLENDING_MODE_DISABLED:
config.video.frame_blending_mode = "disabled";
break;
case GB_FRAME_BLENDING_MODE_SIMPLE:
config.video.frame_blending_mode = "simple";
break;
case GB_FRAME_BLENDING_MODE_ACCURATE:
case GB_FRAME_BLENDING_MODE_ACCURATE_ODD:
config.video.frame_blending_mode = "accurate";
break;
}
}
GB_border_mode_t get_display_border_mode(void) {
if (config.video.display_border_mode == NULL) goto default_value;
if (g_strcmp0(config.video.display_border_mode, "never") == 0) {
return GB_BORDER_NEVER;
}
else if (g_strcmp0(config.video.display_border_mode, "sgb_only") == 0) {
return GB_BORDER_SGB;
}
else if (g_strcmp0(config.video.display_border_mode, "always") == 0) {
return GB_BORDER_ALWAYS;
}
// This should not happen
g_warning("Unknown SGB border mode: %s\nFalling back to “Never”", config.video.display_border_mode);
default_value: return GB_BORDER_NEVER;
}
void set_display_border_mode(GB_border_mode_t mode) {
switch (mode) {
case GB_BORDER_NEVER:
config.video.display_border_mode = "never";
break;
case GB_BORDER_SGB:
config.video.display_border_mode = "sgb_only";
break;
case GB_BORDER_ALWAYS:
config.video.display_border_mode = "always";
break;
}
}
const GB_palette_t* get_monochrome_palette(void) {
if (config.video.monochrome_palette_id == NULL) goto default_value;
if (g_strcmp0(config.video.monochrome_palette_id, "greyscale") == 0) {
return &GB_PALETTE_GREY;
}
else if (g_strcmp0(config.video.monochrome_palette_id, "lime") == 0) {
return &GB_PALETTE_DMG;
}
else if (g_strcmp0(config.video.monochrome_palette_id, "olive") == 0) {
return &GB_PALETTE_MGB;
}
else if (g_strcmp0(config.video.monochrome_palette_id, "teal") == 0) {
return &GB_PALETTE_GBL;
}
// This should not happen
g_warning("Unknown monochrome palette: %s\nFalling back to “Greyscale”", config.video.monochrome_palette_id);
default_value: return &GB_PALETTE_GREY;
}
void set_monochrome_palette(const GB_palette_t *mode) {
g_message("%p | %p | %p | %p | %p", mode, &GB_PALETTE_GREY, &GB_PALETTE_DMG, &GB_PALETTE_MGB, &GB_PALETTE_GBL);
if (mode == &GB_PALETTE_GREY) {
config.video.monochrome_palette_id = "greyscale";
}
else if (mode == &GB_PALETTE_DMG) {
config.video.monochrome_palette_id = "lime";
}
else if (mode == &GB_PALETTE_MGB) {
config.video.monochrome_palette_id = "olive";
}
else if (mode == &GB_PALETTE_GBL) {
config.video.monochrome_palette_id = "teal";
}
}
GB_highpass_mode_t get_highpass_mode(void) {
if (config.audio.high_pass_filter_id == NULL) goto default_value;
if (g_strcmp0(config.audio.high_pass_filter_id, "disabled") == 0) {
return GB_HIGHPASS_OFF;
}
else if (g_strcmp0(config.audio.high_pass_filter_id, "emulate_hardware") == 0) {
return GB_HIGHPASS_ACCURATE;
}
else if (g_strcmp0(config.audio.high_pass_filter_id, "preserve_waveform") == 0) {
return GB_HIGHPASS_REMOVE_DC_OFFSET;
}
// This should not happen
g_warning("Unknown highpass mode: %s\nFalling back to “Accurate”", config.audio.high_pass_filter_id);
default_value: return GB_HIGHPASS_ACCURATE;
}
void set_highpass_mode(GB_highpass_mode_t mode) {
switch (mode) {
case GB_HIGHPASS_OFF:
config.audio.high_pass_filter_id = "disabled";
break;
case GB_HIGHPASS_MAX:
g_warning("GB_HIGHPASS_MAX is not a valid highpass mode, falling back to “Accurate”.");
case GB_HIGHPASS_ACCURATE:
config.audio.high_pass_filter_id = "emulate_hardware";
break;
case GB_HIGHPASS_REMOVE_DC_OFFSET:
config.audio.high_pass_filter_id = "preserve_waveform";
break;
}
}
const GB_rumble_mode_t get_rumble_mode(void) {
if (config.controls.rumble_mode == NULL) goto default_value;
if (g_strcmp0(config.controls.rumble_mode, "never") == 0) {
return GB_RUMBLE_DISABLED;
}
else if (g_strcmp0(config.controls.rumble_mode, "rumble_cartridges") == 0) {
return GB_RUMBLE_CARTRIDGE_ONLY;
}
else if (g_strcmp0(config.controls.rumble_mode, "always") == 0) {
return GB_RUMBLE_ALL_GAMES;
}
// This should not happen
g_warning("Unknown highpass mode: %s\nFalling back to “Never”", config.controls.rumble_mode);
default_value: return GB_RUMBLE_DISABLED;
}
void set_rumble_mode(GB_rumble_mode_t mode) {
switch (mode) {
case GB_RUMBLE_DISABLED:
config.controls.rumble_mode = "never";
break;
case GB_RUMBLE_CARTRIDGE_ONLY:
config.controls.rumble_mode = "rumble_cartridges";
break;
case GB_RUMBLE_ALL_GAMES:
config.controls.rumble_mode = "always";
break;
}
}
GB_model_t get_model(void) {
if (g_strcmp0(config.emulation.model, "DMG") == 0) {
return get_dmg_model();
}
else if (g_strcmp0(config.emulation.model, "MGB") == 0) {
g_warning("Emulation of MGBs is unsupported, falling back to DMG.");
return get_dmg_model();
}
else if (g_strcmp0(config.emulation.model, "AGB") == 0) {
return GB_MODEL_AGB;
}
else if (g_strcmp0(config.emulation.model, "SGB") == 0) {
return get_sgb_model();
}
return get_cgb_model();
}
void set_model(GB_model_t model) {
switch (model & GB_MODEL_FAMILY_MASK) {
case GB_MODEL_DMG_FAMILY:
if (model & GB_MODEL_SGB) {
config.emulation.model = "SGB";
}
else {
config.emulation.model = "DMG";
}
break;
case GB_MODEL_MGB_FAMILY:
config.emulation.model = "MGB";
break;
case GB_MODEL_CGB_FAMILY:
if (model & GB_MODEL_AGB) {
config.emulation.model = "AGB";
}
else {
config.emulation.model = "CGB";
}
break;
}
}
GB_model_t get_dmg_model(void) {
if (config.emulation.dmg_revision_name == NULL) goto default_value;
// TODO: Synchronize with GB_model_t (Core/gb.h)
if (g_strcmp0(config.emulation.dmg_revision_name, "DMG_CPU_B") == 0) {
return GB_MODEL_DMG_B;
}
default_value: return GB_MODEL_DMG_B;
}
GB_model_t get_sgb_model(void) {
if (config.emulation.sgb_revision_name == NULL) goto default_value;
// TODO: Synchronize with GB_model_t (Core/gb.h)
if (g_strcmp0(config.emulation.sgb_revision_name, "SGB1_NTSC") == 0) {
return GB_MODEL_SGB_NTSC;
}
else if (g_strcmp0(config.emulation.sgb_revision_name, "SGB1_PAL") == 0) {
return GB_MODEL_SGB_PAL;
}
else if (g_strcmp0(config.emulation.sgb_revision_name, "SGB2") == 0) {
return GB_MODEL_SGB2;
}
default_value: return GB_MODEL_SGB2;
}
GB_model_t get_cgb_model(void) {
if (config.emulation.cgb_revision_name == NULL) goto default_value;
// TODO: Synchronize with GB_model_t (Core/gb.h)
if (g_strcmp0(config.emulation.cgb_revision_name, "CPU_CGB_C") == 0) {
return GB_MODEL_CGB_C;
}
else if (g_strcmp0(config.emulation.cgb_revision_name, "CPU_CGB_E") == 0) {
return GB_MODEL_CGB_E;
}
default_value: return GB_MODEL_CGB_E;
}