/* Gtk+ User Interface Builder * Copyright (C) 1998 Damon Chaplin * * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "gladeconfig.h" #include #include #include #include #include #ifndef _WIN32 #include #endif #include #include #include #include "glade_project.h" #include "gbwidget.h" #include "save.h" #include "utils.h" /* This is set if we are saving a session. A bit of a hack. */ char *GladeSessionFile = NULL; /* The stuff to output at the start of XML files. */ static gchar *GLADE_XML_BEGIN = " \n" "\n\n"; static gchar *GLADE_PROJECT_XML_BEGIN = " \n" "\n\n"; /* An internal struct to pass data to the save_component() and save_named_style_callback() callbacks. */ typedef struct _GladeSaveCallbackData GladeSaveCallbackData; struct _GladeSaveCallbackData { GbWidgetGetArgData *data; FILE *fp; }; /* An internal struct to pass data to the save_requires_tags_cb(). */ typedef struct _GladeSaveRequiresData GladeSaveRequiresData; struct _GladeSaveRequiresData { /* We only support these 4 libs so we just have flags for each of them. */ gboolean require_gnome; gboolean require_gnome_canvas; gboolean require_gnomedb; gboolean require_bonobo; }; static GladeError* save_project_file_internal (GladeProject *project); static GladeError* save_xml_file_internal (GladeProject *project); static void save_requires_tags (FILE *fp, GladeProject *project); static void save_component (GtkWidget * item, GladeSaveCallbackData * save_data); #ifdef GLADE_STYLE_SUPPORT static void save_named_style_callback (const gchar * name, GbStyle * gbstyle, GladeSaveCallbackData * save_data); static void save_named_style (const gchar * name, GbStyle * gbstyle, GbWidgetGetArgData * data, FILE *fp); #endif static void save_buffer_flush (GbWidgetGetArgData * data, FILE *fp); static void save_translatable_strings (GbWidgetGetArgData * data); /* We need this to make sure that numbers are output in a portable syntax, instead of using the current locale. This code is from glibc info docs. */ GladeError* save_project_file (GladeProject *project) { gchar *old_locale, *saved_locale; GladeError *error; old_locale = setlocale (LC_NUMERIC, NULL); saved_locale = g_strdup (old_locale); setlocale (LC_NUMERIC, "C"); error = save_project_file_internal (project); if (!error) error = save_xml_file_internal (project); setlocale (LC_NUMERIC, saved_locale); g_free (saved_locale); return error; } /* Backup the file, to .bak, if it exists. */ static GladeError* backup_file (const gchar *filename) { gchar *backup_filename; GladeError *error = NULL; int status; if (!glade_util_file_exists (filename)) return NULL; backup_filename = g_strdup_printf ("%s.bak", filename); #if defined (__EMX__) || defined (_WIN32) /* for OS/2 rename dosn't work if the dest. file exist ! remove it! */ status = remove (backup_filename); #endif status = rename (filename, backup_filename); if (status == -1) { error = glade_error_new_system (_("Couldn't rename file:\n %s\nto:\n %s\n"), filename, backup_filename); } g_free (backup_filename); return error; } /* The main function to output the XML. It creates and initializes the GbWidgetGetArgData, opens the file, outputs the project options, the named styles, and then each component (window/dialog) in the interface. */ static GladeError* save_project_file_internal (GladeProject *project) { gchar *xml_filename, *filename; GladeError *error; FILE *fp; if (GladeSessionFile) xml_filename = GladeSessionFile; else xml_filename = glade_project_get_xml_filename (project); filename = g_strdup_printf ("%sp", xml_filename); if (!GladeSessionFile) { error = backup_file (filename); if (error) goto out; } fp = glade_util_fopen (filename, "w"); if (fp == NULL) { error = glade_error_new_system (_("Couldn't create file:\n %s\n"), filename); goto out; } fprintf (fp, "%s", GLADE_PROJECT_XML_BEGIN); error = glade_project_save_options (project, fp); fclose (fp); out: g_free (filename); return error; } /* The main function to output the XML. It creates and initializes the GbWidgetGetArgData, opens the file, outputs the project options, the named styles, and then each component (window/dialog) in the interface. */ static GladeError* save_xml_file_internal (GladeProject *project) { GbWidgetGetArgData data = { 0 }; GladeSaveCallbackData save_data; gchar *filename; FILE *fp; MSG ("Saving project"); data.project = project; data.action = GB_SAVING; data.copying_to_clipboard = FALSE; data.error = NULL; if (GladeSessionFile) filename = GladeSessionFile; else filename = glade_project_get_xml_filename (project); if (!GladeSessionFile) { data.error = backup_file (filename); if (data.error) return data.error; } fp = glade_util_fopen (filename, "w"); if (fp == NULL) return glade_error_new_system (_("Couldn't create file:\n %s\n"), filename); /* Initialize the output buffer. */ data.buffer = g_string_sized_new (1024); data.indent = 0; /* See if we need the translatable strings file output. */ data.save_translatable_strings = FALSE; data.translatable_strings = NULL; if (!GladeSessionFile) { data.save_translatable_strings = glade_project_get_output_translatable_strings (data.project); if (data.save_translatable_strings) data.translatable_strings = g_string_sized_new (1024); } /* Output the XML version info and our root element ''. */ fprintf (fp, "%s", GLADE_XML_BEGIN); fprintf (fp, "\n"); /* Output the requires tags. */ save_requires_tags (fp, project); /* Set up the struct to pass data to the callbacks. */ save_data.data = &data; save_data.fp = fp; #ifdef GLADE_STYLE_SUPPORT if (!data.error) { /* Save default gbstyle first. */ MSG ("Saving styles"); save_named_style (gb_widget_default_gb_style->name, gb_widget_default_gb_style, &data, fp); /* Now save all the other named styles. */ g_hash_table_foreach (gb_style_hash, (GHFunc) save_named_style_callback, &save_data); } #endif if (!data.error) { MSG ("Saving components"); glade_project_foreach_component (data.project, (GtkCallback) save_component, &save_data); } /* Finish the root element. */ if (!data.error) { fprintf (fp, "\n\n"); } /* Save the translatable strings file, if needed. */ if (!data.error) { if (data.save_translatable_strings) save_translatable_strings (&data); } /* Free any memory used while saving. */ g_string_free (data.buffer, TRUE); if (data.translatable_strings) g_string_free (data.translatable_strings, TRUE); fclose (fp); return data.error; } static void save_requires_tags_cb (GtkWidget *widget, GladeSaveRequiresData * requires_data) { const char *type_name; type_name = g_type_name (G_OBJECT_TYPE (widget)); /* We assume that any classes starting with GnomeDB require the GnomeDB support etc. Note that GNOME apps will probably require Bonobo for the BonoboDock widgets used in GnomeApp. */ if (!strncmp (type_name, "GnomeDb", 7)) requires_data->require_gnomedb = TRUE; else if (!strncmp (type_name, "GnomeCanvas", 11)) requires_data->require_gnome_canvas = TRUE; else if (!strncmp (type_name, "Gnome", 5)) requires_data->require_gnome = TRUE; else if (!strncmp (type_name, "Bonobo", 6)) requires_data->require_bonobo = TRUE; /* Recursively check all children. */ gb_widget_children_foreach (widget, (GtkCallback) save_requires_tags_cb, requires_data); } static void save_requires_tags (FILE *fp, GladeProject *project) { GladeSaveRequiresData requires_data; /* Assume we don't need any of them until we find one of their widgets. */ requires_data.require_gnome = FALSE; requires_data.require_gnome_canvas = FALSE; requires_data.require_gnomedb = FALSE; requires_data.require_bonobo = FALSE; glade_project_foreach_component (project, (GtkCallback) save_requires_tags_cb, &requires_data); /* Require "gnome", as provided in libgnomeui/glade/glade-gnome.c. We now require GNOME for all GNOME projects, since even if they don't contain GNOME widgets they may use stock GNOME items. */ if (requires_data.require_gnome || glade_project_get_gnome_support (project)) fprintf (fp, "\n"); /* Require "canvas", as provided in libgnomecanvas/glade/glade-canvas.c. */ if (requires_data.require_gnome_canvas) fprintf (fp, "\n"); /* Require "gnomedb", as provided in libgnomedb/glade/glade-gnomedb.c. */ if (requires_data.require_gnomedb) fprintf (fp, "\n"); /* Require "bonobo", as provided in libbonoboui/glade/glade-bonobo.c. */ if (requires_data.require_bonobo) fprintf (fp, "\n"); } /* This is called when iterating over the components in a project, to output each component. It simply calls gb_widget_save() to recursively save the XML for the component into the buffer, and it then flushes the buffer to the output file. */ static void save_component (GtkWidget * component, GladeSaveCallbackData * save_data) { /* If an error has occurred, we return. */ if (save_data->data->error) return; gb_widget_save (component, save_data->data); save_buffer_flush (save_data->data, save_data->fp); } /* This is called when iterating over the GHashTable of named styles. If the style isn't the default style it is output here. The default style is output first, since for all other styles we only output the differences from the default. */ #ifdef GLADE_STYLE_SUPPORT static void save_named_style_callback (const gchar * name, GbStyle * gbstyle, GladeSaveCallbackData * save_data) { /* If an error has occurred, or this is the default GbStyle, we return. */ if (save_data->data->error || gbstyle == gb_widget_default_gb_style) { return; } save_named_style (name, gbstyle, save_data->data, save_data->fp); } /* Outputs a named style. */ static void save_named_style (const gchar * name, GbStyle * gbstyle, GbWidgetGetArgData * data, FILE *fp) { gboolean save_all = FALSE; MSG1 ("Saving style: %s", name); /* If this is the default style, only save it if it is different to the GTK default style, and if it is make sure we save everything. */ if (gbstyle == gb_widget_default_gb_style) { if (gbstyle->style == gtk_widget_get_default_style ()) return; else save_all = TRUE; } gb_widget_save_style (gbstyle, data, save_all); save_buffer_flush (data, fp); } #endif /* Adds a start tag, e.g. "", to the output buffer. */ void save_start_tag (GbWidgetGetArgData * data, const gchar * tag_name) { save_buffer_add_indent (data->buffer, data->indent); g_string_append_c (data->buffer, '<'); g_string_append (data->buffer, tag_name); g_string_append (data->buffer, ">\n"); data->indent++; } /* Adds an end tag, e.g. "", to the output buffer. */ void save_end_tag (GbWidgetGetArgData * data, const gchar * tag_name) { data->indent--; save_buffer_add_indent (data->buffer, data->indent); g_string_append (data->buffer, "buffer, tag_name); g_string_append (data->buffer, ">\n"); } /* Adds a start tag to the output buffer, with the given class_name and id, if they are not NULL. */ void save_widget_start_tag (GbWidgetGetArgData * data, const gchar * class_name, const gchar *id) { save_buffer_add_indent (data->buffer, data->indent); g_string_append (data->buffer, "buffer, " class=\""); save_buffer_add_string (data->buffer, class_name); g_string_append_c (data->buffer, '"'); } if (id) { g_string_append (data->buffer, " id=\""); save_buffer_add_string (data->buffer, id); g_string_append_c (data->buffer, '"'); } g_string_append (data->buffer, ">\n"); data->indent++; } /* Adds a start tag to the output buffer, with the given child_name if it is not NULL. */ void save_child_start_tag (GbWidgetGetArgData * data, const gchar * child_name) { save_buffer_add_indent (data->buffer, data->indent); g_string_append (data->buffer, "buffer, " internal-child=\""); save_buffer_add_string (data->buffer, child_name); g_string_append_c (data->buffer, '"'); } g_string_append (data->buffer, ">\n"); data->indent++; } /* Adds a tag to the output buffer. */ void save_placeholder (GbWidgetGetArgData * data) { save_buffer_add_indent (data->buffer, data->indent); g_string_append (data->buffer, "\n"); } /* Starts a new line in the output buffer (without indenting). */ void save_newline (GbWidgetGetArgData * data) { g_string_append_c (data->buffer, '\n'); } /* These functions are called to save different types of widget properties. They all convert the property to a string representation and call save_string() to output it. The tag_name is usually the long name of the property, e.g. "GtkLabel::justify", so we cut out the first part and output .... */ static void save_string_internal (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value, gboolean translatable, const gchar *translator_comments, gboolean has_context_prefix) { gchar *tag_name_start; if (tag_value == NULL) return; tag_name_start = glade_util_find_start_of_tag_name (tag_name); save_buffer_add_indent (data->buffer, data->indent); g_string_append (data->buffer, "agent) { g_string_append (data->buffer, " agent=\""); save_buffer_add_string (data->buffer, data->agent); g_string_append_c (data->buffer, '"'); } g_string_append (data->buffer, " name=\""); save_buffer_add_string (data->buffer, tag_name_start); g_string_append_c (data->buffer, '"'); if (translatable) { g_string_append (data->buffer, " translatable=\"yes\""); if (has_context_prefix) g_string_append (data->buffer, " context=\"yes\""); if (translator_comments && *translator_comments) { g_string_append (data->buffer, " comments=\""); save_buffer_add_string (data->buffer, translator_comments); g_string_append_c (data->buffer, '"'); } } g_string_append_c (data->buffer, '>'); save_buffer_add_string (data->buffer, tag_value); g_string_append (data->buffer, "\n"); } void save_string (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { save_string_internal (data, tag_name, tag_value, FALSE, NULL, FALSE); } void save_translatable_string_internal (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { gboolean translatable, context; gchar *comments; glade_util_get_translation_properties (data->widget, tag_name, &translatable, &comments, &context); save_string_internal (data, tag_name, tag_value, translatable, comments, context); } void save_translatable_string (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { save_translatable_string_internal (data, tag_name, tag_value); if (data->save_translatable_strings) save_add_translatable_string (data, tag_value); } void save_text (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { save_string (data, tag_name, tag_value); } void save_translatable_text (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { save_translatable_string (data, tag_name, tag_value); } /* This is like save_translatable_text() except it splits the text into lines when adding to the translatable strings file. This is used for option menu items and combo items. */ void save_translatable_text_in_lines (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { /* FIXME: This probably won't work if other apps just try to translate the entire string. */ save_translatable_string_internal (data, tag_name, tag_value); if (data->save_translatable_strings && tag_value) { gchar *items, *pos, *items_end; items = pos = g_strdup (tag_value); items_end = &items[strlen (items)]; while (pos < items_end) { gchar *item_end = strchr (pos, '\n'); if (item_end == NULL) item_end = items_end; *item_end = '\0'; save_add_translatable_string (data, pos); pos = item_end + 1; } g_free (items); } } void save_int (GbWidgetGetArgData * data, const gchar * tag_name, const gint tag_value) { gchar buf[32]; sprintf (buf, "%i", tag_value); save_string (data, tag_name, buf); } void save_float (GbWidgetGetArgData * data, const gchar * tag_name, gfloat tag_value) { gchar buf[32]; sprintf (buf, "%.12g", tag_value); save_string (data, tag_name, buf); } void save_bool (GbWidgetGetArgData * data, const gchar * tag_name, gint tag_value) { save_string (data, tag_name, tag_value ? "True" : "False"); } void save_choice (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { save_string (data, tag_name, tag_value); } void save_combo (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { save_string (data, tag_name, tag_value); } /* Colors are now saved as '#rrrrggggbbbb', where rgb are 0-65535. We use gdk_color_parse() when loading, so color names can be used as well.*/ void save_color (GbWidgetGetArgData * data, const gchar * tag_name, GdkColor * tag_value) { gchar buf[32]; sprintf (buf, "#%04x%04x%04x", tag_value->red, tag_value->green, tag_value->blue); save_string (data, tag_name, buf); } void save_bgpixmap (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { save_string (data, tag_name, tag_value); } void save_dialog (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { save_string (data, tag_name, tag_value); } /* FIXME: I think this is broken. It should save it relative to the XML file.*/ void save_filename (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { save_string (data, tag_name, tag_value); } void save_pixmap_filename (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { if (data->copying_to_clipboard) { /* When saving to the clipboard, we just save the full path. This means it will still work if we paste into other projects. */ save_string (data, tag_name, tag_value); } else { /* When saving the XML file, we save only the basename of the pixmap files, since they should all be in the pixmaps directory. */ gchar *pixmaps_dir, *filename; pixmaps_dir = glade_project_get_pixmaps_directory (data->project); g_return_if_fail (pixmaps_dir != NULL); g_return_if_fail (pixmaps_dir[0] != '\0'); if (tag_value == NULL || tag_value[0] == '\0') { filename = NULL; } else { filename = (gchar*) g_basename (tag_value); } save_string (data, tag_name, filename); } } void save_font (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { save_string (data, tag_name, tag_value); } /* This saves a date in the RFC1123 format (an update of RFC822), e.g. 'Sun, 06 Nov 1994 08:49:37 GMT'. */ static void format_date (char *buffer, time_t tag_value) { time_t time; struct tm *t; time = tag_value; t = gmtime (&time); sprintf (buffer, "%s, %02d %s %04d %02d:%02d:%02d GMT", GladeDayNames[t->tm_wday], t->tm_mday, GladeMonthNames[t->tm_mon], t->tm_year + 1900, t->tm_hour, t->tm_min, t->tm_sec); } /* This saves a date in the RFC1123 format (an update of RFC822), e.g. 'Sun, 06 Nov 1994 08:49:37 GMT'. */ void save_date (GbWidgetGetArgData * data, const gchar * tag_name, time_t tag_value) { gchar buffer[32]; format_date (buffer, tag_value); save_string (data, tag_name, buffer); } void save_icon (GbWidgetGetArgData * data, const gchar * tag_name, const gchar * tag_value) { /* If it is a stock icon we can save it as an oridnary string. If it isn't, we need to save it as a pixmap filename, i.e. get rid of the path and just save the basename. */ if (glade_util_check_is_stock_id (tag_value)) save_string (data, tag_name, tag_value); else save_pixmap_filename (data, tag_name, tag_value); } /* Adds a string to the output buffer, converting special characters to entities, e.g. "<" is output as "<". */ void save_buffer_add_string (GString * buffer, const gchar * string) { gchar ch; while ((ch = *string++)) { if (ch == '<') g_string_append (buffer, "<"); else if (ch == '>') g_string_append (buffer, ">"); else if (ch == '&') g_string_append (buffer, "&"); else if (ch == '"') g_string_append (buffer, """); else g_string_append_c (buffer, ch); } } /* Outputs the contents of the buffer to the file and resets the buffer. */ static void save_buffer_flush (GbWidgetGetArgData * data, FILE *fp) { gint bytes_written; bytes_written = fwrite (data->buffer->str, sizeof (gchar), data->buffer->len, fp); if (bytes_written != data->buffer->len) { MSG2 ("Bytes: %i Written: %i", data->buffer->len, bytes_written); data->error = glade_error_new_system (_("Error writing XML file\n")); } /* Reset the output buffer. */ g_string_truncate (data->buffer, 0); data->indent = 0; } /* Outputs tabs & spaces to indent the line according to the current indentation level. Tabs are used to cut down on the file size a bit. */ void save_buffer_add_indent (GString *buffer, gint indent) { gint i, ntabs, nspaces; ntabs = (indent * 2) / 8; nspaces = (indent * 2) % 8; for (i = 0; i < ntabs; i++) g_string_append_c (buffer, '\t'); for (i = 0; i < nspaces; i++) g_string_append_c (buffer, ' '); } /* * Translatable string functions. */ /* This adds a translatable string to the buffer, wrapping it in the N_() macro so xgettext can find it. */ void save_add_translatable_string (GbWidgetGetArgData * data, const gchar * string) { GString *buffer; gchar escape_buffer[16]; const gchar *p; /* If it is an empty string don't bother outputting it. */ if (!string || string[0] == '\0') return; buffer = data->translatable_strings; g_string_append (buffer, "gchar *s = N_(\""); /* Step through each character of the given string, adding it to our GString buffer, converting it so that it is valid in a literal C string. */ for (p = string; *p; p++) { switch (*p) { case '\n': g_string_append (buffer, "\\n\"\n \""); break; case '\r': g_string_append (buffer, "\\r"); break; case '\t': g_string_append (buffer, "\\t"); break; case '\\': g_string_append (buffer, "\\\\"); break; case '"': g_string_append (buffer, "\\\""); break; default: if (isprint ((unsigned char) *p)) { g_string_append_c (buffer, *p); } else { sprintf (escape_buffer, "\\%02o", (guchar) *p); g_string_append (buffer, escape_buffer); } break; } } g_string_append (buffer, "\");\n"); } /* This outputs the file containing all the translatable strings. */ static void save_translatable_strings (GbWidgetGetArgData * data) { gchar *filename; FILE *fp; filename = glade_project_get_translatable_strings_file (data->project); fp = glade_util_fopen (filename, "w"); if (fp == NULL) { data->error = glade_error_new_system (_("Couldn't create file:\n %s\n"), filename); return; } fprintf (fp, _("/*\n" " * Translatable strings file generated by Glade.\n" " * Add this file to your project's POTFILES.in.\n" " * DO NOT compile it as part of your application.\n" " */\n" "\n")); fprintf (fp, "%s", data->translatable_strings->str); fclose (fp); } void save_signal (GbWidgetGetArgData *data, gchar *signal_name, gchar *handler, gboolean after, gchar *object, time_t last_modification_time) { /* Don't save signals without names or handlers. */ if (!signal_name || !signal_name[0] || ! handler || !handler[0]) return; save_buffer_add_indent (data->buffer, data->indent); g_string_append (data->buffer, "buffer, signal_name); g_string_append (data->buffer, "\" handler=\""); save_buffer_add_string (data->buffer, handler); g_string_append (data->buffer, "\""); if (after) g_string_append (data->buffer, " after=\"yes\""); if (object) { g_string_append (data->buffer, " object=\""); save_buffer_add_string (data->buffer, object); g_string_append (data->buffer, "\""); } if (last_modification_time != 0) { gchar buffer[32]; format_date (buffer, last_modification_time); g_string_append (data->buffer, " last_modification_time=\""); save_buffer_add_string (data->buffer, buffer); g_string_append (data->buffer, "\""); } g_string_append (data->buffer, "/>\n"); } void save_accelerator (GbWidgetGetArgData *data, guint8 modifiers, gchar *key, gchar *signal) { gchar *modifiers_string; /* Don't save accelerators without signals or keys. */ if (!key || !key[0] || !signal || !signal[0]) return; save_buffer_add_indent (data->buffer, data->indent); g_string_append (data->buffer, "buffer, key); g_string_append (data->buffer, "\""); modifiers_string = glade_util_create_modifiers_string (modifiers); g_string_append (data->buffer, " modifiers=\""); save_buffer_add_string (data->buffer, modifiers_string); g_string_append (data->buffer, "\""); g_string_append (data->buffer, " signal=\""); save_buffer_add_string (data->buffer, signal); g_string_append (data->buffer, "\""); g_string_append (data->buffer, "/>\n"); }