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, GtkTreeIter *result)
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);
823 if (result) *result = iter;
826 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
833 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
836 gchar *mark, *name, *casefold;
839 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
840 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
841 KEYWORD_COLUMN_NAME, &name,
842 KEYWORD_COLUMN_CASEFOLD, &casefold,
843 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
845 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
846 KEYWORD_COLUMN_NAME, name,
847 KEYWORD_COLUMN_CASEFOLD, casefold,
848 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
854 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
856 GtkTreeIter from_child;
858 keyword_copy(keyword_tree, to, from);
860 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
864 GtkTreeIter to_child;
865 gtk_tree_store_append(keyword_tree, &to_child, to);
866 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
867 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
871 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
873 keyword_copy_recursive(keyword_tree, to, from);
874 keyword_delete(keyword_tree, from);
877 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
880 GtkTreeIter iter = *iter_ptr;
885 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
886 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
892 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
896 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
900 GtkTreeIter children;
903 gchar *name = keyword_get_name(keyword_tree, &iter);
904 if (strcmp(name, path->data) == 0) break;
906 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
915 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
921 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
923 if (!casefold_list) return FALSE;
925 if (!keyword_get_is_keyword(keyword_tree, &iter))
927 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
929 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
930 return FALSE; /* this should happen only on empty helpers */
934 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
935 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
943 if (keyword_get_is_keyword(keyword_tree, &iter))
945 GList *work = casefold_list;
946 gboolean found = FALSE;
947 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
950 const gchar *casefold = work->data;
953 if (strcmp(iter_casefold, casefold) == 0)
959 g_free(iter_casefold);
960 if (!found) return FALSE;
963 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
968 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
971 GList *casefold_list = NULL;
977 const gchar *kw = work->data;
980 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
983 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
985 string_list_free(casefold_list);
989 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
991 GtkTreeIter iter = *iter_ptr;
996 if (keyword_get_is_keyword(keyword_tree, &iter))
998 gchar *name = keyword_get_name(keyword_tree, &iter);
999 if (!find_string_in_list_utf8nocase(*kw_list, name))
1001 *kw_list = g_list_append(*kw_list, name);
1009 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1014 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1018 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1020 name = keyword_get_name(keyword_tree, iter);
1021 found = find_string_in_list_utf8nocase(*kw_list, name);
1025 *kw_list = g_list_remove(*kw_list, found);
1031 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1034 keyword_tree_reset1(keyword_tree, iter, kw_list);
1036 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1040 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1041 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1045 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1049 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1050 return TRUE; /* this should happen only on empty helpers */
1054 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1055 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1059 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1061 GtkTreeIter iter = *iter_ptr;
1063 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1065 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1068 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1071 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1072 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1077 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1081 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1083 keyword_delete(keyword_tree, &child);
1086 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1088 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1091 gtk_tree_store_remove(keyword_tree, iter_ptr);
1095 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1098 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1099 if (!g_list_find(list, id))
1101 list = g_list_prepend(list, id);
1102 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1106 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1109 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1110 list = g_list_remove(list, id);
1111 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1114 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1117 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1118 return !!g_list_find(list, id);
1121 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1123 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1127 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1129 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1132 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1134 GtkTreeIter iter = *iter_ptr;
1137 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1139 keyword_hide_in(keyword_tree, &iter, id);
1140 /* no need to check children of hidden node */
1145 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1147 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1150 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1154 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1157 gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter);
1158 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1161 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1163 GtkTreeIter iter = *iter_ptr;
1164 GList *keywords = data;
1165 gpointer id = keywords->data;
1166 keywords = keywords->next; /* hack */
1167 if (keyword_tree_is_set(model, &iter, keywords))
1172 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1173 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1180 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1182 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1183 keywords = g_list_prepend(keywords, id);
1184 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1185 keywords = g_list_delete_link(keywords, keywords);
1189 void keyword_tree_new(void)
1191 if (keyword_tree) return;
1193 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1197 void keyword_tree_new_default(void)
1199 if (keyword_tree) return;
1203 GtkTreeIter i1, i2, i3;
1205 gtk_tree_store_append(keyword_tree, &i1, NULL);
1206 keyword_set(keyword_tree, &i1, "animal", TRUE);
1208 gtk_tree_store_append(keyword_tree, &i2, &i1);
1209 keyword_set(keyword_tree, &i2, "mammal", TRUE);
1211 gtk_tree_store_append(keyword_tree, &i3, &i2);
1212 keyword_set(keyword_tree, &i3, "dog", TRUE);
1214 gtk_tree_store_append(keyword_tree, &i3, &i2);
1215 keyword_set(keyword_tree, &i3, "cat", TRUE);
1217 gtk_tree_store_append(keyword_tree, &i2, &i1);
1218 keyword_set(keyword_tree, &i2, "insect", TRUE);
1220 gtk_tree_store_append(keyword_tree, &i3, &i2);
1221 keyword_set(keyword_tree, &i3, "fly", TRUE);
1223 gtk_tree_store_append(keyword_tree, &i3, &i2);
1224 keyword_set(keyword_tree, &i3, "dragonfly", TRUE);
1226 gtk_tree_store_append(keyword_tree, &i1, NULL);
1227 keyword_set(keyword_tree, &i1, "daytime", FALSE);
1229 gtk_tree_store_append(keyword_tree, &i2, &i1);
1230 keyword_set(keyword_tree, &i2, "morning", TRUE);
1232 gtk_tree_store_append(keyword_tree, &i2, &i1);
1233 keyword_set(keyword_tree, &i2, "noon", TRUE);
1238 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1240 GtkTreeIter iter = *iter_ptr;
1243 GtkTreeIter children;
1246 WRITE_NL(); WRITE_STRING("<keyword ");
1247 name = keyword_get_name(keyword_tree, &iter);
1248 write_char_option(outstr, indent, "name", name);
1250 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1251 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1255 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1257 WRITE_NL(); WRITE_STRING("</keyword>");
1263 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1267 void keyword_tree_write_config(GString *outstr, gint indent)
1270 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1273 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1275 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1278 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1281 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1284 gboolean is_kw = TRUE;
1286 while (*attribute_names)
1288 const gchar *option = *attribute_names++;
1289 const gchar *value = *attribute_values++;
1291 if (READ_CHAR_FULL("name", name)) continue;
1292 if (READ_BOOL_FULL("kw", is_kw)) continue;
1294 log_printf("unknown attribute %s = %s\n", option, value);
1296 if (name && name[0])
1299 /* re-use existing keyword if any */
1300 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1302 gtk_tree_store_append(keyword_tree, &iter, parent);
1304 keyword_set(keyword_tree, &iter, name, is_kw);
1306 return gtk_tree_iter_copy(&iter);
1312 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */