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(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_append_list(FileData *fd, const gchar *key, const GList *values)
838 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
842 return metadata_write_list(fd, key, values);
847 list = g_list_concat(list, string_list_copy(values));
848 list = remove_duplicate_strings_from_list(list);
850 ret = metadata_write_list(fd, key, list);
851 string_list_free(list);
857 * \see find_string_in_list
859 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
861 gchar *string_casefold = g_utf8_casefold(string, -1);
865 gchar *haystack = list->data;
870 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
872 equal = (strcmp(haystack_casefold, string_casefold) == 0);
873 g_free(haystack_casefold);
877 g_free(string_casefold);
885 g_free(string_casefold);
890 * \see find_string_in_list
892 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
896 gchar *haystack = list->data;
898 if (haystack && strcmp(haystack, string) == 0)
905 } // gchar *find_string_in_list_utf...
908 * \brief Find a existent string in a list.
910 * This is a switch between find_string_in_list_utf8case and
911 * find_string_in_list_utf8nocase to search with or without case for the
912 * existence of a string.
914 * \param list The list to search in
915 * \param string The string to search for
916 * \return The string or NULL
918 * \see find_string_in_list_utf8case
919 * \see find_string_in_list_utf8nocase
921 gchar *find_string_in_list(GList *list, const gchar *string)
923 if (options->metadata.keywords_case_sensitive)
924 return find_string_in_list_utf8case(list, string);
926 return find_string_in_list_utf8nocase(list, string);
929 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
931 GList *string_to_keywords_list(const gchar *text)
934 const gchar *ptr = text;
941 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
943 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
949 /* trim starting and ending whitespaces */
950 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
951 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
955 gchar *keyword = g_strndup(begin, l);
957 /* only add if not already in the list */
958 if (!find_string_in_list(list, keyword))
959 list = g_list_append(list, keyword);
973 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
975 /* FIXME: do not use global keyword_tree */
978 gboolean found = FALSE;
979 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
983 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
984 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
991 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
994 GList *keywords = NULL;
997 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
999 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1001 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1005 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1009 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1011 metadata_write_list(fd, KEYWORD_KEY, keywords);
1014 string_list_free(keywords);
1020 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1023 FileDataGetMarkFunc get_mark_func;
1024 FileDataSetMarkFunc set_mark_func;
1025 gpointer mark_func_data;
1029 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1031 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1032 if (get_mark_func == meta_data_get_keyword_mark)
1034 GtkTreeIter old_kw_iter;
1035 GList *old_path = mark_func_data;
1037 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1038 (i == mark || /* release any previous connection of given mark */
1039 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1041 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
1042 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1048 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1052 path = keyword_tree_get_path(keyword_tree, kw_iter);
1053 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
1055 mark_str = g_strdup_printf("%d", mark + 1);
1056 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1063 *-------------------------------------------------------------------
1065 *-------------------------------------------------------------------
1070 GtkTreeStore *keyword_tree;
1072 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1075 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1079 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1082 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1086 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1088 gboolean is_keyword;
1089 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1093 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1095 gchar *casefold = g_utf8_casefold(name, -1);
1096 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1097 KEYWORD_COLUMN_NAME, name,
1098 KEYWORD_COLUMN_CASEFOLD, casefold,
1099 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1103 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1105 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1106 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1107 gint ret = gtk_tree_path_compare(pa, pb);
1108 gtk_tree_path_free(pa);
1109 gtk_tree_path_free(pb);
1113 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1115 GtkTreeIter parent_a;
1116 GtkTreeIter parent_b;
1118 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1119 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1121 if (valid_pa && valid_pb)
1123 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1127 return (!valid_pa && !valid_pb); /* both are toplevel */
1131 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1135 gboolean toplevel = FALSE;
1141 parent = *parent_ptr;
1145 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1152 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
1154 casefold = g_utf8_casefold(name, -1);
1159 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
1161 if (options->metadata.keywords_case_sensitive)
1163 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1164 ret = strcmp(name, iter_name) == 0;
1169 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1170 ret = strcmp(casefold, iter_casefold) == 0;
1171 g_free(iter_casefold);
1172 } // if (options->metadata.tags_cas...
1176 if (result) *result = iter;
1179 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1186 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1189 gchar *mark, *name, *casefold;
1190 gboolean is_keyword;
1192 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1193 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1194 KEYWORD_COLUMN_NAME, &name,
1195 KEYWORD_COLUMN_CASEFOLD, &casefold,
1196 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1198 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1199 KEYWORD_COLUMN_NAME, name,
1200 KEYWORD_COLUMN_CASEFOLD, casefold,
1201 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1207 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1209 GtkTreeIter from_child;
1211 keyword_copy(keyword_tree, to, from);
1213 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1217 GtkTreeIter to_child;
1218 gtk_tree_store_append(keyword_tree, &to_child, to);
1219 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1220 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1224 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1226 keyword_copy_recursive(keyword_tree, to, from);
1227 keyword_delete(keyword_tree, from);
1230 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1233 GtkTreeIter iter = *iter_ptr;
1238 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1239 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1245 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1249 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1253 GtkTreeIter children;
1256 gchar *name = keyword_get_name(keyword_tree, &iter);
1257 if (strcmp(name, path->data) == 0) break;
1259 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1268 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1274 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1276 if (!casefold_list) return FALSE;
1278 if (!keyword_get_is_keyword(keyword_tree, &iter))
1280 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1282 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1283 return FALSE; /* this should happen only on empty helpers */
1287 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1288 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1296 if (keyword_get_is_keyword(keyword_tree, &iter))
1298 GList *work = casefold_list;
1299 gboolean found = FALSE;
1300 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1303 const gchar *casefold = work->data;
1306 if (strcmp(iter_casefold, casefold) == 0)
1312 g_free(iter_casefold);
1313 if (!found) return FALSE;
1316 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1321 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1323 if (!kw_list) return FALSE;
1325 if (!keyword_get_is_keyword(keyword_tree, &iter))
1327 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1329 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1330 return FALSE; /* this should happen only on empty helpers */
1334 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1335 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1343 if (keyword_get_is_keyword(keyword_tree, &iter))
1345 GList *work = kw_list;
1346 gboolean found = FALSE;
1347 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1350 const gchar *name = work->data;
1353 if (strcmp(iter_name, name) == 0)
1360 if (!found) return FALSE;
1363 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1368 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1371 GList *casefold_list = NULL;
1374 if (options->metadata.keywords_case_sensitive)
1376 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1383 const gchar *kw = work->data;
1386 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1389 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1391 string_list_free(casefold_list);
1397 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1399 GtkTreeIter iter = *iter_ptr;
1404 if (keyword_get_is_keyword(keyword_tree, &iter))
1406 gchar *name = keyword_get_name(keyword_tree, &iter);
1407 if (!find_string_in_list(*kw_list, name))
1409 *kw_list = g_list_append(*kw_list, name);
1417 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1422 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1424 GtkTreeIter iter = *iter_ptr;
1425 GList *kw_list = NULL;
1431 if (keyword_get_is_keyword(keyword_tree, &iter))
1433 gchar *name = keyword_get_name(keyword_tree, &iter);
1434 kw_list = g_list_append(kw_list, name);
1437 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1440 } // GList *keyword_tree_get(GtkTre...
1442 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1446 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1448 name = keyword_get_name(keyword_tree, iter);
1449 found = find_string_in_list(*kw_list, name);
1453 *kw_list = g_list_remove(*kw_list, found);
1459 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1462 keyword_tree_reset1(keyword_tree, iter, kw_list);
1464 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1468 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1469 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1473 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1477 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1478 return TRUE; /* this should happen only on empty helpers */
1482 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1483 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1487 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1489 GtkTreeIter iter = *iter_ptr;
1491 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1493 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1496 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1499 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1500 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1505 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1509 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1511 keyword_delete(keyword_tree, &child);
1514 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1516 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1519 gtk_tree_store_remove(keyword_tree, iter_ptr);
1523 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1526 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1527 if (!g_list_find(list, id))
1529 list = g_list_prepend(list, id);
1530 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1534 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1537 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1538 list = g_list_remove(list, id);
1539 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1542 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1545 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1546 return !!g_list_find(list, id);
1549 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1551 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1555 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1557 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1560 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1562 GtkTreeIter iter = *iter_ptr;
1565 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1567 keyword_hide_in(keyword_tree, &iter, id);
1568 /* no need to check children of hidden node */
1573 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1575 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1578 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1582 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1585 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1586 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1589 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1591 GtkTreeIter iter = *iter_ptr;
1592 GList *keywords = data;
1593 gpointer id = keywords->data;
1594 keywords = keywords->next; /* hack */
1595 if (keyword_tree_is_set(model, &iter, keywords))
1600 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1601 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1608 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1610 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1611 keywords = g_list_prepend(keywords, id);
1612 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1613 keywords = g_list_delete_link(keywords, keywords);
1617 void keyword_tree_new(void)
1619 if (keyword_tree) return;
1621 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1624 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1627 gtk_tree_store_append(keyword_tree, &iter, parent);
1628 keyword_set(keyword_tree, &iter, name, is_keyword);
1632 void keyword_tree_new_default(void)
1636 if (!keyword_tree) keyword_tree_new();
1638 i1 = keyword_tree_default_append(keyword_tree, NULL, _("People"), TRUE);
1639 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1640 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1641 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1642 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1643 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1644 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1645 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Nature"), TRUE);
1646 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1647 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1648 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1649 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1650 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1651 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1652 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1653 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1654 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1655 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1656 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1657 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1658 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1659 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1660 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Art"), TRUE);
1661 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1662 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1663 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1664 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1665 i1 = keyword_tree_default_append(keyword_tree, NULL, _("City"), TRUE);
1666 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1667 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1668 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1669 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Architecture"), TRUE);
1670 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1671 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1672 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1673 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1674 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1675 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1676 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1677 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1678 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1679 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Places"), FALSE);
1680 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Conditions"), FALSE);
1681 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1682 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1683 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1684 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1685 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1686 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1687 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1688 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1689 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1690 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1691 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Photo"), FALSE);
1692 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1693 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1694 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1695 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1696 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1697 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1701 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1703 GtkTreeIter iter = *iter_ptr;
1706 GtkTreeIter children;
1709 WRITE_NL(); WRITE_STRING("<keyword ");
1710 name = keyword_get_name(keyword_tree, &iter);
1711 write_char_option(outstr, indent, "name", name);
1713 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1714 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1718 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1720 WRITE_NL(); WRITE_STRING("</keyword>");
1726 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1730 void keyword_tree_write_config(GString *outstr, gint indent)
1733 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1736 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1738 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1741 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1744 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1747 gboolean is_kw = TRUE;
1749 while (*attribute_names)
1751 const gchar *option = *attribute_names++;
1752 const gchar *value = *attribute_values++;
1754 if (READ_CHAR_FULL("name", name)) continue;
1755 if (READ_BOOL_FULL("kw", is_kw)) continue;
1757 log_printf("unknown attribute %s = %s\n", option, value);
1759 if (name && name[0])
1762 /* re-use existing keyword if any */
1763 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1765 gtk_tree_store_append(keyword_tree, &iter, parent);
1767 keyword_set(keyword_tree, &iter, name, is_kw);
1769 return gtk_tree_iter_copy(&iter);
1775 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */