dnd fixes
[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
28 typedef enum {
29         MK_NONE,
30         MK_KEYWORDS,
31         MK_COMMENT
32 } MetadataKey;
33
34 static const gchar *group_keys[] = {KEYWORD_KEY, COMMENT_KEY, NULL}; /* tags that will be written to all files in a group */
35
36 static gboolean metadata_write_queue_idle_cb(gpointer data);
37 static gint metadata_legacy_write(FileData *fd);
38 static void metadata_legacy_delete(FileData *fd, const gchar *except);
39
40
41
42 /*
43  *-------------------------------------------------------------------
44  * write queue
45  *-------------------------------------------------------------------
46  */
47
48 static GList *metadata_write_queue = NULL;
49 static gint metadata_write_idle_id = -1;
50
51 static void metadata_write_queue_add(FileData *fd)
52 {
53         if (!g_list_find(metadata_write_queue, fd))
54                 {
55                 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
56                 file_data_ref(fd);
57                 
58                 layout_status_update_write_all();
59                 }
60
61         if (metadata_write_idle_id != -1) 
62                 {
63                 g_source_remove(metadata_write_idle_id);
64                 metadata_write_idle_id = -1;
65                 }
66         
67         if (options->metadata.confirm_after_timeout)
68                 {
69                 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, NULL);
70                 }
71 }
72
73
74 gboolean metadata_write_queue_remove(FileData *fd)
75 {
76         g_hash_table_destroy(fd->modified_xmp);
77         fd->modified_xmp = NULL;
78
79         metadata_write_queue = g_list_remove(metadata_write_queue, fd);
80         
81         file_data_increment_version(fd);
82         file_data_send_notification(fd, NOTIFY_TYPE_REREAD);
83
84         file_data_unref(fd);
85
86         layout_status_update_write_all();
87         return TRUE;
88 }
89
90 gboolean metadata_write_queue_remove_list(GList *list)
91 {
92         GList *work;
93         gboolean ret = TRUE;
94         
95         work = list;
96         while (work)
97                 {
98                 FileData *fd = work->data;
99                 work = work->next;
100                 ret = ret && metadata_write_queue_remove(fd);
101                 }
102         return ret;
103 }
104
105
106 gboolean metadata_write_queue_confirm(FileUtilDoneFunc done_func, gpointer done_data)
107 {
108         GList *work;
109         GList *to_approve = NULL;
110         
111         work = metadata_write_queue;
112         while (work)
113                 {
114                 FileData *fd = work->data;
115                 work = work->next;
116                 
117                 if (fd->change) continue; /* another operation in progress, skip this file for now */
118                 
119                 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
120                 }
121
122         file_util_write_metadata(NULL, to_approve, NULL, done_func, done_data);
123         
124         filelist_free(to_approve);
125         
126         return (metadata_write_queue != NULL);
127 }
128
129 static gboolean metadata_write_queue_idle_cb(gpointer data)
130 {
131         metadata_write_queue_confirm(NULL, NULL);
132         metadata_write_idle_id = -1;
133         return FALSE;
134 }
135
136 gboolean metadata_write_perform(FileData *fd)
137 {
138         gboolean success;
139         ExifData *exif;
140         
141         g_assert(fd->change);
142         
143         if (fd->change->dest && 
144             strcmp(extension_from_path(fd->change->dest), GQ_CACHE_EXT_METADATA) == 0)
145                 {
146                 success = metadata_legacy_write(fd);
147                 if (success) metadata_legacy_delete(fd, fd->change->dest);
148                 return success;
149                 }
150
151         /* write via exiv2 */
152         /*  we can either use cached metadata which have fd->modified_xmp already applied 
153                                      or read metadata from file and apply fd->modified_xmp
154             metadata are read also if the file was modified meanwhile */
155         exif = exif_read_fd(fd); 
156         if (!exif) return FALSE;
157
158         success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
159         exif_free_fd(fd, exif);
160
161         if (fd->change->dest)
162                 /* this will create a FileData for the sidecar and link it to the main file 
163                    (we can't wait until the sidecar is discovered by directory scanning because
164                     exif_read_fd is called before that and it would read the main file only and 
165                     store the metadata in the cache)
166                     FIXME: this does not catch new sidecars created by independent external programs
167                 */
168                 file_data_unref(file_data_new_simple(fd->change->dest)); 
169                 
170         if (success) metadata_legacy_delete(fd, fd->change->dest);
171         return success;
172 }
173
174 gint metadata_queue_length(void)
175 {
176         return g_list_length(metadata_write_queue);
177 }
178
179 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
180 {
181         const gchar **k = keys;
182         
183         while (*k)
184                 {
185                 if (strcmp(key, *k) == 0) return TRUE;
186                 k++;
187                 }
188         return FALSE;
189 }
190
191 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
192 {
193         if (!fd->modified_xmp)
194                 {
195                 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
196                 }
197         g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
198         if (fd->exif)
199                 {
200                 exif_update_metadata(fd->exif, key, values);
201                 }
202         metadata_write_queue_add(fd);
203         file_data_increment_version(fd);
204         file_data_send_notification(fd, NOTIFY_TYPE_INTERNAL);
205
206         if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
207                 {
208                 GList *work = fd->sidecar_files;
209                 
210                 while (work)
211                         {
212                         FileData *sfd = work->data;
213                         work = work->next;
214                         
215                         if (filter_file_class(sfd->extension, FORMAT_CLASS_META)) continue; 
216
217                         metadata_write_list(sfd, key, values);
218                         }
219                 }
220
221
222         return TRUE;
223 }
224         
225 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
226 {
227         GList *list = g_list_append(NULL, g_strdup(value));
228         gboolean ret = metadata_write_list(fd, key, list);
229         string_list_free(list);
230         return ret;
231 }
232
233
234 /*
235  *-------------------------------------------------------------------
236  * keyword / comment read/write
237  *-------------------------------------------------------------------
238  */
239
240 static gint metadata_file_write(gchar *path, GHashTable *modified_xmp)
241 {
242         SecureSaveInfo *ssi;
243         GList *keywords = g_hash_table_lookup(modified_xmp, KEYWORD_KEY);
244         GList *comment_l = g_hash_table_lookup(modified_xmp, COMMENT_KEY);
245         gchar *comment = comment_l ? comment_l->data : NULL;
246
247         ssi = secure_open(path);
248         if (!ssi) return FALSE;
249
250         secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
251
252         secure_fprintf(ssi, "[keywords]\n");
253         while (keywords && secsave_errno == SS_ERR_NONE)
254                 {
255                 const gchar *word = keywords->data;
256                 keywords = keywords->next;
257
258                 secure_fprintf(ssi, "%s\n", word);
259                 }
260         secure_fputc(ssi, '\n');
261
262         secure_fprintf(ssi, "[comment]\n");
263         secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
264
265         secure_fprintf(ssi, "#end\n");
266
267         return (secure_close(ssi) == 0);
268 }
269
270 static gint metadata_legacy_write(FileData *fd)
271 {
272         gint success = FALSE;
273
274         g_assert(fd->change && fd->change->dest);
275         gchar *metadata_pathl;
276
277         DEBUG_1("Saving comment: %s", fd->change->dest);
278
279         metadata_pathl = path_from_utf8(fd->change->dest);
280
281         success = metadata_file_write(metadata_pathl, fd->modified_xmp);
282
283         g_free(metadata_pathl);
284
285         return success;
286 }
287
288 static gint metadata_file_read(gchar *path, GList **keywords, gchar **comment)
289 {
290         FILE *f;
291         gchar s_buf[1024];
292         MetadataKey key = MK_NONE;
293         GList *list = NULL;
294         GString *comment_build = NULL;
295
296         f = fopen(path, "r");
297         if (!f) return FALSE;
298
299         while (fgets(s_buf, sizeof(s_buf), f))
300                 {
301                 gchar *ptr = s_buf;
302
303                 if (*ptr == '#') continue;
304                 if (*ptr == '[' && key != MK_COMMENT)
305                         {
306                         gchar *keystr = ++ptr;
307                         
308                         key = MK_NONE;
309                         while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
310                         
311                         if (*ptr == ']')
312                                 {
313                                 *ptr = '\0';
314                                 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
315                                         key = MK_KEYWORDS;
316                                 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
317                                         key = MK_COMMENT;
318                                 }
319                         continue;
320                         }
321                 
322                 switch (key)
323                         {
324                         case MK_NONE:
325                                 break;
326                         case MK_KEYWORDS:
327                                 {
328                                 while (*ptr != '\n' && *ptr != '\0') ptr++;
329                                 *ptr = '\0';
330                                 if (strlen(s_buf) > 0)
331                                         {
332                                         gchar *kw = utf8_validate_or_convert(s_buf);
333
334                                         list = g_list_prepend(list, kw);
335                                         }
336                                 }
337                                 break;
338                         case MK_COMMENT:
339                                 if (!comment_build) comment_build = g_string_new("");
340                                 g_string_append(comment_build, s_buf);
341                                 break;
342                         }
343                 }
344         
345         fclose(f);
346
347         if (keywords) 
348                 {
349                 *keywords = g_list_reverse(list);
350                 }
351         else
352                 {
353                 string_list_free(list);
354                 }
355                 
356         if (comment_build)
357                 {
358                 if (comment)
359                         {
360                         gint len;
361                         gchar *ptr = comment_build->str;
362
363                         /* strip leading and trailing newlines */
364                         while (*ptr == '\n') ptr++;
365                         len = strlen(ptr);
366                         while (len > 0 && ptr[len - 1] == '\n') len--;
367                         if (ptr[len] == '\n') len++; /* keep the last one */
368                         if (len > 0)
369                                 {
370                                 gchar *text = g_strndup(ptr, len);
371
372                                 *comment = utf8_validate_or_convert(text);
373                                 g_free(text);
374                                 }
375                         }
376                 g_string_free(comment_build, TRUE);
377                 }
378
379         return TRUE;
380 }
381
382 static void metadata_legacy_delete(FileData *fd, const gchar *except)
383 {
384         gchar *metadata_path;
385         gchar *metadata_pathl;
386         if (!fd) return;
387
388         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
389         if (metadata_path && (!except || strcmp(metadata_path, except) != 0)) 
390                 {
391                 metadata_pathl = path_from_utf8(metadata_path);
392                 unlink(metadata_pathl);
393                 g_free(metadata_pathl);
394                 g_free(metadata_path);
395                 }
396         metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
397         if (metadata_path && (!except || strcmp(metadata_path, except) != 0)) 
398                 {
399                 metadata_pathl = path_from_utf8(metadata_path);
400                 unlink(metadata_pathl);
401                 g_free(metadata_pathl);
402                 g_free(metadata_path);
403                 }
404 }
405
406 static gint metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
407 {
408         gchar *metadata_path;
409         gchar *metadata_pathl;
410         gint success = FALSE;
411         if (!fd) return FALSE;
412
413         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
414         if (!metadata_path) return FALSE;
415
416         metadata_pathl = path_from_utf8(metadata_path);
417
418         success = metadata_file_read(metadata_pathl, keywords, comment);
419
420         g_free(metadata_pathl);
421         g_free(metadata_path);
422
423         return success;
424 }
425
426 static GList *remove_duplicate_strings_from_list(GList *list)
427 {
428         GList *work = list;
429         GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
430         GList *newlist = NULL;
431
432         while (work)
433                 {
434                 gchar *key = work->data;
435
436                 if (g_hash_table_lookup(hashtable, key) == NULL)
437                         {
438                         g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
439                         newlist = g_list_prepend(newlist, key);
440                         }
441                 work = work->next;
442                 }
443
444         g_hash_table_destroy(hashtable);
445         g_list_free(list);
446
447         return g_list_reverse(newlist);
448 }
449
450 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
451 {
452         ExifData *exif;
453         GList *list = NULL;
454         if (!fd) return NULL;
455
456         /* unwritten data overide everything */
457         if (fd->modified_xmp && format == METADATA_PLAIN)
458                 {
459                 list = g_hash_table_lookup(fd->modified_xmp, key);
460                 if (list) return string_list_copy(list);
461                 }
462
463         /* 
464             Legacy metadata file is the primary source if it exists.
465             Merging the lists does not make much sense, because the existence of
466             legacy metadata file indicates that the other metadata sources are not
467             writable and thus it would not be possible to delete the keywords
468             that comes from the image file.
469         */
470         if (strcmp(key, KEYWORD_KEY) == 0)
471                 {
472                 if (metadata_legacy_read(fd, &list, NULL)) return list;
473                 }
474
475         if (strcmp(key, COMMENT_KEY) == 0)
476                 {
477                 gchar *comment = NULL;
478                 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
479                 }
480         
481         exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
482         if (!exif) return NULL;
483         list = exif_get_metadata(exif, key, format);
484         exif_free_fd(fd, exif);
485         return list;
486 }
487
488 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
489 {
490         GList *string_list = metadata_read_list(fd, key, format);
491         if (string_list)
492                 {
493                 gchar *str = string_list->data;
494                 string_list->data = NULL;
495                 string_list_free(string_list);
496                 return str;
497                 }
498         return NULL;
499 }
500
501 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
502 {
503         guint64 ret;
504         gchar *endptr;
505         gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
506         if (!string) return fallback;
507         
508         ret = g_ascii_strtoull(string, &endptr, 10);
509         if (string == endptr) ret = fallback;
510         g_free(string);
511         return ret;
512 }
513         
514 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
515 {
516         gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
517         
518         if (!str) 
519                 {
520                 return metadata_write_string(fd, key, value);
521                 }
522         else
523                 {
524                 gchar *new_string = g_strconcat(str, value, NULL);
525                 gboolean ret = metadata_write_string(fd, key, new_string);
526                 g_free(str);
527                 g_free(new_string);
528                 return ret;
529                 }
530 }
531
532 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
533 {
534         GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
535         
536         if (!list) 
537                 {
538                 return metadata_write_list(fd, key, values);
539                 }
540         else
541                 {
542                 gboolean ret;
543                 list = g_list_concat(list, string_list_copy(values));
544                 list = remove_duplicate_strings_from_list(list);
545                 
546                 ret = metadata_write_list(fd, key, list);
547                 string_list_free(list);
548                 return ret;
549                 }
550 }
551
552 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
553 {
554         gchar *string_casefold = g_utf8_casefold(string, -1);
555
556         while (list)
557                 {
558                 gchar *haystack = list->data;
559                 
560                 if (haystack)
561                         {
562                         gboolean equal;
563                         gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
564
565                         equal = (strcmp(haystack_casefold, string_casefold) == 0);
566                         g_free(haystack_casefold);
567
568                         if (equal)
569                                 {
570                                 g_free(string_casefold);
571                                 return haystack;
572                                 }
573                         }
574         
575                 list = list->next;
576                 }
577         
578         g_free(string_casefold);
579         return NULL;
580 }
581
582
583 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
584
585 GList *string_to_keywords_list(const gchar *text)
586 {
587         GList *list = NULL;
588         const gchar *ptr = text;
589
590         while (*ptr != '\0')
591                 {
592                 const gchar *begin;
593                 gint l = 0;
594
595                 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
596                 begin = ptr;
597                 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
598                         {
599                         ptr++;
600                         l++;
601                         }
602
603                 /* trim starting and ending whitespaces */
604                 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
605                 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
606
607                 if (l > 0)
608                         {
609                         gchar *keyword = g_strndup(begin, l);
610
611                         /* only add if not already in the list */
612                         if (!find_string_in_list_utf8nocase(list, keyword))
613                                 list = g_list_append(list, keyword);
614                         else
615                                 g_free(keyword);
616                         }
617                 }
618
619         return list;
620 }
621
622 /*
623  * keywords to marks
624  */
625  
626
627 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
628 {
629         GList *keywords;
630         gboolean found = FALSE;
631         keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
632         if (keywords)
633                 {
634                 GList *work = keywords;
635
636                 while (work)
637                         {
638                         gchar *kw = work->data;
639                         work = work->next;
640                         
641                         if (strcmp(kw, data) == 0)
642                                 {
643                                 found = TRUE;
644                                 break;
645                                 }
646                         }
647                 string_list_free(keywords);
648                 }
649         return found;
650 }
651
652 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
653 {
654         GList *keywords = NULL;
655         gboolean found = FALSE;
656         gboolean changed = FALSE;
657         GList *work;
658         keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
659
660         work = keywords;
661
662         while (work)
663                 {
664                 gchar *kw = work->data;
665                 
666                 if (strcmp(kw, data) == 0)
667                         {
668                         found = TRUE;
669                         if (!value) 
670                                 {
671                                 changed = TRUE;
672                                 keywords = g_list_delete_link(keywords, work);
673                                 g_free(kw);
674                                 }
675                         break;
676                         }
677                 work = work->next;
678                 }
679         if (value && !found) 
680                 {
681                 changed = TRUE;
682                 keywords = g_list_append(keywords, g_strdup(data));
683                 }
684         
685         if (changed) metadata_write_list(fd, KEYWORD_KEY, keywords);
686
687         string_list_free(keywords);
688         return TRUE;
689 }
690
691 /*
692  *-------------------------------------------------------------------
693  * keyword tree
694  *-------------------------------------------------------------------
695  */
696
697
698
699 GtkTreeStore *keyword_tree;
700
701 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
702 {
703         gchar *name;
704         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
705         return name;
706 }
707
708 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
709 {
710         gchar *casefold;
711         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
712         return casefold;
713 }
714
715 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
716 {
717         gboolean is_keyword;
718         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
719         return is_keyword;
720 }
721
722 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
723 {
724         gchar *casefold = g_utf8_casefold(name, -1);
725         gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
726                                                 KEYWORD_COLUMN_NAME, name,
727                                                 KEYWORD_COLUMN_CASEFOLD, casefold,
728                                                 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
729         g_free(casefold);
730 }
731
732 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
733 {
734         GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
735         GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
736         gint ret = gtk_tree_path_compare(pa, pb);
737         gtk_tree_path_free(pa);
738         gtk_tree_path_free(pb);
739         return ret;
740 }
741
742 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
743 {
744
745         gchar *mark, *name, *casefold;
746         gboolean is_keyword;
747
748         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
749                                                 KEYWORD_COLUMN_NAME, &name,
750                                                 KEYWORD_COLUMN_CASEFOLD, &casefold,
751                                                 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
752
753         gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
754                                                 KEYWORD_COLUMN_NAME, name,
755                                                 KEYWORD_COLUMN_CASEFOLD, casefold,
756                                                 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
757         g_free(mark);
758         g_free(name);
759         g_free(casefold);
760 }
761
762 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
763 {
764         GtkTreeIter from_child;
765         
766         keyword_copy(keyword_tree, to, from);
767         
768         if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
769         
770         while (TRUE)
771                 {
772                 GtkTreeIter to_child;
773                 gtk_tree_store_append(keyword_tree, &to_child, to);
774                 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
775                 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
776                 }
777 }
778
779 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
780 {
781         keyword_copy_recursive(keyword_tree, to, from);
782         gtk_tree_store_remove(keyword_tree, from);
783 }
784
785 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
786 {
787         GList *path = NULL;
788         GtkTreeIter iter = *iter_ptr;
789         
790         while (TRUE)
791                 {
792                 GtkTreeIter parent;
793                 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
794                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
795                 iter = parent;
796                 }
797         return path;
798 }
799
800 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
801 {
802         GtkTreeIter iter;
803
804         if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
805         
806         while (TRUE)
807                 {
808                 GtkTreeIter children;
809                 while (TRUE)
810                         {
811                         gchar *name = keyword_get_name(keyword_tree, &iter);
812                         if (strcmp(name, path->data) == 0) break;
813                         g_free(name);
814                         if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
815                         }
816                 path = path->next;
817                 if (!path) 
818                         {
819                         *iter_ptr = iter;
820                         return TRUE;
821                         }
822                         
823                 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
824                 iter = children;
825                 }
826 }
827
828
829 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
830 {
831         if (!casefold_list) return FALSE;
832         
833         while (TRUE)
834                 {
835                 GtkTreeIter parent;
836
837                 if (keyword_get_is_keyword(keyword_tree, &iter))
838                         {
839                         GList *work = casefold_list;
840                         gboolean found = FALSE;
841                         gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
842                         while (work)
843                                 {
844                                 const gchar *casefold = work->data;
845                                 work = work->next;
846
847                                 if (strcmp(iter_casefold, casefold) == 0)
848                                         {
849                                         found = TRUE;
850                                         break;
851                                         }
852                                 }
853                         g_free(iter_casefold);
854                         if (!found) return FALSE;
855                         }
856                 
857                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
858                 iter = parent;
859                 }
860 }
861
862 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
863 {
864         gboolean ret;
865         GList *casefold_list = NULL;
866         GList *work;
867
868         if (!keyword_get_is_keyword(keyword_tree, iter)) return FALSE;
869         
870         work = kw_list;
871         while (work)
872                 {
873                 const gchar *kw = work->data;
874                 work = work->next;
875                 
876                 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
877                 }
878         
879         ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
880         
881         string_list_free(casefold_list);
882         return ret;
883 }
884
885 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
886 {
887         GtkTreeIter iter = *iter_ptr;
888         while (TRUE)
889                 {
890                 GtkTreeIter parent;
891
892                 if (keyword_get_is_keyword(keyword_tree, &iter))
893                         {
894                         gchar *name = keyword_get_name(keyword_tree, &iter);
895                         if (!find_string_in_list_utf8nocase(*kw_list, name))
896                                 {
897                                 *kw_list = g_list_append(*kw_list, name);
898                                 }
899                         else
900                                 {
901                                 g_free(name);
902                                 }
903                         }
904
905                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
906                 iter = parent;
907                 }
908 }
909
910 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
911 {
912         gchar *found;
913         gchar *name;
914         if (!keyword_get_is_keyword(keyword_tree, iter)) return;
915
916         name = keyword_get_name(keyword_tree, iter);
917         found = find_string_in_list_utf8nocase(*kw_list, name);
918
919         if (found)
920                 {
921                 *kw_list = g_list_remove(*kw_list, found);
922                 g_free(found);
923                 }
924         g_free(name);
925 }
926
927 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
928 {
929         GtkTreeIter child;
930         keyword_tree_reset1(keyword_tree, iter, kw_list);
931         
932         if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
933
934         while (TRUE)
935                 {
936                 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
937                 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
938                 }
939 }
940
941 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
942 {
943         GtkTreeIter iter;
944         
945         if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent)) 
946                 return TRUE; /* this should happen only on empty helpers */
947
948         while (TRUE)
949                 {
950                 if (keyword_get_is_keyword(keyword_tree, &iter))
951                         {
952                         if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
953                         }
954                 else
955                         {
956                         /* for helpers we have to check recursively */
957                         if (!keyword_tree_check_empty_children(keyword_tree, &iter, kw_list)) return FALSE;
958                         }
959                 
960                 if (!gtk_tree_model_iter_next(keyword_tree, &iter))
961                         {
962                         return TRUE;
963                         }
964                 }
965 }
966
967 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
968 {
969         GtkTreeIter iter = *iter_ptr;
970         GtkTreeIter parent;
971         keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
972
973         if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
974         iter = parent;
975         
976         while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
977                 {
978                 GtkTreeIter parent;
979                 keyword_tree_reset1(keyword_tree, &iter, kw_list);
980                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
981                 iter = parent;
982                 }
983 }
984
985
986 void keyword_tree_new_default(void)
987 {
988         keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
989
990         GtkTreeIter i1, i2, i3;
991
992         gtk_tree_store_append(keyword_tree, &i1, NULL);
993         keyword_set(keyword_tree, &i1, "animal", TRUE);
994
995                 gtk_tree_store_append(keyword_tree, &i2, &i1);
996                 keyword_set(keyword_tree, &i2, "mammal", TRUE);
997
998                         gtk_tree_store_append(keyword_tree, &i3, &i2);
999                         keyword_set(keyword_tree, &i3, "dog", TRUE);
1000
1001                         gtk_tree_store_append(keyword_tree, &i3, &i2);
1002                         keyword_set(keyword_tree, &i3, "cat", TRUE);
1003
1004                 gtk_tree_store_append(keyword_tree, &i2, &i1);
1005                 keyword_set(keyword_tree, &i2, "insect", TRUE);
1006
1007                         gtk_tree_store_append(keyword_tree, &i3, &i2);
1008                         keyword_set(keyword_tree, &i3, "fly", TRUE);
1009
1010                         gtk_tree_store_append(keyword_tree, &i3, &i2);
1011                         keyword_set(keyword_tree, &i3, "dragonfly", TRUE);
1012
1013         gtk_tree_store_append(keyword_tree, &i1, NULL);
1014         keyword_set(keyword_tree, &i1, "daytime", FALSE);
1015
1016                 gtk_tree_store_append(keyword_tree, &i2, &i1);
1017                 keyword_set(keyword_tree, &i2, "morning", TRUE);
1018
1019                 gtk_tree_store_append(keyword_tree, &i2, &i1);
1020                 keyword_set(keyword_tree, &i2, "noon", TRUE);
1021
1022 }
1023
1024
1025 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */