4 * Copyright (C) 2008 - 2012 The Geeqie Team
6 * Author: John Ellis, Laurent Monin
8 * This software is released under the GNU General Public License (GNU GPL).
9 * Please read the included file COPYING for more information.
10 * This software comes with no warranty of any kind, use at your own risk!
21 #include "secure_save.h"
22 #include "ui_fileops.h"
25 #include "filefilter.h"
26 #include "layout_util.h"
35 static const gchar *group_keys[] = { /* tags that will be written to all files in a group, options->metadata.sync_grouped_files */
37 "Xmp.photoshop.Urgency",
38 "Xmp.photoshop.Category",
39 "Xmp.photoshop.SupplementalCategory",
42 "Xmp.photoshop.Instruction",
43 "Xmp.photoshop.DateCreated",
45 "Xmp.photoshop.AuthorsPosition",
47 "Xmp.photoshop.State",
48 "Xmp.iptc.CountryCode",
49 "Xmp.photoshop.Country",
50 "Xmp.photoshop.TransmissionReference",
51 "Xmp.photoshop.Headline",
52 "Xmp.photoshop.Credit",
53 "Xmp.photoshop.Source",
56 "Xmp.photoshop.CaptionWriter",
59 static gboolean metadata_write_queue_idle_cb(gpointer data);
60 static gboolean metadata_legacy_write(FileData *fd);
61 static void metadata_legacy_delete(FileData *fd, const gchar *except);
62 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment);
66 *-------------------------------------------------------------------
67 * long-term cache - keep keywords from whole dir in memory
68 *-------------------------------------------------------------------
71 /* fd->cached metadata list of lists
72 each particular list contains key as a first entry, then the values
75 static void metadata_cache_update(FileData *fd, const gchar *key, const GList *values)
79 work = fd->cached_metadata;
82 GList *entry = work->data;
83 gchar *entry_key = entry->data;
85 if (strcmp(entry_key, key) == 0)
87 /* key found - just replace values */
88 GList *old_values = entry->next;
90 old_values->prev = NULL;
91 string_list_free(old_values);
92 work->data = g_list_append(entry, string_list_copy(values));
93 DEBUG_1("updated %s %s\n", key, fd->path);
99 /* key not found - prepend new entry */
100 fd->cached_metadata = g_list_prepend(fd->cached_metadata,
101 g_list_prepend(string_list_copy(values), g_strdup(key)));
102 DEBUG_1("added %s %s\n", key, fd->path);
106 static const GList *metadata_cache_get(FileData *fd, const gchar *key)
110 work = fd->cached_metadata;
113 GList *entry = work->data;
114 gchar *entry_key = entry->data;
116 if (strcmp(entry_key, key) == 0)
119 DEBUG_1("found %s %s\n", key, fd->path);
125 DEBUG_1("not found %s %s\n", key, fd->path);
128 static void metadata_cache_remove(FileData *fd, const gchar *key)
132 work = fd->cached_metadata;
135 GList *entry = work->data;
136 gchar *entry_key = entry->data;
138 if (strcmp(entry_key, key) == 0)
141 string_list_free(entry);
142 fd->cached_metadata = g_list_delete_link(fd->cached_metadata, work);
143 DEBUG_1("removed %s %s\n", key, fd->path);
148 DEBUG_1("not removed %s %s\n", key, fd->path);
151 void metadata_cache_free(FileData *fd)
154 if (fd->cached_metadata) DEBUG_1("freed %s\n", fd->path);
156 work = fd->cached_metadata;
159 GList *entry = work->data;
160 string_list_free(entry);
164 g_list_free(fd->cached_metadata);
165 fd->cached_metadata = NULL;
174 *-------------------------------------------------------------------
176 *-------------------------------------------------------------------
179 static GList *metadata_write_queue = NULL;
180 static guint metadata_write_idle_id = 0; /* event source id */
182 static void metadata_write_queue_add(FileData *fd)
184 if (!g_list_find(metadata_write_queue, fd))
186 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
189 layout_util_status_update_write_all();
192 if (metadata_write_idle_id)
194 g_source_remove(metadata_write_idle_id);
195 metadata_write_idle_id = 0;
198 if (options->metadata.confirm_after_timeout)
200 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, NULL);
205 gboolean metadata_write_queue_remove(FileData *fd)
207 g_hash_table_destroy(fd->modified_xmp);
208 fd->modified_xmp = NULL;
210 metadata_write_queue = g_list_remove(metadata_write_queue, fd);
212 file_data_increment_version(fd);
213 file_data_send_notification(fd, NOTIFY_REREAD);
217 layout_util_status_update_write_all();
221 gboolean metadata_write_queue_remove_list(GList *list)
229 FileData *fd = work->data;
231 ret = ret && metadata_write_queue_remove(fd);
236 void metadata_notify_cb(FileData *fd, NotifyType type, gpointer data)
238 if (type & (NOTIFY_REREAD | NOTIFY_CHANGE))
240 metadata_cache_free(fd);
242 if (g_list_find(metadata_write_queue, fd))
244 DEBUG_1("Notify metadata: %s %04x", fd->path, type);
245 if (!isname(fd->path))
247 /* ignore deleted files */
248 metadata_write_queue_remove(fd);
254 gboolean metadata_write_queue_confirm(gboolean force_dialog, FileUtilDoneFunc done_func, gpointer done_data)
257 GList *to_approve = NULL;
259 work = metadata_write_queue;
262 FileData *fd = work->data;
265 if (!isname(fd->path))
267 /* ignore deleted files */
268 metadata_write_queue_remove(fd);
272 if (fd->change) continue; /* another operation in progress, skip this file for now */
274 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
277 file_util_write_metadata(NULL, to_approve, NULL, force_dialog, done_func, done_data);
279 return (metadata_write_queue != NULL);
282 static gboolean metadata_write_queue_idle_cb(gpointer data)
284 metadata_write_queue_confirm(FALSE, NULL, NULL);
285 metadata_write_idle_id = 0;
289 gboolean metadata_write_perform(FileData *fd)
294 g_assert(fd->change);
296 if (fd->change->dest &&
297 strcmp(extension_from_path(fd->change->dest), GQ_CACHE_EXT_METADATA) == 0)
299 success = metadata_legacy_write(fd);
300 if (success) metadata_legacy_delete(fd, fd->change->dest);
304 /* write via exiv2 */
305 /* we can either use cached metadata which have fd->modified_xmp already applied
306 or read metadata from file and apply fd->modified_xmp
307 metadata are read also if the file was modified meanwhile */
308 exif = exif_read_fd(fd);
309 if (!exif) return FALSE;
311 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
312 exif_free_fd(fd, exif);
314 if (fd->change->dest)
315 /* this will create a FileData for the sidecar and link it to the main file
316 (we can't wait until the sidecar is discovered by directory scanning because
317 exif_read_fd is called before that and it would read the main file only and
318 store the metadata in the cache)
319 FIXME: this does not catch new sidecars created by independent external programs
321 file_data_unref(file_data_new_group(fd->change->dest));
323 if (success) metadata_legacy_delete(fd, fd->change->dest);
327 gint metadata_queue_length(void)
329 return g_list_length(metadata_write_queue);
332 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
334 const gchar **k = keys;
338 if (strcmp(key, *k) == 0) return TRUE;
344 gboolean metadata_write_revert(FileData *fd, const gchar *key)
346 if (!fd->modified_xmp) return FALSE;
348 g_hash_table_remove(fd->modified_xmp, key);
350 if (g_hash_table_size(fd->modified_xmp) == 0)
352 metadata_write_queue_remove(fd);
356 /* reread the metadata to restore the original value */
357 file_data_increment_version(fd);
358 file_data_send_notification(fd, NOTIFY_REREAD);
363 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
365 if (!fd->modified_xmp)
367 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
369 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
371 metadata_cache_remove(fd, key);
375 exif_update_metadata(fd->exif, key, values);
377 metadata_write_queue_add(fd);
378 file_data_increment_version(fd);
379 file_data_send_notification(fd, NOTIFY_METADATA);
381 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
383 GList *work = fd->sidecar_files;
387 FileData *sfd = work->data;
390 if (filter_file_class(sfd->extension, FORMAT_CLASS_META)) continue;
392 metadata_write_list(sfd, key, values);
400 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
402 GList *list = g_list_append(NULL, g_strdup(value));
403 gboolean ret = metadata_write_list(fd, key, list);
404 string_list_free(list);
408 gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
412 g_snprintf(string, sizeof(string), "%llu", (unsigned long long) value);
413 return metadata_write_string(fd, key, string);
417 *-------------------------------------------------------------------
418 * keyword / comment read/write
419 *-------------------------------------------------------------------
422 static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
426 ssi = secure_open(path);
427 if (!ssi) return FALSE;
429 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
431 secure_fprintf(ssi, "[keywords]\n");
432 while (keywords && secsave_errno == SS_ERR_NONE)
434 const gchar *word = keywords->data;
435 keywords = keywords->next;
437 secure_fprintf(ssi, "%s\n", word);
439 secure_fputc(ssi, '\n');
441 secure_fprintf(ssi, "[comment]\n");
442 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
444 secure_fprintf(ssi, "#end\n");
446 return (secure_close(ssi) == 0);
449 static gboolean metadata_legacy_write(FileData *fd)
451 gboolean success = FALSE;
452 gchar *metadata_pathl;
455 gboolean have_keywords;
456 gboolean have_comment;
457 const gchar *comment;
458 GList *orig_keywords = NULL;
459 gchar *orig_comment = NULL;
461 g_assert(fd->change && fd->change->dest);
463 DEBUG_1("Saving comment: %s", fd->change->dest);
465 if (!fd->modified_xmp) return TRUE;
467 metadata_pathl = path_from_utf8(fd->change->dest);
469 have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, NULL, &keywords);
470 have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, NULL, &comment_l);
471 comment = (have_comment && comment_l) ? ((GList *)comment_l)->data : NULL;
473 if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
475 success = metadata_file_write(metadata_pathl,
476 have_keywords ? (GList *)keywords : orig_keywords,
477 have_comment ? comment : orig_comment);
479 g_free(metadata_pathl);
480 g_free(orig_comment);
481 string_list_free(orig_keywords);
486 static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
490 MetadataKey key = MK_NONE;
492 GString *comment_build = NULL;
494 f = fopen(path, "r");
495 if (!f) return FALSE;
497 while (fgets(s_buf, sizeof(s_buf), f))
501 if (*ptr == '#') continue;
502 if (*ptr == '[' && key != MK_COMMENT)
504 gchar *keystr = ++ptr;
507 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
512 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
514 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
526 while (*ptr != '\n' && *ptr != '\0') ptr++;
528 if (strlen(s_buf) > 0)
530 gchar *kw = utf8_validate_or_convert(s_buf);
532 list = g_list_prepend(list, kw);
537 if (!comment_build) comment_build = g_string_new("");
538 g_string_append(comment_build, s_buf);
547 *keywords = g_list_reverse(list);
551 string_list_free(list);
559 gchar *ptr = comment_build->str;
561 /* strip leading and trailing newlines */
562 while (*ptr == '\n') ptr++;
564 while (len > 0 && ptr[len - 1] == '\n') len--;
565 if (ptr[len] == '\n') len++; /* keep the last one */
568 gchar *text = g_strndup(ptr, len);
570 *comment = utf8_validate_or_convert(text);
574 g_string_free(comment_build, TRUE);
580 static void metadata_legacy_delete(FileData *fd, const gchar *except)
582 gchar *metadata_path;
583 gchar *metadata_pathl;
586 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
587 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
589 metadata_pathl = path_from_utf8(metadata_path);
590 unlink(metadata_pathl);
591 g_free(metadata_pathl);
592 g_free(metadata_path);
596 /* without exiv2: do not delete xmp metadata because we are not able to convert it,
598 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
599 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
601 metadata_pathl = path_from_utf8(metadata_path);
602 unlink(metadata_pathl);
603 g_free(metadata_pathl);
604 g_free(metadata_path);
609 static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
611 gchar *metadata_path;
612 gchar *metadata_pathl;
613 gboolean success = FALSE;
615 if (!fd) return FALSE;
617 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
618 if (!metadata_path) return FALSE;
620 metadata_pathl = path_from_utf8(metadata_path);
622 success = metadata_file_read(metadata_pathl, keywords, comment);
624 g_free(metadata_pathl);
625 g_free(metadata_path);
630 static GList *remove_duplicate_strings_from_list(GList *list)
633 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
634 GList *newlist = NULL;
638 gchar *key = work->data;
640 if (g_hash_table_lookup(hashtable, key) == NULL)
642 g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
643 newlist = g_list_prepend(newlist, key);
648 g_hash_table_destroy(hashtable);
651 return g_list_reverse(newlist);
654 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
658 const GList *cache_entry;
659 if (!fd) return NULL;
661 /* unwritten data overide everything */
662 if (fd->modified_xmp && format == METADATA_PLAIN)
664 list = g_hash_table_lookup(fd->modified_xmp, key);
665 if (list) return string_list_copy(list);
669 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
670 && (cache_entry = metadata_cache_get(fd, key)))
672 return string_list_copy(cache_entry->next);
676 Legacy metadata file is the primary source if it exists.
677 Merging the lists does not make much sense, because the existence of
678 legacy metadata file indicates that the other metadata sources are not
679 writable and thus it would not be possible to delete the keywords
680 that comes from the image file.
682 if (strcmp(key, KEYWORD_KEY) == 0)
684 if (metadata_legacy_read(fd, &list, NULL))
686 if (format == METADATA_PLAIN)
688 metadata_cache_update(fd, key, list);
693 else if (strcmp(key, COMMENT_KEY) == 0)
695 gchar *comment = NULL;
696 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
698 else if (strncmp(key, "file.", 5) == 0)
700 return g_list_append(NULL, metadata_file_info(fd, key, format));
703 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
704 if (!exif) return NULL;
705 list = exif_get_metadata(exif, key, format);
706 exif_free_fd(fd, exif);
708 if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
710 metadata_cache_update(fd, key, list);
716 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
718 GList *string_list = metadata_read_list(fd, key, format);
721 gchar *str = string_list->data;
722 string_list->data = NULL;
723 string_list_free(string_list);
729 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
733 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
734 if (!string) return fallback;
736 ret = g_ascii_strtoull(string, &endptr, 10);
737 if (string == endptr) ret = fallback;
742 gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
746 gdouble deg, min, sec;
748 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
749 if (!string) return fallback;
751 deg = g_ascii_strtod(string, &endptr);
754 min = g_ascii_strtod(endptr + 1, &endptr);
756 sec = g_ascii_strtod(endptr + 1, &endptr);
761 if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
763 coord = deg + min /60.0 + sec / 3600.0;
765 if (*endptr == 'S' || *endptr == 'W') coord = -coord;
772 log_printf("unable to parse GPS coordinate '%s'\n", string);
779 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
781 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
785 return metadata_write_string(fd, key, value);
789 gchar *new_string = g_strconcat(str, value, NULL);
790 gboolean ret = metadata_write_string(fd, key, new_string);
797 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
799 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
803 return metadata_write_list(fd, key, values);
808 list = g_list_concat(list, string_list_copy(values));
809 list = remove_duplicate_strings_from_list(list);
811 ret = metadata_write_list(fd, key, list);
812 string_list_free(list);
818 * \see find_string_in_list
820 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
822 gchar *string_casefold = g_utf8_casefold(string, -1);
826 gchar *haystack = list->data;
831 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
833 equal = (strcmp(haystack_casefold, string_casefold) == 0);
834 g_free(haystack_casefold);
838 g_free(string_casefold);
846 g_free(string_casefold);
851 * \see find_string_in_list
853 gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
857 gchar *haystack = list->data;
859 if (haystack && strcmp(haystack, string) == 0)
866 } // gchar *find_string_in_list_utf...
869 * \brief Find a existent string in a list.
871 * This is a switch between find_string_in_list_utf8case and
872 * find_string_in_list_utf8nocase to search with or without case for the
873 * existence of a string.
875 * \param list The list to search in
876 * \param string The string to search for
877 * \return The string or NULL
879 * \see find_string_in_list_utf8case
880 * \see find_string_in_list_utf8nocase
882 gchar *find_string_in_list(GList *list, const gchar *string)
884 if (options->metadata.keywords_case_sensitive)
885 return find_string_in_list_utf8case(list, string);
887 return find_string_in_list_utf8nocase(list, string);
890 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
892 GList *string_to_keywords_list(const gchar *text)
895 const gchar *ptr = text;
902 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
904 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
910 /* trim starting and ending whitespaces */
911 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
912 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
916 gchar *keyword = g_strndup(begin, l);
918 /* only add if not already in the list */
919 if (!find_string_in_list(list, keyword))
920 list = g_list_append(list, keyword);
934 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
936 /* FIXME: do not use global keyword_tree */
939 gboolean found = FALSE;
940 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
944 if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
945 keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
952 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
955 GList *keywords = NULL;
958 if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
960 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
962 if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
966 keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
970 keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
972 metadata_write_list(fd, KEYWORD_KEY, keywords);
975 string_list_free(keywords);
981 void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
984 FileDataGetMarkFunc get_mark_func;
985 FileDataSetMarkFunc set_mark_func;
986 gpointer mark_func_data;
990 for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
992 file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
993 if (get_mark_func == meta_data_get_keyword_mark)
995 GtkTreeIter old_kw_iter;
996 GList *old_path = mark_func_data;
998 if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
999 (i == mark || /* release any previous connection of given mark */
1000 keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
1002 file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
1003 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
1009 if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
1013 path = keyword_tree_get_path(keyword_tree, kw_iter);
1014 file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
1016 mark_str = g_strdup_printf("%d", mark + 1);
1017 gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
1024 *-------------------------------------------------------------------
1026 *-------------------------------------------------------------------
1031 GtkTreeStore *keyword_tree;
1033 gchar *keyword_get_name(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1036 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_NAME, &name, -1);
1040 gchar *keyword_get_casefold(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1043 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_CASEFOLD, &casefold, -1);
1047 gboolean keyword_get_is_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *iter)
1049 gboolean is_keyword;
1050 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1054 void keyword_set(GtkTreeStore *keyword_tree, GtkTreeIter *iter, const gchar *name, gboolean is_keyword)
1056 gchar *casefold = g_utf8_casefold(name, -1);
1057 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_MARK, "",
1058 KEYWORD_COLUMN_NAME, name,
1059 KEYWORD_COLUMN_CASEFOLD, casefold,
1060 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1064 gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1066 GtkTreePath *pa = gtk_tree_model_get_path(keyword_tree, a);
1067 GtkTreePath *pb = gtk_tree_model_get_path(keyword_tree, b);
1068 gint ret = gtk_tree_path_compare(pa, pb);
1069 gtk_tree_path_free(pa);
1070 gtk_tree_path_free(pb);
1074 gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
1076 GtkTreeIter parent_a;
1077 GtkTreeIter parent_b;
1079 gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
1080 gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
1082 if (valid_pa && valid_pb)
1084 return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
1088 return (!valid_pa && !valid_pb); /* both are toplevel */
1092 gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
1096 gboolean toplevel = FALSE;
1102 parent = *parent_ptr;
1106 toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
1113 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
1115 casefold = g_utf8_casefold(name, -1);
1120 if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
1122 if (options->metadata.keywords_case_sensitive)
1124 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1125 ret = strcmp(name, iter_name) == 0;
1130 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1131 ret = strcmp(casefold, iter_casefold) == 0;
1132 g_free(iter_casefold);
1133 } // if (options->metadata.tags_cas...
1137 if (result) *result = iter;
1140 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
1147 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1150 gchar *mark, *name, *casefold;
1151 gboolean is_keyword;
1153 /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
1154 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
1155 KEYWORD_COLUMN_NAME, &name,
1156 KEYWORD_COLUMN_CASEFOLD, &casefold,
1157 KEYWORD_COLUMN_IS_KEYWORD, &is_keyword, -1);
1159 gtk_tree_store_set(keyword_tree, to, KEYWORD_COLUMN_MARK, mark,
1160 KEYWORD_COLUMN_NAME, name,
1161 KEYWORD_COLUMN_CASEFOLD, casefold,
1162 KEYWORD_COLUMN_IS_KEYWORD, is_keyword, -1);
1168 void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1170 GtkTreeIter from_child;
1172 keyword_copy(keyword_tree, to, from);
1174 if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &from_child, from)) return;
1178 GtkTreeIter to_child;
1179 gtk_tree_store_append(keyword_tree, &to_child, to);
1180 keyword_copy_recursive(keyword_tree, &to_child, &from_child);
1181 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &from_child)) return;
1185 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
1187 keyword_copy_recursive(keyword_tree, to, from);
1188 keyword_delete(keyword_tree, from);
1191 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1194 GtkTreeIter iter = *iter_ptr;
1199 path = g_list_prepend(path, keyword_get_name(keyword_tree, &iter));
1200 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) break;
1206 gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *path)
1210 if (!gtk_tree_model_get_iter_first(keyword_tree, &iter)) return FALSE;
1214 GtkTreeIter children;
1217 gchar *name = keyword_get_name(keyword_tree, &iter);
1218 if (strcmp(name, path->data) == 0) break;
1220 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
1229 if (!gtk_tree_model_iter_children(keyword_tree, &children, &iter)) return FALSE;
1235 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
1237 if (!casefold_list) return FALSE;
1239 if (!keyword_get_is_keyword(keyword_tree, &iter))
1241 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1243 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1244 return FALSE; /* this should happen only on empty helpers */
1248 if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
1249 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1257 if (keyword_get_is_keyword(keyword_tree, &iter))
1259 GList *work = casefold_list;
1260 gboolean found = FALSE;
1261 gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
1264 const gchar *casefold = work->data;
1267 if (strcmp(iter_casefold, casefold) == 0)
1273 g_free(iter_casefold);
1274 if (!found) return FALSE;
1277 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1282 static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
1284 if (!kw_list) return FALSE;
1286 if (!keyword_get_is_keyword(keyword_tree, &iter))
1288 /* for the purpose of expanding and hiding, a helper is set if it has any children set */
1290 if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
1291 return FALSE; /* this should happen only on empty helpers */
1295 if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
1296 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
1304 if (keyword_get_is_keyword(keyword_tree, &iter))
1306 GList *work = kw_list;
1307 gboolean found = FALSE;
1308 gchar *iter_name = keyword_get_name(keyword_tree, &iter);
1311 const gchar *name = work->data;
1314 if (strcmp(iter_name, name) == 0)
1321 if (!found) return FALSE;
1324 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
1329 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
1332 GList *casefold_list = NULL;
1335 if (options->metadata.keywords_case_sensitive)
1337 ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
1344 const gchar *kw = work->data;
1347 casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
1350 ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
1352 string_list_free(casefold_list);
1358 void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1360 GtkTreeIter iter = *iter_ptr;
1365 if (keyword_get_is_keyword(keyword_tree, &iter))
1367 gchar *name = keyword_get_name(keyword_tree, &iter);
1368 if (!find_string_in_list(*kw_list, name))
1370 *kw_list = g_list_append(*kw_list, name);
1378 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1383 GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
1385 GtkTreeIter iter = *iter_ptr;
1386 GList *kw_list = NULL;
1392 if (keyword_get_is_keyword(keyword_tree, &iter))
1394 gchar *name = keyword_get_name(keyword_tree, &iter);
1395 kw_list = g_list_append(kw_list, name);
1398 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
1401 } // GList *keyword_tree_get(GtkTre...
1403 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1407 if (!keyword_get_is_keyword(keyword_tree, iter)) return;
1409 name = keyword_get_name(keyword_tree, iter);
1410 found = find_string_in_list(*kw_list, name);
1414 *kw_list = g_list_remove(*kw_list, found);
1420 static void keyword_tree_reset_recursive(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
1423 keyword_tree_reset1(keyword_tree, iter, kw_list);
1425 if (!gtk_tree_model_iter_children(keyword_tree, &child, iter)) return;
1429 keyword_tree_reset_recursive(keyword_tree, &child, kw_list);
1430 if (!gtk_tree_model_iter_next(keyword_tree, &child)) return;
1434 static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, GtkTreeIter *parent, GList *kw_list)
1438 if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
1439 return TRUE; /* this should happen only on empty helpers */
1443 if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
1444 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
1448 void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList **kw_list)
1450 GtkTreeIter iter = *iter_ptr;
1452 keyword_tree_reset_recursive(keyword_tree, &iter, kw_list);
1454 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1457 while (keyword_tree_check_empty_children(keyword_tree, &iter, *kw_list))
1460 keyword_tree_reset1(keyword_tree, &iter, kw_list);
1461 if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return;
1466 void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
1470 while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
1472 keyword_delete(keyword_tree, &child);
1475 meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
1477 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1480 gtk_tree_store_remove(keyword_tree, iter_ptr);
1484 void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1487 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1488 if (!g_list_find(list, id))
1490 list = g_list_prepend(list, id);
1491 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1495 void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
1498 gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1499 list = g_list_remove(list, id);
1500 gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
1503 gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
1506 gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
1507 return !!g_list_find(list, id);
1510 static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
1512 keyword_show_in(GTK_TREE_STORE(model), iter, data);
1516 void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
1518 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
1521 static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
1523 GtkTreeIter iter = *iter_ptr;
1526 if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
1528 keyword_hide_in(keyword_tree, &iter, id);
1529 /* no need to check children of hidden node */
1534 if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
1536 keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
1539 if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1543 void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1546 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
1547 keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
1550 static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
1552 GtkTreeIter iter = *iter_ptr;
1553 GList *keywords = data;
1554 gpointer id = keywords->data;
1555 keywords = keywords->next; /* hack */
1556 if (keyword_tree_is_set(model, &iter, keywords))
1561 keyword_show_in(GTK_TREE_STORE(model), &iter, id);
1562 if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
1569 void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
1571 /* hack: pass id to keyword_hide_unset_in_cb in the list */
1572 keywords = g_list_prepend(keywords, id);
1573 gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
1574 keywords = g_list_delete_link(keywords, keywords);
1578 void keyword_tree_new(void)
1580 if (keyword_tree) return;
1582 keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
1585 static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
1588 gtk_tree_store_append(keyword_tree, &iter, parent);
1589 keyword_set(keyword_tree, &iter, name, is_keyword);
1593 void keyword_tree_new_default(void)
1597 if (!keyword_tree) keyword_tree_new();
1599 i1 = keyword_tree_default_append(keyword_tree, NULL, _("People"), TRUE);
1600 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
1601 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
1602 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
1603 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
1604 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
1605 keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
1606 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Nature"), TRUE);
1607 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
1608 keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
1609 keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
1610 keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
1611 keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
1612 keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
1613 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
1614 keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
1615 keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
1616 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
1617 keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
1618 keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
1619 keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
1620 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
1621 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Art"), TRUE);
1622 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
1623 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
1624 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1625 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1626 i1 = keyword_tree_default_append(keyword_tree, NULL, _("City"), TRUE);
1627 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
1628 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
1629 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
1630 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Architecture"), TRUE);
1631 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
1632 keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
1633 keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
1634 keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
1635 keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
1636 keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
1637 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
1638 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
1639 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
1640 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Places"), FALSE);
1641 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Conditions"), FALSE);
1642 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
1643 keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
1644 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
1645 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
1646 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
1647 keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
1648 keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
1649 keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
1650 keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
1651 keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
1652 i1 = keyword_tree_default_append(keyword_tree, NULL, _("Photo"), FALSE);
1653 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
1654 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
1655 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
1656 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
1657 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
1658 i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
1662 static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GString *outstr, gint indent)
1664 GtkTreeIter iter = *iter_ptr;
1667 GtkTreeIter children;
1670 WRITE_NL(); WRITE_STRING("<keyword ");
1671 name = keyword_get_name(keyword_tree, &iter);
1672 write_char_option(outstr, indent, "name", name);
1674 write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
1675 if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
1679 keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
1681 WRITE_NL(); WRITE_STRING("</keyword>");
1687 if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
1691 void keyword_tree_write_config(GString *outstr, gint indent)
1694 WRITE_NL(); WRITE_STRING("<keyword_tree>");
1697 if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
1699 keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
1702 WRITE_NL(); WRITE_STRING("</keyword_tree>");
1705 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
1708 gboolean is_kw = TRUE;
1710 while (*attribute_names)
1712 const gchar *option = *attribute_names++;
1713 const gchar *value = *attribute_values++;
1715 if (READ_CHAR_FULL("name", name)) continue;
1716 if (READ_BOOL_FULL("kw", is_kw)) continue;
1718 log_printf("unknown attribute %s = %s\n", option, value);
1720 if (name && name[0])
1723 /* re-use existing keyword if any */
1724 if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
1726 gtk_tree_store_append(keyword_tree, &iter, parent);
1728 keyword_set(keyword_tree, &iter, name, is_kw);
1730 return gtk_tree_iter_copy(&iter);
1736 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */