No issues found
1 /*
2 *
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU Lesser General Public
5 * License as published by the Free Software Foundation; either
6 * version 2 of the License, or (at your option) version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public
14 * License along with the program; if not, see <http://www.gnu.org/licenses/>
15 *
16 *
17 * Copyright (C) 1999-2009 Novell, Inc. (www.novell.com)
18 *
19 */
20
21 #ifdef HAVE_CONFIG_H
22 #include <config.h>
23 #endif
24
25 #include <glib/gi18n.h>
26 #include <gtk/gtk.h>
27
28 #include "e-datetime-format.h"
29 #include "e-util.h"
30
31 #define KEYS_FILENAME "datetime-formats.ini"
32 #define KEYS_GROUPNAME "formats"
33
34 #ifdef G_OS_WIN32
35 #ifdef localtime_r
36 #undef localtime_r
37 #endif
38 /* The localtime() in Microsoft's C library *is* thread-safe */
39 #define localtime_r(timep, result) (localtime (timep) ? memcpy ((result), localtime (timep), sizeof (*(result))) : 0)
40 #endif
41
42 static GHashTable *key2fmt = NULL;
43
44 static GKeyFile *setup_keyfile = NULL; /* used on the combo */
45 static gint setup_keyfile_instances = 0;
46
47 static void
48 save_keyfile (GKeyFile *keyfile)
49 {
50 gchar *contents;
51 gchar *filename;
52 gsize length;
53 GError *error = NULL;
54
55 g_return_if_fail (keyfile != NULL);
56
57 filename = g_build_filename (e_get_user_data_dir (), KEYS_FILENAME, NULL);
58 contents = g_key_file_to_data (keyfile, &length, NULL);
59
60 g_file_set_contents (filename, contents, length, &error);
61
62 if (error != NULL) {
63 g_warning ("%s", error->message);
64 g_error_free (error);
65 }
66
67 g_free (contents);
68 g_free (filename);
69 }
70
71 static void
72 ensure_loaded (void)
73 {
74 GKeyFile *keyfile;
75 gchar *str, **keys;
76 gint i;
77
78 if (key2fmt)
79 return;
80
81 key2fmt = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
82 keyfile = g_key_file_new ();
83
84 str = g_build_filename (e_get_user_data_dir (), KEYS_FILENAME, NULL);
85 g_key_file_load_from_file (keyfile, str, G_KEY_FILE_NONE, NULL);
86 g_free (str);
87
88 keys = g_key_file_get_keys (keyfile, KEYS_GROUPNAME, NULL, NULL);
89
90 if (keys) {
91 for (i = 0; keys[i]; i++) {
92 str = g_key_file_get_string (keyfile, KEYS_GROUPNAME, keys[i], NULL);
93 if (str)
94 g_hash_table_insert (key2fmt, g_strdup (keys[i]), str);
95 }
96
97 g_strfreev (keys);
98 }
99
100 g_key_file_free (keyfile);
101 }
102
103 static const gchar *
104 get_default_format (DTFormatKind kind,
105 const gchar *key)
106 {
107 const gchar *res = NULL;
108
109 ensure_loaded ();
110
111 switch (kind) {
112 case DTFormatKindDate:
113 res = g_hash_table_lookup (key2fmt, "Default-Date");
114 if (!res)
115 res = "%x";
116 break;
117 case DTFormatKindTime:
118 res = g_hash_table_lookup (key2fmt, "Default-Time");
119 if (!res)
120 res = "%X";
121 break;
122 case DTFormatKindDateTime:
123 res = g_hash_table_lookup (key2fmt, "Default-DateTime");
124 if (!res && key && g_str_has_prefix (key, "mail-table"))
125 res = "%ad %H:%M";
126 if (!res)
127 res = "%x %X"; /* %c is also possible, but it doesn't play well with time zone identifiers */
128 break;
129 case DTFormatKindShortDate:
130 res = g_hash_table_lookup (key2fmt, "Default-ShortDate");
131 if (!res)
132 res = "%A, %B %d";
133 break;
134 }
135
136 if (!res)
137 res = "%x %X";
138
139 return res;
140 }
141
142 static const gchar *
143 get_format_internal (const gchar *key,
144 DTFormatKind kind)
145 {
146 const gchar *res;
147
148 ensure_loaded ();
149
150 g_return_val_if_fail (key != NULL, NULL);
151 g_return_val_if_fail (key2fmt != NULL, NULL);
152
153 res = g_hash_table_lookup (key2fmt, key);
154 if (!res)
155 res = get_default_format (kind, key);
156
157 return res;
158 }
159
160 static void
161 set_format_internal (const gchar *key,
162 const gchar *fmt,
163 GKeyFile *keyfile)
164 {
165 ensure_loaded ();
166
167 g_return_if_fail (key != NULL);
168 g_return_if_fail (key2fmt != NULL);
169 g_return_if_fail (keyfile != NULL);
170
171 if (!fmt || !*fmt) {
172 g_hash_table_remove (key2fmt, key);
173 g_key_file_remove_key (keyfile, KEYS_GROUPNAME, key, NULL);
174 } else {
175 g_hash_table_insert (key2fmt, g_strdup (key), g_strdup (fmt));
176 g_key_file_set_string (keyfile, KEYS_GROUPNAME, key, fmt);
177 }
178 }
179
180 static gchar *
181 format_relative_date (time_t tvalue,
182 time_t ttoday,
183 const struct tm *value,
184 const struct tm *today)
185 {
186 gchar *res = g_strdup (get_default_format (DTFormatKindDate, NULL));
187 GDate now, val;
188 gint diff;
189
190 g_return_val_if_fail (value != NULL, res);
191 g_return_val_if_fail (today != NULL, res);
192
193 g_date_set_time_t (&now, ttoday);
194 g_date_set_time_t (&val, tvalue);
195
196 diff = g_date_get_julian (&now) - g_date_get_julian (&val);
197 /* if it's more than a week, use the default date format */
198 if (ABS (diff) > 7)
199 return res;
200
201 g_free (res);
202
203 if (value->tm_year == today->tm_year &&
204 value->tm_mon == today->tm_mon &&
205 value->tm_mday == today->tm_mday) {
206 res = g_strdup (_("Today"));
207 } else {
208 gboolean future = FALSE;
209
210 if (diff < 0)
211 future = TRUE;
212
213 diff = ABS (diff);
214
215 if (diff <= 1) {
216 if (future)
217 res = g_strdup (_("Tomorrow"));
218 else
219 res = g_strdup (_("Yesterday"));
220 } else {
221 if (future) {
222 switch (g_date_get_weekday (&val)) {
223 case 1:
224 /* Translators: This is used for abbreviated days in the future.
225 * You can use strftime modifiers here too, like "Next %a", to avoid
226 * repeated translation of the abbreviated day name. */
227 res = g_strdup (C_ ("DateFmt", "Next Mon"));
228 break;
229 case 2:
230 /* Translators: This is used for abbreviated days in the future.
231 * You can use strftime modifiers here too, like "Next %a", to avoid
232 * repeated translation of the abbreviated day name. */
233 res = g_strdup (C_ ("DateFmt", "Next Tue"));
234 break;
235 case 3:
236 /* Translators: This is used for abbreviated days in the future.
237 * You can use strftime modifiers here too, like "Next %a", to avoid
238 * repeated translation of the abbreviated day name. */
239 res = g_strdup (C_ ("DateFmt", "Next Wed"));
240 break;
241 case 4:
242 /* Translators: This is used for abbreviated days in the future.
243 * You can use strftime modifiers here too, like "Next %a", to avoid
244 * repeated translation of the abbreviated day name. */
245 res = g_strdup (C_ ("DateFmt", "Next Thu"));
246 break;
247 case 5:
248 /* Translators: This is used for abbreviated days in the future.
249 * You can use strftime modifiers here too, like "Next %a", to avoid
250 * repeated translation of the abbreviated day name. */
251 res = g_strdup (C_ ("DateFmt", "Next Fri"));
252 break;
253 case 6:
254 /* Translators: This is used for abbreviated days in the future.
255 * You can use strftime modifiers here too, like "Next %a", to avoid
256 * repeated translation of the abbreviated day name. */
257 res = g_strdup (C_ ("DateFmt", "Next Sat"));
258 break;
259 case 7:
260 /* Translators: This is used for abbreviated days in the future.
261 * You can use strftime modifiers here too, like "Next %a", to avoid
262 * repeated translation of the abbreviated day name. */
263 res = g_strdup (C_ ("DateFmt", "Next Sun"));
264 break;
265 default:
266 g_return_val_if_reached (NULL);
267 break;
268 }
269 } else {
270 res = g_strdup ("%a");
271 }
272 }
273 }
274
275 return res;
276 }
277
278 static gchar *
279 format_internal (const gchar *key,
280 DTFormatKind kind,
281 time_t tvalue,
282 struct tm *tm_value)
283 {
284 const gchar *fmt;
285 gchar buff[129];
286 GString *use_fmt = NULL;
287 gint i, last = 0;
288 struct tm today, value;
289 time_t ttoday;
290
291 tzset ();
292 if (!tm_value) {
293 localtime_r (&tvalue, &value);
294 tm_value = &value;
295 } else {
296 /* recalculate tvalue to local (system) timezone */
297 tvalue = mktime (tm_value);
298 localtime_r (&tvalue, &value);
299 }
300
301 fmt = get_format_internal (key, kind);
302 for (i = 0; fmt[i]; i++) {
303 if (fmt[i] == '%') {
304 if (fmt[i + 1] == '%') {
305 i++;
306 } else if (fmt[i + 1] == 'a' && fmt[i + 2] == 'd' && (fmt[i + 3] == 0 || !g_ascii_isalpha (fmt[i + 3]))) {
307 gchar *ad;
308
309 /* "%ad" for abbreviated date */
310 if (!use_fmt) {
311 use_fmt = g_string_new ("");
312
313 ttoday = time (NULL);
314 localtime_r (&ttoday, &today);
315 }
316
317 g_string_append_len (use_fmt, fmt + last, i - last);
318 last = i + 3;
319 i += 2;
320
321 ad = format_relative_date (tvalue, ttoday, &value, &today);
322 if (ad)
323 g_string_append (use_fmt, ad);
324 else if (g_ascii_isspace (fmt[i + 3]))
325 i++;
326
327 g_free (ad);
328 }
329 }
330 }
331
332 if (use_fmt && last < i) {
333 g_string_append_len (use_fmt, fmt + last, i - last);
334 }
335
336 e_utf8_strftime_fix_am_pm (buff, sizeof (buff) - 1, use_fmt ? use_fmt->str : fmt, tm_value);
337
338 if (use_fmt)
339 g_string_free (use_fmt, TRUE);
340
341 return g_strstrip (g_strdup (buff));
342 }
343
344 static void
345 fill_combo_formats (GtkWidget *combo,
346 const gchar *key,
347 DTFormatKind kind)
348 {
349 const gchar *date_items[] = {
350 N_ ("Use locale default"),
351 "%m/%d/%y", /* American style */
352 "%m/%d/%Y", /* American style, full year */
353 "%d.%m.%y", /* non-American style */
354 "%d.%m.%Y", /* non-American style, full year */
355 "%ad", /* abbreviated date, like "Today" */
356 NULL
357 };
358
359 const gchar *time_items[] = {
360 N_ ("Use locale default"),
361 "%I:%M:%S %p", /* 12hours style */
362 "%I:%M %p", /* 12hours style, without seconds */
363 "%H:%M:%S", /* 24hours style */
364 "%H:%M", /* 24hours style, without seconds */
365 NULL
366 };
367
368 const gchar *datetime_items[] = {
369 N_ ("Use locale default"),
370 "%m/%d/%y %I:%M:%S %p", /* American style */
371 "%m/%d/%Y %I:%M:%S %p", /* American style, full year */
372 "%m/%d/%y %I:%M %p", /* American style, without seconds */
373 "%m/%d/%Y %I:%M %p", /* American style, without seconds, full year */
374 "%ad %I:%M:%S %p", /* %ad is an abbreviated date, like "Today" */
375 "%ad %I:%M %p", /* %ad is an abbreviated date, like "Today", without seconds */
376 "%d.%m.%y %H:%M:%S", /* non-American style */
377 "%d.%m.%Y %H:%M:%S", /* non-American style, full year */
378 "%d.%m.%y %H:%M", /* non-American style, without seconds */
379 "%d.%m.%Y %H:%M", /* non-American style, without seconds, full year */
380 "%ad %H:%M:%S",
381 "%ad %H:%M", /* without seconds */
382 NULL
383 };
384
385 const gchar *shortdate_items[] = {
386 "%A, %B %d",
387 "%A, %d %B",
388 "%a, %b %d",
389 "%a, %d %b",
390 NULL
391 };
392
393 const gchar **items = NULL;
394 gint i, idx = 0;
395 const gchar *fmt;
396
397 g_return_if_fail (GTK_IS_COMBO_BOX (combo));
398
399 switch (kind) {
400 case DTFormatKindDate:
401 items = date_items;
402 break;
403 case DTFormatKindTime:
404 items = time_items;
405 break;
406 case DTFormatKindDateTime:
407 items = datetime_items;
408 break;
409 case DTFormatKindShortDate:
410 items = shortdate_items;
411 break;
412 }
413
414 g_return_if_fail (items != NULL);
415
416 fmt = get_format_internal (key, kind);
417
418 for (i = 0; items[i]; i++) {
419 if (i == 0) {
420 gtk_combo_box_text_append_text (
421 GTK_COMBO_BOX_TEXT (combo), _(items[i]));
422 } else {
423 gtk_combo_box_text_append_text (
424 GTK_COMBO_BOX_TEXT (combo), items[i]);
425 if (!idx && fmt && g_str_equal (fmt, items[i]))
426 idx = i;
427 }
428 }
429
430 if (idx == 0 && fmt && !g_str_equal (fmt, get_default_format (kind, key))) {
431 gtk_combo_box_text_append_text (
432 GTK_COMBO_BOX_TEXT (combo), fmt);
433 idx = i;
434 }
435
436 gtk_combo_box_set_active ((GtkComboBox *) combo, idx);
437 }
438
439 static void
440 update_preview_widget (GtkWidget *combo)
441 {
442 GtkWidget *preview;
443 const gchar *key;
444 gchar *value;
445 time_t now;
446
447 g_return_if_fail (GTK_IS_COMBO_BOX (combo));
448
449 preview = g_object_get_data (G_OBJECT (combo), "preview-label");
450 g_return_if_fail (preview != NULL);
451 g_return_if_fail (GTK_IS_LABEL (preview));
452
453 key = g_object_get_data (G_OBJECT (combo), "format-key");
454 g_return_if_fail (key != NULL);
455
456 time (&now);
457
458 value = format_internal (key, GPOINTER_TO_INT (g_object_get_data (G_OBJECT (combo), "format-kind")), now, NULL);
459 gtk_label_set_text (GTK_LABEL (preview), value ? value : "");
460 g_free (value);
461 }
462
463 static void
464 format_combo_changed_cb (GtkWidget *combo,
465 gpointer user_data)
466 {
467 const gchar *key;
468 DTFormatKind kind;
469 GKeyFile *keyfile;
470
471 g_return_if_fail (GTK_IS_COMBO_BOX (combo));
472
473 key = g_object_get_data (G_OBJECT (combo), "format-key");
474 g_return_if_fail (key != NULL);
475
476 kind = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (combo), "format-kind"));
477 keyfile = g_object_get_data (G_OBJECT (combo), "setup-key-file");
478
479 if (kind != DTFormatKindShortDate && gtk_combo_box_get_active (GTK_COMBO_BOX (combo)) == 0) {
480 /* use locale default */
481 set_format_internal (key, NULL, keyfile);
482 } else {
483 gchar *text;
484
485 text = gtk_combo_box_text_get_active_text (
486 GTK_COMBO_BOX_TEXT (combo));
487 set_format_internal (key, text, keyfile);
488 g_free (text);
489 }
490
491 update_preview_widget (combo);
492
493 /* save on every change only because 'unref_setup_keyfile' is never called :(
494 * how about in kill - bonobo? */
495 save_keyfile (keyfile);
496 }
497
498 static gchar *
499 gen_key (const gchar *component,
500 const gchar *part,
501 DTFormatKind kind)
502 {
503 const gchar *kind_str = NULL;
504
505 g_return_val_if_fail (component != NULL, NULL);
506 g_return_val_if_fail (*component != 0, NULL);
507
508 switch (kind) {
509 case DTFormatKindDate:
510 kind_str = "Date";
511 break;
512 case DTFormatKindTime:
513 kind_str = "Time";
514 break;
515 case DTFormatKindDateTime:
516 kind_str = "DateTime";
517 break;
518 case DTFormatKindShortDate:
519 kind_str = "ShortDate";
520 break;
521 }
522
523 return g_strconcat (component, (part && *part) ? "-" : "", part && *part ? part : "", "-", kind_str, NULL);
524 }
525
526 static void
527 unref_setup_keyfile (gpointer ptr)
528 {
529 g_return_if_fail (ptr == setup_keyfile);
530 g_return_if_fail (setup_keyfile != NULL);
531 g_return_if_fail (setup_keyfile_instances > 0);
532
533 /* this is never called */
534 setup_keyfile_instances--;
535 if (setup_keyfile_instances == 0) {
536 save_keyfile (setup_keyfile);
537 g_key_file_free (setup_keyfile);
538 setup_keyfile = NULL;
539 }
540 }
541
542 /**
543 * e_datetime_format_add_setup_widget:
544 * @table: Where to attach widgets. Requires 3 columns.
545 * @row: On which row to attach.
546 * @component: Component identifier for the format. Cannot be empty nor NULL.
547 * @part: Part in the component, can be NULL or empty string.
548 * @kind: Kind of the format for the component/part.
549 * @caption: Caption for the widget, can be NULL, then the "Format:" is used.
550 *
551 * Adds a setup widget for a component and part. The table should have 3 columns.
552 * All the work related to loading and saving the value is done automatically,
553 * on user's changes.
554 **/
555 void
556 e_datetime_format_add_setup_widget (GtkWidget *table,
557 gint row,
558 const gchar *component,
559 const gchar *part,
560 DTFormatKind kind,
561 const gchar *caption)
562 {
563 GtkListStore *store;
564 GtkWidget *label, *combo, *preview, *align;
565 gchar *key;
566
567 g_return_if_fail (table != NULL);
568 g_return_if_fail (row >= 0);
569 g_return_if_fail (component != NULL);
570 g_return_if_fail (*component != 0);
571
572 key = gen_key (component, part, kind);
573
574 label = gtk_label_new_with_mnemonic (caption ? caption : _("Format:"));
575
576 store = gtk_list_store_new (1, G_TYPE_STRING);
577 combo = g_object_new (
578 GTK_TYPE_COMBO_BOX_TEXT,
579 "model", store,
580 "has-entry", TRUE,
581 "entry-text-column", 0,
582 NULL);
583 g_object_unref (store);
584
585 fill_combo_formats (combo, key, kind);
586 gtk_label_set_mnemonic_widget ((GtkLabel *) label, combo);
587
588 align = gtk_alignment_new (0.0, 0.5, 0.0, 0.0);
589 gtk_container_add (GTK_CONTAINER (align), combo);
590
591 gtk_table_attach ((GtkTable *) table, label, 0, 1, row, row + 1, 0, 0, 2, 0);
592 gtk_table_attach ((GtkTable *) table, align, 1, 2, row, row + 1, 0, 0, 2, 0);
593
594 preview = gtk_label_new ("");
595 gtk_misc_set_alignment (GTK_MISC (preview), 0.0, 0.5);
596 gtk_label_set_ellipsize (GTK_LABEL (preview), PANGO_ELLIPSIZE_END);
597 gtk_table_attach ((GtkTable *) table, preview, 2, 3, row, row + 1, GTK_EXPAND | GTK_FILL, 0, 2, 0);
598
599 if (!setup_keyfile) {
600 gchar *filename;
601
602 filename = g_build_filename (e_get_user_data_dir (), KEYS_FILENAME, NULL);
603 setup_keyfile = g_key_file_new ();
604 g_key_file_load_from_file (setup_keyfile, filename, G_KEY_FILE_NONE, NULL);
605 g_free (filename);
606
607 setup_keyfile_instances = 1;
608 } else {
609 setup_keyfile_instances++;
610 }
611
612 g_object_set_data (G_OBJECT (combo), "preview-label", preview);
613 g_object_set_data (G_OBJECT (combo), "format-kind", GINT_TO_POINTER (kind));
614 g_object_set_data_full (G_OBJECT (combo), "format-key", key, g_free);
615 g_object_set_data_full (G_OBJECT (combo), "setup-key-file", setup_keyfile, unref_setup_keyfile);
616 g_signal_connect (
617 combo, "changed",
618 G_CALLBACK (format_combo_changed_cb), NULL);
619
620 update_preview_widget (combo);
621
622 gtk_widget_show_all (table);
623 }
624
625 gchar *
626 e_datetime_format_format (const gchar *component,
627 const gchar *part,
628 DTFormatKind kind,
629 time_t value)
630 {
631 gchar *key, *res;
632
633 g_return_val_if_fail (component != NULL, NULL);
634 g_return_val_if_fail (*component != 0, NULL);
635
636 key = gen_key (component, part, kind);
637 g_return_val_if_fail (key != NULL, NULL);
638
639 res = format_internal (key, kind, value, NULL);
640
641 g_free (key);
642
643 return res;
644 }
645
646 gchar *
647 e_datetime_format_format_tm (const gchar *component,
648 const gchar *part,
649 DTFormatKind kind,
650 struct tm *tm_time)
651 {
652 gchar *key, *res;
653
654 g_return_val_if_fail (component != NULL, NULL);
655 g_return_val_if_fail (*component != 0, NULL);
656 g_return_val_if_fail (tm_time != NULL, NULL);
657
658 key = gen_key (component, part, kind);
659 g_return_val_if_fail (key != NULL, NULL);
660
661 res = format_internal (key, kind, 0, tm_time);
662
663 g_free (key);
664
665 return res;
666 }
667
668 gboolean
669 e_datetime_format_includes_day_name (const gchar *component,
670 const gchar *part,
671 DTFormatKind kind)
672 {
673 gchar *key;
674 const gchar *fmt;
675 gboolean res;
676
677 g_return_val_if_fail (component != NULL, FALSE);
678 g_return_val_if_fail (*component != 0, FALSE);
679
680 key = gen_key (component, part, kind);
681 g_return_val_if_fail (key != NULL, FALSE);
682
683 fmt = get_format_internal (key, kind);
684
685 res = fmt && (strstr (fmt, "%a") != NULL || strstr (fmt, "%A") != NULL);
686
687 g_free (key);
688
689 return res;
690 }