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.
29 #include "secure_save.h"
30 #include "ui_fileops.h"
33 #include "filefilter.h"
34 #include "layout_util.h"
43 static const gchar *group_keys[] = { /* tags that will be written to all files in a group, options->metadata.sync_grouped_files */
45 "Xmp.photoshop.Urgency",
46 "Xmp.photoshop.Category",
47 "Xmp.photoshop.SupplementalCategory",
50 "Xmp.photoshop.Instruction",
51 "Xmp.photoshop.DateCreated",
53 "Xmp.photoshop.AuthorsPosition",
55 "Xmp.photoshop.State",
56 "Xmp.iptc.CountryCode",
57 "Xmp.photoshop.Country",
58 "Xmp.photoshop.TransmissionReference",
59 "Xmp.photoshop.Headline",
60 "Xmp.photoshop.Credit",
61 "Xmp.photoshop.Source",
64 "Xmp.photoshop.CaptionWriter",
67 static gboolean metadata_write_queue_idle_cb(gpointer data);
68 static gboolean metadata_legacy_write(FileData *fd);
69 static void metadata_legacy_delete(FileData *fd, const gchar *except);
70 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment);
74 *-------------------------------------------------------------------
75 * long-term cache - keep keywords from whole dir in memory
76 *-------------------------------------------------------------------
79 /* fd->cached metadata list of lists
80 each particular list contains key as a first entry, then the values
83 static void metadata_cache_update(FileData *fd, const gchar *key, const GList *values)
87 work = fd->cached_metadata;
90 GList *entry = work->data;
91 gchar *entry_key = entry->data;
93 if (strcmp(entry_key, key) == 0)
95 /* key found - just replace values */
96 GList *old_values = entry->next;
98 old_values->prev = NULL;
99 string_list_free(old_values);
100 work->data = g_list_append(entry, string_list_copy(values));
101 DEBUG_1("updated %s %s\n", key, fd->path);
107 /* key not found - prepend new entry */
108 fd->cached_metadata = g_list_prepend(fd->cached_metadata,
109 g_list_prepend(string_list_copy(values), g_strdup(key)));
110 DEBUG_1("added %s %s\n", key, fd->path);
114 static const GList *metadata_cache_get(FileData *fd, const gchar *key)
118 work = fd->cached_metadata;
121 GList *entry = work->data;
122 gchar *entry_key = entry->data;
124 if (strcmp(entry_key, key) == 0)
127 DEBUG_1("found %s %s\n", key, fd->path);
133 DEBUG_1("not found %s %s\n", key, fd->path);
136 static void metadata_cache_remove(FileData *fd, const gchar *key)
140 work = fd->cached_metadata;
143 GList *entry = work->data;
144 gchar *entry_key = entry->data;
146 if (strcmp(entry_key, key) == 0)
149 string_list_free(entry);
150 fd->cached_metadata = g_list_delete_link(fd->cached_metadata, work);
151 DEBUG_1("removed %s %s\n", key, fd->path);
156 DEBUG_1("not removed %s %s\n", key, fd->path);
159 void metadata_cache_free(FileData *fd)
162 if (fd->cached_metadata) DEBUG_1("freed %s\n", fd->path);
164 work = fd->cached_metadata;
167 GList *entry = work->data;
168 string_list_free(entry);
172 g_list_free(fd->cached_metadata);
173 fd->cached_metadata = NULL;
182 *-------------------------------------------------------------------
184 *-------------------------------------------------------------------
187 static GList *metadata_write_queue = NULL;
188 static guint metadata_write_idle_id = 0; /* event source id */
190 static void metadata_write_queue_add(FileData *fd)
192 if (!g_list_find(metadata_write_queue, fd))
194 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
197 layout_util_status_update_write_all();
200 if (metadata_write_idle_id)
202 g_source_remove(metadata_write_idle_id);
203 metadata_write_idle_id = 0;
206 if (options->metadata.confirm_after_timeout)
208 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, NULL);
213 gboolean metadata_write_queue_remove(FileData *fd)
215 g_hash_table_destroy(fd->modified_xmp);
216 fd->modified_xmp = NULL;
218 metadata_write_queue = g_list_remove(metadata_write_queue, fd);
220 file_data_increment_version(fd);
221 file_data_send_notification(fd, NOTIFY_REREAD);
225 layout_util_status_update_write_all();
229 gboolean metadata_write_queue_remove_list(GList *list)
237 FileData *fd = work->data;
239 ret = ret && metadata_write_queue_remove(fd);
244 void metadata_notify_cb(FileData *fd, NotifyType type, gpointer data)
246 if (type & (NOTIFY_REREAD | NOTIFY_CHANGE))
248 metadata_cache_free(fd);
250 if (g_list_find(metadata_write_queue, fd))
252 DEBUG_1("Notify metadata: %s %04x", fd->path, type);
253 if (!isname(fd->path))
255 /* ignore deleted files */
256 metadata_write_queue_remove(fd);
262 gboolean metadata_write_queue_confirm(gboolean force_dialog, FileUtilDoneFunc done_func, gpointer done_data)
265 GList *to_approve = NULL;
267 work = metadata_write_queue;
270 FileData *fd = work->data;
273 if (!isname(fd->path))
275 /* ignore deleted files */
276 metadata_write_queue_remove(fd);
280 if (fd->change) continue; /* another operation in progress, skip this file for now */
282 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
285 file_util_write_metadata(NULL, to_approve, NULL, force_dialog, done_func, done_data);
287 return (metadata_write_queue != NULL);
290 static gboolean metadata_write_queue_idle_cb(gpointer data)
292 metadata_write_queue_confirm(FALSE, NULL, NULL);
293 metadata_write_idle_id = 0;
297 gboolean metadata_write_perform(FileData *fd)
303 g_assert(fd->change);
305 lf = strlen(GQ_CACHE_EXT_METADATA);
306 if (fd->change->dest &&
307 g_ascii_strncasecmp(fd->change->dest + strlen(fd->change->dest) - lf, GQ_CACHE_EXT_METADATA, lf) == 0)
309 success = metadata_legacy_write(fd);
310 if (success) metadata_legacy_delete(fd, fd->change->dest);
314 /* write via exiv2 */
315 /* we can either use cached metadata which have fd->modified_xmp already applied
316 or read metadata from file and apply fd->modified_xmp
317 metadata are read also if the file was modified meanwhile */
318 exif = exif_read_fd(fd);
319 if (!exif) return FALSE;
321 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
322 exif_free_fd(fd, exif);
324 if (fd->change->dest)
325 /* this will create a FileData for the sidecar and link it to the main file
326 (we can't wait until the sidecar is discovered by directory scanning because
327 exif_read_fd is called before that and it would read the main file only and
328 store the metadata in the cache)
329 FIXME: this does not catch new sidecars created by independent external programs
331 file_data_unref(file_data_new_group(fd->change->dest));
333 if (success) metadata_legacy_delete(fd, fd->change->dest);
337 gint metadata_queue_length(void)
339 return g_list_length(metadata_write_queue);
342 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
344 const gchar **k = keys;
348 if (strcmp(key, *k) == 0) return TRUE;
354 gboolean metadata_write_revert(FileData *fd, const gchar *key)
356 if (!fd->modified_xmp) return FALSE;
358 g_hash_table_remove(fd->modified_xmp, key);
360 if (g_hash_table_size(fd->modified_xmp) == 0)
362 metadata_write_queue_remove(fd);
366 /* reread the metadata to restore the original value */
367 file_data_increment_version(fd);
368 file_data_send_notification(fd, NOTIFY_REREAD);
373 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
375 if (!fd->modified_xmp)
377 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
379 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
381 metadata_cache_remove(fd, key);
385 exif_update_metadata(fd->exif, key, values);
387 metadata_write_queue_add(fd);
388 file_data_increment_version(fd);
389 file_data_send_notification(fd, NOTIFY_METADATA);
391 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
393 GList *work = fd->sidecar_files;
397 FileData *sfd = work->data;
400 if (sfd->format_class == FORMAT_CLASS_META) continue;
402 metadata_write_list(sfd, key, values);
410 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
412 GList *list = g_list_append(NULL, g_strdup(value));
413 gboolean ret = metadata_write_list(fd, key, list);
414 string_list_free(list);
418 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
422 g_snprintf(string, sizeof(string), "%llu", (unsigned long long) value);
423 return metadata_write_string(fd, key, string);
427 *-------------------------------------------------------------------
428 * keyword / comment read/write
429 *-------------------------------------------------------------------
432 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
436 ssi = secure_open(path);
437 if (!ssi) return FALSE;
439 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
441 secure_fprintf(ssi, "[keywords]\n");
442 while (keywords && secsave_errno == SS_ERR_NONE)
444 const gchar *word = keywords->data;
445 keywords = keywords->next;
447 secure_fprintf(ssi, "%s\n", word);
449 secure_fputc(ssi, '\n');
451 secure_fprintf(ssi, "[comment]\n");
452 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
454 secure_fprintf(ssi, "#end\n");
456 return (secure_close(ssi) == 0);
459 static gboolean metadata_legacy_write(FileData *fd)
461 gboolean success = FALSE;
462 gchar *metadata_pathl;
465 gboolean have_keywords;
466 gboolean have_comment;
467 const gchar *comment;
468 GList *orig_keywords = NULL;
469 gchar *orig_comment = NULL;
471 g_assert(fd->change && fd->change->dest);
473 DEBUG_1("Saving comment: %s", fd->change->dest);
475 if (!fd->modified_xmp) return TRUE;
477 metadata_pathl = path_from_utf8(fd->change->dest);
479 have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, NULL, &keywords);
480 have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, NULL, &comment_l);
481 comment = (have_comment && comment_l) ? ((GList *)comment_l)->data : NULL;
483 if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
485 success = metadata_file_write(metadata_pathl,
486 have_keywords ? (GList *)keywords : orig_keywords,
487 have_comment ? comment : orig_comment);
489 g_free(metadata_pathl);
490 g_free(orig_comment);
491 string_list_free(orig_keywords);
496 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
500 MetadataKey key = MK_NONE;
502 GString *comment_build = NULL;
504 f = fopen(path, "r");
505 if (!f) return FALSE;
507 while (fgets(s_buf, sizeof(s_buf), f))
511 if (*ptr == '#') continue;
512 if (*ptr == '[' && key != MK_COMMENT)
514 gchar *keystr = ++ptr;
517 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
522 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
524 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
536 while (*ptr != '\n' && *ptr != '\0') ptr++;
538 if (strlen(s_buf) > 0)
540 gchar *kw = utf8_validate_or_convert(s_buf);
542 list = g_list_prepend(list, kw);
547 if (!comment_build) comment_build = g_string_new("");
548 g_string_append(comment_build, s_buf);
557 *keywords = g_list_reverse(list);
561 string_list_free(list);
569 gchar *ptr = comment_build->str;
571 /* strip leading and trailing newlines */
572 while (*ptr == '\n') ptr++;
574 while (len > 0 && ptr[len - 1] == '\n') len--;
575 if (ptr[len] == '\n') len++; /* keep the last one */
578 gchar *text = g_strndup(ptr, len);
580 *comment = utf8_validate_or_convert(text);
584 g_string_free(comment_build, TRUE);
590 static void metadata_legacy_delete(FileData *fd, const gchar *except)
592 gchar *metadata_path;
593 gchar *metadata_pathl;
596 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
597 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
599 metadata_pathl = path_from_utf8(metadata_path);
600 unlink(metadata_pathl);
601 g_free(metadata_pathl);
602 g_free(metadata_path);
606 /* without exiv2: do not delete xmp metadata because we are not able to convert it,
608 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
609 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
611 metadata_pathl = path_from_utf8(metadata_path);
612 unlink(metadata_pathl);
613 g_free(metadata_pathl);
614 g_free(metadata_path);
619 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
621 gchar *metadata_path;
622 gchar *metadata_pathl;
623 gboolean success = FALSE;
625 if (!fd) return FALSE;
627 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
628 if (!metadata_path) return FALSE;
630 metadata_pathl = path_from_utf8(metadata_path);
632 success = metadata_file_read(metadata_pathl, keywords, comment);
634 g_free(metadata_pathl);
635 g_free(metadata_path);
640 static GList *remove_duplicate_strings_from_list(GList *list)
643 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
644 GList *newlist = NULL;
648 gchar *key = work->data;
650 if (g_hash_table_lookup(hashtable, key) == NULL)
652 g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
653 newlist = g_list_prepend(newlist, key);
658 g_hash_table_destroy(hashtable);
661 return g_list_reverse(newlist);
664 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
668 const GList *cache_entry;
669 if (!fd) return NULL;
671 /* unwritten data overide everything */
672 if (fd->modified_xmp && format == METADATA_PLAIN)
674 list = g_hash_table_lookup(fd->modified_xmp, key);
675 if (list) return string_list_copy(list);
679 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
680 && (cache_entry = metadata_cache_get(fd, key)))
682 return string_list_copy(cache_entry->next);
686 Legacy metadata file is the primary source if it exists.
687 Merging the lists does not make much sense, because the existence of
688 legacy metadata file indicates that the other metadata sources are not
689 writable and thus it would not be possible to delete the keywords
690 that comes from the image file.
692 if (strcmp(key, KEYWORD_KEY) == 0)
694 if (metadata_legacy_read(fd, &list, NULL))
696 if (format == METADATA_PLAIN)
698 metadata_cache_update(fd, key, list);
703 else if (strcmp(key, COMMENT_KEY) == 0)
705 gchar *comment = NULL;
706 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
708 else if (strncmp(key, "file.", 5) == 0)
710 return g_list_append(NULL, metadata_file_info(fd, key, format));
713 else if (strncmp(key, "lua.", 4) == 0)
715 return g_list_append(NULL, metadata_lua_info(fd, key, format));
719 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
720 if (!exif) return NULL;
721 list = exif_get_metadata(exif, key, format);
722 exif_free_fd(fd, exif);
724 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
726 metadata_cache_update(fd, key, list);
732 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
734 GList *string_list = metadata_read_list(fd, key, format);
737 gchar *str = string_list->data;
738 string_list->data = NULL;
739 string_list_free(string_list);
745 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
749 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
750 if (!string) return fallback;
752 ret = g_ascii_strtoull(string, &endptr, 10);
753 if (string == endptr) ret = fallback;
758 gchar *metadata_read_rating_stars(FileData *fd)
761 gint n = metadata_read_int(fd, RATING_KEY, METADATA_PLAIN);
763 ret = convert_rating_to_stars(n);
768 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
772 gdouble deg, min, sec;
774 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
775 if (!string) return fallback;
777 deg = g_ascii_strtod(string, &endptr);
780 min = g_ascii_strtod(endptr + 1, &endptr);
782 sec = g_ascii_strtod(endptr + 1, &endptr);
787 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
789 coord = deg + min /60.0 + sec / 3600.0;
791 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
798 log_printf("unable to parse GPS coordinate '%s'\n", string);
805 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
810 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
811 if (!string) return fallback;
813 DEBUG_3("GPS_direction: %s\n", string);
814 deg = g_ascii_strtod(string, &endptr);
816 /* Expected text string is of the format e.g.:
828 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
836 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
838 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
842 return metadata_write_string(fd, key, value);
846 gchar *new_string = g_strconcat(str, value, NULL);
847 gboolean ret = metadata_write_string(fd, key, new_string);
854 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
867 min = (param * 60) - (deg * 60);
868 if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
873 else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
880 log_printf("unknown GPS parameter key '%s'\n", key);
886 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
887 metadata_write_string(fd, key, coordinate );
894 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
896 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
900 return metadata_write_list(fd, key, values);
905 list = g_list_concat(list, string_list_copy(values));
906 list = remove_duplicate_strings_from_list(list);
908 ret = metadata_write_list(fd, key, list);
909 string_list_free(list);
915 * \see find_string_in_list
917 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
919 gchar *string_casefold = g_utf8_casefold(string, -1);
923 gchar *haystack = list->data;
928 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
930 equal = (strcmp(haystack_casefold, string_casefold) == 0);
931 g_free(haystack_casefold);
935 g_free(string_casefold);
943 g_free(string_casefold);
948 * \see find_string_in_list
950 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
954 gchar *haystack = list->data;
956 if (haystack && strcmp(haystack, string) == 0)
963 } // gchar *find_string_in_list_utf...
966 * \brief Find a existent string in a list.
968 * This is a switch between find_string_in_list_utf8case and
969 * find_string_in_list_utf8nocase to search with or without case for the
970 * existence of a string.
972 * \param list The list to search in
973 * \param string The string to search for
974 * \return The string or NULL
976 * \see find_string_in_list_utf8case
977 * \see find_string_in_list_utf8nocase
979 gchar *find_string_in_list(GList *list, const gchar *string)
981 if (options->metadata.keywords_case_sensitive)
982 return find_string_in_list_utf8case(list, string);
984 return find_string_in_list_utf8nocase(list, string);
987 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
989 GList *string_to_keywords_list(const gchar *text)
992 const gchar *ptr = text;
999 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
1001 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
1007 /* trim starting and ending whitespaces */
1008 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
1009 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
1013 gchar *keyword = g_strndup(begin, l);
1015 /* only add if not already in the list */
1016 if (!find_string_in_list(list, keyword))
1017 list = g_list_append(list, keyword);
1031 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
1033 /* FIXME: do not use global keyword_tree */
1036 gboolean found = FALSE;
1037 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1041 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1042 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1049 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
1052 GList *keywords = NULL;
1055 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1057 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1059 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1063 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1067 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1069 metadata_write_list(fd, KEYWORD_KEY, keywords);
1072 string_list_free(keywords);
1078 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1081 FileDataGetMarkFunc get_mark_func;
1082 FileDataSetMarkFunc set_mark_func;
1083 gpointer mark_func_data;
1087 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1089 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1090 if (get_mark_func == meta_data_get_keyword_mark)
1092 GtkTreeIter old_kw_iter;
1093 GList *old_path = mark_func_data;
1095 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1096 (i == mark || /* release any previous connection of given mark */
1097 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1099 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
1100 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1106 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1110 path = keyword_tree_get_path(keyword_tree, kw_iter);
1111 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
1113 mark_str = g_strdup_printf("%d", (mark < 9 ? mark : -1) + 1);
1114 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1121 *-------------------------------------------------------------------
1123 *-------------------------------------------------------------------
1128 GtkTreeStore *keyword_tree;
1130 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1133 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1137 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1141 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1145 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1148 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1152 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1154 gboolean is_keyword;
1155 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1159 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1161 gchar *casefold = g_utf8_casefold(name, -1);
1162 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1163 KEYWORD_COLUMN_NAME, name,
1164 KEYWORD_COLUMN_CASEFOLD, casefold,
1165 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1169 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1171 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1172 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1173 gint ret = gtk_tree_path_compare(pa, pb);
1174 gtk_tree_path_free(pa);
1175 gtk_tree_path_free(pb);
1179 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1181 GtkTreeIter parent_a;
1182 GtkTreeIter parent_b;
1184 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1185 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1187 if (valid_pa && valid_pb)
1189 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1193 return (!valid_pa && !valid_pb); /* both are toplevel */
1197 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1201 gboolean toplevel = FALSE;
1207 parent = *parent_ptr;
1211 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1218 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
1220 casefold = g_utf8_casefold(name, -1);
1225 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
1227 if (options->metadata.keywords_case_sensitive)
1229 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1230 ret = strcmp(name, iter_name) == 0;
1235 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1236 ret = strcmp(casefold, iter_casefold) == 0;
1237 g_free(iter_casefold);
1238 } // if (options->metadata.tags_cas...
1242 if (result) *result = iter;
1245 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1252 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1255 gchar *mark, *name, *casefold;
1256 gboolean is_keyword;
1258 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1259 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1260 KEYWORD_COLUMN_NAME, &name,
1261 KEYWORD_COLUMN_CASEFOLD, &casefold,
1262 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1264 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1265 KEYWORD_COLUMN_NAME, name,
1266 KEYWORD_COLUMN_CASEFOLD, casefold,
1267 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1273 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1275 GtkTreeIter from_child;
1277 keyword_copy(keyword_tree, to, from);
1279 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1283 GtkTreeIter to_child;
1284 gtk_tree_store_append(keyword_tree, &to_child, to);
1285 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1286 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1290 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1292 keyword_copy_recursive(keyword_tree, to, from);
1293 keyword_delete(keyword_tree, from);
1296 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1299 GtkTreeIter iter = *iter_ptr;
1304 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1305 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1311 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1315 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1319 GtkTreeIter children;
1322 gchar *name = keyword_get_name(keyword_tree, &iter);
1323 if (strcmp(name, path->data) == 0) break;
1325 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1334 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1340 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1342 if (!casefold_list) return FALSE;
1344 if (!keyword_get_is_keyword(keyword_tree, &iter))
1346 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1348 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1349 return FALSE; /* this should happen only on empty helpers */
1353 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1354 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1362 if (keyword_get_is_keyword(keyword_tree, &iter))
1364 GList *work = casefold_list;
1365 gboolean found = FALSE;
1366 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1369 const gchar *casefold = work->data;
1372 if (strcmp(iter_casefold, casefold) == 0)
1378 g_free(iter_casefold);
1379 if (!found) return FALSE;
1382 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1387 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1389 if (!kw_list) return FALSE;
1391 if (!keyword_get_is_keyword(keyword_tree, &iter))
1393 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1395 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1396 return FALSE; /* this should happen only on empty helpers */
1400 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1401 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1409 if (keyword_get_is_keyword(keyword_tree, &iter))
1411 GList *work = kw_list;
1412 gboolean found = FALSE;
1413 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1416 const gchar *name = work->data;
1419 if (strcmp(iter_name, name) == 0)
1426 if (!found) return FALSE;
1429 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1434 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1437 GList *casefold_list = NULL;
1440 if (options->metadata.keywords_case_sensitive)
1442 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1449 const gchar *kw = work->data;
1452 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1455 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1457 string_list_free(casefold_list);
1463 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1465 GtkTreeIter iter = *iter_ptr;
1470 if (keyword_get_is_keyword(keyword_tree, &iter))
1472 gchar *name = keyword_get_name(keyword_tree, &iter);
1473 if (!find_string_in_list(*kw_list, name))
1475 *kw_list = g_list_append(*kw_list, name);
1483 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1488 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1490 GtkTreeIter iter = *iter_ptr;
1491 GList *kw_list = NULL;
1497 if (keyword_get_is_keyword(keyword_tree, &iter))
1499 gchar *name = keyword_get_name(keyword_tree, &iter);
1500 kw_list = g_list_append(kw_list, name);
1503 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1506 } // GList *keyword_tree_get(GtkTre...
1508 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1512 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1514 name = keyword_get_name(keyword_tree, iter);
1515 found = find_string_in_list(*kw_list, name);
1519 *kw_list = g_list_remove(*kw_list, found);
1525 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1528 keyword_tree_reset1(keyword_tree, iter, kw_list);
1530 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1534 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1535 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1539 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1543 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1544 return TRUE; /* this should happen only on empty helpers */
1548 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1549 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1553 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1555 GtkTreeIter iter = *iter_ptr;
1557 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1559 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1562 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1565 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1566 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1571 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1575 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1577 keyword_delete(keyword_tree, &child);
1580 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1582 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1585 gtk_tree_store_remove(keyword_tree, iter_ptr);
1589 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1592 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1593 if (!g_list_find(list, id))
1595 list = g_list_prepend(list, id);
1596 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1600 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1603 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1604 list = g_list_remove(list, id);
1605 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1608 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1611 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1612 return !!g_list_find(list, id);
1615 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1617 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1621 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1623 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1626 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1628 if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1630 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1635 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1637 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1640 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1642 GtkTreeIter iter = *iter_ptr;
1645 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1647 keyword_hide_in(keyword_tree, &iter, id);
1648 /* no need to check children of hidden node */
1653 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1655 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1658 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1662 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1665 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1666 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1669 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1671 GtkTreeIter iter = *iter_ptr;
1672 GList *keywords = data;
1673 gpointer id = keywords->data;
1674 keywords = keywords->next; /* hack */
1675 if (keyword_tree_is_set(model, &iter, keywords))
1680 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1681 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1688 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1690 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1691 keywords = g_list_prepend(keywords, id);
1692 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1693 keywords = g_list_delete_link(keywords, keywords);
1697 void keyword_tree_new(void)
1699 if (keyword_tree) return;
1701 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1704 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1707 gtk_tree_store_append(keyword_tree, &iter, parent);
1708 keyword_set(keyword_tree, &iter, name, is_keyword);
1712 void keyword_tree_new_default(void)
1716 if (!keyword_tree) keyword_tree_new();
1718 i1 = keyword_tree_default_append(keyword_tree, NULL, _("People"), TRUE);
1719 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1720 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1721 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1722 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1723 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1724 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1725 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Nature"), TRUE);
1726 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1727 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1728 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1729 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1730 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1731 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1732 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1733 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1734 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1735 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1736 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1737 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1738 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1739 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1740 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Art"), TRUE);
1741 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1742 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1743 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1744 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1745 i1 = keyword_tree_default_append(keyword_tree, NULL, _("City"), TRUE);
1746 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1747 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1748 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1749 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Architecture"), TRUE);
1750 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1751 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1752 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1753 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1754 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1755 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1756 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1757 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1758 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1759 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Places"), FALSE);
1760 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Conditions"), FALSE);
1761 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1762 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1763 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1764 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1765 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1766 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1767 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1768 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1769 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1770 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1771 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Photo"), FALSE);
1772 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1773 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1774 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1775 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1776 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1777 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1781 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1783 GtkTreeIter iter = *iter_ptr;
1786 GtkTreeIter children;
1790 WRITE_NL(); WRITE_STRING("<keyword ");
1791 name = keyword_get_name(keyword_tree, &iter);
1792 write_char_option(outstr, indent, "name", name);
1794 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1795 mark_str = keyword_get_mark(keyword_tree, &iter);
1796 if (mark_str && mark_str[0])
1798 write_char_option(outstr, indent, "mark", mark_str);
1801 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1805 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1807 WRITE_NL(); WRITE_STRING("</keyword>");
1813 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1817 void keyword_tree_write_config(GString *outstr, gint indent)
1820 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1823 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1825 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1828 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1831 void keyword_tree_node_disconnect_marks(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1833 GtkTreeIter iter = *iter_ptr;
1837 GtkTreeIter children;
1839 meta_data_connect_mark_with_keyword((keyword_tree), &iter, -1);
1841 if (gtk_tree_model_iter_children((keyword_tree), &children, &iter))
1843 keyword_tree_node_disconnect_marks((keyword_tree), &children);
1846 if (!gtk_tree_model_iter_next((keyword_tree), &iter)) return;
1850 void keyword_tree_disconnect_marks()
1854 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1856 keyword_tree_node_disconnect_marks(GTK_TREE_MODEL(keyword_tree), &iter);
1860 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1863 gboolean is_kw = TRUE;
1864 gchar *mark_str = NULL;
1866 while (*attribute_names)
1868 const gchar *option = *attribute_names++;
1869 const gchar *value = *attribute_values++;
1871 if (READ_CHAR_FULL("name", name)) continue;
1872 if (READ_BOOL_FULL("kw", is_kw)) continue;
1873 if (READ_CHAR_FULL("mark", mark_str)) continue;
1875 log_printf("unknown attribute %s = %s\n", option, value);
1877 if (name && name[0])
1880 /* re-use existing keyword if any */
1881 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1883 gtk_tree_store_append(keyword_tree, &iter, parent);
1885 keyword_set(keyword_tree, &iter, name, is_kw);
1889 gint i = (gint)atoi(mark_str);
1892 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1897 return gtk_tree_iter_copy(&iter);
1903 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */