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 "layout-util.h"
34 #include "main-defines.h"
37 #include "secure-save.h"
38 #include "ui-fileops.h"
47 /* If contents change, keep GuideOptionsMetadata.xml up to date */
49 * @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
51 static const gchar *group_keys[] = {
53 "Xmp.photoshop.Urgency",
54 "Xmp.photoshop.Category",
55 "Xmp.photoshop.SupplementalCategory",
58 "Xmp.photoshop.Instruction",
59 "Xmp.photoshop.DateCreated",
61 "Xmp.photoshop.AuthorsPosition",
63 "Xmp.photoshop.State",
64 "Xmp.iptc.CountryCode",
65 "Xmp.photoshop.Country",
66 "Xmp.photoshop.TransmissionReference",
67 "Xmp.photoshop.Headline",
68 "Xmp.photoshop.Credit",
69 "Xmp.photoshop.Source",
72 "Xmp.photoshop.CaptionWriter",
75 static gboolean metadata_write_queue_idle_cb(gpointer data);
76 static gboolean metadata_legacy_write(FileData *fd);
77 static void metadata_legacy_delete(FileData *fd, const gchar *except);
78 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment);
82 *-------------------------------------------------------------------
83 * long-term cache - keep keywords from whole dir in memory
84 *-------------------------------------------------------------------
87 /* fd->cached metadata list of lists
88 each particular list contains key as a first entry, then the values
91 static void metadata_cache_update(FileData *fd, const gchar *key, const GList *values)
95 work = fd->cached_metadata;
98 auto entry = static_cast<GList *>(work->data);
99 auto entry_key = static_cast<gchar *>(entry->data);
101 if (strcmp(entry_key, key) == 0)
103 /* key found - just replace values */
104 GList *old_values = entry->next;
105 entry->next = nullptr;
106 old_values->prev = nullptr;
107 g_list_free_full(old_values, g_free);
108 work->data = g_list_append(entry, string_list_copy(values));
109 DEBUG_1("updated %s %s\n", key, fd->path);
115 /* key not found - prepend new entry */
116 fd->cached_metadata = g_list_prepend(fd->cached_metadata,
117 g_list_prepend(string_list_copy(values), g_strdup(key)));
118 DEBUG_1("added %s %s\n", key, fd->path);
122 static const GList *metadata_cache_get(FileData *fd, const gchar *key)
126 work = fd->cached_metadata;
129 auto entry = static_cast<GList *>(work->data);
130 auto entry_key = static_cast<gchar *>(entry->data);
132 if (strcmp(entry_key, key) == 0)
135 DEBUG_1("found %s %s\n", key, fd->path);
141 DEBUG_1("not found %s %s\n", key, fd->path);
144 static void metadata_cache_remove(FileData *fd, const gchar *key)
148 work = fd->cached_metadata;
151 auto entry = static_cast<GList *>(work->data);
152 auto entry_key = static_cast<gchar *>(entry->data);
154 if (strcmp(entry_key, key) == 0)
157 g_list_free_full(entry, g_free);
158 fd->cached_metadata = g_list_delete_link(fd->cached_metadata, work);
159 DEBUG_1("removed %s %s\n", key, fd->path);
164 DEBUG_1("not removed %s %s\n", key, fd->path);
167 void metadata_cache_free(FileData *fd)
169 if (fd->cached_metadata) DEBUG_1("freed %s\n", fd->path);
171 g_list_free_full(fd->cached_metadata, [](gpointer data)
173 auto entry = static_cast<GList *>(data);
174 g_list_free_full(entry, g_free);
176 fd->cached_metadata = nullptr;
185 *-------------------------------------------------------------------
187 *-------------------------------------------------------------------
190 static GList *metadata_write_queue = nullptr;
191 static guint metadata_write_idle_id = 0; /* event source id */
193 static void metadata_write_queue_add(FileData *fd)
195 if (!g_list_find(metadata_write_queue, fd))
197 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
200 layout_util_status_update_write_all();
203 if (metadata_write_idle_id)
205 g_source_remove(metadata_write_idle_id);
206 metadata_write_idle_id = 0;
209 if (options->metadata.confirm_after_timeout)
211 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, nullptr);
216 gboolean metadata_write_queue_remove(FileData *fd)
218 g_hash_table_destroy(fd->modified_xmp);
219 fd->modified_xmp = nullptr;
221 metadata_write_queue = g_list_remove(metadata_write_queue, fd);
223 file_data_increment_version(fd);
224 file_data_send_notification(fd, NOTIFY_REREAD);
228 layout_util_status_update_write_all();
232 #pragma GCC diagnostic push
233 #pragma GCC diagnostic ignored "-Wunused-function"
234 gboolean metadata_write_queue_remove_list_unused(GList *list)
242 auto *fd = static_cast<FileData *>(work->data);
244 ret = ret && metadata_write_queue_remove(fd);
248 #pragma GCC diagnostic pop
250 void metadata_notify_cb(FileData *fd, NotifyType type, gpointer)
252 if (type & (NOTIFY_REREAD | NOTIFY_CHANGE))
254 metadata_cache_free(fd);
256 if (g_list_find(metadata_write_queue, fd))
258 DEBUG_1("Notify metadata: %s %04x", fd->path, type);
259 if (!isname(fd->path))
261 /* ignore deleted files */
262 metadata_write_queue_remove(fd);
268 gboolean metadata_write_queue_confirm(gboolean force_dialog, FileUtilDoneFunc done_func, gpointer done_data)
271 GList *to_approve = nullptr;
273 work = metadata_write_queue;
276 auto fd = static_cast<FileData *>(work->data);
279 if (!isname(fd->path))
281 /* ignore deleted files */
282 metadata_write_queue_remove(fd);
286 if (fd->change) continue; /* another operation in progress, skip this file for now */
288 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
291 file_util_write_metadata(nullptr, to_approve, nullptr, force_dialog, done_func, done_data);
293 return (metadata_write_queue != nullptr);
296 static gboolean metadata_write_queue_idle_cb(gpointer)
298 metadata_write_queue_confirm(FALSE, nullptr, nullptr);
299 metadata_write_idle_id = 0;
303 gboolean metadata_write_perform(FileData *fd)
309 g_assert(fd->change);
311 lf = strlen(GQ_CACHE_EXT_METADATA);
312 if (fd->change->dest &&
313 g_ascii_strncasecmp(fd->change->dest + strlen(fd->change->dest) - lf, GQ_CACHE_EXT_METADATA, lf) == 0)
315 success = metadata_legacy_write(fd);
316 if (success) metadata_legacy_delete(fd, fd->change->dest);
320 /* write via exiv2 */
321 /* we can either use cached metadata which have fd->modified_xmp already applied
322 or read metadata from file and apply fd->modified_xmp
323 metadata are read also if the file was modified meanwhile */
324 exif = exif_read_fd(fd);
325 if (!exif) return FALSE;
327 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
328 exif_free_fd(fd, exif);
330 if (fd->change->dest)
331 /* this will create a FileData for the sidecar and link it to the main file
332 (we can't wait until the sidecar is discovered by directory scanning because
333 exif_read_fd is called before that and it would read the main file only and
334 store the metadata in the cache)
337 @FIXME this does not catch new sidecars created by independent external programs
339 file_data_unref(file_data_new_group(fd->change->dest));
341 if (success) metadata_legacy_delete(fd, fd->change->dest);
345 gint metadata_queue_length()
347 return g_list_length(metadata_write_queue);
350 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
352 const gchar **k = keys;
356 if (strcmp(key, *k) == 0) return TRUE;
362 gboolean metadata_write_revert(FileData *fd, const gchar *key)
364 if (!fd->modified_xmp) return FALSE;
366 g_hash_table_remove(fd->modified_xmp, key);
368 if (g_hash_table_size(fd->modified_xmp) == 0)
370 metadata_write_queue_remove(fd);
374 /* reread the metadata to restore the original value */
375 file_data_increment_version(fd);
376 file_data_send_notification(fd, NOTIFY_REREAD);
381 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
383 if (!fd->modified_xmp)
385 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, reinterpret_cast<GDestroyNotify>(string_list_free));
387 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy(const_cast<GList *>(values)));
389 metadata_cache_remove(fd, key);
393 exif_update_metadata(fd->exif, key, values);
395 metadata_write_queue_add(fd);
396 file_data_increment_version(fd);
397 file_data_send_notification(fd, NOTIFY_METADATA);
399 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
401 GList *work = fd->sidecar_files;
405 auto sfd = static_cast<FileData *>(work->data);
408 if (sfd->format_class == FORMAT_CLASS_META) continue;
410 metadata_write_list(sfd, key, values);
418 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
420 GList *list = g_list_append(nullptr, g_strdup(value));
421 gboolean ret = metadata_write_list(fd, key, list);
422 g_list_free_full(list, g_free);
426 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
430 g_snprintf(string, sizeof(string), "%llu", static_cast<unsigned long long>(value));
431 return metadata_write_string(fd, key, string);
435 *-------------------------------------------------------------------
436 * keyword / comment read/write
437 *-------------------------------------------------------------------
440 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
444 ssi = secure_open(path);
445 if (!ssi) return FALSE;
447 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
449 secure_fprintf(ssi, "[keywords]\n");
450 while (keywords && secsave_errno == SS_ERR_NONE)
452 auto word = static_cast<const gchar *>(keywords->data);
453 keywords = keywords->next;
455 secure_fprintf(ssi, "%s\n", word);
457 secure_fputc(ssi, '\n');
459 secure_fprintf(ssi, "[comment]\n");
460 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
462 secure_fprintf(ssi, "#end\n");
464 return (secure_close(ssi) == 0);
467 static gboolean metadata_legacy_write(FileData *fd)
469 gboolean success = FALSE;
470 gchar *metadata_pathl;
473 gboolean have_keywords;
474 gboolean have_comment;
475 const gchar *comment;
476 GList *orig_keywords = nullptr;
477 gchar *orig_comment = nullptr;
479 g_assert(fd->change && fd->change->dest);
481 DEBUG_1("Saving comment: %s", fd->change->dest);
483 if (!fd->modified_xmp) return TRUE;
485 metadata_pathl = path_from_utf8(fd->change->dest);
487 have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, nullptr, &keywords);
488 have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, nullptr, &comment_l);
489 comment = static_cast<const gchar *>((have_comment && comment_l) ? (static_cast<GList *>(comment_l))->data : nullptr);
491 if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
493 success = metadata_file_write(metadata_pathl,
494 have_keywords ? static_cast<GList *>(keywords) : orig_keywords,
495 have_comment ? comment : orig_comment);
497 g_free(metadata_pathl);
498 g_free(orig_comment);
499 g_list_free_full(orig_keywords, g_free);
504 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
508 MetadataKey key = MK_NONE;
509 GList *list = nullptr;
510 GString *comment_build = nullptr;
512 f = fopen(path, "r");
513 if (!f) return FALSE;
515 while (fgets(s_buf, sizeof(s_buf), f))
519 if (*ptr == '#') continue;
520 if (*ptr == '[' && key != MK_COMMENT)
522 gchar *keystr = ++ptr;
525 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
530 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
532 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
544 while (*ptr != '\n' && *ptr != '\0') ptr++;
546 if (strlen(s_buf) > 0)
548 gchar *kw = utf8_validate_or_convert(s_buf);
550 list = g_list_prepend(list, kw);
555 if (!comment_build) comment_build = g_string_new("");
556 g_string_append(comment_build, s_buf);
565 *keywords = g_list_reverse(list);
569 g_list_free_full(list, g_free);
577 gchar *ptr = comment_build->str;
579 /* strip leading and trailing newlines */
580 while (*ptr == '\n') ptr++;
582 while (len > 0 && ptr[len - 1] == '\n') len--;
583 if (ptr[len] == '\n') len++; /* keep the last one */
586 gchar *text = g_strndup(ptr, len);
588 *comment = utf8_validate_or_convert(text);
592 g_string_free(comment_build, TRUE);
598 static void metadata_legacy_delete(FileData *fd, const gchar *except)
600 gchar *metadata_path;
601 gchar *metadata_pathl;
604 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
605 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
607 metadata_pathl = path_from_utf8(metadata_path);
608 unlink(metadata_pathl);
609 g_free(metadata_pathl);
610 g_free(metadata_path);
614 /* without exiv2: do not delete xmp metadata because we are not able to convert it,
616 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
617 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
619 metadata_pathl = path_from_utf8(metadata_path);
620 unlink(metadata_pathl);
621 g_free(metadata_pathl);
622 g_free(metadata_path);
627 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
629 gchar *metadata_path;
630 gchar *metadata_pathl;
631 gboolean success = FALSE;
633 if (!fd) return FALSE;
635 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
636 if (!metadata_path) return FALSE;
638 metadata_pathl = path_from_utf8(metadata_path);
640 success = metadata_file_read(metadata_pathl, keywords, comment);
642 g_free(metadata_pathl);
643 g_free(metadata_path);
648 static GList *remove_duplicate_strings_from_list(GList *list)
651 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
652 GList *newlist = nullptr;
656 auto key = static_cast<gchar *>(work->data);
658 if (g_hash_table_lookup(hashtable, key) == nullptr)
660 g_hash_table_insert(hashtable, key, GINT_TO_POINTER(1));
661 newlist = g_list_prepend(newlist, key);
666 g_hash_table_destroy(hashtable);
669 return g_list_reverse(newlist);
672 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
675 GList *list = nullptr;
676 const GList *cache_entry;
677 if (!fd) return nullptr;
679 /* unwritten data override everything */
680 if (fd->modified_xmp && format == METADATA_PLAIN)
682 list = static_cast<GList *>(g_hash_table_lookup(fd->modified_xmp, key));
683 if (list) return string_list_copy(list);
687 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
688 && (cache_entry = metadata_cache_get(fd, key)))
690 return string_list_copy(cache_entry->next);
694 Legacy metadata file is the primary source if it exists.
695 Merging the lists does not make much sense, because the existence of
696 legacy metadata file indicates that the other metadata sources are not
697 writable and thus it would not be possible to delete the keywords
698 that comes from the image file.
700 if (strcmp(key, KEYWORD_KEY) == 0)
702 if (metadata_legacy_read(fd, &list, nullptr))
704 if (format == METADATA_PLAIN)
706 metadata_cache_update(fd, key, list);
711 else if (strcmp(key, COMMENT_KEY) == 0)
713 gchar *comment = nullptr;
714 if (metadata_legacy_read(fd, nullptr, &comment)) return g_list_append(nullptr, comment);
716 else if (strncmp(key, "file.", 5) == 0)
718 return g_list_append(nullptr, metadata_file_info(fd, key, format));
721 else if (strncmp(key, "lua.", 4) == 0)
723 return g_list_append(nullptr, metadata_lua_info(fd, key, format));
727 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
728 if (!exif) return nullptr;
729 list = exif_get_metadata(exif, key, format);
730 exif_free_fd(fd, exif);
732 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
734 metadata_cache_update(fd, key, list);
740 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
742 GList *string_list = metadata_read_list(fd, key, format);
745 auto str = static_cast<gchar *>(string_list->data);
746 string_list->data = nullptr;
747 g_list_free_full(string_list, g_free);
753 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
757 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
758 if (!string) return fallback;
760 ret = g_ascii_strtoull(string, &endptr, 10);
761 if (string == endptr) ret = fallback;
766 gchar *metadata_read_rating_stars(FileData *fd)
769 gint n = metadata_read_int(fd, RATING_KEY, METADATA_PLAIN);
771 ret = convert_rating_to_stars(n);
776 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
784 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
785 if (!string) return fallback;
787 deg = g_ascii_strtod(string, &endptr);
790 min = g_ascii_strtod(endptr + 1, &endptr);
792 sec = g_ascii_strtod(endptr + 1, &endptr);
797 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
799 coord = deg + min /60.0 + sec / 3600.0;
801 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
808 log_printf("unable to parse GPS coordinate '%s'\n", string);
815 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
820 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
821 if (!string) return fallback;
823 DEBUG_3("GPS_direction: %s\n", string);
824 deg = g_ascii_strtod(string, &endptr);
826 /* Expected text string is of the format e.g.:
838 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
846 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
848 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
852 return metadata_write_string(fd, key, value);
855 gchar *new_string = g_strconcat(str, value, NULL);
856 gboolean ret = metadata_write_string(fd, key, new_string);
862 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
877 min = (param * 60) - (deg * 60);
878 if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
883 else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
890 log_printf("unknown GPS parameter key '%s'\n", key);
896 /* Avoid locale problems with commas and decimal points in numbers */
897 old_locale = setlocale(LC_ALL, nullptr);
898 saved_locale = strdup(old_locale);
899 if (saved_locale == nullptr)
903 setlocale(LC_ALL, "C");
905 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
906 metadata_write_string(fd, key, coordinate );
908 setlocale(LC_ALL, saved_locale);
916 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
918 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
922 return metadata_write_list(fd, key, values);
926 list = g_list_concat(list, string_list_copy(values));
927 list = remove_duplicate_strings_from_list(list);
929 ret = metadata_write_list(fd, key, list);
930 g_list_free_full(list, g_free);
935 * @see find_string_in_list
937 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
939 gchar *string_casefold = g_utf8_casefold(string, -1);
943 auto haystack = static_cast<gchar *>(list->data);
948 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
950 equal = (strcmp(haystack_casefold, string_casefold) == 0);
951 g_free(haystack_casefold);
955 g_free(string_casefold);
963 g_free(string_casefold);
968 * @see find_string_in_list
970 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
974 auto haystack = static_cast<gchar *>(list->data);
976 if (haystack && strcmp(haystack, string) == 0)
983 } // gchar *find_string_in_list_utf...
986 * @brief Find a existent string in a list.
988 * This is a switch between find_string_in_list_utf8case and
989 * find_string_in_list_utf8nocase to search with or without case for the
990 * existence of a string.
992 * @param list The list to search in
993 * @param string The string to search for
994 * @return The string or NULL
996 * @see find_string_in_list_utf8case
997 * @see find_string_in_list_utf8nocase
999 gchar *find_string_in_list(GList *list, const gchar *string)
1001 if (options->metadata.keywords_case_sensitive)
1002 return find_string_in_list_utf8case(list, string);
1004 return find_string_in_list_utf8nocase(list, string);
1007 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
1009 GList *string_to_keywords_list(const gchar *text)
1011 GList *list = nullptr;
1012 const gchar *ptr = text;
1014 while (*ptr != '\0')
1019 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
1021 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
1027 /* trim starting and ending whitespaces */
1028 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
1029 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
1033 gchar *keyword = g_strndup(begin, l);
1035 /* only add if not already in the list */
1036 if (!find_string_in_list(list, keyword))
1037 list = g_list_append(list, keyword);
1051 gboolean meta_data_get_keyword_mark(FileData *fd, gint, gpointer data)
1053 /** @FIXME do not use global keyword_tree */
1054 auto path = static_cast<GList *>(data);
1056 gboolean found = FALSE;
1057 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1061 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1062 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1069 gboolean meta_data_set_keyword_mark(FileData *fd, gint, gboolean value, gpointer data)
1071 auto path = static_cast<GList *>(data);
1072 GList *keywords = nullptr;
1075 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1077 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1079 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1083 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1087 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1089 metadata_write_list(fd, KEYWORD_KEY, keywords);
1092 g_list_free_full(keywords, g_free);
1098 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1101 FileDataGetMarkFunc get_mark_func;
1102 FileDataSetMarkFunc set_mark_func;
1103 gpointer mark_func_data;
1107 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1109 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1110 if (get_mark_func == meta_data_get_keyword_mark)
1112 GtkTreeIter old_kw_iter;
1113 auto old_path = static_cast<GList *>(mark_func_data);
1115 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1116 (i == mark || /* release any previous connection of given mark */
1117 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1119 file_data_register_mark_func(i, nullptr, nullptr, nullptr, nullptr);
1120 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1126 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1130 path = keyword_tree_get_path(keyword_tree, kw_iter);
1131 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, reinterpret_cast<GDestroyNotify>(string_list_free));
1133 mark_str = g_strdup_printf("%d", (mark < 9 ? mark : -1) + 1);
1134 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1141 *-------------------------------------------------------------------
1143 *-------------------------------------------------------------------
1148 GtkTreeStore *keyword_tree;
1150 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1153 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1157 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1161 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1165 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1168 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1172 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1174 gboolean is_keyword;
1175 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1179 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1181 gchar *casefold = g_utf8_casefold(name, -1);
1182 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1183 KEYWORD_COLUMN_NAME, name,
1184 KEYWORD_COLUMN_CASEFOLD, casefold,
1185 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1189 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1191 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1192 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1193 gint ret = gtk_tree_path_compare(pa, pb);
1194 gtk_tree_path_free(pa);
1195 gtk_tree_path_free(pb);
1199 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1201 GtkTreeIter parent_a;
1202 GtkTreeIter parent_b;
1204 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1205 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1207 if (valid_pa && valid_pb)
1209 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1212 return (!valid_pa && !valid_pb); /* both are toplevel */
1215 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1219 gboolean toplevel = FALSE;
1225 parent = *parent_ptr;
1229 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1236 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? nullptr : &parent)) return FALSE;
1238 casefold = g_utf8_casefold(name, -1);
1243 if (!exclude_sibling || !sibling || keyword_compare(keyword_tree, &iter, sibling) != 0)
1245 if (options->metadata.keywords_case_sensitive)
1247 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1248 ret = strcmp(name, iter_name) == 0;
1253 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1254 ret = strcmp(casefold, iter_casefold) == 0;
1255 g_free(iter_casefold);
1256 } // if (options->metadata.tags_cas...
1260 if (result) *result = iter;
1263 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1270 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1276 gboolean is_keyword;
1278 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1279 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1280 KEYWORD_COLUMN_NAME, &name,
1281 KEYWORD_COLUMN_CASEFOLD, &casefold,
1282 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1284 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1285 KEYWORD_COLUMN_NAME, name,
1286 KEYWORD_COLUMN_CASEFOLD, casefold,
1287 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1293 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1295 GtkTreeIter from_child;
1297 keyword_copy(keyword_tree, to, from);
1299 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1303 GtkTreeIter to_child;
1304 gtk_tree_store_append(keyword_tree, &to_child, to);
1305 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1306 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1310 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1312 keyword_copy_recursive(keyword_tree, to, from);
1313 keyword_delete(keyword_tree, from);
1316 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1318 GList *path = nullptr;
1319 GtkTreeIter iter = *iter_ptr;
1324 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1325 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1331 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1335 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1339 GtkTreeIter children;
1342 gchar *name = keyword_get_name(keyword_tree, &iter);
1343 if (strcmp(name, static_cast<const gchar *>(path->data)) == 0) break;
1345 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1354 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1360 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1362 if (!casefold_list) return FALSE;
1364 if (!keyword_get_is_keyword(keyword_tree, &iter))
1366 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1368 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1369 return FALSE; /* this should happen only on empty helpers */
1373 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1374 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1382 if (keyword_get_is_keyword(keyword_tree, &iter))
1384 GList *work = casefold_list;
1385 gboolean found = FALSE;
1386 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1389 auto casefold = static_cast<const gchar *>(work->data);
1392 if (strcmp(iter_casefold, casefold) == 0)
1398 g_free(iter_casefold);
1399 if (!found) return FALSE;
1402 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1407 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1409 if (!kw_list) return FALSE;
1411 if (!keyword_get_is_keyword(keyword_tree, &iter))
1413 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1415 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1416 return FALSE; /* this should happen only on empty helpers */
1420 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1421 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1429 if (keyword_get_is_keyword(keyword_tree, &iter))
1431 GList *work = kw_list;
1432 gboolean found = FALSE;
1433 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1436 auto name = static_cast<const gchar *>(work->data);
1439 if (strcmp(iter_name, name) == 0)
1446 if (!found) return FALSE;
1449 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1454 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1457 GList *casefold_list = nullptr;
1460 if (options->metadata.keywords_case_sensitive)
1462 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1469 auto kw = static_cast<const gchar *>(work->data);
1472 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1475 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1477 g_list_free_full(casefold_list, g_free);
1483 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1485 GtkTreeIter iter = *iter_ptr;
1490 if (keyword_get_is_keyword(keyword_tree, &iter))
1492 gchar *name = keyword_get_name(keyword_tree, &iter);
1493 if (!find_string_in_list(*kw_list, name))
1495 *kw_list = g_list_append(*kw_list, name);
1503 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1508 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1510 GtkTreeIter iter = *iter_ptr;
1511 GList *kw_list = nullptr;
1517 if (keyword_get_is_keyword(keyword_tree, &iter))
1519 gchar *name = keyword_get_name(keyword_tree, &iter);
1520 kw_list = g_list_append(kw_list, name);
1523 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1526 } // GList *keyword_tree_get(GtkTre...
1528 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1532 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1534 name = keyword_get_name(keyword_tree, iter);
1535 found = find_string_in_list(*kw_list, name);
1539 *kw_list = g_list_remove(*kw_list, found);
1545 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1548 keyword_tree_reset1(keyword_tree, iter, kw_list);
1550 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1554 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1555 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1559 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1563 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1564 return TRUE; /* this should happen only on empty helpers */
1568 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1569 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1573 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1575 GtkTreeIter iter = *iter_ptr;
1577 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1579 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1582 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1585 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1586 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1591 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1595 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1597 keyword_delete(keyword_tree, &child);
1600 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1602 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1605 gtk_tree_store_remove(keyword_tree, iter_ptr);
1609 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1612 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1613 if (!g_list_find(list, id))
1615 list = g_list_prepend(list, id);
1616 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1620 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1623 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1624 list = g_list_remove(list, id);
1625 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1628 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1631 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1632 return !!g_list_find(list, id);
1635 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1637 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1641 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1643 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1646 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1648 if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1650 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1655 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1657 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1660 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1662 GtkTreeIter iter = *iter_ptr;
1665 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1667 keyword_hide_in(keyword_tree, &iter, id);
1668 /* no need to check children of hidden node */
1673 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1675 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1678 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1682 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1685 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1686 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1689 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter_ptr, gpointer data)
1691 GtkTreeIter iter = *iter_ptr;
1692 auto keywords = static_cast<GList *>(data);
1693 gpointer id = keywords->data;
1694 keywords = keywords->next; /* hack */
1695 if (keyword_tree_is_set(model, &iter, keywords))
1700 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1701 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1708 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1710 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1711 keywords = g_list_prepend(keywords, id);
1712 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1713 keywords = g_list_delete_link(keywords, keywords);
1717 void keyword_tree_new()
1719 if (keyword_tree) return;
1721 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1724 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1727 gtk_tree_store_append(keyword_tree, &iter, parent);
1728 keyword_set(keyword_tree, &iter, name, is_keyword);
1732 void keyword_tree_new_default()
1737 if (!keyword_tree) keyword_tree_new();
1739 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("People"), TRUE);
1740 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1741 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1742 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1743 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1744 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1745 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1746 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Nature"), TRUE);
1747 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1748 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1749 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1750 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1751 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1752 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1753 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1754 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1755 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1756 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1757 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1758 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1759 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1760 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1761 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Art"), TRUE);
1762 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1763 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1764 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1765 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1766 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("City"), TRUE);
1767 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1768 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1769 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1770 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Architecture"), TRUE);
1771 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1772 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1773 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1774 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1775 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1776 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1777 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), 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, _("Places"), FALSE);
1781 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Conditions"), FALSE);
1782 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1783 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1784 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1785 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1786 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1787 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1788 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1789 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1790 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1791 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1792 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Photo"), FALSE);
1793 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1794 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1795 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1796 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1797 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1798 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1802 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1804 GtkTreeIter iter = *iter_ptr;
1807 GtkTreeIter children;
1811 WRITE_NL(); WRITE_STRING("<keyword ");
1812 name = keyword_get_name(keyword_tree, &iter);
1813 write_char_option(outstr, indent, "name", name);
1815 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1816 mark_str = keyword_get_mark(keyword_tree, &iter);
1817 if (mark_str && mark_str[0])
1819 write_char_option(outstr, indent, "mark", mark_str);
1822 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1826 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1828 WRITE_NL(); WRITE_STRING("</keyword>");
1834 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1838 void keyword_tree_write_config(GString *outstr, gint indent)
1841 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1844 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1846 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1849 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1852 void keyword_tree_node_disconnect_marks(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1854 GtkTreeIter iter = *iter_ptr;
1858 GtkTreeIter children;
1860 meta_data_connect_mark_with_keyword((keyword_tree), &iter, -1);
1862 if (gtk_tree_model_iter_children((keyword_tree), &children, &iter))
1864 keyword_tree_node_disconnect_marks((keyword_tree), &children);
1867 if (!gtk_tree_model_iter_next((keyword_tree), &iter)) return;
1871 void keyword_tree_disconnect_marks()
1875 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1877 keyword_tree_node_disconnect_marks(GTK_TREE_MODEL(keyword_tree), &iter);
1881 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1883 gchar *name = nullptr;
1884 gboolean is_kw = TRUE;
1885 gchar *mark_str = nullptr;
1887 while (*attribute_names)
1889 const gchar *option = *attribute_names++;
1890 const gchar *value = *attribute_values++;
1892 if (READ_CHAR_FULL("name", name)) continue;
1893 if (READ_BOOL_FULL("kw", is_kw)) continue;
1894 if (READ_CHAR_FULL("mark", mark_str)) continue;
1896 log_printf("unknown attribute %s = %s\n", option, value);
1898 if (name && name[0])
1901 /* re-use existing keyword if any */
1902 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, nullptr, name, FALSE, &iter))
1904 gtk_tree_store_append(keyword_tree, &iter, parent);
1906 keyword_set(keyword_tree, &iter, name, is_kw);
1910 gint i = static_cast<gint>(atoi(mark_str));
1913 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1918 return gtk_tree_iter_copy(&iter);
1924 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */