2019-09-24 14:14:06 +00:00
|
|
|
#include "settings.h"
|
|
|
|
|
2019-09-25 00:48:07 +00:00
|
|
|
static void print_config_error(GError *error) {
|
|
|
|
if (error == NULL) return;
|
2019-09-24 14:14:06 +00:00
|
|
|
|
2019-09-25 20:47:04 +00:00
|
|
|
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)) {
|
2019-10-14 15:33:03 +00:00
|
|
|
g_warning("Config error: %s", error->message);
|
2019-09-25 00:48:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-14 15:33:03 +00:00
|
|
|
void _print_config(config_t *config, GLogLevelFlags log_level) {
|
2019-09-25 00:48:07 +00:00
|
|
|
#define EXPAND_GROUP(group_name, members) \
|
2019-10-14 15:33:03 +00:00
|
|
|
g_log(G_LOG_DOMAIN, log_level, "[%s]", #group_name); \
|
2019-09-25 00:48:07 +00:00
|
|
|
members
|
|
|
|
|
2019-09-27 21:10:28 +00:00
|
|
|
#define EXPAND_GROUP_MEMBER(member, key_type, default_value) \
|
2019-10-14 15:33:03 +00:00
|
|
|
g_log(G_LOG_DOMAIN, log_level, "%s="FORMAT_FOR_KEY_TYPE(key_type)"", #member, config->member);
|
2019-09-25 00:48:07 +00:00
|
|
|
|
|
|
|
EXPAND_CONFIG
|
|
|
|
|
|
|
|
#undef EXPAND_GROUP
|
|
|
|
#undef EXPAND_GROUP_MEMBER
|
|
|
|
}
|
|
|
|
|
|
|
|
void load_config_from_key_file(config_t *config, GKeyFile *key_file) {
|
2019-10-14 15:33:03 +00:00
|
|
|
g_message("Loading config from key file");
|
2019-09-25 20:47:04 +00:00
|
|
|
GError *error = NULL;
|
2019-09-25 00:48:07 +00:00
|
|
|
gchar *group_name;
|
|
|
|
|
|
|
|
#define EXPAND_GROUP(name, members) \
|
|
|
|
group_name = #name; \
|
2019-09-29 00:07:33 +00:00
|
|
|
members
|
2019-09-25 00:48:07 +00:00
|
|
|
|
2019-09-27 21:10:28 +00:00
|
|
|
#define EXPAND_GROUP_MEMBER(member, key_type, default_value) \
|
2019-09-25 00:48:07 +00:00
|
|
|
config->member = g_key_file_get_##key_type(key_file, group_name, #member, &error); \
|
2019-09-27 21:10:28 +00:00
|
|
|
if (error != NULL) { \
|
|
|
|
config->member = default_value; \
|
|
|
|
print_config_error(error); \
|
|
|
|
g_clear_error(&error); \
|
|
|
|
}
|
2019-09-25 00:48:07 +00:00
|
|
|
|
|
|
|
EXPAND_CONFIG
|
|
|
|
|
|
|
|
if (config->rewind_duration > 600) {
|
2019-10-14 15:33:03 +00:00
|
|
|
g_warning("Setting Emulation.rewind_duration too high might affect performance.");
|
2019-09-25 00:48:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#undef EXPAND_GROUP
|
|
|
|
#undef EXPAND_GROUP_MEMBER
|
|
|
|
}
|
|
|
|
|
2019-10-14 15:33:03 +00:00
|
|
|
void print_config(config_t *config) {
|
|
|
|
_print_config(config, G_LOG_LEVEL_MESSAGE);
|
|
|
|
}
|
|
|
|
|
2019-09-25 20:47:04 +00:00
|
|
|
void save_config_to_key_file(config_t *config, GKeyFile *key_file) {
|
2019-10-14 15:33:03 +00:00
|
|
|
g_message("Saving config to key file");
|
2019-09-25 20:47:04 +00:00
|
|
|
GError *error = NULL;
|
|
|
|
gchar *group_name;
|
|
|
|
|
|
|
|
#define EXPAND_GROUP(name, members) \
|
|
|
|
group_name = #name; \
|
|
|
|
members
|
|
|
|
|
2019-09-27 21:10:28 +00:00
|
|
|
#define EXPAND_GROUP_MEMBER_IF_0(member, key_type, default_value) \
|
2019-09-25 20:47:04 +00:00
|
|
|
g_key_file_set_##key_type(key_file, group_name, #member, config->member);
|
|
|
|
|
2019-09-27 21:10:28 +00:00
|
|
|
#define EXPAND_GROUP_MEMBER_IF_1(member, key_type, default_value) \
|
2019-09-25 20:47:04 +00:00
|
|
|
if (config->member != NULL) { \
|
|
|
|
g_key_file_set_##key_type(key_file, group_name, #member, config->member); \
|
|
|
|
} \
|
2019-09-26 14:22:30 +00:00
|
|
|
else if (g_key_file_has_key(key_file, group_name, #member, &error)) { \
|
|
|
|
if (error != NULL) { \
|
2019-10-14 15:33:03 +00:00
|
|
|
g_warning("%s", error->message); \
|
2019-09-26 14:22:30 +00:00
|
|
|
g_clear_error(&error); \
|
|
|
|
} \
|
2019-09-25 20:47:04 +00:00
|
|
|
g_key_file_remove_key(key_file, group_name, #member, &error); \
|
|
|
|
if (error != NULL) { \
|
2019-10-14 15:33:03 +00:00
|
|
|
g_warning("%s", error->message); \
|
2019-09-25 20:47:04 +00:00
|
|
|
g_clear_error(&error); \
|
|
|
|
} \
|
|
|
|
}
|
|
|
|
|
2019-09-27 21:10:28 +00:00
|
|
|
#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)
|
2019-09-25 20:47:04 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-09-30 20:30:33 +00:00
|
|
|
void on_preferences_realize(GtkWidget *w, gpointer builder_ptr) {
|
2019-09-27 21:10:28 +00:00
|
|
|
GtkWindow *preferences = GTK_WINDOW(w);
|
|
|
|
GtkBuilder *builder = (GtkBuilder *) builder_ptr;
|
|
|
|
|
|
|
|
update_boot_rom_selector(builder);
|
|
|
|
|
|
|
|
// Hook up the static preferences
|
2019-10-03 01:13:53 +00:00
|
|
|
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "rewind_duration_selector"), itoa(config.rewind_duration));
|
|
|
|
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "dmg_revision_selector"), config.dmg_revision_name);
|
|
|
|
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "sgb_revision_selector"), config.sgb_revision_name);
|
|
|
|
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "cgb_revision_selector"), config.cgb_revision_name);
|
|
|
|
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "shader_selector"), config.shader);
|
|
|
|
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "color_correction_selector"), config.color_correction_id);
|
|
|
|
gtk_toggle_button_set_active(builder_get(GTK_TOGGLE_BUTTON, "integer_scaling_toggle"), config.use_integer_scaling);
|
|
|
|
gtk_toggle_button_set_active(builder_get(GTK_TOGGLE_BUTTON, "aspect_ratio_toggle"), config.keep_aspect_ratio);
|
|
|
|
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "highpass_filter_selector"), config.high_pass_filter_id);
|
2019-10-12 21:11:26 +00:00
|
|
|
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "sample_rate_selector"), itoa(config.sample_rate));
|
2019-09-27 21:10:28 +00:00
|
|
|
|
|
|
|
#if ! NDEBUG
|
2019-10-03 01:13:53 +00:00
|
|
|
gtk_combo_box_set_active_id(builder_get(GTK_COMBO_BOX, "menubar_override_selector"), config.menubar_override);
|
2019-09-27 21:10:28 +00:00
|
|
|
#else
|
2019-10-03 01:13:53 +00:00
|
|
|
if (builder_get(GTK_COMBO_BOX, "menubar_override_selector") != NULL) {
|
|
|
|
gtk_widget_destroy(GTK_WIDGET(builder_get(GTK_COMBO_BOX, "menubar_override_selector")));
|
|
|
|
gtk_widget_destroy(GTK_WIDGET(builder_get(GTK_COMBO_BOX, "menubar_override_selector_label")));
|
2019-09-27 21:10:28 +00:00
|
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2019-09-25 20:47:04 +00:00
|
|
|
void init_settings(gchar *path, GtkWindow *preferences) {
|
2019-09-25 00:48:07 +00:00
|
|
|
free_settings();
|
|
|
|
key_file = g_key_file_new();
|
2019-09-24 14:14:06 +00:00
|
|
|
|
|
|
|
if (path != NULL) {
|
|
|
|
settings_file_path = path;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
settings_file_path = g_build_filename(g_get_user_config_dir(), SETTINGS_FILE, NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
load_settings();
|
|
|
|
}
|
|
|
|
|
2019-09-25 00:48:07 +00:00
|
|
|
int load_settings(void) {
|
2019-09-24 14:14:06 +00:00
|
|
|
GError *error = NULL;
|
|
|
|
|
2019-10-14 15:33:03 +00:00
|
|
|
g_message("Trying to load settings from %s", settings_file_path);
|
2019-09-24 14:14:06 +00:00
|
|
|
|
|
|
|
if (!g_key_file_load_from_file(key_file, settings_file_path, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, &error)) {
|
2019-09-25 00:48:07 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2019-09-24 14:14:06 +00:00
|
|
|
g_error_free(error);
|
|
|
|
}
|
2019-09-25 00:48:07 +00:00
|
|
|
|
|
|
|
load_config_from_key_file(&config, key_file);
|
2019-10-14 15:33:03 +00:00
|
|
|
_print_config(&config, G_LOG_LEVEL_DEBUG);
|
2019-09-25 00:48:07 +00:00
|
|
|
|
|
|
|
return 0;
|
2019-09-24 14:14:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void save_settings(void) {
|
|
|
|
GError *error = NULL;
|
|
|
|
|
2019-10-14 15:33:03 +00:00
|
|
|
g_message("Trying to save settings to %s", settings_file_path);
|
2019-09-24 14:14:06 +00:00
|
|
|
|
2019-09-25 20:47:04 +00:00
|
|
|
save_config_to_key_file(&config, key_file);
|
|
|
|
|
2019-09-24 14:14:06 +00:00
|
|
|
if (!g_key_file_save_to_file(key_file, settings_file_path, &error)) {
|
2019-09-25 00:48:07 +00:00
|
|
|
g_warning ("Failed to save %s: %s", settings_file_path, error->message);
|
2019-09-24 14:14:06 +00:00
|
|
|
g_error_free(error);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2019-09-25 00:48:07 +00:00
|
|
|
|
|
|
|
void free_settings(void) {
|
|
|
|
if (key_file != NULL) {
|
|
|
|
g_key_file_free(key_file);
|
|
|
|
key_file = NULL;
|
|
|
|
}
|
|
|
|
}
|
2019-09-25 20:47:04 +00:00
|
|
|
|
2019-09-27 21:10:28 +00:00
|
|
|
void update_boot_rom_selector(GtkBuilder *builder) {
|
2019-10-03 01:13:53 +00:00
|
|
|
GtkComboBoxText *combo_box = builder_get(GTK_COMBO_BOX_TEXT, "boot_rom_selector");
|
2019-09-27 21:10:28 +00:00
|
|
|
gtk_combo_box_text_remove_all(combo_box);
|
|
|
|
gtk_combo_box_text_append(combo_box, "auto", "Use Built-in Boot ROMs");
|
|
|
|
if (config.boot_rom_path != NULL && !g_str_equal(config.boot_rom_path, "auto") && !g_str_equal(config.boot_rom_path, "other")) {
|
|
|
|
gtk_combo_box_text_append(combo_box, config.boot_rom_path, config.boot_rom_path);
|
|
|
|
gtk_combo_box_set_active_id(GTK_COMBO_BOX(combo_box), config.boot_rom_path);
|
|
|
|
}
|
2019-09-27 22:15:37 +00:00
|
|
|
else {
|
|
|
|
gtk_combo_box_set_active_id(GTK_COMBO_BOX(combo_box), "auto");
|
|
|
|
}
|
2019-09-27 21:10:28 +00:00
|
|
|
gtk_combo_box_text_append_text(combo_box, "<separator>");
|
|
|
|
gtk_combo_box_text_append(combo_box, "other", "Other");
|
|
|
|
}
|
|
|
|
|
2019-09-29 23:49:41 +00:00
|
|
|
enum menubar_type_t get_show_menubar(void) {
|
2019-09-26 14:22:30 +00:00
|
|
|
if (config.menubar_override == NULL) goto default_value;
|
|
|
|
|
2019-09-29 00:07:33 +00:00
|
|
|
if (g_strcmp0(config.menubar_override, "auto") == 0) {
|
|
|
|
return MENUBAR_AUTO;
|
|
|
|
}
|
2019-09-29 23:49:41 +00:00
|
|
|
else if (g_strcmp0(config.menubar_override, "show_in_shell") == 0) {
|
|
|
|
return MENUBAR_SHOW_IN_SHELL;
|
2019-09-25 20:47:04 +00:00
|
|
|
}
|
2019-09-29 23:49:41 +00:00
|
|
|
else if (g_strcmp0(config.menubar_override, "show_in_window") == 0) {
|
|
|
|
return MENUBAR_SHOW_IN_WINDOW;
|
|
|
|
}
|
|
|
|
else if (g_strcmp0(config.menubar_override, "show_hamburger") == 0) {
|
|
|
|
return MENUBAR_SHOW_HAMBURGER;
|
2019-09-25 20:47:04 +00:00
|
|
|
}
|
|
|
|
|
2019-09-26 14:22:30 +00:00
|
|
|
// This should not happen
|
2019-10-14 15:33:03 +00:00
|
|
|
g_warning("Unknown menubar setting: %s\nFalling back to “Auto”", config.menubar_override);
|
2019-09-26 14:22:30 +00:00
|
|
|
default_value: return MENUBAR_AUTO;
|
2019-09-25 20:47:04 +00:00
|
|
|
}
|
|
|
|
|
2019-09-29 23:49:41 +00:00
|
|
|
void set_show_menubar(enum menubar_type_t value) {
|
2019-09-25 20:47:04 +00:00
|
|
|
switch (value) {
|
|
|
|
case MENUBAR_AUTO:
|
|
|
|
config.menubar_override = "auto";
|
|
|
|
break;
|
2019-09-29 23:49:41 +00:00
|
|
|
case MENUBAR_SHOW_IN_SHELL:
|
|
|
|
config.menubar_override = "show_in_shell";
|
|
|
|
break;
|
|
|
|
case MENUBAR_SHOW_IN_WINDOW:
|
|
|
|
config.menubar_override = "show_in_window";
|
2019-09-25 20:47:04 +00:00
|
|
|
break;
|
2019-09-29 23:49:41 +00:00
|
|
|
case MENUBAR_SHOW_HAMBURGER:
|
|
|
|
config.menubar_override = "show_hamburger";
|
2019-09-26 14:22:30 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
GB_color_correction_mode_t get_color_correction_mode(void) {
|
|
|
|
if (config.color_correction_id == NULL) goto default_value;
|
|
|
|
|
|
|
|
if (g_strcmp0(config.color_correction_id, "disabled") == 0) {
|
|
|
|
return GB_COLOR_CORRECTION_DISABLED;
|
|
|
|
}
|
|
|
|
else if (g_strcmp0(config.color_correction_id, "correct_color_curves") == 0) {
|
|
|
|
return GB_COLOR_CORRECTION_CORRECT_CURVES;
|
|
|
|
}
|
|
|
|
else if (g_strcmp0(config.color_correction_id, "emulate_hardware") == 0) {
|
|
|
|
return GB_COLOR_CORRECTION_EMULATE_HARDWARE;
|
|
|
|
}
|
|
|
|
else if (g_strcmp0(config.color_correction_id, "preserve_brightness") == 0) {
|
|
|
|
return GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This should not happen
|
2019-10-14 15:33:03 +00:00
|
|
|
g_warning("Unknown color correction mode: %s\nFalling back to “Emulate Hardware”", config.color_correction_id);
|
2019-09-26 14:22:30 +00:00
|
|
|
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.color_correction_id = "disabled";
|
|
|
|
break;
|
|
|
|
case GB_COLOR_CORRECTION_CORRECT_CURVES:
|
|
|
|
config.color_correction_id = "correct_color_curves";
|
|
|
|
break;
|
|
|
|
case GB_COLOR_CORRECTION_EMULATE_HARDWARE:
|
|
|
|
config.color_correction_id = "emulate_hardware";
|
|
|
|
break;
|
|
|
|
case GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS:
|
|
|
|
config.color_correction_id = "preserve_brightness";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
GB_highpass_mode_t get_highpass_mode(void) {
|
|
|
|
if (config.high_pass_filter_id == NULL) goto default_value;
|
|
|
|
|
|
|
|
if (g_strcmp0(config.high_pass_filter_id, "disabled") == 0) {
|
|
|
|
return GB_HIGHPASS_OFF;
|
|
|
|
}
|
|
|
|
else if (g_strcmp0(config.high_pass_filter_id, "emulate_hardware") == 0) {
|
|
|
|
return GB_HIGHPASS_ACCURATE;
|
|
|
|
}
|
|
|
|
else if (g_strcmp0(config.high_pass_filter_id, "preserve_waveform") == 0) {
|
|
|
|
return GB_HIGHPASS_REMOVE_DC_OFFSET;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This should not happen
|
2019-10-14 15:33:03 +00:00
|
|
|
g_warning("Unknown highpass mode: %s\nFalling back to “Accurate”", config.high_pass_filter_id);
|
2019-09-26 14:22:30 +00:00
|
|
|
default_value: return GB_HIGHPASS_ACCURATE;
|
|
|
|
}
|
|
|
|
|
|
|
|
void set_highpass_mode(GB_highpass_mode_t mode) {
|
|
|
|
switch (mode) {
|
|
|
|
case GB_HIGHPASS_OFF:
|
|
|
|
config.high_pass_filter_id = "disabled";
|
|
|
|
break;
|
|
|
|
case GB_HIGHPASS_MAX:
|
2019-10-14 15:33:03 +00:00
|
|
|
g_warning("GB_HIGHPASS_MAX is not a valid highpass mode, falling back to “Accurate”.");
|
2019-09-26 14:22:30 +00:00
|
|
|
case GB_HIGHPASS_ACCURATE:
|
|
|
|
config.high_pass_filter_id = "emulate_hardware";
|
|
|
|
break;
|
|
|
|
case GB_HIGHPASS_REMOVE_DC_OFFSET:
|
|
|
|
config.high_pass_filter_id = "preserve_waveform";
|
2019-09-25 20:47:04 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2019-10-14 14:01:51 +00:00
|
|
|
|
|
|
|
GB_model_t get_dmg_model(void) {
|
|
|
|
if (config.dmg_revision_name == NULL) goto default_value;
|
|
|
|
|
|
|
|
// TODO: Synchronize with GB_model_t (Core/gb.h)
|
|
|
|
if (g_strcmp0(config.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.sgb_revision_name == NULL) goto default_value;
|
|
|
|
|
|
|
|
// TODO: Synchronize with GB_model_t (Core/gb.h)
|
|
|
|
if (g_strcmp0(config.sgb_revision_name, "SGB1_NTSC") == 0) {
|
|
|
|
return GB_MODEL_SGB_NTSC;
|
|
|
|
}
|
|
|
|
else if (g_strcmp0(config.sgb_revision_name, "SGB1_PAL") == 0) {
|
|
|
|
return GB_MODEL_SGB_PAL;
|
|
|
|
}
|
|
|
|
else if (g_strcmp0(config.sgb_revision_name, "SGB2") == 0) {
|
|
|
|
return GB_MODEL_SGB2;
|
|
|
|
}
|
|
|
|
|
|
|
|
default_value: return GB_MODEL_SGB2;
|
|
|
|
}
|
|
|
|
|
|
|
|
GB_model_t get_cgb_model(void) {
|
|
|
|
if (config.cgb_revision_name == NULL) goto default_value;
|
|
|
|
|
|
|
|
// TODO: Synchronize with GB_model_t (Core/gb.h)
|
|
|
|
if (g_strcmp0(config.cgb_revision_name, "CPU_CGB_C") == 0) {
|
|
|
|
return GB_MODEL_CGB_C;
|
|
|
|
}
|
|
|
|
else if (g_strcmp0(config.cgb_revision_name, "CPU_CGB_E") == 0) {
|
|
|
|
return GB_MODEL_CGB_E;
|
|
|
|
}
|
|
|
|
|
|
|
|
default_value: return GB_MODEL_CGB_E;
|
|
|
|
}
|