diff options
Diffstat (limited to 'e-util')
-rw-r--r-- | e-util/Makefile.am | 2 | ||||
-rw-r--r-- | e-util/e-datetime-format.c | 571 | ||||
-rw-r--r-- | e-util/e-datetime-format.h | 46 |
3 files changed, 619 insertions, 0 deletions
diff --git a/e-util/Makefile.am b/e-util/Makefile.am index d723e3b290..35d254b4eb 100644 --- a/e-util/Makefile.am +++ b/e-util/Makefile.am @@ -45,6 +45,7 @@ eutilinclude_HEADERS = \ e-config.h \ e-corba-utils.h \ e-cursor.h \ + e-datetime-format.h \ e-dialog-utils.h \ e-dialog-widgets.h \ e-error.h \ @@ -85,6 +86,7 @@ libeutil_la_SOURCES = \ e-config.c \ e-corba-utils.c \ e-cursor.c \ + e-datetime-format.c \ e-dialog-utils.c \ e-dialog-widgets.c \ e-error.c \ diff --git a/e-util/e-datetime-format.c b/e-util/e-datetime-format.c new file mode 100644 index 0000000000..c6900ff8ca --- /dev/null +++ b/e-util/e-datetime-format.c @@ -0,0 +1,571 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2009 Novell, Inc. (www.novell.com) + * + */ + +#include <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#include "e-datetime-format.h" +#include "e-util.h" + +#define KEYS_FILENAME "datetime-formats" +#define KEYS_GROUPNAME "formats" + +#ifdef G_OS_WIN32 +/* The localtime() in Microsoft's C library *is* thread-safe */ +#define localtime_r(timep, result) (localtime (timep) ? memcpy ((result), localtime (timep), sizeof (*(result))) : 0) +#endif + +static GHashTable *key2fmt = NULL; + +static GKeyFile *setup_keyfile = NULL; /* used on the combo */ +static gint setup_keyfile_instances = 0; + +static void +save_keyfile (GKeyFile *keyfile) +{ + gchar *contents; + gchar *filename; + gsize length; + GError *error = NULL; + + g_return_if_fail (keyfile != NULL); + + filename = g_build_filename (e_get_user_data_dir (), KEYS_FILENAME, NULL); + contents = g_key_file_to_data (keyfile, &length, NULL); + + g_file_set_contents (filename, contents, length, &error); + + if (error != NULL) { + g_warning ("%s", error->message); + g_error_free (error); + } + + g_free (contents); + g_free (filename); +} + +static void +ensure_loaded (void) +{ + GKeyFile *keyfile; + gchar *str, **keys; + gint i; + + if (key2fmt) + return; + + key2fmt = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + keyfile = g_key_file_new (); + + str = g_build_filename (e_get_user_data_dir (), KEYS_FILENAME, NULL); + g_key_file_load_from_file (keyfile, str, G_KEY_FILE_NONE, NULL); + g_free (str); + + keys = g_key_file_get_keys (keyfile, KEYS_GROUPNAME, NULL, NULL); + + if (keys) { + for (i = 0; keys [i]; i++) { + str = g_key_file_get_string (keyfile, KEYS_GROUPNAME, keys [i], NULL); + if (str) + g_hash_table_insert (key2fmt, g_strdup (keys [i]), str); + } + + g_strfreev (keys); + } + + g_key_file_free (keyfile); +} + +static const gchar * +get_default_format (DTFormatKind kind, const gchar *key) +{ + const gchar *res = NULL; + + ensure_loaded (); + + switch (kind) { + case DTFormatKindDate: + res = g_hash_table_lookup (key2fmt, "Default-Date"); + if (!res) + res = "%x"; + break; + case DTFormatKindTime: + res = g_hash_table_lookup (key2fmt, "Default-Time"); + if (!res) + res = "%X"; + break; + case DTFormatKindDateTime: + res = g_hash_table_lookup (key2fmt, "Default-DateTime"); + if (!res && key && g_str_has_prefix (key, "mail-table")) + res = "%ad %H:%M"; + if (!res) + res = "%x %X"; /* %c is also possible, but it doesn't play well with time zone identifiers */ + break; + case DTFormatKindShortDate: + res = g_hash_table_lookup (key2fmt, "Default-ShortDate"); + if (!res) + res = "%A, %B %d"; + break; + } + + if (!res) + res = "%x %X"; + + return res; +} + +static const gchar * +get_format_internal (const gchar *key, DTFormatKind kind) +{ + const gchar *res; + + ensure_loaded (); + + g_return_val_if_fail (key != NULL, NULL); + g_return_val_if_fail (key2fmt != NULL, NULL); + + res = g_hash_table_lookup (key2fmt, key); + if (!res) + res = get_default_format (kind, key); + + return res; +} + +static void +set_format_internal (const gchar *key, const gchar *fmt, GKeyFile *keyfile) +{ + ensure_loaded (); + + g_return_if_fail (key != NULL); + g_return_if_fail (key2fmt != NULL); + g_return_if_fail (keyfile != NULL); + + if (!fmt || !*fmt) { + g_hash_table_remove (key2fmt, key); + g_key_file_remove_key (keyfile, KEYS_GROUPNAME, key, NULL); + } else { + g_hash_table_insert (key2fmt, g_strdup (key), g_strdup (fmt)); + g_key_file_set_string (keyfile, KEYS_GROUPNAME, key, fmt); + } +} + +static gchar * +format_relative_date (time_t tvalue, time_t ttoday, const struct tm *value, const struct tm *today) +{ + gchar *res = g_strdup (get_default_format (DTFormatKindDate, NULL)); + + g_return_val_if_fail (value != NULL, res); + g_return_val_if_fail (today != NULL, res); + + /* if it's more than a week, use the default date format */ + if (ttoday - tvalue > 7 * 24 * 60 * 60 || + tvalue - ttoday > 7 * 24 * 60 * 60) + return res; + + g_free (res); + + if (value->tm_year == today->tm_year && + value->tm_mon == today->tm_mon && + value->tm_mday == today->tm_mday) { + res = g_strdup (_("Today")); + } else { + gint diff = (gint) (tvalue - ttoday); + gint since_midnight = today->tm_sec + (60 * today->tm_min) + (60 * 60 * today->tm_hour); + gboolean future = (diff > 0); + + if (!future) + diff *= -1; + + diff = (diff - since_midnight) / (24 * 60 * 60); + if (diff <= 1) { + if (future) + res = g_strdup (_("Tomorrow")); + else + res = g_strdup (_("Yesterday")); + } else { + if (future) + res = g_strdup_printf (_("%d days from now"), diff); + else + res = g_strdup_printf (_("%d days ago"), diff); + } + } + + return res; +} + +static gchar * +format_internal (const gchar *key, DTFormatKind kind, time_t tvalue, struct tm *tm_value) +{ + const gchar *fmt; + gchar buff[129]; + GString *use_fmt = NULL; + gint i, last = 0; + struct tm today, value; + time_t ttoday; + + tzset(); + if (!tm_value) { + localtime_r (&tvalue, &value); + tm_value = &value; + } else { + /* recalculate tvalue to local (system) timezone */ + tvalue = mktime (tm_value); + localtime_r (&tvalue, &value); + } + + fmt = get_format_internal (key, kind); + for (i = 0; fmt [i]; i++) { + if (fmt [i] == '%') { + if (fmt [i + 1] == '%') { + i++; + } else if (fmt [i + 1] == 'a' && fmt [i + 2] == 'd' && (fmt [i + 3] == 0 || !g_ascii_isalpha (fmt [i + 3]))) { + gchar *ad; + + /* "%ad" for abbreviated date */ + if (!use_fmt) { + use_fmt = g_string_new (""); + + ttoday = time (NULL); + localtime_r (&ttoday, &today); + } + + g_string_append_len (use_fmt, fmt + last, i - last); + last = i + 3; + i += 2; + + ad = format_relative_date (tvalue, ttoday, &value, &today); + if (ad) + g_string_append (use_fmt, ad); + else if (g_ascii_isspace (fmt [i + 3])) + i++; + + g_free (ad); + } + } + } + + if (use_fmt && last < i) { + g_string_append_len (use_fmt, fmt + last, i - last); + } + + e_utf8_strftime_fix_am_pm (buff, sizeof (buff) - 1, use_fmt ? use_fmt->str : fmt, tm_value); + + if (use_fmt) + g_string_free (use_fmt, TRUE); + + return g_strstrip (g_strdup (buff)); +} + +static void +fill_combo_formats (GtkWidget *combo, const gchar *key, DTFormatKind kind) +{ + const gchar *date_items [] = { + N_ ("Use locale default"), + "%m/%d/%y", /* American style */ + "%m/%d/%Y", /* American style, full year */ + "%d.%m.%y", /* non-American style */ + "%d.%m.%Y", /* non-American style, full year */ + "%ad", /* abbreviated date, like "Today" */ + NULL + }; + + const gchar *time_items [] = { + N_ ("Use locale default"), + "%I:%M:%S %p", /* 12hours style */ + "%I:%M %p", /* 12hours style, without seconds */ + "%H:%M:%S", /* 24hours style */ + "%H:%M", /* 24hours style, without seconds */ + NULL + }; + + const gchar *datetime_items [] = { + N_ ("Use locale default"), + "%m/%d/%y %I:%M:%S %p", /* American style */ + "%m/%d/%Y %I:%M:%S %p", /* American style, full year */ + "%m/%d/%y %I:%M %p", /* American style, without seconds */ + "%m/%d/%Y %I:%M %p", /* American style, without seconds, full year */ + "%ad %I:%M:%S %p", /* %ad is an abbreviated date, like "Today" */ + "%ad %I:%M %p", /* %ad is an abbreviated date, like "Today", without seconds */ + "%d.%m.%y %H:%M:%S", /* non-American style */ + "%d.%m.%Y %H:%M:%S", /* non-American style, full year */ + "%d.%m.%y %H:%M", /* non-American style, without seconds */ + "%d.%m.%Y %H:%M", /* non-American style, without seconds, full year */ + "%ad %H:%M:%S", + "%ad %H:%M", /* without seconds */ + NULL + }; + + const gchar *shortdate_items [] = { + "%A, %B %d", + "%A, %d %B", + "%a, %b %d", + "%a, %d %b", + NULL + }; + + const gchar **items = NULL; + int i, idx = 0; + const gchar *fmt; + + g_return_if_fail (GTK_IS_COMBO_BOX_ENTRY (combo)); + + switch (kind) { + case DTFormatKindDate: + items = date_items; + break; + case DTFormatKindTime: + items = time_items; + break; + case DTFormatKindDateTime: + items = datetime_items; + break; + case DTFormatKindShortDate: + items = shortdate_items; + break; + } + + g_return_if_fail (items != NULL); + + fmt = get_format_internal (key, kind); + + for (i = 0; items [i]; i++) { + if (i == 0) { + gtk_combo_box_append_text ((GtkComboBox *) combo, _(items[i])); + } else { + gtk_combo_box_append_text ((GtkComboBox *) combo, items[i]); + if (!idx && fmt && g_str_equal (fmt, items[i])) + idx = i; + } + } + + if (idx == 0 && fmt && !g_str_equal (fmt, get_default_format (kind, key))) { + gtk_combo_box_append_text ((GtkComboBox *) combo, fmt); + idx = i; + } + + gtk_combo_box_set_active ((GtkComboBox *) combo, idx); +} + +static void +update_preview_widget (GtkWidget *combo) +{ + GtkWidget *preview; + const gchar *key; + gchar *value; + time_t now; + + g_return_if_fail (combo != NULL); + g_return_if_fail (GTK_IS_COMBO_BOX_ENTRY (combo)); + + preview = g_object_get_data (G_OBJECT (combo), "preview-label"); + g_return_if_fail (preview != NULL); + g_return_if_fail (GTK_IS_LABEL (preview)); + + key = g_object_get_data (G_OBJECT (combo), "format-key"); + g_return_if_fail (key != NULL); + + time (&now); + + value = format_internal (key, GPOINTER_TO_INT (g_object_get_data (G_OBJECT (combo), "format-kind")), now, NULL); + gtk_label_set_text (GTK_LABEL (preview), value ? value : ""); + g_free (value); +} + +static void +format_combo_changed_cb (GtkWidget *combo, gpointer user_data) +{ + const gchar *key; + DTFormatKind kind; + GKeyFile *keyfile; + + g_return_if_fail (combo != NULL); + g_return_if_fail (GTK_IS_COMBO_BOX_ENTRY (combo)); + + key = g_object_get_data (G_OBJECT (combo), "format-key"); + g_return_if_fail (key != NULL); + + kind = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (combo), "format-kind")); + keyfile = g_object_get_data (G_OBJECT (combo), "setup-key-file"); + + if (kind != DTFormatKindShortDate && gtk_combo_box_get_active (GTK_COMBO_BOX (combo)) == 0) { + /* use locale default */ + set_format_internal (key, NULL, keyfile); + } else { + gchar *text; + + text = gtk_combo_box_get_active_text (GTK_COMBO_BOX (combo)); + set_format_internal (key, text, keyfile); + g_free (text); + } + + update_preview_widget (combo); + + /* save on every change only because 'unref_setup_keyfile' is never called :( + how about in kill-bonobo? */ + save_keyfile (keyfile); +} + +static gchar * +gen_key (const gchar *component, const gchar *part, DTFormatKind kind) +{ + const gchar *kind_str = NULL; + + g_return_val_if_fail (component != NULL, NULL); + g_return_val_if_fail (*component != 0, NULL); + + + switch (kind) { + case DTFormatKindDate: + kind_str = "Date"; + break; + case DTFormatKindTime: + kind_str = "Time"; + break; + case DTFormatKindDateTime: + kind_str = "DateTime"; + break; + case DTFormatKindShortDate: + kind_str = "ShortDate"; + break; + } + + return g_strconcat (component, (part && *part) ? "-" : "", part && *part ? part : "", "-", kind_str, NULL); +} + +static void +unref_setup_keyfile (gpointer ptr) +{ + g_return_if_fail (ptr == setup_keyfile); + g_return_if_fail (setup_keyfile != NULL); + g_return_if_fail (setup_keyfile_instances > 0); + + /* this is never called :( */ + setup_keyfile_instances--; + if (setup_keyfile_instances == 0) { + save_keyfile (setup_keyfile); + g_key_file_free (setup_keyfile); + setup_keyfile = NULL; + } +} + +/** + * e_datetime_format_add_setup_widget: + * @table: Where to attach widgets. Requires 3 columns. + * @row: On which row to attach. + * @component: Component identifier for the format. Cannot be empty nor NULL. + * @part: Part in the component, can be NULL or empty string. + * @kind: Kind of the format for the component/part. + * @caption: Caption for the widget, can be NULL, then the "Format:" is used. + * + * Adds a setup widget for a component and part. The table should have 3 columns. + * All the work related to loading and saving the value is done automatically, + * on user's changes. + **/ +void +e_datetime_format_add_setup_widget (GtkWidget *table, gint row, const gchar *component, const gchar *part, DTFormatKind kind, const gchar *caption) +{ + GtkWidget *label, *combo, *preview, *align; + gchar *key; + + g_return_if_fail (table != NULL); + g_return_if_fail (row >= 0); + g_return_if_fail (component != NULL); + g_return_if_fail (*component != 0); + + key = gen_key (component, part, kind); + + label = gtk_label_new_with_mnemonic (caption ? caption : _("Format:")); + combo = gtk_combo_box_entry_new_text (); + + fill_combo_formats (combo, key, kind); + gtk_label_set_mnemonic_widget ((GtkLabel *)label, combo); + + align = gtk_alignment_new (0.0, 0.5, 0.0, 0.0); + gtk_container_add (GTK_CONTAINER (align), combo); + + gtk_table_attach ((GtkTable *) table, label, 0, 1, row, row + 1, 0, 0, 2, 0); + gtk_table_attach ((GtkTable *) table, align, 1, 2, row, row + 1, 0, 0, 2, 0); + + preview = gtk_label_new (""); + gtk_misc_set_alignment (GTK_MISC (preview), 0.0, 0.5); + gtk_label_set_ellipsize (GTK_LABEL (preview), PANGO_ELLIPSIZE_END); + gtk_table_attach ((GtkTable *) table, preview, 2, 3, row, row + 1, GTK_EXPAND | GTK_FILL, 0, 2, 0); + + if (!setup_keyfile) { + gchar *filename; + + filename = g_build_filename (e_get_user_data_dir (), KEYS_FILENAME, NULL); + setup_keyfile = g_key_file_new (); + g_key_file_load_from_file (setup_keyfile, filename, G_KEY_FILE_NONE, NULL); + g_free (filename); + + setup_keyfile_instances = 1; + } else { + setup_keyfile_instances++; + } + + g_object_set_data (G_OBJECT (combo), "preview-label", preview); + g_object_set_data (G_OBJECT (combo), "format-kind", GINT_TO_POINTER (kind)); + g_object_set_data_full (G_OBJECT (combo), "format-key", key, g_free); + g_object_set_data_full (G_OBJECT (combo), "setup-key-file", setup_keyfile, unref_setup_keyfile); + g_signal_connect (combo, "changed", G_CALLBACK (format_combo_changed_cb), NULL); + + update_preview_widget (combo); + + gtk_widget_show_all (table); +} + +gchar * +e_datetime_format_format (const gchar *component, const gchar *part, DTFormatKind kind, time_t value) +{ + gchar *key, *res; + + g_return_val_if_fail (component != NULL, NULL); + g_return_val_if_fail (*component != 0, NULL); + + key = gen_key (component, part, kind); + g_return_val_if_fail (key != NULL, NULL); + + res = format_internal (key, kind, value, NULL); + + g_free (key); + + return res; +} + +gchar * +e_datetime_format_format_tm (const gchar *component, const gchar *part, DTFormatKind kind, struct tm *tm_time) +{ + gchar *key, *res; + + g_return_val_if_fail (component != NULL, NULL); + g_return_val_if_fail (*component != 0, NULL); + g_return_val_if_fail (tm_time != NULL, NULL); + + key = gen_key (component, part, kind); + g_return_val_if_fail (key != NULL, NULL); + + res = format_internal (key, kind, 0, tm_time); + + g_free (key); + + return res; +} diff --git a/e-util/e-datetime-format.h b/e-util/e-datetime-format.h new file mode 100644 index 0000000000..80f7d18826 --- /dev/null +++ b/e-util/e-datetime-format.h @@ -0,0 +1,46 @@ +/* + * + * Customizable date/time formatting in Evolution + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + * + * Copyright (C) 1999-2009 Novell, Inc. (www.novell.com) + * + */ + +#ifndef __E_DATETIME_FORMAT__ +#define __E_DATETIME_FORMAT__ + +#include <time.h> +#include <glib.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +typedef enum _DTFormatKind { + DTFormatKindDate, + DTFormatKindTime, + DTFormatKindDateTime, + DTFormatKindShortDate +} DTFormatKind; + +void e_datetime_format_add_setup_widget (GtkWidget *table, gint row, const gchar *component, const gchar *part, DTFormatKind kind, const gchar *caption); + +gchar *e_datetime_format_format (const gchar *component, const gchar *part, DTFormatKind kind, time_t value); +gchar *e_datetime_format_format_tm (const gchar *component, const gchar *part, DTFormatKind kind, struct tm *tm_time); + +G_END_DECLS + +#endif |