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