2 * Copyright (C) 2004 John Ellis
3 * Copyright (C) 2008 - 2016 The Geeqie Team
5 * Authors: John Ellis, Laurent Monin
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
33 #include <glib-object.h>
42 #include "layout-util.h"
43 #include "main-defines.h"
47 #include "secure-save.h"
48 #include "ui-fileops.h"
62 /* If contents change, keep GuideOptionsMetadata.xml up to date */
64 * @brief Tags that will be written to all files in a group - selected by: options->metadata.sync_grouped_files, Preferences/Metadata/Write The Same Description Tags To All Grouped Sidecars
66 constexpr std::array<const gchar *, 21> group_keys{
68 "Xmp.photoshop.Urgency",
69 "Xmp.photoshop.Category",
70 "Xmp.photoshop.SupplementalCategory",
73 "Xmp.photoshop.Instruction",
74 "Xmp.photoshop.DateCreated",
76 "Xmp.photoshop.AuthorsPosition",
78 "Xmp.photoshop.State",
79 "Xmp.iptc.CountryCode",
80 "Xmp.photoshop.Country",
81 "Xmp.photoshop.TransmissionReference",
82 "Xmp.photoshop.Headline",
83 "Xmp.photoshop.Credit",
84 "Xmp.photoshop.Source",
87 "Xmp.photoshop.CaptionWriter",
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 gboolean metadata_write_revert(FileData *fd, const gchar *key)
378 if (!fd->modified_xmp) return FALSE;
380 g_hash_table_remove(fd->modified_xmp, key);
382 if (g_hash_table_size(fd->modified_xmp) == 0)
384 metadata_write_queue_remove(fd);
388 /* reread the metadata to restore the original value */
389 file_data_increment_version(fd);
390 file_data_send_notification(fd, NOTIFY_REREAD);
395 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
397 if (!fd->modified_xmp)
399 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, reinterpret_cast<GDestroyNotify>(string_list_free));
401 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy(const_cast<GList *>(values)));
403 metadata_cache_remove(fd, key);
407 exif_update_metadata(fd->exif, key, values);
409 metadata_write_queue_add(fd);
410 file_data_increment_version(fd);
411 file_data_send_notification(fd, NOTIFY_METADATA);
413 auto metadata_check_key = [key](const gchar *k) { return strcmp(key, k) == 0; };
414 if (options->metadata.sync_grouped_files && std::any_of(group_keys.cbegin(), group_keys.cend(), metadata_check_key))
416 GList *work = fd->sidecar_files;
420 auto sfd = static_cast<FileData *>(work->data);
423 if (sfd->format_class == FORMAT_CLASS_META) continue;
425 metadata_write_list(sfd, key, values);
433 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
435 GList *list = g_list_append(nullptr, g_strdup(value));
436 gboolean ret = metadata_write_list(fd, key, list);
437 g_list_free_full(list, g_free);
441 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
445 g_snprintf(string, sizeof(string), "%llu", static_cast<unsigned long long>(value));
446 return metadata_write_string(fd, key, string);
450 *-------------------------------------------------------------------
451 * keyword / comment read/write
452 *-------------------------------------------------------------------
455 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
459 ssi = secure_open(path);
460 if (!ssi) return FALSE;
462 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
464 secure_fprintf(ssi, "[keywords]\n");
465 while (keywords && secsave_errno == SS_ERR_NONE)
467 auto word = static_cast<const gchar *>(keywords->data);
468 keywords = keywords->next;
470 secure_fprintf(ssi, "%s\n", word);
472 secure_fputc(ssi, '\n');
474 secure_fprintf(ssi, "[comment]\n");
475 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
477 secure_fprintf(ssi, "#end\n");
479 return (secure_close(ssi) == 0);
482 static gboolean metadata_legacy_write(FileData *fd)
484 gboolean success = FALSE;
485 gchar *metadata_pathl;
488 gboolean have_keywords;
489 gboolean have_comment;
490 const gchar *comment;
491 GList *orig_keywords = nullptr;
492 gchar *orig_comment = nullptr;
494 g_assert(fd->change && fd->change->dest);
496 DEBUG_1("Saving comment: %s", fd->change->dest);
498 if (!fd->modified_xmp) return TRUE;
500 metadata_pathl = path_from_utf8(fd->change->dest);
502 have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, nullptr, &keywords);
503 have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, nullptr, &comment_l);
504 comment = static_cast<const gchar *>((have_comment && comment_l) ? (static_cast<GList *>(comment_l))->data : nullptr);
506 if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
508 success = metadata_file_write(metadata_pathl,
509 have_keywords ? static_cast<GList *>(keywords) : orig_keywords,
510 have_comment ? comment : orig_comment);
512 g_free(metadata_pathl);
513 g_free(orig_comment);
514 g_list_free_full(orig_keywords, g_free);
519 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
523 MetadataKey key = MK_NONE;
524 GList *list = nullptr;
525 GString *comment_build = nullptr;
527 f = fopen(path, "r");
528 if (!f) return FALSE;
530 while (fgets(s_buf, sizeof(s_buf), f))
534 if (*ptr == '#') continue;
535 if (*ptr == '[' && key != MK_COMMENT)
537 gchar *keystr = ++ptr;
540 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
545 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
547 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
559 while (*ptr != '\n' && *ptr != '\0') ptr++;
561 if (strlen(s_buf) > 0)
563 gchar *kw = utf8_validate_or_convert(s_buf);
565 list = g_list_prepend(list, kw);
570 if (!comment_build) comment_build = g_string_new("");
571 g_string_append(comment_build, s_buf);
580 *keywords = g_list_reverse(list);
584 g_list_free_full(list, g_free);
592 gchar *ptr = comment_build->str;
594 /* strip leading and trailing newlines */
595 while (*ptr == '\n') ptr++;
597 while (len > 0 && ptr[len - 1] == '\n') len--;
598 if (ptr[len] == '\n') len++; /* keep the last one */
601 gchar *text = g_strndup(ptr, len);
603 *comment = utf8_validate_or_convert(text);
607 g_string_free(comment_build, TRUE);
613 static void metadata_legacy_delete(FileData *fd, const gchar *except)
615 gchar *metadata_path;
616 gchar *metadata_pathl;
619 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
620 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
622 metadata_pathl = path_from_utf8(metadata_path);
623 unlink(metadata_pathl);
624 g_free(metadata_pathl);
625 g_free(metadata_path);
629 /* without exiv2: do not delete xmp metadata because we are not able to convert it,
631 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
632 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
634 metadata_pathl = path_from_utf8(metadata_path);
635 unlink(metadata_pathl);
636 g_free(metadata_pathl);
637 g_free(metadata_path);
642 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
644 gchar *metadata_path;
645 gchar *metadata_pathl;
646 gboolean success = FALSE;
648 if (!fd) return FALSE;
650 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
651 if (!metadata_path) return FALSE;
653 metadata_pathl = path_from_utf8(metadata_path);
655 success = metadata_file_read(metadata_pathl, keywords, comment);
657 g_free(metadata_pathl);
658 g_free(metadata_path);
663 static GList *remove_duplicate_strings_from_list(GList *list)
666 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
667 GList *newlist = nullptr;
671 auto key = static_cast<gchar *>(work->data);
673 if (g_hash_table_lookup(hashtable, key) == nullptr)
675 g_hash_table_insert(hashtable, key, GINT_TO_POINTER(1));
676 newlist = g_list_prepend(newlist, key);
681 g_hash_table_destroy(hashtable);
684 return g_list_reverse(newlist);
687 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
690 GList *list = nullptr;
691 const GList *cache_entry;
692 if (!fd) return nullptr;
694 /* unwritten data override everything */
695 if (fd->modified_xmp && format == METADATA_PLAIN)
697 list = static_cast<GList *>(g_hash_table_lookup(fd->modified_xmp, key));
698 if (list) return string_list_copy(list);
702 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
703 && (cache_entry = metadata_cache_get(fd, key)))
705 return string_list_copy(cache_entry->next);
709 Legacy metadata file is the primary source if it exists.
710 Merging the lists does not make much sense, because the existence of
711 legacy metadata file indicates that the other metadata sources are not
712 writable and thus it would not be possible to delete the keywords
713 that comes from the image file.
715 if (strcmp(key, KEYWORD_KEY) == 0)
717 if (metadata_legacy_read(fd, &list, nullptr))
719 if (format == METADATA_PLAIN)
721 metadata_cache_update(fd, key, list);
726 else if (strcmp(key, COMMENT_KEY) == 0)
728 gchar *comment = nullptr;
729 if (metadata_legacy_read(fd, nullptr, &comment)) return g_list_append(nullptr, comment);
731 else if (strncmp(key, "file.", 5) == 0)
733 return g_list_append(nullptr, metadata_file_info(fd, key, format));
736 else if (strncmp(key, "lua.", 4) == 0)
738 return g_list_append(nullptr, metadata_lua_info(fd, key, format));
742 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
743 if (!exif) return nullptr;
744 list = exif_get_metadata(exif, key, format);
745 exif_free_fd(fd, exif);
747 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
749 metadata_cache_update(fd, key, list);
755 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
757 GList *string_list = metadata_read_list(fd, key, format);
760 auto str = static_cast<gchar *>(string_list->data);
761 string_list->data = nullptr;
762 g_list_free_full(string_list, g_free);
768 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
772 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
773 if (!string) return fallback;
775 ret = g_ascii_strtoull(string, &endptr, 10);
776 if (string == endptr) ret = fallback;
781 gchar *metadata_read_rating_stars(FileData *fd)
784 gint n = metadata_read_int(fd, RATING_KEY, METADATA_PLAIN);
786 ret = convert_rating_to_stars(n);
791 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
799 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
800 if (!string) return fallback;
802 deg = g_ascii_strtod(string, &endptr);
805 min = g_ascii_strtod(endptr + 1, &endptr);
807 sec = g_ascii_strtod(endptr + 1, &endptr);
812 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
814 coord = deg + min /60.0 + sec / 3600.0;
816 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
823 log_printf("unable to parse GPS coordinate '%s'\n", string);
830 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
835 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
836 if (!string) return fallback;
838 DEBUG_3("GPS_direction: %s\n", string);
839 deg = g_ascii_strtod(string, &endptr);
841 /* Expected text string is of the format e.g.:
853 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
861 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
863 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
867 return metadata_write_string(fd, key, value);
870 gchar *new_string = g_strconcat(str, value, NULL);
871 gboolean ret = metadata_write_string(fd, key, new_string);
877 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
892 min = (param * 60) - (deg * 60);
893 if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
898 else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
905 log_printf("unknown GPS parameter key '%s'\n", key);
911 /* Avoid locale problems with commas and decimal points in numbers */
912 old_locale = setlocale(LC_ALL, nullptr);
913 saved_locale = strdup(old_locale);
914 if (saved_locale == nullptr)
918 setlocale(LC_ALL, "C");
920 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
921 metadata_write_string(fd, key, coordinate );
923 setlocale(LC_ALL, saved_locale);
931 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
933 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
937 return metadata_write_list(fd, key, values);
941 list = g_list_concat(list, string_list_copy(values));
942 list = remove_duplicate_strings_from_list(list);
944 ret = metadata_write_list(fd, key, list);
945 g_list_free_full(list, g_free);
950 * @see find_string_in_list
952 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
954 gchar *string_casefold = g_utf8_casefold(string, -1);
958 auto haystack = static_cast<gchar *>(list->data);
963 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
965 equal = (strcmp(haystack_casefold, string_casefold) == 0);
966 g_free(haystack_casefold);
970 g_free(string_casefold);
978 g_free(string_casefold);
983 * @see find_string_in_list
985 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
989 auto haystack = static_cast<gchar *>(list->data);
991 if (haystack && strcmp(haystack, string) == 0)
998 } // gchar *find_string_in_list_utf...
1001 * @brief Find a existent string in a list.
1003 * This is a switch between find_string_in_list_utf8case and
1004 * find_string_in_list_utf8nocase to search with or without case for the
1005 * existence of a string.
1007 * @param list The list to search in
1008 * @param string The string to search for
1009 * @return The string or NULL
1011 * @see find_string_in_list_utf8case
1012 * @see find_string_in_list_utf8nocase
1014 gchar *find_string_in_list(GList *list, const gchar *string)
1016 if (options->metadata.keywords_case_sensitive)
1017 return find_string_in_list_utf8case(list, string);
1019 return find_string_in_list_utf8nocase(list, string);
1022 GList *string_to_keywords_list(const gchar *text)
1024 GList *list = nullptr;
1025 const gchar *ptr = text;
1027 while (*ptr != '\0')
1032 while (is_keywords_separator(*ptr)) ptr++;
1034 while (*ptr != '\0' && !is_keywords_separator(*ptr))
1040 /* trim starting and ending whitespaces */
1041 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
1042 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
1046 gchar *keyword = g_strndup(begin, l);
1048 /* only add if not already in the list */
1049 if (!find_string_in_list(list, keyword))
1050 list = g_list_append(list, keyword);
1064 gboolean meta_data_get_keyword_mark(FileData *fd, gint, gpointer data)
1066 /** @FIXME do not use global keyword_tree */
1067 auto path = static_cast<GList *>(data);
1069 gboolean found = FALSE;
1070 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1074 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1075 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1082 gboolean meta_data_set_keyword_mark(FileData *fd, gint, gboolean value, gpointer data)
1084 auto path = static_cast<GList *>(data);
1085 GList *keywords = nullptr;
1088 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1090 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1092 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1096 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1100 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1102 metadata_write_list(fd, KEYWORD_KEY, keywords);
1105 g_list_free_full(keywords, g_free);
1111 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1114 FileDataGetMarkFunc get_mark_func;
1115 FileDataSetMarkFunc set_mark_func;
1116 gpointer mark_func_data;
1120 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1122 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1123 if (get_mark_func == meta_data_get_keyword_mark)
1125 GtkTreeIter old_kw_iter;
1126 auto old_path = static_cast<GList *>(mark_func_data);
1128 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1129 (i == mark || /* release any previous connection of given mark */
1130 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1132 file_data_register_mark_func(i, nullptr, nullptr, nullptr, nullptr);
1133 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1139 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1143 path = keyword_tree_get_path(keyword_tree, kw_iter);
1144 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, reinterpret_cast<GDestroyNotify>(string_list_free));
1146 mark_str = g_strdup_printf("%d", (mark < 9 ? mark : -1) + 1);
1147 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1154 *-------------------------------------------------------------------
1156 *-------------------------------------------------------------------
1161 GtkTreeStore *keyword_tree;
1163 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1166 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1170 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1174 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1178 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1181 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1185 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1187 gboolean is_keyword;
1188 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1192 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1194 gchar *casefold = g_utf8_casefold(name, -1);
1195 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1196 KEYWORD_COLUMN_NAME, name,
1197 KEYWORD_COLUMN_CASEFOLD, casefold,
1198 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1202 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1204 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1205 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1206 gint ret = gtk_tree_path_compare(pa, pb);
1207 gtk_tree_path_free(pa);
1208 gtk_tree_path_free(pb);
1212 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1214 GtkTreeIter parent_a;
1215 GtkTreeIter parent_b;
1217 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1218 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1220 if (valid_pa && valid_pb)
1222 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1225 return (!valid_pa && !valid_pb); /* both are toplevel */
1228 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1232 gboolean toplevel = FALSE;
1238 parent = *parent_ptr;
1242 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1249 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? nullptr : &parent)) return FALSE;
1251 casefold = g_utf8_casefold(name, -1);
1256 if (!exclude_sibling || !sibling || keyword_compare(keyword_tree, &iter, sibling) != 0)
1258 if (options->metadata.keywords_case_sensitive)
1260 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1261 ret = strcmp(name, iter_name) == 0;
1266 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1267 ret = strcmp(casefold, iter_casefold) == 0;
1268 g_free(iter_casefold);
1269 } // if (options->metadata.tags_cas...
1273 if (result) *result = iter;
1276 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1283 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1289 gboolean is_keyword;
1291 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1292 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1293 KEYWORD_COLUMN_NAME, &name,
1294 KEYWORD_COLUMN_CASEFOLD, &casefold,
1295 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1297 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1298 KEYWORD_COLUMN_NAME, name,
1299 KEYWORD_COLUMN_CASEFOLD, casefold,
1300 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1306 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1308 GtkTreeIter from_child;
1310 keyword_copy(keyword_tree, to, from);
1312 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1316 GtkTreeIter to_child;
1317 gtk_tree_store_append(keyword_tree, &to_child, to);
1318 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1319 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1323 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1325 keyword_copy_recursive(keyword_tree, to, from);
1326 keyword_delete(keyword_tree, from);
1329 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1331 GList *path = nullptr;
1332 GtkTreeIter iter = *iter_ptr;
1337 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1338 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1344 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1348 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1352 GtkTreeIter children;
1355 gchar *name = keyword_get_name(keyword_tree, &iter);
1356 if (strcmp(name, static_cast<const gchar *>(path->data)) == 0) break;
1358 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1367 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1373 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1375 if (!casefold_list) return FALSE;
1377 if (!keyword_get_is_keyword(keyword_tree, &iter))
1379 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1381 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1382 return FALSE; /* this should happen only on empty helpers */
1386 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1387 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1395 if (keyword_get_is_keyword(keyword_tree, &iter))
1397 GList *work = casefold_list;
1398 gboolean found = FALSE;
1399 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1402 auto casefold = static_cast<const gchar *>(work->data);
1405 if (strcmp(iter_casefold, casefold) == 0)
1411 g_free(iter_casefold);
1412 if (!found) return FALSE;
1415 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1420 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1422 if (!kw_list) return FALSE;
1424 if (!keyword_get_is_keyword(keyword_tree, &iter))
1426 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1428 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1429 return FALSE; /* this should happen only on empty helpers */
1433 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1434 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1442 if (keyword_get_is_keyword(keyword_tree, &iter))
1444 GList *work = kw_list;
1445 gboolean found = FALSE;
1446 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1449 auto name = static_cast<const gchar *>(work->data);
1452 if (strcmp(iter_name, name) == 0)
1459 if (!found) return FALSE;
1462 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1467 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1470 GList *casefold_list = nullptr;
1473 if (options->metadata.keywords_case_sensitive)
1475 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1482 auto kw = static_cast<const gchar *>(work->data);
1485 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1488 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1490 g_list_free_full(casefold_list, g_free);
1496 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1498 GtkTreeIter iter = *iter_ptr;
1503 if (keyword_get_is_keyword(keyword_tree, &iter))
1505 gchar *name = keyword_get_name(keyword_tree, &iter);
1506 if (!find_string_in_list(*kw_list, name))
1508 *kw_list = g_list_append(*kw_list, name);
1516 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1521 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1523 GtkTreeIter iter = *iter_ptr;
1524 GList *kw_list = nullptr;
1530 if (keyword_get_is_keyword(keyword_tree, &iter))
1532 gchar *name = keyword_get_name(keyword_tree, &iter);
1533 kw_list = g_list_append(kw_list, name);
1536 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1539 } // GList *keyword_tree_get(GtkTre...
1541 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1545 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1547 name = keyword_get_name(keyword_tree, iter);
1548 found = find_string_in_list(*kw_list, name);
1552 *kw_list = g_list_remove(*kw_list, found);
1558 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1561 keyword_tree_reset1(keyword_tree, iter, kw_list);
1563 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1567 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1568 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1572 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1576 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1577 return TRUE; /* this should happen only on empty helpers */
1581 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1582 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1586 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1588 GtkTreeIter iter = *iter_ptr;
1590 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1592 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1595 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1598 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1599 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1604 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1608 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1610 keyword_delete(keyword_tree, &child);
1613 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1615 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1618 gtk_tree_store_remove(keyword_tree, iter_ptr);
1622 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1625 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1626 if (!g_list_find(list, id))
1628 list = g_list_prepend(list, id);
1629 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1633 void keyword_show_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 list = g_list_remove(list, id);
1638 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1641 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1644 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1645 return !!g_list_find(list, id);
1648 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1650 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1654 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1656 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1659 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1661 if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1663 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1668 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1670 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1673 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1675 GtkTreeIter iter = *iter_ptr;
1678 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1680 keyword_hide_in(keyword_tree, &iter, id);
1681 /* no need to check children of hidden node */
1686 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1688 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1691 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1695 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1698 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1699 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1702 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter_ptr, gpointer data)
1704 GtkTreeIter iter = *iter_ptr;
1705 auto keywords = static_cast<GList *>(data);
1706 gpointer id = keywords->data;
1707 keywords = keywords->next; /* hack */
1708 if (keyword_tree_is_set(model, &iter, keywords))
1713 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1714 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1721 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1723 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1724 keywords = g_list_prepend(keywords, id);
1725 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1726 keywords = g_list_delete_link(keywords, keywords);
1730 void keyword_tree_new()
1732 if (keyword_tree) return;
1734 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1737 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1740 gtk_tree_store_append(keyword_tree, &iter, parent);
1741 keyword_set(keyword_tree, &iter, name, is_keyword);
1745 void keyword_tree_new_default()
1750 if (!keyword_tree) keyword_tree_new();
1752 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("People"), TRUE);
1753 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1754 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1755 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1756 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1757 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1758 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1759 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Nature"), TRUE);
1760 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1761 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1762 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1763 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1764 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1765 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1766 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1767 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1768 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1769 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1770 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1771 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1772 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1773 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1774 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Art"), TRUE);
1775 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1776 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1777 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1778 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1779 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("City"), TRUE);
1780 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1781 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1782 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1783 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Architecture"), TRUE);
1784 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1785 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1786 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1787 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1788 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1789 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1790 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1791 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1792 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1793 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Places"), FALSE);
1794 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Conditions"), FALSE);
1795 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1796 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1797 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1798 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1799 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1800 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1801 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1802 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1803 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1804 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1805 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Photo"), FALSE);
1806 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1807 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1808 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1809 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1810 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1811 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1815 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1817 GtkTreeIter iter = *iter_ptr;
1820 GtkTreeIter children;
1824 WRITE_NL(); WRITE_STRING("<keyword ");
1825 name = keyword_get_name(keyword_tree, &iter);
1826 write_char_option(outstr, indent, "name", name);
1828 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1829 mark_str = keyword_get_mark(keyword_tree, &iter);
1830 if (mark_str && mark_str[0])
1832 write_char_option(outstr, indent, "mark", mark_str);
1835 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1839 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1841 WRITE_NL(); WRITE_STRING("</keyword>");
1847 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1851 void keyword_tree_write_config(GString *outstr, gint indent)
1854 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1857 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1859 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1862 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1865 void keyword_tree_node_disconnect_marks(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1867 GtkTreeIter iter = *iter_ptr;
1871 GtkTreeIter children;
1873 meta_data_connect_mark_with_keyword((keyword_tree), &iter, -1);
1875 if (gtk_tree_model_iter_children((keyword_tree), &children, &iter))
1877 keyword_tree_node_disconnect_marks((keyword_tree), &children);
1880 if (!gtk_tree_model_iter_next((keyword_tree), &iter)) return;
1884 void keyword_tree_disconnect_marks()
1888 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1890 keyword_tree_node_disconnect_marks(GTK_TREE_MODEL(keyword_tree), &iter);
1894 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1896 gchar *name = nullptr;
1897 gboolean is_kw = TRUE;
1898 gchar *mark_str = nullptr;
1900 while (*attribute_names)
1902 const gchar *option = *attribute_names++;
1903 const gchar *value = *attribute_values++;
1905 if (READ_CHAR_FULL("name", name)) continue;
1906 if (READ_BOOL_FULL("kw", is_kw)) continue;
1907 if (READ_CHAR_FULL("mark", mark_str)) continue;
1909 log_printf("unknown attribute %s = %s\n", option, value);
1911 if (name && name[0])
1914 /* re-use existing keyword if any */
1915 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, nullptr, name, FALSE, &iter))
1917 gtk_tree_store_append(keyword_tree, &iter, parent);
1919 keyword_set(keyword_tree, &iter, name, is_kw);
1923 gint i = static_cast<gint>(atoi(mark_str));
1926 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1931 return gtk_tree_iter_copy(&iter);
1937 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */