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