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 gboolean 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_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_METADATA);
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 gboolean 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 gboolean metadata_legacy_write(FileData *fd)
273 gboolean success = FALSE;
274 gchar *metadata_pathl;
276 g_assert(fd->change && fd->change->dest);
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 gboolean 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 gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
409 gchar *metadata_path;
410 gchar *metadata_pathl;
411 gboolean success = FALSE;
413 if (!fd) return FALSE;
415 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
416 if (!metadata_path) return FALSE;
418 metadata_pathl = path_from_utf8(metadata_path);
420 success = metadata_file_read(metadata_pathl, keywords, comment);
422 g_free(metadata_pathl);
423 g_free(metadata_path);
428 static GList *remove_duplicate_strings_from_list(GList *list)
431 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
432 GList *newlist = NULL;
436 gchar *key = work->data;
438 if (g_hash_table_lookup(hashtable, key) == NULL)
440 g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
441 newlist = g_list_prepend(newlist, key);
446 g_hash_table_destroy(hashtable);
449 return g_list_reverse(newlist);
452 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
456 if (!fd) return NULL;
458 /* unwritten data overide everything */
459 if (fd->modified_xmp && format == METADATA_PLAIN)
461 list = g_hash_table_lookup(fd->modified_xmp, key);
462 if (list) return string_list_copy(list);
466 Legacy metadata file is the primary source if it exists.
467 Merging the lists does not make much sense, because the existence of
468 legacy metadata file indicates that the other metadata sources are not
469 writable and thus it would not be possible to delete the keywords
470 that comes from the image file.
472 if (strcmp(key, KEYWORD_KEY) == 0)
474 if (metadata_legacy_read(fd, &list, NULL)) return list;
476 else if (strcmp(key, COMMENT_KEY) == 0)
478 gchar *comment = NULL;
479 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
481 else if (strncmp(key, "file.", 5) == 0)
483 return g_list_append(NULL, metadata_file_info(fd, key, format));
486 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
487 if (!exif) return NULL;
488 list = exif_get_metadata(exif, key, format);
489 exif_free_fd(fd, exif);
493 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
495 GList *string_list = metadata_read_list(fd, key, format);
498 gchar *str = string_list->data;
499 string_list->data = NULL;
500 string_list_free(string_list);
506 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
510 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
511 if (!string) return fallback;
513 ret = g_ascii_strtoull(string, &endptr, 10);
514 if (string == endptr) ret = fallback;
519 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
523 gdouble deg, min, sec;
525 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
526 if (!string) return fallback;
528 deg = g_ascii_strtod(string, &endptr);
531 min = g_ascii_strtod(endptr + 1, &endptr);
533 sec = g_ascii_strtod(endptr + 1, &endptr);
538 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
540 coord = deg + min /60.0 + sec / 3600.0;
542 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
549 log_printf("unable to parse GPS coordinate '%s'\n", string);
556 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
558 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
562 return metadata_write_string(fd, key, value);
566 gchar *new_string = g_strconcat(str, value, NULL);
567 gboolean ret = metadata_write_string(fd, key, new_string);
574 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
576 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
580 return metadata_write_list(fd, key, values);
585 list = g_list_concat(list, string_list_copy(values));
586 list = remove_duplicate_strings_from_list(list);
588 ret = metadata_write_list(fd, key, list);
589 string_list_free(list);
594 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
596 gchar *string_casefold = g_utf8_casefold(string, -1);
600 gchar *haystack = list->data;
605 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
607 equal = (strcmp(haystack_casefold, string_casefold) == 0);
608 g_free(haystack_casefold);
612 g_free(string_casefold);
620 g_free(string_casefold);
625 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
627 GList *string_to_keywords_list(const gchar *text)
630 const gchar *ptr = text;
637 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
639 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
645 /* trim starting and ending whitespaces */
646 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
647 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
651 gchar *keyword = g_strndup(begin, l);
653 /* only add if not already in the list */
654 if (!find_string_in_list_utf8nocase(list, keyword))
655 list = g_list_append(list, keyword);
669 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
671 /* FIXME: do not use global keyword_tree */
674 gboolean found = FALSE;
675 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
679 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
680 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
687 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
690 GList *keywords = NULL;
693 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
695 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
697 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
701 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
705 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
707 metadata_write_list(fd, KEYWORD_KEY, keywords);
710 string_list_free(keywords);
716 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
719 FileDataGetMarkFunc get_mark_func;
720 FileDataSetMarkFunc set_mark_func;
721 gpointer mark_func_data;
725 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
727 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
728 if (get_mark_func == meta_data_get_keyword_mark)
730 GtkTreeIter old_kw_iter;
731 GList *old_path = mark_func_data;
733 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
734 (i == mark || /* release any previous connection of given mark */
735 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
737 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
738 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
744 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
748 path = keyword_tree_get_path(keyword_tree, kw_iter);
749 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
751 mark_str = g_strdup_printf("%d", mark + 1);
752 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
759 *-------------------------------------------------------------------
761 *-------------------------------------------------------------------
766 GtkTreeStore *keyword_tree;
768 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
771 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
775 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
778 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
782 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
785 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
789 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
791 gchar *casefold = g_utf8_casefold(name, -1);
792 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
793 KEYWORD_COLUMN_NAME, name,
794 KEYWORD_COLUMN_CASEFOLD, casefold,
795 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
799 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
801 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
802 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
803 gint ret = gtk_tree_path_compare(pa, pb);
804 gtk_tree_path_free(pa);
805 gtk_tree_path_free(pb);
809 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
811 GtkTreeIter parent_a;
812 GtkTreeIter parent_b;
814 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
815 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
817 if (valid_pa && valid_pb)
819 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
823 return (!valid_pa && !valid_pb); /* both are toplevel */
827 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
831 gboolean toplevel = FALSE;
837 parent = *parent_ptr;
841 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
848 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
850 casefold = g_utf8_casefold(name, -1);
855 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
857 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
858 ret = strcmp(casefold, iter_casefold) == 0;
859 g_free(iter_casefold);
863 if (result) *result = iter;
866 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
873 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
876 gchar *mark, *name, *casefold;
879 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
880 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
881 KEYWORD_COLUMN_NAME, &name,
882 KEYWORD_COLUMN_CASEFOLD, &casefold,
883 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
885 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
886 KEYWORD_COLUMN_NAME, name,
887 KEYWORD_COLUMN_CASEFOLD, casefold,
888 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
894 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
896 GtkTreeIter from_child;
898 keyword_copy(keyword_tree, to, from);
900 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
904 GtkTreeIter to_child;
905 gtk_tree_store_append(keyword_tree, &to_child, to);
906 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
907 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
911 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
913 keyword_copy_recursive(keyword_tree, to, from);
914 keyword_delete(keyword_tree, from);
917 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
920 GtkTreeIter iter = *iter_ptr;
925 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
926 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
932 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
936 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
940 GtkTreeIter children;
943 gchar *name = keyword_get_name(keyword_tree, &iter);
944 if (strcmp(name, path->data) == 0) break;
946 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
955 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
961 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
963 if (!casefold_list) return FALSE;
965 if (!keyword_get_is_keyword(keyword_tree, &iter))
967 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
969 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
970 return FALSE; /* this should happen only on empty helpers */
974 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
975 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
983 if (keyword_get_is_keyword(keyword_tree, &iter))
985 GList *work = casefold_list;
986 gboolean found = FALSE;
987 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
990 const gchar *casefold = work->data;
993 if (strcmp(iter_casefold, casefold) == 0)
999 g_free(iter_casefold);
1000 if (!found) return FALSE;
1003 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1008 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1011 GList *casefold_list = NULL;
1017 const gchar *kw = work->data;
1020 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1023 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1025 string_list_free(casefold_list);
1029 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1031 GtkTreeIter iter = *iter_ptr;
1036 if (keyword_get_is_keyword(keyword_tree, &iter))
1038 gchar *name = keyword_get_name(keyword_tree, &iter);
1039 if (!find_string_in_list_utf8nocase(*kw_list, name))
1041 *kw_list = g_list_append(*kw_list, name);
1049 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1054 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1058 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1060 name = keyword_get_name(keyword_tree, iter);
1061 found = find_string_in_list_utf8nocase(*kw_list, name);
1065 *kw_list = g_list_remove(*kw_list, found);
1071 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1074 keyword_tree_reset1(keyword_tree, iter, kw_list);
1076 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1080 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1081 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1085 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1089 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1090 return TRUE; /* this should happen only on empty helpers */
1094 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1095 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1099 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1101 GtkTreeIter iter = *iter_ptr;
1103 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1105 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1108 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1111 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1112 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1117 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1121 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1123 keyword_delete(keyword_tree, &child);
1126 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1128 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1131 gtk_tree_store_remove(keyword_tree, iter_ptr);
1135 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1138 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1139 if (!g_list_find(list, id))
1141 list = g_list_prepend(list, id);
1142 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1146 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1149 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1150 list = g_list_remove(list, id);
1151 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1154 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1157 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1158 return !!g_list_find(list, id);
1161 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1163 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1167 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1169 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1172 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1174 GtkTreeIter iter = *iter_ptr;
1177 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1179 keyword_hide_in(keyword_tree, &iter, id);
1180 /* no need to check children of hidden node */
1185 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1187 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1190 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1194 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1197 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1198 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1201 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1203 GtkTreeIter iter = *iter_ptr;
1204 GList *keywords = data;
1205 gpointer id = keywords->data;
1206 keywords = keywords->next; /* hack */
1207 if (keyword_tree_is_set(model, &iter, keywords))
1212 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1213 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1220 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1222 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1223 keywords = g_list_prepend(keywords, id);
1224 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1225 keywords = g_list_delete_link(keywords, keywords);
1229 void keyword_tree_new(void)
1231 if (keyword_tree) return;
1233 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1237 void keyword_tree_new_default(void)
1239 if (keyword_tree) return;
1243 GtkTreeIter i1, i2, i3;
1245 gtk_tree_store_append(keyword_tree, &i1, NULL);
1246 keyword_set(keyword_tree, &i1, "animal", TRUE);
1248 gtk_tree_store_append(keyword_tree, &i2, &i1);
1249 keyword_set(keyword_tree, &i2, "mammal", TRUE);
1251 gtk_tree_store_append(keyword_tree, &i3, &i2);
1252 keyword_set(keyword_tree, &i3, "dog", TRUE);
1254 gtk_tree_store_append(keyword_tree, &i3, &i2);
1255 keyword_set(keyword_tree, &i3, "cat", TRUE);
1257 gtk_tree_store_append(keyword_tree, &i2, &i1);
1258 keyword_set(keyword_tree, &i2, "insect", TRUE);
1260 gtk_tree_store_append(keyword_tree, &i3, &i2);
1261 keyword_set(keyword_tree, &i3, "fly", TRUE);
1263 gtk_tree_store_append(keyword_tree, &i3, &i2);
1264 keyword_set(keyword_tree, &i3, "dragonfly", TRUE);
1266 gtk_tree_store_append(keyword_tree, &i1, NULL);
1267 keyword_set(keyword_tree, &i1, "daytime", FALSE);
1269 gtk_tree_store_append(keyword_tree, &i2, &i1);
1270 keyword_set(keyword_tree, &i2, "morning", TRUE);
1272 gtk_tree_store_append(keyword_tree, &i2, &i1);
1273 keyword_set(keyword_tree, &i2, "noon", TRUE);
1278 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1280 GtkTreeIter iter = *iter_ptr;
1283 GtkTreeIter children;
1286 WRITE_NL(); WRITE_STRING("<keyword ");
1287 name = keyword_get_name(keyword_tree, &iter);
1288 write_char_option(outstr, indent, "name", name);
1290 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1291 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1295 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1297 WRITE_NL(); WRITE_STRING("</keyword>");
1303 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1307 void keyword_tree_write_config(GString *outstr, gint indent)
1310 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1313 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1315 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1318 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1321 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1324 gboolean is_kw = TRUE;
1326 while (*attribute_names)
1328 const gchar *option = *attribute_names++;
1329 const gchar *value = *attribute_values++;
1331 if (READ_CHAR_FULL("name", name)) continue;
1332 if (READ_BOOL_FULL("kw", is_kw)) continue;
1334 log_printf("unknown attribute %s = %s\n", option, value);
1336 if (name && name[0])
1339 /* re-use existing keyword if any */
1340 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1342 gtk_tree_store_append(keyword_tree, &iter, parent);
1344 keyword_set(keyword_tree, &iter, name, is_kw);
1346 return gtk_tree_iter_copy(&iter);
1352 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */