2 * Copyright (C) 2004 John Ellis
3 * Copyright (C) 2008 - 2016 The Geeqie Team
5 * Authors: John Ellis, Laurent Monin
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.
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.
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.
31 #include "secure-save.h"
32 #include "ui-fileops.h"
34 #include "layout-util.h"
43 /* If contents change, keep GuideOptionsMetadata.xml up to date */
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
47 static const gchar *group_keys[] = {
49 "Xmp.photoshop.Urgency",
50 "Xmp.photoshop.Category",
51 "Xmp.photoshop.SupplementalCategory",
54 "Xmp.photoshop.Instruction",
55 "Xmp.photoshop.DateCreated",
57 "Xmp.photoshop.AuthorsPosition",
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",
68 "Xmp.photoshop.CaptionWriter",
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);
78 *-------------------------------------------------------------------
79 * long-term cache - keep keywords from whole dir in memory
80 *-------------------------------------------------------------------
83 /* fd->cached metadata list of lists
84 each particular list contains key as a first entry, then the values
87 static void metadata_cache_update(FileData *fd, const gchar *key, const GList *values)
91 work = fd->cached_metadata;
94 auto entry = static_cast<GList *>(work->data);
95 auto entry_key = static_cast<gchar *>(entry->data);
97 if (strcmp(entry_key, key) == 0)
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);
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);
118 static const GList *metadata_cache_get(FileData *fd, const gchar *key)
122 work = fd->cached_metadata;
125 auto entry = static_cast<GList *>(work->data);
126 auto entry_key = static_cast<gchar *>(entry->data);
128 if (strcmp(entry_key, key) == 0)
131 DEBUG_1("found %s %s\n", key, fd->path);
137 DEBUG_1("not found %s %s\n", key, fd->path);
140 static void metadata_cache_remove(FileData *fd, const gchar *key)
144 work = fd->cached_metadata;
147 auto entry = static_cast<GList *>(work->data);
148 auto entry_key = static_cast<gchar *>(entry->data);
150 if (strcmp(entry_key, key) == 0)
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);
160 DEBUG_1("not removed %s %s\n", key, fd->path);
163 void metadata_cache_free(FileData *fd)
165 if (fd->cached_metadata) DEBUG_1("freed %s\n", fd->path);
167 g_list_free_full(fd->cached_metadata, [](gpointer data)
169 auto entry = static_cast<GList *>(data);
170 g_list_free_full(entry, g_free);
172 fd->cached_metadata = nullptr;
181 *-------------------------------------------------------------------
183 *-------------------------------------------------------------------
186 static GList *metadata_write_queue = nullptr;
187 static guint metadata_write_idle_id = 0; /* event source id */
189 static void metadata_write_queue_add(FileData *fd)
191 if (!g_list_find(metadata_write_queue, fd))
193 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
196 layout_util_status_update_write_all();
199 if (metadata_write_idle_id)
201 g_source_remove(metadata_write_idle_id);
202 metadata_write_idle_id = 0;
205 if (options->metadata.confirm_after_timeout)
207 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, nullptr);
212 gboolean metadata_write_queue_remove(FileData *fd)
214 g_hash_table_destroy(fd->modified_xmp);
215 fd->modified_xmp = nullptr;
217 metadata_write_queue = g_list_remove(metadata_write_queue, fd);
219 file_data_increment_version(fd);
220 file_data_send_notification(fd, NOTIFY_REREAD);
224 layout_util_status_update_write_all();
228 #pragma GCC diagnostic push
229 #pragma GCC diagnostic ignored "-Wunused-function"
230 gboolean metadata_write_queue_remove_list_unused(GList *list)
238 auto *fd = static_cast<FileData *>(work->data);
240 ret = ret && metadata_write_queue_remove(fd);
244 #pragma GCC diagnostic pop
246 void metadata_notify_cb(FileData *fd, NotifyType type, gpointer)
248 if (type & (NOTIFY_REREAD | NOTIFY_CHANGE))
250 metadata_cache_free(fd);
252 if (g_list_find(metadata_write_queue, fd))
254 DEBUG_1("Notify metadata: %s %04x", fd->path, type);
255 if (!isname(fd->path))
257 /* ignore deleted files */
258 metadata_write_queue_remove(fd);
264 gboolean metadata_write_queue_confirm(gboolean force_dialog, FileUtilDoneFunc done_func, gpointer done_data)
267 GList *to_approve = nullptr;
269 work = metadata_write_queue;
272 auto fd = static_cast<FileData *>(work->data);
275 if (!isname(fd->path))
277 /* ignore deleted files */
278 metadata_write_queue_remove(fd);
282 if (fd->change) continue; /* another operation in progress, skip this file for now */
284 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
287 file_util_write_metadata(nullptr, to_approve, nullptr, force_dialog, done_func, done_data);
289 return (metadata_write_queue != nullptr);
292 static gboolean metadata_write_queue_idle_cb(gpointer)
294 metadata_write_queue_confirm(FALSE, nullptr, nullptr);
295 metadata_write_idle_id = 0;
299 gboolean metadata_write_perform(FileData *fd)
305 g_assert(fd->change);
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)
311 success = metadata_legacy_write(fd);
312 if (success) metadata_legacy_delete(fd, fd->change->dest);
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;
323 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
324 exif_free_fd(fd, exif);
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)
333 @FIXME this does not catch new sidecars created by independent external programs
335 file_data_unref(file_data_new_group(fd->change->dest));
337 if (success) metadata_legacy_delete(fd, fd->change->dest);
341 gint metadata_queue_length()
343 return g_list_length(metadata_write_queue);
346 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
348 const gchar **k = keys;
352 if (strcmp(key, *k) == 0) return TRUE;
358 gboolean metadata_write_revert(FileData *fd, const gchar *key)
360 if (!fd->modified_xmp) return FALSE;
362 g_hash_table_remove(fd->modified_xmp, key);
364 if (g_hash_table_size(fd->modified_xmp) == 0)
366 metadata_write_queue_remove(fd);
370 /* reread the metadata to restore the original value */
371 file_data_increment_version(fd);
372 file_data_send_notification(fd, NOTIFY_REREAD);
377 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
379 if (!fd->modified_xmp)
381 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, reinterpret_cast<GDestroyNotify>(string_list_free));
383 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy(const_cast<GList *>(values)));
385 metadata_cache_remove(fd, key);
389 exif_update_metadata(fd->exif, key, values);
391 metadata_write_queue_add(fd);
392 file_data_increment_version(fd);
393 file_data_send_notification(fd, NOTIFY_METADATA);
395 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
397 GList *work = fd->sidecar_files;
401 auto sfd = static_cast<FileData *>(work->data);
404 if (sfd->format_class == FORMAT_CLASS_META) continue;
406 metadata_write_list(sfd, key, values);
414 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
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);
422 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
426 g_snprintf(string, sizeof(string), "%llu", static_cast<unsigned long long>(value));
427 return metadata_write_string(fd, key, string);
431 *-------------------------------------------------------------------
432 * keyword / comment read/write
433 *-------------------------------------------------------------------
436 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
440 ssi = secure_open(path);
441 if (!ssi) return FALSE;
443 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
445 secure_fprintf(ssi, "[keywords]\n");
446 while (keywords && secsave_errno == SS_ERR_NONE)
448 auto word = static_cast<const gchar *>(keywords->data);
449 keywords = keywords->next;
451 secure_fprintf(ssi, "%s\n", word);
453 secure_fputc(ssi, '\n');
455 secure_fprintf(ssi, "[comment]\n");
456 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
458 secure_fprintf(ssi, "#end\n");
460 return (secure_close(ssi) == 0);
463 static gboolean metadata_legacy_write(FileData *fd)
465 gboolean success = FALSE;
466 gchar *metadata_pathl;
469 gboolean have_keywords;
470 gboolean have_comment;
471 const gchar *comment;
472 GList *orig_keywords = nullptr;
473 gchar *orig_comment = nullptr;
475 g_assert(fd->change && fd->change->dest);
477 DEBUG_1("Saving comment: %s", fd->change->dest);
479 if (!fd->modified_xmp) return TRUE;
481 metadata_pathl = path_from_utf8(fd->change->dest);
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);
487 if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
489 success = metadata_file_write(metadata_pathl,
490 have_keywords ? static_cast<GList *>(keywords) : orig_keywords,
491 have_comment ? comment : orig_comment);
493 g_free(metadata_pathl);
494 g_free(orig_comment);
495 g_list_free_full(orig_keywords, g_free);
500 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
504 MetadataKey key = MK_NONE;
505 GList *list = nullptr;
506 GString *comment_build = nullptr;
508 f = fopen(path, "r");
509 if (!f) return FALSE;
511 while (fgets(s_buf, sizeof(s_buf), f))
515 if (*ptr == '#') continue;
516 if (*ptr == '[' && key != MK_COMMENT)
518 gchar *keystr = ++ptr;
521 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
526 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
528 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
540 while (*ptr != '\n' && *ptr != '\0') ptr++;
542 if (strlen(s_buf) > 0)
544 gchar *kw = utf8_validate_or_convert(s_buf);
546 list = g_list_prepend(list, kw);
551 if (!comment_build) comment_build = g_string_new("");
552 g_string_append(comment_build, s_buf);
561 *keywords = g_list_reverse(list);
565 g_list_free_full(list, g_free);
573 gchar *ptr = comment_build->str;
575 /* strip leading and trailing newlines */
576 while (*ptr == '\n') ptr++;
578 while (len > 0 && ptr[len - 1] == '\n') len--;
579 if (ptr[len] == '\n') len++; /* keep the last one */
582 gchar *text = g_strndup(ptr, len);
584 *comment = utf8_validate_or_convert(text);
588 g_string_free(comment_build, TRUE);
594 static void metadata_legacy_delete(FileData *fd, const gchar *except)
596 gchar *metadata_path;
597 gchar *metadata_pathl;
600 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
601 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
603 metadata_pathl = path_from_utf8(metadata_path);
604 unlink(metadata_pathl);
605 g_free(metadata_pathl);
606 g_free(metadata_path);
610 /* without exiv2: do not delete xmp metadata because we are not able to convert it,
612 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
613 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
615 metadata_pathl = path_from_utf8(metadata_path);
616 unlink(metadata_pathl);
617 g_free(metadata_pathl);
618 g_free(metadata_path);
623 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
625 gchar *metadata_path;
626 gchar *metadata_pathl;
627 gboolean success = FALSE;
629 if (!fd) return FALSE;
631 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
632 if (!metadata_path) return FALSE;
634 metadata_pathl = path_from_utf8(metadata_path);
636 success = metadata_file_read(metadata_pathl, keywords, comment);
638 g_free(metadata_pathl);
639 g_free(metadata_path);
644 static GList *remove_duplicate_strings_from_list(GList *list)
647 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
648 GList *newlist = nullptr;
652 auto key = static_cast<gchar *>(work->data);
654 if (g_hash_table_lookup(hashtable, key) == nullptr)
656 g_hash_table_insert(hashtable, key, GINT_TO_POINTER(1));
657 newlist = g_list_prepend(newlist, key);
662 g_hash_table_destroy(hashtable);
665 return g_list_reverse(newlist);
668 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
671 GList *list = nullptr;
672 const GList *cache_entry;
673 if (!fd) return nullptr;
675 /* unwritten data override everything */
676 if (fd->modified_xmp && format == METADATA_PLAIN)
678 list = static_cast<GList *>(g_hash_table_lookup(fd->modified_xmp, key));
679 if (list) return string_list_copy(list);
683 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
684 && (cache_entry = metadata_cache_get(fd, key)))
686 return string_list_copy(cache_entry->next);
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.
696 if (strcmp(key, KEYWORD_KEY) == 0)
698 if (metadata_legacy_read(fd, &list, nullptr))
700 if (format == METADATA_PLAIN)
702 metadata_cache_update(fd, key, list);
707 else if (strcmp(key, COMMENT_KEY) == 0)
709 gchar *comment = nullptr;
710 if (metadata_legacy_read(fd, nullptr, &comment)) return g_list_append(nullptr, comment);
712 else if (strncmp(key, "file.", 5) == 0)
714 return g_list_append(nullptr, metadata_file_info(fd, key, format));
717 else if (strncmp(key, "lua.", 4) == 0)
719 return g_list_append(nullptr, metadata_lua_info(fd, key, format));
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);
728 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
730 metadata_cache_update(fd, key, list);
736 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
738 GList *string_list = metadata_read_list(fd, key, format);
741 auto str = static_cast<gchar *>(string_list->data);
742 string_list->data = nullptr;
743 g_list_free_full(string_list, g_free);
749 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
753 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
754 if (!string) return fallback;
756 ret = g_ascii_strtoull(string, &endptr, 10);
757 if (string == endptr) ret = fallback;
762 gchar *metadata_read_rating_stars(FileData *fd)
765 gint n = metadata_read_int(fd, RATING_KEY, METADATA_PLAIN);
767 ret = convert_rating_to_stars(n);
772 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
776 gdouble deg, min, sec;
778 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
779 if (!string) return fallback;
781 deg = g_ascii_strtod(string, &endptr);
784 min = g_ascii_strtod(endptr + 1, &endptr);
786 sec = g_ascii_strtod(endptr + 1, &endptr);
791 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
793 coord = deg + min /60.0 + sec / 3600.0;
795 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
802 log_printf("unable to parse GPS coordinate '%s'\n", string);
809 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
814 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
815 if (!string) return fallback;
817 DEBUG_3("GPS_direction: %s\n", string);
818 deg = g_ascii_strtod(string, &endptr);
820 /* Expected text string is of the format e.g.:
832 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
840 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
842 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
846 return metadata_write_string(fd, key, value);
849 gchar *new_string = g_strconcat(str, value, NULL);
850 gboolean ret = metadata_write_string(fd, key, new_string);
856 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
864 char *old_locale, *saved_locale;
870 min = (param * 60) - (deg * 60);
871 if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
876 else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
883 log_printf("unknown GPS parameter key '%s'\n", key);
889 /* Avoid locale problems with commas and decimal points in numbers */
890 old_locale = setlocale(LC_ALL, nullptr);
891 saved_locale = strdup(old_locale);
892 if (saved_locale == nullptr)
896 setlocale(LC_ALL, "C");
898 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
899 metadata_write_string(fd, key, coordinate );
901 setlocale(LC_ALL, saved_locale);
909 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
911 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
915 return metadata_write_list(fd, key, values);
919 list = g_list_concat(list, string_list_copy(values));
920 list = remove_duplicate_strings_from_list(list);
922 ret = metadata_write_list(fd, key, list);
923 g_list_free_full(list, g_free);
928 * @see find_string_in_list
930 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
932 gchar *string_casefold = g_utf8_casefold(string, -1);
936 auto haystack = static_cast<gchar *>(list->data);
941 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
943 equal = (strcmp(haystack_casefold, string_casefold) == 0);
944 g_free(haystack_casefold);
948 g_free(string_casefold);
956 g_free(string_casefold);
961 * @see find_string_in_list
963 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
967 auto haystack = static_cast<gchar *>(list->data);
969 if (haystack && strcmp(haystack, string) == 0)
976 } // gchar *find_string_in_list_utf...
979 * @brief Find a existent string in a list.
981 * This is a switch between find_string_in_list_utf8case and
982 * find_string_in_list_utf8nocase to search with or without case for the
983 * existence of a string.
985 * @param list The list to search in
986 * @param string The string to search for
987 * @return The string or NULL
989 * @see find_string_in_list_utf8case
990 * @see find_string_in_list_utf8nocase
992 gchar *find_string_in_list(GList *list, const gchar *string)
994 if (options->metadata.keywords_case_sensitive)
995 return find_string_in_list_utf8case(list, string);
997 return find_string_in_list_utf8nocase(list, string);
1000 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
1002 GList *string_to_keywords_list(const gchar *text)
1004 GList *list = nullptr;
1005 const gchar *ptr = text;
1007 while (*ptr != '\0')
1012 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
1014 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
1020 /* trim starting and ending whitespaces */
1021 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
1022 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
1026 gchar *keyword = g_strndup(begin, l);
1028 /* only add if not already in the list */
1029 if (!find_string_in_list(list, keyword))
1030 list = g_list_append(list, keyword);
1044 gboolean meta_data_get_keyword_mark(FileData *fd, gint, gpointer data)
1046 /** @FIXME do not use global keyword_tree */
1047 auto path = static_cast<GList *>(data);
1049 gboolean found = FALSE;
1050 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1054 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1055 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1062 gboolean meta_data_set_keyword_mark(FileData *fd, gint, gboolean value, gpointer data)
1064 auto path = static_cast<GList *>(data);
1065 GList *keywords = nullptr;
1068 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1070 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1072 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1076 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1080 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1082 metadata_write_list(fd, KEYWORD_KEY, keywords);
1085 g_list_free_full(keywords, g_free);
1091 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1094 FileDataGetMarkFunc get_mark_func;
1095 FileDataSetMarkFunc set_mark_func;
1096 gpointer mark_func_data;
1100 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1102 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1103 if (get_mark_func == meta_data_get_keyword_mark)
1105 GtkTreeIter old_kw_iter;
1106 auto old_path = static_cast<GList *>(mark_func_data);
1108 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1109 (i == mark || /* release any previous connection of given mark */
1110 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1112 file_data_register_mark_func(i, nullptr, nullptr, nullptr, nullptr);
1113 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1119 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1123 path = keyword_tree_get_path(keyword_tree, kw_iter);
1124 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, reinterpret_cast<GDestroyNotify>(string_list_free));
1126 mark_str = g_strdup_printf("%d", (mark < 9 ? mark : -1) + 1);
1127 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1134 *-------------------------------------------------------------------
1136 *-------------------------------------------------------------------
1141 GtkTreeStore *keyword_tree;
1143 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1146 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1150 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1154 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1158 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1161 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1165 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1167 gboolean is_keyword;
1168 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1172 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1174 gchar *casefold = g_utf8_casefold(name, -1);
1175 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1176 KEYWORD_COLUMN_NAME, name,
1177 KEYWORD_COLUMN_CASEFOLD, casefold,
1178 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1182 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1184 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1185 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1186 gint ret = gtk_tree_path_compare(pa, pb);
1187 gtk_tree_path_free(pa);
1188 gtk_tree_path_free(pb);
1192 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1194 GtkTreeIter parent_a;
1195 GtkTreeIter parent_b;
1197 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1198 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1200 if (valid_pa && valid_pb)
1202 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1205 return (!valid_pa && !valid_pb); /* both are toplevel */
1208 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1212 gboolean toplevel = FALSE;
1218 parent = *parent_ptr;
1222 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1229 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? nullptr : &parent)) return FALSE;
1231 casefold = g_utf8_casefold(name, -1);
1236 if (!exclude_sibling || !sibling || keyword_compare(keyword_tree, &iter, sibling) != 0)
1238 if (options->metadata.keywords_case_sensitive)
1240 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1241 ret = strcmp(name, iter_name) == 0;
1246 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1247 ret = strcmp(casefold, iter_casefold) == 0;
1248 g_free(iter_casefold);
1249 } // if (options->metadata.tags_cas...
1253 if (result) *result = iter;
1256 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1263 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1266 gchar *mark, *name, *casefold;
1267 gboolean is_keyword;
1269 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1270 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1271 KEYWORD_COLUMN_NAME, &name,
1272 KEYWORD_COLUMN_CASEFOLD, &casefold,
1273 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1275 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1276 KEYWORD_COLUMN_NAME, name,
1277 KEYWORD_COLUMN_CASEFOLD, casefold,
1278 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1284 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1286 GtkTreeIter from_child;
1288 keyword_copy(keyword_tree, to, from);
1290 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1294 GtkTreeIter to_child;
1295 gtk_tree_store_append(keyword_tree, &to_child, to);
1296 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1297 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1301 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1303 keyword_copy_recursive(keyword_tree, to, from);
1304 keyword_delete(keyword_tree, from);
1307 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1309 GList *path = nullptr;
1310 GtkTreeIter iter = *iter_ptr;
1315 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1316 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1322 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1326 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1330 GtkTreeIter children;
1333 gchar *name = keyword_get_name(keyword_tree, &iter);
1334 if (strcmp(name, static_cast<const gchar *>(path->data)) == 0) break;
1336 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1345 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1351 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1353 if (!casefold_list) return FALSE;
1355 if (!keyword_get_is_keyword(keyword_tree, &iter))
1357 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1359 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1360 return FALSE; /* this should happen only on empty helpers */
1364 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1365 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1373 if (keyword_get_is_keyword(keyword_tree, &iter))
1375 GList *work = casefold_list;
1376 gboolean found = FALSE;
1377 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1380 auto casefold = static_cast<const gchar *>(work->data);
1383 if (strcmp(iter_casefold, casefold) == 0)
1389 g_free(iter_casefold);
1390 if (!found) return FALSE;
1393 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1398 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1400 if (!kw_list) return FALSE;
1402 if (!keyword_get_is_keyword(keyword_tree, &iter))
1404 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1406 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1407 return FALSE; /* this should happen only on empty helpers */
1411 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1412 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1420 if (keyword_get_is_keyword(keyword_tree, &iter))
1422 GList *work = kw_list;
1423 gboolean found = FALSE;
1424 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1427 auto name = static_cast<const gchar *>(work->data);
1430 if (strcmp(iter_name, name) == 0)
1437 if (!found) return FALSE;
1440 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1445 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1448 GList *casefold_list = nullptr;
1451 if (options->metadata.keywords_case_sensitive)
1453 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1460 auto kw = static_cast<const gchar *>(work->data);
1463 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1466 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1468 g_list_free_full(casefold_list, g_free);
1474 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1476 GtkTreeIter iter = *iter_ptr;
1481 if (keyword_get_is_keyword(keyword_tree, &iter))
1483 gchar *name = keyword_get_name(keyword_tree, &iter);
1484 if (!find_string_in_list(*kw_list, name))
1486 *kw_list = g_list_append(*kw_list, name);
1494 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1499 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1501 GtkTreeIter iter = *iter_ptr;
1502 GList *kw_list = nullptr;
1508 if (keyword_get_is_keyword(keyword_tree, &iter))
1510 gchar *name = keyword_get_name(keyword_tree, &iter);
1511 kw_list = g_list_append(kw_list, name);
1514 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1517 } // GList *keyword_tree_get(GtkTre...
1519 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1523 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1525 name = keyword_get_name(keyword_tree, iter);
1526 found = find_string_in_list(*kw_list, name);
1530 *kw_list = g_list_remove(*kw_list, found);
1536 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1539 keyword_tree_reset1(keyword_tree, iter, kw_list);
1541 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1545 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1546 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1550 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1554 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1555 return TRUE; /* this should happen only on empty helpers */
1559 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1560 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1564 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1566 GtkTreeIter iter = *iter_ptr;
1568 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1570 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1573 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1576 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1577 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1582 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1586 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1588 keyword_delete(keyword_tree, &child);
1591 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1593 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1596 gtk_tree_store_remove(keyword_tree, iter_ptr);
1600 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1603 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1604 if (!g_list_find(list, id))
1606 list = g_list_prepend(list, id);
1607 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1611 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1614 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1615 list = g_list_remove(list, id);
1616 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1619 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1622 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1623 return !!g_list_find(list, id);
1626 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1628 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1632 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1634 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1637 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1639 if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1641 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1646 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1648 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1651 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1653 GtkTreeIter iter = *iter_ptr;
1656 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1658 keyword_hide_in(keyword_tree, &iter, id);
1659 /* no need to check children of hidden node */
1664 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1666 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1669 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1673 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1676 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1677 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1680 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter_ptr, gpointer data)
1682 GtkTreeIter iter = *iter_ptr;
1683 auto keywords = static_cast<GList *>(data);
1684 gpointer id = keywords->data;
1685 keywords = keywords->next; /* hack */
1686 if (keyword_tree_is_set(model, &iter, keywords))
1691 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1692 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1699 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1701 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1702 keywords = g_list_prepend(keywords, id);
1703 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1704 keywords = g_list_delete_link(keywords, keywords);
1708 void keyword_tree_new()
1710 if (keyword_tree) return;
1712 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1715 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1718 gtk_tree_store_append(keyword_tree, &iter, parent);
1719 keyword_set(keyword_tree, &iter, name, is_keyword);
1723 void keyword_tree_new_default()
1727 if (!keyword_tree) keyword_tree_new();
1729 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("People"), TRUE);
1730 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1731 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1732 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1733 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1734 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1735 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1736 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Nature"), TRUE);
1737 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1738 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1739 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1740 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1741 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1742 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1743 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1744 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1745 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1746 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1747 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1748 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1749 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1750 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1751 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Art"), TRUE);
1752 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1753 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1754 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1755 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1756 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("City"), TRUE);
1757 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1758 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1759 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1760 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Architecture"), TRUE);
1761 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1762 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1763 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1764 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1765 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1766 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1767 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1768 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1769 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1770 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Places"), FALSE);
1771 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Conditions"), FALSE);
1772 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1773 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1774 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1775 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1776 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1777 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1778 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1779 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1780 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1781 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1782 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Photo"), FALSE);
1783 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1784 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1785 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1786 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1787 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1788 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1792 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1794 GtkTreeIter iter = *iter_ptr;
1797 GtkTreeIter children;
1801 WRITE_NL(); WRITE_STRING("<keyword ");
1802 name = keyword_get_name(keyword_tree, &iter);
1803 write_char_option(outstr, indent, "name", name);
1805 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1806 mark_str = keyword_get_mark(keyword_tree, &iter);
1807 if (mark_str && mark_str[0])
1809 write_char_option(outstr, indent, "mark", mark_str);
1812 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1816 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1818 WRITE_NL(); WRITE_STRING("</keyword>");
1824 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1828 void keyword_tree_write_config(GString *outstr, gint indent)
1831 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1834 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1836 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1839 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1842 void keyword_tree_node_disconnect_marks(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1844 GtkTreeIter iter = *iter_ptr;
1848 GtkTreeIter children;
1850 meta_data_connect_mark_with_keyword((keyword_tree), &iter, -1);
1852 if (gtk_tree_model_iter_children((keyword_tree), &children, &iter))
1854 keyword_tree_node_disconnect_marks((keyword_tree), &children);
1857 if (!gtk_tree_model_iter_next((keyword_tree), &iter)) return;
1861 void keyword_tree_disconnect_marks()
1865 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1867 keyword_tree_node_disconnect_marks(GTK_TREE_MODEL(keyword_tree), &iter);
1871 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1873 gchar *name = nullptr;
1874 gboolean is_kw = TRUE;
1875 gchar *mark_str = nullptr;
1877 while (*attribute_names)
1879 const gchar *option = *attribute_names++;
1880 const gchar *value = *attribute_values++;
1882 if (READ_CHAR_FULL("name", name)) continue;
1883 if (READ_BOOL_FULL("kw", is_kw)) continue;
1884 if (READ_CHAR_FULL("mark", mark_str)) continue;
1886 log_printf("unknown attribute %s = %s\n", option, value);
1888 if (name && name[0])
1891 /* re-use existing keyword if any */
1892 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, nullptr, name, FALSE, &iter))
1894 gtk_tree_store_append(keyword_tree, &iter, parent);
1896 keyword_set(keyword_tree, &iter, name, is_kw);
1900 gint i = static_cast<gint>(atoi(mark_str));
1903 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1908 return gtk_tree_iter_copy(&iter);
1914 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */