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