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 if (strncmp(key, "file.", 5) == 0)
485 return g_list_append(NULL, metadata_file_info(fd, key, format));
488 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
489 if (!exif) return NULL;
490 list = exif_get_metadata(exif, key, format);
491 exif_free_fd(fd, exif);
495 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
497 GList *string_list = metadata_read_list(fd, key, format);
500 gchar *str = string_list->data;
501 string_list->data = NULL;
502 string_list_free(string_list);
508 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
512 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
513 if (!string) return fallback;
515 ret = g_ascii_strtoull(string, &endptr, 10);
516 if (string == endptr) ret = fallback;
521 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
523 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
527 return metadata_write_string(fd, key, value);
531 gchar *new_string = g_strconcat(str, value, NULL);
532 gboolean ret = metadata_write_string(fd, key, new_string);
539 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
541 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
545 return metadata_write_list(fd, key, values);
550 list = g_list_concat(list, string_list_copy(values));
551 list = remove_duplicate_strings_from_list(list);
553 ret = metadata_write_list(fd, key, list);
554 string_list_free(list);
559 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
561 gchar *string_casefold = g_utf8_casefold(string, -1);
565 gchar *haystack = list->data;
570 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
572 equal = (strcmp(haystack_casefold, string_casefold) == 0);
573 g_free(haystack_casefold);
577 g_free(string_casefold);
585 g_free(string_casefold);
590 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
592 GList *string_to_keywords_list(const gchar *text)
595 const gchar *ptr = text;
602 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
604 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
610 /* trim starting and ending whitespaces */
611 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
612 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
616 gchar *keyword = g_strndup(begin, l);
618 /* only add if not already in the list */
619 if (!find_string_in_list_utf8nocase(list, keyword))
620 list = g_list_append(list, keyword);
634 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
636 /* FIXME: do not use global keyword_tree */
639 gboolean found = FALSE;
640 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
644 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
645 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
652 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
655 GList *keywords = NULL;
658 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
660 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
662 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
666 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
670 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
672 metadata_write_list(fd, KEYWORD_KEY, keywords);
675 string_list_free(keywords);
681 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
684 FileDataGetMarkFunc get_mark_func;
685 FileDataSetMarkFunc set_mark_func;
686 gpointer mark_func_data;
690 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
692 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
693 if (get_mark_func == meta_data_get_keyword_mark)
695 GtkTreeIter old_kw_iter;
696 GList *old_path = mark_func_data;
698 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
699 (i == mark || /* release any previous connection of given mark */
700 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
702 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
703 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
709 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
713 path = keyword_tree_get_path(keyword_tree, kw_iter);
714 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
716 mark_str = g_strdup_printf("%d", mark + 1);
717 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
724 *-------------------------------------------------------------------
726 *-------------------------------------------------------------------
731 GtkTreeStore *keyword_tree;
733 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
736 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
740 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
743 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
747 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
750 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
754 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
756 gchar *casefold = g_utf8_casefold(name, -1);
757 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
758 KEYWORD_COLUMN_NAME, name,
759 KEYWORD_COLUMN_CASEFOLD, casefold,
760 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
764 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
766 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
767 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
768 gint ret = gtk_tree_path_compare(pa, pb);
769 gtk_tree_path_free(pa);
770 gtk_tree_path_free(pb);
774 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
776 GtkTreeIter parent_a;
777 GtkTreeIter parent_b;
779 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
780 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
782 if (valid_pa && valid_pb)
784 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
788 return (!valid_pa && !valid_pb); /* both are toplevel */
792 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
796 gboolean toplevel = FALSE;
802 parent = *parent_ptr;
806 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
813 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
815 casefold = g_utf8_casefold(name, -1);
820 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
822 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
823 ret = strcmp(casefold, iter_casefold) == 0;
824 g_free(iter_casefold);
828 if (result) *result = iter;
831 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
838 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
841 gchar *mark, *name, *casefold;
844 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
845 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
846 KEYWORD_COLUMN_NAME, &name,
847 KEYWORD_COLUMN_CASEFOLD, &casefold,
848 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
850 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
851 KEYWORD_COLUMN_NAME, name,
852 KEYWORD_COLUMN_CASEFOLD, casefold,
853 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
859 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
861 GtkTreeIter from_child;
863 keyword_copy(keyword_tree, to, from);
865 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
869 GtkTreeIter to_child;
870 gtk_tree_store_append(keyword_tree, &to_child, to);
871 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
872 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
876 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
878 keyword_copy_recursive(keyword_tree, to, from);
879 keyword_delete(keyword_tree, from);
882 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
885 GtkTreeIter iter = *iter_ptr;
890 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
891 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
897 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
901 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
905 GtkTreeIter children;
908 gchar *name = keyword_get_name(keyword_tree, &iter);
909 if (strcmp(name, path->data) == 0) break;
911 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
920 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
926 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
928 if (!casefold_list) return FALSE;
930 if (!keyword_get_is_keyword(keyword_tree, &iter))
932 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
934 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
935 return FALSE; /* this should happen only on empty helpers */
939 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
940 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
948 if (keyword_get_is_keyword(keyword_tree, &iter))
950 GList *work = casefold_list;
951 gboolean found = FALSE;
952 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
955 const gchar *casefold = work->data;
958 if (strcmp(iter_casefold, casefold) == 0)
964 g_free(iter_casefold);
965 if (!found) return FALSE;
968 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
973 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
976 GList *casefold_list = NULL;
982 const gchar *kw = work->data;
985 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
988 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
990 string_list_free(casefold_list);
994 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
996 GtkTreeIter iter = *iter_ptr;
1001 if (keyword_get_is_keyword(keyword_tree, &iter))
1003 gchar *name = keyword_get_name(keyword_tree, &iter);
1004 if (!find_string_in_list_utf8nocase(*kw_list, name))
1006 *kw_list = g_list_append(*kw_list, name);
1014 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1019 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1023 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1025 name = keyword_get_name(keyword_tree, iter);
1026 found = find_string_in_list_utf8nocase(*kw_list, name);
1030 *kw_list = g_list_remove(*kw_list, found);
1036 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1039 keyword_tree_reset1(keyword_tree, iter, kw_list);
1041 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1045 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1046 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1050 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1054 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1055 return TRUE; /* this should happen only on empty helpers */
1059 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1060 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1064 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1066 GtkTreeIter iter = *iter_ptr;
1068 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1070 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1073 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1076 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1077 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1082 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1086 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1088 keyword_delete(keyword_tree, &child);
1091 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1093 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1096 gtk_tree_store_remove(keyword_tree, iter_ptr);
1100 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1103 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1104 if (!g_list_find(list, id))
1106 list = g_list_prepend(list, id);
1107 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1111 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1114 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1115 list = g_list_remove(list, id);
1116 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1119 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1122 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1123 return !!g_list_find(list, id);
1126 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1128 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1132 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1134 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1137 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1139 GtkTreeIter iter = *iter_ptr;
1142 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1144 keyword_hide_in(keyword_tree, &iter, id);
1145 /* no need to check children of hidden node */
1150 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1152 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1155 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1159 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1162 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1163 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1166 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1168 GtkTreeIter iter = *iter_ptr;
1169 GList *keywords = data;
1170 gpointer id = keywords->data;
1171 keywords = keywords->next; /* hack */
1172 if (keyword_tree_is_set(model, &iter, keywords))
1177 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1178 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1185 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1187 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1188 keywords = g_list_prepend(keywords, id);
1189 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1190 keywords = g_list_delete_link(keywords, keywords);
1194 void keyword_tree_new(void)
1196 if (keyword_tree) return;
1198 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1202 void keyword_tree_new_default(void)
1204 if (keyword_tree) return;
1208 GtkTreeIter i1, i2, i3;
1210 gtk_tree_store_append(keyword_tree, &i1, NULL);
1211 keyword_set(keyword_tree, &i1, "animal", TRUE);
1213 gtk_tree_store_append(keyword_tree, &i2, &i1);
1214 keyword_set(keyword_tree, &i2, "mammal", TRUE);
1216 gtk_tree_store_append(keyword_tree, &i3, &i2);
1217 keyword_set(keyword_tree, &i3, "dog", TRUE);
1219 gtk_tree_store_append(keyword_tree, &i3, &i2);
1220 keyword_set(keyword_tree, &i3, "cat", TRUE);
1222 gtk_tree_store_append(keyword_tree, &i2, &i1);
1223 keyword_set(keyword_tree, &i2, "insect", TRUE);
1225 gtk_tree_store_append(keyword_tree, &i3, &i2);
1226 keyword_set(keyword_tree, &i3, "fly", TRUE);
1228 gtk_tree_store_append(keyword_tree, &i3, &i2);
1229 keyword_set(keyword_tree, &i3, "dragonfly", TRUE);
1231 gtk_tree_store_append(keyword_tree, &i1, NULL);
1232 keyword_set(keyword_tree, &i1, "daytime", FALSE);
1234 gtk_tree_store_append(keyword_tree, &i2, &i1);
1235 keyword_set(keyword_tree, &i2, "morning", TRUE);
1237 gtk_tree_store_append(keyword_tree, &i2, &i1);
1238 keyword_set(keyword_tree, &i2, "noon", TRUE);
1243 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1245 GtkTreeIter iter = *iter_ptr;
1248 GtkTreeIter children;
1251 WRITE_NL(); WRITE_STRING("<keyword ");
1252 name = keyword_get_name(keyword_tree, &iter);
1253 write_char_option(outstr, indent, "name", name);
1255 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1256 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1260 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1262 WRITE_NL(); WRITE_STRING("</keyword>");
1268 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1272 void keyword_tree_write_config(GString *outstr, gint indent)
1275 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1278 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1280 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1283 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1286 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1289 gboolean is_kw = TRUE;
1291 while (*attribute_names)
1293 const gchar *option = *attribute_names++;
1294 const gchar *value = *attribute_values++;
1296 if (READ_CHAR_FULL("name", name)) continue;
1297 if (READ_BOOL_FULL("kw", is_kw)) continue;
1299 log_printf("unknown attribute %s = %s\n", option, value);
1301 if (name && name[0])
1304 /* re-use existing keyword if any */
1305 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1307 gtk_tree_store_append(keyword_tree, &iter, parent);
1309 keyword_set(keyword_tree, &iter, name, is_kw);
1311 return gtk_tree_iter_copy(&iter);
1317 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */