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 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
521 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
525 return metadata_write_string(fd, key, value);
529 gchar *new_string = g_strconcat(str, value, NULL);
530 gboolean ret = metadata_write_string(fd, key, new_string);
537 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
539 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
543 return metadata_write_list(fd, key, values);
548 list = g_list_concat(list, string_list_copy(values));
549 list = remove_duplicate_strings_from_list(list);
551 ret = metadata_write_list(fd, key, list);
552 string_list_free(list);
557 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
559 gchar *string_casefold = g_utf8_casefold(string, -1);
563 gchar *haystack = list->data;
568 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
570 equal = (strcmp(haystack_casefold, string_casefold) == 0);
571 g_free(haystack_casefold);
575 g_free(string_casefold);
583 g_free(string_casefold);
588 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
590 GList *string_to_keywords_list(const gchar *text)
593 const gchar *ptr = text;
600 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
602 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
608 /* trim starting and ending whitespaces */
609 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
610 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
614 gchar *keyword = g_strndup(begin, l);
616 /* only add if not already in the list */
617 if (!find_string_in_list_utf8nocase(list, keyword))
618 list = g_list_append(list, keyword);
632 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
634 /* FIXME: do not use global keyword_tree */
637 gboolean found = FALSE;
638 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
642 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
643 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
650 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
653 GList *keywords = NULL;
656 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
658 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
660 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
664 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
668 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
670 metadata_write_list(fd, KEYWORD_KEY, keywords);
673 string_list_free(keywords);
679 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
682 FileDataGetMarkFunc get_mark_func;
683 FileDataSetMarkFunc set_mark_func;
684 gpointer mark_func_data;
688 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
690 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
691 if (get_mark_func == meta_data_get_keyword_mark)
693 GtkTreeIter old_kw_iter;
694 GList *old_path = mark_func_data;
696 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
697 (i == mark || /* release any previous connection of given mark */
698 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
700 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
701 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
707 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
711 path = keyword_tree_get_path(keyword_tree, kw_iter);
712 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
714 mark_str = g_strdup_printf("%d", mark + 1);
715 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
722 *-------------------------------------------------------------------
724 *-------------------------------------------------------------------
729 GtkTreeStore *keyword_tree;
731 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
734 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
738 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
741 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
745 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
748 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
752 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
754 gchar *casefold = g_utf8_casefold(name, -1);
755 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
756 KEYWORD_COLUMN_NAME, name,
757 KEYWORD_COLUMN_CASEFOLD, casefold,
758 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
762 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
764 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
765 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
766 gint ret = gtk_tree_path_compare(pa, pb);
767 gtk_tree_path_free(pa);
768 gtk_tree_path_free(pb);
772 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
774 GtkTreeIter parent_a;
775 GtkTreeIter parent_b;
777 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
778 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
780 if (valid_pa && valid_pb)
782 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
786 return (!valid_pa && !valid_pb); /* both are toplevel */
790 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
794 gboolean toplevel = FALSE;
800 parent = *parent_ptr;
804 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
811 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
813 casefold = g_utf8_casefold(name, -1);
818 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
820 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
821 ret = strcmp(casefold, iter_casefold) == 0;
822 g_free(iter_casefold);
826 if (result) *result = iter;
829 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
836 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
839 gchar *mark, *name, *casefold;
842 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
843 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
844 KEYWORD_COLUMN_NAME, &name,
845 KEYWORD_COLUMN_CASEFOLD, &casefold,
846 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
848 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
849 KEYWORD_COLUMN_NAME, name,
850 KEYWORD_COLUMN_CASEFOLD, casefold,
851 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
857 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
859 GtkTreeIter from_child;
861 keyword_copy(keyword_tree, to, from);
863 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
867 GtkTreeIter to_child;
868 gtk_tree_store_append(keyword_tree, &to_child, to);
869 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
870 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
874 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
876 keyword_copy_recursive(keyword_tree, to, from);
877 keyword_delete(keyword_tree, from);
880 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
883 GtkTreeIter iter = *iter_ptr;
888 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
889 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
895 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
899 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
903 GtkTreeIter children;
906 gchar *name = keyword_get_name(keyword_tree, &iter);
907 if (strcmp(name, path->data) == 0) break;
909 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
918 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
924 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
926 if (!casefold_list) return FALSE;
928 if (!keyword_get_is_keyword(keyword_tree, &iter))
930 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
932 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
933 return FALSE; /* this should happen only on empty helpers */
937 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
938 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
946 if (keyword_get_is_keyword(keyword_tree, &iter))
948 GList *work = casefold_list;
949 gboolean found = FALSE;
950 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
953 const gchar *casefold = work->data;
956 if (strcmp(iter_casefold, casefold) == 0)
962 g_free(iter_casefold);
963 if (!found) return FALSE;
966 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
971 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
974 GList *casefold_list = NULL;
980 const gchar *kw = work->data;
983 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
986 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
988 string_list_free(casefold_list);
992 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
994 GtkTreeIter iter = *iter_ptr;
999 if (keyword_get_is_keyword(keyword_tree, &iter))
1001 gchar *name = keyword_get_name(keyword_tree, &iter);
1002 if (!find_string_in_list_utf8nocase(*kw_list, name))
1004 *kw_list = g_list_append(*kw_list, name);
1012 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1017 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1021 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1023 name = keyword_get_name(keyword_tree, iter);
1024 found = find_string_in_list_utf8nocase(*kw_list, name);
1028 *kw_list = g_list_remove(*kw_list, found);
1034 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1037 keyword_tree_reset1(keyword_tree, iter, kw_list);
1039 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1043 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1044 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1048 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1052 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1053 return TRUE; /* this should happen only on empty helpers */
1057 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1058 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1062 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1064 GtkTreeIter iter = *iter_ptr;
1066 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1068 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1071 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1074 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1075 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1080 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1084 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1086 keyword_delete(keyword_tree, &child);
1089 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1091 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1094 gtk_tree_store_remove(keyword_tree, iter_ptr);
1098 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1101 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1102 if (!g_list_find(list, id))
1104 list = g_list_prepend(list, id);
1105 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1109 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1112 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1113 list = g_list_remove(list, id);
1114 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1117 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1120 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1121 return !!g_list_find(list, id);
1124 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1126 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1130 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1132 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1135 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1137 GtkTreeIter iter = *iter_ptr;
1140 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1142 keyword_hide_in(keyword_tree, &iter, id);
1143 /* no need to check children of hidden node */
1148 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1150 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1153 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1157 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1160 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1161 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1164 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1166 GtkTreeIter iter = *iter_ptr;
1167 GList *keywords = data;
1168 gpointer id = keywords->data;
1169 keywords = keywords->next; /* hack */
1170 if (keyword_tree_is_set(model, &iter, keywords))
1175 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1176 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1183 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1185 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1186 keywords = g_list_prepend(keywords, id);
1187 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1188 keywords = g_list_delete_link(keywords, keywords);
1192 void keyword_tree_new(void)
1194 if (keyword_tree) return;
1196 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1200 void keyword_tree_new_default(void)
1202 if (keyword_tree) return;
1206 GtkTreeIter i1, i2, i3;
1208 gtk_tree_store_append(keyword_tree, &i1, NULL);
1209 keyword_set(keyword_tree, &i1, "animal", TRUE);
1211 gtk_tree_store_append(keyword_tree, &i2, &i1);
1212 keyword_set(keyword_tree, &i2, "mammal", TRUE);
1214 gtk_tree_store_append(keyword_tree, &i3, &i2);
1215 keyword_set(keyword_tree, &i3, "dog", TRUE);
1217 gtk_tree_store_append(keyword_tree, &i3, &i2);
1218 keyword_set(keyword_tree, &i3, "cat", TRUE);
1220 gtk_tree_store_append(keyword_tree, &i2, &i1);
1221 keyword_set(keyword_tree, &i2, "insect", TRUE);
1223 gtk_tree_store_append(keyword_tree, &i3, &i2);
1224 keyword_set(keyword_tree, &i3, "fly", TRUE);
1226 gtk_tree_store_append(keyword_tree, &i3, &i2);
1227 keyword_set(keyword_tree, &i3, "dragonfly", TRUE);
1229 gtk_tree_store_append(keyword_tree, &i1, NULL);
1230 keyword_set(keyword_tree, &i1, "daytime", FALSE);
1232 gtk_tree_store_append(keyword_tree, &i2, &i1);
1233 keyword_set(keyword_tree, &i2, "morning", TRUE);
1235 gtk_tree_store_append(keyword_tree, &i2, &i1);
1236 keyword_set(keyword_tree, &i2, "noon", TRUE);
1241 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1243 GtkTreeIter iter = *iter_ptr;
1246 GtkTreeIter children;
1249 WRITE_NL(); WRITE_STRING("<keyword ");
1250 name = keyword_get_name(keyword_tree, &iter);
1251 write_char_option(outstr, indent, "name", name);
1253 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1254 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1258 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1260 WRITE_NL(); WRITE_STRING("</keyword>");
1266 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1270 void keyword_tree_write_config(GString *outstr, gint indent)
1273 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1276 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1278 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1281 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1284 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1287 gboolean is_kw = TRUE;
1289 while (*attribute_names)
1291 const gchar *option = *attribute_names++;
1292 const gchar *value = *attribute_values++;
1294 if (READ_CHAR_FULL("name", name)) continue;
1295 if (READ_BOOL_FULL("kw", is_kw)) continue;
1297 log_printf("unknown attribute %s = %s\n", option, value);
1299 if (name && name[0])
1302 /* re-use existing keyword if any */
1303 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1305 gtk_tree_store_append(keyword_tree, &iter, parent);
1307 keyword_set(keyword_tree, &iter, name, is_kw);
1309 return gtk_tree_iter_copy(&iter);
1315 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */