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_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1122 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1126 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1128 gboolean is_keyword;
1129 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1133 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1135 gchar *casefold = g_utf8_casefold(name, -1);
1136 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1137 KEYWORD_COLUMN_NAME, name,
1138 KEYWORD_COLUMN_CASEFOLD, casefold,
1139 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1143 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1145 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1146 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1147 gint ret = gtk_tree_path_compare(pa, pb);
1148 gtk_tree_path_free(pa);
1149 gtk_tree_path_free(pb);
1153 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1155 GtkTreeIter parent_a;
1156 GtkTreeIter parent_b;
1158 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1159 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1161 if (valid_pa && valid_pb)
1163 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1167 return (!valid_pa && !valid_pb); /* both are toplevel */
1171 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1175 gboolean toplevel = FALSE;
1181 parent = *parent_ptr;
1185 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1192 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
1194 casefold = g_utf8_casefold(name, -1);
1199 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
1201 if (options->metadata.keywords_case_sensitive)
1203 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1204 ret = strcmp(name, iter_name) == 0;
1209 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1210 ret = strcmp(casefold, iter_casefold) == 0;
1211 g_free(iter_casefold);
1212 } // if (options->metadata.tags_cas...
1216 if (result) *result = iter;
1219 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1226 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1229 gchar *mark, *name, *casefold;
1230 gboolean is_keyword;
1232 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1233 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1234 KEYWORD_COLUMN_NAME, &name,
1235 KEYWORD_COLUMN_CASEFOLD, &casefold,
1236 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1238 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1239 KEYWORD_COLUMN_NAME, name,
1240 KEYWORD_COLUMN_CASEFOLD, casefold,
1241 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1247 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1249 GtkTreeIter from_child;
1251 keyword_copy(keyword_tree, to, from);
1253 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1257 GtkTreeIter to_child;
1258 gtk_tree_store_append(keyword_tree, &to_child, to);
1259 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1260 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1264 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1266 keyword_copy_recursive(keyword_tree, to, from);
1267 keyword_delete(keyword_tree, from);
1270 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1273 GtkTreeIter iter = *iter_ptr;
1278 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1279 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1285 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1289 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1293 GtkTreeIter children;
1296 gchar *name = keyword_get_name(keyword_tree, &iter);
1297 if (strcmp(name, path->data) == 0) break;
1299 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1308 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1314 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1316 if (!casefold_list) return FALSE;
1318 if (!keyword_get_is_keyword(keyword_tree, &iter))
1320 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1322 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1323 return FALSE; /* this should happen only on empty helpers */
1327 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1328 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1336 if (keyword_get_is_keyword(keyword_tree, &iter))
1338 GList *work = casefold_list;
1339 gboolean found = FALSE;
1340 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1343 const gchar *casefold = work->data;
1346 if (strcmp(iter_casefold, casefold) == 0)
1352 g_free(iter_casefold);
1353 if (!found) return FALSE;
1356 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1361 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1363 if (!kw_list) return FALSE;
1365 if (!keyword_get_is_keyword(keyword_tree, &iter))
1367 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1369 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1370 return FALSE; /* this should happen only on empty helpers */
1374 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1375 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1383 if (keyword_get_is_keyword(keyword_tree, &iter))
1385 GList *work = kw_list;
1386 gboolean found = FALSE;
1387 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1390 const gchar *name = work->data;
1393 if (strcmp(iter_name, name) == 0)
1400 if (!found) return FALSE;
1403 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1408 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1411 GList *casefold_list = NULL;
1414 if (options->metadata.keywords_case_sensitive)
1416 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1423 const gchar *kw = work->data;
1426 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1429 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1431 string_list_free(casefold_list);
1437 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1439 GtkTreeIter iter = *iter_ptr;
1444 if (keyword_get_is_keyword(keyword_tree, &iter))
1446 gchar *name = keyword_get_name(keyword_tree, &iter);
1447 if (!find_string_in_list(*kw_list, name))
1449 *kw_list = g_list_append(*kw_list, name);
1457 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1462 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1464 GtkTreeIter iter = *iter_ptr;
1465 GList *kw_list = NULL;
1471 if (keyword_get_is_keyword(keyword_tree, &iter))
1473 gchar *name = keyword_get_name(keyword_tree, &iter);
1474 kw_list = g_list_append(kw_list, name);
1477 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1480 } // GList *keyword_tree_get(GtkTre...
1482 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1486 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1488 name = keyword_get_name(keyword_tree, iter);
1489 found = find_string_in_list(*kw_list, name);
1493 *kw_list = g_list_remove(*kw_list, found);
1499 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1502 keyword_tree_reset1(keyword_tree, iter, kw_list);
1504 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1508 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1509 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1513 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1517 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1518 return TRUE; /* this should happen only on empty helpers */
1522 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1523 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1527 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1529 GtkTreeIter iter = *iter_ptr;
1531 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1533 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1536 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1539 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1540 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1545 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1549 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1551 keyword_delete(keyword_tree, &child);
1554 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1556 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1559 gtk_tree_store_remove(keyword_tree, iter_ptr);
1563 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1566 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1567 if (!g_list_find(list, id))
1569 list = g_list_prepend(list, id);
1570 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1574 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1577 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1578 list = g_list_remove(list, id);
1579 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1582 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1585 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1586 return !!g_list_find(list, id);
1589 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1591 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1595 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1597 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1600 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1602 GtkTreeIter iter = *iter_ptr;
1605 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1607 keyword_hide_in(keyword_tree, &iter, id);
1608 /* no need to check children of hidden node */
1613 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1615 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1618 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1622 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1625 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1626 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1629 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1631 GtkTreeIter iter = *iter_ptr;
1632 GList *keywords = data;
1633 gpointer id = keywords->data;
1634 keywords = keywords->next; /* hack */
1635 if (keyword_tree_is_set(model, &iter, keywords))
1640 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1641 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1648 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1650 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1651 keywords = g_list_prepend(keywords, id);
1652 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1653 keywords = g_list_delete_link(keywords, keywords);
1657 void keyword_tree_new(void)
1659 if (keyword_tree) return;
1661 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1664 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1667 gtk_tree_store_append(keyword_tree, &iter, parent);
1668 keyword_set(keyword_tree, &iter, name, is_keyword);
1672 void keyword_tree_new_default(void)
1676 if (!keyword_tree) keyword_tree_new();
1678 i1 = keyword_tree_default_append(keyword_tree, NULL, _("People"), TRUE);
1679 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1680 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1681 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1682 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1683 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1684 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1685 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Nature"), TRUE);
1686 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1687 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1688 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1689 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1690 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1691 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1692 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1693 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1694 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1695 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1696 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1697 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1698 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1699 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1700 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Art"), TRUE);
1701 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1702 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1703 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1704 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1705 i1 = keyword_tree_default_append(keyword_tree, NULL, _("City"), TRUE);
1706 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1707 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1708 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1709 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Architecture"), TRUE);
1710 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1711 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1712 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1713 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1714 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1715 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1716 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1717 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1718 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1719 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Places"), FALSE);
1720 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Conditions"), FALSE);
1721 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1722 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1723 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1724 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1725 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1726 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1727 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1728 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1729 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1730 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1731 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Photo"), FALSE);
1732 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1733 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1734 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1735 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1736 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1737 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1741 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1743 GtkTreeIter iter = *iter_ptr;
1746 GtkTreeIter children;
1749 WRITE_NL(); WRITE_STRING("<keyword ");
1750 name = keyword_get_name(keyword_tree, &iter);
1751 write_char_option(outstr, indent, "name", name);
1753 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1754 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1758 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1760 WRITE_NL(); WRITE_STRING("</keyword>");
1766 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1770 void keyword_tree_write_config(GString *outstr, gint indent)
1773 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1776 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1778 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1781 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1784 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1787 gboolean is_kw = TRUE;
1789 while (*attribute_names)
1791 const gchar *option = *attribute_names++;
1792 const gchar *value = *attribute_values++;
1794 if (READ_CHAR_FULL("name", name)) continue;
1795 if (READ_BOOL_FULL("kw", is_kw)) continue;
1797 log_printf("unknown attribute %s = %s\n", option, value);
1799 if (name && name[0])
1802 /* re-use existing keyword if any */
1803 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1805 gtk_tree_store_append(keyword_tree, &iter, parent);
1807 keyword_set(keyword_tree, &iter, name, is_kw);
1809 return gtk_tree_iter_copy(&iter);
1815 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */