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 string_list_free(old_values);
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 string_list_free(entry);
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)
166 if (fd->cached_metadata) DEBUG_1("freed %s\n", fd->path);
168 work = fd->cached_metadata;
171 auto entry = static_cast<GList *>(work->data);
172 string_list_free(entry);
176 g_list_free(fd->cached_metadata);
177 fd->cached_metadata = nullptr;
186 *-------------------------------------------------------------------
188 *-------------------------------------------------------------------
191 static GList *metadata_write_queue = nullptr;
192 static guint metadata_write_idle_id = 0; /* event source id */
194 static void metadata_write_queue_add(FileData *fd)
196 if (!g_list_find(metadata_write_queue, fd))
198 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
201 layout_util_status_update_write_all();
204 if (metadata_write_idle_id)
206 g_source_remove(metadata_write_idle_id);
207 metadata_write_idle_id = 0;
210 if (options->metadata.confirm_after_timeout)
212 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, nullptr);
217 gboolean metadata_write_queue_remove(FileData *fd)
219 g_hash_table_destroy(fd->modified_xmp);
220 fd->modified_xmp = nullptr;
222 metadata_write_queue = g_list_remove(metadata_write_queue, fd);
224 file_data_increment_version(fd);
225 file_data_send_notification(fd, NOTIFY_REREAD);
229 layout_util_status_update_write_all();
233 #pragma GCC diagnostic push
234 #pragma GCC diagnostic ignored "-Wunused-function"
235 gboolean metadata_write_queue_remove_list_unused(GList *list)
243 FileData *fd = static_cast<FileData *>(work->data);
245 ret = ret && metadata_write_queue_remove(fd);
249 #pragma GCC diagnostic pop
251 void metadata_notify_cb(FileData *fd, NotifyType type, gpointer UNUSED(data))
253 if (type & (NOTIFY_REREAD | NOTIFY_CHANGE))
255 metadata_cache_free(fd);
257 if (g_list_find(metadata_write_queue, fd))
259 DEBUG_1("Notify metadata: %s %04x", fd->path, type);
260 if (!isname(fd->path))
262 /* ignore deleted files */
263 metadata_write_queue_remove(fd);
269 gboolean metadata_write_queue_confirm(gboolean force_dialog, FileUtilDoneFunc done_func, gpointer done_data)
272 GList *to_approve = nullptr;
274 work = metadata_write_queue;
277 auto fd = static_cast<FileData *>(work->data);
280 if (!isname(fd->path))
282 /* ignore deleted files */
283 metadata_write_queue_remove(fd);
287 if (fd->change) continue; /* another operation in progress, skip this file for now */
289 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
292 file_util_write_metadata(nullptr, to_approve, nullptr, force_dialog, done_func, done_data);
294 return (metadata_write_queue != nullptr);
297 static gboolean metadata_write_queue_idle_cb(gpointer UNUSED(data))
299 metadata_write_queue_confirm(FALSE, nullptr, nullptr);
300 metadata_write_idle_id = 0;
304 gboolean metadata_write_perform(FileData *fd)
310 g_assert(fd->change);
312 lf = strlen(GQ_CACHE_EXT_METADATA);
313 if (fd->change->dest &&
314 g_ascii_strncasecmp(fd->change->dest + strlen(fd->change->dest) - lf, GQ_CACHE_EXT_METADATA, lf) == 0)
316 success = metadata_legacy_write(fd);
317 if (success) metadata_legacy_delete(fd, fd->change->dest);
321 /* write via exiv2 */
322 /* we can either use cached metadata which have fd->modified_xmp already applied
323 or read metadata from file and apply fd->modified_xmp
324 metadata are read also if the file was modified meanwhile */
325 exif = exif_read_fd(fd);
326 if (!exif) return FALSE;
328 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
329 exif_free_fd(fd, exif);
331 if (fd->change->dest)
332 /* this will create a FileData for the sidecar and link it to the main file
333 (we can't wait until the sidecar is discovered by directory scanning because
334 exif_read_fd is called before that and it would read the main file only and
335 store the metadata in the cache)
338 @FIXME this does not catch new sidecars created by independent external programs
340 file_data_unref(file_data_new_group(fd->change->dest));
342 if (success) metadata_legacy_delete(fd, fd->change->dest);
346 gint metadata_queue_length()
348 return g_list_length(metadata_write_queue);
351 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
353 const gchar **k = keys;
357 if (strcmp(key, *k) == 0) return TRUE;
363 gboolean metadata_write_revert(FileData *fd, const gchar *key)
365 if (!fd->modified_xmp) return FALSE;
367 g_hash_table_remove(fd->modified_xmp, key);
369 if (g_hash_table_size(fd->modified_xmp) == 0)
371 metadata_write_queue_remove(fd);
375 /* reread the metadata to restore the original value */
376 file_data_increment_version(fd);
377 file_data_send_notification(fd, NOTIFY_REREAD);
382 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
384 if (!fd->modified_xmp)
386 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, reinterpret_cast<GDestroyNotify>(string_list_free));
388 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy(const_cast<GList *>(values)));
390 metadata_cache_remove(fd, key);
394 exif_update_metadata(fd->exif, key, values);
396 metadata_write_queue_add(fd);
397 file_data_increment_version(fd);
398 file_data_send_notification(fd, NOTIFY_METADATA);
400 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
402 GList *work = fd->sidecar_files;
406 auto sfd = static_cast<FileData *>(work->data);
409 if (sfd->format_class == FORMAT_CLASS_META) continue;
411 metadata_write_list(sfd, key, values);
419 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
421 GList *list = g_list_append(nullptr, g_strdup(value));
422 gboolean ret = metadata_write_list(fd, key, list);
423 string_list_free(list);
427 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
431 g_snprintf(string, sizeof(string), "%llu", static_cast<unsigned long long>(value));
432 return metadata_write_string(fd, key, string);
436 *-------------------------------------------------------------------
437 * keyword / comment read/write
438 *-------------------------------------------------------------------
441 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
445 ssi = secure_open(path);
446 if (!ssi) return FALSE;
448 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
450 secure_fprintf(ssi, "[keywords]\n");
451 while (keywords && secsave_errno == SS_ERR_NONE)
453 auto word = static_cast<const gchar *>(keywords->data);
454 keywords = keywords->next;
456 secure_fprintf(ssi, "%s\n", word);
458 secure_fputc(ssi, '\n');
460 secure_fprintf(ssi, "[comment]\n");
461 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
463 secure_fprintf(ssi, "#end\n");
465 return (secure_close(ssi) == 0);
468 static gboolean metadata_legacy_write(FileData *fd)
470 gboolean success = FALSE;
471 gchar *metadata_pathl;
474 gboolean have_keywords;
475 gboolean have_comment;
476 const gchar *comment;
477 GList *orig_keywords = nullptr;
478 gchar *orig_comment = nullptr;
480 g_assert(fd->change && fd->change->dest);
482 DEBUG_1("Saving comment: %s", fd->change->dest);
484 if (!fd->modified_xmp) return TRUE;
486 metadata_pathl = path_from_utf8(fd->change->dest);
488 have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, nullptr, &keywords);
489 have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, nullptr, &comment_l);
490 comment = static_cast<const gchar *>((have_comment && comment_l) ? (static_cast<GList *>(comment_l))->data : nullptr);
492 if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
494 success = metadata_file_write(metadata_pathl,
495 have_keywords ? static_cast<GList *>(keywords) : orig_keywords,
496 have_comment ? comment : orig_comment);
498 g_free(metadata_pathl);
499 g_free(orig_comment);
500 string_list_free(orig_keywords);
505 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
509 MetadataKey key = MK_NONE;
510 GList *list = nullptr;
511 GString *comment_build = nullptr;
513 f = fopen(path, "r");
514 if (!f) return FALSE;
516 while (fgets(s_buf, sizeof(s_buf), f))
520 if (*ptr == '#') continue;
521 if (*ptr == '[' && key != MK_COMMENT)
523 gchar *keystr = ++ptr;
526 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
531 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
533 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
545 while (*ptr != '\n' && *ptr != '\0') ptr++;
547 if (strlen(s_buf) > 0)
549 gchar *kw = utf8_validate_or_convert(s_buf);
551 list = g_list_prepend(list, kw);
556 if (!comment_build) comment_build = g_string_new("");
557 g_string_append(comment_build, s_buf);
566 *keywords = g_list_reverse(list);
570 string_list_free(list);
578 gchar *ptr = comment_build->str;
580 /* strip leading and trailing newlines */
581 while (*ptr == '\n') ptr++;
583 while (len > 0 && ptr[len - 1] == '\n') len--;
584 if (ptr[len] == '\n') len++; /* keep the last one */
587 gchar *text = g_strndup(ptr, len);
589 *comment = utf8_validate_or_convert(text);
593 g_string_free(comment_build, TRUE);
599 static void metadata_legacy_delete(FileData *fd, const gchar *except)
601 gchar *metadata_path;
602 gchar *metadata_pathl;
605 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
606 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
608 metadata_pathl = path_from_utf8(metadata_path);
609 unlink(metadata_pathl);
610 g_free(metadata_pathl);
611 g_free(metadata_path);
615 /* without exiv2: do not delete xmp metadata because we are not able to convert it,
617 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
618 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
620 metadata_pathl = path_from_utf8(metadata_path);
621 unlink(metadata_pathl);
622 g_free(metadata_pathl);
623 g_free(metadata_path);
628 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
630 gchar *metadata_path;
631 gchar *metadata_pathl;
632 gboolean success = FALSE;
634 if (!fd) return FALSE;
636 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
637 if (!metadata_path) return FALSE;
639 metadata_pathl = path_from_utf8(metadata_path);
641 success = metadata_file_read(metadata_pathl, keywords, comment);
643 g_free(metadata_pathl);
644 g_free(metadata_path);
649 static GList *remove_duplicate_strings_from_list(GList *list)
652 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
653 GList *newlist = nullptr;
657 auto key = static_cast<gchar *>(work->data);
659 if (g_hash_table_lookup(hashtable, key) == nullptr)
661 g_hash_table_insert(hashtable, key, GINT_TO_POINTER(1));
662 newlist = g_list_prepend(newlist, key);
667 g_hash_table_destroy(hashtable);
670 return g_list_reverse(newlist);
673 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
676 GList *list = nullptr;
677 const GList *cache_entry;
678 if (!fd) return nullptr;
680 /* unwritten data override everything */
681 if (fd->modified_xmp && format == METADATA_PLAIN)
683 list = static_cast<GList *>(g_hash_table_lookup(fd->modified_xmp, key));
684 if (list) return string_list_copy(list);
688 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
689 && (cache_entry = metadata_cache_get(fd, key)))
691 return string_list_copy(cache_entry->next);
695 Legacy metadata file is the primary source if it exists.
696 Merging the lists does not make much sense, because the existence of
697 legacy metadata file indicates that the other metadata sources are not
698 writable and thus it would not be possible to delete the keywords
699 that comes from the image file.
701 if (strcmp(key, KEYWORD_KEY) == 0)
703 if (metadata_legacy_read(fd, &list, nullptr))
705 if (format == METADATA_PLAIN)
707 metadata_cache_update(fd, key, list);
712 else if (strcmp(key, COMMENT_KEY) == 0)
714 gchar *comment = nullptr;
715 if (metadata_legacy_read(fd, nullptr, &comment)) return g_list_append(nullptr, comment);
717 else if (strncmp(key, "file.", 5) == 0)
719 return g_list_append(nullptr, metadata_file_info(fd, key, format));
722 else if (strncmp(key, "lua.", 4) == 0)
724 return g_list_append(nullptr, metadata_lua_info(fd, key, format));
728 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
729 if (!exif) return nullptr;
730 list = exif_get_metadata(exif, key, format);
731 exif_free_fd(fd, exif);
733 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
735 metadata_cache_update(fd, key, list);
741 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
743 GList *string_list = metadata_read_list(fd, key, format);
746 auto str = static_cast<gchar *>(string_list->data);
747 string_list->data = nullptr;
748 string_list_free(string_list);
754 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
758 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
759 if (!string) return fallback;
761 ret = g_ascii_strtoull(string, &endptr, 10);
762 if (string == endptr) ret = fallback;
767 gchar *metadata_read_rating_stars(FileData *fd)
770 gint n = metadata_read_int(fd, RATING_KEY, METADATA_PLAIN);
772 ret = convert_rating_to_stars(n);
777 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
781 gdouble deg, min, sec;
783 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
784 if (!string) return fallback;
786 deg = g_ascii_strtod(string, &endptr);
789 min = g_ascii_strtod(endptr + 1, &endptr);
791 sec = g_ascii_strtod(endptr + 1, &endptr);
796 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
798 coord = deg + min /60.0 + sec / 3600.0;
800 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
807 log_printf("unable to parse GPS coordinate '%s'\n", string);
814 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
819 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
820 if (!string) return fallback;
822 DEBUG_3("GPS_direction: %s\n", string);
823 deg = g_ascii_strtod(string, &endptr);
825 /* Expected text string is of the format e.g.:
837 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
845 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
847 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
851 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);
863 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
871 char *old_locale, *saved_locale;
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);
927 list = g_list_concat(list, string_list_copy(values));
928 list = remove_duplicate_strings_from_list(list);
930 ret = metadata_write_list(fd, key, list);
931 string_list_free(list);
937 * @see find_string_in_list
939 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
941 gchar *string_casefold = g_utf8_casefold(string, -1);
945 auto haystack = static_cast<gchar *>(list->data);
950 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
952 equal = (strcmp(haystack_casefold, string_casefold) == 0);
953 g_free(haystack_casefold);
957 g_free(string_casefold);
965 g_free(string_casefold);
970 * @see find_string_in_list
972 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
976 auto haystack = static_cast<gchar *>(list->data);
978 if (haystack && strcmp(haystack, string) == 0)
985 } // gchar *find_string_in_list_utf...
988 * @brief Find a existent string in a list.
990 * This is a switch between find_string_in_list_utf8case and
991 * find_string_in_list_utf8nocase to search with or without case for the
992 * existence of a string.
994 * @param list The list to search in
995 * @param string The string to search for
996 * @return The string or NULL
998 * @see find_string_in_list_utf8case
999 * @see find_string_in_list_utf8nocase
1001 gchar *find_string_in_list(GList *list, const gchar *string)
1003 if (options->metadata.keywords_case_sensitive)
1004 return find_string_in_list_utf8case(list, string);
1006 return find_string_in_list_utf8nocase(list, string);
1009 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
1011 GList *string_to_keywords_list(const gchar *text)
1013 GList *list = nullptr;
1014 const gchar *ptr = text;
1016 while (*ptr != '\0')
1021 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
1023 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
1029 /* trim starting and ending whitespaces */
1030 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
1031 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
1035 gchar *keyword = g_strndup(begin, l);
1037 /* only add if not already in the list */
1038 if (!find_string_in_list(list, keyword))
1039 list = g_list_append(list, keyword);
1053 gboolean meta_data_get_keyword_mark(FileData *fd, gint UNUSED(n), gpointer data)
1055 /** @FIXME do not use global keyword_tree */
1056 auto path = static_cast<GList *>(data);
1058 gboolean found = FALSE;
1059 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1063 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1064 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1071 gboolean meta_data_set_keyword_mark(FileData *fd, gint UNUSED(n), gboolean value, gpointer data)
1073 auto path = static_cast<GList *>(data);
1074 GList *keywords = nullptr;
1077 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1079 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1081 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1085 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1089 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1091 metadata_write_list(fd, KEYWORD_KEY, keywords);
1094 string_list_free(keywords);
1100 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1103 FileDataGetMarkFunc get_mark_func;
1104 FileDataSetMarkFunc set_mark_func;
1105 gpointer mark_func_data;
1109 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1111 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1112 if (get_mark_func == meta_data_get_keyword_mark)
1114 GtkTreeIter old_kw_iter;
1115 auto old_path = static_cast<GList *>(mark_func_data);
1117 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1118 (i == mark || /* release any previous connection of given mark */
1119 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1121 file_data_register_mark_func(i, nullptr, nullptr, nullptr, nullptr);
1122 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1128 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1132 path = keyword_tree_get_path(keyword_tree, kw_iter);
1133 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, reinterpret_cast<GDestroyNotify>(string_list_free));
1135 mark_str = g_strdup_printf("%d", (mark < 9 ? mark : -1) + 1);
1136 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1143 *-------------------------------------------------------------------
1145 *-------------------------------------------------------------------
1150 GtkTreeStore *keyword_tree;
1152 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1155 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1159 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1163 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1167 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1170 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1174 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1176 gboolean is_keyword;
1177 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1181 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1183 gchar *casefold = g_utf8_casefold(name, -1);
1184 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1185 KEYWORD_COLUMN_NAME, name,
1186 KEYWORD_COLUMN_CASEFOLD, casefold,
1187 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1191 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1193 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1194 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1195 gint ret = gtk_tree_path_compare(pa, pb);
1196 gtk_tree_path_free(pa);
1197 gtk_tree_path_free(pb);
1201 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1203 GtkTreeIter parent_a;
1204 GtkTreeIter parent_b;
1206 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1207 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1209 if (valid_pa && valid_pb)
1211 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1215 return (!valid_pa && !valid_pb); /* both are toplevel */
1219 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1223 gboolean toplevel = FALSE;
1229 parent = *parent_ptr;
1233 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1240 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? nullptr : &parent)) return FALSE;
1242 casefold = g_utf8_casefold(name, -1);
1247 if (!exclude_sibling || !sibling || keyword_compare(keyword_tree, &iter, sibling) != 0)
1249 if (options->metadata.keywords_case_sensitive)
1251 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1252 ret = strcmp(name, iter_name) == 0;
1257 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1258 ret = strcmp(casefold, iter_casefold) == 0;
1259 g_free(iter_casefold);
1260 } // if (options->metadata.tags_cas...
1264 if (result) *result = iter;
1267 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1274 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1277 gchar *mark, *name, *casefold;
1278 gboolean is_keyword;
1280 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1281 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1282 KEYWORD_COLUMN_NAME, &name,
1283 KEYWORD_COLUMN_CASEFOLD, &casefold,
1284 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1286 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1287 KEYWORD_COLUMN_NAME, name,
1288 KEYWORD_COLUMN_CASEFOLD, casefold,
1289 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1295 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1297 GtkTreeIter from_child;
1299 keyword_copy(keyword_tree, to, from);
1301 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1305 GtkTreeIter to_child;
1306 gtk_tree_store_append(keyword_tree, &to_child, to);
1307 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1308 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1312 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1314 keyword_copy_recursive(keyword_tree, to, from);
1315 keyword_delete(keyword_tree, from);
1318 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1320 GList *path = nullptr;
1321 GtkTreeIter iter = *iter_ptr;
1326 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1327 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1333 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1337 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1341 GtkTreeIter children;
1344 gchar *name = keyword_get_name(keyword_tree, &iter);
1345 if (strcmp(name, static_cast<const gchar *>(path->data)) == 0) break;
1347 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1356 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1362 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1364 if (!casefold_list) return FALSE;
1366 if (!keyword_get_is_keyword(keyword_tree, &iter))
1368 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1370 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1371 return FALSE; /* this should happen only on empty helpers */
1375 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1376 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1384 if (keyword_get_is_keyword(keyword_tree, &iter))
1386 GList *work = casefold_list;
1387 gboolean found = FALSE;
1388 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1391 auto casefold = static_cast<const gchar *>(work->data);
1394 if (strcmp(iter_casefold, casefold) == 0)
1400 g_free(iter_casefold);
1401 if (!found) return FALSE;
1404 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1409 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1411 if (!kw_list) return FALSE;
1413 if (!keyword_get_is_keyword(keyword_tree, &iter))
1415 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1417 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1418 return FALSE; /* this should happen only on empty helpers */
1422 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1423 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1431 if (keyword_get_is_keyword(keyword_tree, &iter))
1433 GList *work = kw_list;
1434 gboolean found = FALSE;
1435 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1438 auto name = static_cast<const gchar *>(work->data);
1441 if (strcmp(iter_name, name) == 0)
1448 if (!found) return FALSE;
1451 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1456 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1459 GList *casefold_list = nullptr;
1462 if (options->metadata.keywords_case_sensitive)
1464 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1471 auto kw = static_cast<const gchar *>(work->data);
1474 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1477 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1479 string_list_free(casefold_list);
1485 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1487 GtkTreeIter iter = *iter_ptr;
1492 if (keyword_get_is_keyword(keyword_tree, &iter))
1494 gchar *name = keyword_get_name(keyword_tree, &iter);
1495 if (!find_string_in_list(*kw_list, name))
1497 *kw_list = g_list_append(*kw_list, name);
1505 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1510 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1512 GtkTreeIter iter = *iter_ptr;
1513 GList *kw_list = nullptr;
1519 if (keyword_get_is_keyword(keyword_tree, &iter))
1521 gchar *name = keyword_get_name(keyword_tree, &iter);
1522 kw_list = g_list_append(kw_list, name);
1525 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1528 } // GList *keyword_tree_get(GtkTre...
1530 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1534 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1536 name = keyword_get_name(keyword_tree, iter);
1537 found = find_string_in_list(*kw_list, name);
1541 *kw_list = g_list_remove(*kw_list, found);
1547 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1550 keyword_tree_reset1(keyword_tree, iter, kw_list);
1552 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1556 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1557 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1561 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1565 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1566 return TRUE; /* this should happen only on empty helpers */
1570 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1571 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1575 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1577 GtkTreeIter iter = *iter_ptr;
1579 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1581 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1584 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1587 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1588 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1593 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1597 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1599 keyword_delete(keyword_tree, &child);
1602 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1604 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1607 gtk_tree_store_remove(keyword_tree, iter_ptr);
1611 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1614 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1615 if (!g_list_find(list, id))
1617 list = g_list_prepend(list, id);
1618 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1622 void keyword_show_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 list = g_list_remove(list, id);
1627 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1630 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1633 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1634 return !!g_list_find(list, id);
1637 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *UNUSED(path), GtkTreeIter *iter, gpointer data)
1639 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1643 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1645 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1648 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *UNUSED(path), GtkTreeIter *iter, gpointer data)
1650 if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1652 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1657 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1659 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1662 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1664 GtkTreeIter iter = *iter_ptr;
1667 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1669 keyword_hide_in(keyword_tree, &iter, id);
1670 /* no need to check children of hidden node */
1675 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1677 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1680 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1684 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1687 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1688 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1691 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *UNUSED(path), GtkTreeIter *iter_ptr, gpointer data)
1693 GtkTreeIter iter = *iter_ptr;
1694 auto keywords = static_cast<GList *>(data);
1695 gpointer id = keywords->data;
1696 keywords = keywords->next; /* hack */
1697 if (keyword_tree_is_set(model, &iter, keywords))
1702 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1703 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1710 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1712 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1713 keywords = g_list_prepend(keywords, id);
1714 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1715 keywords = g_list_delete_link(keywords, keywords);
1719 void keyword_tree_new()
1721 if (keyword_tree) return;
1723 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1726 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1729 gtk_tree_store_append(keyword_tree, &iter, parent);
1730 keyword_set(keyword_tree, &iter, name, is_keyword);
1734 void keyword_tree_new_default()
1738 if (!keyword_tree) keyword_tree_new();
1740 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("People"), TRUE);
1741 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1742 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1743 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1744 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1745 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1746 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1747 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Nature"), TRUE);
1748 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1749 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1750 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1751 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1752 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1753 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1754 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1755 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1756 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1757 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1758 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1759 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1760 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1761 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1762 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Art"), TRUE);
1763 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1764 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1765 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1766 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1767 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("City"), TRUE);
1768 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1769 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1770 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1771 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Architecture"), TRUE);
1772 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1773 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1774 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1775 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1776 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1777 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1778 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1779 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1780 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1781 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Places"), FALSE);
1782 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Conditions"), FALSE);
1783 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1784 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1785 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1786 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1787 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1788 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1789 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1790 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1791 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1792 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1793 i1 = keyword_tree_default_append(keyword_tree, nullptr, _("Photo"), FALSE);
1794 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1795 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1796 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1797 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1798 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1799 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1803 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1805 GtkTreeIter iter = *iter_ptr;
1808 GtkTreeIter children;
1812 WRITE_NL(); WRITE_STRING("<keyword ");
1813 name = keyword_get_name(keyword_tree, &iter);
1814 write_char_option(outstr, indent, "name", name);
1816 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1817 mark_str = keyword_get_mark(keyword_tree, &iter);
1818 if (mark_str && mark_str[0])
1820 write_char_option(outstr, indent, "mark", mark_str);
1823 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1827 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1829 WRITE_NL(); WRITE_STRING("</keyword>");
1835 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1839 void keyword_tree_write_config(GString *outstr, gint indent)
1842 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1845 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1847 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1850 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1853 void keyword_tree_node_disconnect_marks(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1855 GtkTreeIter iter = *iter_ptr;
1859 GtkTreeIter children;
1861 meta_data_connect_mark_with_keyword((keyword_tree), &iter, -1);
1863 if (gtk_tree_model_iter_children((keyword_tree), &children, &iter))
1865 keyword_tree_node_disconnect_marks((keyword_tree), &children);
1868 if (!gtk_tree_model_iter_next((keyword_tree), &iter)) return;
1872 void keyword_tree_disconnect_marks()
1876 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1878 keyword_tree_node_disconnect_marks(GTK_TREE_MODEL(keyword_tree), &iter);
1882 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1884 gchar *name = nullptr;
1885 gboolean is_kw = TRUE;
1886 gchar *mark_str = nullptr;
1888 while (*attribute_names)
1890 const gchar *option = *attribute_names++;
1891 const gchar *value = *attribute_values++;
1893 if (READ_CHAR_FULL("name", name)) continue;
1894 if (READ_BOOL_FULL("kw", is_kw)) continue;
1895 if (READ_CHAR_FULL("mark", mark_str)) continue;
1897 log_printf("unknown attribute %s = %s\n", option, value);
1899 if (name && name[0])
1902 /* re-use existing keyword if any */
1903 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, nullptr, name, FALSE, &iter))
1905 gtk_tree_store_append(keyword_tree, &iter, parent);
1907 keyword_set(keyword_tree, &iter, name, is_kw);
1911 gint i = static_cast<gint>(atoi(mark_str));
1914 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1919 return gtk_tree_iter_copy(&iter);
1925 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */