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