fixed possible crash
[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 gint metadata_write_idle_id = -1;
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 != -1) 
63                 {
64                 g_source_remove(metadata_write_idle_id);
65                 metadata_write_idle_id = -1;
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 = -1;
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
477         if (strcmp(key, COMMENT_KEY) == 0)
478                 {
479                 gchar *comment = NULL;
480                 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
481                 }
482         
483         if (strncmp(key, "file.", 5) == 0)
484                 {
485                 return g_list_append(NULL, metadata_file_info(fd, key, format));
486                 }
487         
488         exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
489         if (!exif) return NULL;
490         list = exif_get_metadata(exif, key, format);
491         exif_free_fd(fd, exif);
492         return list;
493 }
494
495 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
496 {
497         GList *string_list = metadata_read_list(fd, key, format);
498         if (string_list)
499                 {
500                 gchar *str = string_list->data;
501                 string_list->data = NULL;
502                 string_list_free(string_list);
503                 return str;
504                 }
505         return NULL;
506 }
507
508 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
509 {
510         guint64 ret;
511         gchar *endptr;
512         gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
513         if (!string) return fallback;
514         
515         ret = g_ascii_strtoull(string, &endptr, 10);
516         if (string == endptr) ret = fallback;
517         g_free(string);
518         return ret;
519 }
520         
521 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
522 {
523         gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
524         
525         if (!str) 
526                 {
527                 return metadata_write_string(fd, key, value);
528                 }
529         else
530                 {
531                 gchar *new_string = g_strconcat(str, value, NULL);
532                 gboolean ret = metadata_write_string(fd, key, new_string);
533                 g_free(str);
534                 g_free(new_string);
535                 return ret;
536                 }
537 }
538
539 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
540 {
541         GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
542         
543         if (!list) 
544                 {
545                 return metadata_write_list(fd, key, values);
546                 }
547         else
548                 {
549                 gboolean ret;
550                 list = g_list_concat(list, string_list_copy(values));
551                 list = remove_duplicate_strings_from_list(list);
552                 
553                 ret = metadata_write_list(fd, key, list);
554                 string_list_free(list);
555                 return ret;
556                 }
557 }
558
559 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
560 {
561         gchar *string_casefold = g_utf8_casefold(string, -1);
562
563         while (list)
564                 {
565                 gchar *haystack = list->data;
566                 
567                 if (haystack)
568                         {
569                         gboolean equal;
570                         gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
571
572                         equal = (strcmp(haystack_casefold, string_casefold) == 0);
573                         g_free(haystack_casefold);
574
575                         if (equal)
576                                 {
577                                 g_free(string_casefold);
578                                 return haystack;
579                                 }
580                         }
581         
582                 list = list->next;
583                 }
584         
585         g_free(string_casefold);
586         return NULL;
587 }
588
589
590 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
591
592 GList *string_to_keywords_list(const gchar *text)
593 {
594         GList *list = NULL;
595         const gchar *ptr = text;
596
597         while (*ptr != '\0')
598                 {
599                 const gchar *begin;
600                 gint l = 0;
601
602                 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
603                 begin = ptr;
604                 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
605                         {
606                         ptr++;
607                         l++;
608                         }
609
610                 /* trim starting and ending whitespaces */
611                 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
612                 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
613
614                 if (l > 0)
615                         {
616                         gchar *keyword = g_strndup(begin, l);
617
618                         /* only add if not already in the list */
619                         if (!find_string_in_list_utf8nocase(list, keyword))
620                                 list = g_list_append(list, keyword);
621                         else
622                                 g_free(keyword);
623                         }
624                 }
625
626         return list;
627 }
628
629 /*
630  * keywords to marks
631  */
632  
633
634 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
635 {
636         /* FIXME: do not use global keyword_tree */
637         GList *path = data;
638         GList *keywords;
639         gboolean found = FALSE;
640         keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
641         if (keywords)
642                 {
643                 GtkTreeIter iter;
644                 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
645                     keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
646                         found = TRUE;
647
648                 }
649         return found;
650 }
651
652 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
653 {
654         GList *path = data;
655         GList *keywords = NULL;
656         GtkTreeIter iter;
657         
658         if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
659
660         keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
661
662         if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
663                 {
664                 if (value) 
665                         {
666                         keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
667                         }
668                 else
669                         {
670                         keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
671                         }
672                 metadata_write_list(fd, KEYWORD_KEY, keywords);
673                 }
674
675         string_list_free(keywords);
676         return TRUE;
677 }
678
679
680
681 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
682 {
683
684         FileDataGetMarkFunc get_mark_func;
685         FileDataSetMarkFunc set_mark_func;
686         gpointer mark_func_data;
687
688         gint i;
689
690         for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
691                 {
692                 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
693                 if (get_mark_func == meta_data_get_keyword_mark) 
694                         {
695                         GtkTreeIter old_kw_iter;
696                         GList *old_path = mark_func_data;
697                         
698                         if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) && 
699                             (i == mark || /* release any previous connection of given mark */
700                              keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
701                                 {
702                                 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
703                                 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
704                                 }
705                         }
706                 }
707
708
709         if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
710                 {
711                 GList *path;
712                 gchar *mark_str;
713                 path = keyword_tree_get_path(keyword_tree, kw_iter);
714                 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
715                 
716                 mark_str = g_strdup_printf("%d", mark + 1);
717                 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
718                 g_free(mark_str);
719                 }
720 }
721
722
723 /*
724  *-------------------------------------------------------------------
725  * keyword tree
726  *-------------------------------------------------------------------
727  */
728
729
730
731 GtkTreeStore *keyword_tree;
732
733 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
734 {
735         gchar *name;
736         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
737         return name;
738 }
739
740 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
741 {
742         gchar *casefold;
743         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
744         return casefold;
745 }
746
747 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
748 {
749         gboolean is_keyword;
750         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
751         return is_keyword;
752 }
753
754 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
755 {
756         gchar *casefold = g_utf8_casefold(name, -1);
757         gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
758                                                 KEYWORD_COLUMN_NAME, name,
759                                                 KEYWORD_COLUMN_CASEFOLD, casefold,
760                                                 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
761         g_free(casefold);
762 }
763
764 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
765 {
766         GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
767         GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
768         gint ret = gtk_tree_path_compare(pa, pb);
769         gtk_tree_path_free(pa);
770         gtk_tree_path_free(pb);
771         return ret;
772 }
773
774 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
775 {
776         GtkTreeIter parent_a;
777         GtkTreeIter parent_b;
778         
779         gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
780         gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
781
782         if (valid_pa && valid_pb)
783                 {
784                 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
785                 }
786         else
787                 {
788                 return (!valid_pa && !valid_pb); /* both are toplevel */
789                 }
790 }
791
792 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
793 {
794         GtkTreeIter parent;
795         GtkTreeIter iter;
796         gboolean toplevel = FALSE;
797         gboolean ret;
798         gchar *casefold;
799         
800         if (parent_ptr)
801                 {
802                 parent = *parent_ptr;
803                 }
804         else if (sibling)
805                 {
806                 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
807                 }
808         else
809                 {
810                 toplevel = TRUE;
811                 }
812         
813         if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
814         
815         casefold = g_utf8_casefold(name, -1);
816         ret = FALSE;
817         
818         while (TRUE)
819                 {
820                 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
821                         {
822                         gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
823                         ret = strcmp(casefold, iter_casefold) == 0;
824                         g_free(iter_casefold);
825                         }
826                 if (ret) 
827                         {
828                         if (result) *result = iter;
829                         break;
830                         }
831                 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
832                 }
833         g_free(casefold);
834         return ret;
835 }
836
837
838 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
839 {
840
841         gchar *mark, *name, *casefold;
842         gboolean is_keyword;
843
844         /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
845         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
846                                                 KEYWORD_COLUMN_NAME, &name,
847                                                 KEYWORD_COLUMN_CASEFOLD, &casefold,
848                                                 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
849
850         gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
851                                                 KEYWORD_COLUMN_NAME, name,
852                                                 KEYWORD_COLUMN_CASEFOLD, casefold,
853                                                 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
854         g_free(mark);
855         g_free(name);
856         g_free(casefold);
857 }
858
859 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
860 {
861         GtkTreeIter from_child;
862         
863         keyword_copy(keyword_tree, to, from);
864         
865         if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
866         
867         while (TRUE)
868                 {
869                 GtkTreeIter to_child;
870                 gtk_tree_store_append(keyword_tree, &to_child, to);
871                 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
872                 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
873                 }
874 }
875
876 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
877 {
878         keyword_copy_recursive(keyword_tree, to, from);
879         keyword_delete(keyword_tree, from);
880 }
881
882 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
883 {
884         GList *path = NULL;
885         GtkTreeIter iter = *iter_ptr;
886         
887         while (TRUE)
888                 {
889                 GtkTreeIter parent;
890                 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
891                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
892                 iter = parent;
893                 }
894         return path;
895 }
896
897 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
898 {
899         GtkTreeIter iter;
900
901         if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
902         
903         while (TRUE)
904                 {
905                 GtkTreeIter children;
906                 while (TRUE)
907                         {
908                         gchar *name = keyword_get_name(keyword_tree, &iter);
909                         if (strcmp(name, path->data) == 0) break;
910                         g_free(name);
911                         if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
912                         }
913                 path = path->next;
914                 if (!path) 
915                         {
916                         *iter_ptr = iter;
917                         return TRUE;
918                         }
919                         
920                 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
921                 iter = children;
922                 }
923 }
924
925
926 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
927 {
928         if (!casefold_list) return FALSE;
929
930         if (!keyword_get_is_keyword(keyword_tree, &iter))
931                 {
932                 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
933                 GtkTreeIter child;
934                 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter)) 
935                         return FALSE; /* this should happen only on empty helpers */
936
937                 while (TRUE)
938                         {
939                         if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
940                         if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
941                         }
942                 }
943         
944         while (TRUE)
945                 {
946                 GtkTreeIter parent;
947
948                 if (keyword_get_is_keyword(keyword_tree, &iter))
949                         {
950                         GList *work = casefold_list;
951                         gboolean found = FALSE;
952                         gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
953                         while (work)
954                                 {
955                                 const gchar *casefold = work->data;
956                                 work = work->next;
957
958                                 if (strcmp(iter_casefold, casefold) == 0)
959                                         {
960                                         found = TRUE;
961                                         break;
962                                         }
963                                 }
964                         g_free(iter_casefold);
965                         if (!found) return FALSE;
966                         }
967                 
968                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
969                 iter = parent;
970                 }
971 }
972
973 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
974 {
975         gboolean ret;
976         GList *casefold_list = NULL;
977         GList *work;
978
979         work = kw_list;
980         while (work)
981                 {
982                 const gchar *kw = work->data;
983                 work = work->next;
984
985                 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
986                 }
987         
988         ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
989         
990         string_list_free(casefold_list);
991         return ret;
992 }
993
994 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
995 {
996         GtkTreeIter iter = *iter_ptr;
997         while (TRUE)
998                 {
999                 GtkTreeIter parent;
1000
1001                 if (keyword_get_is_keyword(keyword_tree, &iter))
1002                         {
1003                         gchar *name = keyword_get_name(keyword_tree, &iter);
1004                         if (!find_string_in_list_utf8nocase(*kw_list, name))
1005                                 {
1006                                 *kw_list = g_list_append(*kw_list, name);
1007                                 }
1008                         else
1009                                 {
1010                                 g_free(name);
1011                                 }
1012                         }
1013
1014                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1015                 iter = parent;
1016                 }
1017 }
1018
1019 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1020 {
1021         gchar *found;
1022         gchar *name;
1023         if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1024
1025         name = keyword_get_name(keyword_tree, iter);
1026         found = find_string_in_list_utf8nocase(*kw_list, name);
1027
1028         if (found)
1029                 {
1030                 *kw_list = g_list_remove(*kw_list, found);
1031                 g_free(found);
1032                 }
1033         g_free(name);
1034 }
1035
1036 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1037 {
1038         GtkTreeIter child;
1039         keyword_tree_reset1(keyword_tree, iter, kw_list);
1040         
1041         if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1042
1043         while (TRUE)
1044                 {
1045                 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1046                 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1047                 }
1048 }
1049
1050 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1051 {
1052         GtkTreeIter iter;
1053         
1054         if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent)) 
1055                 return TRUE; /* this should happen only on empty helpers */
1056
1057         while (TRUE)
1058                 {
1059                 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1060                 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1061                 }
1062 }
1063
1064 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1065 {
1066         GtkTreeIter iter = *iter_ptr;
1067         GtkTreeIter parent;
1068         keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1069
1070         if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1071         iter = parent;
1072         
1073         while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1074                 {
1075                 GtkTreeIter parent;
1076                 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1077                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1078                 iter = parent;
1079                 }
1080 }
1081
1082 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1083 {
1084         GList *list;
1085         GtkTreeIter child;
1086         while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1087                 {
1088                 keyword_delete(keyword_tree, &child);
1089                 }
1090         
1091         meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1092
1093         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1094         g_list_free(list);
1095         
1096         gtk_tree_store_remove(keyword_tree, iter_ptr);
1097 }
1098
1099
1100 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1101 {
1102         GList *list;
1103         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1104         if (!g_list_find(list, id))
1105                 {
1106                 list = g_list_prepend(list, id);
1107                 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1108                 }
1109 }
1110
1111 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1112 {
1113         GList *list;
1114         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1115         list = g_list_remove(list, id);
1116         gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1117 }
1118
1119 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1120 {
1121         GList *list;
1122         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1123         return !!g_list_find(list, id);
1124 }
1125
1126 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1127 {
1128         keyword_show_in(GTK_TREE_STORE(model), iter, data);
1129         return FALSE;
1130 }
1131
1132 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1133 {
1134         gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1135 }
1136
1137 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1138 {
1139         GtkTreeIter iter = *iter_ptr;
1140         while (TRUE)
1141                 {
1142                 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1143                         {
1144                         keyword_hide_in(keyword_tree, &iter, id);
1145                         /* no need to check children of hidden node */
1146                         }
1147                 else
1148                         {
1149                         GtkTreeIter child;
1150                         if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter)) 
1151                                 {
1152                                 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1153                                 }
1154                         }
1155                 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1156                 }
1157 }
1158
1159 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1160 {
1161         GtkTreeIter iter;
1162         if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1163         keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1164 }
1165
1166 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1167 {
1168         GtkTreeIter iter = *iter_ptr;
1169         GList *keywords = data;
1170         gpointer id = keywords->data;
1171         keywords = keywords->next; /* hack */
1172         if (keyword_tree_is_set(model, &iter, keywords))
1173                 {
1174                 while (TRUE)
1175                         {
1176                         GtkTreeIter parent;
1177                         keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1178                         if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1179                         iter = parent;
1180                         }
1181                 }
1182         return FALSE;
1183 }
1184
1185 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1186 {
1187         /* hack: pass id to keyword_hide_unset_in_cb in the list */
1188         keywords = g_list_prepend(keywords, id);
1189         gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1190         keywords = g_list_delete_link(keywords, keywords);
1191 }
1192
1193
1194 void keyword_tree_new(void)
1195 {
1196         if (keyword_tree) return;
1197         
1198         keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1199 }
1200
1201
1202 void keyword_tree_new_default(void)
1203 {
1204         if (keyword_tree) return;
1205         
1206         keyword_tree_new();
1207
1208         GtkTreeIter i1, i2, i3;
1209
1210         gtk_tree_store_append(keyword_tree, &i1, NULL);
1211         keyword_set(keyword_tree, &i1, "animal", TRUE);
1212
1213                 gtk_tree_store_append(keyword_tree, &i2, &i1);
1214                 keyword_set(keyword_tree, &i2, "mammal", TRUE);
1215
1216                         gtk_tree_store_append(keyword_tree, &i3, &i2);
1217                         keyword_set(keyword_tree, &i3, "dog", TRUE);
1218
1219                         gtk_tree_store_append(keyword_tree, &i3, &i2);
1220                         keyword_set(keyword_tree, &i3, "cat", TRUE);
1221
1222                 gtk_tree_store_append(keyword_tree, &i2, &i1);
1223                 keyword_set(keyword_tree, &i2, "insect", TRUE);
1224
1225                         gtk_tree_store_append(keyword_tree, &i3, &i2);
1226                         keyword_set(keyword_tree, &i3, "fly", TRUE);
1227
1228                         gtk_tree_store_append(keyword_tree, &i3, &i2);
1229                         keyword_set(keyword_tree, &i3, "dragonfly", TRUE);
1230
1231         gtk_tree_store_append(keyword_tree, &i1, NULL);
1232         keyword_set(keyword_tree, &i1, "daytime", FALSE);
1233
1234                 gtk_tree_store_append(keyword_tree, &i2, &i1);
1235                 keyword_set(keyword_tree, &i2, "morning", TRUE);
1236
1237                 gtk_tree_store_append(keyword_tree, &i2, &i1);
1238                 keyword_set(keyword_tree, &i2, "noon", TRUE);
1239
1240 }
1241
1242
1243 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1244 {
1245         GtkTreeIter iter = *iter_ptr;
1246         while (TRUE)
1247                 {
1248                 GtkTreeIter children;
1249                 gchar *name;
1250
1251                 WRITE_NL(); WRITE_STRING("<keyword ");
1252                 name = keyword_get_name(keyword_tree, &iter);
1253                 write_char_option(outstr, indent, "name", name);
1254                 g_free(name);
1255                 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1256                 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter)) 
1257                         {
1258                         WRITE_STRING(">");
1259                         indent++;
1260                         keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1261                         indent--;
1262                         WRITE_NL(); WRITE_STRING("</keyword>");
1263                         }
1264                 else 
1265                         {
1266                         WRITE_STRING("/>");
1267                         }
1268                 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1269                 }
1270 }
1271
1272 void keyword_tree_write_config(GString *outstr, gint indent)
1273 {
1274         GtkTreeIter iter;
1275         WRITE_NL(); WRITE_STRING("<keyword_tree>");
1276         indent++;
1277         
1278         if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1279                 {
1280                 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1281                 }
1282         indent--;
1283         WRITE_NL(); WRITE_STRING("</keyword_tree>");
1284 }
1285
1286 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1287 {
1288         gchar *name = NULL;
1289         gboolean is_kw = TRUE;
1290
1291         while (*attribute_names)
1292                 {
1293                 const gchar *option = *attribute_names++;
1294                 const gchar *value = *attribute_values++;
1295
1296                 if (READ_CHAR_FULL("name", name)) continue;
1297                 if (READ_BOOL_FULL("kw", is_kw)) continue;
1298
1299                 log_printf("unknown attribute %s = %s\n", option, value);
1300                 }
1301         if (name && name[0]) 
1302                 {
1303                 GtkTreeIter iter;
1304                 /* re-use existing keyword if any */
1305                 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1306                         {
1307                         gtk_tree_store_append(keyword_tree, &iter, parent);
1308                         }
1309                 keyword_set(keyword_tree, &iter, name, is_kw);
1310                 g_free(name);
1311                 return gtk_tree_iter_copy(&iter);
1312                 }
1313         g_free(name);
1314         return NULL;
1315 }
1316
1317 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */