extended the list of grouped metadata tags
[geeqie.git] / src / metadata.c
1 /*
2  * Geeqie
3  * (C) 2004 John Ellis
4  * Copyright (C) 2008 - 2009 The Geeqie Team
5  *
6  * Author: John Ellis, Laurent Monin
7  *
8  * This software is released under the GNU General Public License (GNU GPL).
9  * Please read the included file COPYING for more information.
10  * This software comes with no warranty of any kind, use at your own risk!
11  */
12
13
14 #include "main.h"
15 #include "metadata.h"
16
17 #include "cache.h"
18 #include "exif.h"
19 #include "filedata.h"
20 #include "misc.h"
21 #include "secure_save.h"
22 #include "ui_fileops.h"
23 #include "ui_misc.h"
24 #include "utilops.h"
25 #include "filefilter.h"
26 #include "layout_util.h"
27 #include "rcfile.h"
28
29 typedef enum {
30         MK_NONE,
31         MK_KEYWORDS,
32         MK_COMMENT
33 } MetadataKey;
34
35 static const gchar *group_keys[] = { /* tags that will be written to all files in a group, options->metadata.sync_grouped_files */
36         "Xmp.dc.title",
37         "Xmp.photoshop.Urgency",
38         "Xmp.photoshop.Category",
39         "Xmp.photoshop.SupplementalCategory",
40         "Xmp.dc.subject",
41         "Xmp.iptc.Location",
42         "Xmp.photoshop.Instruction",
43         "Xmp.photoshop.DateCreated",
44         "Xmp.dc.creator",
45         "Xmp.photoshop.AuthorsPosition",
46         "Xmp.photoshop.City",
47         "Xmp.photoshop.State",
48         "Xmp.iptc.CountryCode",
49         "Xmp.photoshop.Country",
50         "Xmp.photoshop.TransmissionReference",
51         "Xmp.photoshop.Headline",
52         "Xmp.photoshop.Credit",
53         "Xmp.photoshop.Source",
54         "Xmp.dc.rights",
55         "Xmp.dc.description",
56         "Xmp.photoshop.CaptionWriter",
57         NULL};
58
59 static gboolean metadata_write_queue_idle_cb(gpointer data);
60 static gboolean metadata_legacy_write(FileData *fd);
61 static void metadata_legacy_delete(FileData *fd, const gchar *except);
62
63
64
65 /*
66  *-------------------------------------------------------------------
67  * write queue
68  *-------------------------------------------------------------------
69  */
70
71 static GList *metadata_write_queue = NULL;
72 static guint metadata_write_idle_id = 0; /* event source id */
73
74 static void metadata_write_queue_add(FileData *fd)
75 {
76         if (!g_list_find(metadata_write_queue, fd))
77                 {
78                 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
79                 file_data_ref(fd);
80                 
81                 layout_util_status_update_write_all();
82                 }
83
84         if (metadata_write_idle_id) 
85                 {
86                 g_source_remove(metadata_write_idle_id);
87                 metadata_write_idle_id = 0;
88                 }
89         
90         if (options->metadata.confirm_after_timeout)
91                 {
92                 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, NULL);
93                 }
94 }
95
96
97 gboolean metadata_write_queue_remove(FileData *fd)
98 {
99         g_hash_table_destroy(fd->modified_xmp);
100         fd->modified_xmp = NULL;
101
102         metadata_write_queue = g_list_remove(metadata_write_queue, fd);
103         
104         file_data_increment_version(fd);
105         file_data_send_notification(fd, NOTIFY_REREAD);
106
107         file_data_unref(fd);
108
109         layout_util_status_update_write_all();
110         return TRUE;
111 }
112
113 gboolean metadata_write_queue_remove_list(GList *list)
114 {
115         GList *work;
116         gboolean ret = TRUE;
117         
118         work = list;
119         while (work)
120                 {
121                 FileData *fd = work->data;
122                 work = work->next;
123                 ret = ret && metadata_write_queue_remove(fd);
124                 }
125         return ret;
126 }
127
128
129 gboolean metadata_write_queue_confirm(gboolean force_dialog, FileUtilDoneFunc done_func, gpointer done_data)
130 {
131         GList *work;
132         GList *to_approve = NULL;
133         
134         work = metadata_write_queue;
135         while (work)
136                 {
137                 FileData *fd = work->data;
138                 work = work->next;
139                 
140                 if (fd->change) continue; /* another operation in progress, skip this file for now */
141                 
142                 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
143                 }
144
145         file_util_write_metadata(NULL, to_approve, NULL, force_dialog, done_func, done_data);
146         
147         filelist_free(to_approve);
148         
149         return (metadata_write_queue != NULL);
150 }
151
152 static gboolean metadata_write_queue_idle_cb(gpointer data)
153 {
154         metadata_write_queue_confirm(FALSE, NULL, NULL);
155         metadata_write_idle_id = 0;
156         return FALSE;
157 }
158
159 gboolean metadata_write_perform(FileData *fd)
160 {
161         gboolean success;
162         ExifData *exif;
163         
164         g_assert(fd->change);
165         
166         if (fd->change->dest && 
167             strcmp(extension_from_path(fd->change->dest), GQ_CACHE_EXT_METADATA) == 0)
168                 {
169                 success = metadata_legacy_write(fd);
170                 if (success) metadata_legacy_delete(fd, fd->change->dest);
171                 return success;
172                 }
173
174         /* write via exiv2 */
175         /*  we can either use cached metadata which have fd->modified_xmp already applied 
176                                      or read metadata from file and apply fd->modified_xmp
177             metadata are read also if the file was modified meanwhile */
178         exif = exif_read_fd(fd); 
179         if (!exif) return FALSE;
180
181         success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
182         exif_free_fd(fd, exif);
183
184         if (fd->change->dest)
185                 /* this will create a FileData for the sidecar and link it to the main file 
186                    (we can't wait until the sidecar is discovered by directory scanning because
187                     exif_read_fd is called before that and it would read the main file only and 
188                     store the metadata in the cache)
189                     FIXME: this does not catch new sidecars created by independent external programs
190                 */
191                 file_data_unref(file_data_new_simple(fd->change->dest)); 
192                 
193         if (success) metadata_legacy_delete(fd, fd->change->dest);
194         return success;
195 }
196
197 gint metadata_queue_length(void)
198 {
199         return g_list_length(metadata_write_queue);
200 }
201
202 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
203 {
204         const gchar **k = keys;
205         
206         while (*k)
207                 {
208                 if (strcmp(key, *k) == 0) return TRUE;
209                 k++;
210                 }
211         return FALSE;
212 }
213
214 gboolean metadata_write_revert(FileData *fd, const gchar *key)
215 {
216         if (!fd->modified_xmp) return FALSE;
217         
218         g_hash_table_remove(fd->modified_xmp, key);
219         
220         if (g_hash_table_size(fd->modified_xmp) == 0)
221                 {
222                 metadata_write_queue_remove(fd);
223                 }
224         else
225                 {
226                 /* reread the metadata to restore the original value */
227                 file_data_increment_version(fd);
228                 file_data_send_notification(fd, NOTIFY_REREAD);
229                 }
230         return TRUE;
231 }
232
233 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
234 {
235         if (!fd->modified_xmp)
236                 {
237                 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
238                 }
239         g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
240         if (fd->exif)
241                 {
242                 exif_update_metadata(fd->exif, key, values);
243                 }
244         metadata_write_queue_add(fd);
245         file_data_increment_version(fd);
246         file_data_send_notification(fd, NOTIFY_METADATA);
247
248         if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
249                 {
250                 GList *work = fd->sidecar_files;
251                 
252                 while (work)
253                         {
254                         FileData *sfd = work->data;
255                         work = work->next;
256                         
257                         if (filter_file_class(sfd->extension, FORMAT_CLASS_META)) continue; 
258
259                         metadata_write_list(sfd, key, values);
260                         }
261                 }
262
263
264         return TRUE;
265 }
266         
267 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
268 {
269         GList *list = g_list_append(NULL, g_strdup(value));
270         gboolean ret = metadata_write_list(fd, key, list);
271         string_list_free(list);
272         return ret;
273 }
274
275 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
276 {
277         gchar string[50];
278         
279         g_snprintf(string, sizeof(string), "%ld", value);
280         return metadata_write_string(fd, key, string);
281 }
282
283 /*
284  *-------------------------------------------------------------------
285  * keyword / comment read/write
286  *-------------------------------------------------------------------
287  */
288
289 static gboolean metadata_file_write(gchar *path, GHashTable *modified_xmp)
290 {
291         SecureSaveInfo *ssi;
292         GList *keywords = g_hash_table_lookup(modified_xmp, KEYWORD_KEY);
293         GList *comment_l = g_hash_table_lookup(modified_xmp, COMMENT_KEY);
294         gchar *comment = comment_l ? comment_l->data : NULL;
295
296         ssi = secure_open(path);
297         if (!ssi) return FALSE;
298
299         secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
300
301         secure_fprintf(ssi, "[keywords]\n");
302         while (keywords && secsave_errno == SS_ERR_NONE)
303                 {
304                 const gchar *word = keywords->data;
305                 keywords = keywords->next;
306
307                 secure_fprintf(ssi, "%s\n", word);
308                 }
309         secure_fputc(ssi, '\n');
310
311         secure_fprintf(ssi, "[comment]\n");
312         secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
313
314         secure_fprintf(ssi, "#end\n");
315
316         return (secure_close(ssi) == 0);
317 }
318
319 static gboolean metadata_legacy_write(FileData *fd)
320 {
321         gboolean success = FALSE;
322         gchar *metadata_pathl;
323
324         g_assert(fd->change && fd->change->dest);
325
326         DEBUG_1("Saving comment: %s", fd->change->dest);
327
328         metadata_pathl = path_from_utf8(fd->change->dest);
329
330         success = metadata_file_write(metadata_pathl, fd->modified_xmp);
331
332         g_free(metadata_pathl);
333
334         return success;
335 }
336
337 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
338 {
339         FILE *f;
340         gchar s_buf[1024];
341         MetadataKey key = MK_NONE;
342         GList *list = NULL;
343         GString *comment_build = NULL;
344
345         f = fopen(path, "r");
346         if (!f) return FALSE;
347
348         while (fgets(s_buf, sizeof(s_buf), f))
349                 {
350                 gchar *ptr = s_buf;
351
352                 if (*ptr == '#') continue;
353                 if (*ptr == '[' && key != MK_COMMENT)
354                         {
355                         gchar *keystr = ++ptr;
356                         
357                         key = MK_NONE;
358                         while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
359                         
360                         if (*ptr == ']')
361                                 {
362                                 *ptr = '\0';
363                                 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
364                                         key = MK_KEYWORDS;
365                                 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
366                                         key = MK_COMMENT;
367                                 }
368                         continue;
369                         }
370                 
371                 switch (key)
372                         {
373                         case MK_NONE:
374                                 break;
375                         case MK_KEYWORDS:
376                                 {
377                                 while (*ptr != '\n' && *ptr != '\0') ptr++;
378                                 *ptr = '\0';
379                                 if (strlen(s_buf) > 0)
380                                         {
381                                         gchar *kw = utf8_validate_or_convert(s_buf);
382
383                                         list = g_list_prepend(list, kw);
384                                         }
385                                 }
386                                 break;
387                         case MK_COMMENT:
388                                 if (!comment_build) comment_build = g_string_new("");
389                                 g_string_append(comment_build, s_buf);
390                                 break;
391                         }
392                 }
393         
394         fclose(f);
395
396         if (keywords) 
397                 {
398                 *keywords = g_list_reverse(list);
399                 }
400         else
401                 {
402                 string_list_free(list);
403                 }
404                 
405         if (comment_build)
406                 {
407                 if (comment)
408                         {
409                         gint len;
410                         gchar *ptr = comment_build->str;
411
412                         /* strip leading and trailing newlines */
413                         while (*ptr == '\n') ptr++;
414                         len = strlen(ptr);
415                         while (len > 0 && ptr[len - 1] == '\n') len--;
416                         if (ptr[len] == '\n') len++; /* keep the last one */
417                         if (len > 0)
418                                 {
419                                 gchar *text = g_strndup(ptr, len);
420
421                                 *comment = utf8_validate_or_convert(text);
422                                 g_free(text);
423                                 }
424                         }
425                 g_string_free(comment_build, TRUE);
426                 }
427
428         return TRUE;
429 }
430
431 static void metadata_legacy_delete(FileData *fd, const gchar *except)
432 {
433         gchar *metadata_path;
434         gchar *metadata_pathl;
435         if (!fd) return;
436
437         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
438         if (metadata_path && (!except || strcmp(metadata_path, except) != 0)) 
439                 {
440                 metadata_pathl = path_from_utf8(metadata_path);
441                 unlink(metadata_pathl);
442                 g_free(metadata_pathl);
443                 g_free(metadata_path);
444                 }
445         metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
446         if (metadata_path && (!except || strcmp(metadata_path, except) != 0)) 
447                 {
448                 metadata_pathl = path_from_utf8(metadata_path);
449                 unlink(metadata_pathl);
450                 g_free(metadata_pathl);
451                 g_free(metadata_path);
452                 }
453 }
454
455 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
456 {
457         gchar *metadata_path;
458         gchar *metadata_pathl;
459         gboolean success = FALSE;
460         
461         if (!fd) return FALSE;
462
463         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
464         if (!metadata_path) return FALSE;
465
466         metadata_pathl = path_from_utf8(metadata_path);
467
468         success = metadata_file_read(metadata_pathl, keywords, comment);
469
470         g_free(metadata_pathl);
471         g_free(metadata_path);
472
473         return success;
474 }
475
476 static GList *remove_duplicate_strings_from_list(GList *list)
477 {
478         GList *work = list;
479         GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
480         GList *newlist = NULL;
481
482         while (work)
483                 {
484                 gchar *key = work->data;
485
486                 if (g_hash_table_lookup(hashtable, key) == NULL)
487                         {
488                         g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
489                         newlist = g_list_prepend(newlist, key);
490                         }
491                 work = work->next;
492                 }
493
494         g_hash_table_destroy(hashtable);
495         g_list_free(list);
496
497         return g_list_reverse(newlist);
498 }
499
500 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
501 {
502         ExifData *exif;
503         GList *list = NULL;
504         if (!fd) return NULL;
505
506         /* unwritten data overide everything */
507         if (fd->modified_xmp && format == METADATA_PLAIN)
508                 {
509                 list = g_hash_table_lookup(fd->modified_xmp, key);
510                 if (list) return string_list_copy(list);
511                 }
512
513         /* 
514             Legacy metadata file is the primary source if it exists.
515             Merging the lists does not make much sense, because the existence of
516             legacy metadata file indicates that the other metadata sources are not
517             writable and thus it would not be possible to delete the keywords
518             that comes from the image file.
519         */
520         if (strcmp(key, KEYWORD_KEY) == 0)
521                 {
522                 if (metadata_legacy_read(fd, &list, NULL)) return list;
523                 }
524         else if (strcmp(key, COMMENT_KEY) == 0)
525                 {
526                 gchar *comment = NULL;
527                 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
528                 }
529         else if (strncmp(key, "file.", 5) == 0)
530                 {
531                 return g_list_append(NULL, metadata_file_info(fd, key, format));
532                 }
533         
534         exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
535         if (!exif) return NULL;
536         list = exif_get_metadata(exif, key, format);
537         exif_free_fd(fd, exif);
538         return list;
539 }
540
541 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
542 {
543         GList *string_list = metadata_read_list(fd, key, format);
544         if (string_list)
545                 {
546                 gchar *str = string_list->data;
547                 string_list->data = NULL;
548                 string_list_free(string_list);
549                 return str;
550                 }
551         return NULL;
552 }
553
554 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
555 {
556         guint64 ret;
557         gchar *endptr;
558         gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
559         if (!string) return fallback;
560         
561         ret = g_ascii_strtoull(string, &endptr, 10);
562         if (string == endptr) ret = fallback;
563         g_free(string);
564         return ret;
565 }
566
567 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
568 {
569         gdouble coord;
570         gchar *endptr;
571         gdouble deg, min, sec;
572         gboolean ok = FALSE;
573         gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
574         if (!string) return fallback;
575         
576         deg = g_ascii_strtod(string, &endptr);
577         if (*endptr == ',')
578                 {
579                 min = g_ascii_strtod(endptr + 1, &endptr);
580                 if (*endptr == ',')
581                         sec = g_ascii_strtod(endptr + 1, &endptr);
582                 else 
583                         sec = 0.0;
584                 
585                 
586                 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E') 
587                         {
588                         coord = deg + min /60.0 + sec / 3600.0;
589                         ok = TRUE;
590                         if (*endptr == 'S' || *endptr == 'W') coord = -coord;
591                         }
592                 }
593         
594         if (!ok)
595                 {
596                 coord = fallback;
597                 log_printf("unable to parse GPS coordinate '%s'\n", string);
598                 }
599         
600         g_free(string);
601         return coord;
602 }
603         
604 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
605 {
606         gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
607         
608         if (!str) 
609                 {
610                 return metadata_write_string(fd, key, value);
611                 }
612         else
613                 {
614                 gchar *new_string = g_strconcat(str, value, NULL);
615                 gboolean ret = metadata_write_string(fd, key, new_string);
616                 g_free(str);
617                 g_free(new_string);
618                 return ret;
619                 }
620 }
621
622 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
623 {
624         GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
625         
626         if (!list) 
627                 {
628                 return metadata_write_list(fd, key, values);
629                 }
630         else
631                 {
632                 gboolean ret;
633                 list = g_list_concat(list, string_list_copy(values));
634                 list = remove_duplicate_strings_from_list(list);
635                 
636                 ret = metadata_write_list(fd, key, list);
637                 string_list_free(list);
638                 return ret;
639                 }
640 }
641
642 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
643 {
644         gchar *string_casefold = g_utf8_casefold(string, -1);
645
646         while (list)
647                 {
648                 gchar *haystack = list->data;
649                 
650                 if (haystack)
651                         {
652                         gboolean equal;
653                         gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
654
655                         equal = (strcmp(haystack_casefold, string_casefold) == 0);
656                         g_free(haystack_casefold);
657
658                         if (equal)
659                                 {
660                                 g_free(string_casefold);
661                                 return haystack;
662                                 }
663                         }
664         
665                 list = list->next;
666                 }
667         
668         g_free(string_casefold);
669         return NULL;
670 }
671
672
673 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
674
675 GList *string_to_keywords_list(const gchar *text)
676 {
677         GList *list = NULL;
678         const gchar *ptr = text;
679
680         while (*ptr != '\0')
681                 {
682                 const gchar *begin;
683                 gint l = 0;
684
685                 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
686                 begin = ptr;
687                 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
688                         {
689                         ptr++;
690                         l++;
691                         }
692
693                 /* trim starting and ending whitespaces */
694                 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
695                 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
696
697                 if (l > 0)
698                         {
699                         gchar *keyword = g_strndup(begin, l);
700
701                         /* only add if not already in the list */
702                         if (!find_string_in_list_utf8nocase(list, keyword))
703                                 list = g_list_append(list, keyword);
704                         else
705                                 g_free(keyword);
706                         }
707                 }
708
709         return list;
710 }
711
712 /*
713  * keywords to marks
714  */
715  
716
717 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
718 {
719         /* FIXME: do not use global keyword_tree */
720         GList *path = data;
721         GList *keywords;
722         gboolean found = FALSE;
723         keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
724         if (keywords)
725                 {
726                 GtkTreeIter iter;
727                 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
728                     keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
729                         found = TRUE;
730
731                 }
732         return found;
733 }
734
735 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
736 {
737         GList *path = data;
738         GList *keywords = NULL;
739         GtkTreeIter iter;
740         
741         if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
742
743         keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
744
745         if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
746                 {
747                 if (value) 
748                         {
749                         keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
750                         }
751                 else
752                         {
753                         keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
754                         }
755                 metadata_write_list(fd, KEYWORD_KEY, keywords);
756                 }
757
758         string_list_free(keywords);
759         return TRUE;
760 }
761
762
763
764 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
765 {
766
767         FileDataGetMarkFunc get_mark_func;
768         FileDataSetMarkFunc set_mark_func;
769         gpointer mark_func_data;
770
771         gint i;
772
773         for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
774                 {
775                 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
776                 if (get_mark_func == meta_data_get_keyword_mark) 
777                         {
778                         GtkTreeIter old_kw_iter;
779                         GList *old_path = mark_func_data;
780                         
781                         if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) && 
782                             (i == mark || /* release any previous connection of given mark */
783                              keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
784                                 {
785                                 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
786                                 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
787                                 }
788                         }
789                 }
790
791
792         if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
793                 {
794                 GList *path;
795                 gchar *mark_str;
796                 path = keyword_tree_get_path(keyword_tree, kw_iter);
797                 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
798                 
799                 mark_str = g_strdup_printf("%d", mark + 1);
800                 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
801                 g_free(mark_str);
802                 }
803 }
804
805
806 /*
807  *-------------------------------------------------------------------
808  * keyword tree
809  *-------------------------------------------------------------------
810  */
811
812
813
814 GtkTreeStore *keyword_tree;
815
816 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
817 {
818         gchar *name;
819         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
820         return name;
821 }
822
823 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
824 {
825         gchar *casefold;
826         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
827         return casefold;
828 }
829
830 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
831 {
832         gboolean is_keyword;
833         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
834         return is_keyword;
835 }
836
837 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
838 {
839         gchar *casefold = g_utf8_casefold(name, -1);
840         gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
841                                                 KEYWORD_COLUMN_NAME, name,
842                                                 KEYWORD_COLUMN_CASEFOLD, casefold,
843                                                 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
844         g_free(casefold);
845 }
846
847 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
848 {
849         GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
850         GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
851         gint ret = gtk_tree_path_compare(pa, pb);
852         gtk_tree_path_free(pa);
853         gtk_tree_path_free(pb);
854         return ret;
855 }
856
857 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
858 {
859         GtkTreeIter parent_a;
860         GtkTreeIter parent_b;
861         
862         gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
863         gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
864
865         if (valid_pa && valid_pb)
866                 {
867                 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
868                 }
869         else
870                 {
871                 return (!valid_pa && !valid_pb); /* both are toplevel */
872                 }
873 }
874
875 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
876 {
877         GtkTreeIter parent;
878         GtkTreeIter iter;
879         gboolean toplevel = FALSE;
880         gboolean ret;
881         gchar *casefold;
882         
883         if (parent_ptr)
884                 {
885                 parent = *parent_ptr;
886                 }
887         else if (sibling)
888                 {
889                 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
890                 }
891         else
892                 {
893                 toplevel = TRUE;
894                 }
895         
896         if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
897         
898         casefold = g_utf8_casefold(name, -1);
899         ret = FALSE;
900         
901         while (TRUE)
902                 {
903                 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
904                         {
905                         if (options->metadata.keywords_case_sensitive)
906                                 {
907                                 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
908                                 ret = strcmp(name, iter_name) == 0;
909                                 g_free(iter_name);
910                                 }
911                         else
912                                 {
913                                 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
914                                 ret = strcmp(casefold, iter_casefold) == 0;
915                                 g_free(iter_casefold);
916                                 } // if (options->metadata.tags_cas...
917                         }
918                 if (ret) 
919                         {
920                         if (result) *result = iter;
921                         break;
922                         }
923                 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
924                 }
925         g_free(casefold);
926         return ret;
927 }
928
929
930 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
931 {
932
933         gchar *mark, *name, *casefold;
934         gboolean is_keyword;
935
936         /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
937         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
938                                                 KEYWORD_COLUMN_NAME, &name,
939                                                 KEYWORD_COLUMN_CASEFOLD, &casefold,
940                                                 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
941
942         gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
943                                                 KEYWORD_COLUMN_NAME, name,
944                                                 KEYWORD_COLUMN_CASEFOLD, casefold,
945                                                 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
946         g_free(mark);
947         g_free(name);
948         g_free(casefold);
949 }
950
951 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
952 {
953         GtkTreeIter from_child;
954         
955         keyword_copy(keyword_tree, to, from);
956         
957         if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
958         
959         while (TRUE)
960                 {
961                 GtkTreeIter to_child;
962                 gtk_tree_store_append(keyword_tree, &to_child, to);
963                 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
964                 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
965                 }
966 }
967
968 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
969 {
970         keyword_copy_recursive(keyword_tree, to, from);
971         keyword_delete(keyword_tree, from);
972 }
973
974 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
975 {
976         GList *path = NULL;
977         GtkTreeIter iter = *iter_ptr;
978         
979         while (TRUE)
980                 {
981                 GtkTreeIter parent;
982                 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
983                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
984                 iter = parent;
985                 }
986         return path;
987 }
988
989 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
990 {
991         GtkTreeIter iter;
992
993         if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
994         
995         while (TRUE)
996                 {
997                 GtkTreeIter children;
998                 while (TRUE)
999                         {
1000                         gchar *name = keyword_get_name(keyword_tree, &iter);
1001                         if (strcmp(name, path->data) == 0) break;
1002                         g_free(name);
1003                         if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1004                         }
1005                 path = path->next;
1006                 if (!path) 
1007                         {
1008                         *iter_ptr = iter;
1009                         return TRUE;
1010                         }
1011                         
1012                 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1013                 iter = children;
1014                 }
1015 }
1016
1017
1018 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1019 {
1020         if (!casefold_list) return FALSE;
1021
1022         if (!keyword_get_is_keyword(keyword_tree, &iter))
1023                 {
1024                 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1025                 GtkTreeIter child;
1026                 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter)) 
1027                         return FALSE; /* this should happen only on empty helpers */
1028
1029                 while (TRUE)
1030                         {
1031                         if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1032                         if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1033                         }
1034                 }
1035         
1036         while (TRUE)
1037                 {
1038                 GtkTreeIter parent;
1039
1040                 if (keyword_get_is_keyword(keyword_tree, &iter))
1041                         {
1042                         GList *work = casefold_list;
1043                         gboolean found = FALSE;
1044                         gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1045                         while (work)
1046                                 {
1047                                 const gchar *casefold = work->data;
1048                                 work = work->next;
1049
1050                                 if (strcmp(iter_casefold, casefold) == 0)
1051                                         {
1052                                         found = TRUE;
1053                                         break;
1054                                         }
1055                                 }
1056                         g_free(iter_casefold);
1057                         if (!found) return FALSE;
1058                         }
1059                 
1060                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1061                 iter = parent;
1062                 }
1063 }
1064
1065 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1066 {
1067         if (!kw_list) return FALSE;
1068
1069         if (!keyword_get_is_keyword(keyword_tree, &iter))
1070                 {
1071                 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1072                 GtkTreeIter child;
1073                 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1074                         return FALSE; /* this should happen only on empty helpers */
1075
1076                 while (TRUE)
1077                         {
1078                         if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1079                         if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1080                         }
1081                 }
1082
1083         while (TRUE)
1084                 {
1085                 GtkTreeIter parent;
1086
1087                 if (keyword_get_is_keyword(keyword_tree, &iter))
1088                         {
1089                         GList *work = kw_list;
1090                         gboolean found = FALSE;
1091                         gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1092                         while (work)
1093                                 {
1094                                 const gchar *name = work->data;
1095                                 work = work->next;
1096
1097                                 if (strcmp(iter_name, name) == 0)
1098                                         {
1099                                         found = TRUE;
1100                                         break;
1101                                         }
1102                                 }
1103                         g_free(iter_name);
1104                         if (!found) return FALSE;
1105                         }
1106
1107                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1108                 iter = parent;
1109                 }
1110 }
1111
1112 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1113 {
1114         gboolean ret;
1115         GList *casefold_list = NULL;
1116         GList *work;
1117
1118         if (options->metadata.keywords_case_sensitive)
1119                 {
1120                 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1121                 }
1122         else
1123                 {
1124                 work = kw_list;
1125                 while (work)
1126                         {
1127                         const gchar *kw = work->data;
1128                         work = work->next;
1129
1130                         casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1131                         }
1132
1133                 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1134
1135                 string_list_free(casefold_list);
1136                 }
1137
1138         return ret;
1139 }
1140
1141 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1142 {
1143         GtkTreeIter iter = *iter_ptr;
1144         while (TRUE)
1145                 {
1146                 GtkTreeIter parent;
1147
1148                 if (keyword_get_is_keyword(keyword_tree, &iter))
1149                         {
1150                         gchar *name = keyword_get_name(keyword_tree, &iter);
1151                         if (!find_string_in_list_utf8nocase(*kw_list, name))
1152                                 {
1153                                 *kw_list = g_list_append(*kw_list, name);
1154                                 }
1155                         else
1156                                 {
1157                                 g_free(name);
1158                                 }
1159                         }
1160
1161                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1162                 iter = parent;
1163                 }
1164 }
1165
1166 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1167 {
1168         gchar *found;
1169         gchar *name;
1170         if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1171
1172         name = keyword_get_name(keyword_tree, iter);
1173         found = find_string_in_list_utf8nocase(*kw_list, name);
1174
1175         if (found)
1176                 {
1177                 *kw_list = g_list_remove(*kw_list, found);
1178                 g_free(found);
1179                 }
1180         g_free(name);
1181 }
1182
1183 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1184 {
1185         GtkTreeIter child;
1186         keyword_tree_reset1(keyword_tree, iter, kw_list);
1187         
1188         if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1189
1190         while (TRUE)
1191                 {
1192                 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1193                 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1194                 }
1195 }
1196
1197 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1198 {
1199         GtkTreeIter iter;
1200         
1201         if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent)) 
1202                 return TRUE; /* this should happen only on empty helpers */
1203
1204         while (TRUE)
1205                 {
1206                 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1207                 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1208                 }
1209 }
1210
1211 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1212 {
1213         GtkTreeIter iter = *iter_ptr;
1214         GtkTreeIter parent;
1215         keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1216
1217         if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1218         iter = parent;
1219         
1220         while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1221                 {
1222                 GtkTreeIter parent;
1223                 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1224                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1225                 iter = parent;
1226                 }
1227 }
1228
1229 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1230 {
1231         GList *list;
1232         GtkTreeIter child;
1233         while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1234                 {
1235                 keyword_delete(keyword_tree, &child);
1236                 }
1237         
1238         meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1239
1240         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1241         g_list_free(list);
1242         
1243         gtk_tree_store_remove(keyword_tree, iter_ptr);
1244 }
1245
1246
1247 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1248 {
1249         GList *list;
1250         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1251         if (!g_list_find(list, id))
1252                 {
1253                 list = g_list_prepend(list, id);
1254                 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1255                 }
1256 }
1257
1258 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1259 {
1260         GList *list;
1261         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1262         list = g_list_remove(list, id);
1263         gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1264 }
1265
1266 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1267 {
1268         GList *list;
1269         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1270         return !!g_list_find(list, id);
1271 }
1272
1273 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1274 {
1275         keyword_show_in(GTK_TREE_STORE(model), iter, data);
1276         return FALSE;
1277 }
1278
1279 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1280 {
1281         gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1282 }
1283
1284 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1285 {
1286         GtkTreeIter iter = *iter_ptr;
1287         while (TRUE)
1288                 {
1289                 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1290                         {
1291                         keyword_hide_in(keyword_tree, &iter, id);
1292                         /* no need to check children of hidden node */
1293                         }
1294                 else
1295                         {
1296                         GtkTreeIter child;
1297                         if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter)) 
1298                                 {
1299                                 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1300                                 }
1301                         }
1302                 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1303                 }
1304 }
1305
1306 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1307 {
1308         GtkTreeIter iter;
1309         if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1310         keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1311 }
1312
1313 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1314 {
1315         GtkTreeIter iter = *iter_ptr;
1316         GList *keywords = data;
1317         gpointer id = keywords->data;
1318         keywords = keywords->next; /* hack */
1319         if (keyword_tree_is_set(model, &iter, keywords))
1320                 {
1321                 while (TRUE)
1322                         {
1323                         GtkTreeIter parent;
1324                         keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1325                         if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1326                         iter = parent;
1327                         }
1328                 }
1329         return FALSE;
1330 }
1331
1332 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1333 {
1334         /* hack: pass id to keyword_hide_unset_in_cb in the list */
1335         keywords = g_list_prepend(keywords, id);
1336         gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1337         keywords = g_list_delete_link(keywords, keywords);
1338 }
1339
1340
1341 void keyword_tree_new(void)
1342 {
1343         if (keyword_tree) return;
1344         
1345         keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1346 }
1347
1348 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1349 {
1350         GtkTreeIter iter;
1351         gtk_tree_store_append(keyword_tree, &iter, parent);
1352         keyword_set(keyword_tree, &iter, name, is_keyword);
1353         return iter;
1354 }
1355
1356 void keyword_tree_new_default(void)
1357 {
1358         GtkTreeIter i1, i2, i3;
1359
1360         if (!keyword_tree) keyword_tree_new();
1361
1362         i1 = keyword_tree_default_append(keyword_tree, NULL, _("People"), TRUE); 
1363                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE); 
1364                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE); 
1365                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE); 
1366                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE); 
1367                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE); 
1368                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE); 
1369         i1 = keyword_tree_default_append(keyword_tree, NULL, _("Nature"), TRUE); 
1370                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE); 
1371                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE); 
1372                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE); 
1373                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE); 
1374                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE); 
1375                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE); 
1376                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE); 
1377                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE); 
1378                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE); 
1379                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE); 
1380                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE); 
1381                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE); 
1382                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE); 
1383                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE); 
1384         i1 = keyword_tree_default_append(keyword_tree, NULL, _("Art"), TRUE); 
1385                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE); 
1386                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE); 
1387                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE); 
1388                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE); 
1389         i1 = keyword_tree_default_append(keyword_tree, NULL, _("City"), TRUE); 
1390                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE); 
1391                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE); 
1392                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE); 
1393         i1 = keyword_tree_default_append(keyword_tree, NULL, _("Architecture"), TRUE); 
1394                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE); 
1395                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE); 
1396                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE); 
1397                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE); 
1398                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE); 
1399                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE); 
1400                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE); 
1401                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE); 
1402                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE); 
1403         i1 = keyword_tree_default_append(keyword_tree, NULL, _("Places"), FALSE); 
1404         i1 = keyword_tree_default_append(keyword_tree, NULL, _("Conditions"), FALSE); 
1405                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE); 
1406                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE); 
1407                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE); 
1408                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE); 
1409                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE); 
1410                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE); 
1411                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE); 
1412                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE); 
1413                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE); 
1414                         i3 = keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE); 
1415         i1 = keyword_tree_default_append(keyword_tree, NULL, _("Photo"), FALSE); 
1416                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE); 
1417                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE); 
1418                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE); 
1419                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE); 
1420                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE); 
1421                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE); 
1422 }
1423
1424
1425 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1426 {
1427         GtkTreeIter iter = *iter_ptr;
1428         while (TRUE)
1429                 {
1430                 GtkTreeIter children;
1431                 gchar *name;
1432
1433                 WRITE_NL(); WRITE_STRING("<keyword ");
1434                 name = keyword_get_name(keyword_tree, &iter);
1435                 write_char_option(outstr, indent, "name", name);
1436                 g_free(name);
1437                 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1438                 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter)) 
1439                         {
1440                         WRITE_STRING(">");
1441                         indent++;
1442                         keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1443                         indent--;
1444                         WRITE_NL(); WRITE_STRING("</keyword>");
1445                         }
1446                 else 
1447                         {
1448                         WRITE_STRING("/>");
1449                         }
1450                 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1451                 }
1452 }
1453
1454 void keyword_tree_write_config(GString *outstr, gint indent)
1455 {
1456         GtkTreeIter iter;
1457         WRITE_NL(); WRITE_STRING("<keyword_tree>");
1458         indent++;
1459         
1460         if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1461                 {
1462                 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1463                 }
1464         indent--;
1465         WRITE_NL(); WRITE_STRING("</keyword_tree>");
1466 }
1467
1468 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1469 {
1470         gchar *name = NULL;
1471         gboolean is_kw = TRUE;
1472
1473         while (*attribute_names)
1474                 {
1475                 const gchar *option = *attribute_names++;
1476                 const gchar *value = *attribute_values++;
1477
1478                 if (READ_CHAR_FULL("name", name)) continue;
1479                 if (READ_BOOL_FULL("kw", is_kw)) continue;
1480
1481                 log_printf("unknown attribute %s = %s\n", option, value);
1482                 }
1483         if (name && name[0]) 
1484                 {
1485                 GtkTreeIter iter;
1486                 /* re-use existing keyword if any */
1487                 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1488                         {
1489                         gtk_tree_store_append(keyword_tree, &iter, parent);
1490                         }
1491                 keyword_set(keyword_tree, &iter, name, is_kw);
1492                 g_free(name);
1493                 return gtk_tree_iter_copy(&iter);
1494                 }
1495         g_free(name);
1496         return NULL;
1497 }
1498
1499 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */