2 * Copyright (C) 2004 John Ellis
3 * Copyright (C) 2008 - 2016 The Geeqie Team
5 * Authors: John Ellis, Laurent Monin
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
31 #include "secure-save.h"
32 #include "ui-fileops.h"
34 #include "layout-util.h"
43 /* If contents change, keep GuideOptionsMetadata.xml up to date */
45 * @brief Tags that will be written to all files in a group - selected by: options->metadata.sync_grouped_files, Preferences/Metadata/Write The Same Description Tags To All Grouped Sidecars
47 static const gchar *group_keys[] = {
49 "Xmp.photoshop.Urgency",
50 "Xmp.photoshop.Category",
51 "Xmp.photoshop.SupplementalCategory",
54 "Xmp.photoshop.Instruction",
55 "Xmp.photoshop.DateCreated",
57 "Xmp.photoshop.AuthorsPosition",
59 "Xmp.photoshop.State",
60 "Xmp.iptc.CountryCode",
61 "Xmp.photoshop.Country",
62 "Xmp.photoshop.TransmissionReference",
63 "Xmp.photoshop.Headline",
64 "Xmp.photoshop.Credit",
65 "Xmp.photoshop.Source",
68 "Xmp.photoshop.CaptionWriter",
71 static gboolean metadata_write_queue_idle_cb(gpointer data);
72 static gboolean metadata_legacy_write(FileData *fd);
73 static void metadata_legacy_delete(FileData *fd, const gchar *except);
74 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment);
78 *-------------------------------------------------------------------
79 * long-term cache - keep keywords from whole dir in memory
80 *-------------------------------------------------------------------
83 /* fd->cached metadata list of lists
84 each particular list contains key as a first entry, then the values
87 static void metadata_cache_update(FileData *fd, const gchar *key, const GList *values)
91 work = fd->cached_metadata;
94 auto entry = static_cast<GList *>(work->data);
95 auto entry_key = static_cast<gchar *>(entry->data);
97 if (strcmp(entry_key, key) == 0)
99 /* key found - just replace values */
100 GList *old_values = entry->next;
101 entry->next = nullptr;
102 old_values->prev = nullptr;
103 g_list_free_full(old_values, g_free);
104 work->data = g_list_append(entry, string_list_copy(values));
105 DEBUG_1("updated %s %s\n", key, fd->path);
111 /* key not found - prepend new entry */
112 fd->cached_metadata = g_list_prepend(fd->cached_metadata,
113 g_list_prepend(string_list_copy(values), g_strdup(key)));
114 DEBUG_1("added %s %s\n", key, fd->path);
118 static const GList *metadata_cache_get(FileData *fd, const gchar *key)
122 work = fd->cached_metadata;
125 auto entry = static_cast<GList *>(work->data);
126 auto entry_key = static_cast<gchar *>(entry->data);
128 if (strcmp(entry_key, key) == 0)
131 DEBUG_1("found %s %s\n", key, fd->path);
137 DEBUG_1("not found %s %s\n", key, fd->path);
140 static void metadata_cache_remove(FileData *fd, const gchar *key)
144 work = fd->cached_metadata;
147 auto entry = static_cast<GList *>(work->data);
148 auto entry_key = static_cast<gchar *>(entry->data);
150 if (strcmp(entry_key, key) == 0)
153 g_list_free_full(entry, g_free);
154 fd->cached_metadata = g_list_delete_link(fd->cached_metadata, work);
155 DEBUG_1("removed %s %s\n", key, fd->path);
160 DEBUG_1("not removed %s %s\n", key, fd->path);
163 void metadata_cache_free(FileData *fd)
165 if (fd->cached_metadata) DEBUG_1("freed %s\n", fd->path);
167 g_list_free_full(fd->cached_metadata, [](gpointer data)
169 auto entry = static_cast<GList *>(data);
170 g_list_free_full(entry, g_free);
172 fd->cached_metadata = nullptr;
181 *-------------------------------------------------------------------
183 *-------------------------------------------------------------------
186 static GList *metadata_write_queue = nullptr;
187 static guint metadata_write_idle_id = 0; /* event source id */
189 static void metadata_write_queue_add(FileData *fd)
191 if (!g_list_find(metadata_write_queue, fd))
193 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
196 layout_util_status_update_write_all();
199 if (metadata_write_idle_id)
201 g_source_remove(metadata_write_idle_id);
202 metadata_write_idle_id = 0;
205 if (options->metadata.confirm_after_timeout)
207 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, nullptr);
212 gboolean metadata_write_queue_remove(FileData *fd)
214 g_hash_table_destroy(fd->modified_xmp);
215 fd->modified_xmp = nullptr;
217 metadata_write_queue = g_list_remove(metadata_write_queue, fd);
219 file_data_increment_version(fd);
220 file_data_send_notification(fd, NOTIFY_REREAD);
224 layout_util_status_update_write_all();
228 #pragma GCC diagnostic push
229 #pragma GCC diagnostic ignored "-Wunused-function"
230 gboolean metadata_write_queue_remove_list_unused(GList *list)
238 FileData *fd = static_cast<FileData *>(work->data);
240 ret = ret && metadata_write_queue_remove(fd);
244 #pragma GCC diagnostic pop
246 void metadata_notify_cb(FileData *fd, NotifyType type, gpointer)
248 if (type & (NOTIFY_REREAD | NOTIFY_CHANGE))
250 metadata_cache_free(fd);
252 if (g_list_find(metadata_write_queue, fd))
254 DEBUG_1("Notify metadata: %s %04x", fd->path, type);
255 if (!isname(fd->path))
257 /* ignore deleted files */
258 metadata_write_queue_remove(fd);
264 gboolean metadata_write_queue_confirm(gboolean force_dialog, FileUtilDoneFunc done_func, gpointer done_data)
267 GList *to_approve = nullptr;
269 work = metadata_write_queue;
272 auto fd = static_cast<FileData *>(work->data);
275 if (!isname(fd->path))
277 /* ignore deleted files */
278 metadata_write_queue_remove(fd);
282 if (fd->change) continue; /* another operation in progress, skip this file for now */
284 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
287 file_util_write_metadata(nullptr, to_approve, nullptr, force_dialog, done_func, done_data);
289 return (metadata_write_queue != nullptr);
292 static gboolean metadata_write_queue_idle_cb(gpointer)
294 metadata_write_queue_confirm(FALSE, nullptr, nullptr);
295 metadata_write_idle_id = 0;
299 gboolean metadata_write_perform(FileData *fd)
305 g_assert(fd->change);
307 lf = strlen(GQ_CACHE_EXT_METADATA);
308 if (fd->change->dest &&
309 g_ascii_strncasecmp(fd->change->dest + strlen(fd->change->dest) - lf, GQ_CACHE_EXT_METADATA, lf) == 0)
311 success = metadata_legacy_write(fd);
312 if (success) metadata_legacy_delete(fd, fd->change->dest);
316 /* write via exiv2 */
317 /* we can either use cached metadata which have fd->modified_xmp already applied
318 or read metadata from file and apply fd->modified_xmp
319 metadata are read also if the file was modified meanwhile */
320 exif = exif_read_fd(fd);
321 if (!exif) return FALSE;
323 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
324 exif_free_fd(fd, exif);
326 if (fd->change->dest)
327 /* this will create a FileData for the sidecar and link it to the main file
328 (we can't wait until the sidecar is discovered by directory scanning because
329 exif_read_fd is called before that and it would read the main file only and
330 store the metadata in the cache)
333 @FIXME this does not catch new sidecars created by independent external programs
335 file_data_unref(file_data_new_group(fd->change->dest));
337 if (success) metadata_legacy_delete(fd, fd->change->dest);
341 gint metadata_queue_length()
343 return g_list_length(metadata_write_queue);
346 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
348 const gchar **k = keys;
352 if (strcmp(key, *k) == 0) return TRUE;
358 gboolean metadata_write_revert(FileData *fd, const gchar *key)
360 if (!fd->modified_xmp) return FALSE;
362 g_hash_table_remove(fd->modified_xmp, key);
364 if (g_hash_table_size(fd->modified_xmp) == 0)
366 metadata_write_queue_remove(fd);
370 /* reread the metadata to restore the original value */
371 file_data_increment_version(fd);
372 file_data_send_notification(fd, NOTIFY_REREAD);
377 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
379 if (!fd->modified_xmp)
381 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, reinterpret_cast<GDestroyNotify>(string_list_free));
383 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy(const_cast<GList *>(values)));
385 metadata_cache_remove(fd, key);
389 exif_update_metadata(fd->exif, key, values);
391 metadata_write_queue_add(fd);
392 file_data_increment_version(fd);
393 file_data_send_notification(fd, NOTIFY_METADATA);
395 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
397 GList *work = fd->sidecar_files;
401 auto sfd = static_cast<FileData *>(work->data);
404 if (sfd->format_class == FORMAT_CLASS_META) continue;
406 metadata_write_list(sfd, key, values);
414 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
416 GList *list = g_list_append(nullptr, g_strdup(value));
417 gboolean ret = metadata_write_list(fd, key, list);
418 g_list_free_full(list, g_free);
422 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
426 g_snprintf(string, sizeof(string), "%llu", static_cast<unsigned long long>(value));
427 return metadata_write_string(fd, key, string);
431 *-------------------------------------------------------------------
432 * keyword / comment read/write
433 *-------------------------------------------------------------------
436 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
440 ssi = secure_open(path);
441 if (!ssi) return FALSE;
443 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
445 secure_fprintf(ssi, "[keywords]\n");
446 while (keywords && secsave_errno == SS_ERR_NONE)
448 auto word = static_cast<const gchar *>(keywords->data);
449 keywords = keywords->next;
451 secure_fprintf(ssi, "%s\n", word);
453 secure_fputc(ssi, '\n');
455 secure_fprintf(ssi, "[comment]\n");
456 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
458 secure_fprintf(ssi, "#end\n");
460 return (secure_close(ssi) == 0);
463 static gboolean metadata_legacy_write(FileData *fd)
465 gboolean success = FALSE;
466 gchar *metadata_pathl;
469 gboolean have_keywords;
470 gboolean have_comment;
471 const gchar *comment;
472 GList *orig_keywords = nullptr;
473 gchar *orig_comment = nullptr;
475 g_assert(fd->change && fd->change->dest);
477 DEBUG_1("Saving comment: %s", fd->change->dest);
479 if (!fd->modified_xmp) return TRUE;
481 metadata_pathl = path_from_utf8(fd->change->dest);
483 have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, nullptr, &keywords);
484 have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, nullptr, &comment_l);
485 comment = static_cast<const gchar *>((have_comment && comment_l) ? (static_cast<GList *>(comment_l))->data : nullptr);
487 if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
489 success = metadata_file_write(metadata_pathl,
490 have_keywords ? static_cast<GList *>(keywords) : orig_keywords,
491 have_comment ? comment : orig_comment);
493 g_free(metadata_pathl);
494 g_free(orig_comment);
495 g_list_free_full(orig_keywords, g_free);
500 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
504 MetadataKey key = MK_NONE;
505 GList *list = nullptr;
506 GString *comment_build = nullptr;
508 f = fopen(path, "r");
509 if (!f) return FALSE;
511 while (fgets(s_buf, sizeof(s_buf), f))
515 if (*ptr == '#') continue;
516 if (*ptr == '[' && key != MK_COMMENT)
518 gchar *keystr = ++ptr;
521 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
526 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
528 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
540 while (*ptr != '\n' && *ptr != '\0') ptr++;
542 if (strlen(s_buf) > 0)
544 gchar *kw = utf8_validate_or_convert(s_buf);
546 list = g_list_prepend(list, kw);
551 if (!comment_build) comment_build = g_string_new("");
552 g_string_append(comment_build, s_buf);
561 *keywords = g_list_reverse(list);
565 g_list_free_full(list, g_free);
573 gchar *ptr = comment_build->str;
575 /* strip leading and trailing newlines */
576 while (*ptr == '\n') ptr++;
578 while (len > 0 && ptr[len - 1] == '\n') len--;
579 if (ptr[len] == '\n') len++; /* keep the last one */
582 gchar *text = g_strndup(ptr, len);
584 *comment = utf8_validate_or_convert(text);
588 g_string_free(comment_build, TRUE);
594 static void metadata_legacy_delete(FileData *fd, const gchar *except)
596 gchar *metadata_path;
597 gchar *metadata_pathl;
600 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
601 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
603 metadata_pathl = path_from_utf8(metadata_path);
604 unlink(metadata_pathl);
605 g_free(metadata_pathl);
606 g_free(metadata_path);
610 /* without exiv2: do not delete xmp metadata because we are not able to convert it,
612 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
613 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
615 metadata_pathl = path_from_utf8(metadata_path);
616 unlink(metadata_pathl);
617 g_free(metadata_pathl);
618 g_free(metadata_path);
623 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
625 gchar *metadata_path;
626 gchar *metadata_pathl;
627 gboolean success = FALSE;
629 if (!fd) return FALSE;
631 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
632 if (!metadata_path) return FALSE;
634 metadata_pathl = path_from_utf8(metadata_path);
636 success = metadata_file_read(metadata_pathl, keywords, comment);
638 g_free(metadata_pathl);
639 g_free(metadata_path);
644 static GList *remove_duplicate_strings_from_list(GList *list)
647 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
648 GList *newlist = nullptr;
652 auto key = static_cast<gchar *>(work->data);
654 if (g_hash_table_lookup(hashtable, key) == nullptr)
656 g_hash_table_insert(hashtable, key, GINT_TO_POINTER(1));
657 newlist = g_list_prepend(newlist, key);
662 g_hash_table_destroy(hashtable);
665 return g_list_reverse(newlist);
668 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
671 GList *list = nullptr;
672 const GList *cache_entry;
673 if (!fd) return nullptr;
675 /* unwritten data override everything */
676 if (fd->modified_xmp && format == METADATA_PLAIN)
678 list = static_cast<GList *>(g_hash_table_lookup(fd->modified_xmp, key));
679 if (list) return string_list_copy(list);
683 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
684 && (cache_entry = metadata_cache_get(fd, key)))
686 return string_list_copy(cache_entry->next);
690 Legacy metadata file is the primary source if it exists.
691 Merging the lists does not make much sense, because the existence of
692 legacy metadata file indicates that the other metadata sources are not
693 writable and thus it would not be possible to delete the keywords
694 that comes from the image file.
696 if (strcmp(key, KEYWORD_KEY) == 0)
698 if (metadata_legacy_read(fd, &list, nullptr))
700 if (format == METADATA_PLAIN)
702 metadata_cache_update(fd, key, list);
707 else if (strcmp(key, COMMENT_KEY) == 0)
709 gchar *comment = nullptr;
710 if (metadata_legacy_read(fd, nullptr, &comment)) return g_list_append(nullptr, comment);
712 else if (strncmp(key, "file.", 5) == 0)
714 return g_list_append(nullptr, metadata_file_info(fd, key, format));
717 else if (strncmp(key, "lua.", 4) == 0)
719 return g_list_append(nullptr, metadata_lua_info(fd, key, format));
723 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
724 if (!exif) return nullptr;
725 list = exif_get_metadata(exif, key, format);
726 exif_free_fd(fd, exif);
728 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
730 metadata_cache_update(fd, key, list);
736 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
738 GList *string_list = metadata_read_list(fd, key, format);
741 auto str = static_cast<gchar *>(string_list->data);
742 string_list->data = nullptr;
743 g_list_free_full(string_list, g_free);
749 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
753 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
754 if (!string) return fallback;
756 ret = g_ascii_strtoull(string, &endptr, 10);
757 if (string == endptr) ret = fallback;
762 gchar *metadata_read_rating_stars(FileData *fd)
765 gint n = metadata_read_int(fd, RATING_KEY, METADATA_PLAIN);
767 ret = convert_rating_to_stars(n);
772 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
776 gdouble deg, min, sec;
778 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
779 if (!string) return fallback;
781 deg = g_ascii_strtod(string, &endptr);
784 min = g_ascii_strtod(endptr + 1, &endptr);
786 sec = g_ascii_strtod(endptr + 1, &endptr);
791 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
793 coord = deg + min /60.0 + sec / 3600.0;
795 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
802 log_printf("unable to parse GPS coordinate '%s'\n", string);
809 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
814 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
815 if (!string) return fallback;
817 DEBUG_3("GPS_direction: %s\n", string);
818 deg = g_ascii_strtod(string, &endptr);
820 /* Expected text string is of the format e.g.:
832 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
840 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
842 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
846 return metadata_write_string(fd, key, value);
850 gchar *new_string = g_strconcat(str, value, NULL);
851 gboolean ret = metadata_write_string(fd, key, new_string);
858 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
866 char *old_locale, *saved_locale;
872 min = (param * 60) - (deg * 60);
873 if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
878 else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
885 log_printf("unknown GPS parameter key '%s'\n", key);
891 /* Avoid locale problems with commas and decimal points in numbers */
892 old_locale = setlocale(LC_ALL, nullptr);
893 saved_locale = strdup(old_locale);
894 if (saved_locale == nullptr)
898 setlocale(LC_ALL, "C");
900 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
901 metadata_write_string(fd, key, coordinate );
903 setlocale(LC_ALL, saved_locale);
911 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
913 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
917 return metadata_write_list(fd, key, values);
922 list = g_list_concat(list, string_list_copy(values));
923 list = remove_duplicate_strings_from_list(list);
925 ret = metadata_write_list(fd, key, list);
926 g_list_free_full(list, g_free);
932 * @see find_string_in_list
934 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
936 gchar *string_casefold = g_utf8_casefold(string, -1);
940 auto haystack = static_cast<gchar *>(list->data);
945 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
947 equal = (strcmp(haystack_casefold, string_casefold) == 0);
948 g_free(haystack_casefold);
952 g_free(string_casefold);
960 g_free(string_casefold);
965 * @see find_string_in_list
967 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
971 auto haystack = static_cast<gchar *>(list->data);
973 if (haystack && strcmp(haystack, string) == 0)
980 } // gchar *find_string_in_list_utf...
983 * @brief Find a existent string in a list.
985 * This is a switch between find_string_in_list_utf8case and
986 * find_string_in_list_utf8nocase to search with or without case for the
987 * existence of a string.
989 * @param list The list to search in
990 * @param string The string to search for
991 * @return The string or NULL
993 * @see find_string_in_list_utf8case
994 * @see find_string_in_list_utf8nocase
996 gchar *find_string_in_list(GList *list, const gchar *string)
998 if (options->metadata.keywords_case_sensitive)
999 return find_string_in_list_utf8case(list, string);
1001 return find_string_in_list_utf8nocase(list, string);
1004 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
1006 GList *string_to_keywords_list(const gchar *text)
1008 GList *list = nullptr;
1009 const gchar *ptr = text;
1011 while (*ptr != '\0')
1016 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
1018 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
1024 /* trim starting and ending whitespaces */
1025 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
1026 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
1030 gchar *keyword = g_strndup(begin, l);
1032 /* only add if not already in the list */
1033 if (!find_string_in_list(list, keyword))
1034 list = g_list_append(list, keyword);
1048 gboolean meta_data_get_keyword_mark(FileData *fd, gint, gpointer data)
1050 /** @FIXME do not use global keyword_tree */
1051 auto path = static_cast<GList *>(data);
1053 gboolean found = FALSE;
1054 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1058 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1059 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1066 gboolean meta_data_set_keyword_mark(FileData *fd, gint, gboolean value, gpointer data)
1068 auto path = static_cast<GList *>(data);
1069 GList *keywords = nullptr;
1072 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1074 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1076 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1080 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1084 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1086 metadata_write_list(fd, KEYWORD_KEY, keywords);
1089 g_list_free_full(keywords, g_free);
1095 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1098 FileDataGetMarkFunc get_mark_func;
1099 FileDataSetMarkFunc set_mark_func;
1100 gpointer mark_func_data;
1104 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1106 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1107 if (get_mark_func == meta_data_get_keyword_mark)
1109 GtkTreeIter old_kw_iter;
1110 auto old_path = static_cast<GList *>(mark_func_data);
1112 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1113 (i == mark || /* release any previous connection of given mark */
1114 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1116 file_data_register_mark_func(i, nullptr, nullptr, nullptr, nullptr);
1117 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1123 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1127 path = keyword_tree_get_path(keyword_tree, kw_iter);
1128 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, reinterpret_cast<GDestroyNotify>(string_list_free));
1130 mark_str = g_strdup_printf("%d", (mark < 9 ? mark : -1) + 1);
1131 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1138 *-------------------------------------------------------------------
1140 *-------------------------------------------------------------------
1145 GtkTreeStore *keyword_tree;
1147 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1150 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1154 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1158 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1162 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1165 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1169 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1171 gboolean is_keyword;
1172 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1176 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1178 gchar *casefold = g_utf8_casefold(name, -1);
1179 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1180 KEYWORD_COLUMN_NAME, name,
1181 KEYWORD_COLUMN_CASEFOLD, casefold,
1182 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1186 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1188 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1189 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1190 gint ret = gtk_tree_path_compare(pa, pb);
1191 gtk_tree_path_free(pa);
1192 gtk_tree_path_free(pb);
1196 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1198 GtkTreeIter parent_a;
1199 GtkTreeIter parent_b;
1201 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1202 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1204 if (valid_pa && valid_pb)
1206 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1210 return (!valid_pa && !valid_pb); /* both are toplevel */
1214 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1218 gboolean toplevel = FALSE;
1224 parent = *parent_ptr;
1228 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1235 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? nullptr : &parent)) return FALSE;
1237 casefold = g_utf8_casefold(name, -1);
1242 if (!exclude_sibling || !sibling || keyword_compare(keyword_tree, &iter, sibling) != 0)
1244 if (options->metadata.keywords_case_sensitive)
1246 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1247 ret = strcmp(name, iter_name) == 0;
1252 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1253 ret = strcmp(casefold, iter_casefold) == 0;
1254 g_free(iter_casefold);
1255 } // if (options->metadata.tags_cas...
1259 if (result) *result = iter;
1262 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1269 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1272 gchar *mark, *name, *casefold;
1273 gboolean is_keyword;
1275 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1276 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1277 KEYWORD_COLUMN_NAME, &name,
1278 KEYWORD_COLUMN_CASEFOLD, &casefold,
1279 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1281 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1282 KEYWORD_COLUMN_NAME, name,
1283 KEYWORD_COLUMN_CASEFOLD, casefold,
1284 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1290 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1292 GtkTreeIter from_child;
1294 keyword_copy(keyword_tree, to, from);
1296 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1300 GtkTreeIter to_child;
1301 gtk_tree_store_append(keyword_tree, &to_child, to);
1302 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1303 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1307 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1309 keyword_copy_recursive(keyword_tree, to, from);
1310 keyword_delete(keyword_tree, from);
1313 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1315 GList *path = nullptr;
1316 GtkTreeIter iter = *iter_ptr;
1321 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1322 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1328 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1332 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1336 GtkTreeIter children;
1339 gchar *name = keyword_get_name(keyword_tree, &iter);
1340 if (strcmp(name, static_cast<const gchar *>(path->data)) == 0) break;
1342 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1351 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1357 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1359 if (!casefold_list) return FALSE;
1361 if (!keyword_get_is_keyword(keyword_tree, &iter))
1363 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1365 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1366 return FALSE; /* this should happen only on empty helpers */
1370 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1371 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1379 if (keyword_get_is_keyword(keyword_tree, &iter))
1381 GList *work = casefold_list;
1382 gboolean found = FALSE;
1383 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1386 auto casefold = static_cast<const gchar *>(work->data);
1389 if (strcmp(iter_casefold, casefold) == 0)
1395 g_free(iter_casefold);
1396 if (!found) return FALSE;
1399 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1404 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1406 if (!kw_list) return FALSE;
1408 if (!keyword_get_is_keyword(keyword_tree, &iter))
1410 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1412 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1413 return FALSE; /* this should happen only on empty helpers */
1417 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1418 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1426 if (keyword_get_is_keyword(keyword_tree, &iter))
1428 GList *work = kw_list;
1429 gboolean found = FALSE;
1430 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1433 auto name = static_cast<const gchar *>(work->data);
1436 if (strcmp(iter_name, name) == 0)
1443 if (!found) return FALSE;
1446 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1451 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1454 GList *casefold_list = nullptr;
1457 if (options->metadata.keywords_case_sensitive)
1459 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1466 auto kw = static_cast<const gchar *>(work->data);
1469 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1472 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1474 g_list_free_full(casefold_list, g_free);
1480 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1482 GtkTreeIter iter = *iter_ptr;
1487 if (keyword_get_is_keyword(keyword_tree, &iter))
1489 gchar *name = keyword_get_name(keyword_tree, &iter);
1490 if (!find_string_in_list(*kw_list, name))
1492 *kw_list = g_list_append(*kw_list, name);
1500 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1505 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1507 GtkTreeIter iter = *iter_ptr;
1508 GList *kw_list = nullptr;
1514 if (keyword_get_is_keyword(keyword_tree, &iter))
1516 gchar *name = keyword_get_name(keyword_tree, &iter);
1517 kw_list = g_list_append(kw_list, name);
1520 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1523 } // GList *keyword_tree_get(GtkTre...
1525 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1529 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1531 name = keyword_get_name(keyword_tree, iter);
1532 found = find_string_in_list(*kw_list, name);
1536 *kw_list = g_list_remove(*kw_list, found);
1542 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1545 keyword_tree_reset1(keyword_tree, iter, kw_list);
1547 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1551 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1552 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1556 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1560 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1561 return TRUE; /* this should happen only on empty helpers */
1565 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1566 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1570 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1572 GtkTreeIter iter = *iter_ptr;
1574 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1576 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1579 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1582 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1583 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1588 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1592 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1594 keyword_delete(keyword_tree, &child);
1597 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1599 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1602 gtk_tree_store_remove(keyword_tree, iter_ptr);
1606 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1609 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1610 if (!g_list_find(list, id))
1612 list = g_list_prepend(list, id);
1613 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1617 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1620 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1621 list = g_list_remove(list, id);
1622 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1625 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1628 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1629 return !!g_list_find(list, id);
1632 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1634 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1638 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1640 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1643 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer data)
1645 if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1647 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1652 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1654 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1657 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1659 GtkTreeIter iter = *iter_ptr;
1662 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1664 keyword_hide_in(keyword_tree, &iter, id);
1665 /* no need to check children of hidden node */
1670 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1672 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1675 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1679 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1682 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1683 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1686 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter_ptr, gpointer data)
1688 GtkTreeIter iter = *iter_ptr;
1689 auto keywords = static_cast<GList *>(data);
1690 gpointer id = keywords->data;
1691 keywords = keywords->next; /* hack */
1692 if (keyword_tree_is_set(model, &iter, keywords))
1697 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1698 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1705 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1707 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1708 keywords = g_list_prepend(keywords, id);
1709 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1710 keywords = g_list_delete_link(keywords, keywords);
1714 void keyword_tree_new()
1716 if (keyword_tree) return;
1718 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1721 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1724 gtk_tree_store_append(keyword_tree, &iter, parent);
1725 keyword_set(keyword_tree, &iter, name, is_keyword);
1729 void keyword_tree_new_default()
1733 if (!keyword_tree) keyword_tree_new();
1735 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("People"), TRUE);
1736 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1737 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1738 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1739 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1740 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1741 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1742 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Nature"), TRUE);
1743 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1744 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1745 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1746 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1747 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1748 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1749 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1750 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1751 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1752 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1753 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1754 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1755 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1756 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1757 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Art"), TRUE);
1758 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1759 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1760 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1761 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1762 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("City"), TRUE);
1763 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1764 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1765 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1766 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Architecture"), TRUE);
1767 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1768 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1769 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1770 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1771 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1772 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1773 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1774 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1775 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1776 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Places"), FALSE);
1777 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Conditions"), FALSE);
1778 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1779 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1780 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1781 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1782 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1783 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1784 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1785 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1786 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1787 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1788 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Photo"), FALSE);
1789 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1790 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1791 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1792 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1793 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1794 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1798 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1800 GtkTreeIter iter = *iter_ptr;
1803 GtkTreeIter children;
1807 WRITE_NL(); WRITE_STRING("<keyword ");
1808 name = keyword_get_name(keyword_tree, &iter);
1809 write_char_option(outstr, indent, "name", name);
1811 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1812 mark_str = keyword_get_mark(keyword_tree, &iter);
1813 if (mark_str && mark_str[0])
1815 write_char_option(outstr, indent, "mark", mark_str);
1818 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1822 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1824 WRITE_NL(); WRITE_STRING("</keyword>");
1830 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1834 void keyword_tree_write_config(GString *outstr, gint indent)
1837 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1840 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1842 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1845 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1848 void keyword_tree_node_disconnect_marks(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1850 GtkTreeIter iter = *iter_ptr;
1854 GtkTreeIter children;
1856 meta_data_connect_mark_with_keyword((keyword_tree), &iter, -1);
1858 if (gtk_tree_model_iter_children((keyword_tree), &children, &iter))
1860 keyword_tree_node_disconnect_marks((keyword_tree), &children);
1863 if (!gtk_tree_model_iter_next((keyword_tree), &iter)) return;
1867 void keyword_tree_disconnect_marks()
1871 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1873 keyword_tree_node_disconnect_marks(GTK_TREE_MODEL(keyword_tree), &iter);
1877 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1879 gchar *name = nullptr;
1880 gboolean is_kw = TRUE;
1881 gchar *mark_str = nullptr;
1883 while (*attribute_names)
1885 const gchar *option = *attribute_names++;
1886 const gchar *value = *attribute_values++;
1888 if (READ_CHAR_FULL("name", name)) continue;
1889 if (READ_BOOL_FULL("kw", is_kw)) continue;
1890 if (READ_CHAR_FULL("mark", mark_str)) continue;
1892 log_printf("unknown attribute %s = %s\n", option, value);
1894 if (name && name[0])
1897 /* re-use existing keyword if any */
1898 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, nullptr, name, FALSE, &iter))
1900 gtk_tree_store_append(keyword_tree, &iter, parent);
1902 keyword_set(keyword_tree, &iter, name, is_kw);
1906 gint i = static_cast<gint>(atoi(mark_str));
1909 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1914 return gtk_tree_iter_copy(&iter);
1920 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */