aboutsummaryrefslogblamecommitdiffstats
path: root/e-util/e-datetime-format.c
blob: c41fc08c419a0d67ac94894d3e222e2e8f524d41 (plain) (tree)


























                                                                             
                                            


                                


                  



























































































































                                                                                                                      
 
















                                                                                                   

                       



                                                  

                                         
 
                                                                   
                                                                   
                           








                                               
                                        
 

                                      
 





                                                                






                                                                                                                     















                                                                                         
                 

































                                                                                                                                      
 
































































                                                                                                       
                        











































































































                                                                                                                        








































































































































                                                                                                                                                   
/*
 *
 * 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.ini"
#define KEYS_GROUPNAME "formats"

#ifdef G_OS_WIN32
#ifdef localtime_r
#undef localtime_r
#endif
/* 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));
    GDate now, val;
    gint diff;

    g_return_val_if_fail (value != NULL, res);
    g_return_val_if_fail (today != NULL, res);

    g_date_set_time_t (&now, ttoday);
    g_date_set_time_t (&val, tvalue);

    diff = g_date_get_julian (&now) - g_date_get_julian (&val);
    /* if it's more than a week, use the default date format */
    if (ABS (diff) > 7)
        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 {
        gboolean future = FALSE;

        if (diff < 0)
            future = TRUE;

        if (diff <= 1) {
            if (future)
                res = g_strdup (_("Tomorrow"));
            else
                res = g_strdup (_("Yesterday"));
        } else {
            if (future) {
                /* Translators: %d is replaced with a number of days. It's always greater than 1 */
                res = g_strdup_printf (ngettext ("%d day from now", "%d days from now", diff), diff);
            } else {
                /* Translators: %d is replaced with a number of days. It's always greater than 1 */
                res = g_strdup_printf (ngettext ("%d day ago", "%d days ago", diff), 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;
    gint 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;
}