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 ln = strlen(fd->change->dest);
306 lf = strlen(GQ_CACHE_EXT_METADATA);
307 if (fd->change->dest &&
308 g_ascii_strncasecmp(fd->change->dest + ln - lf, GQ_CACHE_EXT_METADATA, lf) == 0)
310 success = metadata_legacy_write(fd);
311 if (success) metadata_legacy_delete(fd, fd->change->dest);
315 /* write via exiv2 */
316 /* we can either use cached metadata which have fd->modified_xmp already applied
317 or read metadata from file and apply fd->modified_xmp
318 metadata are read also if the file was modified meanwhile */
319 exif = exif_read_fd(fd);
320 if (!exif) return FALSE;
322 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
323 exif_free_fd(fd, exif);
325 if (fd->change->dest)
326 /* this will create a FileData for the sidecar and link it to the main file
327 (we can't wait until the sidecar is discovered by directory scanning because
328 exif_read_fd is called before that and it would read the main file only and
329 store the metadata in the cache)
330 FIXME: this does not catch new sidecars created by independent external programs
332 file_data_unref(file_data_new_group(fd->change->dest));
334 if (success) metadata_legacy_delete(fd, fd->change->dest);
338 gint metadata_queue_length(void)
340 return g_list_length(metadata_write_queue);
343 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
345 const gchar **k = keys;
349 if (strcmp(key, *k) == 0) return TRUE;
355 gboolean metadata_write_revert(FileData *fd, const gchar *key)
357 if (!fd->modified_xmp) return FALSE;
359 g_hash_table_remove(fd->modified_xmp, key);
361 if (g_hash_table_size(fd->modified_xmp) == 0)
363 metadata_write_queue_remove(fd);
367 /* reread the metadata to restore the original value */
368 file_data_increment_version(fd);
369 file_data_send_notification(fd, NOTIFY_REREAD);
374 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
376 if (!fd->modified_xmp)
378 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
380 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
382 metadata_cache_remove(fd, key);
386 exif_update_metadata(fd->exif, key, values);
388 metadata_write_queue_add(fd);
389 file_data_increment_version(fd);
390 file_data_send_notification(fd, NOTIFY_METADATA);
392 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
394 GList *work = fd->sidecar_files;
398 FileData *sfd = work->data;
401 if (sfd->format_class == FORMAT_CLASS_META) continue;
403 metadata_write_list(sfd, key, values);
411 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
413 GList *list = g_list_append(NULL, g_strdup(value));
414 gboolean ret = metadata_write_list(fd, key, list);
415 string_list_free(list);
419 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
423 g_snprintf(string, sizeof(string), "%llu", (unsigned long long) value);
424 return metadata_write_string(fd, key, string);
428 *-------------------------------------------------------------------
429 * keyword / comment read/write
430 *-------------------------------------------------------------------
433 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
437 ssi = secure_open(path);
438 if (!ssi) return FALSE;
440 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
442 secure_fprintf(ssi, "[keywords]\n");
443 while (keywords && secsave_errno == SS_ERR_NONE)
445 const gchar *word = keywords->data;
446 keywords = keywords->next;
448 secure_fprintf(ssi, "%s\n", word);
450 secure_fputc(ssi, '\n');
452 secure_fprintf(ssi, "[comment]\n");
453 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
455 secure_fprintf(ssi, "#end\n");
457 return (secure_close(ssi) == 0);
460 static gboolean metadata_legacy_write(FileData *fd)
462 gboolean success = FALSE;
463 gchar *metadata_pathl;
466 gboolean have_keywords;
467 gboolean have_comment;
468 const gchar *comment;
469 GList *orig_keywords = NULL;
470 gchar *orig_comment = NULL;
472 g_assert(fd->change && fd->change->dest);
474 DEBUG_1("Saving comment: %s", fd->change->dest);
476 if (!fd->modified_xmp) return TRUE;
478 metadata_pathl = path_from_utf8(fd->change->dest);
480 have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, NULL, &keywords);
481 have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, NULL, &comment_l);
482 comment = (have_comment && comment_l) ? ((GList *)comment_l)->data : NULL;
484 if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
486 success = metadata_file_write(metadata_pathl,
487 have_keywords ? (GList *)keywords : orig_keywords,
488 have_comment ? comment : orig_comment);
490 g_free(metadata_pathl);
491 g_free(orig_comment);
492 string_list_free(orig_keywords);
497 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
501 MetadataKey key = MK_NONE;
503 GString *comment_build = NULL;
505 f = fopen(path, "r");
506 if (!f) return FALSE;
508 while (fgets(s_buf, sizeof(s_buf), f))
512 if (*ptr == '#') continue;
513 if (*ptr == '[' && key != MK_COMMENT)
515 gchar *keystr = ++ptr;
518 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
523 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
525 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
537 while (*ptr != '\n' && *ptr != '\0') ptr++;
539 if (strlen(s_buf) > 0)
541 gchar *kw = utf8_validate_or_convert(s_buf);
543 list = g_list_prepend(list, kw);
548 if (!comment_build) comment_build = g_string_new("");
549 g_string_append(comment_build, s_buf);
558 *keywords = g_list_reverse(list);
562 string_list_free(list);
570 gchar *ptr = comment_build->str;
572 /* strip leading and trailing newlines */
573 while (*ptr == '\n') ptr++;
575 while (len > 0 && ptr[len - 1] == '\n') len--;
576 if (ptr[len] == '\n') len++; /* keep the last one */
579 gchar *text = g_strndup(ptr, len);
581 *comment = utf8_validate_or_convert(text);
585 g_string_free(comment_build, TRUE);
591 static void metadata_legacy_delete(FileData *fd, const gchar *except)
593 gchar *metadata_path;
594 gchar *metadata_pathl;
597 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
598 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
600 metadata_pathl = path_from_utf8(metadata_path);
601 unlink(metadata_pathl);
602 g_free(metadata_pathl);
603 g_free(metadata_path);
607 /* without exiv2: do not delete xmp metadata because we are not able to convert it,
609 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
610 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
612 metadata_pathl = path_from_utf8(metadata_path);
613 unlink(metadata_pathl);
614 g_free(metadata_pathl);
615 g_free(metadata_path);
620 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
622 gchar *metadata_path;
623 gchar *metadata_pathl;
624 gboolean success = FALSE;
626 if (!fd) return FALSE;
628 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
629 if (!metadata_path) return FALSE;
631 metadata_pathl = path_from_utf8(metadata_path);
633 success = metadata_file_read(metadata_pathl, keywords, comment);
635 g_free(metadata_pathl);
636 g_free(metadata_path);
641 static GList *remove_duplicate_strings_from_list(GList *list)
644 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
645 GList *newlist = NULL;
649 gchar *key = work->data;
651 if (g_hash_table_lookup(hashtable, key) == NULL)
653 g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
654 newlist = g_list_prepend(newlist, key);
659 g_hash_table_destroy(hashtable);
662 return g_list_reverse(newlist);
665 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
669 const GList *cache_entry;
670 if (!fd) return NULL;
672 /* unwritten data overide everything */
673 if (fd->modified_xmp && format == METADATA_PLAIN)
675 list = g_hash_table_lookup(fd->modified_xmp, key);
676 if (list) return string_list_copy(list);
680 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
681 && (cache_entry = metadata_cache_get(fd, key)))
683 return string_list_copy(cache_entry->next);
687 Legacy metadata file is the primary source if it exists.
688 Merging the lists does not make much sense, because the existence of
689 legacy metadata file indicates that the other metadata sources are not
690 writable and thus it would not be possible to delete the keywords
691 that comes from the image file.
693 if (strcmp(key, KEYWORD_KEY) == 0)
695 if (metadata_legacy_read(fd, &list, NULL))
697 if (format == METADATA_PLAIN)
699 metadata_cache_update(fd, key, list);
704 else if (strcmp(key, COMMENT_KEY) == 0)
706 gchar *comment = NULL;
707 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
709 else if (strncmp(key, "file.", 5) == 0)
711 return g_list_append(NULL, metadata_file_info(fd, key, format));
714 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
715 if (!exif) return NULL;
716 list = exif_get_metadata(exif, key, format);
717 exif_free_fd(fd, exif);
719 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
721 metadata_cache_update(fd, key, list);
727 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
729 GList *string_list = metadata_read_list(fd, key, format);
732 gchar *str = string_list->data;
733 string_list->data = NULL;
734 string_list_free(string_list);
740 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
744 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
745 if (!string) return fallback;
747 ret = g_ascii_strtoull(string, &endptr, 10);
748 if (string == endptr) ret = fallback;
753 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
757 gdouble deg, min, sec;
759 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
760 if (!string) return fallback;
762 deg = g_ascii_strtod(string, &endptr);
765 min = g_ascii_strtod(endptr + 1, &endptr);
767 sec = g_ascii_strtod(endptr + 1, &endptr);
772 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
774 coord = deg + min /60.0 + sec / 3600.0;
776 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
783 log_printf("unable to parse GPS coordinate '%s'\n", string);
790 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
795 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
796 if (!string) return fallback;
798 DEBUG_3("GPS_direction: %s\n", string);
799 deg = g_ascii_strtod(string, &endptr);
801 /* Expected text string is of the format e.g.:
813 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
821 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
823 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
827 return metadata_write_string(fd, key, value);
831 gchar *new_string = g_strconcat(str, value, NULL);
832 gboolean ret = metadata_write_string(fd, key, new_string);
839 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
852 min = (param * 60) - (deg * 60);
853 if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
858 else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
865 log_printf("unknown GPS parameter key '%s'\n", key);
871 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
872 metadata_write_string(fd, key, coordinate );
879 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
881 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
885 return metadata_write_list(fd, key, values);
890 list = g_list_concat(list, string_list_copy(values));
891 list = remove_duplicate_strings_from_list(list);
893 ret = metadata_write_list(fd, key, list);
894 string_list_free(list);
900 * \see find_string_in_list
902 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
904 gchar *string_casefold = g_utf8_casefold(string, -1);
908 gchar *haystack = list->data;
913 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
915 equal = (strcmp(haystack_casefold, string_casefold) == 0);
916 g_free(haystack_casefold);
920 g_free(string_casefold);
928 g_free(string_casefold);
933 * \see find_string_in_list
935 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
939 gchar *haystack = list->data;
941 if (haystack && strcmp(haystack, string) == 0)
948 } // gchar *find_string_in_list_utf...
951 * \brief Find a existent string in a list.
953 * This is a switch between find_string_in_list_utf8case and
954 * find_string_in_list_utf8nocase to search with or without case for the
955 * existence of a string.
957 * \param list The list to search in
958 * \param string The string to search for
959 * \return The string or NULL
961 * \see find_string_in_list_utf8case
962 * \see find_string_in_list_utf8nocase
964 gchar *find_string_in_list(GList *list, const gchar *string)
966 if (options->metadata.keywords_case_sensitive)
967 return find_string_in_list_utf8case(list, string);
969 return find_string_in_list_utf8nocase(list, string);
972 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
974 GList *string_to_keywords_list(const gchar *text)
977 const gchar *ptr = text;
984 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
986 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
992 /* trim starting and ending whitespaces */
993 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
994 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
998 gchar *keyword = g_strndup(begin, l);
1000 /* only add if not already in the list */
1001 if (!find_string_in_list(list, keyword))
1002 list = g_list_append(list, keyword);
1016 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
1018 /* FIXME: do not use global keyword_tree */
1021 gboolean found = FALSE;
1022 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1026 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1027 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1034 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
1037 GList *keywords = NULL;
1040 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1042 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1044 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1048 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1052 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1054 metadata_write_list(fd, KEYWORD_KEY, keywords);
1057 string_list_free(keywords);
1063 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1066 FileDataGetMarkFunc get_mark_func;
1067 FileDataSetMarkFunc set_mark_func;
1068 gpointer mark_func_data;
1072 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1074 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1075 if (get_mark_func == meta_data_get_keyword_mark)
1077 GtkTreeIter old_kw_iter;
1078 GList *old_path = mark_func_data;
1080 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1081 (i == mark || /* release any previous connection of given mark */
1082 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1084 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
1085 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1091 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1095 path = keyword_tree_get_path(keyword_tree, kw_iter);
1096 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
1098 mark_str = g_strdup_printf("%d", (mark < 9 ? mark : -1) + 1);
1099 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1106 *-------------------------------------------------------------------
1108 *-------------------------------------------------------------------
1113 GtkTreeStore *keyword_tree;
1115 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1118 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1122 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1126 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1130 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1133 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1137 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1139 gboolean is_keyword;
1140 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1144 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1146 gchar *casefold = g_utf8_casefold(name, -1);
1147 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1148 KEYWORD_COLUMN_NAME, name,
1149 KEYWORD_COLUMN_CASEFOLD, casefold,
1150 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1154 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1156 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1157 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1158 gint ret = gtk_tree_path_compare(pa, pb);
1159 gtk_tree_path_free(pa);
1160 gtk_tree_path_free(pb);
1164 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1166 GtkTreeIter parent_a;
1167 GtkTreeIter parent_b;
1169 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1170 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1172 if (valid_pa && valid_pb)
1174 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1178 return (!valid_pa && !valid_pb); /* both are toplevel */
1182 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1186 gboolean toplevel = FALSE;
1192 parent = *parent_ptr;
1196 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1203 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
1205 casefold = g_utf8_casefold(name, -1);
1210 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
1212 if (options->metadata.keywords_case_sensitive)
1214 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1215 ret = strcmp(name, iter_name) == 0;
1220 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1221 ret = strcmp(casefold, iter_casefold) == 0;
1222 g_free(iter_casefold);
1223 } // if (options->metadata.tags_cas...
1227 if (result) *result = iter;
1230 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1237 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1240 gchar *mark, *name, *casefold;
1241 gboolean is_keyword;
1243 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1244 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1245 KEYWORD_COLUMN_NAME, &name,
1246 KEYWORD_COLUMN_CASEFOLD, &casefold,
1247 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1249 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1250 KEYWORD_COLUMN_NAME, name,
1251 KEYWORD_COLUMN_CASEFOLD, casefold,
1252 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1258 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1260 GtkTreeIter from_child;
1262 keyword_copy(keyword_tree, to, from);
1264 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1268 GtkTreeIter to_child;
1269 gtk_tree_store_append(keyword_tree, &to_child, to);
1270 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1271 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1275 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1277 keyword_copy_recursive(keyword_tree, to, from);
1278 keyword_delete(keyword_tree, from);
1281 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1284 GtkTreeIter iter = *iter_ptr;
1289 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1290 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1296 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1300 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1304 GtkTreeIter children;
1307 gchar *name = keyword_get_name(keyword_tree, &iter);
1308 if (strcmp(name, path->data) == 0) break;
1310 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1319 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1325 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1327 if (!casefold_list) return FALSE;
1329 if (!keyword_get_is_keyword(keyword_tree, &iter))
1331 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1333 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1334 return FALSE; /* this should happen only on empty helpers */
1338 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1339 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1347 if (keyword_get_is_keyword(keyword_tree, &iter))
1349 GList *work = casefold_list;
1350 gboolean found = FALSE;
1351 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1354 const gchar *casefold = work->data;
1357 if (strcmp(iter_casefold, casefold) == 0)
1363 g_free(iter_casefold);
1364 if (!found) return FALSE;
1367 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1372 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1374 if (!kw_list) return FALSE;
1376 if (!keyword_get_is_keyword(keyword_tree, &iter))
1378 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1380 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1381 return FALSE; /* this should happen only on empty helpers */
1385 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1386 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1394 if (keyword_get_is_keyword(keyword_tree, &iter))
1396 GList *work = kw_list;
1397 gboolean found = FALSE;
1398 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1401 const gchar *name = work->data;
1404 if (strcmp(iter_name, name) == 0)
1411 if (!found) return FALSE;
1414 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1419 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1422 GList *casefold_list = NULL;
1425 if (options->metadata.keywords_case_sensitive)
1427 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1434 const gchar *kw = work->data;
1437 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1440 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1442 string_list_free(casefold_list);
1448 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1450 GtkTreeIter iter = *iter_ptr;
1455 if (keyword_get_is_keyword(keyword_tree, &iter))
1457 gchar *name = keyword_get_name(keyword_tree, &iter);
1458 if (!find_string_in_list(*kw_list, name))
1460 *kw_list = g_list_append(*kw_list, name);
1468 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1473 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1475 GtkTreeIter iter = *iter_ptr;
1476 GList *kw_list = NULL;
1482 if (keyword_get_is_keyword(keyword_tree, &iter))
1484 gchar *name = keyword_get_name(keyword_tree, &iter);
1485 kw_list = g_list_append(kw_list, name);
1488 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1491 } // GList *keyword_tree_get(GtkTre...
1493 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1497 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1499 name = keyword_get_name(keyword_tree, iter);
1500 found = find_string_in_list(*kw_list, name);
1504 *kw_list = g_list_remove(*kw_list, found);
1510 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1513 keyword_tree_reset1(keyword_tree, iter, kw_list);
1515 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1519 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1520 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1524 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1528 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1529 return TRUE; /* this should happen only on empty helpers */
1533 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1534 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1538 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1540 GtkTreeIter iter = *iter_ptr;
1542 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1544 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1547 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1550 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1551 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1556 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1560 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1562 keyword_delete(keyword_tree, &child);
1565 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1567 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1570 gtk_tree_store_remove(keyword_tree, iter_ptr);
1574 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1577 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1578 if (!g_list_find(list, id))
1580 list = g_list_prepend(list, id);
1581 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1585 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1588 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1589 list = g_list_remove(list, id);
1590 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1593 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1596 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1597 return !!g_list_find(list, id);
1600 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1602 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1606 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1608 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1611 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1613 if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1615 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1620 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1622 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1625 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1627 GtkTreeIter iter = *iter_ptr;
1630 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1632 keyword_hide_in(keyword_tree, &iter, id);
1633 /* no need to check children of hidden node */
1638 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1640 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1643 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1647 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1650 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1651 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1654 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1656 GtkTreeIter iter = *iter_ptr;
1657 GList *keywords = data;
1658 gpointer id = keywords->data;
1659 keywords = keywords->next; /* hack */
1660 if (keyword_tree_is_set(model, &iter, keywords))
1665 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1666 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1673 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1675 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1676 keywords = g_list_prepend(keywords, id);
1677 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1678 keywords = g_list_delete_link(keywords, keywords);
1682 void keyword_tree_new(void)
1684 if (keyword_tree) return;
1686 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1689 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1692 gtk_tree_store_append(keyword_tree, &iter, parent);
1693 keyword_set(keyword_tree, &iter, name, is_keyword);
1697 void keyword_tree_new_default(void)
1701 if (!keyword_tree) keyword_tree_new();
1703 i1 = keyword_tree_default_append(keyword_tree, NULL, _("People"), TRUE);
1704 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1705 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1706 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1707 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1708 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1709 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1710 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Nature"), TRUE);
1711 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1712 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1713 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1714 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1715 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1716 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1717 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1718 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1719 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1720 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1721 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1722 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1723 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1724 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1725 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Art"), TRUE);
1726 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1727 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1728 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1729 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1730 i1 = keyword_tree_default_append(keyword_tree, NULL, _("City"), TRUE);
1731 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1732 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1733 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1734 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Architecture"), TRUE);
1735 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1736 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1737 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1738 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1739 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1740 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1741 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1742 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1743 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1744 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Places"), FALSE);
1745 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Conditions"), FALSE);
1746 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1747 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1748 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1749 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1750 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1751 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1752 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1753 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1754 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1755 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1756 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Photo"), FALSE);
1757 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1758 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1759 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1760 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1761 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1762 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1766 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1768 GtkTreeIter iter = *iter_ptr;
1771 GtkTreeIter children;
1775 WRITE_NL(); WRITE_STRING("<keyword ");
1776 name = keyword_get_name(keyword_tree, &iter);
1777 write_char_option(outstr, indent, "name", name);
1779 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1780 mark_str = keyword_get_mark(keyword_tree, &iter);
1781 if (mark_str && mark_str[0])
1783 write_char_option(outstr, indent, "mark", mark_str);
1786 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1790 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1792 WRITE_NL(); WRITE_STRING("</keyword>");
1798 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1802 void keyword_tree_write_config(GString *outstr, gint indent)
1805 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1808 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1810 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1813 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1816 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1819 gboolean is_kw = TRUE;
1820 gchar *mark_str = NULL;
1822 while (*attribute_names)
1824 const gchar *option = *attribute_names++;
1825 const gchar *value = *attribute_values++;
1827 if (READ_CHAR_FULL("name", name)) continue;
1828 if (READ_BOOL_FULL("kw", is_kw)) continue;
1829 if (READ_CHAR_FULL("mark", mark_str)) continue;
1831 log_printf("unknown attribute %s = %s\n", option, value);
1833 if (name && name[0])
1836 /* re-use existing keyword if any */
1837 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1839 gtk_tree_store_append(keyword_tree, &iter, parent);
1841 keyword_set(keyword_tree, &iter, name, is_kw);
1845 gint i = (gint)atoi(mark_str);
1848 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1853 return gtk_tree_iter_copy(&iter);
1859 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */