/* * stash.c - this file is part of Geany, a fast and lightweight IDE * * Copyright 2008 Nick Treleaven * Copyright 2008 Enrico Tröger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * * $Id$ */ /* Mini-library for reading/writing GKeyFile settings and synchronizing widgets with * C variables. */ /* Terms * 'Setting' is used only for data stored on disk or in memory. * 'Pref' can also include visual widget information. * * Memory Usage * Stash will not duplicate strings if they are normally static arrays, such as * keyfile group names and key names, string default values or widget_id names. * String settings and other dynamically allocated settings must be initialized to NULL. */ /* Implementation Note * We use a GArray to hold prefs. It would be more efficient for user code to declare * a static array of GeanyPrefEntry structs, but we don't do this because: * * * It would be more ugly (lots of casts and NULLs). * * Less type checking. * * The API would have to break when adding/changing fields. * * Usually the prefs code isn't what user code will spend most of its time doing, so this * should be efficient enough. But, if desired we could add a stash_group_set_size() function * to reduce reallocation. * */ #include #include "stash.h" #include "utils.h" /* only for utils_get_setting_*(). Stash should not depend on Geany. */ #define foreach_array(type, item, array) \ foreach_c_array(item, ((type*)(gpointer)array->data), array->len) struct GeanyPrefEntry { GType setting_type; /* e.g. G_TYPE_INT */ gpointer setting; const gchar *key_name; gpointer default_value; GType widget_type; /* e.g. GTK_TYPE_TOGGLE_BUTTON */ gpointer widget_id; /* can be GtkWidget or gchararray */ gpointer fields; /* extra fields */ }; struct GeanyPrefGroup { const gchar *name; /* group name to use in the keyfile */ GArray *entries; /* array of GeanyPrefEntry */ gboolean write_once; /* only write settings if they don't already exist */ gboolean use_defaults; /* use default values if there's no keyfile entry */ }; typedef struct EnumWidget { gpointer widget_id; gint enum_id; } EnumWidget; typedef enum SettingAction { SETTING_READ, SETTING_WRITE } SettingAction; typedef enum PrefAction { PREF_DISPLAY, PREF_UPDATE } PrefAction; static void handle_boolean_setting(GeanyPrefGroup *group, GeanyPrefEntry *se, GKeyFile *config, SettingAction action) { gboolean *setting = se->setting; switch (action) { case SETTING_READ: *setting = utils_get_setting_boolean(config, group->name, se->key_name, GPOINTER_TO_INT(se->default_value)); break; case SETTING_WRITE: g_key_file_set_boolean(config, group->name, se->key_name, *setting); break; } } static void handle_integer_setting(GeanyPrefGroup *group, GeanyPrefEntry *se, GKeyFile *config, SettingAction action) { gboolean *setting = se->setting; switch (action) { case SETTING_READ: *setting = utils_get_setting_integer(config, group->name, se->key_name, GPOINTER_TO_INT(se->default_value)); break; case SETTING_WRITE: g_key_file_set_integer(config, group->name, se->key_name, *setting); break; } } static void handle_string_setting(GeanyPrefGroup *group, GeanyPrefEntry *se, GKeyFile *config, SettingAction action) { gchararray *setting = se->setting; switch (action) { case SETTING_READ: g_free(*setting); *setting = utils_get_setting_string(config, group->name, se->key_name, se->default_value); break; case SETTING_WRITE: g_key_file_set_string(config, group->name, se->key_name, *setting); break; } } static void handle_strv_setting(GeanyPrefGroup *group, GeanyPrefEntry *se, GKeyFile *config, SettingAction action) { gchararray **setting = se->setting; switch (action) { case SETTING_READ: g_strfreev(*setting); *setting = g_key_file_get_string_list(config, group->name, se->key_name, NULL, NULL); if (*setting == NULL) *setting = g_strdupv(se->default_value); break; case SETTING_WRITE: { /* don't try to save a NULL string vector */ gchar *dummy[] = { "", NULL }; gchar **strv = *setting ? *setting : dummy; g_key_file_set_string_list(config, group->name, se->key_name, (const gchar**)strv, g_strv_length(strv)); break; } } } static void keyfile_action(SettingAction action, GeanyPrefGroup *group, GKeyFile *keyfile) { GeanyPrefEntry *entry; foreach_array(GeanyPrefEntry, entry, group->entries) { /* don't overwrite write_once prefs */ if (group->write_once && action == SETTING_WRITE && g_key_file_has_key(keyfile, group->name, entry->key_name, NULL)) continue; /* don't override settings with default values */ if (!group->use_defaults && action == SETTING_READ && !g_key_file_has_key(keyfile, group->name, entry->key_name, NULL)) continue; switch (entry->setting_type) { case G_TYPE_BOOLEAN: handle_boolean_setting(group, entry, keyfile, action); break; case G_TYPE_INT: handle_integer_setting(group, entry, keyfile, action); break; case G_TYPE_STRING: handle_string_setting(group, entry, keyfile, action); break; default: /* G_TYPE_STRV is not a constant */ if (entry->setting_type == G_TYPE_STRV) handle_strv_setting(group, entry, keyfile, action); else g_warning("Unhandled type for %s::%s in %s()!", group->name, entry->key_name, G_STRFUNC); } } } void stash_group_load_from_key_file(GeanyPrefGroup *group, GKeyFile *keyfile) { keyfile_action(SETTING_READ, group, keyfile); } void stash_group_save_to_key_file(GeanyPrefGroup *group, GKeyFile *keyfile) { keyfile_action(SETTING_WRITE, group, keyfile); } GeanyPrefGroup *stash_group_new(const gchar *name) { GeanyPrefGroup *group = g_new0(GeanyPrefGroup, 1); group->name = name; group->entries = g_array_new(FALSE, FALSE, sizeof(GeanyPrefEntry)); group->use_defaults = TRUE; return group; } void stash_group_free(GeanyPrefGroup *group) { GeanyPrefEntry *entry; foreach_array(GeanyPrefEntry, entry, group->entries) { if (entry->widget_type == GTK_TYPE_RADIO_BUTTON) g_free(entry->fields); else g_assert(entry->fields == NULL); } g_array_free(group->entries, TRUE); g_free(group); } /* Useful so the user can edit the keyfile manually while the program is running, * and the setting won't be overridden. * @c FALSE by default. */ void stash_group_set_write_once(GeanyPrefGroup *group, gboolean write_once) { group->write_once = write_once; } /* When @c FALSE, Stash doesn't change the setting if there is no keyfile entry, so it * remains whatever it was initialized/set to by user code. * @c TRUE by default. */ void stash_group_set_use_defaults(GeanyPrefGroup *group, gboolean use_defaults) { group->use_defaults = use_defaults; } static GeanyPrefEntry * add_pref(GeanyPrefGroup *group, GType type, gpointer setting, const gchar *key_name, gpointer default_value) { GeanyPrefEntry entry = {type, setting, key_name, default_value, G_TYPE_NONE, NULL, NULL}; GArray *array = group->entries; g_array_append_val(array, entry); return &g_array_index(array, GeanyPrefEntry, array->len - 1); } void stash_group_add_boolean(GeanyPrefGroup *group, gboolean *setting, const gchar *key_name, gboolean default_value) { add_pref(group, G_TYPE_BOOLEAN, setting, key_name, GINT_TO_POINTER(default_value)); } void stash_group_add_integer(GeanyPrefGroup *group, gint *setting, const gchar *key_name, gint default_value) { add_pref(group, G_TYPE_INT, setting, key_name, GINT_TO_POINTER(default_value)); } /* The contents of @a setting will be freed before being replaced, so make sure it is * allocated, or initialized to @c NULL. * @param default_value Not duplicated. */ void stash_group_add_string(GeanyPrefGroup *group, gchar **setting, const gchar *key_name, const gchar *default_value) { add_pref(group, G_TYPE_STRING, setting, key_name, (gpointer)default_value); } /* The contents of @a setting will be freed before being replaced, so make sure it is * allocated, or initialized to @c NULL. * @param default_value Not duplicated. */ void stash_group_add_string_vector(GeanyPrefGroup *group, gchar ***setting, const gchar *key_name, const gchar **default_value) { add_pref(group, G_TYPE_STRV, setting, key_name, (gpointer)default_value); } /* *** GTK-related functions *** */ static void handle_toggle_button(GtkWidget *widget, gboolean *setting, PrefAction action) { switch (action) { case PREF_DISPLAY: gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), *setting); break; case PREF_UPDATE: *setting = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)); break; } } static void handle_spin_button(GtkWidget *widget, GeanyPrefEntry *entry, PrefAction action) { gint *setting = entry->setting; g_assert(entry->setting_type == G_TYPE_INT); /* only int spin prefs */ switch (action) { case PREF_DISPLAY: gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget), *setting); break; case PREF_UPDATE: *setting = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(widget)); break; } } static void handle_combo_box(GtkWidget *widget, GeanyPrefEntry *entry, PrefAction action) { gint *setting = entry->setting; switch (action) { case PREF_DISPLAY: gtk_combo_box_set_active(GTK_COMBO_BOX(widget), *setting); break; case PREF_UPDATE: *setting = gtk_combo_box_get_active(GTK_COMBO_BOX(widget)); break; } } static void handle_entry(GtkWidget *widget, GeanyPrefEntry *entry, PrefAction action) { gchararray *setting = entry->setting; switch (action) { case PREF_DISPLAY: gtk_entry_set_text(GTK_ENTRY(widget), *setting); break; case PREF_UPDATE: g_free(*setting); *setting = g_strdup(gtk_entry_get_text(GTK_ENTRY(widget))); break; } } static void handle_combo_box_entry(GtkWidget *widget, GeanyPrefEntry *entry, PrefAction action) { widget = gtk_bin_get_child(GTK_BIN(widget)); handle_entry(widget, entry, action); } /* taken from Glade 2.x generated support.c */ static GtkWidget* lookup_widget (GtkWidget *widget, const gchar *widget_name) { GtkWidget *parent, *found_widget; for (;;) { if (GTK_IS_MENU (widget)) parent = gtk_menu_get_attach_widget (GTK_MENU (widget)); else parent = widget->parent; if (!parent) parent = (GtkWidget*) g_object_get_data (G_OBJECT (widget), "GladeParentKey"); if (parent == NULL) break; widget = parent; } found_widget = (GtkWidget*) g_object_get_data (G_OBJECT (widget), widget_name); if (!found_widget) g_warning ("Widget not found: %s", widget_name); return found_widget; } static GtkWidget * get_widget(GtkWidget *owner, gpointer widget_id) { GtkWidget *widget = widget_id; if (owner) { const gchar *widget_name = widget_id; widget = lookup_widget(owner, widget_name); } if (!GTK_IS_WIDGET(widget)) { g_warning("Unknown widget in %s()!", G_STRFUNC); return NULL; } return widget; } static void handle_radio_button(GtkWidget *widget, gint enum_id, gboolean *setting, PrefAction action) { switch (action) { case PREF_DISPLAY: if (*setting == enum_id) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), TRUE); break; case PREF_UPDATE: if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) *setting = enum_id; break; } } static void handle_radio_buttons(GtkWidget *owner, EnumWidget *fields, gboolean *setting, PrefAction action) { EnumWidget *field = fields; gsize count = 0; GtkWidget *widget = NULL; while (1) { widget = get_widget(owner, field->widget_id); if (!widget) continue; count++; handle_radio_button(widget, field->enum_id, setting, action); field++; if (!field->widget_id) break; } if (g_slist_length(gtk_radio_button_get_group(GTK_RADIO_BUTTON(widget))) != count) g_warning("Missing/invalid radio button widget IDs found!"); } static void pref_action(PrefAction action, GeanyPrefGroup *group, GtkWidget *owner) { GeanyPrefEntry *entry; foreach_array(GeanyPrefEntry, entry, group->entries) { GtkWidget *widget; if (entry->widget_type == G_TYPE_NONE) continue; if (entry->widget_type == GTK_TYPE_RADIO_BUTTON) { handle_radio_buttons(owner, entry->fields, entry->setting, action); continue; } widget = get_widget(owner, entry->widget_id); if (!widget) { g_warning("Unknown widget for %s::%s in %s()!", group->name, entry->key_name, G_STRFUNC); continue; } /* note: can't use switch for GTK_TYPE macros */ if (entry->widget_type == GTK_TYPE_TOGGLE_BUTTON) handle_toggle_button(widget, entry->setting, action); else if (entry->widget_type == GTK_TYPE_SPIN_BUTTON) handle_spin_button(widget, entry, action); else if (entry->widget_type == GTK_TYPE_COMBO_BOX) handle_combo_box(widget, entry, action); else if (entry->widget_type == GTK_TYPE_COMBO_BOX_ENTRY) handle_combo_box_entry(widget, entry, action); else if (entry->widget_type == GTK_TYPE_ENTRY) handle_entry(widget, entry, action); else g_warning("Unhandled type for %s::%s in %s()!", group->name, entry->key_name, G_STRFUNC); } } /** @param owner If non-NULL, used to lookup widgets by name. */ void stash_group_display(GeanyPrefGroup *group, GtkWidget *owner) { pref_action(PREF_DISPLAY, group, owner); } void stash_group_update(GeanyPrefGroup *group, GtkWidget *owner) { pref_action(PREF_UPDATE, group, owner); } static GeanyPrefEntry * add_widget_pref(GeanyPrefGroup *group, GType setting_type, gpointer setting, const gchar *key_name, gpointer default_value, GType widget_type, gpointer widget_id) { GeanyPrefEntry *entry = add_pref(group, setting_type, setting, key_name, default_value); entry->widget_type = widget_type; entry->widget_id = widget_id; return entry; } /* Used for GtkCheckButton or GtkToggleButton widgets. * @see stash_group_add_radio_buttons(). */ void stash_group_add_toggle_button(GeanyPrefGroup *group, gboolean *setting, const gchar *key_name, gboolean default_value, gpointer widget_id) { add_widget_pref(group, G_TYPE_BOOLEAN, setting, key_name, GINT_TO_POINTER(default_value), GTK_TYPE_TOGGLE_BUTTON, widget_id); } /* @param ... pairs of widget_id, enum_id. * Example (using widget lookup strings, but widget pointers can also work): * @code * enum {FOO, BAR}; * stash_group_add_radio_buttons(group, &which_one_setting, "which_one", BAR, * "radio_foo", FOO, "radio_bar", BAR, NULL); * @endcode */ void stash_group_add_radio_buttons(GeanyPrefGroup *group, gint *setting, const gchar *key_name, gint default_value, gpointer widget_id, gint enum_id, ...) { GeanyPrefEntry *entry = add_widget_pref(group, G_TYPE_INT, setting, key_name, GINT_TO_POINTER(default_value), GTK_TYPE_RADIO_BUTTON, NULL); va_list args; gsize count = 1; EnumWidget *item, *array; /* count pairs of args */ va_start(args, enum_id); while (1) { gint dummy; if (!va_arg(args, gpointer)) break; dummy = va_arg(args, gint); count++; } va_end(args); array = g_new0(EnumWidget, count + 1); entry->fields = array; va_start(args, enum_id); foreach_c_array(item, array, count) { if (item == array) { /* first element */ item->widget_id = widget_id; item->enum_id = enum_id; } else { item->widget_id = va_arg(args, gpointer); item->enum_id = va_arg(args, gint); } } va_end(args); } void stash_group_add_spin_button_integer(GeanyPrefGroup *group, gint *setting, const gchar *key_name, gint default_value, gpointer widget_id) { add_widget_pref(group, G_TYPE_INT, setting, key_name, GINT_TO_POINTER(default_value), GTK_TYPE_SPIN_BUTTON, widget_id); } /* @see stash_group_add_combo_box_entry(). */ void stash_group_add_combo_box(GeanyPrefGroup *group, gint *setting, const gchar *key_name, gint default_value, gpointer widget_id) { add_widget_pref(group, G_TYPE_INT, setting, key_name, GINT_TO_POINTER(default_value), GTK_TYPE_COMBO_BOX, widget_id); } /* We could maybe also have something like stash_group_add_combo_box_entry_with_menu() * for the history list - or should that be stored as a separate setting? */ void stash_group_add_combo_box_entry(GeanyPrefGroup *group, gchar **setting, const gchar *key_name, const gchar *default_value, gpointer widget_id) { add_widget_pref(group, G_TYPE_STRING, setting, key_name, (gpointer)default_value, GTK_TYPE_COMBO_BOX_ENTRY, widget_id); } void stash_group_add_entry(GeanyPrefGroup *group, gchar **setting, const gchar *key_name, const gchar *default_value, gpointer widget_id) { add_widget_pref(group, G_TYPE_STRING, setting, key_name, (gpointer)default_value, GTK_TYPE_ENTRY, widget_id); }