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)
302 g_assert(fd->change);
304 if (fd->change->dest &&
305 strcmp(registered_extension_from_path(fd->change->dest), GQ_CACHE_EXT_METADATA) == 0)
307 success = metadata_legacy_write(fd);
308 if (success) metadata_legacy_delete(fd, fd->change->dest);
312 /* write via exiv2 */
313 /* we can either use cached metadata which have fd->modified_xmp already applied
314 or read metadata from file and apply fd->modified_xmp
315 metadata are read also if the file was modified meanwhile */
316 exif = exif_read_fd(fd);
317 if (!exif) return FALSE;
319 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
320 exif_free_fd(fd, exif);
322 if (fd->change->dest)
323 /* this will create a FileData for the sidecar and link it to the main file
324 (we can't wait until the sidecar is discovered by directory scanning because
325 exif_read_fd is called before that and it would read the main file only and
326 store the metadata in the cache)
327 FIXME: this does not catch new sidecars created by independent external programs
329 file_data_unref(file_data_new_group(fd->change->dest));
331 if (success) metadata_legacy_delete(fd, fd->change->dest);
335 gint metadata_queue_length(void)
337 return g_list_length(metadata_write_queue);
340 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
342 const gchar **k = keys;
346 if (strcmp(key, *k) == 0) return TRUE;
352 gboolean metadata_write_revert(FileData *fd, const gchar *key)
354 if (!fd->modified_xmp) return FALSE;
356 g_hash_table_remove(fd->modified_xmp, key);
358 if (g_hash_table_size(fd->modified_xmp) == 0)
360 metadata_write_queue_remove(fd);
364 /* reread the metadata to restore the original value */
365 file_data_increment_version(fd);
366 file_data_send_notification(fd, NOTIFY_REREAD);
371 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
373 if (!fd->modified_xmp)
375 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
377 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
379 metadata_cache_remove(fd, key);
383 exif_update_metadata(fd->exif, key, values);
385 metadata_write_queue_add(fd);
386 file_data_increment_version(fd);
387 file_data_send_notification(fd, NOTIFY_METADATA);
389 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
391 GList *work = fd->sidecar_files;
395 FileData *sfd = work->data;
398 if (filter_file_class(sfd->extension, FORMAT_CLASS_META)) continue;
400 metadata_write_list(sfd, key, values);
408 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
410 GList *list = g_list_append(NULL, g_strdup(value));
411 gboolean ret = metadata_write_list(fd, key, list);
412 string_list_free(list);
416 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
420 g_snprintf(string, sizeof(string), "%llu", (unsigned long long) value);
421 return metadata_write_string(fd, key, string);
425 *-------------------------------------------------------------------
426 * keyword / comment read/write
427 *-------------------------------------------------------------------
430 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
434 ssi = secure_open(path);
435 if (!ssi) return FALSE;
437 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
439 secure_fprintf(ssi, "[keywords]\n");
440 while (keywords && secsave_errno == SS_ERR_NONE)
442 const gchar *word = keywords->data;
443 keywords = keywords->next;
445 secure_fprintf(ssi, "%s\n", word);
447 secure_fputc(ssi, '\n');
449 secure_fprintf(ssi, "[comment]\n");
450 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
452 secure_fprintf(ssi, "#end\n");
454 return (secure_close(ssi) == 0);
457 static gboolean metadata_legacy_write(FileData *fd)
459 gboolean success = FALSE;
460 gchar *metadata_pathl;
463 gboolean have_keywords;
464 gboolean have_comment;
465 const gchar *comment;
466 GList *orig_keywords = NULL;
467 gchar *orig_comment = NULL;
469 g_assert(fd->change && fd->change->dest);
471 DEBUG_1("Saving comment: %s", fd->change->dest);
473 if (!fd->modified_xmp) return TRUE;
475 metadata_pathl = path_from_utf8(fd->change->dest);
477 have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, NULL, &keywords);
478 have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, NULL, &comment_l);
479 comment = (have_comment && comment_l) ? ((GList *)comment_l)->data : NULL;
481 if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
483 success = metadata_file_write(metadata_pathl,
484 have_keywords ? (GList *)keywords : orig_keywords,
485 have_comment ? comment : orig_comment);
487 g_free(metadata_pathl);
488 g_free(orig_comment);
489 string_list_free(orig_keywords);
494 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
498 MetadataKey key = MK_NONE;
500 GString *comment_build = NULL;
502 f = fopen(path, "r");
503 if (!f) return FALSE;
505 while (fgets(s_buf, sizeof(s_buf), f))
509 if (*ptr == '#') continue;
510 if (*ptr == '[' && key != MK_COMMENT)
512 gchar *keystr = ++ptr;
515 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
520 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
522 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
534 while (*ptr != '\n' && *ptr != '\0') ptr++;
536 if (strlen(s_buf) > 0)
538 gchar *kw = utf8_validate_or_convert(s_buf);
540 list = g_list_prepend(list, kw);
545 if (!comment_build) comment_build = g_string_new("");
546 g_string_append(comment_build, s_buf);
555 *keywords = g_list_reverse(list);
559 string_list_free(list);
567 gchar *ptr = comment_build->str;
569 /* strip leading and trailing newlines */
570 while (*ptr == '\n') ptr++;
572 while (len > 0 && ptr[len - 1] == '\n') len--;
573 if (ptr[len] == '\n') len++; /* keep the last one */
576 gchar *text = g_strndup(ptr, len);
578 *comment = utf8_validate_or_convert(text);
582 g_string_free(comment_build, TRUE);
588 static void metadata_legacy_delete(FileData *fd, const gchar *except)
590 gchar *metadata_path;
591 gchar *metadata_pathl;
594 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
595 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
597 metadata_pathl = path_from_utf8(metadata_path);
598 unlink(metadata_pathl);
599 g_free(metadata_pathl);
600 g_free(metadata_path);
604 /* without exiv2: do not delete xmp metadata because we are not able to convert it,
606 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
607 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
609 metadata_pathl = path_from_utf8(metadata_path);
610 unlink(metadata_pathl);
611 g_free(metadata_pathl);
612 g_free(metadata_path);
617 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
619 gchar *metadata_path;
620 gchar *metadata_pathl;
621 gboolean success = FALSE;
623 if (!fd) return FALSE;
625 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
626 if (!metadata_path) return FALSE;
628 metadata_pathl = path_from_utf8(metadata_path);
630 success = metadata_file_read(metadata_pathl, keywords, comment);
632 g_free(metadata_pathl);
633 g_free(metadata_path);
638 static GList *remove_duplicate_strings_from_list(GList *list)
641 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
642 GList *newlist = NULL;
646 gchar *key = work->data;
648 if (g_hash_table_lookup(hashtable, key) == NULL)
650 g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
651 newlist = g_list_prepend(newlist, key);
656 g_hash_table_destroy(hashtable);
659 return g_list_reverse(newlist);
662 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
666 const GList *cache_entry;
667 if (!fd) return NULL;
669 /* unwritten data overide everything */
670 if (fd->modified_xmp && format == METADATA_PLAIN)
672 list = g_hash_table_lookup(fd->modified_xmp, key);
673 if (list) return string_list_copy(list);
677 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
678 && (cache_entry = metadata_cache_get(fd, key)))
680 return string_list_copy(cache_entry->next);
684 Legacy metadata file is the primary source if it exists.
685 Merging the lists does not make much sense, because the existence of
686 legacy metadata file indicates that the other metadata sources are not
687 writable and thus it would not be possible to delete the keywords
688 that comes from the image file.
690 if (strcmp(key, KEYWORD_KEY) == 0)
692 if (metadata_legacy_read(fd, &list, NULL))
694 if (format == METADATA_PLAIN)
696 metadata_cache_update(fd, key, list);
701 else if (strcmp(key, COMMENT_KEY) == 0)
703 gchar *comment = NULL;
704 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
706 else if (strncmp(key, "file.", 5) == 0)
708 return g_list_append(NULL, metadata_file_info(fd, key, format));
711 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
712 if (!exif) return NULL;
713 list = exif_get_metadata(exif, key, format);
714 exif_free_fd(fd, exif);
716 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
718 metadata_cache_update(fd, key, list);
724 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
726 GList *string_list = metadata_read_list(fd, key, format);
729 gchar *str = string_list->data;
730 string_list->data = NULL;
731 string_list_free(string_list);
737 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
741 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
742 if (!string) return fallback;
744 ret = g_ascii_strtoull(string, &endptr, 10);
745 if (string == endptr) ret = fallback;
750 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
754 gdouble deg, min, sec;
756 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
757 if (!string) return fallback;
759 deg = g_ascii_strtod(string, &endptr);
762 min = g_ascii_strtod(endptr + 1, &endptr);
764 sec = g_ascii_strtod(endptr + 1, &endptr);
769 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
771 coord = deg + min /60.0 + sec / 3600.0;
773 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
780 log_printf("unable to parse GPS coordinate '%s'\n", string);
787 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
792 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
793 if (!string) return fallback;
795 DEBUG_3("GPS_direction: %s\n", string);
796 deg = g_ascii_strtod(string, &endptr);
798 /* Expected text string is of the format e.g.:
810 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
818 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
820 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
824 return metadata_write_string(fd, key, value);
828 gchar *new_string = g_strconcat(str, value, NULL);
829 gboolean ret = metadata_write_string(fd, key, new_string);
836 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
849 min = (param * 60) - (deg * 60);
850 if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
855 else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
862 log_printf("unknown GPS parameter key '%s'\n", key);
868 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
869 metadata_write_string(fd, key, coordinate );
876 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
878 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
882 return metadata_write_list(fd, key, values);
887 list = g_list_concat(list, string_list_copy(values));
888 list = remove_duplicate_strings_from_list(list);
890 ret = metadata_write_list(fd, key, list);
891 string_list_free(list);
897 * \see find_string_in_list
899 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
901 gchar *string_casefold = g_utf8_casefold(string, -1);
905 gchar *haystack = list->data;
910 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
912 equal = (strcmp(haystack_casefold, string_casefold) == 0);
913 g_free(haystack_casefold);
917 g_free(string_casefold);
925 g_free(string_casefold);
930 * \see find_string_in_list
932 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
936 gchar *haystack = list->data;
938 if (haystack && strcmp(haystack, string) == 0)
945 } // gchar *find_string_in_list_utf...
948 * \brief Find a existent string in a list.
950 * This is a switch between find_string_in_list_utf8case and
951 * find_string_in_list_utf8nocase to search with or without case for the
952 * existence of a string.
954 * \param list The list to search in
955 * \param string The string to search for
956 * \return The string or NULL
958 * \see find_string_in_list_utf8case
959 * \see find_string_in_list_utf8nocase
961 gchar *find_string_in_list(GList *list, const gchar *string)
963 if (options->metadata.keywords_case_sensitive)
964 return find_string_in_list_utf8case(list, string);
966 return find_string_in_list_utf8nocase(list, string);
969 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
971 GList *string_to_keywords_list(const gchar *text)
974 const gchar *ptr = text;
981 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
983 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
989 /* trim starting and ending whitespaces */
990 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
991 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
995 gchar *keyword = g_strndup(begin, l);
997 /* only add if not already in the list */
998 if (!find_string_in_list(list, keyword))
999 list = g_list_append(list, keyword);
1013 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
1015 /* FIXME: do not use global keyword_tree */
1018 gboolean found = FALSE;
1019 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1023 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1024 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1031 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
1034 GList *keywords = NULL;
1037 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1039 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1041 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1045 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1049 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1051 metadata_write_list(fd, KEYWORD_KEY, keywords);
1054 string_list_free(keywords);
1060 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1063 FileDataGetMarkFunc get_mark_func;
1064 FileDataSetMarkFunc set_mark_func;
1065 gpointer mark_func_data;
1069 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1071 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1072 if (get_mark_func == meta_data_get_keyword_mark)
1074 GtkTreeIter old_kw_iter;
1075 GList *old_path = mark_func_data;
1077 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1078 (i == mark || /* release any previous connection of given mark */
1079 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1081 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
1082 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1088 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1092 path = keyword_tree_get_path(keyword_tree, kw_iter);
1093 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
1095 mark_str = g_strdup_printf("%d", mark + 1);
1096 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1103 *-------------------------------------------------------------------
1105 *-------------------------------------------------------------------
1110 GtkTreeStore *keyword_tree;
1112 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1115 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1119 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1123 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1127 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1130 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1134 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1136 gboolean is_keyword;
1137 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1141 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1143 gchar *casefold = g_utf8_casefold(name, -1);
1144 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1145 KEYWORD_COLUMN_NAME, name,
1146 KEYWORD_COLUMN_CASEFOLD, casefold,
1147 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1151 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1153 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1154 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1155 gint ret = gtk_tree_path_compare(pa, pb);
1156 gtk_tree_path_free(pa);
1157 gtk_tree_path_free(pb);
1161 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1163 GtkTreeIter parent_a;
1164 GtkTreeIter parent_b;
1166 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1167 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1169 if (valid_pa && valid_pb)
1171 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1175 return (!valid_pa && !valid_pb); /* both are toplevel */
1179 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1183 gboolean toplevel = FALSE;
1189 parent = *parent_ptr;
1193 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1200 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
1202 casefold = g_utf8_casefold(name, -1);
1207 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
1209 if (options->metadata.keywords_case_sensitive)
1211 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1212 ret = strcmp(name, iter_name) == 0;
1217 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1218 ret = strcmp(casefold, iter_casefold) == 0;
1219 g_free(iter_casefold);
1220 } // if (options->metadata.tags_cas...
1224 if (result) *result = iter;
1227 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1234 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1237 gchar *mark, *name, *casefold;
1238 gboolean is_keyword;
1240 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1241 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1242 KEYWORD_COLUMN_NAME, &name,
1243 KEYWORD_COLUMN_CASEFOLD, &casefold,
1244 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1246 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1247 KEYWORD_COLUMN_NAME, name,
1248 KEYWORD_COLUMN_CASEFOLD, casefold,
1249 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1255 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1257 GtkTreeIter from_child;
1259 keyword_copy(keyword_tree, to, from);
1261 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1265 GtkTreeIter to_child;
1266 gtk_tree_store_append(keyword_tree, &to_child, to);
1267 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1268 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1272 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1274 keyword_copy_recursive(keyword_tree, to, from);
1275 keyword_delete(keyword_tree, from);
1278 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1281 GtkTreeIter iter = *iter_ptr;
1286 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1287 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1293 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1297 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1301 GtkTreeIter children;
1304 gchar *name = keyword_get_name(keyword_tree, &iter);
1305 if (strcmp(name, path->data) == 0) break;
1307 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1316 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1322 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1324 if (!casefold_list) return FALSE;
1326 if (!keyword_get_is_keyword(keyword_tree, &iter))
1328 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1330 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1331 return FALSE; /* this should happen only on empty helpers */
1335 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1336 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1344 if (keyword_get_is_keyword(keyword_tree, &iter))
1346 GList *work = casefold_list;
1347 gboolean found = FALSE;
1348 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1351 const gchar *casefold = work->data;
1354 if (strcmp(iter_casefold, casefold) == 0)
1360 g_free(iter_casefold);
1361 if (!found) return FALSE;
1364 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1369 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1371 if (!kw_list) return FALSE;
1373 if (!keyword_get_is_keyword(keyword_tree, &iter))
1375 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1377 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1378 return FALSE; /* this should happen only on empty helpers */
1382 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1383 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1391 if (keyword_get_is_keyword(keyword_tree, &iter))
1393 GList *work = kw_list;
1394 gboolean found = FALSE;
1395 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1398 const gchar *name = work->data;
1401 if (strcmp(iter_name, name) == 0)
1408 if (!found) return FALSE;
1411 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1416 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1419 GList *casefold_list = NULL;
1422 if (options->metadata.keywords_case_sensitive)
1424 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1431 const gchar *kw = work->data;
1434 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1437 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1439 string_list_free(casefold_list);
1445 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1447 GtkTreeIter iter = *iter_ptr;
1452 if (keyword_get_is_keyword(keyword_tree, &iter))
1454 gchar *name = keyword_get_name(keyword_tree, &iter);
1455 if (!find_string_in_list(*kw_list, name))
1457 *kw_list = g_list_append(*kw_list, name);
1465 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1470 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1472 GtkTreeIter iter = *iter_ptr;
1473 GList *kw_list = NULL;
1479 if (keyword_get_is_keyword(keyword_tree, &iter))
1481 gchar *name = keyword_get_name(keyword_tree, &iter);
1482 kw_list = g_list_append(kw_list, name);
1485 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1488 } // GList *keyword_tree_get(GtkTre...
1490 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1494 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1496 name = keyword_get_name(keyword_tree, iter);
1497 found = find_string_in_list(*kw_list, name);
1501 *kw_list = g_list_remove(*kw_list, found);
1507 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1510 keyword_tree_reset1(keyword_tree, iter, kw_list);
1512 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1516 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1517 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1521 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1525 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1526 return TRUE; /* this should happen only on empty helpers */
1530 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1531 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1535 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1537 GtkTreeIter iter = *iter_ptr;
1539 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1541 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1544 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1547 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1548 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1553 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1557 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1559 keyword_delete(keyword_tree, &child);
1562 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1564 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1567 gtk_tree_store_remove(keyword_tree, iter_ptr);
1571 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1574 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1575 if (!g_list_find(list, id))
1577 list = g_list_prepend(list, id);
1578 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1582 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1585 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1586 list = g_list_remove(list, id);
1587 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1590 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1593 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1594 return !!g_list_find(list, id);
1597 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1599 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1603 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1605 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1608 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1610 GtkTreeIter iter = *iter_ptr;
1613 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1615 keyword_hide_in(keyword_tree, &iter, id);
1616 /* no need to check children of hidden node */
1621 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1623 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1626 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1630 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1633 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1634 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1637 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1639 GtkTreeIter iter = *iter_ptr;
1640 GList *keywords = data;
1641 gpointer id = keywords->data;
1642 keywords = keywords->next; /* hack */
1643 if (keyword_tree_is_set(model, &iter, keywords))
1648 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1649 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1656 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1658 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1659 keywords = g_list_prepend(keywords, id);
1660 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1661 keywords = g_list_delete_link(keywords, keywords);
1665 void keyword_tree_new(void)
1667 if (keyword_tree) return;
1669 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1672 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1675 gtk_tree_store_append(keyword_tree, &iter, parent);
1676 keyword_set(keyword_tree, &iter, name, is_keyword);
1680 void keyword_tree_new_default(void)
1684 if (!keyword_tree) keyword_tree_new();
1686 i1 = keyword_tree_default_append(keyword_tree, NULL, _("People"), TRUE);
1687 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1688 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1689 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1690 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1691 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1692 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1693 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Nature"), TRUE);
1694 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1695 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1696 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1697 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1698 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1699 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1700 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1701 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1702 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1703 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1704 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1705 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1706 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1707 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1708 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Art"), TRUE);
1709 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1710 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1711 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1712 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1713 i1 = keyword_tree_default_append(keyword_tree, NULL, _("City"), TRUE);
1714 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1715 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1716 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1717 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Architecture"), TRUE);
1718 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1719 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1720 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1721 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1722 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1723 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1724 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1725 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1726 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1727 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Places"), FALSE);
1728 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Conditions"), FALSE);
1729 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1730 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1731 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1732 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1733 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1734 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1735 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1736 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1737 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1738 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1739 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Photo"), FALSE);
1740 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1741 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1742 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1743 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1744 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1745 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1749 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1751 GtkTreeIter iter = *iter_ptr;
1754 GtkTreeIter children;
1758 WRITE_NL(); WRITE_STRING("<keyword ");
1759 name = keyword_get_name(keyword_tree, &iter);
1760 write_char_option(outstr, indent, "name", name);
1762 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1763 mark_str = keyword_get_mark(keyword_tree, &iter);
1764 if (mark_str && mark_str[0])
1766 write_char_option(outstr, indent, "mark", mark_str);
1769 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1773 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1775 WRITE_NL(); WRITE_STRING("</keyword>");
1781 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1785 void keyword_tree_write_config(GString *outstr, gint indent)
1788 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1791 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1793 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1796 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1799 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1802 gboolean is_kw = TRUE;
1803 gchar *mark_str = NULL;
1805 while (*attribute_names)
1807 const gchar *option = *attribute_names++;
1808 const gchar *value = *attribute_values++;
1810 if (READ_CHAR_FULL("name", name)) continue;
1811 if (READ_BOOL_FULL("kw", is_kw)) continue;
1812 if (READ_CHAR_FULL("mark", mark_str)) continue;
1814 log_printf("unknown attribute %s = %s\n", option, value);
1816 if (name && name[0])
1819 /* re-use existing keyword if any */
1820 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1822 gtk_tree_store_append(keyword_tree, &iter, parent);
1824 keyword_set(keyword_tree, &iter, name, is_kw);
1828 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1829 &iter, (gint)atoi(mark_str) - 1);
1833 return gtk_tree_iter_copy(&iter);
1839 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */