2 * Copyright (C) 2004 John Ellis
3 * Copyright (C) 2008 - 2016 The Geeqie Team
5 * Authors: John Ellis, Laurent Monin
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
29 #include "secure_save.h"
30 #include "ui_fileops.h"
33 #include "filefilter.h"
34 #include "layout_util.h"
43 static const gchar *group_keys[] = { /* tags that will be written to all files in a group, options->metadata.sync_grouped_files */
45 "Xmp.photoshop.Urgency",
46 "Xmp.photoshop.Category",
47 "Xmp.photoshop.SupplementalCategory",
50 "Xmp.photoshop.Instruction",
51 "Xmp.photoshop.DateCreated",
53 "Xmp.photoshop.AuthorsPosition",
55 "Xmp.photoshop.State",
56 "Xmp.iptc.CountryCode",
57 "Xmp.photoshop.Country",
58 "Xmp.photoshop.TransmissionReference",
59 "Xmp.photoshop.Headline",
60 "Xmp.photoshop.Credit",
61 "Xmp.photoshop.Source",
64 "Xmp.photoshop.CaptionWriter",
67 static gboolean metadata_write_queue_idle_cb(gpointer data);
68 static gboolean metadata_legacy_write(FileData *fd);
69 static void metadata_legacy_delete(FileData *fd, const gchar *except);
70 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment);
74 *-------------------------------------------------------------------
75 * long-term cache - keep keywords from whole dir in memory
76 *-------------------------------------------------------------------
79 /* fd->cached metadata list of lists
80 each particular list contains key as a first entry, then the values
83 static void metadata_cache_update(FileData *fd, const gchar *key, const GList *values)
87 work = fd->cached_metadata;
90 GList *entry = work->data;
91 gchar *entry_key = entry->data;
93 if (strcmp(entry_key, key) == 0)
95 /* key found - just replace values */
96 GList *old_values = entry->next;
98 old_values->prev = NULL;
99 string_list_free(old_values);
100 work->data = g_list_append(entry, string_list_copy(values));
101 DEBUG_1("updated %s %s\n", key, fd->path);
107 /* key not found - prepend new entry */
108 fd->cached_metadata = g_list_prepend(fd->cached_metadata,
109 g_list_prepend(string_list_copy(values), g_strdup(key)));
110 DEBUG_1("added %s %s\n", key, fd->path);
114 static const GList *metadata_cache_get(FileData *fd, const gchar *key)
118 work = fd->cached_metadata;
121 GList *entry = work->data;
122 gchar *entry_key = entry->data;
124 if (strcmp(entry_key, key) == 0)
127 DEBUG_1("found %s %s\n", key, fd->path);
133 DEBUG_1("not found %s %s\n", key, fd->path);
136 static void metadata_cache_remove(FileData *fd, const gchar *key)
140 work = fd->cached_metadata;
143 GList *entry = work->data;
144 gchar *entry_key = entry->data;
146 if (strcmp(entry_key, key) == 0)
149 string_list_free(entry);
150 fd->cached_metadata = g_list_delete_link(fd->cached_metadata, work);
151 DEBUG_1("removed %s %s\n", key, fd->path);
156 DEBUG_1("not removed %s %s\n", key, fd->path);
159 void metadata_cache_free(FileData *fd)
162 if (fd->cached_metadata) DEBUG_1("freed %s\n", fd->path);
164 work = fd->cached_metadata;
167 GList *entry = work->data;
168 string_list_free(entry);
172 g_list_free(fd->cached_metadata);
173 fd->cached_metadata = NULL;
182 *-------------------------------------------------------------------
184 *-------------------------------------------------------------------
187 static GList *metadata_write_queue = NULL;
188 static guint metadata_write_idle_id = 0; /* event source id */
190 static void metadata_write_queue_add(FileData *fd)
192 if (!g_list_find(metadata_write_queue, fd))
194 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
197 layout_util_status_update_write_all();
200 if (metadata_write_idle_id)
202 g_source_remove(metadata_write_idle_id);
203 metadata_write_idle_id = 0;
206 if (options->metadata.confirm_after_timeout)
208 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, NULL);
213 gboolean metadata_write_queue_remove(FileData *fd)
215 g_hash_table_destroy(fd->modified_xmp);
216 fd->modified_xmp = NULL;
218 metadata_write_queue = g_list_remove(metadata_write_queue, fd);
220 file_data_increment_version(fd);
221 file_data_send_notification(fd, NOTIFY_REREAD);
225 layout_util_status_update_write_all();
229 gboolean metadata_write_queue_remove_list(GList *list)
237 FileData *fd = work->data;
239 ret = ret && metadata_write_queue_remove(fd);
244 void metadata_notify_cb(FileData *fd, NotifyType type, gpointer data)
246 if (type & (NOTIFY_REREAD | NOTIFY_CHANGE))
248 metadata_cache_free(fd);
250 if (g_list_find(metadata_write_queue, fd))
252 DEBUG_1("Notify metadata: %s %04x", fd->path, type);
253 if (!isname(fd->path))
255 /* ignore deleted files */
256 metadata_write_queue_remove(fd);
262 gboolean metadata_write_queue_confirm(gboolean force_dialog, FileUtilDoneFunc done_func, gpointer done_data)
265 GList *to_approve = NULL;
267 work = metadata_write_queue;
270 FileData *fd = work->data;
273 if (!isname(fd->path))
275 /* ignore deleted files */
276 metadata_write_queue_remove(fd);
280 if (fd->change) continue; /* another operation in progress, skip this file for now */
282 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
285 file_util_write_metadata(NULL, to_approve, NULL, force_dialog, done_func, done_data);
287 return (metadata_write_queue != NULL);
290 static gboolean metadata_write_queue_idle_cb(gpointer data)
292 metadata_write_queue_confirm(FALSE, NULL, NULL);
293 metadata_write_idle_id = 0;
297 gboolean metadata_write_perform(FileData *fd)
303 g_assert(fd->change);
305 lf = strlen(GQ_CACHE_EXT_METADATA);
306 if (fd->change->dest &&
307 g_ascii_strncasecmp(fd->change->dest + strlen(fd->change->dest) - lf, GQ_CACHE_EXT_METADATA, lf) == 0)
309 success = metadata_legacy_write(fd);
310 if (success) metadata_legacy_delete(fd, fd->change->dest);
314 /* write via exiv2 */
315 /* we can either use cached metadata which have fd->modified_xmp already applied
316 or read metadata from file and apply fd->modified_xmp
317 metadata are read also if the file was modified meanwhile */
318 exif = exif_read_fd(fd);
319 if (!exif) return FALSE;
321 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
322 exif_free_fd(fd, exif);
324 if (fd->change->dest)
325 /* this will create a FileData for the sidecar and link it to the main file
326 (we can't wait until the sidecar is discovered by directory scanning because
327 exif_read_fd is called before that and it would read the main file only and
328 store the metadata in the cache)
329 FIXME: this does not catch new sidecars created by independent external programs
331 file_data_unref(file_data_new_group(fd->change->dest));
333 if (success) metadata_legacy_delete(fd, fd->change->dest);
337 gint metadata_queue_length(void)
339 return g_list_length(metadata_write_queue);
342 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
344 const gchar **k = keys;
348 if (strcmp(key, *k) == 0) return TRUE;
354 gboolean metadata_write_revert(FileData *fd, const gchar *key)
356 if (!fd->modified_xmp) return FALSE;
358 g_hash_table_remove(fd->modified_xmp, key);
360 if (g_hash_table_size(fd->modified_xmp) == 0)
362 metadata_write_queue_remove(fd);
366 /* reread the metadata to restore the original value */
367 file_data_increment_version(fd);
368 file_data_send_notification(fd, NOTIFY_REREAD);
373 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
375 if (!fd->modified_xmp)
377 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
379 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
381 metadata_cache_remove(fd, key);
385 exif_update_metadata(fd->exif, key, values);
387 metadata_write_queue_add(fd);
388 file_data_increment_version(fd);
389 file_data_send_notification(fd, NOTIFY_METADATA);
391 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
393 GList *work = fd->sidecar_files;
397 FileData *sfd = work->data;
400 if (sfd->format_class == FORMAT_CLASS_META) continue;
402 metadata_write_list(sfd, key, values);
410 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
412 GList *list = g_list_append(NULL, g_strdup(value));
413 gboolean ret = metadata_write_list(fd, key, list);
414 string_list_free(list);
418 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
422 g_snprintf(string, sizeof(string), "%llu", (unsigned long long) value);
423 return metadata_write_string(fd, key, string);
427 *-------------------------------------------------------------------
428 * keyword / comment read/write
429 *-------------------------------------------------------------------
432 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
436 ssi = secure_open(path);
437 if (!ssi) return FALSE;
439 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
441 secure_fprintf(ssi, "[keywords]\n");
442 while (keywords && secsave_errno == SS_ERR_NONE)
444 const gchar *word = keywords->data;
445 keywords = keywords->next;
447 secure_fprintf(ssi, "%s\n", word);
449 secure_fputc(ssi, '\n');
451 secure_fprintf(ssi, "[comment]\n");
452 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
454 secure_fprintf(ssi, "#end\n");
456 return (secure_close(ssi) == 0);
459 static gboolean metadata_legacy_write(FileData *fd)
461 gboolean success = FALSE;
462 gchar *metadata_pathl;
465 gboolean have_keywords;
466 gboolean have_comment;
467 const gchar *comment;
468 GList *orig_keywords = NULL;
469 gchar *orig_comment = NULL;
471 g_assert(fd->change && fd->change->dest);
473 DEBUG_1("Saving comment: %s", fd->change->dest);
475 if (!fd->modified_xmp) return TRUE;
477 metadata_pathl = path_from_utf8(fd->change->dest);
479 have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, NULL, &keywords);
480 have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, NULL, &comment_l);
481 comment = (have_comment && comment_l) ? ((GList *)comment_l)->data : NULL;
483 if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
485 success = metadata_file_write(metadata_pathl,
486 have_keywords ? (GList *)keywords : orig_keywords,
487 have_comment ? comment : orig_comment);
489 g_free(metadata_pathl);
490 g_free(orig_comment);
491 string_list_free(orig_keywords);
496 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
500 MetadataKey key = MK_NONE;
502 GString *comment_build = NULL;
504 f = fopen(path, "r");
505 if (!f) return FALSE;
507 while (fgets(s_buf, sizeof(s_buf), f))
511 if (*ptr == '#') continue;
512 if (*ptr == '[' && key != MK_COMMENT)
514 gchar *keystr = ++ptr;
517 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
522 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
524 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
536 while (*ptr != '\n' && *ptr != '\0') ptr++;
538 if (strlen(s_buf) > 0)
540 gchar *kw = utf8_validate_or_convert(s_buf);
542 list = g_list_prepend(list, kw);
547 if (!comment_build) comment_build = g_string_new("");
548 g_string_append(comment_build, s_buf);
557 *keywords = g_list_reverse(list);
561 string_list_free(list);
569 gchar *ptr = comment_build->str;
571 /* strip leading and trailing newlines */
572 while (*ptr == '\n') ptr++;
574 while (len > 0 && ptr[len - 1] == '\n') len--;
575 if (ptr[len] == '\n') len++; /* keep the last one */
578 gchar *text = g_strndup(ptr, len);
580 *comment = utf8_validate_or_convert(text);
584 g_string_free(comment_build, TRUE);
590 static void metadata_legacy_delete(FileData *fd, const gchar *except)
592 gchar *metadata_path;
593 gchar *metadata_pathl;
596 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
597 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
599 metadata_pathl = path_from_utf8(metadata_path);
600 unlink(metadata_pathl);
601 g_free(metadata_pathl);
602 g_free(metadata_path);
606 /* without exiv2: do not delete xmp metadata because we are not able to convert it,
608 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
609 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
611 metadata_pathl = path_from_utf8(metadata_path);
612 unlink(metadata_pathl);
613 g_free(metadata_pathl);
614 g_free(metadata_path);
619 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
621 gchar *metadata_path;
622 gchar *metadata_pathl;
623 gboolean success = FALSE;
625 if (!fd) return FALSE;
627 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
628 if (!metadata_path) return FALSE;
630 metadata_pathl = path_from_utf8(metadata_path);
632 success = metadata_file_read(metadata_pathl, keywords, comment);
634 g_free(metadata_pathl);
635 g_free(metadata_path);
640 static GList *remove_duplicate_strings_from_list(GList *list)
643 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
644 GList *newlist = NULL;
648 gchar *key = work->data;
650 if (g_hash_table_lookup(hashtable, key) == NULL)
652 g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
653 newlist = g_list_prepend(newlist, key);
658 g_hash_table_destroy(hashtable);
661 return g_list_reverse(newlist);
664 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
668 const GList *cache_entry;
669 if (!fd) return NULL;
671 /* unwritten data overide everything */
672 if (fd->modified_xmp && format == METADATA_PLAIN)
674 list = g_hash_table_lookup(fd->modified_xmp, key);
675 if (list) return string_list_copy(list);
679 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
680 && (cache_entry = metadata_cache_get(fd, key)))
682 return string_list_copy(cache_entry->next);
686 Legacy metadata file is the primary source if it exists.
687 Merging the lists does not make much sense, because the existence of
688 legacy metadata file indicates that the other metadata sources are not
689 writable and thus it would not be possible to delete the keywords
690 that comes from the image file.
692 if (strcmp(key, KEYWORD_KEY) == 0)
694 if (metadata_legacy_read(fd, &list, NULL))
696 if (format == METADATA_PLAIN)
698 metadata_cache_update(fd, key, list);
703 else if (strcmp(key, COMMENT_KEY) == 0)
705 gchar *comment = NULL;
706 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
708 else if (strncmp(key, "file.", 5) == 0)
710 return g_list_append(NULL, metadata_file_info(fd, key, format));
713 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
714 if (!exif) return NULL;
715 list = exif_get_metadata(exif, key, format);
716 exif_free_fd(fd, exif);
718 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
720 metadata_cache_update(fd, key, list);
726 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
728 GList *string_list = metadata_read_list(fd, key, format);
731 gchar *str = string_list->data;
732 string_list->data = NULL;
733 string_list_free(string_list);
739 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
743 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
744 if (!string) return fallback;
746 ret = g_ascii_strtoull(string, &endptr, 10);
747 if (string == endptr) ret = fallback;
752 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
756 gdouble deg, min, sec;
758 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
759 if (!string) return fallback;
761 deg = g_ascii_strtod(string, &endptr);
764 min = g_ascii_strtod(endptr + 1, &endptr);
766 sec = g_ascii_strtod(endptr + 1, &endptr);
771 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
773 coord = deg + min /60.0 + sec / 3600.0;
775 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
782 log_printf("unable to parse GPS coordinate '%s'\n", string);
789 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
794 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
795 if (!string) return fallback;
797 DEBUG_3("GPS_direction: %s\n", string);
798 deg = g_ascii_strtod(string, &endptr);
800 /* Expected text string is of the format e.g.:
812 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
820 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
822 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
826 return metadata_write_string(fd, key, value);
830 gchar *new_string = g_strconcat(str, value, NULL);
831 gboolean ret = metadata_write_string(fd, key, new_string);
838 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
851 min = (param * 60) - (deg * 60);
852 if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
857 else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
864 log_printf("unknown GPS parameter key '%s'\n", key);
870 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
871 metadata_write_string(fd, key, coordinate );
878 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
880 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
884 return metadata_write_list(fd, key, values);
889 list = g_list_concat(list, string_list_copy(values));
890 list = remove_duplicate_strings_from_list(list);
892 ret = metadata_write_list(fd, key, list);
893 string_list_free(list);
899 * \see find_string_in_list
901 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
903 gchar *string_casefold = g_utf8_casefold(string, -1);
907 gchar *haystack = list->data;
912 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
914 equal = (strcmp(haystack_casefold, string_casefold) == 0);
915 g_free(haystack_casefold);
919 g_free(string_casefold);
927 g_free(string_casefold);
932 * \see find_string_in_list
934 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
938 gchar *haystack = list->data;
940 if (haystack && strcmp(haystack, string) == 0)
947 } // gchar *find_string_in_list_utf...
950 * \brief Find a existent string in a list.
952 * This is a switch between find_string_in_list_utf8case and
953 * find_string_in_list_utf8nocase to search with or without case for the
954 * existence of a string.
956 * \param list The list to search in
957 * \param string The string to search for
958 * \return The string or NULL
960 * \see find_string_in_list_utf8case
961 * \see find_string_in_list_utf8nocase
963 gchar *find_string_in_list(GList *list, const gchar *string)
965 if (options->metadata.keywords_case_sensitive)
966 return find_string_in_list_utf8case(list, string);
968 return find_string_in_list_utf8nocase(list, string);
971 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
973 GList *string_to_keywords_list(const gchar *text)
976 const gchar *ptr = text;
983 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
985 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
991 /* trim starting and ending whitespaces */
992 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
993 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
997 gchar *keyword = g_strndup(begin, l);
999 /* only add if not already in the list */
1000 if (!find_string_in_list(list, keyword))
1001 list = g_list_append(list, keyword);
1015 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
1017 /* FIXME: do not use global keyword_tree */
1020 gboolean found = FALSE;
1021 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1025 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1026 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1033 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
1036 GList *keywords = NULL;
1039 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1041 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1043 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1047 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1051 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1053 metadata_write_list(fd, KEYWORD_KEY, keywords);
1056 string_list_free(keywords);
1062 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1065 FileDataGetMarkFunc get_mark_func;
1066 FileDataSetMarkFunc set_mark_func;
1067 gpointer mark_func_data;
1071 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1073 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1074 if (get_mark_func == meta_data_get_keyword_mark)
1076 GtkTreeIter old_kw_iter;
1077 GList *old_path = mark_func_data;
1079 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1080 (i == mark || /* release any previous connection of given mark */
1081 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1083 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
1084 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1090 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1094 path = keyword_tree_get_path(keyword_tree, kw_iter);
1095 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
1097 mark_str = g_strdup_printf("%d", (mark < 9 ? mark : -1) + 1);
1098 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1105 *-------------------------------------------------------------------
1107 *-------------------------------------------------------------------
1112 GtkTreeStore *keyword_tree;
1114 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1117 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1121 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1125 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1129 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1132 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1136 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1138 gboolean is_keyword;
1139 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1143 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1145 gchar *casefold = g_utf8_casefold(name, -1);
1146 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1147 KEYWORD_COLUMN_NAME, name,
1148 KEYWORD_COLUMN_CASEFOLD, casefold,
1149 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1153 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1155 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1156 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1157 gint ret = gtk_tree_path_compare(pa, pb);
1158 gtk_tree_path_free(pa);
1159 gtk_tree_path_free(pb);
1163 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1165 GtkTreeIter parent_a;
1166 GtkTreeIter parent_b;
1168 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1169 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1171 if (valid_pa && valid_pb)
1173 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1177 return (!valid_pa && !valid_pb); /* both are toplevel */
1181 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1185 gboolean toplevel = FALSE;
1191 parent = *parent_ptr;
1195 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1202 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
1204 casefold = g_utf8_casefold(name, -1);
1209 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
1211 if (options->metadata.keywords_case_sensitive)
1213 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1214 ret = strcmp(name, iter_name) == 0;
1219 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1220 ret = strcmp(casefold, iter_casefold) == 0;
1221 g_free(iter_casefold);
1222 } // if (options->metadata.tags_cas...
1226 if (result) *result = iter;
1229 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1236 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1239 gchar *mark, *name, *casefold;
1240 gboolean is_keyword;
1242 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1243 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1244 KEYWORD_COLUMN_NAME, &name,
1245 KEYWORD_COLUMN_CASEFOLD, &casefold,
1246 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1248 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1249 KEYWORD_COLUMN_NAME, name,
1250 KEYWORD_COLUMN_CASEFOLD, casefold,
1251 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1257 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1259 GtkTreeIter from_child;
1261 keyword_copy(keyword_tree, to, from);
1263 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1267 GtkTreeIter to_child;
1268 gtk_tree_store_append(keyword_tree, &to_child, to);
1269 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1270 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1274 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1276 keyword_copy_recursive(keyword_tree, to, from);
1277 keyword_delete(keyword_tree, from);
1280 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1283 GtkTreeIter iter = *iter_ptr;
1288 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1289 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1295 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1299 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1303 GtkTreeIter children;
1306 gchar *name = keyword_get_name(keyword_tree, &iter);
1307 if (strcmp(name, path->data) == 0) break;
1309 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1318 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1324 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1326 if (!casefold_list) return FALSE;
1328 if (!keyword_get_is_keyword(keyword_tree, &iter))
1330 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1332 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1333 return FALSE; /* this should happen only on empty helpers */
1337 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1338 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1346 if (keyword_get_is_keyword(keyword_tree, &iter))
1348 GList *work = casefold_list;
1349 gboolean found = FALSE;
1350 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1353 const gchar *casefold = work->data;
1356 if (strcmp(iter_casefold, casefold) == 0)
1362 g_free(iter_casefold);
1363 if (!found) return FALSE;
1366 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1371 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1373 if (!kw_list) return FALSE;
1375 if (!keyword_get_is_keyword(keyword_tree, &iter))
1377 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1379 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1380 return FALSE; /* this should happen only on empty helpers */
1384 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1385 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1393 if (keyword_get_is_keyword(keyword_tree, &iter))
1395 GList *work = kw_list;
1396 gboolean found = FALSE;
1397 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1400 const gchar *name = work->data;
1403 if (strcmp(iter_name, name) == 0)
1410 if (!found) return FALSE;
1413 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1418 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1421 GList *casefold_list = NULL;
1424 if (options->metadata.keywords_case_sensitive)
1426 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1433 const gchar *kw = work->data;
1436 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1439 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1441 string_list_free(casefold_list);
1447 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1449 GtkTreeIter iter = *iter_ptr;
1454 if (keyword_get_is_keyword(keyword_tree, &iter))
1456 gchar *name = keyword_get_name(keyword_tree, &iter);
1457 if (!find_string_in_list(*kw_list, name))
1459 *kw_list = g_list_append(*kw_list, name);
1467 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1472 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1474 GtkTreeIter iter = *iter_ptr;
1475 GList *kw_list = NULL;
1481 if (keyword_get_is_keyword(keyword_tree, &iter))
1483 gchar *name = keyword_get_name(keyword_tree, &iter);
1484 kw_list = g_list_append(kw_list, name);
1487 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1490 } // GList *keyword_tree_get(GtkTre...
1492 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1496 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1498 name = keyword_get_name(keyword_tree, iter);
1499 found = find_string_in_list(*kw_list, name);
1503 *kw_list = g_list_remove(*kw_list, found);
1509 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1512 keyword_tree_reset1(keyword_tree, iter, kw_list);
1514 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1518 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1519 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1523 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1527 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1528 return TRUE; /* this should happen only on empty helpers */
1532 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1533 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1537 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1539 GtkTreeIter iter = *iter_ptr;
1541 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1543 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1546 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1549 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1550 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1555 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1559 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1561 keyword_delete(keyword_tree, &child);
1564 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1566 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1569 gtk_tree_store_remove(keyword_tree, iter_ptr);
1573 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1576 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1577 if (!g_list_find(list, id))
1579 list = g_list_prepend(list, id);
1580 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1584 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1587 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1588 list = g_list_remove(list, id);
1589 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1592 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1595 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1596 return !!g_list_find(list, id);
1599 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1601 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1605 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1607 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1610 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1612 if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1614 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1619 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1621 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1624 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1626 GtkTreeIter iter = *iter_ptr;
1629 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1631 keyword_hide_in(keyword_tree, &iter, id);
1632 /* no need to check children of hidden node */
1637 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1639 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1642 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1646 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1649 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1650 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1653 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1655 GtkTreeIter iter = *iter_ptr;
1656 GList *keywords = data;
1657 gpointer id = keywords->data;
1658 keywords = keywords->next; /* hack */
1659 if (keyword_tree_is_set(model, &iter, keywords))
1664 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1665 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1672 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1674 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1675 keywords = g_list_prepend(keywords, id);
1676 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1677 keywords = g_list_delete_link(keywords, keywords);
1681 void keyword_tree_new(void)
1683 if (keyword_tree) return;
1685 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1688 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1691 gtk_tree_store_append(keyword_tree, &iter, parent);
1692 keyword_set(keyword_tree, &iter, name, is_keyword);
1696 void keyword_tree_new_default(void)
1700 if (!keyword_tree) keyword_tree_new();
1702 i1 = keyword_tree_default_append(keyword_tree, NULL, _("People"), TRUE);
1703 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1704 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1705 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1706 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1707 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1708 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1709 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Nature"), TRUE);
1710 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1711 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1712 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1713 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1714 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1715 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1716 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1717 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1718 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1719 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1720 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1721 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1722 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1723 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1724 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Art"), TRUE);
1725 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1726 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1727 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1728 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1729 i1 = keyword_tree_default_append(keyword_tree, NULL, _("City"), TRUE);
1730 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1731 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1732 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1733 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Architecture"), TRUE);
1734 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1735 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1736 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1737 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1738 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1739 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1740 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1741 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1742 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1743 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Places"), FALSE);
1744 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Conditions"), FALSE);
1745 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1746 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1747 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1748 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1749 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1750 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1751 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1752 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1753 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1754 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1755 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Photo"), FALSE);
1756 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1757 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1758 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1759 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1760 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1761 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1765 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1767 GtkTreeIter iter = *iter_ptr;
1770 GtkTreeIter children;
1774 WRITE_NL(); WRITE_STRING("<keyword ");
1775 name = keyword_get_name(keyword_tree, &iter);
1776 write_char_option(outstr, indent, "name", name);
1778 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1779 mark_str = keyword_get_mark(keyword_tree, &iter);
1780 if (mark_str && mark_str[0])
1782 write_char_option(outstr, indent, "mark", mark_str);
1785 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1789 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1791 WRITE_NL(); WRITE_STRING("</keyword>");
1797 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1801 void keyword_tree_write_config(GString *outstr, gint indent)
1804 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1807 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1809 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1812 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1815 void keyword_tree_node_disconnect_marks(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1817 GtkTreeIter iter = *iter_ptr;
1821 GtkTreeIter children;
1823 meta_data_connect_mark_with_keyword((keyword_tree), &iter, -1);
1825 if (gtk_tree_model_iter_children((keyword_tree), &children, &iter))
1827 keyword_tree_node_disconnect_marks((keyword_tree), &children);
1830 if (!gtk_tree_model_iter_next((keyword_tree), &iter)) return;
1834 void keyword_tree_disconnect_marks()
1838 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1840 keyword_tree_node_disconnect_marks(GTK_TREE_MODEL(keyword_tree), &iter);
1844 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1847 gboolean is_kw = TRUE;
1848 gchar *mark_str = NULL;
1850 while (*attribute_names)
1852 const gchar *option = *attribute_names++;
1853 const gchar *value = *attribute_values++;
1855 if (READ_CHAR_FULL("name", name)) continue;
1856 if (READ_BOOL_FULL("kw", is_kw)) continue;
1857 if (READ_CHAR_FULL("mark", mark_str)) continue;
1859 log_printf("unknown attribute %s = %s\n", option, value);
1861 if (name && name[0])
1864 /* re-use existing keyword if any */
1865 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1867 gtk_tree_store_append(keyword_tree, &iter, parent);
1869 keyword_set(keyword_tree, &iter, name, is_kw);
1873 gint i = (gint)atoi(mark_str);
1876 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1881 return gtk_tree_iter_copy(&iter);
1887 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */