4 * Copyright (C) 2008 - 2009 The Geeqie Team
6 * Author: John Ellis, Laurent Monin
8 * This software is released under the GNU General Public License (GNU GPL).
9 * Please read the included file COPYING for more information.
10 * This software comes with no warranty of any kind, use at your own risk!
21 #include "secure_save.h"
22 #include "ui_fileops.h"
25 #include "filefilter.h"
35 static const gchar *group_keys[] = {KEYWORD_KEY, COMMENT_KEY, NULL}; /* tags that will be written to all files in a group */
37 static gboolean metadata_write_queue_idle_cb(gpointer data);
38 static gint metadata_legacy_write(FileData *fd);
39 static void metadata_legacy_delete(FileData *fd, const gchar *except);
44 *-------------------------------------------------------------------
46 *-------------------------------------------------------------------
49 static GList *metadata_write_queue = NULL;
50 static gint metadata_write_idle_id = -1;
52 static void metadata_write_queue_add(FileData *fd)
54 if (!g_list_find(metadata_write_queue, fd))
56 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
59 layout_status_update_write_all();
62 if (metadata_write_idle_id != -1)
64 g_source_remove(metadata_write_idle_id);
65 metadata_write_idle_id = -1;
68 if (options->metadata.confirm_after_timeout)
70 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, NULL);
75 gboolean metadata_write_queue_remove(FileData *fd)
77 g_hash_table_destroy(fd->modified_xmp);
78 fd->modified_xmp = NULL;
80 metadata_write_queue = g_list_remove(metadata_write_queue, fd);
82 file_data_increment_version(fd);
83 file_data_send_notification(fd, NOTIFY_TYPE_REREAD);
87 layout_status_update_write_all();
91 gboolean metadata_write_queue_remove_list(GList *list)
99 FileData *fd = work->data;
101 ret = ret && metadata_write_queue_remove(fd);
107 gboolean metadata_write_queue_confirm(FileUtilDoneFunc done_func, gpointer done_data)
110 GList *to_approve = NULL;
112 work = metadata_write_queue;
115 FileData *fd = work->data;
118 if (fd->change) continue; /* another operation in progress, skip this file for now */
120 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
123 file_util_write_metadata(NULL, to_approve, NULL, done_func, done_data);
125 filelist_free(to_approve);
127 return (metadata_write_queue != NULL);
130 static gboolean metadata_write_queue_idle_cb(gpointer data)
132 metadata_write_queue_confirm(NULL, NULL);
133 metadata_write_idle_id = -1;
137 gboolean metadata_write_perform(FileData *fd)
142 g_assert(fd->change);
144 if (fd->change->dest &&
145 strcmp(extension_from_path(fd->change->dest), GQ_CACHE_EXT_METADATA) == 0)
147 success = metadata_legacy_write(fd);
148 if (success) metadata_legacy_delete(fd, fd->change->dest);
152 /* write via exiv2 */
153 /* we can either use cached metadata which have fd->modified_xmp already applied
154 or read metadata from file and apply fd->modified_xmp
155 metadata are read also if the file was modified meanwhile */
156 exif = exif_read_fd(fd);
157 if (!exif) return FALSE;
159 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
160 exif_free_fd(fd, exif);
162 if (fd->change->dest)
163 /* this will create a FileData for the sidecar and link it to the main file
164 (we can't wait until the sidecar is discovered by directory scanning because
165 exif_read_fd is called before that and it would read the main file only and
166 store the metadata in the cache)
167 FIXME: this does not catch new sidecars created by independent external programs
169 file_data_unref(file_data_new_simple(fd->change->dest));
171 if (success) metadata_legacy_delete(fd, fd->change->dest);
175 gint metadata_queue_length(void)
177 return g_list_length(metadata_write_queue);
180 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
182 const gchar **k = keys;
186 if (strcmp(key, *k) == 0) return TRUE;
192 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
194 if (!fd->modified_xmp)
196 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
198 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
201 exif_update_metadata(fd->exif, key, values);
203 metadata_write_queue_add(fd);
204 file_data_increment_version(fd);
205 file_data_send_notification(fd, NOTIFY_TYPE_INTERNAL);
207 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
209 GList *work = fd->sidecar_files;
213 FileData *sfd = work->data;
216 if (filter_file_class(sfd->extension, FORMAT_CLASS_META)) continue;
218 metadata_write_list(sfd, key, values);
226 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
228 GList *list = g_list_append(NULL, g_strdup(value));
229 gboolean ret = metadata_write_list(fd, key, list);
230 string_list_free(list);
236 *-------------------------------------------------------------------
237 * keyword / comment read/write
238 *-------------------------------------------------------------------
241 static gint metadata_file_write(gchar *path, GHashTable *modified_xmp)
244 GList *keywords = g_hash_table_lookup(modified_xmp, KEYWORD_KEY);
245 GList *comment_l = g_hash_table_lookup(modified_xmp, COMMENT_KEY);
246 gchar *comment = comment_l ? comment_l->data : NULL;
248 ssi = secure_open(path);
249 if (!ssi) return FALSE;
251 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
253 secure_fprintf(ssi, "[keywords]\n");
254 while (keywords && secsave_errno == SS_ERR_NONE)
256 const gchar *word = keywords->data;
257 keywords = keywords->next;
259 secure_fprintf(ssi, "%s\n", word);
261 secure_fputc(ssi, '\n');
263 secure_fprintf(ssi, "[comment]\n");
264 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
266 secure_fprintf(ssi, "#end\n");
268 return (secure_close(ssi) == 0);
271 static gint metadata_legacy_write(FileData *fd)
273 gint success = FALSE;
275 g_assert(fd->change && fd->change->dest);
276 gchar *metadata_pathl;
278 DEBUG_1("Saving comment: %s", fd->change->dest);
280 metadata_pathl = path_from_utf8(fd->change->dest);
282 success = metadata_file_write(metadata_pathl, fd->modified_xmp);
284 g_free(metadata_pathl);
289 static gint metadata_file_read(gchar *path, GList **keywords, gchar **comment)
293 MetadataKey key = MK_NONE;
295 GString *comment_build = NULL;
297 f = fopen(path, "r");
298 if (!f) return FALSE;
300 while (fgets(s_buf, sizeof(s_buf), f))
304 if (*ptr == '#') continue;
305 if (*ptr == '[' && key != MK_COMMENT)
307 gchar *keystr = ++ptr;
310 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
315 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
317 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
329 while (*ptr != '\n' && *ptr != '\0') ptr++;
331 if (strlen(s_buf) > 0)
333 gchar *kw = utf8_validate_or_convert(s_buf);
335 list = g_list_prepend(list, kw);
340 if (!comment_build) comment_build = g_string_new("");
341 g_string_append(comment_build, s_buf);
350 *keywords = g_list_reverse(list);
354 string_list_free(list);
362 gchar *ptr = comment_build->str;
364 /* strip leading and trailing newlines */
365 while (*ptr == '\n') ptr++;
367 while (len > 0 && ptr[len - 1] == '\n') len--;
368 if (ptr[len] == '\n') len++; /* keep the last one */
371 gchar *text = g_strndup(ptr, len);
373 *comment = utf8_validate_or_convert(text);
377 g_string_free(comment_build, TRUE);
383 static void metadata_legacy_delete(FileData *fd, const gchar *except)
385 gchar *metadata_path;
386 gchar *metadata_pathl;
389 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
390 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
392 metadata_pathl = path_from_utf8(metadata_path);
393 unlink(metadata_pathl);
394 g_free(metadata_pathl);
395 g_free(metadata_path);
397 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
398 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
400 metadata_pathl = path_from_utf8(metadata_path);
401 unlink(metadata_pathl);
402 g_free(metadata_pathl);
403 g_free(metadata_path);
407 static gint metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
409 gchar *metadata_path;
410 gchar *metadata_pathl;
411 gint success = FALSE;
412 if (!fd) return FALSE;
414 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
415 if (!metadata_path) return FALSE;
417 metadata_pathl = path_from_utf8(metadata_path);
419 success = metadata_file_read(metadata_pathl, keywords, comment);
421 g_free(metadata_pathl);
422 g_free(metadata_path);
427 static GList *remove_duplicate_strings_from_list(GList *list)
430 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
431 GList *newlist = NULL;
435 gchar *key = work->data;
437 if (g_hash_table_lookup(hashtable, key) == NULL)
439 g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
440 newlist = g_list_prepend(newlist, key);
445 g_hash_table_destroy(hashtable);
448 return g_list_reverse(newlist);
451 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
455 if (!fd) return NULL;
457 /* unwritten data overide everything */
458 if (fd->modified_xmp && format == METADATA_PLAIN)
460 list = g_hash_table_lookup(fd->modified_xmp, key);
461 if (list) return string_list_copy(list);
465 Legacy metadata file is the primary source if it exists.
466 Merging the lists does not make much sense, because the existence of
467 legacy metadata file indicates that the other metadata sources are not
468 writable and thus it would not be possible to delete the keywords
469 that comes from the image file.
471 if (strcmp(key, KEYWORD_KEY) == 0)
473 if (metadata_legacy_read(fd, &list, NULL)) return list;
476 if (strcmp(key, COMMENT_KEY) == 0)
478 gchar *comment = NULL;
479 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
482 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
483 if (!exif) return NULL;
484 list = exif_get_metadata(exif, key, format);
485 exif_free_fd(fd, exif);
489 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
491 GList *string_list = metadata_read_list(fd, key, format);
494 gchar *str = string_list->data;
495 string_list->data = NULL;
496 string_list_free(string_list);
502 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
506 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
507 if (!string) return fallback;
509 ret = g_ascii_strtoull(string, &endptr, 10);
510 if (string == endptr) ret = fallback;
515 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
517 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
521 return metadata_write_string(fd, key, value);
525 gchar *new_string = g_strconcat(str, value, NULL);
526 gboolean ret = metadata_write_string(fd, key, new_string);
533 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
535 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
539 return metadata_write_list(fd, key, values);
544 list = g_list_concat(list, string_list_copy(values));
545 list = remove_duplicate_strings_from_list(list);
547 ret = metadata_write_list(fd, key, list);
548 string_list_free(list);
553 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
555 gchar *string_casefold = g_utf8_casefold(string, -1);
559 gchar *haystack = list->data;
564 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
566 equal = (strcmp(haystack_casefold, string_casefold) == 0);
567 g_free(haystack_casefold);
571 g_free(string_casefold);
579 g_free(string_casefold);
584 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
586 GList *string_to_keywords_list(const gchar *text)
589 const gchar *ptr = text;
596 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
598 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
604 /* trim starting and ending whitespaces */
605 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
606 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
610 gchar *keyword = g_strndup(begin, l);
612 /* only add if not already in the list */
613 if (!find_string_in_list_utf8nocase(list, keyword))
614 list = g_list_append(list, keyword);
628 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
631 gboolean found = FALSE;
632 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
635 GList *work = keywords;
639 gchar *kw = work->data;
642 if (strcmp(kw, data) == 0)
648 string_list_free(keywords);
653 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
655 GList *keywords = NULL;
656 gboolean found = FALSE;
657 gboolean changed = FALSE;
659 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
665 gchar *kw = work->data;
667 if (strcmp(kw, data) == 0)
673 keywords = g_list_delete_link(keywords, work);
683 keywords = g_list_append(keywords, g_strdup(data));
686 if (changed) metadata_write_list(fd, KEYWORD_KEY, keywords);
688 string_list_free(keywords);
693 *-------------------------------------------------------------------
695 *-------------------------------------------------------------------
700 GtkTreeStore *keyword_tree;
702 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
705 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
709 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
712 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
716 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
719 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
723 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
725 gchar *casefold = g_utf8_casefold(name, -1);
726 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
727 KEYWORD_COLUMN_NAME, name,
728 KEYWORD_COLUMN_CASEFOLD, casefold,
729 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
733 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
735 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
736 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
737 gint ret = gtk_tree_path_compare(pa, pb);
738 gtk_tree_path_free(pa);
739 gtk_tree_path_free(pb);
743 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
746 gchar *mark, *name, *casefold;
749 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
750 KEYWORD_COLUMN_NAME, &name,
751 KEYWORD_COLUMN_CASEFOLD, &casefold,
752 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
754 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
755 KEYWORD_COLUMN_NAME, name,
756 KEYWORD_COLUMN_CASEFOLD, casefold,
757 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
763 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
765 GtkTreeIter from_child;
767 keyword_copy(keyword_tree, to, from);
769 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
773 GtkTreeIter to_child;
774 gtk_tree_store_append(keyword_tree, &to_child, to);
775 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
776 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
780 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
782 keyword_copy_recursive(keyword_tree, to, from);
783 gtk_tree_store_remove(keyword_tree, from);
786 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
789 GtkTreeIter iter = *iter_ptr;
794 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
795 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
801 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
805 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
809 GtkTreeIter children;
812 gchar *name = keyword_get_name(keyword_tree, &iter);
813 if (strcmp(name, path->data) == 0) break;
815 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
824 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
830 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
832 if (!casefold_list) return FALSE;
838 if (keyword_get_is_keyword(keyword_tree, &iter))
840 GList *work = casefold_list;
841 gboolean found = FALSE;
842 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
845 const gchar *casefold = work->data;
848 if (strcmp(iter_casefold, casefold) == 0)
854 g_free(iter_casefold);
855 if (!found) return FALSE;
858 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
863 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
866 GList *casefold_list = NULL;
869 if (!keyword_get_is_keyword(keyword_tree, iter)) return FALSE;
874 const gchar *kw = work->data;
877 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
880 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
882 string_list_free(casefold_list);
886 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
888 GtkTreeIter iter = *iter_ptr;
893 if (keyword_get_is_keyword(keyword_tree, &iter))
895 gchar *name = keyword_get_name(keyword_tree, &iter);
896 if (!find_string_in_list_utf8nocase(*kw_list, name))
898 *kw_list = g_list_append(*kw_list, name);
906 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
911 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
915 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
917 name = keyword_get_name(keyword_tree, iter);
918 found = find_string_in_list_utf8nocase(*kw_list, name);
922 *kw_list = g_list_remove(*kw_list, found);
928 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
931 keyword_tree_reset1(keyword_tree, iter, kw_list);
933 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
937 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
938 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
942 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
946 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
947 return TRUE; /* this should happen only on empty helpers */
951 if (keyword_get_is_keyword(keyword_tree, &iter))
953 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
957 /* for helpers we have to check recursively */
958 if (!keyword_tree_check_empty_children(keyword_tree, &iter, kw_list)) return FALSE;
961 if (!gtk_tree_model_iter_next(keyword_tree, &iter))
968 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
970 GtkTreeIter iter = *iter_ptr;
972 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
974 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
977 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
980 keyword_tree_reset1(keyword_tree, &iter, kw_list);
981 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
986 void keyword_tree_new(void)
988 if (keyword_tree) return;
990 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
994 void keyword_tree_new_default(void)
996 if (keyword_tree) return;
1000 GtkTreeIter i1, i2, i3;
1002 gtk_tree_store_append(keyword_tree, &i1, NULL);
1003 keyword_set(keyword_tree, &i1, "animal", TRUE);
1005 gtk_tree_store_append(keyword_tree, &i2, &i1);
1006 keyword_set(keyword_tree, &i2, "mammal", TRUE);
1008 gtk_tree_store_append(keyword_tree, &i3, &i2);
1009 keyword_set(keyword_tree, &i3, "dog", TRUE);
1011 gtk_tree_store_append(keyword_tree, &i3, &i2);
1012 keyword_set(keyword_tree, &i3, "cat", TRUE);
1014 gtk_tree_store_append(keyword_tree, &i2, &i1);
1015 keyword_set(keyword_tree, &i2, "insect", TRUE);
1017 gtk_tree_store_append(keyword_tree, &i3, &i2);
1018 keyword_set(keyword_tree, &i3, "fly", TRUE);
1020 gtk_tree_store_append(keyword_tree, &i3, &i2);
1021 keyword_set(keyword_tree, &i3, "dragonfly", TRUE);
1023 gtk_tree_store_append(keyword_tree, &i1, NULL);
1024 keyword_set(keyword_tree, &i1, "daytime", FALSE);
1026 gtk_tree_store_append(keyword_tree, &i2, &i1);
1027 keyword_set(keyword_tree, &i2, "morning", TRUE);
1029 gtk_tree_store_append(keyword_tree, &i2, &i1);
1030 keyword_set(keyword_tree, &i2, "noon", TRUE);
1035 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1037 GtkTreeIter iter = *iter_ptr;
1040 GtkTreeIter children;
1043 WRITE_STRING("<keyword\n");
1045 name = keyword_get_name(keyword_tree, &iter);
1046 write_char_option(outstr, indent, "name", name);
1048 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1050 WRITE_STRING(">\n");
1052 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1054 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1057 WRITE_STRING("</keyword>\n");
1058 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1062 void keyword_tree_write_config(GString *outstr, gint indent)
1065 WRITE_STRING("<keyword_tree>\n");
1068 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1070 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1073 WRITE_STRING("</keyword_tree>\n");
1076 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1079 gboolean is_kw = TRUE;
1081 while (*attribute_names)
1083 const gchar *option = *attribute_names++;
1084 const gchar *value = *attribute_values++;
1086 if (READ_CHAR_FULL("name", name)) continue;
1087 if (READ_BOOL_FULL("kw", is_kw)) continue;
1089 DEBUG_1("unknown attribute %s = %s", option, value);
1091 if (name && name[0])
1094 gtk_tree_store_append(keyword_tree, &iter, parent);
1095 keyword_set(keyword_tree, &iter, name, is_kw);
1097 return gtk_tree_iter_copy(&iter);
1103 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */