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 gchar *metadata_read_rating_stars(FileData *fd)
755 gint n = metadata_read_int(fd, RATING_KEY, METADATA_PLAIN);
757 ret = convert_rating_to_stars(n);
762 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
766 gdouble deg, min, sec;
768 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
769 if (!string) return fallback;
771 deg = g_ascii_strtod(string, &endptr);
774 min = g_ascii_strtod(endptr + 1, &endptr);
776 sec = g_ascii_strtod(endptr + 1, &endptr);
781 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
783 coord = deg + min /60.0 + sec / 3600.0;
785 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
792 log_printf("unable to parse GPS coordinate '%s'\n", string);
799 gdouble metadata_read_GPS_direction(FileData *fd, const gchar *key, gdouble fallback)
804 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
805 if (!string) return fallback;
807 DEBUG_3("GPS_direction: %s\n", string);
808 deg = g_ascii_strtod(string, &endptr);
810 /* Expected text string is of the format e.g.:
822 log_printf("unable to parse GPS direction '%s: %f'\n", string, deg);
830 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
832 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
836 return metadata_write_string(fd, key, value);
840 gchar *new_string = g_strconcat(str, value, NULL);
841 gboolean ret = metadata_write_string(fd, key, new_string);
848 gboolean metadata_write_GPS_coord(FileData *fd, const gchar *key, gdouble value)
861 min = (param * 60) - (deg * 60);
862 if (g_strcmp0(key, "Xmp.exif.GPSLongitude") == 0)
867 else if (g_strcmp0(key, "Xmp.exif.GPSLatitude") == 0)
874 log_printf("unknown GPS parameter key '%s'\n", key);
880 coordinate = g_strdup_printf("%i,%lf,%s", deg, min, ref);
881 metadata_write_string(fd, key, coordinate );
888 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
890 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
894 return metadata_write_list(fd, key, values);
899 list = g_list_concat(list, string_list_copy(values));
900 list = remove_duplicate_strings_from_list(list);
902 ret = metadata_write_list(fd, key, list);
903 string_list_free(list);
909 * \see find_string_in_list
911 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
913 gchar *string_casefold = g_utf8_casefold(string, -1);
917 gchar *haystack = list->data;
922 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
924 equal = (strcmp(haystack_casefold, string_casefold) == 0);
925 g_free(haystack_casefold);
929 g_free(string_casefold);
937 g_free(string_casefold);
942 * \see find_string_in_list
944 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
948 gchar *haystack = list->data;
950 if (haystack && strcmp(haystack, string) == 0)
957 } // gchar *find_string_in_list_utf...
960 * \brief Find a existent string in a list.
962 * This is a switch between find_string_in_list_utf8case and
963 * find_string_in_list_utf8nocase to search with or without case for the
964 * existence of a string.
966 * \param list The list to search in
967 * \param string The string to search for
968 * \return The string or NULL
970 * \see find_string_in_list_utf8case
971 * \see find_string_in_list_utf8nocase
973 gchar *find_string_in_list(GList *list, const gchar *string)
975 if (options->metadata.keywords_case_sensitive)
976 return find_string_in_list_utf8case(list, string);
978 return find_string_in_list_utf8nocase(list, string);
981 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
983 GList *string_to_keywords_list(const gchar *text)
986 const gchar *ptr = text;
993 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
995 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
1001 /* trim starting and ending whitespaces */
1002 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
1003 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
1007 gchar *keyword = g_strndup(begin, l);
1009 /* only add if not already in the list */
1010 if (!find_string_in_list(list, keyword))
1011 list = g_list_append(list, keyword);
1025 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
1027 /* FIXME: do not use global keyword_tree */
1030 gboolean found = FALSE;
1031 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1035 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
1036 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1043 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
1046 GList *keywords = NULL;
1049 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
1051 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
1053 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
1057 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1061 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
1063 metadata_write_list(fd, KEYWORD_KEY, keywords);
1066 string_list_free(keywords);
1072 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
1075 FileDataGetMarkFunc get_mark_func;
1076 FileDataSetMarkFunc set_mark_func;
1077 gpointer mark_func_data;
1081 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
1083 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
1084 if (get_mark_func == meta_data_get_keyword_mark)
1086 GtkTreeIter old_kw_iter;
1087 GList *old_path = mark_func_data;
1089 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
1090 (i == mark || /* release any previous connection of given mark */
1091 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1093 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
1094 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1100 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1104 path = keyword_tree_get_path(keyword_tree, kw_iter);
1105 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
1107 mark_str = g_strdup_printf("%d", (mark < 9 ? mark : -1) + 1);
1108 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1115 *-------------------------------------------------------------------
1117 *-------------------------------------------------------------------
1122 GtkTreeStore *keyword_tree;
1124 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1127 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1131 gchar *keyword_get_mark(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1135 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_MARK, &mark_str, -1);
1139 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1142 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1146 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1148 gboolean is_keyword;
1149 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1153 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1155 gchar *casefold = g_utf8_casefold(name, -1);
1156 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1157 KEYWORD_COLUMN_NAME, name,
1158 KEYWORD_COLUMN_CASEFOLD, casefold,
1159 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1163 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1165 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1166 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1167 gint ret = gtk_tree_path_compare(pa, pb);
1168 gtk_tree_path_free(pa);
1169 gtk_tree_path_free(pb);
1173 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1175 GtkTreeIter parent_a;
1176 GtkTreeIter parent_b;
1178 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1179 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1181 if (valid_pa && valid_pb)
1183 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1187 return (!valid_pa && !valid_pb); /* both are toplevel */
1191 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1195 gboolean toplevel = FALSE;
1201 parent = *parent_ptr;
1205 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1212 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
1214 casefold = g_utf8_casefold(name, -1);
1219 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
1221 if (options->metadata.keywords_case_sensitive)
1223 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1224 ret = strcmp(name, iter_name) == 0;
1229 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1230 ret = strcmp(casefold, iter_casefold) == 0;
1231 g_free(iter_casefold);
1232 } // if (options->metadata.tags_cas...
1236 if (result) *result = iter;
1239 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1246 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1249 gchar *mark, *name, *casefold;
1250 gboolean is_keyword;
1252 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1253 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1254 KEYWORD_COLUMN_NAME, &name,
1255 KEYWORD_COLUMN_CASEFOLD, &casefold,
1256 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1258 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1259 KEYWORD_COLUMN_NAME, name,
1260 KEYWORD_COLUMN_CASEFOLD, casefold,
1261 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1267 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1269 GtkTreeIter from_child;
1271 keyword_copy(keyword_tree, to, from);
1273 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1277 GtkTreeIter to_child;
1278 gtk_tree_store_append(keyword_tree, &to_child, to);
1279 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1280 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1284 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1286 keyword_copy_recursive(keyword_tree, to, from);
1287 keyword_delete(keyword_tree, from);
1290 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1293 GtkTreeIter iter = *iter_ptr;
1298 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1299 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1305 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1309 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1313 GtkTreeIter children;
1316 gchar *name = keyword_get_name(keyword_tree, &iter);
1317 if (strcmp(name, path->data) == 0) break;
1319 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1328 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1334 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1336 if (!casefold_list) return FALSE;
1338 if (!keyword_get_is_keyword(keyword_tree, &iter))
1340 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1342 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1343 return FALSE; /* this should happen only on empty helpers */
1347 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1348 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1356 if (keyword_get_is_keyword(keyword_tree, &iter))
1358 GList *work = casefold_list;
1359 gboolean found = FALSE;
1360 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1363 const gchar *casefold = work->data;
1366 if (strcmp(iter_casefold, casefold) == 0)
1372 g_free(iter_casefold);
1373 if (!found) return FALSE;
1376 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1381 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1383 if (!kw_list) return FALSE;
1385 if (!keyword_get_is_keyword(keyword_tree, &iter))
1387 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1389 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1390 return FALSE; /* this should happen only on empty helpers */
1394 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1395 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1403 if (keyword_get_is_keyword(keyword_tree, &iter))
1405 GList *work = kw_list;
1406 gboolean found = FALSE;
1407 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1410 const gchar *name = work->data;
1413 if (strcmp(iter_name, name) == 0)
1420 if (!found) return FALSE;
1423 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1428 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1431 GList *casefold_list = NULL;
1434 if (options->metadata.keywords_case_sensitive)
1436 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1443 const gchar *kw = work->data;
1446 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1449 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1451 string_list_free(casefold_list);
1457 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1459 GtkTreeIter iter = *iter_ptr;
1464 if (keyword_get_is_keyword(keyword_tree, &iter))
1466 gchar *name = keyword_get_name(keyword_tree, &iter);
1467 if (!find_string_in_list(*kw_list, name))
1469 *kw_list = g_list_append(*kw_list, name);
1477 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1482 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1484 GtkTreeIter iter = *iter_ptr;
1485 GList *kw_list = NULL;
1491 if (keyword_get_is_keyword(keyword_tree, &iter))
1493 gchar *name = keyword_get_name(keyword_tree, &iter);
1494 kw_list = g_list_append(kw_list, name);
1497 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1500 } // GList *keyword_tree_get(GtkTre...
1502 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1506 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1508 name = keyword_get_name(keyword_tree, iter);
1509 found = find_string_in_list(*kw_list, name);
1513 *kw_list = g_list_remove(*kw_list, found);
1519 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1522 keyword_tree_reset1(keyword_tree, iter, kw_list);
1524 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1528 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1529 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1533 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1537 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1538 return TRUE; /* this should happen only on empty helpers */
1542 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1543 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1547 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1549 GtkTreeIter iter = *iter_ptr;
1551 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1553 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1556 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1559 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1560 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1565 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1569 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1571 keyword_delete(keyword_tree, &child);
1574 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1576 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1579 gtk_tree_store_remove(keyword_tree, iter_ptr);
1583 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1586 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1587 if (!g_list_find(list, id))
1589 list = g_list_prepend(list, id);
1590 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1594 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1597 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1598 list = g_list_remove(list, id);
1599 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1602 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1605 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1606 return !!g_list_find(list, id);
1609 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1611 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1615 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1617 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1620 static gboolean keyword_revert_hidden_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1622 if (keyword_is_hidden_in(GTK_TREE_MODEL(keyword_tree), iter, data))
1624 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1629 void keyword_revert_hidden_in(GtkTreeStore *keyword_tree, gpointer id)
1631 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_revert_hidden_in_cb, id);
1634 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1636 GtkTreeIter iter = *iter_ptr;
1639 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1641 keyword_hide_in(keyword_tree, &iter, id);
1642 /* no need to check children of hidden node */
1647 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1649 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1652 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1656 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1659 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1660 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1663 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1665 GtkTreeIter iter = *iter_ptr;
1666 GList *keywords = data;
1667 gpointer id = keywords->data;
1668 keywords = keywords->next; /* hack */
1669 if (keyword_tree_is_set(model, &iter, keywords))
1674 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1675 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1682 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1684 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1685 keywords = g_list_prepend(keywords, id);
1686 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1687 keywords = g_list_delete_link(keywords, keywords);
1691 void keyword_tree_new(void)
1693 if (keyword_tree) return;
1695 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1698 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1701 gtk_tree_store_append(keyword_tree, &iter, parent);
1702 keyword_set(keyword_tree, &iter, name, is_keyword);
1706 void keyword_tree_new_default(void)
1710 if (!keyword_tree) keyword_tree_new();
1712 i1 = keyword_tree_default_append(keyword_tree, NULL, _("People"), TRUE);
1713 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1714 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1715 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1716 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1717 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1718 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1719 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Nature"), TRUE);
1720 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1721 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1722 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1723 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1724 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1725 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1726 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1727 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1728 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1729 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1730 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1731 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1732 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1733 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1734 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Art"), TRUE);
1735 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1736 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1737 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1738 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1739 i1 = keyword_tree_default_append(keyword_tree, NULL, _("City"), TRUE);
1740 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1741 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1742 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1743 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Architecture"), TRUE);
1744 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1745 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1746 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1747 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1748 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1749 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1750 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1751 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1752 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1753 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Places"), FALSE);
1754 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Conditions"), FALSE);
1755 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1756 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1757 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1758 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1759 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1760 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1761 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1762 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1763 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1764 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1765 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Photo"), FALSE);
1766 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1767 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1768 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1769 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1770 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1771 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1775 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1777 GtkTreeIter iter = *iter_ptr;
1780 GtkTreeIter children;
1784 WRITE_NL(); WRITE_STRING("<keyword ");
1785 name = keyword_get_name(keyword_tree, &iter);
1786 write_char_option(outstr, indent, "name", name);
1788 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1789 mark_str = keyword_get_mark(keyword_tree, &iter);
1790 if (mark_str && mark_str[0])
1792 write_char_option(outstr, indent, "mark", mark_str);
1795 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1799 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1801 WRITE_NL(); WRITE_STRING("</keyword>");
1807 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1811 void keyword_tree_write_config(GString *outstr, gint indent)
1814 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1817 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1819 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1822 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1825 void keyword_tree_node_disconnect_marks(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1827 GtkTreeIter iter = *iter_ptr;
1831 GtkTreeIter children;
1833 meta_data_connect_mark_with_keyword((keyword_tree), &iter, -1);
1835 if (gtk_tree_model_iter_children((keyword_tree), &children, &iter))
1837 keyword_tree_node_disconnect_marks((keyword_tree), &children);
1840 if (!gtk_tree_model_iter_next((keyword_tree), &iter)) return;
1844 void keyword_tree_disconnect_marks()
1848 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1850 keyword_tree_node_disconnect_marks(GTK_TREE_MODEL(keyword_tree), &iter);
1854 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1857 gboolean is_kw = TRUE;
1858 gchar *mark_str = NULL;
1860 while (*attribute_names)
1862 const gchar *option = *attribute_names++;
1863 const gchar *value = *attribute_values++;
1865 if (READ_CHAR_FULL("name", name)) continue;
1866 if (READ_BOOL_FULL("kw", is_kw)) continue;
1867 if (READ_CHAR_FULL("mark", mark_str)) continue;
1869 log_printf("unknown attribute %s = %s\n", option, value);
1871 if (name && name[0])
1874 /* re-use existing keyword if any */
1875 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1877 gtk_tree_store_append(keyword_tree, &iter, parent);
1879 keyword_set(keyword_tree, &iter, name, is_kw);
1883 gint i = (gint)atoi(mark_str);
1886 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree),
1891 return gtk_tree_iter_copy(&iter);
1897 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */