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;
477 if (strcmp(key, COMMENT_KEY) == 0)
479 gchar *comment = NULL;
480 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
483 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
484 if (!exif) return NULL;
485 list = exif_get_metadata(exif, key, format);
486 exif_free_fd(fd, exif);
490 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
492 GList *string_list = metadata_read_list(fd, key, format);
495 gchar *str = string_list->data;
496 string_list->data = NULL;
497 string_list_free(string_list);
503 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
507 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
508 if (!string) return fallback;
510 ret = g_ascii_strtoull(string, &endptr, 10);
511 if (string == endptr) ret = fallback;
516 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
518 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
522 return metadata_write_string(fd, key, value);
526 gchar *new_string = g_strconcat(str, value, NULL);
527 gboolean ret = metadata_write_string(fd, key, new_string);
534 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
536 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
540 return metadata_write_list(fd, key, values);
545 list = g_list_concat(list, string_list_copy(values));
546 list = remove_duplicate_strings_from_list(list);
548 ret = metadata_write_list(fd, key, list);
549 string_list_free(list);
554 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
556 gchar *string_casefold = g_utf8_casefold(string, -1);
560 gchar *haystack = list->data;
565 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
567 equal = (strcmp(haystack_casefold, string_casefold) == 0);
568 g_free(haystack_casefold);
572 g_free(string_casefold);
580 g_free(string_casefold);
585 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
587 GList *string_to_keywords_list(const gchar *text)
590 const gchar *ptr = text;
597 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
599 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
605 /* trim starting and ending whitespaces */
606 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
607 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
611 gchar *keyword = g_strndup(begin, l);
613 /* only add if not already in the list */
614 if (!find_string_in_list_utf8nocase(list, keyword))
615 list = g_list_append(list, keyword);
629 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
631 /* FIXME: do not use global keyword_tree */
634 gboolean found = FALSE;
635 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
639 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
640 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
647 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
650 GList *keywords = NULL;
653 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
655 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
657 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
661 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
665 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
667 metadata_write_list(fd, KEYWORD_KEY, keywords);
670 string_list_free(keywords);
676 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
679 FileDataGetMarkFunc get_mark_func;
680 FileDataSetMarkFunc set_mark_func;
681 gpointer mark_func_data;
685 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
687 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
688 if (get_mark_func == meta_data_get_keyword_mark)
690 GtkTreeIter old_kw_iter;
691 GList *old_path = mark_func_data;
693 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
694 (i == mark || /* release any previous connection of given mark */
695 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
697 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
698 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
704 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
708 path = keyword_tree_get_path(keyword_tree, kw_iter);
709 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
711 mark_str = g_strdup_printf("%d", mark + 1);
712 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
719 *-------------------------------------------------------------------
721 *-------------------------------------------------------------------
726 GtkTreeStore *keyword_tree;
728 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
731 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
735 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
738 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
742 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
745 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
749 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
751 gchar *casefold = g_utf8_casefold(name, -1);
752 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
753 KEYWORD_COLUMN_NAME, name,
754 KEYWORD_COLUMN_CASEFOLD, casefold,
755 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
759 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
761 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
762 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
763 gint ret = gtk_tree_path_compare(pa, pb);
764 gtk_tree_path_free(pa);
765 gtk_tree_path_free(pb);
769 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
771 GtkTreeIter parent_a;
772 GtkTreeIter parent_b;
774 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
775 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
777 if (valid_pa && valid_pb)
779 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
783 return (!valid_pa && !valid_pb); /* both are toplevel */
787 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling)
791 gboolean toplevel = FALSE;
797 parent = *parent_ptr;
801 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
808 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
810 casefold = g_utf8_casefold(name, -1);
815 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
817 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
818 ret = strcmp(casefold, iter_casefold) == 0;
819 g_free(iter_casefold);
822 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
829 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
832 gchar *mark, *name, *casefold;
835 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
836 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
837 KEYWORD_COLUMN_NAME, &name,
838 KEYWORD_COLUMN_CASEFOLD, &casefold,
839 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
841 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
842 KEYWORD_COLUMN_NAME, name,
843 KEYWORD_COLUMN_CASEFOLD, casefold,
844 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
850 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
852 GtkTreeIter from_child;
854 keyword_copy(keyword_tree, to, from);
856 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
860 GtkTreeIter to_child;
861 gtk_tree_store_append(keyword_tree, &to_child, to);
862 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
863 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
867 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
869 keyword_copy_recursive(keyword_tree, to, from);
870 keyword_delete(keyword_tree, from);
873 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
876 GtkTreeIter iter = *iter_ptr;
881 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
882 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
888 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
892 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
896 GtkTreeIter children;
899 gchar *name = keyword_get_name(keyword_tree, &iter);
900 if (strcmp(name, path->data) == 0) break;
902 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
911 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
917 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
919 if (!casefold_list) return FALSE;
925 if (keyword_get_is_keyword(keyword_tree, &iter))
927 GList *work = casefold_list;
928 gboolean found = FALSE;
929 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
932 const gchar *casefold = work->data;
935 if (strcmp(iter_casefold, casefold) == 0)
941 g_free(iter_casefold);
942 if (!found) return FALSE;
945 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
950 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
953 GList *casefold_list = NULL;
956 if (!keyword_get_is_keyword(keyword_tree, iter)) return FALSE;
961 const gchar *kw = work->data;
964 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
967 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
969 string_list_free(casefold_list);
973 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
975 GtkTreeIter iter = *iter_ptr;
980 if (keyword_get_is_keyword(keyword_tree, &iter))
982 gchar *name = keyword_get_name(keyword_tree, &iter);
983 if (!find_string_in_list_utf8nocase(*kw_list, name))
985 *kw_list = g_list_append(*kw_list, name);
993 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
998 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1002 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1004 name = keyword_get_name(keyword_tree, iter);
1005 found = find_string_in_list_utf8nocase(*kw_list, name);
1009 *kw_list = g_list_remove(*kw_list, found);
1015 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1018 keyword_tree_reset1(keyword_tree, iter, kw_list);
1020 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1024 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1025 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1029 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1033 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1034 return TRUE; /* this should happen only on empty helpers */
1038 if (keyword_get_is_keyword(keyword_tree, &iter))
1040 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1044 /* for helpers we have to check recursively */
1045 if (!keyword_tree_check_empty_children(keyword_tree, &iter, kw_list)) return FALSE;
1048 if (!gtk_tree_model_iter_next(keyword_tree, &iter))
1055 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1057 GtkTreeIter iter = *iter_ptr;
1059 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1061 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1064 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1067 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1068 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1073 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1077 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1079 keyword_delete(keyword_tree, &child);
1082 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1084 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1087 gtk_tree_store_remove(keyword_tree, iter_ptr);
1091 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1094 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1095 if (!g_list_find(list, id))
1097 list = g_list_prepend(list, id);
1098 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1102 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1105 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1106 list = g_list_remove(list, id);
1107 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1110 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1113 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1114 return !!g_list_find(list, id);
1117 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1119 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1123 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1125 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1128 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1130 GtkTreeIter iter = *iter_ptr;
1133 if (keyword_get_is_keyword(GTK_TREE_MODEL(keyword_tree), &iter) &&
1134 !keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1136 keyword_hide_in(keyword_tree, &iter, id);
1137 /* no need to check children of hidden node */
1142 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1144 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1147 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1151 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1154 gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter);
1155 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1158 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1160 GtkTreeIter iter = *iter_ptr;
1161 GList *keywords = data;
1162 gpointer id = keywords->data;
1163 keywords = keywords->next; /* hack */
1164 if (keyword_tree_is_set(model, &iter, keywords))
1169 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1170 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1177 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1179 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1180 keywords = g_list_prepend(keywords, id);
1181 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1182 keywords = g_list_delete_link(keywords, keywords);
1186 void keyword_tree_new(void)
1188 if (keyword_tree) return;
1190 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1194 void keyword_tree_new_default(void)
1196 if (keyword_tree) return;
1200 GtkTreeIter i1, i2, i3;
1202 gtk_tree_store_append(keyword_tree, &i1, NULL);
1203 keyword_set(keyword_tree, &i1, "animal", TRUE);
1205 gtk_tree_store_append(keyword_tree, &i2, &i1);
1206 keyword_set(keyword_tree, &i2, "mammal", TRUE);
1208 gtk_tree_store_append(keyword_tree, &i3, &i2);
1209 keyword_set(keyword_tree, &i3, "dog", TRUE);
1211 gtk_tree_store_append(keyword_tree, &i3, &i2);
1212 keyword_set(keyword_tree, &i3, "cat", TRUE);
1214 gtk_tree_store_append(keyword_tree, &i2, &i1);
1215 keyword_set(keyword_tree, &i2, "insect", TRUE);
1217 gtk_tree_store_append(keyword_tree, &i3, &i2);
1218 keyword_set(keyword_tree, &i3, "fly", TRUE);
1220 gtk_tree_store_append(keyword_tree, &i3, &i2);
1221 keyword_set(keyword_tree, &i3, "dragonfly", TRUE);
1223 gtk_tree_store_append(keyword_tree, &i1, NULL);
1224 keyword_set(keyword_tree, &i1, "daytime", FALSE);
1226 gtk_tree_store_append(keyword_tree, &i2, &i1);
1227 keyword_set(keyword_tree, &i2, "morning", TRUE);
1229 gtk_tree_store_append(keyword_tree, &i2, &i1);
1230 keyword_set(keyword_tree, &i2, "noon", TRUE);
1235 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1237 GtkTreeIter iter = *iter_ptr;
1240 GtkTreeIter children;
1243 WRITE_STRING("<keyword\n");
1245 name = keyword_get_name(keyword_tree, &iter);
1246 write_char_option(outstr, indent, "name", name);
1248 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1250 WRITE_STRING(">\n");
1252 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1254 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1257 WRITE_STRING("</keyword>\n");
1258 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1262 void keyword_tree_write_config(GString *outstr, gint indent)
1265 WRITE_STRING("<keyword_tree>\n");
1268 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1270 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1273 WRITE_STRING("</keyword_tree>\n");
1276 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1279 gboolean is_kw = TRUE;
1281 while (*attribute_names)
1283 const gchar *option = *attribute_names++;
1284 const gchar *value = *attribute_values++;
1286 if (READ_CHAR_FULL("name", name)) continue;
1287 if (READ_BOOL_FULL("kw", is_kw)) continue;
1289 DEBUG_1("unknown attribute %s = %s", option, value);
1291 if (name && name[0])
1294 gtk_tree_store_append(keyword_tree, &iter, parent);
1295 keyword_set(keyword_tree, &iter, name, is_kw);
1297 return gtk_tree_iter_copy(&iter);
1303 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */