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