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)
780 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
781 if (!string) return fallback;
783 deg = g_ascii_strtod(string, &endptr);
786 min = g_ascii_strtod(endptr + 1, &endptr);
788 sec = g_ascii_strtod(endptr + 1, &endptr);
793 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
795 coord = deg + min /60.0 + sec / 3600.0;
797 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
804 log_printf("unable to parse GPS coordinate '%s'\n", string);
811 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
816 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
817 if (!string) return fallback;
819 DEBUG_3("GPS_direction: %s\n", string);
820 deg = g_ascii_strtod(string, &endptr);
822 /* Expected text string is of the format e.g.:
834 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
842 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
844 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
848 return metadata_write_string(fd, key, value);
851 gchar *new_string = g_strconcat(str, value, NULL);
852 gboolean ret = metadata_write_string(fd, key, new_string);
858 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
873 min = (param * 60) - (deg * 60);
874 if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
879 else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
886 log_printf("unknown GPS parameter key '%s'\n", key);
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)
899 setlocale(LC_ALL, "C");
901 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
902 metadata_write_string(fd, key, coordinate );
904 setlocale(LC_ALL, saved_locale);
912 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
914 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
918 return metadata_write_list(fd, key, values);
922 list = g_list_concat(list, string_list_copy(values));
923 list = remove_duplicate_strings_from_list(list);
925 ret = metadata_write_list(fd, key, list);
926 g_list_free_full(list, g_free);
931 * @see find_string_in_list
933 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
935 gchar *string_casefold = g_utf8_casefold(string, -1);
939 auto haystack = static_cast<gchar *>(list->data);
944 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
946 equal = (strcmp(haystack_casefold, string_casefold) == 0);
947 g_free(haystack_casefold);
951 g_free(string_casefold);
959 g_free(string_casefold);
964 * @see find_string_in_list
966 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
970 auto haystack = static_cast<gchar *>(list->data);
972 if (haystack && strcmp(haystack, string) == 0)
979 } // gchar *find_string_in_list_utf...
982 * @brief Find a existent string in a list.
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.
988 * @param list The list to search in
989 * @param string The string to search for
990 * @return The string or NULL
992 * @see find_string_in_list_utf8case
993 * @see find_string_in_list_utf8nocase
995 gchar *find_string_in_list(GList *list, const gchar *string)
997 if (options->metadata.keywords_case_sensitive)
998 return find_string_in_list_utf8case(list, string);
1000 return find_string_in_list_utf8nocase(list, string);
1003 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
1005 GList *string_to_keywords_list(const gchar *text)
1007 GList *list = nullptr;
1008 const gchar *ptr = text;
1010 while (*ptr != '\0')
1015 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
1017 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
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--;
1029 gchar *keyword = g_strndup(begin, l);
1031 /* only add if not already in the list */
1032 if (!find_string_in_list(list, keyword))
1033 list = g_list_append(list, keyword);
1047 gboolean meta_data_get_keyword_mark(FileData *fd, gint, gpointer data)
1049 /** @FIXME do not use global keyword_tree */
1050 auto path = static_cast<GList *>(data);
1052 gboolean found = FALSE;
1053 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
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))
1065 gboolean meta_data_set_keyword_mark(FileData *fd, gint, gboolean value, gpointer data)
1067 auto path = static_cast<GList *>(data);
1068 GList *keywords = nullptr;
1071 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1073 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1075 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1079 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1083 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1085 metadata_write_list(fd, KEYWORD_KEY, keywords);
1088 g_list_free_full(keywords, g_free);
1094 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1097 FileDataGetMarkFunc get_mark_func;
1098 FileDataSetMarkFunc set_mark_func;
1099 gpointer mark_func_data;
1103 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
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)
1108 GtkTreeIter old_kw_iter;
1109 auto old_path = static_cast<GList *>(mark_func_data);
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 */
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);
1122 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
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));
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);
1137 *-------------------------------------------------------------------
1139 *-------------------------------------------------------------------
1144 GtkTreeStore *keyword_tree;
1146 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1149 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1153 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1157 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1161 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1164 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1168 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1170 gboolean is_keyword;
1171 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1175 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
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);
1185 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
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);
1195 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1197 GtkTreeIter parent_a;
1198 GtkTreeIter parent_b;
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);
1203 if (valid_pa && valid_pb)
1205 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1208 return (!valid_pa && !valid_pb); /* both are toplevel */
1211 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1215 gboolean toplevel = FALSE;
1221 parent = *parent_ptr;
1225 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1232 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? nullptr : &parent)) return FALSE;
1234 casefold = g_utf8_casefold(name, -1);
1239 if (!exclude_sibling || !sibling || keyword_compare(keyword_tree, &iter, sibling) != 0)
1241 if (options->metadata.keywords_case_sensitive)
1243 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1244 ret = strcmp(name, iter_name) == 0;
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...
1256 if (result) *result = iter;
1259 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1266 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1272 gboolean is_keyword;
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);
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);
1289 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1291 GtkTreeIter from_child;
1293 keyword_copy(keyword_tree, to, from);
1295 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
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;
1306 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1308 keyword_copy_recursive(keyword_tree, to, from);
1309 keyword_delete(keyword_tree, from);
1312 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1314 GList *path = nullptr;
1315 GtkTreeIter iter = *iter_ptr;
1320 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1321 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1327 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1331 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1335 GtkTreeIter children;
1338 gchar *name = keyword_get_name(keyword_tree, &iter);
1339 if (strcmp(name, static_cast<const gchar *>(path->data)) == 0) break;
1341 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1350 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1356 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1358 if (!casefold_list) return FALSE;
1360 if (!keyword_get_is_keyword(keyword_tree, &iter))
1362 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1364 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1365 return FALSE; /* this should happen only on empty helpers */
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;
1378 if (keyword_get_is_keyword(keyword_tree, &iter))
1380 GList *work = casefold_list;
1381 gboolean found = FALSE;
1382 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1385 auto casefold = static_cast<const gchar *>(work->data);
1388 if (strcmp(iter_casefold, casefold) == 0)
1394 g_free(iter_casefold);
1395 if (!found) return FALSE;
1398 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1403 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1405 if (!kw_list) return FALSE;
1407 if (!keyword_get_is_keyword(keyword_tree, &iter))
1409 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1411 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1412 return FALSE; /* this should happen only on empty helpers */
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;
1425 if (keyword_get_is_keyword(keyword_tree, &iter))
1427 GList *work = kw_list;
1428 gboolean found = FALSE;
1429 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1432 auto name = static_cast<const gchar *>(work->data);
1435 if (strcmp(iter_name, name) == 0)
1442 if (!found) return FALSE;
1445 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1450 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1453 GList *casefold_list = nullptr;
1456 if (options->metadata.keywords_case_sensitive)
1458 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1465 auto kw = static_cast<const gchar *>(work->data);
1468 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1471 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1473 g_list_free_full(casefold_list, g_free);
1479 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1481 GtkTreeIter iter = *iter_ptr;
1486 if (keyword_get_is_keyword(keyword_tree, &iter))
1488 gchar *name = keyword_get_name(keyword_tree, &iter);
1489 if (!find_string_in_list(*kw_list, name))
1491 *kw_list = g_list_append(*kw_list, name);
1499 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1504 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1506 GtkTreeIter iter = *iter_ptr;
1507 GList *kw_list = nullptr;
1513 if (keyword_get_is_keyword(keyword_tree, &iter))
1515 gchar *name = keyword_get_name(keyword_tree, &iter);
1516 kw_list = g_list_append(kw_list, name);
1519 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1522 } // GList *keyword_tree_get(GtkTre...
1524 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1528 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1530 name = keyword_get_name(keyword_tree, iter);
1531 found = find_string_in_list(*kw_list, name);
1535 *kw_list = g_list_remove(*kw_list, found);
1541 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1544 keyword_tree_reset1(keyword_tree, iter, kw_list);
1546 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1550 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1551 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1555 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1559 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1560 return TRUE; /* this should happen only on empty helpers */
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;
1569 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1571 GtkTreeIter iter = *iter_ptr;
1573 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1575 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1578 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1581 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1582 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1587 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1591 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1593 keyword_delete(keyword_tree, &child);
1596 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1598 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1601 gtk_tree_store_remove(keyword_tree, iter_ptr);
1605 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1608 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1609 if (!g_list_find(list, id))
1611 list = g_list_prepend(list, id);
1612 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1616 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
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);
1624 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1627 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1628 return !!g_list_find(list, id);
1631 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1633 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1637 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1639 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1642 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1644 if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1646 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1651 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1653 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1656 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1658 GtkTreeIter iter = *iter_ptr;
1661 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1663 keyword_hide_in(keyword_tree, &iter, id);
1664 /* no need to check children of hidden node */
1669 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1671 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1674 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1678 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
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);
1685 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter_ptr, gpointer data)
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))
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;
1704 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
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);
1713 void keyword_tree_new()
1715 if (keyword_tree) return;
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);
1720 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1723 gtk_tree_store_append(keyword_tree, &iter, parent);
1724 keyword_set(keyword_tree, &iter, name, is_keyword);
1728 void keyword_tree_new_default()
1733 if (!keyword_tree) keyword_tree_new();
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);
1798 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1800 GtkTreeIter iter = *iter_ptr;
1803 GtkTreeIter children;
1807 WRITE_NL(); WRITE_STRING("<keyword ");
1808 name = keyword_get_name(keyword_tree, &iter);
1809 write_char_option(outstr, indent, "name", 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])
1815 write_char_option(outstr, indent, "mark", mark_str);
1818 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1822 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1824 WRITE_NL(); WRITE_STRING("</keyword>");
1830 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1834 void keyword_tree_write_config(GString *outstr, gint indent)
1837 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1840 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1842 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1845 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1848 void keyword_tree_node_disconnect_marks(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1850 GtkTreeIter iter = *iter_ptr;
1854 GtkTreeIter children;
1856 meta_data_connect_mark_with_keyword((keyword_tree), &iter, -1);
1858 if (gtk_tree_model_iter_children((keyword_tree), &children, &iter))
1860 keyword_tree_node_disconnect_marks((keyword_tree), &children);
1863 if (!gtk_tree_model_iter_next((keyword_tree), &iter)) return;
1867 void keyword_tree_disconnect_marks()
1871 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1873 keyword_tree_node_disconnect_marks(GTK_TREE_MODEL(keyword_tree), &iter);
1877 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1879 gchar *name = nullptr;
1880 gboolean is_kw = TRUE;
1881 gchar *mark_str = nullptr;
1883 while (*attribute_names)
1885 const gchar *option = *attribute_names++;
1886 const gchar *value = *attribute_values++;
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;
1892 log_printf("unknown attribute %s = %s\n", option, value);
1894 if (name && name[0])
1897 /* re-use existing keyword if any */
1898 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, nullptr, name, FALSE, &iter))
1900 gtk_tree_store_append(keyword_tree, &iter, parent);
1902 keyword_set(keyword_tree, &iter, name, is_kw);
1906 gint i = static_cast<gint>(atoi(mark_str));
1909 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1914 return gtk_tree_iter_copy(&iter);
1920 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */