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 <glib-object.h>
40 #include "layout-util.h"
41 #include "main-defines.h"
45 #include "secure-save.h"
46 #include "ui-fileops.h"
60 /* If contents change, keep GuideOptionsMetadata.xml up to date */
62 * @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
64 // @todo Use std::array
65 const gchar *group_keys[] = {
67 "Xmp.photoshop.Urgency",
68 "Xmp.photoshop.Category",
69 "Xmp.photoshop.SupplementalCategory",
72 "Xmp.photoshop.Instruction",
73 "Xmp.photoshop.DateCreated",
75 "Xmp.photoshop.AuthorsPosition",
77 "Xmp.photoshop.State",
78 "Xmp.iptc.CountryCode",
79 "Xmp.photoshop.Country",
80 "Xmp.photoshop.TransmissionReference",
81 "Xmp.photoshop.Headline",
82 "Xmp.photoshop.Credit",
83 "Xmp.photoshop.Source",
86 "Xmp.photoshop.CaptionWriter",
90 inline gboolean is_keywords_separator(gchar c)
101 static gboolean metadata_write_queue_idle_cb(gpointer data);
102 static gboolean metadata_legacy_write(FileData *fd);
103 static void metadata_legacy_delete(FileData *fd, const gchar *except);
104 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment);
108 *-------------------------------------------------------------------
109 * long-term cache - keep keywords from whole dir in memory
110 *-------------------------------------------------------------------
113 /* fd->cached metadata list of lists
114 each particular list contains key as a first entry, then the values
117 static void metadata_cache_update(FileData *fd, const gchar *key, const GList *values)
121 work = fd->cached_metadata;
124 auto entry = static_cast<GList *>(work->data);
125 auto entry_key = static_cast<gchar *>(entry->data);
127 if (strcmp(entry_key, key) == 0)
129 /* key found - just replace values */
130 GList *old_values = entry->next;
131 entry->next = nullptr;
132 old_values->prev = nullptr;
133 g_list_free_full(old_values, g_free);
134 work->data = g_list_append(entry, string_list_copy(values));
135 DEBUG_1("updated %s %s\n", key, fd->path);
141 /* key not found - prepend new entry */
142 fd->cached_metadata = g_list_prepend(fd->cached_metadata,
143 g_list_prepend(string_list_copy(values), g_strdup(key)));
144 DEBUG_1("added %s %s\n", key, fd->path);
148 static const GList *metadata_cache_get(FileData *fd, const gchar *key)
152 work = fd->cached_metadata;
155 auto entry = static_cast<GList *>(work->data);
156 auto entry_key = static_cast<gchar *>(entry->data);
158 if (strcmp(entry_key, key) == 0)
161 DEBUG_1("found %s %s\n", key, fd->path);
167 DEBUG_1("not found %s %s\n", key, fd->path);
170 static void metadata_cache_remove(FileData *fd, const gchar *key)
174 work = fd->cached_metadata;
177 auto entry = static_cast<GList *>(work->data);
178 auto entry_key = static_cast<gchar *>(entry->data);
180 if (strcmp(entry_key, key) == 0)
183 g_list_free_full(entry, g_free);
184 fd->cached_metadata = g_list_delete_link(fd->cached_metadata, work);
185 DEBUG_1("removed %s %s\n", key, fd->path);
190 DEBUG_1("not removed %s %s\n", key, fd->path);
193 void metadata_cache_free(FileData *fd)
195 if (fd->cached_metadata) DEBUG_1("freed %s\n", fd->path);
197 g_list_free_full(fd->cached_metadata, [](gpointer data)
199 auto entry = static_cast<GList *>(data);
200 g_list_free_full(entry, g_free);
202 fd->cached_metadata = nullptr;
211 *-------------------------------------------------------------------
213 *-------------------------------------------------------------------
216 static GList *metadata_write_queue = nullptr;
217 static guint metadata_write_idle_id = 0; /* event source id */
219 static void metadata_write_queue_add(FileData *fd)
221 if (!g_list_find(metadata_write_queue, fd))
223 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
226 layout_util_status_update_write_all();
229 if (metadata_write_idle_id)
231 g_source_remove(metadata_write_idle_id);
232 metadata_write_idle_id = 0;
235 if (options->metadata.confirm_after_timeout)
237 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, nullptr);
242 gboolean metadata_write_queue_remove(FileData *fd)
244 g_hash_table_destroy(fd->modified_xmp);
245 fd->modified_xmp = nullptr;
247 metadata_write_queue = g_list_remove(metadata_write_queue, fd);
249 file_data_increment_version(fd);
250 file_data_send_notification(fd, NOTIFY_REREAD);
254 layout_util_status_update_write_all();
258 #pragma GCC diagnostic push
259 #pragma GCC diagnostic ignored "-Wunused-function"
260 gboolean metadata_write_queue_remove_list_unused(GList *list)
268 auto *fd = static_cast<FileData *>(work->data);
270 ret = ret && metadata_write_queue_remove(fd);
274 #pragma GCC diagnostic pop
276 void metadata_notify_cb(FileData *fd, NotifyType type, gpointer)
278 if (type & (NOTIFY_REREAD | NOTIFY_CHANGE))
280 metadata_cache_free(fd);
282 if (g_list_find(metadata_write_queue, fd))
284 DEBUG_1("Notify metadata: %s %04x", fd->path, type);
285 if (!isname(fd->path))
287 /* ignore deleted files */
288 metadata_write_queue_remove(fd);
294 gboolean metadata_write_queue_confirm(gboolean force_dialog, FileUtilDoneFunc done_func, gpointer done_data)
297 GList *to_approve = nullptr;
299 work = metadata_write_queue;
302 auto fd = static_cast<FileData *>(work->data);
305 if (!isname(fd->path))
307 /* ignore deleted files */
308 metadata_write_queue_remove(fd);
312 if (fd->change) continue; /* another operation in progress, skip this file for now */
314 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
317 file_util_write_metadata(nullptr, to_approve, nullptr, force_dialog, done_func, done_data);
319 return (metadata_write_queue != nullptr);
322 static gboolean metadata_write_queue_idle_cb(gpointer)
324 metadata_write_queue_confirm(FALSE, nullptr, nullptr);
325 metadata_write_idle_id = 0;
329 gboolean metadata_write_perform(FileData *fd)
335 g_assert(fd->change);
337 lf = strlen(GQ_CACHE_EXT_METADATA);
338 if (fd->change->dest &&
339 g_ascii_strncasecmp(fd->change->dest + strlen(fd->change->dest) - lf, GQ_CACHE_EXT_METADATA, lf) == 0)
341 success = metadata_legacy_write(fd);
342 if (success) metadata_legacy_delete(fd, fd->change->dest);
346 /* write via exiv2 */
347 /* we can either use cached metadata which have fd->modified_xmp already applied
348 or read metadata from file and apply fd->modified_xmp
349 metadata are read also if the file was modified meanwhile */
350 exif = exif_read_fd(fd);
351 if (!exif) return FALSE;
353 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
354 exif_free_fd(fd, exif);
356 if (fd->change->dest)
357 /* this will create a FileData for the sidecar and link it to the main file
358 (we can't wait until the sidecar is discovered by directory scanning because
359 exif_read_fd is called before that and it would read the main file only and
360 store the metadata in the cache)
363 @FIXME this does not catch new sidecars created by independent external programs
365 file_data_unref(file_data_new_group(fd->change->dest));
367 if (success) metadata_legacy_delete(fd, fd->change->dest);
371 gint metadata_queue_length()
373 return g_list_length(metadata_write_queue);
376 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
378 const gchar **k = keys;
382 if (strcmp(key, *k) == 0) return TRUE;
388 gboolean metadata_write_revert(FileData *fd, const gchar *key)
390 if (!fd->modified_xmp) return FALSE;
392 g_hash_table_remove(fd->modified_xmp, key);
394 if (g_hash_table_size(fd->modified_xmp) == 0)
396 metadata_write_queue_remove(fd);
400 /* reread the metadata to restore the original value */
401 file_data_increment_version(fd);
402 file_data_send_notification(fd, NOTIFY_REREAD);
407 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
409 if (!fd->modified_xmp)
411 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, reinterpret_cast<GDestroyNotify>(string_list_free));
413 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy(const_cast<GList *>(values)));
415 metadata_cache_remove(fd, key);
419 exif_update_metadata(fd->exif, key, values);
421 metadata_write_queue_add(fd);
422 file_data_increment_version(fd);
423 file_data_send_notification(fd, NOTIFY_METADATA);
425 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
427 GList *work = fd->sidecar_files;
431 auto sfd = static_cast<FileData *>(work->data);
434 if (sfd->format_class == FORMAT_CLASS_META) continue;
436 metadata_write_list(sfd, key, values);
444 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
446 GList *list = g_list_append(nullptr, g_strdup(value));
447 gboolean ret = metadata_write_list(fd, key, list);
448 g_list_free_full(list, g_free);
452 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
456 g_snprintf(string, sizeof(string), "%llu", static_cast<unsigned long long>(value));
457 return metadata_write_string(fd, key, string);
461 *-------------------------------------------------------------------
462 * keyword / comment read/write
463 *-------------------------------------------------------------------
466 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
470 ssi = secure_open(path);
471 if (!ssi) return FALSE;
473 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
475 secure_fprintf(ssi, "[keywords]\n");
476 while (keywords && secsave_errno == SS_ERR_NONE)
478 auto word = static_cast<const gchar *>(keywords->data);
479 keywords = keywords->next;
481 secure_fprintf(ssi, "%s\n", word);
483 secure_fputc(ssi, '\n');
485 secure_fprintf(ssi, "[comment]\n");
486 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
488 secure_fprintf(ssi, "#end\n");
490 return (secure_close(ssi) == 0);
493 static gboolean metadata_legacy_write(FileData *fd)
495 gboolean success = FALSE;
496 gchar *metadata_pathl;
499 gboolean have_keywords;
500 gboolean have_comment;
501 const gchar *comment;
502 GList *orig_keywords = nullptr;
503 gchar *orig_comment = nullptr;
505 g_assert(fd->change && fd->change->dest);
507 DEBUG_1("Saving comment: %s", fd->change->dest);
509 if (!fd->modified_xmp) return TRUE;
511 metadata_pathl = path_from_utf8(fd->change->dest);
513 have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, nullptr, &keywords);
514 have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, nullptr, &comment_l);
515 comment = static_cast<const gchar *>((have_comment && comment_l) ? (static_cast<GList *>(comment_l))->data : nullptr);
517 if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
519 success = metadata_file_write(metadata_pathl,
520 have_keywords ? static_cast<GList *>(keywords) : orig_keywords,
521 have_comment ? comment : orig_comment);
523 g_free(metadata_pathl);
524 g_free(orig_comment);
525 g_list_free_full(orig_keywords, g_free);
530 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
534 MetadataKey key = MK_NONE;
535 GList *list = nullptr;
536 GString *comment_build = nullptr;
538 f = fopen(path, "r");
539 if (!f) return FALSE;
541 while (fgets(s_buf, sizeof(s_buf), f))
545 if (*ptr == '#') continue;
546 if (*ptr == '[' && key != MK_COMMENT)
548 gchar *keystr = ++ptr;
551 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
556 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
558 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
570 while (*ptr != '\n' && *ptr != '\0') ptr++;
572 if (strlen(s_buf) > 0)
574 gchar *kw = utf8_validate_or_convert(s_buf);
576 list = g_list_prepend(list, kw);
581 if (!comment_build) comment_build = g_string_new("");
582 g_string_append(comment_build, s_buf);
591 *keywords = g_list_reverse(list);
595 g_list_free_full(list, g_free);
603 gchar *ptr = comment_build->str;
605 /* strip leading and trailing newlines */
606 while (*ptr == '\n') ptr++;
608 while (len > 0 && ptr[len - 1] == '\n') len--;
609 if (ptr[len] == '\n') len++; /* keep the last one */
612 gchar *text = g_strndup(ptr, len);
614 *comment = utf8_validate_or_convert(text);
618 g_string_free(comment_build, TRUE);
624 static void metadata_legacy_delete(FileData *fd, const gchar *except)
626 gchar *metadata_path;
627 gchar *metadata_pathl;
630 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
631 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
633 metadata_pathl = path_from_utf8(metadata_path);
634 unlink(metadata_pathl);
635 g_free(metadata_pathl);
636 g_free(metadata_path);
640 /* without exiv2: do not delete xmp metadata because we are not able to convert it,
642 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
643 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
645 metadata_pathl = path_from_utf8(metadata_path);
646 unlink(metadata_pathl);
647 g_free(metadata_pathl);
648 g_free(metadata_path);
653 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
655 gchar *metadata_path;
656 gchar *metadata_pathl;
657 gboolean success = FALSE;
659 if (!fd) return FALSE;
661 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
662 if (!metadata_path) return FALSE;
664 metadata_pathl = path_from_utf8(metadata_path);
666 success = metadata_file_read(metadata_pathl, keywords, comment);
668 g_free(metadata_pathl);
669 g_free(metadata_path);
674 static GList *remove_duplicate_strings_from_list(GList *list)
677 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
678 GList *newlist = nullptr;
682 auto key = static_cast<gchar *>(work->data);
684 if (g_hash_table_lookup(hashtable, key) == nullptr)
686 g_hash_table_insert(hashtable, key, GINT_TO_POINTER(1));
687 newlist = g_list_prepend(newlist, key);
692 g_hash_table_destroy(hashtable);
695 return g_list_reverse(newlist);
698 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
701 GList *list = nullptr;
702 const GList *cache_entry;
703 if (!fd) return nullptr;
705 /* unwritten data override everything */
706 if (fd->modified_xmp && format == METADATA_PLAIN)
708 list = static_cast<GList *>(g_hash_table_lookup(fd->modified_xmp, key));
709 if (list) return string_list_copy(list);
713 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
714 && (cache_entry = metadata_cache_get(fd, key)))
716 return string_list_copy(cache_entry->next);
720 Legacy metadata file is the primary source if it exists.
721 Merging the lists does not make much sense, because the existence of
722 legacy metadata file indicates that the other metadata sources are not
723 writable and thus it would not be possible to delete the keywords
724 that comes from the image file.
726 if (strcmp(key, KEYWORD_KEY) == 0)
728 if (metadata_legacy_read(fd, &list, nullptr))
730 if (format == METADATA_PLAIN)
732 metadata_cache_update(fd, key, list);
737 else if (strcmp(key, COMMENT_KEY) == 0)
739 gchar *comment = nullptr;
740 if (metadata_legacy_read(fd, nullptr, &comment)) return g_list_append(nullptr, comment);
742 else if (strncmp(key, "file.", 5) == 0)
744 return g_list_append(nullptr, metadata_file_info(fd, key, format));
747 else if (strncmp(key, "lua.", 4) == 0)
749 return g_list_append(nullptr, metadata_lua_info(fd, key, format));
753 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
754 if (!exif) return nullptr;
755 list = exif_get_metadata(exif, key, format);
756 exif_free_fd(fd, exif);
758 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
760 metadata_cache_update(fd, key, list);
766 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
768 GList *string_list = metadata_read_list(fd, key, format);
771 auto str = static_cast<gchar *>(string_list->data);
772 string_list->data = nullptr;
773 g_list_free_full(string_list, g_free);
779 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
783 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
784 if (!string) return fallback;
786 ret = g_ascii_strtoull(string, &endptr, 10);
787 if (string == endptr) ret = fallback;
792 gchar *metadata_read_rating_stars(FileData *fd)
795 gint n = metadata_read_int(fd, RATING_KEY, METADATA_PLAIN);
797 ret = convert_rating_to_stars(n);
802 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
810 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
811 if (!string) return fallback;
813 deg = g_ascii_strtod(string, &endptr);
816 min = g_ascii_strtod(endptr + 1, &endptr);
818 sec = g_ascii_strtod(endptr + 1, &endptr);
823 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
825 coord = deg + min /60.0 + sec / 3600.0;
827 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
834 log_printf("unable to parse GPS coordinate '%s'\n", string);
841 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
846 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
847 if (!string) return fallback;
849 DEBUG_3("GPS_direction: %s\n", string);
850 deg = g_ascii_strtod(string, &endptr);
852 /* Expected text string is of the format e.g.:
864 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
872 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
874 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
878 return metadata_write_string(fd, key, value);
881 gchar *new_string = g_strconcat(str, value, NULL);
882 gboolean ret = metadata_write_string(fd, key, new_string);
888 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
903 min = (param * 60) - (deg * 60);
904 if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
909 else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
916 log_printf("unknown GPS parameter key '%s'\n", key);
922 /* Avoid locale problems with commas and decimal points in numbers */
923 old_locale = setlocale(LC_ALL, nullptr);
924 saved_locale = strdup(old_locale);
925 if (saved_locale == nullptr)
929 setlocale(LC_ALL, "C");
931 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
932 metadata_write_string(fd, key, coordinate );
934 setlocale(LC_ALL, saved_locale);
942 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
944 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
948 return metadata_write_list(fd, key, values);
952 list = g_list_concat(list, string_list_copy(values));
953 list = remove_duplicate_strings_from_list(list);
955 ret = metadata_write_list(fd, key, list);
956 g_list_free_full(list, g_free);
961 * @see find_string_in_list
963 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
965 gchar *string_casefold = g_utf8_casefold(string, -1);
969 auto haystack = static_cast<gchar *>(list->data);
974 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
976 equal = (strcmp(haystack_casefold, string_casefold) == 0);
977 g_free(haystack_casefold);
981 g_free(string_casefold);
989 g_free(string_casefold);
994 * @see find_string_in_list
996 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
1000 auto haystack = static_cast<gchar *>(list->data);
1002 if (haystack && strcmp(haystack, string) == 0)
1009 } // gchar *find_string_in_list_utf...
1012 * @brief Find a existent string in a list.
1014 * This is a switch between find_string_in_list_utf8case and
1015 * find_string_in_list_utf8nocase to search with or without case for the
1016 * existence of a string.
1018 * @param list The list to search in
1019 * @param string The string to search for
1020 * @return The string or NULL
1022 * @see find_string_in_list_utf8case
1023 * @see find_string_in_list_utf8nocase
1025 gchar *find_string_in_list(GList *list, const gchar *string)
1027 if (options->metadata.keywords_case_sensitive)
1028 return find_string_in_list_utf8case(list, string);
1030 return find_string_in_list_utf8nocase(list, string);
1033 GList *string_to_keywords_list(const gchar *text)
1035 GList *list = nullptr;
1036 const gchar *ptr = text;
1038 while (*ptr != '\0')
1043 while (is_keywords_separator(*ptr)) ptr++;
1045 while (*ptr != '\0' && !is_keywords_separator(*ptr))
1051 /* trim starting and ending whitespaces */
1052 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
1053 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
1057 gchar *keyword = g_strndup(begin, l);
1059 /* only add if not already in the list */
1060 if (!find_string_in_list(list, keyword))
1061 list = g_list_append(list, keyword);
1075 gboolean meta_data_get_keyword_mark(FileData *fd, gint, gpointer data)
1077 /** @FIXME do not use global keyword_tree */
1078 auto path = static_cast<GList *>(data);
1080 gboolean found = FALSE;
1081 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1085 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1086 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1093 gboolean meta_data_set_keyword_mark(FileData *fd, gint, gboolean value, gpointer data)
1095 auto path = static_cast<GList *>(data);
1096 GList *keywords = nullptr;
1099 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1101 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1103 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1107 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1111 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1113 metadata_write_list(fd, KEYWORD_KEY, keywords);
1116 g_list_free_full(keywords, g_free);
1122 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1125 FileDataGetMarkFunc get_mark_func;
1126 FileDataSetMarkFunc set_mark_func;
1127 gpointer mark_func_data;
1131 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1133 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1134 if (get_mark_func == meta_data_get_keyword_mark)
1136 GtkTreeIter old_kw_iter;
1137 auto old_path = static_cast<GList *>(mark_func_data);
1139 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1140 (i == mark || /* release any previous connection of given mark */
1141 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1143 file_data_register_mark_func(i, nullptr, nullptr, nullptr, nullptr);
1144 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1150 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1154 path = keyword_tree_get_path(keyword_tree, kw_iter);
1155 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, reinterpret_cast<GDestroyNotify>(string_list_free));
1157 mark_str = g_strdup_printf("%d", (mark < 9 ? mark : -1) + 1);
1158 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1165 *-------------------------------------------------------------------
1167 *-------------------------------------------------------------------
1172 GtkTreeStore *keyword_tree;
1174 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1177 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1181 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1185 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1189 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1192 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1196 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1198 gboolean is_keyword;
1199 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1203 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1205 gchar *casefold = g_utf8_casefold(name, -1);
1206 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1207 KEYWORD_COLUMN_NAME, name,
1208 KEYWORD_COLUMN_CASEFOLD, casefold,
1209 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1213 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1215 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1216 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1217 gint ret = gtk_tree_path_compare(pa, pb);
1218 gtk_tree_path_free(pa);
1219 gtk_tree_path_free(pb);
1223 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1225 GtkTreeIter parent_a;
1226 GtkTreeIter parent_b;
1228 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1229 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1231 if (valid_pa && valid_pb)
1233 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1236 return (!valid_pa && !valid_pb); /* both are toplevel */
1239 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1243 gboolean toplevel = FALSE;
1249 parent = *parent_ptr;
1253 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1260 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? nullptr : &parent)) return FALSE;
1262 casefold = g_utf8_casefold(name, -1);
1267 if (!exclude_sibling || !sibling || keyword_compare(keyword_tree, &iter, sibling) != 0)
1269 if (options->metadata.keywords_case_sensitive)
1271 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1272 ret = strcmp(name, iter_name) == 0;
1277 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1278 ret = strcmp(casefold, iter_casefold) == 0;
1279 g_free(iter_casefold);
1280 } // if (options->metadata.tags_cas...
1284 if (result) *result = iter;
1287 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1294 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1300 gboolean is_keyword;
1302 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1303 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1304 KEYWORD_COLUMN_NAME, &name,
1305 KEYWORD_COLUMN_CASEFOLD, &casefold,
1306 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1308 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1309 KEYWORD_COLUMN_NAME, name,
1310 KEYWORD_COLUMN_CASEFOLD, casefold,
1311 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1317 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1319 GtkTreeIter from_child;
1321 keyword_copy(keyword_tree, to, from);
1323 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1327 GtkTreeIter to_child;
1328 gtk_tree_store_append(keyword_tree, &to_child, to);
1329 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1330 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1334 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1336 keyword_copy_recursive(keyword_tree, to, from);
1337 keyword_delete(keyword_tree, from);
1340 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1342 GList *path = nullptr;
1343 GtkTreeIter iter = *iter_ptr;
1348 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1349 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1355 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1359 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1363 GtkTreeIter children;
1366 gchar *name = keyword_get_name(keyword_tree, &iter);
1367 if (strcmp(name, static_cast<const gchar *>(path->data)) == 0) break;
1369 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1378 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1384 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1386 if (!casefold_list) return FALSE;
1388 if (!keyword_get_is_keyword(keyword_tree, &iter))
1390 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1392 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1393 return FALSE; /* this should happen only on empty helpers */
1397 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1398 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1406 if (keyword_get_is_keyword(keyword_tree, &iter))
1408 GList *work = casefold_list;
1409 gboolean found = FALSE;
1410 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1413 auto casefold = static_cast<const gchar *>(work->data);
1416 if (strcmp(iter_casefold, casefold) == 0)
1422 g_free(iter_casefold);
1423 if (!found) return FALSE;
1426 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1431 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1433 if (!kw_list) return FALSE;
1435 if (!keyword_get_is_keyword(keyword_tree, &iter))
1437 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1439 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1440 return FALSE; /* this should happen only on empty helpers */
1444 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1445 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1453 if (keyword_get_is_keyword(keyword_tree, &iter))
1455 GList *work = kw_list;
1456 gboolean found = FALSE;
1457 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1460 auto name = static_cast<const gchar *>(work->data);
1463 if (strcmp(iter_name, name) == 0)
1470 if (!found) return FALSE;
1473 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1478 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1481 GList *casefold_list = nullptr;
1484 if (options->metadata.keywords_case_sensitive)
1486 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1493 auto kw = static_cast<const gchar *>(work->data);
1496 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1499 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1501 g_list_free_full(casefold_list, g_free);
1507 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1509 GtkTreeIter iter = *iter_ptr;
1514 if (keyword_get_is_keyword(keyword_tree, &iter))
1516 gchar *name = keyword_get_name(keyword_tree, &iter);
1517 if (!find_string_in_list(*kw_list, name))
1519 *kw_list = g_list_append(*kw_list, name);
1527 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1532 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1534 GtkTreeIter iter = *iter_ptr;
1535 GList *kw_list = nullptr;
1541 if (keyword_get_is_keyword(keyword_tree, &iter))
1543 gchar *name = keyword_get_name(keyword_tree, &iter);
1544 kw_list = g_list_append(kw_list, name);
1547 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1550 } // GList *keyword_tree_get(GtkTre...
1552 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1556 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1558 name = keyword_get_name(keyword_tree, iter);
1559 found = find_string_in_list(*kw_list, name);
1563 *kw_list = g_list_remove(*kw_list, found);
1569 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1572 keyword_tree_reset1(keyword_tree, iter, kw_list);
1574 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1578 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1579 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1583 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1587 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1588 return TRUE; /* this should happen only on empty helpers */
1592 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1593 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1597 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1599 GtkTreeIter iter = *iter_ptr;
1601 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1603 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1606 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1609 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1610 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1615 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1619 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1621 keyword_delete(keyword_tree, &child);
1624 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1626 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1629 gtk_tree_store_remove(keyword_tree, iter_ptr);
1633 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1636 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1637 if (!g_list_find(list, id))
1639 list = g_list_prepend(list, id);
1640 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1644 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1647 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1648 list = g_list_remove(list, id);
1649 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1652 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1655 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1656 return !!g_list_find(list, id);
1659 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1661 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1665 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1667 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1670 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1672 if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1674 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1679 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1681 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1684 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1686 GtkTreeIter iter = *iter_ptr;
1689 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1691 keyword_hide_in(keyword_tree, &iter, id);
1692 /* no need to check children of hidden node */
1697 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1699 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1702 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1706 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1709 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1710 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1713 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter_ptr, gpointer data)
1715 GtkTreeIter iter = *iter_ptr;
1716 auto keywords = static_cast<GList *>(data);
1717 gpointer id = keywords->data;
1718 keywords = keywords->next; /* hack */
1719 if (keyword_tree_is_set(model, &iter, keywords))
1724 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1725 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1732 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1734 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1735 keywords = g_list_prepend(keywords, id);
1736 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1737 keywords = g_list_delete_link(keywords, keywords);
1741 void keyword_tree_new()
1743 if (keyword_tree) return;
1745 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1748 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1751 gtk_tree_store_append(keyword_tree, &iter, parent);
1752 keyword_set(keyword_tree, &iter, name, is_keyword);
1756 void keyword_tree_new_default()
1761 if (!keyword_tree) keyword_tree_new();
1763 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("People"), TRUE);
1764 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1765 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1766 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1767 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1768 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1769 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1770 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Nature"), TRUE);
1771 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1772 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1773 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1774 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1775 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1776 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1777 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1778 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1779 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1780 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1781 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1782 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1783 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1784 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1785 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Art"), TRUE);
1786 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1787 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1788 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1789 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1790 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("City"), TRUE);
1791 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1792 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1793 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1794 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Architecture"), TRUE);
1795 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1796 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1797 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1798 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1799 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1800 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1801 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1802 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1803 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1804 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Places"), FALSE);
1805 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Conditions"), FALSE);
1806 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1807 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1808 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1809 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1810 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1811 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1812 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1813 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1814 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1815 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1816 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Photo"), FALSE);
1817 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1818 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1819 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1820 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1821 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1822 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1826 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1828 GtkTreeIter iter = *iter_ptr;
1831 GtkTreeIter children;
1835 WRITE_NL(); WRITE_STRING("<keyword ");
1836 name = keyword_get_name(keyword_tree, &iter);
1837 write_char_option(outstr, indent, "name", name);
1839 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1840 mark_str = keyword_get_mark(keyword_tree, &iter);
1841 if (mark_str && mark_str[0])
1843 write_char_option(outstr, indent, "mark", mark_str);
1846 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1850 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1852 WRITE_NL(); WRITE_STRING("</keyword>");
1858 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1862 void keyword_tree_write_config(GString *outstr, gint indent)
1865 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1868 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1870 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1873 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1876 void keyword_tree_node_disconnect_marks(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1878 GtkTreeIter iter = *iter_ptr;
1882 GtkTreeIter children;
1884 meta_data_connect_mark_with_keyword((keyword_tree), &iter, -1);
1886 if (gtk_tree_model_iter_children((keyword_tree), &children, &iter))
1888 keyword_tree_node_disconnect_marks((keyword_tree), &children);
1891 if (!gtk_tree_model_iter_next((keyword_tree), &iter)) return;
1895 void keyword_tree_disconnect_marks()
1899 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1901 keyword_tree_node_disconnect_marks(GTK_TREE_MODEL(keyword_tree), &iter);
1905 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1907 gchar *name = nullptr;
1908 gboolean is_kw = TRUE;
1909 gchar *mark_str = nullptr;
1911 while (*attribute_names)
1913 const gchar *option = *attribute_names++;
1914 const gchar *value = *attribute_values++;
1916 if (READ_CHAR_FULL("name", name)) continue;
1917 if (READ_BOOL_FULL("kw", is_kw)) continue;
1918 if (READ_CHAR_FULL("mark", mark_str)) continue;
1920 log_printf("unknown attribute %s = %s\n", option, value);
1922 if (name && name[0])
1925 /* re-use existing keyword if any */
1926 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, nullptr, name, FALSE, &iter))
1928 gtk_tree_store_append(keyword_tree, &iter, parent);
1930 keyword_set(keyword_tree, &iter, name, is_kw);
1934 gint i = static_cast<gint>(atoi(mark_str));
1937 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1942 return gtk_tree_iter_copy(&iter);
1948 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */