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