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