4 * Copyright (C) 2008 - 2009 The Geeqie Team
6 * Author: John Ellis, Laurent Monin
8 * This software is released under the GNU General Public License (GNU GPL).
9 * Please read the included file COPYING for more information.
10 * This software comes with no warranty of any kind, use at your own risk!
21 #include "secure_save.h"
22 #include "ui_fileops.h"
25 #include "filefilter.h"
35 static const gchar *group_keys[] = {KEYWORD_KEY, COMMENT_KEY, NULL}; /* tags that will be written to all files in a group */
37 static gboolean metadata_write_queue_idle_cb(gpointer data);
38 static gint metadata_legacy_write(FileData *fd);
39 static void metadata_legacy_delete(FileData *fd, const gchar *except);
44 *-------------------------------------------------------------------
46 *-------------------------------------------------------------------
49 static GList *metadata_write_queue = NULL;
50 static gint metadata_write_idle_id = -1;
52 static void metadata_write_queue_add(FileData *fd)
54 if (!g_list_find(metadata_write_queue, fd))
56 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
59 layout_status_update_write_all();
62 if (metadata_write_idle_id != -1)
64 g_source_remove(metadata_write_idle_id);
65 metadata_write_idle_id = -1;
68 if (options->metadata.confirm_after_timeout)
70 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, NULL);
75 gboolean metadata_write_queue_remove(FileData *fd)
77 g_hash_table_destroy(fd->modified_xmp);
78 fd->modified_xmp = NULL;
80 metadata_write_queue = g_list_remove(metadata_write_queue, fd);
82 file_data_increment_version(fd);
83 file_data_send_notification(fd, NOTIFY_TYPE_REREAD);
87 layout_status_update_write_all();
91 gboolean metadata_write_queue_remove_list(GList *list)
99 FileData *fd = work->data;
101 ret = ret && metadata_write_queue_remove(fd);
107 gboolean metadata_write_queue_confirm(FileUtilDoneFunc done_func, gpointer done_data)
110 GList *to_approve = NULL;
112 work = metadata_write_queue;
115 FileData *fd = work->data;
118 if (fd->change) continue; /* another operation in progress, skip this file for now */
120 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
123 file_util_write_metadata(NULL, to_approve, NULL, done_func, done_data);
125 filelist_free(to_approve);
127 return (metadata_write_queue != NULL);
130 static gboolean metadata_write_queue_idle_cb(gpointer data)
132 metadata_write_queue_confirm(NULL, NULL);
133 metadata_write_idle_id = -1;
137 gboolean metadata_write_perform(FileData *fd)
142 g_assert(fd->change);
144 if (fd->change->dest &&
145 strcmp(extension_from_path(fd->change->dest), GQ_CACHE_EXT_METADATA) == 0)
147 success = metadata_legacy_write(fd);
148 if (success) metadata_legacy_delete(fd, fd->change->dest);
152 /* write via exiv2 */
153 /* we can either use cached metadata which have fd->modified_xmp already applied
154 or read metadata from file and apply fd->modified_xmp
155 metadata are read also if the file was modified meanwhile */
156 exif = exif_read_fd(fd);
157 if (!exif) return FALSE;
159 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
160 exif_free_fd(fd, exif);
162 if (fd->change->dest)
163 /* this will create a FileData for the sidecar and link it to the main file
164 (we can't wait until the sidecar is discovered by directory scanning because
165 exif_read_fd is called before that and it would read the main file only and
166 store the metadata in the cache)
167 FIXME: this does not catch new sidecars created by independent external programs
169 file_data_unref(file_data_new_simple(fd->change->dest));
171 if (success) metadata_legacy_delete(fd, fd->change->dest);
175 gint metadata_queue_length(void)
177 return g_list_length(metadata_write_queue);
180 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
182 const gchar **k = keys;
186 if (strcmp(key, *k) == 0) return TRUE;
192 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
194 if (!fd->modified_xmp)
196 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
198 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
201 exif_update_metadata(fd->exif, key, values);
203 metadata_write_queue_add(fd);
204 file_data_increment_version(fd);
205 file_data_send_notification(fd, NOTIFY_TYPE_INTERNAL);
207 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
209 GList *work = fd->sidecar_files;
213 FileData *sfd = work->data;
216 if (filter_file_class(sfd->extension, FORMAT_CLASS_META)) continue;
218 metadata_write_list(sfd, key, values);
226 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
228 GList *list = g_list_append(NULL, g_strdup(value));
229 gboolean ret = metadata_write_list(fd, key, list);
230 string_list_free(list);
236 *-------------------------------------------------------------------
237 * keyword / comment read/write
238 *-------------------------------------------------------------------
241 static gint metadata_file_write(gchar *path, GHashTable *modified_xmp)
244 GList *keywords = g_hash_table_lookup(modified_xmp, KEYWORD_KEY);
245 GList *comment_l = g_hash_table_lookup(modified_xmp, COMMENT_KEY);
246 gchar *comment = comment_l ? comment_l->data : NULL;
248 ssi = secure_open(path);
249 if (!ssi) return FALSE;
251 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
253 secure_fprintf(ssi, "[keywords]\n");
254 while (keywords && secsave_errno == SS_ERR_NONE)
256 const gchar *word = keywords->data;
257 keywords = keywords->next;
259 secure_fprintf(ssi, "%s\n", word);
261 secure_fputc(ssi, '\n');
263 secure_fprintf(ssi, "[comment]\n");
264 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
266 secure_fprintf(ssi, "#end\n");
268 return (secure_close(ssi) == 0);
271 static gint metadata_legacy_write(FileData *fd)
273 gint success = FALSE;
275 g_assert(fd->change && fd->change->dest);
276 gchar *metadata_pathl;
278 DEBUG_1("Saving comment: %s", fd->change->dest);
280 metadata_pathl = path_from_utf8(fd->change->dest);
282 success = metadata_file_write(metadata_pathl, fd->modified_xmp);
284 g_free(metadata_pathl);
289 static gint metadata_file_read(gchar *path, GList **keywords, gchar **comment)
293 MetadataKey key = MK_NONE;
295 GString *comment_build = NULL;
297 f = fopen(path, "r");
298 if (!f) return FALSE;
300 while (fgets(s_buf, sizeof(s_buf), f))
304 if (*ptr == '#') continue;
305 if (*ptr == '[' && key != MK_COMMENT)
307 gchar *keystr = ++ptr;
310 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
315 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
317 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
329 while (*ptr != '\n' && *ptr != '\0') ptr++;
331 if (strlen(s_buf) > 0)
333 gchar *kw = utf8_validate_or_convert(s_buf);
335 list = g_list_prepend(list, kw);
340 if (!comment_build) comment_build = g_string_new("");
341 g_string_append(comment_build, s_buf);
350 *keywords = g_list_reverse(list);
354 string_list_free(list);
362 gchar *ptr = comment_build->str;
364 /* strip leading and trailing newlines */
365 while (*ptr == '\n') ptr++;
367 while (len > 0 && ptr[len - 1] == '\n') len--;
368 if (ptr[len] == '\n') len++; /* keep the last one */
371 gchar *text = g_strndup(ptr, len);
373 *comment = utf8_validate_or_convert(text);
377 g_string_free(comment_build, TRUE);
383 static void metadata_legacy_delete(FileData *fd, const gchar *except)
385 gchar *metadata_path;
386 gchar *metadata_pathl;
389 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
390 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
392 metadata_pathl = path_from_utf8(metadata_path);
393 unlink(metadata_pathl);
394 g_free(metadata_pathl);
395 g_free(metadata_path);
397 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
398 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
400 metadata_pathl = path_from_utf8(metadata_path);
401 unlink(metadata_pathl);
402 g_free(metadata_pathl);
403 g_free(metadata_path);
407 static gint metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
409 gchar *metadata_path;
410 gchar *metadata_pathl;
411 gint success = FALSE;
412 if (!fd) return FALSE;
414 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
415 if (!metadata_path) return FALSE;
417 metadata_pathl = path_from_utf8(metadata_path);
419 success = metadata_file_read(metadata_pathl, keywords, comment);
421 g_free(metadata_pathl);
422 g_free(metadata_path);
427 static GList *remove_duplicate_strings_from_list(GList *list)
430 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
431 GList *newlist = NULL;
435 gchar *key = work->data;
437 if (g_hash_table_lookup(hashtable, key) == NULL)
439 g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
440 newlist = g_list_prepend(newlist, key);
445 g_hash_table_destroy(hashtable);
448 return g_list_reverse(newlist);
451 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
455 if (!fd) return NULL;
457 /* unwritten data overide everything */
458 if (fd->modified_xmp && format == METADATA_PLAIN)
460 list = g_hash_table_lookup(fd->modified_xmp, key);
461 if (list) return string_list_copy(list);
465 Legacy metadata file is the primary source if it exists.
466 Merging the lists does not make much sense, because the existence of
467 legacy metadata file indicates that the other metadata sources are not
468 writable and thus it would not be possible to delete the keywords
469 that comes from the image file.
471 if (strcmp(key, KEYWORD_KEY) == 0)
473 if (metadata_legacy_read(fd, &list, NULL)) return list;
476 if (strcmp(key, COMMENT_KEY) == 0)
478 gchar *comment = NULL;
479 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
482 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
483 if (!exif) return NULL;
484 list = exif_get_metadata(exif, key, format);
485 exif_free_fd(fd, exif);
489 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
491 GList *string_list = metadata_read_list(fd, key, format);
494 gchar *str = string_list->data;
495 string_list->data = NULL;
496 string_list_free(string_list);
502 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
506 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
507 if (!string) return fallback;
509 ret = g_ascii_strtoull(string, &endptr, 10);
510 if (string == endptr) ret = fallback;
515 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
517 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
521 return metadata_write_string(fd, key, value);
525 gchar *new_string = g_strconcat(str, value, NULL);
526 gboolean ret = metadata_write_string(fd, key, new_string);
533 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
535 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
539 return metadata_write_list(fd, key, values);
544 list = g_list_concat(list, string_list_copy(values));
545 list = remove_duplicate_strings_from_list(list);
547 ret = metadata_write_list(fd, key, list);
548 string_list_free(list);
553 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
555 gchar *string_casefold = g_utf8_casefold(string, -1);
559 gchar *haystack = list->data;
564 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
566 equal = (strcmp(haystack_casefold, string_casefold) == 0);
567 g_free(haystack_casefold);
571 g_free(string_casefold);
579 g_free(string_casefold);
584 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
586 GList *string_to_keywords_list(const gchar *text)
589 const gchar *ptr = text;
596 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
598 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
604 /* trim starting and ending whitespaces */
605 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
606 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
610 gchar *keyword = g_strndup(begin, l);
612 /* only add if not already in the list */
613 if (!find_string_in_list_utf8nocase(list, keyword))
614 list = g_list_append(list, keyword);
628 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
630 /* FIXME: do not use global keyword_tree */
633 gboolean found = FALSE;
634 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
638 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
639 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
646 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
649 GList *keywords = NULL;
652 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
654 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
656 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
660 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
664 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
666 metadata_write_list(fd, KEYWORD_KEY, keywords);
669 string_list_free(keywords);
675 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
678 FileDataGetMarkFunc get_mark_func;
679 FileDataSetMarkFunc set_mark_func;
680 gpointer mark_func_data;
684 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
686 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
687 if (get_mark_func == meta_data_get_keyword_mark)
689 GtkTreeIter old_kw_iter;
690 GList *old_path = mark_func_data;
692 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
693 (i == mark || /* release any previous connection of given mark */
694 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
696 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
697 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
703 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
707 path = keyword_tree_get_path(keyword_tree, kw_iter);
708 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
710 mark_str = g_strdup_printf("%d", mark + 1);
711 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
718 *-------------------------------------------------------------------
720 *-------------------------------------------------------------------
725 GtkTreeStore *keyword_tree;
727 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
730 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
734 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
737 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
741 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
744 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
748 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
750 gchar *casefold = g_utf8_casefold(name, -1);
751 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
752 KEYWORD_COLUMN_NAME, name,
753 KEYWORD_COLUMN_CASEFOLD, casefold,
754 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
758 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
760 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
761 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
762 gint ret = gtk_tree_path_compare(pa, pb);
763 gtk_tree_path_free(pa);
764 gtk_tree_path_free(pb);
768 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
770 GtkTreeIter parent_a;
771 GtkTreeIter parent_b;
773 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
774 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
776 if (valid_pa && valid_pb)
778 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
782 return (!valid_pa && !valid_pb); /* both are toplevel */
786 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling)
790 gboolean toplevel = FALSE;
796 parent = *parent_ptr;
800 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
807 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
809 casefold = g_utf8_casefold(name, -1);
814 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
816 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
817 ret = strcmp(casefold, iter_casefold) == 0;
818 g_free(iter_casefold);
821 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
828 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
831 gchar *mark, *name, *casefold;
834 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
835 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
836 KEYWORD_COLUMN_NAME, &name,
837 KEYWORD_COLUMN_CASEFOLD, &casefold,
838 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
840 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
841 KEYWORD_COLUMN_NAME, name,
842 KEYWORD_COLUMN_CASEFOLD, casefold,
843 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
849 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
851 GtkTreeIter from_child;
853 keyword_copy(keyword_tree, to, from);
855 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
859 GtkTreeIter to_child;
860 gtk_tree_store_append(keyword_tree, &to_child, to);
861 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
862 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
866 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
868 keyword_copy_recursive(keyword_tree, to, from);
869 keyword_delete(keyword_tree, from);
872 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
875 GtkTreeIter iter = *iter_ptr;
880 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
881 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
887 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
891 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
895 GtkTreeIter children;
898 gchar *name = keyword_get_name(keyword_tree, &iter);
899 if (strcmp(name, path->data) == 0) break;
901 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
910 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
916 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
918 if (!casefold_list) return FALSE;
924 if (keyword_get_is_keyword(keyword_tree, &iter))
926 GList *work = casefold_list;
927 gboolean found = FALSE;
928 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
931 const gchar *casefold = work->data;
934 if (strcmp(iter_casefold, casefold) == 0)
940 g_free(iter_casefold);
941 if (!found) return FALSE;
944 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
949 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
952 GList *casefold_list = NULL;
955 if (!keyword_get_is_keyword(keyword_tree, iter)) return FALSE;
960 const gchar *kw = work->data;
963 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
966 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
968 string_list_free(casefold_list);
972 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
974 GtkTreeIter iter = *iter_ptr;
979 if (keyword_get_is_keyword(keyword_tree, &iter))
981 gchar *name = keyword_get_name(keyword_tree, &iter);
982 if (!find_string_in_list_utf8nocase(*kw_list, name))
984 *kw_list = g_list_append(*kw_list, name);
992 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
997 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1001 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1003 name = keyword_get_name(keyword_tree, iter);
1004 found = find_string_in_list_utf8nocase(*kw_list, name);
1008 *kw_list = g_list_remove(*kw_list, found);
1014 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1017 keyword_tree_reset1(keyword_tree, iter, kw_list);
1019 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1023 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1024 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1028 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1032 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1033 return TRUE; /* this should happen only on empty helpers */
1037 if (keyword_get_is_keyword(keyword_tree, &iter))
1039 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1043 /* for helpers we have to check recursively */
1044 if (!keyword_tree_check_empty_children(keyword_tree, &iter, kw_list)) return FALSE;
1047 if (!gtk_tree_model_iter_next(keyword_tree, &iter))
1054 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1056 GtkTreeIter iter = *iter_ptr;
1058 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1060 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1063 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1066 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1067 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1072 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1076 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1078 keyword_delete(keyword_tree, &child);
1081 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1083 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1086 gtk_tree_store_remove(keyword_tree, iter_ptr);
1090 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1093 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1094 if (!g_list_find(list, id))
1096 list = g_list_prepend(list, id);
1097 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1101 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1104 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1105 list = g_list_remove(list, id);
1106 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1109 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1112 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1113 return !!g_list_find(list, id);
1116 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1118 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1122 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1124 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1127 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1129 GtkTreeIter iter = *iter_ptr;
1132 if (keyword_get_is_keyword(GTK_TREE_MODEL(keyword_tree), &iter) &&
1133 !keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1135 keyword_hide_in(keyword_tree, &iter, id);
1136 /* no need to check children of hidden node */
1141 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1143 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1146 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1150 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1153 gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter);
1154 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1157 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1159 GtkTreeIter iter = *iter_ptr;
1160 GList *keywords = data;
1161 gpointer id = keywords->data;
1162 keywords = keywords->next; /* hack */
1163 if (keyword_tree_is_set(model, &iter, keywords))
1168 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1169 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1176 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1178 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1179 keywords = g_list_prepend(keywords, id);
1180 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1181 keywords = g_list_delete_link(keywords, keywords);
1185 void keyword_tree_new(void)
1187 if (keyword_tree) return;
1189 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1193 void keyword_tree_new_default(void)
1195 if (keyword_tree) return;
1199 GtkTreeIter i1, i2, i3;
1201 gtk_tree_store_append(keyword_tree, &i1, NULL);
1202 keyword_set(keyword_tree, &i1, "animal", TRUE);
1204 gtk_tree_store_append(keyword_tree, &i2, &i1);
1205 keyword_set(keyword_tree, &i2, "mammal", TRUE);
1207 gtk_tree_store_append(keyword_tree, &i3, &i2);
1208 keyword_set(keyword_tree, &i3, "dog", TRUE);
1210 gtk_tree_store_append(keyword_tree, &i3, &i2);
1211 keyword_set(keyword_tree, &i3, "cat", TRUE);
1213 gtk_tree_store_append(keyword_tree, &i2, &i1);
1214 keyword_set(keyword_tree, &i2, "insect", TRUE);
1216 gtk_tree_store_append(keyword_tree, &i3, &i2);
1217 keyword_set(keyword_tree, &i3, "fly", TRUE);
1219 gtk_tree_store_append(keyword_tree, &i3, &i2);
1220 keyword_set(keyword_tree, &i3, "dragonfly", TRUE);
1222 gtk_tree_store_append(keyword_tree, &i1, NULL);
1223 keyword_set(keyword_tree, &i1, "daytime", FALSE);
1225 gtk_tree_store_append(keyword_tree, &i2, &i1);
1226 keyword_set(keyword_tree, &i2, "morning", TRUE);
1228 gtk_tree_store_append(keyword_tree, &i2, &i1);
1229 keyword_set(keyword_tree, &i2, "noon", TRUE);
1234 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1236 GtkTreeIter iter = *iter_ptr;
1239 GtkTreeIter children;
1242 WRITE_STRING("<keyword\n");
1244 name = keyword_get_name(keyword_tree, &iter);
1245 write_char_option(outstr, indent, "name", name);
1247 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1249 WRITE_STRING(">\n");
1251 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1253 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1256 WRITE_STRING("</keyword>\n");
1257 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1261 void keyword_tree_write_config(GString *outstr, gint indent)
1264 WRITE_STRING("<keyword_tree>\n");
1267 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1269 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1272 WRITE_STRING("</keyword_tree>\n");
1275 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1278 gboolean is_kw = TRUE;
1280 while (*attribute_names)
1282 const gchar *option = *attribute_names++;
1283 const gchar *value = *attribute_values++;
1285 if (READ_CHAR_FULL("name", name)) continue;
1286 if (READ_BOOL_FULL("kw", is_kw)) continue;
1288 DEBUG_1("unknown attribute %s = %s", option, value);
1290 if (name && name[0])
1293 gtk_tree_store_append(keyword_tree, &iter, parent);
1294 keyword_set(keyword_tree, &iter, name, is_kw);
1296 return gtk_tree_iter_copy(&iter);
1302 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */