/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * * Authors: Michael Zucchi <notzed@ximian.com> * * Copyright 2004 Novell Inc. (www.novell.com) * * This program is free software; you can redistribute it and/or * modify it under the terms of version 2 of the GNU General Public * License as published by the Free Software Foundation. * * 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., 59 Temple Street #330, Boston, MA 02111-1307, USA. * */ #ifdef HAVE_CONFIG_H #include <config.h> #endif #include <string.h> #include <sys/types.h> #include <dirent.h> #include <libxml/parser.h> #include <libxml/xmlmemory.h> #include <glib.h> #include <gtk/gtkbutton.h> #include <gtk/gtkhbox.h> #include <gtk/gtkstock.h> #include <gtk/gtkdialog.h> #include <gtk/gtkwindow.h> #include <gtk/gtklabel.h> #include <gtk/gtkimage.h> #include <libgnome/gnome-i18n.h> #include <libgnome/gnome-url.h> #include "e-error.h" #define d(x) struct _e_error_button { struct _e_error_button *next; char *stock; char *label; int response; }; struct _e_error { guint32 flags; char *id; int type; int default_response; char *title; char *primary; char *secondary; char *help_uri; struct _e_error_button *buttons; }; struct _e_error_table { char *domain; char *translation_domain; GHashTable *errors; }; static GHashTable *error_table; static GSList *ee_parent_list; /* ********************************************************************** */ static struct _e_error_button default_ok_button = { NULL, "gtk-ok", NULL, GTK_RESPONSE_OK }; static struct _e_error default_errors[] = { { GTK_DIALOG_MODAL, "error", 3, GTK_RESPONSE_OK, N_("Evolution Error"), "{0}", "{1}", NULL, &default_ok_button }, { GTK_DIALOG_MODAL, "error-primary", 3, GTK_RESPONSE_OK, N_("Evolution Error"), "{0}", NULL, NULL, &default_ok_button }, { GTK_DIALOG_MODAL, "warning", 1, GTK_RESPONSE_OK, N_("Evolution Warning"), "{0}", "{1}", NULL, &default_ok_button }, { GTK_DIALOG_MODAL, "warning-primary", 1, GTK_RESPONSE_OK, N_("Evolution Warning"), "{0}", NULL, NULL, &default_ok_button }, }; /* ********************************************************************** */ static struct { char *name; int id; } response_map[] = { { "GTK_RESPONSE_REJECT", GTK_RESPONSE_REJECT }, { "GTK_RESPONSE_ACCEPT", GTK_RESPONSE_ACCEPT }, { "GTK_RESPONSE_OK", GTK_RESPONSE_OK }, { "GTK_RESPONSE_CANCEL", GTK_RESPONSE_CANCEL }, { "GTK_RESPONSE_CLOSE", GTK_RESPONSE_CLOSE }, { "GTK_RESPONSE_YES", GTK_RESPONSE_YES }, { "GTK_RESPONSE_NO", GTK_RESPONSE_NO }, { "GTK_RESPONSE_APPLY", GTK_RESPONSE_APPLY }, { "GTK_RESPONSE_HELP", GTK_RESPONSE_HELP }, }; static int map_response(const char *name) { int i; for (i=0;i<sizeof(response_map)/sizeof(response_map[0]);i++) if (!strcmp(name, response_map[i].name)) return response_map[i].id; return 0; } static struct { const char *name; const char *icon; const char *title; } type_map[] = { { "info", GTK_STOCK_DIALOG_INFO, N_("Evolution Information") }, { "warning", GTK_STOCK_DIALOG_WARNING, N_("Evolution Warning") }, { "question", GTK_STOCK_DIALOG_QUESTION, N_("Evolution Query") }, { "error", GTK_STOCK_DIALOG_ERROR, N_("Evolution Error") }, }; static int map_type(const char *name) { int i; if (name) { for (i=0;i<sizeof(type_map)/sizeof(type_map[0]);i++) if (!strcmp(name, type_map[i].name)) return i; } return 3; } /* XML format: <error id="error-id" type="info|warning|question|error"? response="default_response"? modal="true"? > <title>Window Title</title>? <primary>Primary error text.</primary>? <secondary>Secondary error text.</secondary>? <help uri="help uri"/> ? <button stock="stock-button-id"? label="button label"? response="response_id"? /> * </error> The tool e-error-tool is used to extract the translatable strings for translation. */ static void ee_load(const char *path) { xmlDocPtr doc; xmlNodePtr root, error, scan; struct _e_error *e; struct _e_error_button *lastbutton; struct _e_error_table *table; char *tmp; d(printf("loading error file %s\n", path)); doc = xmlParseFile(path); if (doc == NULL) { g_warning("Error file '%s' not found", path); return; } root = xmlDocGetRootElement(doc); if (root == NULL || strcmp(root->name, "error-list") != 0 || (tmp = xmlGetProp(root, "domain")) == NULL) { g_warning("Error file '%s' invalid format", path); xmlFreeDoc(doc); return; } table = g_hash_table_lookup(error_table, tmp); if (table == NULL) { char *tmp2; table = g_malloc0(sizeof(*table)); table->domain = g_strdup(tmp); table->errors = g_hash_table_new(g_str_hash, g_str_equal); g_hash_table_insert(error_table, table->domain, table); tmp2 = xmlGetProp(root, "translation-domain"); if (tmp2) { table->translation_domain = g_strdup(tmp2); xmlFree(tmp2); tmp2 = xmlGetProp(root, "translation-localedir"); if (tmp2) { bindtextdomain(table->translation_domain, tmp2); xmlFree(tmp2); } } } else g_warning("Error file '%s', domain '%s' already used, merging", path, tmp); xmlFree(tmp); for (error = root->children;error;error = error->next) { if (!strcmp(error->name, "error")) { tmp = xmlGetProp(error, "id"); if (tmp == NULL) continue; e = g_malloc0(sizeof(*e)); e->id = g_strdup(tmp); xmlFree(tmp); lastbutton = (struct _e_error_button *)&e->buttons; tmp = xmlGetProp(error, "modal"); if (tmp) { if (!strcmp(tmp, "true")) e->flags |= GTK_DIALOG_MODAL; xmlFree(tmp); } tmp = xmlGetProp(error, "type"); e->type = map_type(tmp); if (tmp) xmlFree(tmp); tmp = xmlGetProp(error, "default"); if (tmp) { e->default_response = map_response(tmp); xmlFree(tmp); } for (scan = error->children;scan;scan=scan->next) { if (!strcmp(scan->name, "primary")) { if ((tmp = xmlNodeGetContent(scan))) { e->primary = g_strdup(dgettext(table->translation_domain, tmp)); xmlFree(tmp); } } else if (!strcmp(scan->name, "secondary")) { if ((tmp = xmlNodeGetContent(scan))) { e->secondary = g_strdup(dgettext(table->translation_domain, tmp)); xmlFree(tmp); } } else if (!strcmp(scan->name, "title")) { if ((tmp = xmlNodeGetContent(scan))) { e->title = g_strdup(dgettext(table->translation_domain, tmp)); xmlFree(tmp); } } else if (!strcmp(scan->name, "help")) { tmp = xmlGetProp(scan, "uri"); if (tmp) { e->help_uri = g_strdup(tmp); xmlFree(tmp); } } else if (!strcmp(scan->name, "button")) { struct _e_error_button *b; b = g_malloc0(sizeof(*b)); tmp = xmlGetProp(scan, "stock"); if (tmp) { b->stock = g_strdup(tmp); xmlFree(tmp); } tmp = xmlGetProp(scan, "label"); if (tmp) { b->label = g_strdup(dgettext(table->translation_domain, tmp)); xmlFree(tmp); } tmp = xmlGetProp(scan, "response"); if (tmp) { b->response = map_response(tmp); xmlFree(tmp); } if (b->stock == NULL && b->label == NULL) { g_warning("Error file '%s': missing button details in error '%s'", path, e->id); g_free(b->stock); g_free(b->label); g_free(b); } else { lastbutton->next = b; lastbutton = b; } } } g_hash_table_insert(table->errors, e->id, e); } } xmlFreeDoc(doc); } static void ee_load_tables(void) { DIR *dir; struct dirent *d; const char *base = EVOLUTION_PRIVDATADIR "/errors"; struct _e_error_table *table; int i; if (error_table != NULL) return; error_table = g_hash_table_new(g_str_hash, g_str_equal); /* setup system error types */ table = g_malloc0(sizeof(*table)); table->domain = "builtin"; table->errors = g_hash_table_new(g_str_hash, g_str_equal); for (i=0;i<sizeof(default_errors)/sizeof(default_errors[0]);i++) g_hash_table_insert(table->errors, default_errors[i].id, &default_errors[i]); g_hash_table_insert(error_table, table->domain, table); /* look for installed error tables */ dir = opendir(base); if (dir == NULL) return; while ( (d = readdir(dir)) ) { char *path; if (d->d_name[0] == '.') continue; path = g_build_filename(base, d->d_name, NULL); ee_load(path); g_free(path); } closedir(dir); } /* unfortunately, gmarkup_escape doesn't expose its gstring based api :( */ static void ee_append_text(GString *out, const char *text) { char c; while ( (c=*text++) ) { if (c == '<') g_string_append(out, "<"); else if (c == '>') g_string_append(out, ">"); else if (c == '"') g_string_append(out, """); else if (c == '\'') g_string_append(out, "'"); else if (c == '&') g_string_append(out, "&"); else g_string_append_c(out, c); } } static void ee_build_label(GString *out, const char *fmt, GPtrArray *args) { const char *end, *newstart; int id; while (fmt && (newstart = strchr(fmt, '{')) && (end = strchr(newstart+1, '}'))) { g_string_append_len(out, fmt, newstart-fmt); id = atoi(newstart+1); if (id < args->len) ee_append_text(out, args->pdata[id]); else g_warning("Error references argument %d not supplied by caller", id); fmt = end+1; } g_string_append(out, fmt); } static void ee_response(GtkWidget *w, guint button, struct _e_error *e) { GError *err = NULL; if (button == GTK_RESPONSE_HELP) { g_signal_stop_emission_by_name(w, "response"); gnome_url_show(e->help_uri, &err); if (err) { g_warning("Unable to run help uri: %s", err->message); g_error_free(err); } } } GtkWidget * e_error_newv(GtkWindow *parent, const char *tag, const char *arg0, va_list ap) { struct _e_error_table *table; struct _e_error *e; struct _e_error_button *b; GtkWidget *hbox, *w; char *tmp, *domain, *id; GString *out; GPtrArray *args; GtkDialog *dialog; if (error_table == NULL) ee_load_tables(); dialog = (GtkDialog *)gtk_dialog_new(); gtk_dialog_set_has_separator(dialog, FALSE); gtk_widget_ensure_style ((GtkWidget *)dialog); gtk_container_set_border_width ((GtkContainer *)(dialog->vbox), 0); gtk_container_set_border_width ((GtkContainer *)(dialog->action_area), 12); if (parent == NULL && ee_parent_list) parent = (GtkWindow *)ee_parent_list->data; if (parent) gtk_window_set_transient_for((GtkWindow *)dialog, parent); else g_warning("No parent set, or default parent available for error dialog"); domain = alloca(strlen(tag)+1); strcpy(domain, tag); id = strchr(domain, ':'); if (id) *id++ = 0; if ( id == NULL || (table = g_hash_table_lookup(error_table, domain)) == NULL || (e = g_hash_table_lookup(table->errors, id)) == NULL) { /* setup a dummy error */ tmp = g_strdup_printf(_("<span weight=\"bold\">Internal error, unknown error '%s' requested</span>"), tag); w = gtk_label_new(NULL); gtk_label_set_selectable((GtkLabel *)w, TRUE); gtk_label_set_line_wrap((GtkLabel *)w, TRUE); gtk_label_set_markup((GtkLabel *)w, tmp); gtk_widget_show(w); gtk_box_pack_start((GtkBox *)dialog->vbox, w, TRUE, TRUE, 12); return (GtkWidget *)dialog; } if (e->flags & GTK_DIALOG_MODAL) gtk_window_set_modal((GtkWindow *)dialog, TRUE); gtk_window_set_destroy_with_parent((GtkWindow *)dialog, TRUE); if (e->help_uri) { w = gtk_dialog_add_button(dialog, GTK_STOCK_HELP, GTK_RESPONSE_HELP); g_signal_connect(dialog, "response", G_CALLBACK(ee_response), e); } b = e->buttons; if (b == NULL) { gtk_dialog_add_button(dialog, GTK_STOCK_OK, GTK_RESPONSE_OK); } else { for (b = e->buttons;b;b=b->next) { if (b->stock) { if (b->label) { #if 0 /* FIXME: So although this looks like it will work, it wont. Need to do it the hard way ... it also breaks the default_response stuff */ w = gtk_button_new_from_stock(b->stock); gtk_button_set_label((GtkButton *)w, b->label); gtk_widget_show(w); gtk_dialog_add_action_widget(dialog, w, b->response); #endif gtk_dialog_add_button(dialog, b->label, b->response); } else gtk_dialog_add_button(dialog, b->stock, b->response); } else gtk_dialog_add_button(dialog, b->label, b->response); } } if (e->default_response) gtk_dialog_set_default_response(dialog, e->default_response); hbox = gtk_hbox_new(FALSE, 0); gtk_container_set_border_width((GtkContainer *)hbox, 12); w = gtk_image_new_from_stock(type_map[e->type].icon, GTK_ICON_SIZE_DIALOG); gtk_misc_set_alignment((GtkMisc *)w, 0.0, 0.0); gtk_box_pack_start((GtkBox *)hbox, w, TRUE, TRUE, 12); args = g_ptr_array_new(); tmp = (char *)arg0; while (tmp) { g_ptr_array_add(args, tmp); tmp = va_arg(ap, char *); } out = g_string_new(""); if (e->title) { ee_build_label(out, dgettext(table->translation_domain, e->title), args); gtk_window_set_title((GtkWindow *)dialog, out->str); g_string_truncate(out, 0); } else gtk_window_set_title((GtkWindow *)dialog, dgettext(table->translation_domain, type_map[e->type].title)); if (e->primary) { g_string_append(out, "<span weight=\"bold\" size=\"larger\">"); ee_build_label(out, dgettext(table->translation_domain, e->primary), args); g_string_append(out, "</span>\n\n"); } if (e->secondary) ee_build_label(out, dgettext(table->translation_domain, e->secondary), args); g_ptr_array_free(args, TRUE); w = gtk_label_new(NULL); gtk_label_set_selectable((GtkLabel *)w, TRUE); gtk_label_set_line_wrap((GtkLabel *)w, TRUE); gtk_label_set_markup((GtkLabel *)w, out->str); g_string_free(out, TRUE); gtk_box_pack_start((GtkBox *)hbox, w, FALSE, FALSE, 0); gtk_widget_show_all(hbox); gtk_box_pack_start((GtkBox *)dialog->vbox, hbox, TRUE, TRUE, 0); return (GtkWidget *)dialog; } /** * e_error_new: * @parent: * @tag: error identifier * @arg0: The first argument for the error formatter. The list must * be NULL terminated. * * Creates a new error widget. The @tag argument is used to determine * which error to use, it is in the format domain:error-id. The NULL * terminated list of arguments, starting with @arg0 is used to fill * out the error definition. * * Return value: A GtkDialog which can be used for showing an error * dialog asynchronously. **/ struct _GtkWidget * e_error_new(struct _GtkWindow *parent, const char *tag, const char *arg0, ...) { GtkWidget *w; va_list ap; va_start(ap, arg0); w = e_error_newv(parent, tag, arg0, ap); va_end(ap); return w; } int e_error_runv(GtkWindow *parent, const char *tag, const char *arg0, va_list ap) { GtkWidget *w; int res; w = e_error_newv(parent, tag, arg0, ap); res = gtk_dialog_run((GtkDialog *)w); gtk_widget_destroy(w); return res; } /** * e_error_run: * @parent: * @tag: * @arg0: * * Sets up, displays, runs and destroys a standard evolution error * dialog based on @tag, which is in the format domain:error-id. * * Return value: The response id of the button pressed. **/ int e_error_run(GtkWindow *parent, const char *tag, const char *arg0, ...) { GtkWidget *w; va_list ap; int res; va_start(ap, arg0); w = e_error_newv(parent, tag, arg0, ap); va_end(ap); res = gtk_dialog_run((GtkDialog *)w); gtk_widget_destroy(w); return res; } static void remove_parent(GtkWidget *w, GtkWidget *parent) { ee_parent_list = g_slist_remove(ee_parent_list, parent); } /** * e_error_default_parent: * @parent: * * Bit of a hack, set a default parent that will be used to parent any * error boxes if none is supplied. * * This may be called multiple times, and the last call will be the * main default. This function will keep track of the parents * destruction state. **/ void e_error_default_parent(struct _GtkWindow *parent) { if (g_slist_find(ee_parent_list, parent) == NULL) { ee_parent_list = g_slist_prepend(ee_parent_list, parent); g_signal_connect(parent, "destroy", G_CALLBACK(remove_parent), parent); } }