Trim trailing white spaces.
[geeqie.git] / src / metadata.c
index c12b8fb..88c9cad 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * Geeqie
  * (C) 2004 John Ellis
- * Copyright (C) 2008 - 2009 The Geeqie Team
+ * Copyright (C) 2008 - 2012 The Geeqie Team
  *
  * Author: John Ellis, Laurent Monin
  *
@@ -23,7 +23,7 @@
 #include "ui_misc.h"
 #include "utilops.h"
 #include "filefilter.h"
-#include "layout.h"
+#include "layout_util.h"
 #include "rcfile.h"
 
 typedef enum {
@@ -32,11 +32,141 @@ typedef enum {
        MK_COMMENT
 } MetadataKey;
 
-static const gchar *group_keys[] = {KEYWORD_KEY, COMMENT_KEY, NULL}; /* tags that will be written to all files in a group */
+static const gchar *group_keys[] = { /* tags that will be written to all files in a group, options->metadata.sync_grouped_files */
+       "Xmp.dc.title",
+       "Xmp.photoshop.Urgency",
+       "Xmp.photoshop.Category",
+       "Xmp.photoshop.SupplementalCategory",
+       "Xmp.dc.subject",
+       "Xmp.iptc.Location",
+       "Xmp.photoshop.Instruction",
+       "Xmp.photoshop.DateCreated",
+       "Xmp.dc.creator",
+       "Xmp.photoshop.AuthorsPosition",
+       "Xmp.photoshop.City",
+       "Xmp.photoshop.State",
+       "Xmp.iptc.CountryCode",
+       "Xmp.photoshop.Country",
+       "Xmp.photoshop.TransmissionReference",
+       "Xmp.photoshop.Headline",
+       "Xmp.photoshop.Credit",
+       "Xmp.photoshop.Source",
+       "Xmp.dc.rights",
+       "Xmp.dc.description",
+       "Xmp.photoshop.CaptionWriter",
+       NULL};
 
 static gboolean metadata_write_queue_idle_cb(gpointer data);
-static gint metadata_legacy_write(FileData *fd);
+static gboolean metadata_legacy_write(FileData *fd);
 static void metadata_legacy_delete(FileData *fd, const gchar *except);
+static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment);
+
+
+/*
+ *-------------------------------------------------------------------
+ * long-term cache - keep keywords from whole dir in memory
+ *-------------------------------------------------------------------
+ */
+
+/* fd->cached metadata list of lists
+   each particular list contains key as a first entry, then the values
+*/
+
+static void metadata_cache_update(FileData *fd, const gchar *key, const GList *values)
+{
+       GList *work;
+       
+       work = fd->cached_metadata;
+       while (work)
+               {
+               GList *entry = work->data;
+               gchar *entry_key = entry->data;
+               
+               if (strcmp(entry_key, key) == 0)
+                       {
+                       /* key found - just replace values */
+                       GList *old_values = entry->next;
+                       entry->next = NULL;
+                       old_values->prev = NULL;
+                       string_list_free(old_values);
+                       work->data = g_list_append(entry, string_list_copy(values));
+                       DEBUG_1("updated %s %s\n", key, fd->path);
+                       return;
+                       }
+               work = work->next;
+               }
+       
+       /* key not found - prepend new entry */
+       fd->cached_metadata = g_list_prepend(fd->cached_metadata,
+                               g_list_prepend(string_list_copy(values), g_strdup(key)));
+       DEBUG_1("added %s %s\n", key, fd->path);
+
+}
+
+static const GList *metadata_cache_get(FileData *fd, const gchar *key)
+{
+       GList *work;
+       
+       work = fd->cached_metadata;
+       while (work)
+               {
+               GList *entry = work->data;
+               gchar *entry_key = entry->data;
+               
+               if (strcmp(entry_key, key) == 0)
+                       {
+                       /* key found */
+                       DEBUG_1("found %s %s\n", key, fd->path);
+                       return entry;
+                       }
+               work = work->next;
+               }
+       return NULL;
+       DEBUG_1("not found %s %s\n", key, fd->path);
+}
+
+static void metadata_cache_remove(FileData *fd, const gchar *key)
+{
+       GList *work;
+       
+       work = fd->cached_metadata;
+       while (work)
+               {
+               GList *entry = work->data;
+               gchar *entry_key = entry->data;
+               
+               if (strcmp(entry_key, key) == 0)
+                       {
+                       /* key found */
+                       string_list_free(entry);
+                       fd->cached_metadata = g_list_delete_link(fd->cached_metadata, work);
+                       DEBUG_1("removed %s %s\n", key, fd->path);
+                       return;
+                       }
+               work = work->next;
+               }
+       DEBUG_1("not removed %s %s\n", key, fd->path);
+}
+
+void metadata_cache_free(FileData *fd)
+{
+       GList *work;
+       if (fd->cached_metadata) DEBUG_1("freed %s\n", fd->path);
+       
+       work = fd->cached_metadata;
+       while (work)
+               {
+               GList *entry = work->data;
+               string_list_free(entry);
+               
+               work = work->next;
+               }
+       g_list_free(fd->cached_metadata);
+       fd->cached_metadata = NULL;
+}
+
+
+
 
 
 
@@ -47,7 +177,7 @@ static void metadata_legacy_delete(FileData *fd, const gchar *except);
  */
 
 static GList *metadata_write_queue = NULL;
-static gint metadata_write_idle_id = -1;
+static guint metadata_write_idle_id = 0; /* event source id */
 
 static void metadata_write_queue_add(FileData *fd)
 {
@@ -56,13 +186,13 @@ static void metadata_write_queue_add(FileData *fd)
                metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
                file_data_ref(fd);
                
-               layout_status_update_write_all();
+               layout_util_status_update_write_all();
                }
 
-       if (metadata_write_idle_id != -1) 
+       if (metadata_write_idle_id)
                {
                g_source_remove(metadata_write_idle_id);
-               metadata_write_idle_id = -1;
+               metadata_write_idle_id = 0;
                }
        
        if (options->metadata.confirm_after_timeout)
@@ -80,11 +210,11 @@ gboolean metadata_write_queue_remove(FileData *fd)
        metadata_write_queue = g_list_remove(metadata_write_queue, fd);
        
        file_data_increment_version(fd);
-       file_data_send_notification(fd, NOTIFY_TYPE_REREAD);
+       file_data_send_notification(fd, NOTIFY_REREAD);
 
        file_data_unref(fd);
 
-       layout_status_update_write_all();
+       layout_util_status_update_write_all();
        return TRUE;
 }
 
@@ -103,8 +233,25 @@ gboolean metadata_write_queue_remove_list(GList *list)
        return ret;
 }
 
+void metadata_notify_cb(FileData *fd, NotifyType type, gpointer data)
+{
+       if (type & (NOTIFY_REREAD | NOTIFY_CHANGE))
+               {
+               metadata_cache_free(fd);
+               
+               if (g_list_find(metadata_write_queue, fd))
+                       {
+                       DEBUG_1("Notify metadata: %s %04x", fd->path, type);
+                       if (!isname(fd->path))
+                               {
+                               /* ignore deleted files */
+                               metadata_write_queue_remove(fd);
+                               }
+                       }
+               }
+}
 
-gboolean metadata_write_queue_confirm(FileUtilDoneFunc done_func, gpointer done_data)
+gboolean metadata_write_queue_confirm(gboolean force_dialog, FileUtilDoneFunc done_func, gpointer done_data)
 {
        GList *work;
        GList *to_approve = NULL;
@@ -115,22 +262,27 @@ gboolean metadata_write_queue_confirm(FileUtilDoneFunc done_func, gpointer done_
                FileData *fd = work->data;
                work = work->next;
                
+               if (!isname(fd->path))
+                       {
+                       /* ignore deleted files */
+                       metadata_write_queue_remove(fd);
+                       continue;
+                       }
+               
                if (fd->change) continue; /* another operation in progress, skip this file for now */
                
                to_approve = g_list_prepend(to_approve, file_data_ref(fd));
                }
 
-       file_util_write_metadata(NULL, to_approve, NULL, done_func, done_data);
-       
-       filelist_free(to_approve);
+       file_util_write_metadata(NULL, to_approve, NULL, force_dialog, done_func, done_data);
        
        return (metadata_write_queue != NULL);
 }
 
 static gboolean metadata_write_queue_idle_cb(gpointer data)
 {
-       metadata_write_queue_confirm(NULL, NULL);
-       metadata_write_idle_id = -1;
+       metadata_write_queue_confirm(FALSE, NULL, NULL);
+       metadata_write_idle_id = 0;
        return FALSE;
 }
 
@@ -141,7 +293,7 @@ gboolean metadata_write_perform(FileData *fd)
        
        g_assert(fd->change);
        
-       if (fd->change->dest && 
+       if (fd->change->dest &&
            strcmp(extension_from_path(fd->change->dest), GQ_CACHE_EXT_METADATA) == 0)
                {
                success = metadata_legacy_write(fd);
@@ -150,23 +302,23 @@ gboolean metadata_write_perform(FileData *fd)
                }
 
        /* write via exiv2 */
-       /*  we can either use cached metadata which have fd->modified_xmp already applied 
+       /*  we can either use cached metadata which have fd->modified_xmp already applied
                                     or read metadata from file and apply fd->modified_xmp
            metadata are read also if the file was modified meanwhile */
-       exif = exif_read_fd(fd); 
+       exif = exif_read_fd(fd);
        if (!exif) return FALSE;
 
        success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
        exif_free_fd(fd, exif);
 
        if (fd->change->dest)
-               /* this will create a FileData for the sidecar and link it to the main file 
+               /* this will create a FileData for the sidecar and link it to the main file
                   (we can't wait until the sidecar is discovered by directory scanning because
-                   exif_read_fd is called before that and it would read the main file only and 
+                   exif_read_fd is called before that and it would read the main file only and
                    store the metadata in the cache)
                    FIXME: this does not catch new sidecars created by independent external programs
                */
-               file_data_unref(file_data_new_simple(fd->change->dest)); 
+               file_data_unref(file_data_new_group(fd->change->dest));
                
        if (success) metadata_legacy_delete(fd, fd->change->dest);
        return success;
@@ -189,6 +341,25 @@ static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
        return FALSE;
 }
 
+gboolean metadata_write_revert(FileData *fd, const gchar *key)
+{
+       if (!fd->modified_xmp) return FALSE;
+       
+       g_hash_table_remove(fd->modified_xmp, key);
+       
+       if (g_hash_table_size(fd->modified_xmp) == 0)
+               {
+               metadata_write_queue_remove(fd);
+               }
+       else
+               {
+               /* reread the metadata to restore the original value */
+               file_data_increment_version(fd);
+               file_data_send_notification(fd, NOTIFY_REREAD);
+               }
+       return TRUE;
+}
+
 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
 {
        if (!fd->modified_xmp)
@@ -196,13 +367,16 @@ gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values
                fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
                }
        g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
+       
+       metadata_cache_remove(fd, key);
+       
        if (fd->exif)
                {
                exif_update_metadata(fd->exif, key, values);
                }
        metadata_write_queue_add(fd);
        file_data_increment_version(fd);
-       file_data_send_notification(fd, NOTIFY_TYPE_INTERNAL);
+       file_data_send_notification(fd, NOTIFY_METADATA);
 
        if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
                {
@@ -213,7 +387,7 @@ gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values
                        FileData *sfd = work->data;
                        work = work->next;
                        
-                       if (filter_file_class(sfd->extension, FORMAT_CLASS_META)) continue; 
+                       if (filter_file_class(sfd->extension, FORMAT_CLASS_META)) continue;
 
                        metadata_write_list(sfd, key, values);
                        }
@@ -231,6 +405,13 @@ gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value
        return ret;
 }
 
+gboolean metadata_write_int(FileData *fd, const gchar *key, guint64 value)
+{
+       gchar string[50];
+       
+       g_snprintf(string, sizeof(string), "%llu", (unsigned long long) value);
+       return metadata_write_string(fd, key, string);
+}
 
 /*
  *-------------------------------------------------------------------
@@ -238,12 +419,9 @@ gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value
  *-------------------------------------------------------------------
  */
 
-static gint metadata_file_write(gchar *path, GHashTable *modified_xmp)
+static gboolean metadata_file_write(gchar *path, const GList *keywords, const gchar *comment)
 {
        SecureSaveInfo *ssi;
-       GList *keywords = g_hash_table_lookup(modified_xmp, KEYWORD_KEY);
-       GList *comment_l = g_hash_table_lookup(modified_xmp, COMMENT_KEY);
-       gchar *comment = comment_l ? comment_l->data : NULL;
 
        ssi = secure_open(path);
        if (!ssi) return FALSE;
@@ -268,25 +446,44 @@ static gint metadata_file_write(gchar *path, GHashTable *modified_xmp)
        return (secure_close(ssi) == 0);
 }
 
-static gint metadata_legacy_write(FileData *fd)
+static gboolean metadata_legacy_write(FileData *fd)
 {
-       gint success = FALSE;
+       gboolean success = FALSE;
+       gchar *metadata_pathl;
+       gpointer keywords;
+       gpointer comment_l;
+       gboolean have_keywords;
+       gboolean have_comment;
+       const gchar *comment;
+       GList *orig_keywords = NULL;
+       gchar *orig_comment = NULL;
 
        g_assert(fd->change && fd->change->dest);
-       gchar *metadata_pathl;
 
        DEBUG_1("Saving comment: %s", fd->change->dest);
 
+       if (!fd->modified_xmp) return TRUE;
+
        metadata_pathl = path_from_utf8(fd->change->dest);
 
-       success = metadata_file_write(metadata_pathl, fd->modified_xmp);
+       have_keywords = g_hash_table_lookup_extended(fd->modified_xmp, KEYWORD_KEY, NULL, &keywords);
+       have_comment = g_hash_table_lookup_extended(fd->modified_xmp, COMMENT_KEY, NULL, &comment_l);
+       comment = (have_comment && comment_l) ? ((GList *)comment_l)->data : NULL;
+       
+       if (!have_keywords || !have_comment) metadata_file_read(metadata_pathl, &orig_keywords, &orig_comment);
+       
+       success = metadata_file_write(metadata_pathl,
+                                     have_keywords ? (GList *)keywords : orig_keywords,
+                                     have_comment ? comment : orig_comment);
 
        g_free(metadata_pathl);
-
+       g_free(orig_comment);
+       string_list_free(orig_keywords);
+       
        return success;
 }
 
-static gint metadata_file_read(gchar *path, GList **keywords, gchar **comment)
+static gboolean metadata_file_read(gchar *path, GList **keywords, gchar **comment)
 {
        FILE *f;
        gchar s_buf[1024];
@@ -345,7 +542,7 @@ static gint metadata_file_read(gchar *path, GList **keywords, gchar **comment)
        
        fclose(f);
 
-       if (keywords) 
+       if (keywords)
                {
                *keywords = g_list_reverse(list);
                }
@@ -387,28 +584,34 @@ static void metadata_legacy_delete(FileData *fd, const gchar *except)
        if (!fd) return;
 
        metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
-       if (metadata_path && (!except || strcmp(metadata_path, except) != 0)) 
+       if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
                {
                metadata_pathl = path_from_utf8(metadata_path);
                unlink(metadata_pathl);
                g_free(metadata_pathl);
                g_free(metadata_path);
                }
+
+#ifdef HAVE_EXIV2
+       /* without exiv2: do not delete xmp metadata because we are not able to convert it,
+          just ignore it */
        metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
-       if (metadata_path && (!except || strcmp(metadata_path, except) != 0)) 
+       if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
                {
                metadata_pathl = path_from_utf8(metadata_path);
                unlink(metadata_pathl);
                g_free(metadata_pathl);
                g_free(metadata_path);
                }
+#endif
 }
 
-static gint metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
+static gboolean metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
 {
        gchar *metadata_path;
        gchar *metadata_pathl;
-       gint success = FALSE;
+       gboolean success = FALSE;
+       
        if (!fd) return FALSE;
 
        metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
@@ -452,6 +655,7 @@ GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
 {
        ExifData *exif;
        GList *list = NULL;
+       const GList *cache_entry;
        if (!fd) return NULL;
 
        /* unwritten data overide everything */
@@ -461,7 +665,14 @@ GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
                if (list) return string_list_copy(list);
                }
 
-       /* 
+
+       if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0
+           && (cache_entry = metadata_cache_get(fd, key)))
+               {
+               return string_list_copy(cache_entry->next);
+               }
+
+       /*
            Legacy metadata file is the primary source if it exists.
            Merging the lists does not make much sense, because the existence of
            legacy metadata file indicates that the other metadata sources are not
@@ -470,19 +681,35 @@ GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
        */
        if (strcmp(key, KEYWORD_KEY) == 0)
                {
-               if (metadata_legacy_read(fd, &list, NULL)) return list;
-               }
-
-       if (strcmp(key, COMMENT_KEY) == 0)
+               if (metadata_legacy_read(fd, &list, NULL))
+                       {
+                       if (format == METADATA_PLAIN)
+                               {
+                               metadata_cache_update(fd, key, list);
+                               }
+                       return list;
+                       }
+               }
+       else if (strcmp(key, COMMENT_KEY) == 0)
                {
                gchar *comment = NULL;
                if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
                }
+       else if (strncmp(key, "file.", 5) == 0)
+               {
+               return g_list_append(NULL, metadata_file_info(fd, key, format));
+               }
        
        exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
        if (!exif) return NULL;
        list = exif_get_metadata(exif, key, format);
        exif_free_fd(fd, exif);
+       
+       if (format == METADATA_PLAIN && strcmp(key, KEYWORD_KEY) == 0)
+               {
+               metadata_cache_update(fd, key, list);
+               }
+               
        return list;
 }
 
@@ -511,12 +738,49 @@ guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
        g_free(string);
        return ret;
 }
+
+gdouble metadata_read_GPS_coord(FileData *fd, const gchar *key, gdouble fallback)
+{
+       gdouble coord;
+       gchar *endptr;
+       gdouble deg, min, sec;
+       gboolean ok = FALSE;
+       gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
+       if (!string) return fallback;
+       
+       deg = g_ascii_strtod(string, &endptr);
+       if (*endptr == ',')
+               {
+               min = g_ascii_strtod(endptr + 1, &endptr);
+               if (*endptr == ',')
+                       sec = g_ascii_strtod(endptr + 1, &endptr);
+               else
+                       sec = 0.0;
+               
+               
+               if (*endptr == 'S' || *endptr == 'W' || *endptr == 'N' || *endptr == 'E')
+                       {
+                       coord = deg + min /60.0 + sec / 3600.0;
+                       ok = TRUE;
+                       if (*endptr == 'S' || *endptr == 'W') coord = -coord;
+                       }
+               }
+       
+       if (!ok)
+               {
+               coord = fallback;
+               log_printf("unable to parse GPS coordinate '%s'\n", string);
+               }
+       
+       g_free(string);
+       return coord;
+}
        
 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
 {
        gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
        
-       if (!str) 
+       if (!str)
                {
                return metadata_write_string(fd, key, value);
                }
@@ -534,7 +798,7 @@ gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *value
 {
        GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
        
-       if (!list) 
+       if (!list)
                {
                return metadata_write_list(fd, key, values);
                }
@@ -550,6 +814,9 @@ gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *value
                }
 }
 
+/**
+ * \see find_string_in_list
+ */
 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
 {
        gchar *string_casefold = g_utf8_casefold(string, -1);
@@ -557,7 +824,7 @@ gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
        while (list)
                {
                gchar *haystack = list->data;
-               
+
                if (haystack)
                        {
                        gboolean equal;
@@ -572,14 +839,53 @@ gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
                                return haystack;
                                }
                        }
-       
+
                list = list->next;
                }
-       
+
        g_free(string_casefold);
        return NULL;
 }
 
+/**
+ * \see find_string_in_list
+ */
+gchar *find_string_in_list_utf8case(GList *list, const gchar *string)
+{
+       while (list)
+               {
+               gchar *haystack = list->data;
+
+               if (haystack && strcmp(haystack, string) == 0)
+                       return haystack;
+
+               list = list->next;
+               } // while (list)
+
+       return NULL;
+} // gchar *find_string_in_list_utf...
+
+/**
+ * \brief Find a existent string in a list.
+ *
+ * This is a switch between find_string_in_list_utf8case and
+ * find_string_in_list_utf8nocase to search with or without case for the
+ * existence of a string.
+ *
+ * \param list The list to search in
+ * \param string The string to search for
+ * \return The string or NULL
+ *
+ * \see find_string_in_list_utf8case
+ * \see find_string_in_list_utf8nocase
+ */
+gchar *find_string_in_list(GList *list, const gchar *string)
+{
+       if (options->metadata.keywords_case_sensitive)
+               return find_string_in_list_utf8case(list, string);
+       else
+               return find_string_in_list_utf8nocase(list, string);
+}
 
 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
 
@@ -610,7 +916,7 @@ GList *string_to_keywords_list(const gchar *text)
                        gchar *keyword = g_strndup(begin, l);
 
                        /* only add if not already in the list */
-                       if (!find_string_in_list_utf8nocase(list, keyword))
+                       if (!find_string_in_list(list, keyword))
                                list = g_list_append(list, keyword);
                        else
                                g_free(keyword);
@@ -627,68 +933,93 @@ GList *string_to_keywords_list(const gchar *text)
 
 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
 {
+       /* FIXME: do not use global keyword_tree */
+       GList *path = data;
        GList *keywords;
        gboolean found = FALSE;
        keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
        if (keywords)
                {
-               GList *work = keywords;
+               GtkTreeIter iter;
+               if (keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path) &&
+                   keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
+                       found = TRUE;
 
-               while (work)
-                       {
-                       gchar *kw = work->data;
-                       work = work->next;
-                       
-                       if (strcmp(kw, data) == 0)
-                               {
-                               found = TRUE;
-                               break;
-                               }
-                       }
-               string_list_free(keywords);
                }
        return found;
 }
 
 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
 {
+       GList *path = data;
        GList *keywords = NULL;
-       gboolean found = FALSE;
-       gboolean changed = FALSE;
-       GList *work;
+       GtkTreeIter iter;
+       
+       if (!keyword_tree_get_iter(GTK_TREE_MODEL(keyword_tree), &iter, path)) return FALSE;
+
        keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
 
-       work = keywords;
+       if (!!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords) != !!value)
+               {
+               if (value)
+                       {
+                       keyword_tree_set(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
+                       }
+               else
+                       {
+                       keyword_tree_reset(GTK_TREE_MODEL(keyword_tree), &iter, &keywords);
+                       }
+               metadata_write_list(fd, KEYWORD_KEY, keywords);
+               }
 
-       while (work)
+       string_list_free(keywords);
+       return TRUE;
+}
+
+
+
+void meta_data_connect_mark_with_keyword(GtkTreeModel *keyword_tree, GtkTreeIter *kw_iter, gint mark)
+{
+
+       FileDataGetMarkFunc get_mark_func;
+       FileDataSetMarkFunc set_mark_func;
+       gpointer mark_func_data;
+
+       gint i;
+
+       for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
                {
-               gchar *kw = work->data;
-               
-               if (strcmp(kw, data) == 0)
+               file_data_get_registered_mark_func(i, &get_mark_func, &set_mark_func, &mark_func_data);
+               if (get_mark_func == meta_data_get_keyword_mark)
                        {
-                       found = TRUE;
-                       if (!value) 
+                       GtkTreeIter old_kw_iter;
+                       GList *old_path = mark_func_data;
+                       
+                       if (keyword_tree_get_iter(keyword_tree, &old_kw_iter, old_path) &&
+                           (i == mark || /* release any previous connection of given mark */
+                            keyword_compare(keyword_tree, &old_kw_iter, kw_iter) == 0)) /* or given keyword */
                                {
-                               changed = TRUE;
-                               keywords = g_list_delete_link(keywords, work);
-                               g_free(kw);
+                               file_data_register_mark_func(i, NULL, NULL, NULL, NULL);
+                               gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), &old_kw_iter, KEYWORD_COLUMN_MARK, "", -1);
                                }
-                       break;
                        }
-               work = work->next;
                }
-       if (value && !found) 
+
+
+       if (mark >= 0 && mark < FILEDATA_MARKS_SIZE)
                {
-               changed = TRUE;
-               keywords = g_list_append(keywords, g_strdup(data));
+               GList *path;
+               gchar *mark_str;
+               path = keyword_tree_get_path(keyword_tree, kw_iter);
+               file_data_register_mark_func(mark, meta_data_get_keyword_mark, meta_data_set_keyword_mark, path, (GDestroyNotify)string_list_free);
+               
+               mark_str = g_strdup_printf("%d", mark + 1);
+               gtk_tree_store_set(GTK_TREE_STORE(keyword_tree), kw_iter, KEYWORD_COLUMN_MARK, mark_str, -1);
+               g_free(mark_str);
                }
-       
-       if (changed) metadata_write_list(fd, KEYWORD_KEY, keywords);
-
-       string_list_free(keywords);
-       return TRUE;
 }
 
+
 /*
  *-------------------------------------------------------------------
  * keyword tree
@@ -740,12 +1071,86 @@ gboolean keyword_compare(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter
        return ret;
 }
 
+gboolean keyword_same_parent(GtkTreeModel *keyword_tree, GtkTreeIter *a, GtkTreeIter *b)
+{
+       GtkTreeIter parent_a;
+       GtkTreeIter parent_b;
+       
+       gboolean valid_pa = gtk_tree_model_iter_parent(keyword_tree, &parent_a, a);
+       gboolean valid_pb = gtk_tree_model_iter_parent(keyword_tree, &parent_b, b);
+
+       if (valid_pa && valid_pb)
+               {
+               return keyword_compare(keyword_tree, &parent_a, &parent_b) == 0;
+               }
+       else
+               {
+               return (!valid_pa && !valid_pb); /* both are toplevel */
+               }
+}
+
+gboolean keyword_exists(GtkTreeModel *keyword_tree, GtkTreeIter *parent_ptr, GtkTreeIter *sibling, const gchar *name, gboolean exclude_sibling, GtkTreeIter *result)
+{
+       GtkTreeIter parent;
+       GtkTreeIter iter;
+       gboolean toplevel = FALSE;
+       gboolean ret;
+       gchar *casefold;
+       
+       if (parent_ptr)
+               {
+               parent = *parent_ptr;
+               }
+       else if (sibling)
+               {
+               toplevel = !gtk_tree_model_iter_parent(keyword_tree, &parent, sibling);
+               }
+       else
+               {
+               toplevel = TRUE;
+               }
+       
+       if (!gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &iter, toplevel ? NULL : &parent)) return FALSE;
+       
+       casefold = g_utf8_casefold(name, -1);
+       ret = FALSE;
+       
+       while (TRUE)
+               {
+               if (!(exclude_sibling && sibling && keyword_compare(keyword_tree, &iter, sibling) == 0))
+                       {
+                       if (options->metadata.keywords_case_sensitive)
+                               {
+                               gchar *iter_name = keyword_get_name(keyword_tree, &iter);
+                               ret = strcmp(name, iter_name) == 0;
+                               g_free(iter_name);
+                               }
+                       else
+                               {
+                               gchar *iter_casefold = keyword_get_casefold(keyword_tree, &iter);
+                               ret = strcmp(casefold, iter_casefold) == 0;
+                               g_free(iter_casefold);
+                               } // if (options->metadata.tags_cas...
+                       }
+               if (ret)
+                       {
+                       if (result) *result = iter;
+                       break;
+                       }
+               if (!gtk_tree_model_iter_next(keyword_tree, &iter)) break;
+               }
+       g_free(casefold);
+       return ret;
+}
+
+
 void keyword_copy(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
 {
 
        gchar *mark, *name, *casefold;
        gboolean is_keyword;
 
+       /* do not copy KEYWORD_COLUMN_HIDE_IN, it fully shows the new subtree */
        gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), from, KEYWORD_COLUMN_MARK, &mark,
                                                KEYWORD_COLUMN_NAME, &name,
                                                KEYWORD_COLUMN_CASEFOLD, &casefold,
@@ -780,7 +1185,7 @@ void keyword_copy_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTree
 void keyword_move_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *to, GtkTreeIter *from)
 {
        keyword_copy_recursive(keyword_tree, to, from);
-       gtk_tree_store_remove(keyword_tree, from);
+       keyword_delete(keyword_tree, from);
 }
 
 GList *keyword_tree_get_path(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
@@ -815,7 +1220,7 @@ gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr
                        if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return FALSE;
                        }
                path = path->next;
-               if (!path) 
+               if (!path)
                        {
                        *iter_ptr = iter;
                        return TRUE;
@@ -830,6 +1235,20 @@ gboolean keyword_tree_get_iter(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr
 static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *casefold_list)
 {
        if (!casefold_list) return FALSE;
+
+       if (!keyword_get_is_keyword(keyword_tree, &iter))
+               {
+               /* for the purpose of expanding and hiding, a helper is set if it has any children set */
+               GtkTreeIter child;
+               if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
+                       return FALSE; /* this should happen only on empty helpers */
+
+               while (TRUE)
+                       {
+                       if (keyword_tree_is_set_casefold(keyword_tree, child, casefold_list)) return TRUE;
+                       if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
+                       }
+               }
        
        while (TRUE)
                {
@@ -860,26 +1279,79 @@ static gboolean keyword_tree_is_set_casefold(GtkTreeModel *keyword_tree, GtkTree
                }
 }
 
+static gboolean keyword_tree_is_set_casefull(GtkTreeModel *keyword_tree, GtkTreeIter iter, GList *kw_list)
+{
+       if (!kw_list) return FALSE;
+
+       if (!keyword_get_is_keyword(keyword_tree, &iter))
+               {
+               /* for the purpose of expanding and hiding, a helper is set if it has any children set */
+               GtkTreeIter child;
+               if (!gtk_tree_model_iter_children(keyword_tree, &child, &iter))
+                       return FALSE; /* this should happen only on empty helpers */
+
+               while (TRUE)
+                       {
+                       if (keyword_tree_is_set_casefull(keyword_tree, child, kw_list)) return TRUE;
+                       if (!gtk_tree_model_iter_next(keyword_tree, &child)) return FALSE;
+                       }
+               }
+
+       while (TRUE)
+               {
+               GtkTreeIter parent;
+
+               if (keyword_get_is_keyword(keyword_tree, &iter))
+                       {
+                       GList *work = kw_list;
+                       gboolean found = FALSE;
+                       gchar *iter_name = keyword_get_name(keyword_tree, &iter);
+                       while (work)
+                               {
+                               const gchar *name = work->data;
+                               work = work->next;
+
+                               if (strcmp(iter_name, name) == 0)
+                                       {
+                                       found = TRUE;
+                                       break;
+                                       }
+                               }
+                       g_free(iter_name);
+                       if (!found) return FALSE;
+                       }
+
+               if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return TRUE;
+               iter = parent;
+               }
+}
+
 gboolean keyword_tree_is_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList *kw_list)
 {
        gboolean ret;
        GList *casefold_list = NULL;
        GList *work;
 
-       if (!keyword_get_is_keyword(keyword_tree, iter)) return FALSE;
-       
-       work = kw_list;
-       while (work)
+       if (options->metadata.keywords_case_sensitive)
                {
-               const gchar *kw = work->data;
-               work = work->next;
-               
-               casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
+               ret = keyword_tree_is_set_casefull(keyword_tree, *iter, kw_list);
                }
-       
-       ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
-       
-       string_list_free(casefold_list);
+       else
+               {
+               work = kw_list;
+               while (work)
+                       {
+                       const gchar *kw = work->data;
+                       work = work->next;
+
+                       casefold_list = g_list_prepend(casefold_list, g_utf8_casefold(kw, -1));
+                       }
+
+               ret = keyword_tree_is_set_casefold(keyword_tree, *iter, casefold_list);
+
+               string_list_free(casefold_list);
+               }
+
        return ret;
 }
 
@@ -893,7 +1365,7 @@ void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *
                if (keyword_get_is_keyword(keyword_tree, &iter))
                        {
                        gchar *name = keyword_get_name(keyword_tree, &iter);
-                       if (!find_string_in_list_utf8nocase(*kw_list, name))
+                       if (!find_string_in_list(*kw_list, name))
                                {
                                *kw_list = g_list_append(*kw_list, name);
                                }
@@ -908,6 +1380,26 @@ void keyword_tree_set(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList *
                }
 }
 
+GList *keyword_tree_get(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr)
+{
+       GtkTreeIter iter = *iter_ptr;
+       GList *kw_list = NULL;
+
+       while (TRUE)
+               {
+               GtkTreeIter parent;
+
+               if (keyword_get_is_keyword(keyword_tree, &iter))
+                       {
+                       gchar *name = keyword_get_name(keyword_tree, &iter);
+                       kw_list = g_list_append(kw_list, name);
+                       }
+
+               if (!gtk_tree_model_iter_parent(keyword_tree, &parent, &iter)) return kw_list;
+               iter = parent;
+               }
+} // GList *keyword_tree_get(GtkTre...
+
 static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, GList **kw_list)
 {
        gchar *found;
@@ -915,7 +1407,7 @@ static void keyword_tree_reset1(GtkTreeModel *keyword_tree, GtkTreeIter *iter, G
        if (!keyword_get_is_keyword(keyword_tree, iter)) return;
 
        name = keyword_get_name(keyword_tree, iter);
-       found = find_string_in_list_utf8nocase(*kw_list, name);
+       found = find_string_in_list(*kw_list, name);
 
        if (found)
                {
@@ -943,25 +1435,13 @@ static gboolean keyword_tree_check_empty_children(GtkTreeModel *keyword_tree, Gt
 {
        GtkTreeIter iter;
        
-       if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent)) 
+       if (!gtk_tree_model_iter_children(keyword_tree, &iter, parent))
                return TRUE; /* this should happen only on empty helpers */
 
        while (TRUE)
                {
-               if (keyword_get_is_keyword(keyword_tree, &iter))
-                       {
-                       if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
-                       }
-               else
-                       {
-                       /* for helpers we have to check recursively */
-                       if (!keyword_tree_check_empty_children(keyword_tree, &iter, kw_list)) return FALSE;
-                       }
-               
-               if (!gtk_tree_model_iter_next(keyword_tree, &iter))
-                       {
-                       return TRUE;
-                       }
+               if (keyword_tree_is_set(keyword_tree, &iter, kw_list)) return FALSE;
+               if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return TRUE;
                }
 }
 
@@ -983,52 +1463,199 @@ void keyword_tree_reset(GtkTreeModel *keyword_tree, GtkTreeIter *iter_ptr, GList
                }
 }
 
-void keyword_tree_new(void)
+void keyword_delete(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr)
 {
-       if (keyword_tree) return;
+       GList *list;
+       GtkTreeIter child;
+       while (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, iter_ptr))
+               {
+               keyword_delete(keyword_tree, &child);
+               }
+       
+       meta_data_connect_mark_with_keyword(GTK_TREE_MODEL(keyword_tree), iter_ptr, -1);
+
+       gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter_ptr, KEYWORD_COLUMN_HIDE_IN, &list, -1);
+       g_list_free(list);
        
-       keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
+       gtk_tree_store_remove(keyword_tree, iter_ptr);
 }
 
 
-void keyword_tree_new_default(void)
+void keyword_hide_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
 {
-       if (keyword_tree) return;
-       
-       keyword_tree_new();
+       GList *list;
+       gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
+       if (!g_list_find(list, id))
+               {
+               list = g_list_prepend(list, id);
+               gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
+               }
+}
 
-       GtkTreeIter i1, i2, i3;
+void keyword_show_in(GtkTreeStore *keyword_tree, GtkTreeIter *iter, gpointer id)
+{
+       GList *list;
+       gtk_tree_model_get(GTK_TREE_MODEL(keyword_tree), iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
+       list = g_list_remove(list, id);
+       gtk_tree_store_set(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, list, -1);
+}
 
-       gtk_tree_store_append(keyword_tree, &i1, NULL);
-       keyword_set(keyword_tree, &i1, "animal", TRUE);
+gboolean keyword_is_hidden_in(GtkTreeModel *keyword_tree, GtkTreeIter *iter, gpointer id)
+{
+       GList *list;
+       gtk_tree_model_get(keyword_tree, iter, KEYWORD_COLUMN_HIDE_IN, &list, -1);
+       return !!g_list_find(list, id);
+}
 
-               gtk_tree_store_append(keyword_tree, &i2, &i1);
-               keyword_set(keyword_tree, &i2, "mammal", TRUE);
+static gboolean keyword_show_all_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
+{
+       keyword_show_in(GTK_TREE_STORE(model), iter, data);
+       return FALSE;
+}
 
-                       gtk_tree_store_append(keyword_tree, &i3, &i2);
-                       keyword_set(keyword_tree, &i3, "dog", TRUE);
+void keyword_show_all_in(GtkTreeStore *keyword_tree, gpointer id)
+{
+       gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_all_in_cb, id);
+}
 
-                       gtk_tree_store_append(keyword_tree, &i3, &i2);
-                       keyword_set(keyword_tree, &i3, "cat", TRUE);
+static void keyword_hide_unset_in_recursive(GtkTreeStore *keyword_tree, GtkTreeIter *iter_ptr, gpointer id, GList *keywords)
+{
+       GtkTreeIter iter = *iter_ptr;
+       while (TRUE)
+               {
+               if (!keyword_tree_is_set(GTK_TREE_MODEL(keyword_tree), &iter, keywords))
+                       {
+                       keyword_hide_in(keyword_tree, &iter, id);
+                       /* no need to check children of hidden node */
+                       }
+               else
+                       {
+                       GtkTreeIter child;
+                       if (gtk_tree_model_iter_children(GTK_TREE_MODEL(keyword_tree), &child, &iter))
+                               {
+                               keyword_hide_unset_in_recursive(keyword_tree, &child, id, keywords);
+                               }
+                       }
+               if (!gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_tree), &iter)) return;
+               }
+}
 
-               gtk_tree_store_append(keyword_tree, &i2, &i1);
-               keyword_set(keyword_tree, &i2, "insect", TRUE);
+void keyword_hide_unset_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
+{
+       GtkTreeIter iter;
+       if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) return;
+       keyword_hide_unset_in_recursive(keyword_tree, &iter, id, keywords);
+}
 
-                       gtk_tree_store_append(keyword_tree, &i3, &i2);
-                       keyword_set(keyword_tree, &i3, "fly", TRUE);
+static gboolean keyword_show_set_in_cb(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter_ptr, gpointer data)
+{
+       GtkTreeIter iter = *iter_ptr;
+       GList *keywords = data;
+       gpointer id = keywords->data;
+       keywords = keywords->next; /* hack */
+       if (keyword_tree_is_set(model, &iter, keywords))
+               {
+               while (TRUE)
+                       {
+                       GtkTreeIter parent;
+                       keyword_show_in(GTK_TREE_STORE(model), &iter, id);
+                       if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(keyword_tree), &parent, &iter)) break;
+                       iter = parent;
+                       }
+               }
+       return FALSE;
+}
 
-                       gtk_tree_store_append(keyword_tree, &i3, &i2);
-                       keyword_set(keyword_tree, &i3, "dragonfly", TRUE);
+void keyword_show_set_in(GtkTreeStore *keyword_tree, gpointer id, GList *keywords)
+{
+       /* hack: pass id to keyword_hide_unset_in_cb in the list */
+       keywords = g_list_prepend(keywords, id);
+       gtk_tree_model_foreach(GTK_TREE_MODEL(keyword_tree), keyword_show_set_in_cb, keywords);
+       keywords = g_list_delete_link(keywords, keywords);
+}
 
-       gtk_tree_store_append(keyword_tree, &i1, NULL);
-       keyword_set(keyword_tree, &i1, "daytime", FALSE);
 
-               gtk_tree_store_append(keyword_tree, &i2, &i1);
-               keyword_set(keyword_tree, &i2, "morning", TRUE);
+void keyword_tree_new(void)
+{
+       if (keyword_tree) return;
+       
+       keyword_tree = gtk_tree_store_new(KEYWORD_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_POINTER);
+}
 
-               gtk_tree_store_append(keyword_tree, &i2, &i1);
-               keyword_set(keyword_tree, &i2, "noon", TRUE);
+static GtkTreeIter keyword_tree_default_append(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar *name, gboolean is_keyword)
+{
+       GtkTreeIter iter;
+       gtk_tree_store_append(keyword_tree, &iter, parent);
+       keyword_set(keyword_tree, &iter, name, is_keyword);
+       return iter;
+}
 
+void keyword_tree_new_default(void)
+{
+       GtkTreeIter i1, i2;
+
+       if (!keyword_tree) keyword_tree_new();
+
+       i1 = keyword_tree_default_append(keyword_tree, NULL, _("People"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Family"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Free time"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Children"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sport"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Culture"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Festival"), TRUE);
+       i1 = keyword_tree_default_append(keyword_tree, NULL, _("Nature"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Animal"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Bird"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Insect"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Pets"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Wildlife"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Zoo"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Plant"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Tree"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Flower"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Water"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("River"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Lake"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Sea"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Landscape"), TRUE);
+       i1 = keyword_tree_default_append(keyword_tree, NULL, _("Art"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Statue"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Painting"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
+       i1 = keyword_tree_default_append(keyword_tree, NULL, _("City"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Park"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Street"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Square"), TRUE);
+       i1 = keyword_tree_default_append(keyword_tree, NULL, _("Architecture"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Buildings"), FALSE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("House"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Cathedral"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Palace"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Castle"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Bridge"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Interior"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Historic"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Modern"), TRUE);
+       i1 = keyword_tree_default_append(keyword_tree, NULL, _("Places"), FALSE);
+       i1 = keyword_tree_default_append(keyword_tree, NULL, _("Conditions"), FALSE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Night"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Lights"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Reflections"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Sun"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Weather"), FALSE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Fog"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Rain"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Clouds"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Snow"), TRUE);
+                       keyword_tree_default_append(keyword_tree, &i2, _("Sunny weather"), TRUE);
+       i1 = keyword_tree_default_append(keyword_tree, NULL, _("Photo"), FALSE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Edited"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Detail"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Macro"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Portrait"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Black and White"), TRUE);
+               i2 = keyword_tree_default_append(keyword_tree, &i1, _("Perspective"), TRUE);
 }
 
 
@@ -1040,21 +1667,23 @@ static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIt
                GtkTreeIter children;
                gchar *name;
 
-               WRITE_STRING("<keyword\n");
-               indent++;
+               WRITE_NL(); WRITE_STRING("<keyword ");
                name = keyword_get_name(keyword_tree, &iter);
                write_char_option(outstr, indent, "name", name);
                g_free(name);
                write_bool_option(outstr, indent, "kw", keyword_get_is_keyword(keyword_tree, &iter));
-               indent--;
-               WRITE_STRING(">\n");
-               indent++;
-               if (gtk_tree_model_iter_children(keyword_tree, &children, &iter)) 
+               if (gtk_tree_model_iter_children(keyword_tree, &children, &iter))
                        {
+                       WRITE_STRING(">");
+                       indent++;
                        keyword_tree_node_write_config(keyword_tree, &children, outstr, indent);
+                       indent--;
+                       WRITE_NL(); WRITE_STRING("</keyword>");
+                       }
+               else
+                       {
+                       WRITE_STRING("/>");
                        }
-               indent--;
-               WRITE_STRING("</keyword>\n");
                if (!gtk_tree_model_iter_next(keyword_tree, &iter)) return;
                }
 }
@@ -1062,7 +1691,7 @@ static void keyword_tree_node_write_config(GtkTreeModel *keyword_tree, GtkTreeIt
 void keyword_tree_write_config(GString *outstr, gint indent)
 {
        GtkTreeIter iter;
-       WRITE_STRING("<keyword_tree>\n");
+       WRITE_NL(); WRITE_STRING("<keyword_tree>");
        indent++;
        
        if (keyword_tree && gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter))
@@ -1070,7 +1699,7 @@ void keyword_tree_write_config(GString *outstr, gint indent)
                keyword_tree_node_write_config(GTK_TREE_MODEL(keyword_tree), &iter, outstr, indent);
                }
        indent--;
-       WRITE_STRING("</keyword_tree>\n");
+       WRITE_NL(); WRITE_STRING("</keyword_tree>");
 }
 
 GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *parent, const gchar **attribute_names, const gchar **attribute_values)
@@ -1086,12 +1715,16 @@ GtkTreeIter *keyword_add_from_config(GtkTreeStore *keyword_tree, GtkTreeIter *pa
                if (READ_CHAR_FULL("name", name)) continue;
                if (READ_BOOL_FULL("kw", is_kw)) continue;
 
-               DEBUG_1("unknown attribute %s = %s", option, value);
+               log_printf("unknown attribute %s = %s\n", option, value);
                }
-       if (name && name[0]) 
+       if (name && name[0])
                {
                GtkTreeIter iter;
-               gtk_tree_store_append(keyword_tree, &iter, parent);
+               /* re-use existing keyword if any */
+               if (!keyword_exists(GTK_TREE_MODEL(keyword_tree), parent, NULL, name, FALSE, &iter))
+                       {
+                       gtk_tree_store_append(keyword_tree, &iter, parent);
+                       }
                keyword_set(keyword_tree, &iter, name, is_kw);
                g_free(name);
                return gtk_tree_iter_copy(&iter);