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