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