75ff89afba239ad7a74802a754868ceaaabf7c31
[geeqie.git] / src / metadata.c
1 /*
2  * Copyright (C) 2004 John Ellis
3  * Copyright (C) 2008 - 2016 The Geeqie Team
4  *
5  * Authors: John Ellis, Laurent Monin
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 #include "main.h"
23 #include "metadata.h"
24
25 #include "cache.h"
26 #include "exif.h"
27 #include "filedata.h"
28 #include "misc.h"
29 #include "secure_save.h"
30 #include "ui_fileops.h"
31 #include "ui_misc.h"
32 #include "utilops.h"
33 #include "filefilter.h"
34 #include "layout_util.h"
35 #include "rcfile.h"
36
37 typedef enum {
38         MK_NONE,
39         MK_KEYWORDS,
40         MK_COMMENT
41 } MetadataKey;
42
43 static const gchar *group_keys[] = { /* tags that will be written to all files in a group, options->metadata.sync_grouped_files */
44         "Xmp.dc.title",
45         "Xmp.photoshop.Urgency",
46         "Xmp.photoshop.Category",
47         "Xmp.photoshop.SupplementalCategory",
48         "Xmp.dc.subject",
49         "Xmp.iptc.Location",
50         "Xmp.photoshop.Instruction",
51         "Xmp.photoshop.DateCreated",
52         "Xmp.dc.creator",
53         "Xmp.photoshop.AuthorsPosition",
54         "Xmp.photoshop.City",
55         "Xmp.photoshop.State",
56         "Xmp.iptc.CountryCode",
57         "Xmp.photoshop.Country",
58         "Xmp.photoshop.TransmissionReference",
59         "Xmp.photoshop.Headline",
60         "Xmp.photoshop.Credit",
61         "Xmp.photoshop.Source",
62         "Xmp.dc.rights",
63         "Xmp.dc.description",
64         "Xmp.photoshop.CaptionWriter",
65         NULL};
66
67 static gboolean metadata_write_queue_idle_cb(gpointer data);
68 static gboolean metadata_legacy_write(FileData *fd);
69 static void metadata_legacy_delete(FileData *fd, const gchar *except);
70 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment);
71
72
73 /*
74  *-------------------------------------------------------------------
75  * long-term cache - keep keywords from whole dir in memory
76  *-------------------------------------------------------------------
77  */
78
79 /* fd->cached metadata list of lists
80    each particular list contains key as a first entry, then the values
81 */
82
83 static void metadata_cache_update(FileData *fd, const gchar *key, const GList *values)
84 {
85         GList *work;
86
87         work = fd->cached_metadata;
88         while (work)
89                 {
90                 GList *entry = work->data;
91                 gchar *entry_key = entry->data;
92
93                 if (strcmp(entry_key, key) == 0)
94                         {
95                         /* key found - just replace values */
96                         GList *old_values = entry->next;
97                         entry->next = NULL;
98                         old_values->prev = NULL;
99                         string_list_free(old_values);
100                         work->data = g_list_append(entry, string_list_copy(values));
101                         DEBUG_1("updated %s %s\n", key, fd->path);
102                         return;
103                         }
104                 work = work->next;
105                 }
106
107         /* key not found - prepend new entry */
108         fd->cached_metadata = g_list_prepend(fd->cached_metadata,
109                                 g_list_prepend(string_list_copy(values), g_strdup(key)));
110         DEBUG_1("added %s %s\n", key, fd->path);
111
112 }
113
114 static const GList *metadata_cache_get(FileData *fd, const gchar *key)
115 {
116         GList *work;
117
118         work = fd->cached_metadata;
119         while (work)
120                 {
121                 GList *entry = work->data;
122                 gchar *entry_key = entry->data;
123
124                 if (strcmp(entry_key, key) == 0)
125                         {
126                         /* key found */
127                         DEBUG_1("found %s %s\n", key, fd->path);
128                         return entry;
129                         }
130                 work = work->next;
131                 }
132         return NULL;
133         DEBUG_1("not found %s %s\n", key, fd->path);
134 }
135
136 static void metadata_cache_remove(FileData *fd, const gchar *key)
137 {
138         GList *work;
139
140         work = fd->cached_metadata;
141         while (work)
142                 {
143                 GList *entry = work->data;
144                 gchar *entry_key = entry->data;
145
146                 if (strcmp(entry_key, key) == 0)
147                         {
148                         /* key found */
149                         string_list_free(entry);
150                         fd->cached_metadata = g_list_delete_link(fd->cached_metadata, work);
151                         DEBUG_1("removed %s %s\n", key, fd->path);
152                         return;
153                         }
154                 work = work->next;
155                 }
156         DEBUG_1("not removed %s %s\n", key, fd->path);
157 }
158
159 void metadata_cache_free(FileData *fd)
160 {
161         GList *work;
162         if (fd->cached_metadata) DEBUG_1("freed %s\n", fd->path);
163
164         work = fd->cached_metadata;
165         while (work)
166                 {
167                 GList *entry = work->data;
168                 string_list_free(entry);
169
170                 work = work->next;
171                 }
172         g_list_free(fd->cached_metadata);
173         fd->cached_metadata = NULL;
174 }
175
176
177
178
179
180
181 /*
182  *-------------------------------------------------------------------
183  * write queue
184  *-------------------------------------------------------------------
185  */
186
187 static GList *metadata_write_queue = NULL;
188 static guint metadata_write_idle_id = 0; /* event source id */
189
190 static void metadata_write_queue_add(FileData *fd)
191 {
192         if (!g_list_find(metadata_write_queue, fd))
193                 {
194                 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
195                 file_data_ref(fd);
196
197                 layout_util_status_update_write_all();
198                 }
199
200         if (metadata_write_idle_id)
201                 {
202                 g_source_remove(metadata_write_idle_id);
203                 metadata_write_idle_id = 0;
204                 }
205
206         if (options->metadata.confirm_after_timeout)
207                 {
208                 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, NULL);
209                 }
210 }
211
212
213 gboolean metadata_write_queue_remove(FileData *fd)
214 {
215         g_hash_table_destroy(fd->modified_xmp);
216         fd->modified_xmp = NULL;
217
218         metadata_write_queue = g_list_remove(metadata_write_queue, fd);
219
220         file_data_increment_version(fd);
221         file_data_send_notification(fd, NOTIFY_REREAD);
222
223         file_data_unref(fd);
224
225         layout_util_status_update_write_all();
226         return TRUE;
227 }
228
229 gboolean metadata_write_queue_remove_list(GList *list)
230 {
231         GList *work;
232         gboolean ret = TRUE;
233
234         work = list;
235         while (work)
236                 {
237                 FileData *fd = work->data;
238                 work = work->next;
239                 ret = ret && metadata_write_queue_remove(fd);
240                 }
241         return ret;
242 }
243
244 void metadata_notify_cb(FileData *fd, NotifyType type, gpointer data)
245 {
246         if (type & (NOTIFY_REREAD | NOTIFY_CHANGE))
247                 {
248                 metadata_cache_free(fd);
249
250                 if (g_list_find(metadata_write_queue, fd))
251                         {
252                         DEBUG_1("Notify metadata: %s %04x", fd->path, type);
253                         if (!isname(fd->path))
254                                 {
255                                 /* ignore deleted files */
256                                 metadata_write_queue_remove(fd);
257                                 }
258                         }
259                 }
260 }
261
262 gboolean metadata_write_queue_confirm(gboolean force_dialog, FileUtilDoneFunc done_func, gpointer done_data)
263 {
264         GList *work;
265         GList *to_approve = NULL;
266
267         work = metadata_write_queue;
268         while (work)
269                 {
270                 FileData *fd = work->data;
271                 work = work->next;
272
273                 if (!isname(fd->path))
274                         {
275                         /* ignore deleted files */
276                         metadata_write_queue_remove(fd);
277                         continue;
278                         }
279
280                 if (fd->change) continue; /* another operation in progress, skip this file for now */
281
282                 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
283                 }
284
285         file_util_write_metadata(NULL, to_approve, NULL, force_dialog, done_func, done_data);
286
287         return (metadata_write_queue != NULL);
288 }
289
290 static gboolean metadata_write_queue_idle_cb(gpointer data)
291 {
292         metadata_write_queue_confirm(FALSE, NULL, NULL);
293         metadata_write_idle_id = 0;
294         return FALSE;
295 }
296
297 gboolean metadata_write_perform(FileData *fd)
298 {
299         gboolean success;
300         ExifData *exif;
301         guint ln, lf;
302
303         g_assert(fd->change);
304
305         ln = strlen(fd->change->dest);
306         lf = strlen(GQ_CACHE_EXT_METADATA);
307         if (fd->change->dest &&
308             g_ascii_strncasecmp(fd->change->dest + ln - lf, GQ_CACHE_EXT_METADATA, lf) == 0)
309                 {
310                 success = metadata_legacy_write(fd);
311                 if (success) metadata_legacy_delete(fd, fd->change->dest);
312                 return success;
313                 }
314
315         /* write via exiv2 */
316         /*  we can either use cached metadata which have fd->modified_xmp already applied
317                                      or read metadata from file and apply fd->modified_xmp
318             metadata are read also if the file was modified meanwhile */
319         exif = exif_read_fd(fd);
320         if (!exif) return FALSE;
321
322         success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
323         exif_free_fd(fd, exif);
324
325         if (fd->change->dest)
326                 /* this will create a FileData for the sidecar and link it to the main file
327                    (we can't wait until the sidecar is discovered by directory scanning because
328                     exif_read_fd is called before that and it would read the main file only and
329                     store the metadata in the cache)
330                     FIXME: this does not catch new sidecars created by independent external programs
331                 */
332                 file_data_unref(file_data_new_group(fd->change->dest));
333
334         if (success) metadata_legacy_delete(fd, fd->change->dest);
335         return success;
336 }
337
338 gint metadata_queue_length(void)
339 {
340         return g_list_length(metadata_write_queue);
341 }
342
343 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
344 {
345         const gchar **k = keys;
346
347         while (*k)
348                 {
349                 if (strcmp(key, *k) == 0) return TRUE;
350                 k++;
351                 }
352         return FALSE;
353 }
354
355 gboolean metadata_write_revert(FileData *fd, const gchar *key)
356 {
357         if (!fd->modified_xmp) return FALSE;
358
359         g_hash_table_remove(fd->modified_xmp, key);
360
361         if (g_hash_table_size(fd->modified_xmp) == 0)
362                 {
363                 metadata_write_queue_remove(fd);
364                 }
365         else
366                 {
367                 /* reread the metadata to restore the original value */
368                 file_data_increment_version(fd);
369                 file_data_send_notification(fd, NOTIFY_REREAD);
370                 }
371         return TRUE;
372 }
373
374 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
375 {
376         if (!fd->modified_xmp)
377                 {
378                 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
379                 }
380         g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
381
382         metadata_cache_remove(fd, key);
383
384         if (fd->exif)
385                 {
386                 exif_update_metadata(fd->exif, key, values);
387                 }
388         metadata_write_queue_add(fd);
389         file_data_increment_version(fd);
390         file_data_send_notification(fd, NOTIFY_METADATA);
391
392         if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
393                 {
394                 GList *work = fd->sidecar_files;
395
396                 while (work)
397                         {
398                         FileData *sfd = work->data;
399                         work = work->next;
400
401                         if (sfd->format_class == FORMAT_CLASS_META) continue;
402
403                         metadata_write_list(sfd, key, values);
404                         }
405                 }
406
407
408         return TRUE;
409 }
410
411 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
412 {
413         GList *list = g_list_append(NULL, g_strdup(value));
414         gboolean ret = metadata_write_list(fd, key, list);
415         string_list_free(list);
416         return ret;
417 }
418
419 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
420 {
421         gchar string[50];
422
423         g_snprintf(string, sizeof(string), "%llu", (unsigned long long) value);
424         return metadata_write_string(fd, key, string);
425 }
426
427 /*
428  *-------------------------------------------------------------------
429  * keyword / comment read/write
430  *-------------------------------------------------------------------
431  */
432
433 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
434 {
435         SecureSaveInfo *ssi;
436
437         ssi = secure_open(path);
438         if (!ssi) return FALSE;
439
440         secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
441
442         secure_fprintf(ssi, "[keywords]\n");
443         while (keywords && secsave_errno == SS_ERR_NONE)
444                 {
445                 const gchar *word = keywords->data;
446                 keywords = keywords->next;
447
448                 secure_fprintf(ssi, "%s\n", word);
449                 }
450         secure_fputc(ssi, '\n');
451
452         secure_fprintf(ssi, "[comment]\n");
453         secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
454
455         secure_fprintf(ssi, "#end\n");
456
457         return (secure_close(ssi) == 0);
458 }
459
460 static gboolean metadata_legacy_write(FileData *fd)
461 {
462         gboolean success = FALSE;
463         gchar *metadata_pathl;
464         gpointer keywords;
465         gpointer comment_l;
466         gboolean have_keywords;
467         gboolean have_comment;
468         const gchar *comment;
469         GList *orig_keywords = NULL;
470         gchar *orig_comment = NULL;
471
472         g_assert(fd->change && fd->change->dest);
473
474         DEBUG_1("Saving comment: %s", fd->change->dest);
475
476         if (!fd->modified_xmp) return TRUE;
477
478         metadata_pathl = path_from_utf8(fd->change->dest);
479
480         have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, NULL, &keywords);
481         have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, NULL, &comment_l);
482         comment = (have_comment && comment_l) ? ((GList *)comment_l)->data : NULL;
483
484         if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
485
486         success = metadata_file_write(metadata_pathl,
487                                       have_keywords ? (GList *)keywords : orig_keywords,
488                                       have_comment ? comment : orig_comment);
489
490         g_free(metadata_pathl);
491         g_free(orig_comment);
492         string_list_free(orig_keywords);
493
494         return success;
495 }
496
497 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
498 {
499         FILE *f;
500         gchar s_buf[1024];
501         MetadataKey key = MK_NONE;
502         GList *list = NULL;
503         GString *comment_build = NULL;
504
505         f = fopen(path, "r");
506         if (!f) return FALSE;
507
508         while (fgets(s_buf, sizeof(s_buf), f))
509                 {
510                 gchar *ptr = s_buf;
511
512                 if (*ptr == '#') continue;
513                 if (*ptr == '[' && key != MK_COMMENT)
514                         {
515                         gchar *keystr = ++ptr;
516
517                         key = MK_NONE;
518                         while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
519
520                         if (*ptr == ']')
521                                 {
522                                 *ptr = '\0';
523                                 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
524                                         key = MK_KEYWORDS;
525                                 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
526                                         key = MK_COMMENT;
527                                 }
528                         continue;
529                         }
530
531                 switch (key)
532                         {
533                         case MK_NONE:
534                                 break;
535                         case MK_KEYWORDS:
536                                 {
537                                 while (*ptr != '\n' && *ptr != '\0') ptr++;
538                                 *ptr = '\0';
539                                 if (strlen(s_buf) > 0)
540                                         {
541                                         gchar *kw = utf8_validate_or_convert(s_buf);
542
543                                         list = g_list_prepend(list, kw);
544                                         }
545                                 }
546                                 break;
547                         case MK_COMMENT:
548                                 if (!comment_build) comment_build = g_string_new("");
549                                 g_string_append(comment_build, s_buf);
550                                 break;
551                         }
552                 }
553
554         fclose(f);
555
556         if (keywords)
557                 {
558                 *keywords = g_list_reverse(list);
559                 }
560         else
561                 {
562                 string_list_free(list);
563                 }
564
565         if (comment_build)
566                 {
567                 if (comment)
568                         {
569                         gint len;
570                         gchar *ptr = comment_build->str;
571
572                         /* strip leading and trailing newlines */
573                         while (*ptr == '\n') ptr++;
574                         len = strlen(ptr);
575                         while (len > 0 && ptr[len - 1] == '\n') len--;
576                         if (ptr[len] == '\n') len++; /* keep the last one */
577                         if (len > 0)
578                                 {
579                                 gchar *text = g_strndup(ptr, len);
580
581                                 *comment = utf8_validate_or_convert(text);
582                                 g_free(text);
583                                 }
584                         }
585                 g_string_free(comment_build, TRUE);
586                 }
587
588         return TRUE;
589 }
590
591 static void metadata_legacy_delete(FileData *fd, const gchar *except)
592 {
593         gchar *metadata_path;
594         gchar *metadata_pathl;
595         if (!fd) return;
596
597         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
598         if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
599                 {
600                 metadata_pathl = path_from_utf8(metadata_path);
601                 unlink(metadata_pathl);
602                 g_free(metadata_pathl);
603                 g_free(metadata_path);
604                 }
605
606 #ifdef HAVE_EXIV2
607         /* without exiv2: do not delete xmp metadata because we are not able to convert it,
608            just ignore it */
609         metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
610         if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
611                 {
612                 metadata_pathl = path_from_utf8(metadata_path);
613                 unlink(metadata_pathl);
614                 g_free(metadata_pathl);
615                 g_free(metadata_path);
616                 }
617 #endif
618 }
619
620 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
621 {
622         gchar *metadata_path;
623         gchar *metadata_pathl;
624         gboolean success = FALSE;
625
626         if (!fd) return FALSE;
627
628         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
629         if (!metadata_path) return FALSE;
630
631         metadata_pathl = path_from_utf8(metadata_path);
632
633         success = metadata_file_read(metadata_pathl, keywords, comment);
634
635         g_free(metadata_pathl);
636         g_free(metadata_path);
637
638         return success;
639 }
640
641 static GList *remove_duplicate_strings_from_list(GList *list)
642 {
643         GList *work = list;
644         GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
645         GList *newlist = NULL;
646
647         while (work)
648                 {
649                 gchar *key = work->data;
650
651                 if (g_hash_table_lookup(hashtable, key) == NULL)
652                         {
653                         g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
654                         newlist = g_list_prepend(newlist, key);
655                         }
656                 work = work->next;
657                 }
658
659         g_hash_table_destroy(hashtable);
660         g_list_free(list);
661
662         return g_list_reverse(newlist);
663 }
664
665 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
666 {
667         ExifData *exif;
668         GList *list = NULL;
669         const GList *cache_entry;
670         if (!fd) return NULL;
671
672         /* unwritten data overide everything */
673         if (fd->modified_xmp && format == METADATA_PLAIN)
674                 {
675                 list = g_hash_table_lookup(fd->modified_xmp, key);
676                 if (list) return string_list_copy(list);
677                 }
678
679
680         if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
681             && (cache_entry = metadata_cache_get(fd, key)))
682                 {
683                 return string_list_copy(cache_entry->next);
684                 }
685
686         /*
687             Legacy metadata file is the primary source if it exists.
688             Merging the lists does not make much sense, because the existence of
689             legacy metadata file indicates that the other metadata sources are not
690             writable and thus it would not be possible to delete the keywords
691             that comes from the image file.
692         */
693         if (strcmp(key, KEYWORD_KEY) == 0)
694                 {
695                 if (metadata_legacy_read(fd, &list, NULL))
696                         {
697                         if (format == METADATA_PLAIN)
698                                 {
699                                 metadata_cache_update(fd, key, list);
700                                 }
701                         return list;
702                         }
703                 }
704         else if (strcmp(key, COMMENT_KEY) == 0)
705                 {
706                 gchar *comment = NULL;
707                 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
708                 }
709         else if (strncmp(key, "file.", 5) == 0)
710                 {
711                 return g_list_append(NULL, metadata_file_info(fd, key, format));
712                 }
713
714         exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
715         if (!exif) return NULL;
716         list = exif_get_metadata(exif, key, format);
717         exif_free_fd(fd, exif);
718
719         if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
720                 {
721                 metadata_cache_update(fd, key, list);
722                 }
723
724         return list;
725 }
726
727 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
728 {
729         GList *string_list = metadata_read_list(fd, key, format);
730         if (string_list)
731                 {
732                 gchar *str = string_list->data;
733                 string_list->data = NULL;
734                 string_list_free(string_list);
735                 return str;
736                 }
737         return NULL;
738 }
739
740 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
741 {
742         guint64 ret;
743         gchar *endptr;
744         gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
745         if (!string) return fallback;
746
747         ret = g_ascii_strtoull(string, &endptr, 10);
748         if (string == endptr) ret = fallback;
749         g_free(string);
750         return ret;
751 }
752
753 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
754 {
755         gdouble coord;
756         gchar *endptr;
757         gdouble deg, min, sec;
758         gboolean ok = FALSE;
759         gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
760         if (!string) return fallback;
761
762         deg = g_ascii_strtod(string, &endptr);
763         if (*endptr == ',')
764                 {
765                 min = g_ascii_strtod(endptr + 1, &endptr);
766                 if (*endptr == ',')
767                         sec = g_ascii_strtod(endptr + 1, &endptr);
768                 else
769                         sec = 0.0;
770
771
772                 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
773                         {
774                         coord = deg + min /60.0 + sec / 3600.0;
775                         ok = TRUE;
776                         if (*endptr == 'S' || *endptr == 'W') coord = -coord;
777                         }
778                 }
779
780         if (!ok)
781                 {
782                 coord = fallback;
783                 log_printf("unable to parse GPS coordinate '%s'\n", string);
784                 }
785
786         g_free(string);
787         return coord;
788 }
789
790 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
791 {
792         gchar *endptr;
793         gdouble deg;
794         gboolean ok = FALSE;
795         gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
796         if (!string) return fallback;
797
798         DEBUG_3("GPS_direction: %s\n", string);
799         deg = g_ascii_strtod(string, &endptr);
800
801         /* Expected text string is of the format e.g.:
802          * 18000/100
803          */
804         if (*endptr == '/')
805                 {
806                 deg = deg/100;
807                 ok = TRUE;
808                 }
809
810         if (!ok)
811                 {
812                 deg = fallback;
813                 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
814                 }
815
816         g_free(string);
817
818         return deg;
819 }
820
821 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
822 {
823         gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
824
825         if (!str)
826                 {
827                 return metadata_write_string(fd, key, value);
828                 }
829         else
830                 {
831                 gchar *new_string = g_strconcat(str, value, NULL);
832                 gboolean ret = metadata_write_string(fd, key, new_string);
833                 g_free(str);
834                 g_free(new_string);
835                 return ret;
836                 }
837 }
838
839 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
840 {
841         gint deg;
842         gdouble min;
843         gdouble param;
844         char *coordinate;
845         char *ref;
846         gboolean ok = TRUE;
847
848         param = value;
849         if (param < 0)
850                 param = -param;
851         deg = param;
852         min = (param * 60) - (deg * 60);
853         if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
854                 if (value < 0)
855                         ref = "W";
856                 else
857                         ref = "E";
858         else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
859                 if (value < 0)
860                         ref = "S";
861                 else
862                         ref = "N";
863         else
864                 {
865                 log_printf("unknown GPS parameter key '%s'\n", key);
866                 ok = FALSE;
867                 }
868
869         if (ok)
870                 {
871                 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
872                 metadata_write_string(fd, key, coordinate );
873                 g_free(coordinate);
874                 }
875
876         return ok;
877 }
878
879 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
880 {
881         GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
882
883         if (!list)
884                 {
885                 return metadata_write_list(fd, key, values);
886                 }
887         else
888                 {
889                 gboolean ret;
890                 list = g_list_concat(list, string_list_copy(values));
891                 list = remove_duplicate_strings_from_list(list);
892
893                 ret = metadata_write_list(fd, key, list);
894                 string_list_free(list);
895                 return ret;
896                 }
897 }
898
899 /**
900  * \see find_string_in_list
901  */
902 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
903 {
904         gchar *string_casefold = g_utf8_casefold(string, -1);
905
906         while (list)
907                 {
908                 gchar *haystack = list->data;
909
910                 if (haystack)
911                         {
912                         gboolean equal;
913                         gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
914
915                         equal = (strcmp(haystack_casefold, string_casefold) == 0);
916                         g_free(haystack_casefold);
917
918                         if (equal)
919                                 {
920                                 g_free(string_casefold);
921                                 return haystack;
922                                 }
923                         }
924
925                 list = list->next;
926                 }
927
928         g_free(string_casefold);
929         return NULL;
930 }
931
932 /**
933  * \see find_string_in_list
934  */
935 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
936 {
937         while (list)
938                 {
939                 gchar *haystack = list->data;
940
941                 if (haystack && strcmp(haystack, string) == 0)
942                         return haystack;
943
944                 list = list->next;
945                 } // while (list)
946
947         return NULL;
948 } // gchar *find_string_in_list_utf...
949
950 /**
951  * \brief Find a existent string in a list.
952  *
953  * This is a switch between find_string_in_list_utf8case and
954  * find_string_in_list_utf8nocase to search with or without case for the
955  * existence of a string.
956  *
957  * \param list The list to search in
958  * \param string The string to search for
959  * \return The string or NULL
960  *
961  * \see find_string_in_list_utf8case
962  * \see find_string_in_list_utf8nocase
963  */
964 gchar *find_string_in_list(GList *list, const gchar *string)
965 {
966         if (options->metadata.keywords_case_sensitive)
967                 return find_string_in_list_utf8case(list, string);
968         else
969                 return find_string_in_list_utf8nocase(list, string);
970 }
971
972 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
973
974 GList *string_to_keywords_list(const gchar *text)
975 {
976         GList *list = NULL;
977         const gchar *ptr = text;
978
979         while (*ptr != '\0')
980                 {
981                 const gchar *begin;
982                 gint l = 0;
983
984                 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
985                 begin = ptr;
986                 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
987                         {
988                         ptr++;
989                         l++;
990                         }
991
992                 /* trim starting and ending whitespaces */
993                 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
994                 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
995
996                 if (l > 0)
997                         {
998                         gchar *keyword = g_strndup(begin, l);
999
1000                         /* only add if not already in the list */
1001                         if (!find_string_in_list(list, keyword))
1002                                 list = g_list_append(list, keyword);
1003                         else
1004                                 g_free(keyword);
1005                         }
1006                 }
1007
1008         return list;
1009 }
1010
1011 /*
1012  * keywords to marks
1013  */
1014
1015
1016 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
1017 {
1018         /* FIXME: do not use global keyword_tree */
1019         GList *path = data;
1020         GList *keywords;
1021         gboolean found = FALSE;
1022         keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1023         if (keywords)
1024                 {
1025                 GtkTreeIter iter;
1026                 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1027                     keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1028                         found = TRUE;
1029
1030                 }
1031         return found;
1032 }
1033
1034 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
1035 {
1036         GList *path = data;
1037         GList *keywords = NULL;
1038         GtkTreeIter iter;
1039
1040         if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1041
1042         keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1043
1044         if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1045                 {
1046                 if (value)
1047                         {
1048                         keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1049                         }
1050                 else
1051                         {
1052                         keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1053                         }
1054                 metadata_write_list(fd, KEYWORD_KEY, keywords);
1055                 }
1056
1057         string_list_free(keywords);
1058         return TRUE;
1059 }
1060
1061
1062
1063 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1064 {
1065
1066         FileDataGetMarkFunc get_mark_func;
1067         FileDataSetMarkFunc set_mark_func;
1068         gpointer mark_func_data;
1069
1070         gint i;
1071
1072         for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1073                 {
1074                 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1075                 if (get_mark_func == meta_data_get_keyword_mark)
1076                         {
1077                         GtkTreeIter old_kw_iter;
1078                         GList *old_path = mark_func_data;
1079
1080                         if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1081                             (i == mark || /* release any previous connection of given mark */
1082                              keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1083                                 {
1084                                 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
1085                                 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1086                                 }
1087                         }
1088                 }
1089
1090
1091         if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1092                 {
1093                 GList *path;
1094                 gchar *mark_str;
1095                 path = keyword_tree_get_path(keyword_tree, kw_iter);
1096                 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
1097
1098                 mark_str = g_strdup_printf("%d", (mark < 9 ? mark : -1) + 1);
1099                 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1100                 g_free(mark_str);
1101                 }
1102 }
1103
1104
1105 /*
1106  *-------------------------------------------------------------------
1107  * keyword tree
1108  *-------------------------------------------------------------------
1109  */
1110
1111
1112
1113 GtkTreeStore *keyword_tree;
1114
1115 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1116 {
1117         gchar *name;
1118         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1119         return name;
1120 }
1121
1122 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1123 {
1124         gchar *mark_str;
1125
1126         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1127         return mark_str;
1128 }
1129
1130 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1131 {
1132         gchar *casefold;
1133         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1134         return casefold;
1135 }
1136
1137 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1138 {
1139         gboolean is_keyword;
1140         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1141         return is_keyword;
1142 }
1143
1144 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1145 {
1146         gchar *casefold = g_utf8_casefold(name, -1);
1147         gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1148                                                 KEYWORD_COLUMN_NAME, name,
1149                                                 KEYWORD_COLUMN_CASEFOLD, casefold,
1150                                                 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1151         g_free(casefold);
1152 }
1153
1154 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1155 {
1156         GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1157         GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1158         gint ret = gtk_tree_path_compare(pa, pb);
1159         gtk_tree_path_free(pa);
1160         gtk_tree_path_free(pb);
1161         return ret;
1162 }
1163
1164 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1165 {
1166         GtkTreeIter parent_a;
1167         GtkTreeIter parent_b;
1168
1169         gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1170         gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1171
1172         if (valid_pa && valid_pb)
1173                 {
1174                 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1175                 }
1176         else
1177                 {
1178                 return (!valid_pa && !valid_pb); /* both are toplevel */
1179                 }
1180 }
1181
1182 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1183 {
1184         GtkTreeIter parent;
1185         GtkTreeIter iter;
1186         gboolean toplevel = FALSE;
1187         gboolean ret;
1188         gchar *casefold;
1189
1190         if (parent_ptr)
1191                 {
1192                 parent = *parent_ptr;
1193                 }
1194         else if (sibling)
1195                 {
1196                 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1197                 }
1198         else
1199                 {
1200                 toplevel = TRUE;
1201                 }
1202
1203         if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
1204
1205         casefold = g_utf8_casefold(name, -1);
1206         ret = FALSE;
1207
1208         while (TRUE)
1209                 {
1210                 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
1211                         {
1212                         if (options->metadata.keywords_case_sensitive)
1213                                 {
1214                                 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1215                                 ret = strcmp(name, iter_name) == 0;
1216                                 g_free(iter_name);
1217                                 }
1218                         else
1219                                 {
1220                                 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1221                                 ret = strcmp(casefold, iter_casefold) == 0;
1222                                 g_free(iter_casefold);
1223                                 } // if (options->metadata.tags_cas...
1224                         }
1225                 if (ret)
1226                         {
1227                         if (result) *result = iter;
1228                         break;
1229                         }
1230                 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1231                 }
1232         g_free(casefold);
1233         return ret;
1234 }
1235
1236
1237 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1238 {
1239
1240         gchar *mark, *name, *casefold;
1241         gboolean is_keyword;
1242
1243         /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1244         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1245                                                 KEYWORD_COLUMN_NAME, &name,
1246                                                 KEYWORD_COLUMN_CASEFOLD, &casefold,
1247                                                 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1248
1249         gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1250                                                 KEYWORD_COLUMN_NAME, name,
1251                                                 KEYWORD_COLUMN_CASEFOLD, casefold,
1252                                                 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1253         g_free(mark);
1254         g_free(name);
1255         g_free(casefold);
1256 }
1257
1258 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1259 {
1260         GtkTreeIter from_child;
1261
1262         keyword_copy(keyword_tree, to, from);
1263
1264         if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1265
1266         while (TRUE)
1267                 {
1268                 GtkTreeIter to_child;
1269                 gtk_tree_store_append(keyword_tree, &to_child, to);
1270                 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1271                 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1272                 }
1273 }
1274
1275 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1276 {
1277         keyword_copy_recursive(keyword_tree, to, from);
1278         keyword_delete(keyword_tree, from);
1279 }
1280
1281 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1282 {
1283         GList *path = NULL;
1284         GtkTreeIter iter = *iter_ptr;
1285
1286         while (TRUE)
1287                 {
1288                 GtkTreeIter parent;
1289                 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1290                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1291                 iter = parent;
1292                 }
1293         return path;
1294 }
1295
1296 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1297 {
1298         GtkTreeIter iter;
1299
1300         if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1301
1302         while (TRUE)
1303                 {
1304                 GtkTreeIter children;
1305                 while (TRUE)
1306                         {
1307                         gchar *name = keyword_get_name(keyword_tree, &iter);
1308                         if (strcmp(name, path->data) == 0) break;
1309                         g_free(name);
1310                         if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1311                         }
1312                 path = path->next;
1313                 if (!path)
1314                         {
1315                         *iter_ptr = iter;
1316                         return TRUE;
1317                         }
1318
1319                 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1320                 iter = children;
1321                 }
1322 }
1323
1324
1325 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1326 {
1327         if (!casefold_list) return FALSE;
1328
1329         if (!keyword_get_is_keyword(keyword_tree, &iter))
1330                 {
1331                 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1332                 GtkTreeIter child;
1333                 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1334                         return FALSE; /* this should happen only on empty helpers */
1335
1336                 while (TRUE)
1337                         {
1338                         if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1339                         if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1340                         }
1341                 }
1342
1343         while (TRUE)
1344                 {
1345                 GtkTreeIter parent;
1346
1347                 if (keyword_get_is_keyword(keyword_tree, &iter))
1348                         {
1349                         GList *work = casefold_list;
1350                         gboolean found = FALSE;
1351                         gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1352                         while (work)
1353                                 {
1354                                 const gchar *casefold = work->data;
1355                                 work = work->next;
1356
1357                                 if (strcmp(iter_casefold, casefold) == 0)
1358                                         {
1359                                         found = TRUE;
1360                                         break;
1361                                         }
1362                                 }
1363                         g_free(iter_casefold);
1364                         if (!found) return FALSE;
1365                         }
1366
1367                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1368                 iter = parent;
1369                 }
1370 }
1371
1372 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1373 {
1374         if (!kw_list) return FALSE;
1375
1376         if (!keyword_get_is_keyword(keyword_tree, &iter))
1377                 {
1378                 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1379                 GtkTreeIter child;
1380                 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1381                         return FALSE; /* this should happen only on empty helpers */
1382
1383                 while (TRUE)
1384                         {
1385                         if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1386                         if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1387                         }
1388                 }
1389
1390         while (TRUE)
1391                 {
1392                 GtkTreeIter parent;
1393
1394                 if (keyword_get_is_keyword(keyword_tree, &iter))
1395                         {
1396                         GList *work = kw_list;
1397                         gboolean found = FALSE;
1398                         gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1399                         while (work)
1400                                 {
1401                                 const gchar *name = work->data;
1402                                 work = work->next;
1403
1404                                 if (strcmp(iter_name, name) == 0)
1405                                         {
1406                                         found = TRUE;
1407                                         break;
1408                                         }
1409                                 }
1410                         g_free(iter_name);
1411                         if (!found) return FALSE;
1412                         }
1413
1414                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1415                 iter = parent;
1416                 }
1417 }
1418
1419 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1420 {
1421         gboolean ret;
1422         GList *casefold_list = NULL;
1423         GList *work;
1424
1425         if (options->metadata.keywords_case_sensitive)
1426                 {
1427                 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1428                 }
1429         else
1430                 {
1431                 work = kw_list;
1432                 while (work)
1433                         {
1434                         const gchar *kw = work->data;
1435                         work = work->next;
1436
1437                         casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1438                         }
1439
1440                 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1441
1442                 string_list_free(casefold_list);
1443                 }
1444
1445         return ret;
1446 }
1447
1448 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1449 {
1450         GtkTreeIter iter = *iter_ptr;
1451         while (TRUE)
1452                 {
1453                 GtkTreeIter parent;
1454
1455                 if (keyword_get_is_keyword(keyword_tree, &iter))
1456                         {
1457                         gchar *name = keyword_get_name(keyword_tree, &iter);
1458                         if (!find_string_in_list(*kw_list, name))
1459                                 {
1460                                 *kw_list = g_list_append(*kw_list, name);
1461                                 }
1462                         else
1463                                 {
1464                                 g_free(name);
1465                                 }
1466                         }
1467
1468                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1469                 iter = parent;
1470                 }
1471 }
1472
1473 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1474 {
1475         GtkTreeIter iter = *iter_ptr;
1476         GList *kw_list = NULL;
1477
1478         while (TRUE)
1479                 {
1480                 GtkTreeIter parent;
1481
1482                 if (keyword_get_is_keyword(keyword_tree, &iter))
1483                         {
1484                         gchar *name = keyword_get_name(keyword_tree, &iter);
1485                         kw_list = g_list_append(kw_list, name);
1486                         }
1487
1488                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1489                 iter = parent;
1490                 }
1491 } // GList *keyword_tree_get(GtkTre...
1492
1493 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1494 {
1495         gchar *found;
1496         gchar *name;
1497         if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1498
1499         name = keyword_get_name(keyword_tree, iter);
1500         found = find_string_in_list(*kw_list, name);
1501
1502         if (found)
1503                 {
1504                 *kw_list = g_list_remove(*kw_list, found);
1505                 g_free(found);
1506                 }
1507         g_free(name);
1508 }
1509
1510 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1511 {
1512         GtkTreeIter child;
1513         keyword_tree_reset1(keyword_tree, iter, kw_list);
1514
1515         if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1516
1517         while (TRUE)
1518                 {
1519                 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1520                 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1521                 }
1522 }
1523
1524 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1525 {
1526         GtkTreeIter iter;
1527
1528         if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1529                 return TRUE; /* this should happen only on empty helpers */
1530
1531         while (TRUE)
1532                 {
1533                 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1534                 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1535                 }
1536 }
1537
1538 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1539 {
1540         GtkTreeIter iter = *iter_ptr;
1541         GtkTreeIter parent;
1542         keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1543
1544         if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1545         iter = parent;
1546
1547         while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1548                 {
1549                 GtkTreeIter parent;
1550                 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1551                 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1552                 iter = parent;
1553                 }
1554 }
1555
1556 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1557 {
1558         GList *list;
1559         GtkTreeIter child;
1560         while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1561                 {
1562                 keyword_delete(keyword_tree, &child);
1563                 }
1564
1565         meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1566
1567         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1568         g_list_free(list);
1569
1570         gtk_tree_store_remove(keyword_tree, iter_ptr);
1571 }
1572
1573
1574 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1575 {
1576         GList *list;
1577         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1578         if (!g_list_find(list, id))
1579                 {
1580                 list = g_list_prepend(list, id);
1581                 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1582                 }
1583 }
1584
1585 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1586 {
1587         GList *list;
1588         gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1589         list = g_list_remove(list, id);
1590         gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1591 }
1592
1593 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1594 {
1595         GList *list;
1596         gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1597         return !!g_list_find(list, id);
1598 }
1599
1600 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1601 {
1602         keyword_show_in(GTK_TREE_STORE(model), iter, data);
1603         return FALSE;
1604 }
1605
1606 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1607 {
1608         gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1609 }
1610
1611 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1612 {
1613         if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1614                 {
1615                 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1616                 }
1617         return FALSE;
1618 }
1619
1620 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1621 {
1622         gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1623 }
1624
1625 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1626 {
1627         GtkTreeIter iter = *iter_ptr;
1628         while (TRUE)
1629                 {
1630                 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1631                         {
1632                         keyword_hide_in(keyword_tree, &iter, id);
1633                         /* no need to check children of hidden node */
1634                         }
1635                 else
1636                         {
1637                         GtkTreeIter child;
1638                         if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1639                                 {
1640                                 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1641                                 }
1642                         }
1643                 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1644                 }
1645 }
1646
1647 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1648 {
1649         GtkTreeIter iter;
1650         if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1651         keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1652 }
1653
1654 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1655 {
1656         GtkTreeIter iter = *iter_ptr;
1657         GList *keywords = data;
1658         gpointer id = keywords->data;
1659         keywords = keywords->next; /* hack */
1660         if (keyword_tree_is_set(model, &iter, keywords))
1661                 {
1662                 while (TRUE)
1663                         {
1664                         GtkTreeIter parent;
1665                         keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1666                         if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1667                         iter = parent;
1668                         }
1669                 }
1670         return FALSE;
1671 }
1672
1673 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1674 {
1675         /* hack: pass id to keyword_hide_unset_in_cb in the list */
1676         keywords = g_list_prepend(keywords, id);
1677         gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1678         keywords = g_list_delete_link(keywords, keywords);
1679 }
1680
1681
1682 void keyword_tree_new(void)
1683 {
1684         if (keyword_tree) return;
1685
1686         keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1687 }
1688
1689 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1690 {
1691         GtkTreeIter iter;
1692         gtk_tree_store_append(keyword_tree, &iter, parent);
1693         keyword_set(keyword_tree, &iter, name, is_keyword);
1694         return iter;
1695 }
1696
1697 void keyword_tree_new_default(void)
1698 {
1699         GtkTreeIter i1, i2;
1700
1701         if (!keyword_tree) keyword_tree_new();
1702
1703         i1 = keyword_tree_default_append(keyword_tree, NULL, _("People"), TRUE);
1704                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1705                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1706                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1707                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1708                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1709                         keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1710         i1 = keyword_tree_default_append(keyword_tree, NULL, _("Nature"), TRUE);
1711                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1712                         keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1713                         keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1714                         keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1715                         keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1716                         keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1717                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1718                         keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1719                         keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1720                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1721                         keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1722                         keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1723                         keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1724                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1725         i1 = keyword_tree_default_append(keyword_tree, NULL, _("Art"), TRUE);
1726                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1727                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1728                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1729                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1730         i1 = keyword_tree_default_append(keyword_tree, NULL, _("City"), TRUE);
1731                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1732                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1733                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1734         i1 = keyword_tree_default_append(keyword_tree, NULL, _("Architecture"), TRUE);
1735                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1736                         keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1737                         keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1738                         keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1739                         keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1740                         keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1741                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1742                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1743                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1744         i1 = keyword_tree_default_append(keyword_tree, NULL, _("Places"), FALSE);
1745         i1 = keyword_tree_default_append(keyword_tree, NULL, _("Conditions"), FALSE);
1746                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1747                         keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1748                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1749                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1750                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1751                         keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1752                         keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1753                         keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1754                         keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1755                         keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1756         i1 = keyword_tree_default_append(keyword_tree, NULL, _("Photo"), FALSE);
1757                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1758                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1759                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1760                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1761                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1762                 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1763 }
1764
1765
1766 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1767 {
1768         GtkTreeIter iter = *iter_ptr;
1769         while (TRUE)
1770                 {
1771                 GtkTreeIter children;
1772                 gchar *name;
1773                 gchar *mark_str;
1774
1775                 WRITE_NL(); WRITE_STRING("<keyword ");
1776                 name = keyword_get_name(keyword_tree, &iter);
1777                 write_char_option(outstr, indent, "name", name);
1778                 g_free(name);
1779                 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1780                 mark_str = keyword_get_mark(keyword_tree, &iter);
1781                 if (mark_str && mark_str[0])
1782                         {
1783                         write_char_option(outstr, indent, "mark", mark_str);
1784                         }
1785
1786                 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1787                         {
1788                         WRITE_STRING(">");
1789                         indent++;
1790                         keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1791                         indent--;
1792                         WRITE_NL(); WRITE_STRING("</keyword>");
1793                         }
1794                 else
1795                         {
1796                         WRITE_STRING("/>");
1797                         }
1798                 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1799                 }
1800 }
1801
1802 void keyword_tree_write_config(GString *outstr, gint indent)
1803 {
1804         GtkTreeIter iter;
1805         WRITE_NL(); WRITE_STRING("<keyword_tree>");
1806         indent++;
1807
1808         if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1809                 {
1810                 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1811                 }
1812         indent--;
1813         WRITE_NL(); WRITE_STRING("</keyword_tree>");
1814 }
1815
1816 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1817 {
1818         gchar *name = NULL;
1819         gboolean is_kw = TRUE;
1820         gchar *mark_str = NULL;
1821
1822         while (*attribute_names)
1823                 {
1824                 const gchar *option = *attribute_names++;
1825                 const gchar *value = *attribute_values++;
1826
1827                 if (READ_CHAR_FULL("name", name)) continue;
1828                 if (READ_BOOL_FULL("kw", is_kw)) continue;
1829                 if (READ_CHAR_FULL("mark", mark_str)) continue;
1830
1831                 log_printf("unknown attribute %s = %s\n", option, value);
1832                 }
1833         if (name && name[0])
1834                 {
1835                 GtkTreeIter iter;
1836                 /* re-use existing keyword if any */
1837                 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1838                         {
1839                         gtk_tree_store_append(keyword_tree, &iter, parent);
1840                         }
1841                 keyword_set(keyword_tree, &iter, name, is_kw);
1842
1843                 if (mark_str)
1844                         {
1845                         gint i = (gint)atoi(mark_str);
1846                         if (i == 0) i = 10;
1847
1848                         meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1849                                                                                         &iter, i - 1);
1850                         }
1851
1852                 g_free(name);
1853                 return gtk_tree_iter_copy(&iter);
1854                 }
1855         g_free(name);
1856         return NULL;
1857 }
1858
1859 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */