added a popup menu in keyword tree
[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         GList *keywords;
631         gboolean found = FALSE;
632         keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
633         if (keywords)
634                 {
635                 GList *work = keywords;
636
637                 while (work)
638                         {
639                         gchar *kw = work->data;
640                         work = work->next;
641                         
642                         if (strcmp(kw, data) == 0)
643                                 {
644                                 found = TRUE;
645                                 break;
646                                 }
647                         }
648                 string_list_free(keywords);
649                 }
650         return found;
651 }
652
653 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
654 {
655         GList *keywords = NULL;
656         gboolean found = FALSE;
657         gboolean changed = FALSE;
658         GList *work;
659         keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
660
661         work = keywords;
662
663         while (work)
664                 {
665                 gchar *kw = work->data;
666                 
667                 if (strcmp(kw, data) == 0)
668                         {
669                         found = TRUE;
670                         if (!value) 
671                                 {
672                                 changed = TRUE;
673                                 keywords = g_list_delete_link(keywords, work);
674                                 g_free(kw);
675                                 }
676                         break;
677                         }
678                 work = work->next;
679                 }
680         if (value && !found) 
681                 {
682                 changed = TRUE;
683                 keywords = g_list_append(keywords, g_strdup(data));
684                 }
685         
686         if (changed) metadata_write_list(fd, KEYWORD_KEY, keywords);
687
688         string_list_free(keywords);
689         return TRUE;
690 }
691
692 /*
693  *-------------------------------------------------------------------
694  * keyword tree
695  *-------------------------------------------------------------------
696  */
697
698
699
700 GtkTreeStore *keyword_tree;
701
702 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
703 {
704         gchar *name;
705         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
706         return name;
707 }
708
709 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
710 {
711         gchar *casefold;
712         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
713         return casefold;
714 }
715
716 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
717 {
718         gboolean is_keyword;
719         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
720         return is_keyword;
721 }
722
723 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
724 {
725         gchar *casefold = g_utf8_casefold(name, -1);
726         gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
727                                                 KEYWORD_COLUMN_NAME, name,
728                                                 KEYWORD_COLUMN_CASEFOLD, casefold,
729                                                 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
730         g_free(casefold);
731 }
732
733 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
734 {
735         GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
736         GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
737         gint ret = gtk_tree_path_compare(pa, pb);
738         gtk_tree_path_free(pa);
739         gtk_tree_path_free(pb);
740         return ret;
741 }
742
743 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
744 {
745
746         gchar *mark, *name, *casefold;
747         gboolean is_keyword;
748
749         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
750                                                 KEYWORD_COLUMN_NAME, &name,
751                                                 KEYWORD_COLUMN_CASEFOLD, &casefold,
752                                                 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
753
754         gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
755                                                 KEYWORD_COLUMN_NAME, name,
756                                                 KEYWORD_COLUMN_CASEFOLD, casefold,
757                                                 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
758         g_free(mark);
759         g_free(name);
760         g_free(casefold);
761 }
762
763 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
764 {
765         GtkTreeIter from_child;
766         
767         keyword_copy(keyword_tree, to, from);
768         
769         if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
770         
771         while (TRUE)
772                 {
773                 GtkTreeIter to_child;
774                 gtk_tree_store_append(keyword_tree, &to_child, to);
775                 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
776                 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
777                 }
778 }
779
780 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
781 {
782         keyword_copy_recursive(keyword_tree, to, from);
783         gtk_tree_store_remove(keyword_tree, from);
784 }
785
786 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
787 {
788         GList *path = NULL;
789         GtkTreeIter iter = *iter_ptr;
790         
791         while (TRUE)
792                 {
793                 GtkTreeIter parent;
794                 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
795                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
796                 iter = parent;
797                 }
798         return path;
799 }
800
801 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
802 {
803         GtkTreeIter iter;
804
805         if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
806         
807         while (TRUE)
808                 {
809                 GtkTreeIter children;
810                 while (TRUE)
811                         {
812                         gchar *name = keyword_get_name(keyword_tree, &iter);
813                         if (strcmp(name, path->data) == 0) break;
814                         g_free(name);
815                         if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
816                         }
817                 path = path->next;
818                 if (!path) 
819                         {
820                         *iter_ptr = iter;
821                         return TRUE;
822                         }
823                         
824                 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
825                 iter = children;
826                 }
827 }
828
829
830 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
831 {
832         if (!casefold_list) return FALSE;
833         
834         while (TRUE)
835                 {
836                 GtkTreeIter parent;
837
838                 if (keyword_get_is_keyword(keyword_tree, &iter))
839                         {
840                         GList *work = casefold_list;
841                         gboolean found = FALSE;
842                         gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
843                         while (work)
844                                 {
845                                 const gchar *casefold = work->data;
846                                 work = work->next;
847
848                                 if (strcmp(iter_casefold, casefold) == 0)
849                                         {
850                                         found = TRUE;
851                                         break;
852                                         }
853                                 }
854                         g_free(iter_casefold);
855                         if (!found) return FALSE;
856                         }
857                 
858                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
859                 iter = parent;
860                 }
861 }
862
863 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
864 {
865         gboolean ret;
866         GList *casefold_list = NULL;
867         GList *work;
868
869         if (!keyword_get_is_keyword(keyword_tree, iter)) return FALSE;
870         
871         work = kw_list;
872         while (work)
873                 {
874                 const gchar *kw = work->data;
875                 work = work->next;
876                 
877                 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
878                 }
879         
880         ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
881         
882         string_list_free(casefold_list);
883         return ret;
884 }
885
886 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
887 {
888         GtkTreeIter iter = *iter_ptr;
889         while (TRUE)
890                 {
891                 GtkTreeIter parent;
892
893                 if (keyword_get_is_keyword(keyword_tree, &iter))
894                         {
895                         gchar *name = keyword_get_name(keyword_tree, &iter);
896                         if (!find_string_in_list_utf8nocase(*kw_list, name))
897                                 {
898                                 *kw_list = g_list_append(*kw_list, name);
899                                 }
900                         else
901                                 {
902                                 g_free(name);
903                                 }
904                         }
905
906                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
907                 iter = parent;
908                 }
909 }
910
911 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
912 {
913         gchar *found;
914         gchar *name;
915         if (!keyword_get_is_keyword(keyword_tree, iter)) return;
916
917         name = keyword_get_name(keyword_tree, iter);
918         found = find_string_in_list_utf8nocase(*kw_list, name);
919
920         if (found)
921                 {
922                 *kw_list = g_list_remove(*kw_list, found);
923                 g_free(found);
924                 }
925         g_free(name);
926 }
927
928 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
929 {
930         GtkTreeIter child;
931         keyword_tree_reset1(keyword_tree, iter, kw_list);
932         
933         if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
934
935         while (TRUE)
936                 {
937                 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
938                 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
939                 }
940 }
941
942 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
943 {
944         GtkTreeIter iter;
945         
946         if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent)) 
947                 return TRUE; /* this should happen only on empty helpers */
948
949         while (TRUE)
950                 {
951                 if (keyword_get_is_keyword(keyword_tree, &iter))
952                         {
953                         if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
954                         }
955                 else
956                         {
957                         /* for helpers we have to check recursively */
958                         if (!keyword_tree_check_empty_children(keyword_tree, &iter, kw_list)) return FALSE;
959                         }
960                 
961                 if (!gtk_tree_model_iter_next(keyword_tree, &iter))
962                         {
963                         return TRUE;
964                         }
965                 }
966 }
967
968 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
969 {
970         GtkTreeIter iter = *iter_ptr;
971         GtkTreeIter parent;
972         keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
973
974         if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
975         iter = parent;
976         
977         while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
978                 {
979                 GtkTreeIter parent;
980                 keyword_tree_reset1(keyword_tree, &iter, kw_list);
981                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
982                 iter = parent;
983                 }
984 }
985
986 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
987 {
988         gtk_tree_store_remove(keyword_tree, iter_ptr);
989 }
990
991
992 void keyword_tree_new(void)
993 {
994         if (keyword_tree) return;
995         
996         keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
997 }
998
999
1000 void keyword_tree_new_default(void)
1001 {
1002         if (keyword_tree) return;
1003         
1004         keyword_tree_new();
1005
1006         GtkTreeIter i1, i2, i3;
1007
1008         gtk_tree_store_append(keyword_tree, &i1, NULL);
1009         keyword_set(keyword_tree, &i1, "animal", TRUE);
1010
1011                 gtk_tree_store_append(keyword_tree, &i2, &i1);
1012                 keyword_set(keyword_tree, &i2, "mammal", TRUE);
1013
1014                         gtk_tree_store_append(keyword_tree, &i3, &i2);
1015                         keyword_set(keyword_tree, &i3, "dog", TRUE);
1016
1017                         gtk_tree_store_append(keyword_tree, &i3, &i2);
1018                         keyword_set(keyword_tree, &i3, "cat", TRUE);
1019
1020                 gtk_tree_store_append(keyword_tree, &i2, &i1);
1021                 keyword_set(keyword_tree, &i2, "insect", TRUE);
1022
1023                         gtk_tree_store_append(keyword_tree, &i3, &i2);
1024                         keyword_set(keyword_tree, &i3, "fly", TRUE);
1025
1026                         gtk_tree_store_append(keyword_tree, &i3, &i2);
1027                         keyword_set(keyword_tree, &i3, "dragonfly", TRUE);
1028
1029         gtk_tree_store_append(keyword_tree, &i1, NULL);
1030         keyword_set(keyword_tree, &i1, "daytime", FALSE);
1031
1032                 gtk_tree_store_append(keyword_tree, &i2, &i1);
1033                 keyword_set(keyword_tree, &i2, "morning", TRUE);
1034
1035                 gtk_tree_store_append(keyword_tree, &i2, &i1);
1036                 keyword_set(keyword_tree, &i2, "noon", TRUE);
1037
1038 }
1039
1040
1041 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1042 {
1043         GtkTreeIter iter = *iter_ptr;
1044         while (TRUE)
1045                 {
1046                 GtkTreeIter children;
1047                 gchar *name;
1048
1049                 WRITE_STRING("<keyword\n");
1050                 indent++;
1051                 name = keyword_get_name(keyword_tree, &iter);
1052                 write_char_option(outstr, indent, "name", name);
1053                 g_free(name);
1054                 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1055                 indent--;
1056                 WRITE_STRING(">\n");
1057                 indent++;
1058                 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter)) 
1059                         {
1060                         keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1061                         }
1062                 indent--;
1063                 WRITE_STRING("</keyword>\n");
1064                 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1065                 }
1066 }
1067
1068 void keyword_tree_write_config(GString *outstr, gint indent)
1069 {
1070         GtkTreeIter iter;
1071         WRITE_STRING("<keyword_tree>\n");
1072         indent++;
1073         
1074         if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1075                 {
1076                 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1077                 }
1078         indent--;
1079         WRITE_STRING("</keyword_tree>\n");
1080 }
1081
1082 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1083 {
1084         gchar *name = NULL;
1085         gboolean is_kw = TRUE;
1086
1087         while (*attribute_names)
1088                 {
1089                 const gchar *option = *attribute_names++;
1090                 const gchar *value = *attribute_values++;
1091
1092                 if (READ_CHAR_FULL("name", name)) continue;
1093                 if (READ_BOOL_FULL("kw", is_kw)) continue;
1094
1095                 DEBUG_1("unknown attribute %s = %s", option, value);
1096                 }
1097         if (name && name[0]) 
1098                 {
1099                 GtkTreeIter iter;
1100                 gtk_tree_store_append(keyword_tree, &iter, parent);
1101                 keyword_set(keyword_tree, &iter, name, is_kw);
1102                 g_free(name);
1103                 return gtk_tree_iter_copy(&iter);
1104                 }
1105         g_free(name);
1106         return NULL;
1107 }
1108
1109 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */