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.
33 #include <glib-object.h>
42 #include "layout-util.h"
43 #include "main-defines.h"
47 #include "secure-save.h"
48 #include "ui-fileops.h"
62 /* If contents change, keep GuideOptionsMetadata.xml up to date */
64 * @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
66 constexpr std::array<const gchar *, 22> group_keys{
68 "Xmp.photoshop.Urgency",
69 "Xmp.photoshop.Category",
70 "Xmp.photoshop.SupplementalCategory",
73 "Xmp.photoshop.Instruction",
74 "Xmp.photoshop.DateCreated",
76 "Xmp.photoshop.AuthorsPosition",
78 "Xmp.photoshop.State",
79 "Xmp.iptc.CountryCode",
80 "Xmp.photoshop.Country",
81 "Xmp.photoshop.TransmissionReference",
82 "Xmp.photoshop.Headline",
83 "Xmp.photoshop.Credit",
84 "Xmp.photoshop.Source",
87 "Xmp.photoshop.CaptionWriter",
91 inline gboolean is_keywords_separator(gchar c)
102 static gboolean metadata_write_queue_idle_cb(gpointer data);
103 static gboolean metadata_legacy_write(FileData *fd);
104 static void metadata_legacy_delete(FileData *fd, const gchar *except);
105 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment);
109 *-------------------------------------------------------------------
110 * long-term cache - keep keywords from whole dir in memory
111 *-------------------------------------------------------------------
114 /* fd->cached metadata list of lists
115 each particular list contains key as a first entry, then the values
118 static void metadata_cache_update(FileData *fd, const gchar *key, const GList *values)
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)
130 /* key found - just replace values */
131 GList *old_values = entry->next;
132 entry->next = nullptr;
133 old_values->prev = nullptr;
134 g_list_free_full(old_values, g_free);
135 work->data = g_list_append(entry, string_list_copy(values));
136 DEBUG_1("updated %s %s\n", key, fd->path);
142 /* key not found - prepend new entry */
143 fd->cached_metadata = g_list_prepend(fd->cached_metadata,
144 g_list_prepend(string_list_copy(values), g_strdup(key)));
145 DEBUG_1("added %s %s\n", key, fd->path);
149 static const GList *metadata_cache_get(FileData *fd, const gchar *key)
153 work = fd->cached_metadata;
156 auto entry = static_cast<GList *>(work->data);
157 auto entry_key = static_cast<gchar *>(entry->data);
159 if (strcmp(entry_key, key) == 0)
162 DEBUG_1("found %s %s\n", key, fd->path);
168 DEBUG_1("not found %s %s\n", key, fd->path);
171 static void metadata_cache_remove(FileData *fd, const gchar *key)
175 work = fd->cached_metadata;
178 auto entry = static_cast<GList *>(work->data);
179 auto entry_key = static_cast<gchar *>(entry->data);
181 if (strcmp(entry_key, key) == 0)
184 g_list_free_full(entry, g_free);
185 fd->cached_metadata = g_list_delete_link(fd->cached_metadata, work);
186 DEBUG_1("removed %s %s\n", key, fd->path);
191 DEBUG_1("not removed %s %s\n", key, fd->path);
194 void metadata_cache_free(FileData *fd)
196 if (fd->cached_metadata) DEBUG_1("freed %s\n", fd->path);
198 g_list_free_full(fd->cached_metadata, [](gpointer data)
200 auto entry = static_cast<GList *>(data);
201 g_list_free_full(entry, g_free);
203 fd->cached_metadata = nullptr;
212 *-------------------------------------------------------------------
214 *-------------------------------------------------------------------
217 static GList *metadata_write_queue = nullptr;
218 static guint metadata_write_idle_id = 0; /* event source id */
220 static void metadata_write_queue_add(FileData *fd)
222 if (!g_list_find(metadata_write_queue, fd))
224 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
227 layout_util_status_update_write_all();
230 if (metadata_write_idle_id)
232 g_source_remove(metadata_write_idle_id);
233 metadata_write_idle_id = 0;
236 if (options->metadata.confirm_after_timeout)
238 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, nullptr);
243 gboolean metadata_write_queue_remove(FileData *fd)
245 g_hash_table_destroy(fd->modified_xmp);
246 fd->modified_xmp = nullptr;
248 metadata_write_queue = g_list_remove(metadata_write_queue, fd);
250 file_data_increment_version(fd);
251 file_data_send_notification(fd, NOTIFY_REREAD);
255 layout_util_status_update_write_all();
259 #pragma GCC diagnostic push
260 #pragma GCC diagnostic ignored "-Wunused-function"
261 gboolean metadata_write_queue_remove_list_unused(GList *list)
269 auto *fd = static_cast<FileData *>(work->data);
271 ret = ret && metadata_write_queue_remove(fd);
275 #pragma GCC diagnostic pop
277 void metadata_notify_cb(FileData *fd, NotifyType type, gpointer)
279 if (type & (NOTIFY_REREAD | NOTIFY_CHANGE))
281 metadata_cache_free(fd);
283 if (g_list_find(metadata_write_queue, fd))
285 DEBUG_1("Notify metadata: %s %04x", fd->path, type);
286 if (!isname(fd->path))
288 /* ignore deleted files */
289 metadata_write_queue_remove(fd);
295 gboolean metadata_write_queue_confirm(gboolean force_dialog, FileUtilDoneFunc done_func, gpointer done_data)
298 GList *to_approve = nullptr;
300 work = metadata_write_queue;
303 auto fd = static_cast<FileData *>(work->data);
306 if (!isname(fd->path))
308 /* ignore deleted files */
309 metadata_write_queue_remove(fd);
313 if (fd->change) continue; /* another operation in progress, skip this file for now */
315 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
318 file_util_write_metadata(nullptr, to_approve, nullptr, force_dialog, done_func, done_data);
320 return (metadata_write_queue != nullptr);
323 static gboolean metadata_write_queue_idle_cb(gpointer)
325 metadata_write_queue_confirm(FALSE, nullptr, nullptr);
326 metadata_write_idle_id = 0;
330 gboolean metadata_write_perform(FileData *fd)
336 g_assert(fd->change);
338 lf = strlen(GQ_CACHE_EXT_METADATA);
339 if (fd->change->dest &&
340 g_ascii_strncasecmp(fd->change->dest + strlen(fd->change->dest) - lf, GQ_CACHE_EXT_METADATA, lf) == 0)
342 success = metadata_legacy_write(fd);
343 if (success) metadata_legacy_delete(fd, fd->change->dest);
347 /* write via exiv2 */
348 /* we can either use cached metadata which have fd->modified_xmp already applied
349 or read metadata from file and apply fd->modified_xmp
350 metadata are read also if the file was modified meanwhile */
351 exif = exif_read_fd(fd);
352 if (!exif) return FALSE;
354 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
355 exif_free_fd(fd, exif);
357 if (fd->change->dest)
358 /* this will create a FileData for the sidecar and link it to the main file
359 (we can't wait until the sidecar is discovered by directory scanning because
360 exif_read_fd is called before that and it would read the main file only and
361 store the metadata in the cache)
364 @FIXME this does not catch new sidecars created by independent external programs
366 file_data_unref(file_data_new_group(fd->change->dest));
368 if (success) metadata_legacy_delete(fd, fd->change->dest);
372 gint metadata_queue_length()
374 return g_list_length(metadata_write_queue);
377 gboolean metadata_write_revert(FileData *fd, const gchar *key)
379 if (!fd->modified_xmp) return FALSE;
381 g_hash_table_remove(fd->modified_xmp, key);
383 if (g_hash_table_size(fd->modified_xmp) == 0)
385 metadata_write_queue_remove(fd);
389 /* reread the metadata to restore the original value */
390 file_data_increment_version(fd);
391 file_data_send_notification(fd, NOTIFY_REREAD);
396 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
398 if (!fd->modified_xmp)
400 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, reinterpret_cast<GDestroyNotify>(string_list_free));
402 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy(const_cast<GList *>(values)));
404 metadata_cache_remove(fd, key);
408 exif_update_metadata(fd->exif, key, values);
410 metadata_write_queue_add(fd);
411 file_data_increment_version(fd);
412 file_data_send_notification(fd, NOTIFY_METADATA);
414 auto metadata_check_key = [key](const gchar *k) { return strcmp(key, k) == 0; };
415 if (options->metadata.sync_grouped_files && std::any_of(group_keys.cbegin(), group_keys.cend(), metadata_check_key))
417 GList *work = fd->sidecar_files;
421 auto sfd = static_cast<FileData *>(work->data);
424 if (sfd->format_class == FORMAT_CLASS_META) continue;
426 metadata_write_list(sfd, key, values);
434 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
436 GList *list = g_list_append(nullptr, g_strdup(value));
437 gboolean ret = metadata_write_list(fd, key, list);
438 g_list_free_full(list, g_free);
442 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
446 g_snprintf(string, sizeof(string), "%llu", static_cast<unsigned long long>(value));
447 return metadata_write_string(fd, key, string);
451 *-------------------------------------------------------------------
452 * keyword / comment read/write
453 *-------------------------------------------------------------------
456 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
460 ssi = secure_open(path);
461 if (!ssi) return FALSE;
463 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
465 secure_fprintf(ssi, "[keywords]\n");
466 while (keywords && secsave_errno == SS_ERR_NONE)
468 auto word = static_cast<const gchar *>(keywords->data);
469 keywords = keywords->next;
471 secure_fprintf(ssi, "%s\n", word);
473 secure_fputc(ssi, '\n');
475 secure_fprintf(ssi, "[comment]\n");
476 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
478 secure_fprintf(ssi, "#end\n");
480 return (secure_close(ssi) == 0);
483 static gboolean metadata_legacy_write(FileData *fd)
485 gboolean success = FALSE;
486 gchar *metadata_pathl;
489 gboolean have_keywords;
490 gboolean have_comment;
491 const gchar *comment;
492 GList *orig_keywords = nullptr;
493 gchar *orig_comment = nullptr;
495 g_assert(fd->change && fd->change->dest);
497 DEBUG_1("Saving comment: %s", fd->change->dest);
499 if (!fd->modified_xmp) return TRUE;
501 metadata_pathl = path_from_utf8(fd->change->dest);
503 have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, nullptr, &keywords);
504 have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, nullptr, &comment_l);
505 comment = static_cast<const gchar *>((have_comment && comment_l) ? (static_cast<GList *>(comment_l))->data : nullptr);
507 if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
509 success = metadata_file_write(metadata_pathl,
510 have_keywords ? static_cast<GList *>(keywords) : orig_keywords,
511 have_comment ? comment : orig_comment);
513 g_free(metadata_pathl);
514 g_free(orig_comment);
515 g_list_free_full(orig_keywords, g_free);
520 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
524 MetadataKey key = MK_NONE;
525 GList *list = nullptr;
526 GString *comment_build = nullptr;
528 f = fopen(path, "r");
529 if (!f) return FALSE;
531 while (fgets(s_buf, sizeof(s_buf), f))
535 if (*ptr == '#') continue;
536 if (*ptr == '[' && key != MK_COMMENT)
538 gchar *keystr = ++ptr;
541 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
546 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
548 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
560 while (*ptr != '\n' && *ptr != '\0') ptr++;
562 if (strlen(s_buf) > 0)
564 gchar *kw = utf8_validate_or_convert(s_buf);
566 list = g_list_prepend(list, kw);
571 if (!comment_build) comment_build = g_string_new("");
572 g_string_append(comment_build, s_buf);
581 *keywords = g_list_reverse(list);
585 g_list_free_full(list, g_free);
593 gchar *ptr = comment_build->str;
595 /* strip leading and trailing newlines */
596 while (*ptr == '\n') ptr++;
598 while (len > 0 && ptr[len - 1] == '\n') len--;
599 if (ptr[len] == '\n') len++; /* keep the last one */
602 gchar *text = g_strndup(ptr, len);
604 *comment = utf8_validate_or_convert(text);
608 g_string_free(comment_build, TRUE);
614 static void metadata_legacy_delete(FileData *fd, const gchar *except)
616 gchar *metadata_path;
617 gchar *metadata_pathl;
620 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
621 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
623 metadata_pathl = path_from_utf8(metadata_path);
624 unlink(metadata_pathl);
625 g_free(metadata_pathl);
626 g_free(metadata_path);
630 /* without exiv2: do not delete xmp metadata because we are not able to convert it,
632 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
633 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
635 metadata_pathl = path_from_utf8(metadata_path);
636 unlink(metadata_pathl);
637 g_free(metadata_pathl);
638 g_free(metadata_path);
643 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
645 gchar *metadata_path;
646 gchar *metadata_pathl;
647 gboolean success = FALSE;
649 if (!fd) return FALSE;
651 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
652 if (!metadata_path) return FALSE;
654 metadata_pathl = path_from_utf8(metadata_path);
656 success = metadata_file_read(metadata_pathl, keywords, comment);
658 g_free(metadata_pathl);
659 g_free(metadata_path);
664 static GList *remove_duplicate_strings_from_list(GList *list)
667 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
668 GList *newlist = nullptr;
672 auto key = static_cast<gchar *>(work->data);
674 if (g_hash_table_lookup(hashtable, key) == nullptr)
676 g_hash_table_insert(hashtable, key, GINT_TO_POINTER(1));
677 newlist = g_list_prepend(newlist, key);
682 g_hash_table_destroy(hashtable);
685 return g_list_reverse(newlist);
688 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
691 GList *list = nullptr;
692 const GList *cache_entry;
693 if (!fd) return nullptr;
695 /* unwritten data override everything */
696 if (fd->modified_xmp && format == METADATA_PLAIN)
698 list = static_cast<GList *>(g_hash_table_lookup(fd->modified_xmp, key));
699 if (list) return string_list_copy(list);
703 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
704 && (cache_entry = metadata_cache_get(fd, key)))
706 return string_list_copy(cache_entry->next);
710 Legacy metadata file is the primary source if it exists.
711 Merging the lists does not make much sense, because the existence of
712 legacy metadata file indicates that the other metadata sources are not
713 writable and thus it would not be possible to delete the keywords
714 that comes from the image file.
716 if (strcmp(key, KEYWORD_KEY) == 0)
718 if (metadata_legacy_read(fd, &list, nullptr))
720 if (format == METADATA_PLAIN)
722 metadata_cache_update(fd, key, list);
727 else if (strcmp(key, COMMENT_KEY) == 0)
729 gchar *comment = nullptr;
730 if (metadata_legacy_read(fd, nullptr, &comment)) return g_list_append(nullptr, comment);
732 else if (strncmp(key, "file.", 5) == 0)
734 return g_list_append(nullptr, metadata_file_info(fd, key, format));
737 else if (strncmp(key, "lua.", 4) == 0)
739 return g_list_append(nullptr, metadata_lua_info(fd, key, format));
743 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
744 if (!exif) return nullptr;
745 list = exif_get_metadata(exif, key, format);
746 exif_free_fd(fd, exif);
748 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
750 metadata_cache_update(fd, key, list);
756 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
758 GList *string_list = metadata_read_list(fd, key, format);
761 auto str = static_cast<gchar *>(string_list->data);
762 string_list->data = nullptr;
763 g_list_free_full(string_list, g_free);
769 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
773 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
774 if (!string) return fallback;
776 ret = g_ascii_strtoull(string, &endptr, 10);
777 if (string == endptr) ret = fallback;
782 gchar *metadata_read_rating_stars(FileData *fd)
785 gint n = metadata_read_int(fd, RATING_KEY, METADATA_PLAIN);
787 ret = convert_rating_to_stars(n);
792 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
800 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
801 if (!string) return fallback;
803 deg = g_ascii_strtod(string, &endptr);
806 min = g_ascii_strtod(endptr + 1, &endptr);
808 sec = g_ascii_strtod(endptr + 1, &endptr);
813 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
815 coord = deg + min /60.0 + sec / 3600.0;
817 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
824 log_printf("unable to parse GPS coordinate '%s'\n", string);
831 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
836 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
837 if (!string) return fallback;
839 DEBUG_3("GPS_direction: %s\n", string);
840 deg = g_ascii_strtod(string, &endptr);
842 /* Expected text string is of the format e.g.:
854 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
862 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
864 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
868 return metadata_write_string(fd, key, value);
871 gchar *new_string = g_strconcat(str, value, NULL);
872 gboolean ret = metadata_write_string(fd, key, new_string);
878 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
893 min = (param * 60) - (deg * 60);
894 if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
899 else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
906 log_printf("unknown GPS parameter key '%s'\n", key);
912 /* Avoid locale problems with commas and decimal points in numbers */
913 old_locale = setlocale(LC_ALL, nullptr);
914 saved_locale = strdup(old_locale);
915 if (saved_locale == nullptr)
919 setlocale(LC_ALL, "C");
921 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
922 metadata_write_string(fd, key, coordinate );
924 setlocale(LC_ALL, saved_locale);
932 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
934 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
938 return metadata_write_list(fd, key, values);
942 list = g_list_concat(list, string_list_copy(values));
943 list = remove_duplicate_strings_from_list(list);
945 ret = metadata_write_list(fd, key, list);
946 g_list_free_full(list, g_free);
951 * @see find_string_in_list
953 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
955 gchar *string_casefold = g_utf8_casefold(string, -1);
959 auto haystack = static_cast<gchar *>(list->data);
964 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
966 equal = (strcmp(haystack_casefold, string_casefold) == 0);
967 g_free(haystack_casefold);
971 g_free(string_casefold);
979 g_free(string_casefold);
984 * @see find_string_in_list
986 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
990 auto haystack = static_cast<gchar *>(list->data);
992 if (haystack && strcmp(haystack, string) == 0)
999 } // gchar *find_string_in_list_utf...
1002 * @brief Find a existent string in a list.
1004 * This is a switch between find_string_in_list_utf8case and
1005 * find_string_in_list_utf8nocase to search with or without case for the
1006 * existence of a string.
1008 * @param list The list to search in
1009 * @param string The string to search for
1010 * @return The string or NULL
1012 * @see find_string_in_list_utf8case
1013 * @see find_string_in_list_utf8nocase
1015 gchar *find_string_in_list(GList *list, const gchar *string)
1017 if (options->metadata.keywords_case_sensitive)
1018 return find_string_in_list_utf8case(list, string);
1020 return find_string_in_list_utf8nocase(list, string);
1023 GList *string_to_keywords_list(const gchar *text)
1025 GList *list = nullptr;
1026 const gchar *ptr = text;
1028 while (*ptr != '\0')
1033 while (is_keywords_separator(*ptr)) ptr++;
1035 while (*ptr != '\0' && !is_keywords_separator(*ptr))
1041 /* trim starting and ending whitespaces */
1042 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
1043 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
1047 gchar *keyword = g_strndup(begin, l);
1049 /* only add if not already in the list */
1050 if (!find_string_in_list(list, keyword))
1051 list = g_list_append(list, keyword);
1065 gboolean meta_data_get_keyword_mark(FileData *fd, gint, gpointer data)
1067 /** @FIXME do not use global keyword_tree */
1068 auto path = static_cast<GList *>(data);
1070 gboolean found = FALSE;
1071 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1075 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1076 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1083 gboolean meta_data_set_keyword_mark(FileData *fd, gint, gboolean value, gpointer data)
1085 auto path = static_cast<GList *>(data);
1086 GList *keywords = nullptr;
1089 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1091 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1093 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1097 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1101 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1103 metadata_write_list(fd, KEYWORD_KEY, keywords);
1106 g_list_free_full(keywords, g_free);
1112 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1115 FileDataGetMarkFunc get_mark_func;
1116 FileDataSetMarkFunc set_mark_func;
1117 gpointer mark_func_data;
1121 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1123 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1124 if (get_mark_func == meta_data_get_keyword_mark)
1126 GtkTreeIter old_kw_iter;
1127 auto old_path = static_cast<GList *>(mark_func_data);
1129 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1130 (i == mark || /* release any previous connection of given mark */
1131 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1133 file_data_register_mark_func(i, nullptr, nullptr, nullptr, nullptr);
1134 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1140 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1144 path = keyword_tree_get_path(keyword_tree, kw_iter);
1145 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, reinterpret_cast<GDestroyNotify>(string_list_free));
1147 mark_str = g_strdup_printf("%d", (mark < 9 ? mark : -1) + 1);
1148 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1155 *-------------------------------------------------------------------
1157 *-------------------------------------------------------------------
1162 GtkTreeStore *keyword_tree;
1164 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1167 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1171 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1175 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1179 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1182 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1186 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1188 gboolean is_keyword;
1189 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1193 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1195 gchar *casefold = g_utf8_casefold(name, -1);
1196 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1197 KEYWORD_COLUMN_NAME, name,
1198 KEYWORD_COLUMN_CASEFOLD, casefold,
1199 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1203 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1205 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1206 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1207 gint ret = gtk_tree_path_compare(pa, pb);
1208 gtk_tree_path_free(pa);
1209 gtk_tree_path_free(pb);
1213 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1215 GtkTreeIter parent_a;
1216 GtkTreeIter parent_b;
1218 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1219 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1221 if (valid_pa && valid_pb)
1223 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1226 return (!valid_pa && !valid_pb); /* both are toplevel */
1229 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1233 gboolean toplevel = FALSE;
1239 parent = *parent_ptr;
1243 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1250 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? nullptr : &parent)) return FALSE;
1252 casefold = g_utf8_casefold(name, -1);
1257 if (!exclude_sibling || !sibling || keyword_compare(keyword_tree, &iter, sibling) != 0)
1259 if (options->metadata.keywords_case_sensitive)
1261 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1262 ret = strcmp(name, iter_name) == 0;
1267 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1268 ret = strcmp(casefold, iter_casefold) == 0;
1269 g_free(iter_casefold);
1270 } // if (options->metadata.tags_cas...
1274 if (result) *result = iter;
1277 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1284 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1290 gboolean is_keyword;
1292 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1293 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1294 KEYWORD_COLUMN_NAME, &name,
1295 KEYWORD_COLUMN_CASEFOLD, &casefold,
1296 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1298 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1299 KEYWORD_COLUMN_NAME, name,
1300 KEYWORD_COLUMN_CASEFOLD, casefold,
1301 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1307 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1309 GtkTreeIter from_child;
1311 keyword_copy(keyword_tree, to, from);
1313 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1317 GtkTreeIter to_child;
1318 gtk_tree_store_append(keyword_tree, &to_child, to);
1319 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1320 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1324 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1326 keyword_copy_recursive(keyword_tree, to, from);
1327 keyword_delete(keyword_tree, from);
1330 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1332 GList *path = nullptr;
1333 GtkTreeIter iter = *iter_ptr;
1338 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1339 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1345 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1349 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1353 GtkTreeIter children;
1356 gchar *name = keyword_get_name(keyword_tree, &iter);
1357 if (strcmp(name, static_cast<const gchar *>(path->data)) == 0) break;
1359 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1368 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1374 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1376 if (!casefold_list) return FALSE;
1378 if (!keyword_get_is_keyword(keyword_tree, &iter))
1380 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1382 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1383 return FALSE; /* this should happen only on empty helpers */
1387 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1388 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1396 if (keyword_get_is_keyword(keyword_tree, &iter))
1398 GList *work = casefold_list;
1399 gboolean found = FALSE;
1400 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1403 auto casefold = static_cast<const gchar *>(work->data);
1406 if (strcmp(iter_casefold, casefold) == 0)
1412 g_free(iter_casefold);
1413 if (!found) return FALSE;
1416 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1421 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1423 if (!kw_list) return FALSE;
1425 if (!keyword_get_is_keyword(keyword_tree, &iter))
1427 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1429 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1430 return FALSE; /* this should happen only on empty helpers */
1434 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1435 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1443 if (keyword_get_is_keyword(keyword_tree, &iter))
1445 GList *work = kw_list;
1446 gboolean found = FALSE;
1447 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1450 auto name = static_cast<const gchar *>(work->data);
1453 if (strcmp(iter_name, name) == 0)
1460 if (!found) return FALSE;
1463 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1468 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1471 GList *casefold_list = nullptr;
1474 if (options->metadata.keywords_case_sensitive)
1476 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1483 auto kw = static_cast<const gchar *>(work->data);
1486 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1489 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1491 g_list_free_full(casefold_list, g_free);
1497 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1499 GtkTreeIter iter = *iter_ptr;
1504 if (keyword_get_is_keyword(keyword_tree, &iter))
1506 gchar *name = keyword_get_name(keyword_tree, &iter);
1507 if (!find_string_in_list(*kw_list, name))
1509 *kw_list = g_list_append(*kw_list, name);
1517 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1522 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1524 GtkTreeIter iter = *iter_ptr;
1525 GList *kw_list = nullptr;
1531 if (keyword_get_is_keyword(keyword_tree, &iter))
1533 gchar *name = keyword_get_name(keyword_tree, &iter);
1534 kw_list = g_list_append(kw_list, name);
1537 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1540 } // GList *keyword_tree_get(GtkTre...
1542 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1546 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1548 name = keyword_get_name(keyword_tree, iter);
1549 found = find_string_in_list(*kw_list, name);
1553 *kw_list = g_list_remove(*kw_list, found);
1559 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1562 keyword_tree_reset1(keyword_tree, iter, kw_list);
1564 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1568 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1569 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1573 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1577 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1578 return TRUE; /* this should happen only on empty helpers */
1582 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1583 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1587 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1589 GtkTreeIter iter = *iter_ptr;
1591 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1593 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1596 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1599 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1600 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1605 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1609 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1611 keyword_delete(keyword_tree, &child);
1614 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1616 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1619 gtk_tree_store_remove(keyword_tree, iter_ptr);
1623 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1626 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1627 if (!g_list_find(list, id))
1629 list = g_list_prepend(list, id);
1630 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1634 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1637 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1638 list = g_list_remove(list, id);
1639 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1642 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1645 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1646 return !!g_list_find(list, id);
1649 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1651 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1655 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1657 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1660 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1662 if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1664 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1669 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1671 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1674 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1676 GtkTreeIter iter = *iter_ptr;
1679 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1681 keyword_hide_in(keyword_tree, &iter, id);
1682 /* no need to check children of hidden node */
1687 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1689 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1692 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1696 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1699 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1700 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1703 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter_ptr, gpointer data)
1705 GtkTreeIter iter = *iter_ptr;
1706 auto keywords = static_cast<GList *>(data);
1707 gpointer id = keywords->data;
1708 keywords = keywords->next; /* hack */
1709 if (keyword_tree_is_set(model, &iter, keywords))
1714 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1715 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1722 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1724 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1725 keywords = g_list_prepend(keywords, id);
1726 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1727 keywords = g_list_delete_link(keywords, keywords);
1731 void keyword_tree_new()
1733 if (keyword_tree) return;
1735 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1738 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1741 gtk_tree_store_append(keyword_tree, &iter, parent);
1742 keyword_set(keyword_tree, &iter, name, is_keyword);
1746 void keyword_tree_new_default()
1751 if (!keyword_tree) keyword_tree_new();
1753 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("People"), TRUE);
1754 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1755 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1756 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1757 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1758 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1759 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1760 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Nature"), TRUE);
1761 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1762 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1763 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1764 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1765 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1766 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1767 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1768 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1769 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1770 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1771 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1772 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1773 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1774 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1775 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Art"), TRUE);
1776 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1777 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1778 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1779 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1780 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("City"), TRUE);
1781 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1782 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1783 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1784 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Architecture"), TRUE);
1785 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1786 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1787 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1788 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1789 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1790 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1791 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1792 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1793 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1794 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Places"), FALSE);
1795 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Conditions"), FALSE);
1796 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1797 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1798 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1799 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1800 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1801 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1802 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1803 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1804 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1805 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1806 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Photo"), FALSE);
1807 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1808 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1809 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1810 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1811 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1812 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1816 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1818 GtkTreeIter iter = *iter_ptr;
1821 GtkTreeIter children;
1825 WRITE_NL(); WRITE_STRING("<keyword ");
1826 name = keyword_get_name(keyword_tree, &iter);
1827 write_char_option(outstr, indent, "name", name);
1829 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1830 mark_str = keyword_get_mark(keyword_tree, &iter);
1831 if (mark_str && mark_str[0])
1833 write_char_option(outstr, indent, "mark", mark_str);
1836 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1840 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1842 WRITE_NL(); WRITE_STRING("</keyword>");
1848 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1852 void keyword_tree_write_config(GString *outstr, gint indent)
1855 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1858 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1860 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1863 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1866 void keyword_tree_node_disconnect_marks(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1868 GtkTreeIter iter = *iter_ptr;
1872 GtkTreeIter children;
1874 meta_data_connect_mark_with_keyword((keyword_tree), &iter, -1);
1876 if (gtk_tree_model_iter_children((keyword_tree), &children, &iter))
1878 keyword_tree_node_disconnect_marks((keyword_tree), &children);
1881 if (!gtk_tree_model_iter_next((keyword_tree), &iter)) return;
1885 void keyword_tree_disconnect_marks()
1889 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1891 keyword_tree_node_disconnect_marks(GTK_TREE_MODEL(keyword_tree), &iter);
1895 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1897 gchar *name = nullptr;
1898 gboolean is_kw = TRUE;
1899 gchar *mark_str = nullptr;
1901 while (*attribute_names)
1903 const gchar *option = *attribute_names++;
1904 const gchar *value = *attribute_values++;
1906 if (READ_CHAR_FULL("name", name)) continue;
1907 if (READ_BOOL_FULL("kw", is_kw)) continue;
1908 if (READ_CHAR_FULL("mark", mark_str)) continue;
1910 log_printf("unknown attribute %s = %s\n", option, value);
1912 if (name && name[0])
1915 /* re-use existing keyword if any */
1916 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, nullptr, name, FALSE, &iter))
1918 gtk_tree_store_append(keyword_tree, &iter, parent);
1920 keyword_set(keyword_tree, &iter, name, is_kw);
1924 gint i = static_cast<gint>(atoi(mark_str));
1927 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1932 return gtk_tree_iter_copy(&iter);
1938 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */