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