Merge remote-tracking branches 'merge-requests/6' and 'merge-requests/7'
[geeqie.git] / src / filedata.c
index 48b8350..c99f3a9 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * Geeqie
  * (C) 2006 John Ellis
- * Copyright (C) 2008 - 2009 The Geeqie Team
+ * Copyright (C) 2008 - 2012 The Geeqie Team
  *
  * Author: John Ellis
  *
 #include "trash.h"
 #include "histogram.h"
 
+#include "exif.h"
+
+#include <errno.h>
+
+#ifdef DEBUG_FILEDATA
+gint global_file_data_count = 0;
+#endif
 
 static GHashTable *file_data_pool = NULL;
 static GHashTable *file_data_planned_change_hash = NULL;
-static GHashTable *file_data_basename_hash = NULL;
 
-static gint sidecar_file_priority(const gchar *path);
+static gint sidecar_file_priority(const gchar *extension);
+static void file_data_check_sidecars(const GList *basename_list);
+static void file_data_disconnect_sidecar_file(FileData *target, FileData *sfd);
 
 
+static SortType filelist_sort_method = SORT_NONE;
+static gboolean filelist_sort_ascend = TRUE;
+
 /*
  *-----------------------------------------------------------------------------
  * text conversion utils
@@ -115,7 +126,7 @@ const gchar *text_from_time(time_t t)
        btime = localtime(&t);
 
        /* the %x warning about 2 digit years is not an error */
-       buflen = strftime(buf, sizeof(buf), "%x %H:%M", btime);
+       buflen = strftime(buf, sizeof(buf), "%x %X", btime);
        if (buflen < 1) return "";
 
        g_free(ret);
@@ -132,86 +143,137 @@ const gchar *text_from_time(time_t t)
 
 /*
  *-----------------------------------------------------------------------------
- * file info struct
+ * changed files detection and notification
  *-----------------------------------------------------------------------------
  */
 
-FileData *file_data_merge_sidecar_files(FileData *target, FileData *source);
-static void file_data_check_sidecars(FileData *fd, gboolean stat_sidecars);
-FileData *file_data_disconnect_sidecar_file(FileData *target, FileData *sfd);
-
-
 void file_data_increment_version(FileData *fd)
 {
        fd->version++;
        fd->valid_marks = 0;
-       if (fd->parent) 
+       if (fd->parent)
                {
                fd->parent->version++;
                fd->parent->valid_marks = 0;
                }
 }
 
-static void file_data_basename_hash_insert(FileData *fd)
+static gboolean file_data_check_changed_single_file(FileData *fd, struct stat *st)
 {
-       GList *list;
-       const gchar *ext = extension_from_path(fd->path);
-       gchar *basename = ext ? g_strndup(fd->path, ext - fd->path) : g_strdup(fd->path);
-       if (!file_data_basename_hash)
-               file_data_basename_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
-       
-       list = g_hash_table_lookup(file_data_basename_hash, basename);
-       
-       if (!g_list_find(list, fd))
+       if (fd->size != st->st_size ||
+           fd->date != st->st_mtime)
                {
-               list = g_list_prepend(list, fd);
-               g_hash_table_insert(file_data_basename_hash, basename, list);
+               fd->size = st->st_size;
+               fd->date = st->st_mtime;
+               fd->mode = st->st_mode;
+               if (fd->thumb_pixbuf) g_object_unref(fd->thumb_pixbuf);
+               fd->thumb_pixbuf = NULL;
+               file_data_increment_version(fd);
+               file_data_send_notification(fd, NOTIFY_REREAD);
+               return TRUE;
                }
-       else 
+       return FALSE;
+}
+
+static gboolean file_data_check_changed_files_recursive(FileData *fd, struct stat *st)
+{
+       gboolean ret = FALSE;
+       GList *work;
+
+       ret = file_data_check_changed_single_file(fd, st);
+
+       work = fd->sidecar_files;
+       while (work)
                {
-               g_free(basename);
+               FileData *sfd = work->data;
+               struct stat st;
+               work = work->next;
+
+               if (!stat_utf8(sfd->path, &st))
+                       {
+                       fd->size = 0;
+                       fd->date = 0;
+                       file_data_ref(sfd);
+                       file_data_disconnect_sidecar_file(fd, sfd);
+                       ret = TRUE;
+                       file_data_increment_version(sfd);
+                       file_data_send_notification(sfd, NOTIFY_REREAD);
+                       file_data_unref(sfd);
+                       continue;
+                       }
+
+               ret |= file_data_check_changed_files_recursive(sfd, &st);
                }
+       return ret;
 }
 
-static void file_data_basename_hash_remove(FileData *fd)
+
+gboolean file_data_check_changed_files(FileData *fd)
 {
-       GList *list;
-       const gchar *ext = extension_from_path(fd->path);
-       gchar *basename = ext ? g_strndup(fd->path, ext - fd->path) : g_strdup(fd->path);
-       if (!file_data_basename_hash)
-               file_data_basename_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
-       
-       list = g_hash_table_lookup(file_data_basename_hash, basename);
-       
-       list = g_list_remove(list, fd);
-       
-       if (list)
-               {
-               g_hash_table_insert(file_data_basename_hash, basename, list);
-               }
-       else 
-               {
-               g_hash_table_remove(file_data_basename_hash, basename);
-               g_free(basename);
+       gboolean ret = FALSE;
+       struct stat st;
+
+       if (fd->parent) fd = fd->parent;
+
+       if (!stat_utf8(fd->path, &st))
+               {
+               GList *sidecars;
+               GList *work;
+               FileData *sfd = NULL;
+
+               /* parent is missing, we have to rebuild whole group */
+               ret = TRUE;
+               fd->size = 0;
+               fd->date = 0;
+
+               /* file_data_disconnect_sidecar_file might delete the file,
+                  we have to keep the reference to prevent this */
+               sidecars = filelist_copy(fd->sidecar_files);
+               file_data_ref(fd);
+               work = sidecars;
+               while (work)
+                       {
+                       sfd = work->data;
+                       work = work->next;
+
+                       file_data_disconnect_sidecar_file(fd, sfd);
+                       }
+               file_data_check_sidecars(sidecars); /* this will group the sidecars back together */
+               /* now we can release the sidecars */
+               filelist_free(sidecars);
+               file_data_increment_version(fd);
+               file_data_send_notification(fd, NOTIFY_REREAD);
+               file_data_unref(fd);
+               }
+       else
+               {
+               ret |= file_data_check_changed_files_recursive(fd, &st);
                }
+
+       return ret;
 }
 
+/*
+ *-----------------------------------------------------------------------------
+ * file name, extension, sorting, ...
+ *-----------------------------------------------------------------------------
+ */
+
 static void file_data_set_collate_keys(FileData *fd)
 {
        gchar *caseless_name;
+       gchar *valid_name;
 
-       caseless_name = g_utf8_casefold(fd->name, -1);
+       valid_name = g_filename_display_name(fd->name);
+       caseless_name = g_utf8_casefold(valid_name, -1);
 
        g_free(fd->collate_key_name);
        g_free(fd->collate_key_name_nocase);
 
-#if GLIB_CHECK_VERSION(2, 8, 0)
-       fd->collate_key_name = g_utf8_collate_key_for_filename(fd->name, -1);
-       fd->collate_key_name_nocase = g_utf8_collate_key_for_filename(caseless_name, -1);
-#else
-       fd->collate_key_name = g_utf8_collate_key(fd->name, -1);
+       fd->collate_key_name = g_utf8_collate_key(valid_name, -1);
        fd->collate_key_name_nocase = g_utf8_collate_key(caseless_name, -1);
-#endif
+
+       g_free(valid_name);
        g_free(caseless_name);
 }
 
@@ -220,8 +282,6 @@ static void file_data_set_path(FileData *fd, const gchar *path)
        g_assert(path /* && *path*/); /* view_dir_tree uses FileData with zero length path */
        g_assert(file_data_pool);
 
-       if (fd->path) file_data_basename_hash_remove(fd);
-       
        g_free(fd->path);
 
        if (fd->original_path)
@@ -268,98 +328,29 @@ static void file_data_set_path(FileData *fd, const gchar *path)
                return;
                }
 
-       fd->extension = extension_from_path(fd->path);
+       fd->extension = registered_extension_from_path(fd->path);
        if (fd->extension == NULL)
                {
                fd->extension = fd->name + strlen(fd->name);
                }
 
-       file_data_basename_hash_insert(fd); /* we can ignore the special cases above - they don't have extensions */
-
+       fd->sidecar_priority = sidecar_file_priority(fd->extension);
        file_data_set_collate_keys(fd);
 }
 
-static gboolean file_data_check_changed_files_recursive(FileData *fd, struct stat *st)
-{
-       gboolean ret = FALSE;
-       GList *work;
-       
-       if (fd->size != st->st_size ||
-           fd->date != st->st_mtime)
-               {
-               fd->size = st->st_size;
-               fd->date = st->st_mtime;
-               fd->mode = st->st_mode;
-               if (fd->thumb_pixbuf) g_object_unref(fd->thumb_pixbuf);
-               fd->thumb_pixbuf = NULL;
-               file_data_increment_version(fd);
-               file_data_send_notification(fd, NOTIFY_REREAD);
-               ret = TRUE;
-               }
-
-       work = fd->sidecar_files;
-       while (work)
-               {
-               FileData *sfd = work->data;
-               struct stat st;
-               work = work->next;
-
-               if (!stat_utf8(sfd->path, &st))
-                       {
-                       fd->size = 0;
-                       fd->date = 0;
-                       file_data_disconnect_sidecar_file(fd, sfd);
-                       ret = TRUE;
-                       continue;
-                       }
-
-               ret |= file_data_check_changed_files_recursive(sfd, &st);
-               }
-       return ret;
-}
-
-
-gboolean file_data_check_changed_files(FileData *fd)
-{
-       gboolean ret = FALSE;
-       struct stat st;
-       
-       if (fd->parent) fd = fd->parent;
-
-       if (!stat_utf8(fd->path, &st))
-               {
-               GList *work;
-               FileData *sfd = NULL;
-
-               /* parent is missing, we have to rebuild whole group */
-               ret = TRUE;
-               fd->size = 0;
-               fd->date = 0;
-               
-               work = fd->sidecar_files;
-               while (work)
-                       {
-                       sfd = work->data;
-                       work = work->next;
-               
-                       file_data_disconnect_sidecar_file(fd, sfd);
-                       }
-               if (sfd) file_data_check_sidecars(sfd, FALSE); /* this will group the sidecars back together */
-               file_data_send_notification(fd, NOTIFY_REREAD);
-               }
-       else
-               {
-               ret |= file_data_check_changed_files_recursive(fd, &st);
-               }
-
-       return ret;
-}
+/*
+ *-----------------------------------------------------------------------------
+ * create or reuse Filedata
+ *-----------------------------------------------------------------------------
+ */
 
-static FileData *file_data_new(const gchar *path_utf8, struct stat *st, gboolean check_sidecars, gboolean stat_sidecars)
+static FileData *file_data_new(const gchar *path_utf8, struct stat *st, gboolean disable_sidecars)
 {
        FileData *fd;
 
-       DEBUG_2("file_data_new: '%s' %d %d", path_utf8, check_sidecars, stat_sidecars);
+       DEBUG_2("file_data_new: '%s' %d", path_utf8, disable_sidecars);
+
+       if (S_ISDIR(st->st_mode)) disable_sidecars = TRUE;
 
        if (!file_data_pool)
                file_data_pool = g_hash_table_new(g_str_hash, g_str_equal);
@@ -369,7 +360,7 @@ static FileData *file_data_new(const gchar *path_utf8, struct stat *st, gboolean
                {
                file_data_ref(fd);
                }
-               
+
        if (!fd && file_data_planned_change_hash)
                {
                fd = g_hash_table_lookup(file_data_planned_change_hash, path_utf8);
@@ -380,133 +371,113 @@ static FileData *file_data_new(const gchar *path_utf8, struct stat *st, gboolean
                        file_data_apply_ci(fd);
                        }
                }
-               
+
        if (fd)
                {
                gboolean changed;
-               
-               if (fd->parent)
-                       changed = file_data_check_changed_files(fd);
-               else
-                       changed = file_data_check_changed_files_recursive(fd, st);
-               if (changed && check_sidecars && sidecar_file_priority(fd->extension))
-                       file_data_check_sidecars(fd, stat_sidecars);
+
+               if (disable_sidecars) file_data_disable_grouping(fd, TRUE);
+
+
+               changed = file_data_check_changed_single_file(fd, st);
+
                DEBUG_2("file_data_pool hit: '%s' %s", fd->path, changed ? "(changed)" : "");
-               
+
                return fd;
                }
 
        fd = g_new0(FileData, 1);
-       
+#ifdef DEBUG_FILEDATA
+       global_file_data_count++;
+       DEBUG_2("file data count++: %d", global_file_data_count);
+#endif
+
        fd->size = st->st_size;
        fd->date = st->st_mtime;
        fd->mode = st->st_mode;
        fd->ref = 1;
-       fd->magick = 0x12345678;
+       fd->magick = FD_MAGICK;
 
-       file_data_set_path(fd, path_utf8); /* set path, name, collate_key_*, original_path */
+       if (disable_sidecars) fd->disable_grouping = TRUE;
 
-       if (check_sidecars)
-               file_data_check_sidecars(fd, stat_sidecars);
+       file_data_set_path(fd, path_utf8); /* set path, name, collate_key_*, original_path */
 
        return fd;
 }
 
-static void file_data_check_sidecars(FileData *fd, gboolean stat_sidecars)
+static FileData *file_data_new_local(const gchar *path, struct stat *st, gboolean disable_sidecars)
 {
-       gint base_len;
-       GString *fname;
-       FileData *parent_fd = NULL;
-       GList *work;
-       GList *basename_list = NULL;
-
-       if (fd->disable_grouping || !sidecar_file_priority(fd->extension))
-               return;
+       gchar *path_utf8 = path_to_utf8(path);
+       FileData *ret = file_data_new(path_utf8, st, disable_sidecars);
 
-       base_len = fd->extension - fd->path;
-       fname = g_string_new_len(fd->path, base_len);
+       g_free(path_utf8);
+       return ret;
+}
 
-       if (!stat_sidecars)
+void init_exif_time_data(GList *files)
+{
+       FileData *file;
+       DEBUG_1("%s init_exif_time_data: ...", get_exec_time());
+       while (files)
                {
-               basename_list = g_hash_table_lookup(file_data_basename_hash, fname->str);
-               }
+               file = files->data;
 
-       work = sidecar_ext_get_list();
+               if (file)
+                       file->exifdate = 0;
 
-       while (work)
+               files = files->next;
+               }
+}
+
+void read_exif_time_data(FileData *file)
+{
+       if (file->exifdate > 0)
                {
-               /* check for possible sidecar files;
-                  the sidecar files created here are referenced only via fd->sidecar_files or fd->parent,
-                  they have fd->ref set to 0 and file_data unref must chack and free them all together
-                  (using fd->ref would cause loops and leaks)
-               */
+               DEBUG_1("%s set_exif_time_data: Already exists for %s", get_exec_time(), file->path);
+               return;
+               }
 
-               FileData *new_fd;
-               gchar *ext = work->data;
+       file->exif = exif_read_fd(file);
 
-               work = work->next;
+       if (file->exif)
+               {
+               gchar *tmp = exif_get_data_as_text(file->exif, "Exif.Photo.DateTimeOriginal");
+               DEBUG_2("%s set_exif_time_data: reading %p %s", get_exec_time(), file, file->path);
 
-               if (g_ascii_strcasecmp(ext, fd->extension) == 0)
+               if (tmp)
                        {
-                       new_fd = fd; /* processing the original file */
-                       }
-               else
-                       {
-                       if (stat_sidecars)
-                               {
-                               struct stat nst;
-                               g_string_truncate(fname, base_len);
-                               if (!stat_utf8_case_insensitive_ext(fname, ext, &nst))
-                                       continue;
-                               new_fd = file_data_new(fname->str, &nst, FALSE, FALSE);
-                               }
-                       else
-                               {
-                               GList *work2 = basename_list;
-                               new_fd = NULL;
-                               
-                               while (work2)
-                                       {
-                                       FileData *sfd = work2->data;
-                                       if (g_ascii_strcasecmp(ext, sfd->extension) == 0) 
-                                               {
-                                               new_fd = file_data_ref(sfd);
-                                               break;
-                                               }
-                                       work2 = work2->next;
-                                       }
-                                       
-                               if (!new_fd) continue;
-                               }
-                       
-                       if (new_fd->disable_grouping)
-                               {
-                               file_data_unref(new_fd);
-                               continue;
-                               }
-                       
-                       new_fd->ref--; /* do not use ref here */
+                       struct tm time_str;
+                       uint year, month, day, hour, min, sec;
+
+                       sscanf(tmp, "%4d:%2d:%2d %2d:%2d:%2d", &year, &month, &day, &hour, &min, &sec);
+                       time_str.tm_year  = year - 1900;
+                       time_str.tm_mon   = month - 1;
+                       time_str.tm_mday  = day;
+                       time_str.tm_hour  = hour;
+                       time_str.tm_min   = min;
+                       time_str.tm_sec   = sec;
+                       time_str.tm_isdst = 0;
+
+                       file->exifdate = mktime(&time_str);
+                       g_free(tmp);
                        }
-
-               if (!parent_fd)
-                       parent_fd = new_fd; /* parent is the one with the highest prio, found first */
-               else
-                       file_data_merge_sidecar_files(parent_fd, new_fd);
                }
-       g_string_free(fname, TRUE);
 }
 
-
-static FileData *file_data_new_local(const gchar *path, struct stat *st, gboolean check_sidecars, gboolean stat_sidecars)
+void set_exif_time_data(GList *files)
 {
-       gchar *path_utf8 = path_to_utf8(path);
-       FileData *ret = file_data_new(path_utf8, st, check_sidecars, stat_sidecars);
+       DEBUG_1("%s set_exif_time_data: ...", get_exec_time());
 
-       g_free(path_utf8);
-       return ret;
+       while (files)
+               {
+               FileData *file = files->data;
+
+               read_exif_time_data(file);
+               files = files->next;
+               }
 }
 
-FileData *file_data_new_simple(const gchar *path_utf8)
+FileData *file_data_new_no_grouping(const gchar *path_utf8)
 {
        struct stat st;
 
@@ -516,41 +487,31 @@ FileData *file_data_new_simple(const gchar *path_utf8)
                st.st_mtime = 0;
                }
 
-       return file_data_new(path_utf8, &st, TRUE, TRUE);
-}
-
-FileData *file_data_add_sidecar_file(FileData *target, FileData *sfd)
-{
-       sfd->parent = target;
-       if (!g_list_find(target->sidecar_files, sfd))
-               target->sidecar_files = g_list_prepend(target->sidecar_files, sfd);
-       file_data_increment_version(sfd); /* increments both sfd and target */
-       return target;
+       return file_data_new(path_utf8, &st, TRUE);
 }
 
-
-FileData *file_data_merge_sidecar_files(FileData *target, FileData *source)
+FileData *file_data_new_dir(const gchar *path_utf8)
 {
-       GList *work;
-       
-       file_data_add_sidecar_file(target, source);
+       struct stat st;
 
-       work = source->sidecar_files;
-       while (work)
+       if (!stat_utf8(path_utf8, &st))
                {
-               FileData *sfd = work->data;
-               file_data_add_sidecar_file(target, sfd);
-               work = work->next;
+               st.st_size = 0;
+               st.st_mtime = 0;
                }
+       else
+               /* dir or non-existing yet */
+               g_assert(S_ISDIR(st.st_mode));
 
-       g_list_free(source->sidecar_files);
-       source->sidecar_files = NULL;
-
-       target->sidecar_files = filelist_sort(target->sidecar_files, SORT_NAME, TRUE);
-       
-       return target;
+       return file_data_new(path_utf8, &st, TRUE);
 }
 
+/*
+ *-----------------------------------------------------------------------------
+ * reference counting
+ *-----------------------------------------------------------------------------
+ */
+
 #ifdef DEBUG_FILEDATA
 FileData *file_data_ref_debug(const gchar *file, gint line, FileData *fd)
 #else
@@ -558,27 +519,35 @@ FileData *file_data_ref(FileData *fd)
 #endif
 {
        if (fd == NULL) return NULL;
+       if (fd->magick != FD_MAGICK)
 #ifdef DEBUG_FILEDATA
-       if (fd->magick != 0x12345678)
-               DEBUG_0("fd magick mismatch at %s:%d", file, line);
+               DEBUG_0("fd magick mismatch @ %s:%d  fd=%p", file, line, fd);
+#else
+               DEBUG_0("fd magick mismatch fd=%p", fd);
 #endif
-       g_assert(fd->magick == 0x12345678);
+       g_assert(fd->magick == FD_MAGICK);
        fd->ref++;
 
 #ifdef DEBUG_FILEDATA
-       DEBUG_2("file_data_ref (%d): '%s' @ %s:%d", fd->ref, fd->path, file, line);
+       DEBUG_2("file_data_ref fd=%p (%d): '%s' @ %s:%d", fd, fd->ref, fd->path, file, line);
 #else
-       DEBUG_2("file_data_ref (%d): '%s'", fd->ref, fd->path);
+       DEBUG_2("file_data_ref fd=%p (%d): '%s'", fd, fd->ref, fd->path);
 #endif
        return fd;
 }
 
 static void file_data_free(FileData *fd)
 {
-       g_assert(fd->magick == 0x12345678);
+       g_assert(fd->magick == FD_MAGICK);
        g_assert(fd->ref == 0);
+       g_assert(!fd->locked);
 
-       if (fd->path) file_data_basename_hash_remove(fd);
+#ifdef DEBUG_FILEDATA
+       global_file_data_count--;
+       DEBUG_2("file data count--: %d", global_file_data_count);
+#endif
+
+       metadata_cache_free(fd);
        g_hash_table_remove(file_data_pool, fd->original_path);
 
        g_free(fd->path);
@@ -587,92 +556,343 @@ static void file_data_free(FileData *fd)
        g_free(fd->collate_key_name_nocase);
        if (fd->thumb_pixbuf) g_object_unref(fd->thumb_pixbuf);
        histmap_free(fd->histmap);
-       
+
        g_assert(fd->sidecar_files == NULL); /* sidecar files must be freed before calling this */
 
        file_data_change_info_free(NULL, fd);
        g_free(fd);
 }
 
-#ifdef DEBUG_FILEDATA
-void file_data_unref_debug(const gchar *file, gint line, FileData *fd)
-#else
-void file_data_unref(FileData *fd)
-#endif
+/**
+ * \brief Checks if the FileData is referenced
+ *
+ * Checks the refcount and whether the FileData is locked.
+ */
+static gboolean file_data_check_has_ref(FileData *fd)
 {
-       if (fd == NULL) return;
+       return fd->ref > 0 || fd->locked;
+}
+
+/**
+ * \brief Consider freeing a FileData.
+ *
+ * This function will free a FileData and its children provided that neither its parent nor it has
+ * a positive refcount, and provided that neither is locked.
+ */
+static void file_data_consider_free(FileData *fd)
+{
+       GList *work;
+       FileData *parent = fd->parent ? fd->parent : fd;
+
+       g_assert(fd->magick == FD_MAGICK);
+       if (file_data_check_has_ref(fd)) return;
+       if (file_data_check_has_ref(parent)) return;
+
+       work = parent->sidecar_files;
+       while (work)
+               {
+               FileData *sfd = work->data;
+               if (file_data_check_has_ref(sfd)) return;
+               work = work->next;
+               }
+
+       /* Neither the parent nor the siblings are referenced, so we can free everything */
+       DEBUG_2("file_data_consider_free: deleting '%s', parent '%s'",
+               fd->path, fd->parent ? parent->path : "-");
+
+       work = parent->sidecar_files;
+       while (work)
+               {
+               FileData *sfd = work->data;
+               file_data_free(sfd);
+               work = work->next;
+               }
+
+       g_list_free(parent->sidecar_files);
+       parent->sidecar_files = NULL;
+
+       file_data_free(parent);
+}
+
 #ifdef DEBUG_FILEDATA
-       if (fd->magick != 0x12345678)
-               DEBUG_0("fd magick mismatch @ %s:%d", file, line);
+void file_data_unref_debug(const gchar *file, gint line, FileData *fd)
+#else
+void file_data_unref(FileData *fd)
+#endif
+{
+       if (fd == NULL) return;
+       if (fd->magick != FD_MAGICK)
+#ifdef DEBUG_FILEDATA
+               DEBUG_0("fd magick mismatch @ %s:%d  fd=%p", file, line, fd);
+#else
+               DEBUG_0("fd magick mismatch fd=%p", fd);
 #endif
-       g_assert(fd->magick == 0x12345678);
-       
+       g_assert(fd->magick == FD_MAGICK);
+
        fd->ref--;
 #ifdef DEBUG_FILEDATA
-       DEBUG_2("file_data_unref (%d): '%s' @ %s:%d", fd->ref, fd->path, file, line);
+       DEBUG_2("file_data_unref fd=%p (%d:%d): '%s' @ %s:%d", fd, fd->ref, fd->locked, fd->path,
+               file, line);
 #else
-       DEBUG_2("file_data_unref (%d): '%s'", fd->ref, fd->path);
+       DEBUG_2("file_data_unref fd=%p (%d:%d): '%s'", fd, fd->ref, fd->locked, fd->path);
 #endif
-       if (fd->ref == 0)
+
+       // Free FileData if it's no longer ref'd
+       file_data_consider_free(fd);
+}
+
+/**
+ * \brief Lock the FileData in memory.
+ *
+ * This allows the caller to prevent a FileData from being freed, even after its refcount is zero.
+ * This is intended to be used in cases where a FileData _should_ stay in memory as an optimization,
+ * even if the code would continue to function properly even if the FileData were freed.  Code that
+ * _requires_ the FileData to remain in memory should continue to use file_data_(un)ref.
+ * <p />
+ * Note: This differs from file_data_ref in that the behavior is reentrant -- after N calls to
+ * file_data_lock, a single call to file_data_unlock will unlock the FileData.
+ */
+void file_data_lock(FileData *fd)
+{
+       if (fd == NULL) return;
+       if (fd->magick != FD_MAGICK) DEBUG_0("fd magick mismatch fd=%p", fd);
+
+       g_assert(fd->magick == FD_MAGICK);
+       fd->locked = TRUE;
+
+       DEBUG_2("file_data_ref fd=%p (%d): '%s'", fd, fd->ref, fd->path);
+}
+
+/**
+ * \brief Reset the maintain-FileData-in-memory lock
+ *
+ * This again allows the FileData to be freed when its refcount drops to zero.  Automatically frees
+ * the FileData if its refcount is already zero (which will happen if the lock is the only thing
+ * keeping it from being freed.
+ */
+void file_data_unlock(FileData *fd)
+{
+       if (fd == NULL) return;
+       if (fd->magick != FD_MAGICK) DEBUG_0("fd magick mismatch fd=%p", fd);
+
+       g_assert(fd->magick == FD_MAGICK);
+       fd->locked = FALSE;
+
+       // Free FileData if it's no longer ref'd
+       file_data_consider_free(fd);
+}
+
+/**
+ * \brief Lock all of the FileDatas in the provided list
+ *
+ * \see file_data_lock(FileData)
+ */
+void file_data_lock_list(GList *list)
+{
+       GList *work;
+
+       work = list;
+       while (work)
                {
-               GList *work;
-               FileData *parent = fd->parent ? fd->parent : fd;
-               
-               if (parent->ref > 0) return;
+               FileData *fd = work->data;
+               work = work->next;
+               file_data_lock(fd);
+               }
+}
 
-               work = parent->sidecar_files;
-               while (work)
+/**
+ * \brief Unlock all of the FileDatas in the provided list
+ *
+ * \see file_data_unlock(FileData)
+ */
+void file_data_unlock_list(GList *list)
+{
+       GList *work;
+
+       work = list;
+       while (work)
+               {
+               FileData *fd = work->data;
+               work = work->next;
+               file_data_unlock(fd);
+               }
+}
+
+/*
+ *-----------------------------------------------------------------------------
+ * sidecar file info struct
+ *-----------------------------------------------------------------------------
+ */
+
+static gint file_data_sort_by_ext(gconstpointer a, gconstpointer b)
+{
+       const FileData *fda = a;
+       const FileData *fdb = b;
+
+       if (fda->sidecar_priority < fdb->sidecar_priority) return -1;
+       if (fda->sidecar_priority > fdb->sidecar_priority) return 1;
+
+       return strcmp(fdb->extension, fda->extension);
+}
+
+
+static gint sidecar_file_priority(const gchar *extension)
+{
+       gint i = 1;
+       GList *work;
+
+       if (extension == NULL)
+               return 0;
+
+       work = sidecar_ext_get_list();
+
+       while (work) {
+               gchar *ext = work->data;
+
+               work = work->next;
+               if (g_ascii_strcasecmp(extension, ext) == 0) return i;
+               i++;
+       }
+       return 0;
+}
+
+static void file_data_check_sidecars(const GList *basename_list)
+{
+       /* basename_list contains the new group - first is the parent, then sorted sidecars */
+       /* all files in the list have ref count > 0 */
+
+       const GList *work;
+       GList *s_work, *new_sidecars;
+       FileData *parent_fd;
+
+       if (!basename_list) return;
+
+
+       DEBUG_2("basename start");
+       work = basename_list;
+       while (work)
+               {
+               FileData *fd = work->data;
+               work = work->next;
+               g_assert(fd->magick == FD_MAGICK);
+               DEBUG_2("basename: %p %s", fd, fd->name);
+               if (fd->parent)
                        {
-                       FileData *sfd = work->data;
-                       if (sfd->ref > 0) return;
-                       work = work->next;
+                       g_assert(fd->parent->magick == FD_MAGICK);
+                       DEBUG_2("                  parent: %p", fd->parent);
+                       }
+               s_work = fd->sidecar_files;
+               while (s_work)
+                       {
+                       FileData *sfd = s_work->data;
+                       s_work = s_work->next;
+                       g_assert(sfd->magick == FD_MAGICK);
+                       DEBUG_2("                  sidecar: %p %s", sfd, sfd->name);
                        }
 
-               /* none of parent/children is referenced, we can free everything */
+               g_assert(fd->parent == NULL || fd->sidecar_files == NULL);
+               }
 
-               DEBUG_2("file_data_unref: deleting '%s', parent '%s'", fd->path, fd->parent ? parent->path : "-");
+       parent_fd = basename_list->data;
 
-               work = parent->sidecar_files;
-               while (work)
+       /* check if the second and next entries of basename_list are already connected
+          as sidecars of the first entry (parent_fd) */
+       work = basename_list->next;
+       s_work = parent_fd->sidecar_files;
+
+       while (work && s_work)
+               {
+               if (work->data != s_work->data) break;
+               work = work->next;
+               s_work = s_work->next;
+               }
+
+       if (!work && !s_work)
+               {
+               DEBUG_2("basename no change");
+               return; /* no change in grouping */
+               }
+
+       /* we have to regroup it */
+
+       /* first, disconnect everything and send notification*/
+
+       work = basename_list;
+       while (work)
+               {
+               FileData *fd = work->data;
+               work = work->next;
+               g_assert(fd->parent == NULL || fd->sidecar_files == NULL);
+
+               if (fd->parent)
                        {
-                       FileData *sfd = work->data;
-                       file_data_free(sfd);
-                       work = work->next;
+                       FileData *old_parent = fd->parent;
+                       g_assert(old_parent->parent == NULL || old_parent->sidecar_files == NULL);
+                       file_data_ref(old_parent);
+                       file_data_disconnect_sidecar_file(old_parent, fd);
+                       file_data_send_notification(old_parent, NOTIFY_REREAD);
+                       file_data_unref(old_parent);
+                       }
+
+               while (fd->sidecar_files)
+                       {
+                       FileData *sfd = fd->sidecar_files->data;
+                       g_assert(sfd->parent == NULL || sfd->sidecar_files == NULL);
+                       file_data_ref(sfd);
+                       file_data_disconnect_sidecar_file(fd, sfd);
+                       file_data_send_notification(sfd, NOTIFY_REREAD);
+                       file_data_unref(sfd);
                        }
+               file_data_send_notification(fd, NOTIFY_GROUPING);
 
-               g_list_free(parent->sidecar_files);
-               parent->sidecar_files = NULL;
+               g_assert(fd->parent == NULL && fd->sidecar_files == NULL);
+               }
 
-               file_data_free(parent);
+       /* now we can form the new group */
+       work = basename_list->next;
+       new_sidecars = NULL;
+       while (work)
+               {
+               FileData *sfd = work->data;
+               g_assert(sfd->magick == FD_MAGICK);
+               g_assert(sfd->parent == NULL && sfd->sidecar_files == NULL);
+               sfd->parent = parent_fd;
+               new_sidecars = g_list_prepend(new_sidecars, sfd);
+               work = work->next;
                }
+       g_assert(parent_fd->sidecar_files == NULL);
+       parent_fd->sidecar_files = g_list_reverse(new_sidecars);
+       DEBUG_1("basename group changed for %s", parent_fd->path);
 }
 
-FileData *file_data_disconnect_sidecar_file(FileData *target, FileData *sfd)
+
+static void file_data_disconnect_sidecar_file(FileData *target, FileData *sfd)
 {
-       sfd->parent = target;
+       g_assert(target->magick == FD_MAGICK);
+       g_assert(sfd->magick == FD_MAGICK);
        g_assert(g_list_find(target->sidecar_files, sfd));
-       
+
+       file_data_ref(target);
+       file_data_ref(sfd);
+
+       g_assert(sfd->parent == target);
+
        file_data_increment_version(sfd); /* increments both sfd and target */
 
        target->sidecar_files = g_list_remove(target->sidecar_files, sfd);
        sfd->parent = NULL;
 
-       if (sfd->ref == 0)
-               {
-               file_data_free(sfd);
-               return NULL;
-               }
-
-       return sfd;
+       file_data_unref(target);
+       file_data_unref(sfd);
 }
 
 /* disables / enables grouping for particular file, sends UPDATE notification */
 void file_data_disable_grouping(FileData *fd, gboolean disable)
 {
        if (!fd->disable_grouping == !disable) return;
-       
+
        fd->disable_grouping = !!disable;
-       
+
        if (disable)
                {
                if (fd->parent)
@@ -693,7 +913,7 @@ void file_data_disable_grouping(FileData *fd, gboolean disable)
                                file_data_disconnect_sidecar_file(fd, sfd);
                                file_data_send_notification(sfd, NOTIFY_GROUPING);
                                }
-                       file_data_check_sidecars((FileData *)sidecar_files->data, FALSE); /* this will group the sidecars back together */
+                       file_data_check_sidecars(sidecar_files); /* this will group the sidecars back together */
                        filelist_free(sidecar_files);
                        }
                else
@@ -704,7 +924,7 @@ void file_data_disable_grouping(FileData *fd, gboolean disable)
        else
                {
                file_data_increment_version(fd);
-               file_data_check_sidecars(fd, FALSE);
+               /* file_data_check_sidecars call is not necessary - the file will be re-grouped on next dir read */
                }
        file_data_send_notification(fd, NOTIFY_GROUPING);
 }
@@ -712,126 +932,29 @@ void file_data_disable_grouping(FileData *fd, gboolean disable)
 void file_data_disable_grouping_list(GList *fd_list, gboolean disable)
 {
        GList *work;
-       
+
        work = fd_list;
        while (work)
                {
                FileData *fd = work->data;
-               
-               file_data_disable_grouping(fd, disable);
-               work = work->next;
-               }
-}
-
-
-/* compare name without extension */
-gint file_data_compare_name_without_ext(FileData *fd1, FileData *fd2)
-{
-       size_t len1 = fd1->extension - fd1->name;
-       size_t len2 = fd2->extension - fd2->name;
-
-       if (len1 < len2) return -1;
-       if (len1 > len2) return 1;
 
-       return strncmp(fd1->name, fd2->name, len1); /* FIXME: utf8 */
-}
-
-void file_data_change_info_free(FileDataChangeInfo *fdci, FileData *fd)
-{
-       if (!fdci && fd) fdci = fd->change;
-
-       if (!fdci) return;
-
-       g_free(fdci->source);
-       g_free(fdci->dest);
-
-       g_free(fdci);
-
-       if (fd) fd->change = NULL;
-}
-
-static gboolean file_data_can_write_directly(FileData *fd)
-{
-       return filter_name_is_writable(fd->extension);
-}
-
-static gboolean file_data_can_write_sidecar(FileData *fd)
-{
-       return filter_name_allow_sidecar(fd->extension) && !filter_name_is_writable(fd->extension);
-}
-
-gchar *file_data_get_sidecar_path(FileData *fd, gboolean existing_only)
-{
-       gchar *sidecar_path = NULL;
-       GList *work;
-       
-       if (!file_data_can_write_sidecar(fd)) return NULL;
-       
-       work = fd->parent ? fd->parent->sidecar_files : fd->sidecar_files;
-       while (work)
-               {
-               FileData *sfd = work->data;
+               file_data_disable_grouping(fd, disable);
                work = work->next;
-               if (g_ascii_strcasecmp(sfd->extension, ".xmp") == 0)
-                       {
-                       sidecar_path = g_strdup(sfd->path);
-                       break;
-                       }
                }
-       
-       if (!existing_only && !sidecar_path)
-               {
-               gchar *base = remove_extension_from_path(fd->path);
-               sidecar_path = g_strconcat(base, ".xmp", NULL);
-               g_free(base);
-               }
-
-       return sidecar_path;
 }
 
 
-/*
- *-----------------------------------------------------------------------------
- * sidecar file info struct
- *-----------------------------------------------------------------------------
- */
-
-
-
-static gint sidecar_file_priority(const gchar *path)
-{
-       const gchar *extension = extension_from_path(path);
-       gint i = 1;
-       GList *work;
-
-       if (extension == NULL)
-               return 0;
-
-       work = sidecar_ext_get_list();
-
-       while (work) {
-               gchar *ext = work->data;
-               
-               work = work->next;
-               if (g_ascii_strcasecmp(extension, ext) == 0) return i;
-               i++;
-       }
-       return 0;
-}
-
 
 /*
  *-----------------------------------------------------------------------------
- * load file list
+ * filelist sorting
  *-----------------------------------------------------------------------------
  */
 
-static SortType filelist_sort_method = SORT_NONE;
-static gboolean filelist_sort_ascend = TRUE;
-
 
 gint filelist_sort_compare_filedata(FileData *fa, FileData *fb)
 {
+       gint ret;
        if (!filelist_sort_ascend)
                {
                FileData *tmp = fa;
@@ -853,9 +976,15 @@ gint filelist_sort_compare_filedata(FileData *fa, FileData *fb)
                        if (fa->date > fb->date) return 1;
                        /* fall back to name */
                        break;
+               case SORT_EXIFTIME:
+                       if (fa->exifdate < fb->exifdate) return -1;
+                       if (fa->exifdate > fb->exifdate) return 1;
+                       /* fall back to name */
+                       break;
 #ifdef HAVE_STRVERSCMP
                case SORT_NUMBER:
-                       return strverscmp(fa->name, fb->name);
+                       ret = strverscmp(fa->name, fb->name);
+                       if (ret != 0) return ret;
                        break;
 #endif
                default:
@@ -863,9 +992,16 @@ gint filelist_sort_compare_filedata(FileData *fa, FileData *fb)
                }
 
        if (options->file_sort.case_sensitive)
-               return strcmp(fa->collate_key_name, fb->collate_key_name);
+               ret = strcmp(fa->collate_key_name, fb->collate_key_name);
        else
-               return strcmp(fa->collate_key_name_nocase, fb->collate_key_name_nocase);
+               ret = strcmp(fa->collate_key_name_nocase, fb->collate_key_name_nocase);
+
+       if (ret != 0) return ret;
+
+       /* do not return 0 unless the files are really the same
+          file_data_pool ensures that original_path is unique
+       */
+       return strcmp(fa->original_path, fb->original_path);
 }
 
 gint filelist_sort_compare_filedata_full(FileData *fa, FileData *fb, SortType method, gboolean ascend)
@@ -896,6 +1032,10 @@ GList *filelist_insert_sort_full(GList *list, gpointer data, SortType method, gb
 
 GList *filelist_sort(GList *list, SortType method, gboolean ascend)
 {
+       if (method == SORT_EXIFTIME)
+               {
+               set_exif_time_data(list);
+               }
        return filelist_sort_full(list, method, ascend, (GCompareFunc) filelist_sort_file_cb);
 }
 
@@ -904,6 +1044,53 @@ GList *filelist_insert_sort(GList *list, FileData *fd, SortType method, gboolean
        return filelist_insert_sort_full(list, fd, method, ascend, (GCompareFunc) filelist_sort_file_cb);
 }
 
+/*
+ *-----------------------------------------------------------------------------
+ * basename hash - grouping of sidecars in filelist
+ *-----------------------------------------------------------------------------
+ */
+
+
+static GHashTable *file_data_basename_hash_new(void)
+{
+       return g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+}
+
+static GList * file_data_basename_hash_insert(GHashTable *basename_hash, FileData *fd)
+{
+       GList *list;
+       gchar *basename = g_strndup(fd->path, fd->extension - fd->path);
+
+       list = g_hash_table_lookup(basename_hash, basename);
+
+       if (!g_list_find(list, fd))
+               {
+               list = g_list_insert_sorted(list, file_data_ref(fd), file_data_sort_by_ext);
+               g_hash_table_insert(basename_hash, basename, list);
+               }
+       else
+               {
+               g_free(basename);
+               }
+       return list;
+}
+
+static void file_data_basename_hash_remove_list(gpointer key, gpointer value, gpointer data)
+{
+       filelist_free((GList *)value);
+}
+
+static void file_data_basename_hash_free(GHashTable *basename_hash)
+{
+       g_hash_table_foreach(basename_hash, file_data_basename_hash_remove_list, NULL);
+       g_hash_table_destroy(basename_hash);
+}
+
+/*
+ *-----------------------------------------------------------------------------
+ * handling sidecars in filelist
+ *-----------------------------------------------------------------------------
+ */
 
 static GList *filelist_filter_out_sidecars(GList *flist)
 {
@@ -913,7 +1100,7 @@ static GList *filelist_filter_out_sidecars(GList *flist)
        while (work)
                {
                FileData *fd = work->data;
-       
+
                work = work->next;
                if (fd->parent) /* remove fd's that are children */
                        file_data_unref(fd);
@@ -925,6 +1112,13 @@ static GList *filelist_filter_out_sidecars(GList *flist)
        return flist_filtered;
 }
 
+static void file_data_basename_hash_to_sidecars(gpointer key, gpointer value, gpointer data)
+{
+       GList *basename_list = (GList *)value;
+       file_data_check_sidecars(basename_list);
+}
+
+
 static gboolean is_hidden_file(const gchar *name)
 {
        if (name[0] != '.') return FALSE;
@@ -932,7 +1126,13 @@ static gboolean is_hidden_file(const gchar *name)
        return TRUE;
 }
 
-static gboolean filelist_read_real(FileData *dir_fd, GList **files, GList **dirs, gboolean follow_symlinks)
+/*
+ *-----------------------------------------------------------------------------
+ * the main filelist function
+ *-----------------------------------------------------------------------------
+ */
+
+static gboolean filelist_read_real(const gchar *dir_path, GList **files, GList **dirs, gboolean follow_symlinks)
 {
        DIR *dp;
        struct dirent *dir;
@@ -940,13 +1140,14 @@ static gboolean filelist_read_real(FileData *dir_fd, GList **files, GList **dirs
        GList *dlist = NULL;
        GList *flist = NULL;
        gint (*stat_func)(const gchar *path, struct stat *buf);
+       GHashTable *basename_hash = NULL;
 
        g_assert(files || dirs);
 
        if (files) *files = NULL;
        if (dirs) *dirs = NULL;
 
-       pathl = path_from_utf8(dir_fd->path);
+       pathl = path_from_utf8(dir_path);
        if (!pathl) return FALSE;
 
        dp = opendir(pathl);
@@ -956,6 +1157,8 @@ static gboolean filelist_read_real(FileData *dir_fd, GList **files, GList **dirs
                return FALSE;
                }
 
+       if (files) basename_hash = file_data_basename_hash_new();
+
        if (follow_symlinks)
                stat_func = stat;
        else
@@ -982,40 +1185,95 @@ static gboolean filelist_read_real(FileData *dir_fd, GList **files, GList **dirs
                                    strcmp(name, GQ_CACHE_LOCAL_METADATA) != 0 &&
                                    strcmp(name, THUMB_FOLDER_LOCAL) != 0)
                                        {
-                                       dlist = g_list_prepend(dlist, file_data_new_local(filepath, &ent_sbuf, FALSE, FALSE));
+                                       dlist = g_list_prepend(dlist, file_data_new_local(filepath, &ent_sbuf, TRUE));
                                        }
                                }
                        else
                                {
                                if (files && filter_name_exists(name))
                                        {
-                                       flist = g_list_prepend(flist, file_data_new_local(filepath, &ent_sbuf, TRUE, FALSE));
+                                       FileData *fd = file_data_new_local(filepath, &ent_sbuf, FALSE);
+                                       flist = g_list_prepend(flist, fd);
+                                       if (fd->sidecar_priority && !fd->disable_grouping)
+                                               {
+                                               file_data_basename_hash_insert(basename_hash, fd);
+                                               }
                                        }
                                }
                        }
+               else
+                       {
+                       if (errno == EOVERFLOW)
+                               {
+                               log_printf("stat(): EOVERFLOW, skip '%s'", filepath);
+                               }
+                       }
                g_free(filepath);
                }
 
-       closedir(dp);
-       
-       g_free(pathl);
+       closedir(dp);
+
+       g_free(pathl);
+
+       if (dirs) *dirs = dlist;
+
+       if (files)
+               {
+               g_hash_table_foreach(basename_hash, file_data_basename_hash_to_sidecars, NULL);
+
+               *files = filelist_filter_out_sidecars(flist);
+               }
+       if (basename_hash) file_data_basename_hash_free(basename_hash);
+
+       // Call a separate function to initialize the exif datestamps for the found files..
+       if (files) init_exif_time_data(*files);
+
+       return TRUE;
+}
+
+gboolean filelist_read(FileData *dir_fd, GList **files, GList **dirs)
+{
+       return filelist_read_real(dir_fd->path, files, dirs, TRUE);
+}
+
+gboolean filelist_read_lstat(FileData *dir_fd, GList **files, GList **dirs)
+{
+       return filelist_read_real(dir_fd->path, files, dirs, FALSE);
+}
+
+FileData *file_data_new_group(const gchar *path_utf8)
+{
+       gchar *dir;
+       struct stat st;
+       FileData *fd;
+       GList *files;
+
+       if (!stat_utf8(path_utf8, &st))
+               {
+               st.st_size = 0;
+               st.st_mtime = 0;
+               }
+
+       if (S_ISDIR(st.st_mode))
+               return file_data_new(path_utf8, &st, TRUE);
 
-       if (dirs) *dirs = dlist;
-       if (files) *files = filelist_filter_out_sidecars(flist);
+       dir = remove_level_from_path(path_utf8);
 
-       return TRUE;
-}
+       filelist_read_real(dir, &files, NULL, TRUE);
 
-gboolean filelist_read(FileData *dir_fd, GList **files, GList **dirs)
-{
-       return filelist_read_real(dir_fd, files, dirs, TRUE);
-}
+       fd = g_hash_table_lookup(file_data_pool, path_utf8);
+       if (!fd) fd = file_data_new(path_utf8, &st, TRUE);
+       if (fd)
+               {
+               file_data_ref(fd);
+               }
 
-gboolean filelist_read_lstat(FileData *dir_fd, GList **files, GList **dirs)
-{
-       return filelist_read_real(dir_fd, files, dirs, FALSE);
+       filelist_free(files);
+       g_free(dir);
+       return fd;
 }
 
+
 void filelist_free(GList *list)
 {
        GList *work;
@@ -1063,7 +1321,7 @@ GList *filelist_from_path_list(GList *list)
                path = work->data;
                work = work->next;
 
-               new_list = g_list_prepend(new_list, file_data_new_simple(path));
+               new_list = g_list_prepend(new_list, file_data_new_group(path));
                }
 
        return g_list_reverse(new_list);
@@ -1106,12 +1364,12 @@ GList *filelist_filter(GList *list, gboolean is_dir_list)
                                                       strcmp(name, GQ_CACHE_LOCAL_METADATA) == 0)) )
                        {
                        GList *link = work;
-                       
+
                        list = g_list_remove_link(list, link);
                        file_data_unref(fd);
                        g_list_free(link);
                        }
-       
+
                work = work->next;
                }
 
@@ -1178,6 +1436,65 @@ GList *filelist_recursive(FileData *dir_fd)
        return list;
 }
 
+/*
+ *-----------------------------------------------------------------------------
+ * file modification support
+ *-----------------------------------------------------------------------------
+ */
+
+
+void file_data_change_info_free(FileDataChangeInfo *fdci, FileData *fd)
+{
+       if (!fdci && fd) fdci = fd->change;
+
+       if (!fdci) return;
+
+       g_free(fdci->source);
+       g_free(fdci->dest);
+
+       g_free(fdci);
+
+       if (fd) fd->change = NULL;
+}
+
+static gboolean file_data_can_write_directly(FileData *fd)
+{
+       return filter_name_is_writable(fd->extension);
+}
+
+static gboolean file_data_can_write_sidecar(FileData *fd)
+{
+       return filter_name_allow_sidecar(fd->extension) && !filter_name_is_writable(fd->extension);
+}
+
+gchar *file_data_get_sidecar_path(FileData *fd, gboolean existing_only)
+{
+       gchar *sidecar_path = NULL;
+       GList *work;
+
+       if (!file_data_can_write_sidecar(fd)) return NULL;
+
+       work = fd->parent ? fd->parent->sidecar_files : fd->sidecar_files;
+       while (work)
+               {
+               FileData *sfd = work->data;
+               work = work->next;
+               if (g_ascii_strcasecmp(sfd->extension, ".xmp") == 0)
+                       {
+                       sidecar_path = g_strdup(sfd->path);
+                       break;
+                       }
+               }
+
+       if (!existing_only && !sidecar_path)
+               {
+               gchar *base = g_strndup(fd->path, fd->extension - fd->path);
+               sidecar_path = g_strconcat(base, ".xmp", NULL);
+               g_free(base);
+               }
+
+       return sidecar_path;
+}
 
 /*
  * marks and orientation
@@ -1191,17 +1508,17 @@ static GDestroyNotify file_data_destroy_mark_func[FILEDATA_MARKS_SIZE];
 gboolean file_data_get_mark(FileData *fd, gint n)
 {
        gboolean valid = (fd->valid_marks & (1 << n));
-       
-       if (file_data_get_mark_func[n] && !valid) 
+
+       if (file_data_get_mark_func[n] && !valid)
                {
                guint old = fd->marks;
                gboolean value = (file_data_get_mark_func[n])(fd, n, file_data_mark_func_data[n]);
-               
-               if (!value != !(fd->marks & (1 << n))) 
+
+               if (!value != !(fd->marks & (1 << n)))
                        {
                        fd->marks = fd->marks ^ (1 << n);
                        }
-               
+
                fd->valid_marks |= (1 << n);
                if (old && !fd->marks) /* keep files with non-zero marks in memory */
                        {
@@ -1227,16 +1544,16 @@ void file_data_set_mark(FileData *fd, gint n, gboolean value)
 {
        guint old;
        if (!value == !file_data_get_mark(fd, n)) return;
-       
-       if (file_data_set_mark_func[n]) 
+
+       if (file_data_set_mark_func[n])
                {
                (file_data_set_mark_func[n])(fd, n, value, file_data_mark_func_data[n]);
                }
-       
+
        old = fd->marks;
 
        fd->marks = fd->marks ^ (1 << n);
-       
+
        if (old && !fd->marks) /* keep files with non-zero marks in memory */
                {
                file_data_unref(fd);
@@ -1245,7 +1562,7 @@ void file_data_set_mark(FileData *fd, gint n, gboolean value)
                {
                file_data_ref(fd);
                }
-       
+
        file_data_increment_version(fd);
        file_data_send_notification(fd, NOTIFY_MARKS);
 }
@@ -1289,9 +1606,9 @@ static void file_data_notify_mark_func(gpointer key, gpointer value, gpointer us
 gboolean file_data_register_mark_func(gint n, FileDataGetMarkFunc get_mark_func, FileDataSetMarkFunc set_mark_func, gpointer data, GDestroyNotify notify)
 {
        if (n < 0 || n >= FILEDATA_MARKS_SIZE) return FALSE;
-       
+
        if (file_data_destroy_mark_func[n]) (file_data_destroy_mark_func[n])(file_data_mark_func_data[n]);
-               
+
        file_data_get_mark_func[n] = get_mark_func;
         file_data_set_mark_func[n] = set_mark_func;
         file_data_mark_func_data[n] = data;
@@ -1397,7 +1714,7 @@ gboolean file_data_add_ci(FileData *fd, FileDataChangeType type, const gchar *sr
                fdci->dest = g_strdup(dest);
 
        fd->change = fdci;
-       
+
        return TRUE;
 }
 
@@ -1430,6 +1747,8 @@ void file_data_free_ci(FileData *fd)
 
        file_data_planned_change_remove(fd);
 
+       if (fdci->regroup_when_finished) file_data_disable_grouping(fd, FALSE);
+
        g_free(fdci->source);
        g_free(fdci->dest);
 
@@ -1438,46 +1757,52 @@ void file_data_free_ci(FileData *fd)
        fd->change = NULL;
 }
 
+void file_data_set_regroup_when_finished(FileData *fd, gboolean enable)
+{
+       FileDataChangeInfo *fdci = fd->change;
+       if (!fdci) return;
+       fdci->regroup_when_finished = enable;
+}
 
 static gboolean file_data_sc_add_ci(FileData *fd, FileDataChangeType type)
 {
        GList *work;
 
        if (fd->parent) fd = fd->parent;
-       
+
        if (fd->change) return FALSE;
-       
+
        work = fd->sidecar_files;
        while (work)
                {
                FileData *sfd = work->data;
-               
+
                if (sfd->change) return FALSE;
                work = work->next;
                }
 
        file_data_add_ci(fd, type, NULL, NULL);
-       
+
        work = fd->sidecar_files;
        while (work)
                {
                FileData *sfd = work->data;
-               
+
                file_data_add_ci(sfd, type, NULL, NULL);
                work = work->next;
                }
-               
+
        return TRUE;
 }
 
 static gboolean file_data_sc_check_ci(FileData *fd, FileDataChangeType type)
 {
        GList *work;
-       
+
        if (fd->parent) fd = fd->parent;
-       
+
        if (!fd->change || fd->change->type != type) return FALSE;
-       
+
        work = fd->sidecar_files;
        while (work)
                {
@@ -1534,14 +1859,14 @@ void file_data_sc_free_ci(FileData *fd)
        GList *work;
 
        if (fd->parent) fd = fd->parent;
-       
+
        file_data_free_ci(fd);
-       
+
        work = fd->sidecar_files;
        while (work)
                {
                FileData *sfd = work->data;
-       
+
                file_data_free_ci(sfd);
                work = work->next;
                }
@@ -1556,7 +1881,7 @@ gboolean file_data_sc_add_ci_delete_list(GList *fd_list)
        while (work)
                {
                FileData *fd = work->data;
-       
+
                if (!file_data_sc_add_ci_delete(fd)) ret = FALSE;
                work = work->next;
                }
@@ -1567,12 +1892,12 @@ gboolean file_data_sc_add_ci_delete_list(GList *fd_list)
 static void file_data_sc_revert_ci_list(GList *fd_list)
 {
        GList *work;
-       
+
        work = fd_list;
        while (work)
                {
                FileData *fd = work->data;
-               
+
                file_data_sc_free_ci(fd);
                work = work->prev;
                }
@@ -1581,12 +1906,12 @@ static void file_data_sc_revert_ci_list(GList *fd_list)
 static gboolean file_data_sc_add_ci_list_call_func(GList *fd_list, const gchar *dest, gboolean (*func)(FileData *, const gchar *))
 {
        GList *work;
-       
+
        work = fd_list;
        while (work)
                {
                FileData *fd = work->data;
-               
+
                if (!func(fd, dest))
                        {
                        file_data_sc_revert_ci_list(work->prev);
@@ -1594,7 +1919,7 @@ static gboolean file_data_sc_add_ci_list_call_func(GList *fd_list, const gchar *
                        }
                work = work->next;
                }
-       
+
        return TRUE;
 }
 
@@ -1627,7 +1952,7 @@ gboolean file_data_add_ci_write_metadata_list(GList *fd_list)
        while (work)
                {
                FileData *fd = work->data;
-       
+
                if (!file_data_add_ci_write_metadata(fd)) ret = FALSE;
                work = work->next;
                }
@@ -1638,12 +1963,12 @@ gboolean file_data_add_ci_write_metadata_list(GList *fd_list)
 void file_data_free_ci_list(GList *fd_list)
 {
        GList *work;
-       
+
        work = fd_list;
        while (work)
                {
                FileData *fd = work->data;
-               
+
                file_data_free_ci(fd);
                work = work->next;
                }
@@ -1652,12 +1977,12 @@ void file_data_free_ci_list(GList *fd_list)
 void file_data_sc_free_ci_list(GList *fd_list)
 {
        GList *work;
-       
+
        work = fd_list;
        while (work)
                {
                FileData *fd = work->data;
-               
+
                file_data_sc_free_ci(fd);
                work = work->next;
                }
@@ -1671,14 +1996,14 @@ void file_data_sc_free_ci_list(GList *fd_list)
 static void file_data_update_planned_change_hash(FileData *fd, const gchar *old_path, gchar *new_path)
 {
        FileDataChangeType type = fd->change->type;
-       
+
        if (type == FILEDATA_CHANGE_MOVE || type == FILEDATA_CHANGE_RENAME)
                {
                FileData *ofd;
-               
+
                if (!file_data_planned_change_hash)
                        file_data_planned_change_hash = g_hash_table_new(g_str_hash, g_str_equal);
-               
+
                if (old_path && g_hash_table_lookup(file_data_planned_change_hash, old_path) == fd)
                        {
                        DEBUG_1("planned change: removing %s -> %s", old_path, fd->path);
@@ -1695,7 +2020,7 @@ static void file_data_update_planned_change_hash(FileData *fd, const gchar *old_
                                g_hash_table_remove(file_data_planned_change_hash, new_path);
                                file_data_unref(ofd);
                                }
-                       
+
                        DEBUG_1("planned change: inserting %s -> %s", new_path, fd->path);
                        file_data_ref(fd);
                        g_hash_table_insert(file_data_planned_change_hash, new_path, fd);
@@ -1717,10 +2042,10 @@ static void file_data_update_ci_dest_preserve_ext(FileData *fd, const gchar *des
        const gchar *extension = extension_from_path(fd->change->source);
        gchar *base = remove_extension_from_path(dest_path);
        gchar *old_path = fd->change->dest;
-       
+
        fd->change->dest = g_strconcat(base, extension, NULL);
        file_data_update_planned_change_hash(fd, old_path, fd->change->dest);
-       
+
        g_free(old_path);
        g_free(base);
 }
@@ -1729,9 +2054,9 @@ static void file_data_sc_update_ci(FileData *fd, const gchar *dest_path)
 {
        GList *work;
        gchar *dest_path_full = NULL;
-       
+
        if (fd->parent) fd = fd->parent;
-       
+
        if (!dest_path)
                {
                dest_path = fd->path;
@@ -1739,7 +2064,7 @@ static void file_data_sc_update_ci(FileData *fd, const gchar *dest_path)
        else if (!strchr(dest_path, G_DIR_SEPARATOR)) /* we got only filename, not a full path */
                {
                gchar *dir = remove_level_from_path(fd->path);
-               
+
                dest_path_full = g_build_filename(dir, dest_path, NULL);
                g_free(dir);
                dest_path = dest_path_full;
@@ -1749,18 +2074,18 @@ static void file_data_sc_update_ci(FileData *fd, const gchar *dest_path)
                dest_path_full = g_build_filename(dest_path, fd->name, NULL);
                dest_path = dest_path_full;
                }
-               
+
        file_data_update_ci_dest(fd, dest_path);
-       
+
        work = fd->sidecar_files;
        while (work)
                {
                FileData *sfd = work->data;
-               
+
                file_data_update_ci_dest_preserve_ext(sfd, dest_path);
                work = work->next;
                }
-       
+
        g_free(dest_path_full);
 }
 
@@ -1775,7 +2100,7 @@ gboolean file_data_sc_update_ci_copy(FileData *fd, const gchar *dest_path)
 {
        return file_data_sc_check_update_ci(fd, dest_path, FILEDATA_CHANGE_COPY);
 }
-       
+
 gboolean file_data_sc_update_ci_move(FileData *fd, const gchar *dest_path)
 {
        return file_data_sc_check_update_ci(fd, dest_path, FILEDATA_CHANGE_MOVE);
@@ -1797,16 +2122,16 @@ static gboolean file_data_sc_update_ci_list_call_func(GList *fd_list,
 {
        GList *work;
        gboolean ret = TRUE;
-       
+
        work = fd_list;
        while (work)
                {
                FileData *fd = work->data;
-               
+
                if (!func(fd, dest)) ret = FALSE;
                work = work->next;
                }
-       
+
        return ret;
 }
 
@@ -1835,7 +2160,7 @@ gint file_data_verify_ci(FileData *fd)
 {
        gint ret = CHANGE_OK;
        gchar *dir;
-       
+
        if (!fd->change)
                {
                DEBUG_1("Change checked: no change info: %s", fd->path);
@@ -1849,9 +2174,19 @@ gint file_data_verify_ci(FileData *fd)
                DEBUG_1("Change checked: file does not exist: %s", fd->path);
                return ret;
                }
-               
+
        dir = remove_level_from_path(fd->path);
-       
+
+       if (fd->change->type != FILEDATA_CHANGE_DELETE &&
+           fd->change->type != FILEDATA_CHANGE_MOVE && /* the unsaved metadata should survive move and rename operations */
+           fd->change->type != FILEDATA_CHANGE_RENAME &&
+           fd->change->type != FILEDATA_CHANGE_WRITE_METADATA &&
+           fd->modified_xmp)
+               {
+               ret |= CHANGE_WARN_UNSAVED_META;
+               DEBUG_1("Change checked: unsaved metadata: %s", fd->path);
+               }
+
        if (fd->change->type != FILEDATA_CHANGE_DELETE &&
            fd->change->type != FILEDATA_CHANGE_WRITE_METADATA &&
            !access_file(fd->path, R_OK))
@@ -1882,10 +2217,10 @@ gint file_data_verify_ci(FileData *fd)
                /* determine destination file */
                gboolean have_dest = FALSE;
                gchar *dest_dir = NULL;
-               
+
                if (options->metadata.save_in_image_file)
                        {
-                       if (file_data_can_write_directly(fd)) 
+                       if (file_data_can_write_directly(fd))
                                {
                                /* we can write the file directly */
                                if (access_file(fd->path, W_OK))
@@ -1901,7 +2236,7 @@ gint file_data_verify_ci(FileData *fd)
                                                }
                                        }
                                }
-                       else if (file_data_can_write_sidecar(fd)) 
+                       else if (file_data_can_write_sidecar(fd))
                                {
                                /* we can write sidecar */
                                gchar *sidecar = file_data_get_sidecar_path(fd, FALSE);
@@ -1921,7 +2256,7 @@ gint file_data_verify_ci(FileData *fd)
                                g_free(sidecar);
                                }
                        }
-               
+
                if (!have_dest)
                        {
                        /* write private metadata file under ~/.geeqie */
@@ -1929,9 +2264,13 @@ gint file_data_verify_ci(FileData *fd)
                        /* If an existing metadata file exists, we will try writing to
                         * it's location regardless of the user's preference.
                         */
-                       gchar *metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
+                       gchar *metadata_path = NULL;
+#ifdef HAVE_EXIV2
+                       /* but ignore XMP if we are not able to write it */
+                       metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
+#endif
                        if (!metadata_path) metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
-                       
+
                        if (metadata_path && !access_file(metadata_path, W_OK))
                                {
                                g_free(metadata_path);
@@ -1946,7 +2285,7 @@ gint file_data_verify_ci(FileData *fd)
                                if (recursive_mkdir_if_not_exists(dest_dir, mode))
                                        {
                                        gchar *filename = g_strconcat(fd->name, options->metadata.save_legacy_format ? GQ_CACHE_EXT_METADATA : GQ_CACHE_EXT_XMP_METADATA, NULL);
-                       
+
                                        metadata_path = g_build_filename(dest_dir, filename, NULL);
                                        g_free(filename);
                                        }
@@ -1965,12 +2304,12 @@ gint file_data_verify_ci(FileData *fd)
                        }
                g_free(dest_dir);
                }
-               
+
        if (fd->change->dest && fd->change->type != FILEDATA_CHANGE_WRITE_METADATA)
                {
                gboolean same;
                gchar *dest_dir;
-                       
+
                same = (strcmp(fd->path, fd->change->dest) == 0);
 
                if (!same)
@@ -2002,7 +2341,7 @@ gint file_data_verify_ci(FileData *fd)
                        }
                else if (!access_file(dest_dir, W_OK))
                        {
-                       ret |= CHANGE_NO_WRITE_PERM_DEST_DIR;
+                       ret |= CHANGE_WARN_NO_WRITE_PERM_DEST_DIR;
                        DEBUG_1("Change checked: destination dir is readonly: %s -> %s", fd->path, fd->change->dest);
                        }
                else if (!same)
@@ -2029,7 +2368,7 @@ gint file_data_verify_ci(FileData *fd)
 
                g_free(dest_dir);
                }
-               
+
        fd->change->error = ret;
        if (ret == 0) DEBUG_1("Change checked: OK: %s", fd->path);
 
@@ -2079,7 +2418,7 @@ gchar *file_data_get_error_string(gint error)
                g_string_append(result, _("destination can't be overwritten"));
                }
 
-       if (error & CHANGE_NO_WRITE_PERM_DEST_DIR)
+       if (error & CHANGE_WARN_NO_WRITE_PERM_DEST_DIR)
                {
                if (result->len > 0) g_string_append(result, ", ");
                g_string_append(result, _("destination directory is not writable"));
@@ -2114,7 +2453,7 @@ gchar *file_data_get_error_string(gint error)
                if (result->len > 0) g_string_append(result, ", ");
                g_string_append(result, _("destination already exists and will be overwritten"));
                }
-               
+
        if (error & CHANGE_WARN_SAME)
                {
                if (result->len > 0) g_string_append(result, ", ");
@@ -2127,6 +2466,12 @@ gchar *file_data_get_error_string(gint error)
                g_string_append(result, _("source and destination have different extension"));
                }
 
+       if (error & CHANGE_WARN_UNSAVED_META)
+               {
+               if (result->len > 0) g_string_append(result, ", ");
+               g_string_append(result, _("there are unsaved metadata changes for the file"));
+               }
+
        return g_string_free(result, FALSE);
 }
 
@@ -2138,9 +2483,9 @@ gint file_data_verify_ci_list(GList *list, gchar **desc, gboolean with_sidecars)
        gint num;
        gint *errors;
        gint i;
-       
+
        if (!list) return 0;
-       
+
        num = g_list_length(list);
        errors = g_new(int, num);
        work = list;
@@ -2152,21 +2497,21 @@ gint file_data_verify_ci_list(GList *list, gchar **desc, gboolean with_sidecars)
 
                fd = work->data;
                work = work->next;
-                       
+
                error = with_sidecars ? file_data_sc_verify_ci(fd) : file_data_verify_ci(fd);
                all_errors |= error;
                common_errors &= error;
-               
+
                errors[i] = error;
-               
+
                i++;
                }
-       
+
        if (desc && all_errors)
                {
                GList *work;
                GString *result = g_string_new("");
-               
+
                if (common_errors)
                        {
                        gchar *str = file_data_get_error_string(common_errors);
@@ -2174,7 +2519,7 @@ gint file_data_verify_ci_list(GList *list, gchar **desc, gboolean with_sidecars)
                        g_string_append(result, "\n");
                        g_free(str);
                        }
-               
+
                work = list;
                i = 0;
                while (work)
@@ -2184,9 +2529,9 @@ gint file_data_verify_ci_list(GList *list, gchar **desc, gboolean with_sidecars)
 
                        fd = work->data;
                        work = work->next;
-                       
+
                        error = errors[i] & ~common_errors;
-                       
+
                        if (error)
                                {
                                gchar *str = file_data_get_error_string(error);
@@ -2263,20 +2608,20 @@ gboolean file_data_sc_perform_ci(FileData *fd)
        GList *work;
        gboolean ret = TRUE;
        FileDataChangeType type = fd->change->type;
-       
+
        if (!file_data_sc_check_ci(fd, type)) return FALSE;
 
        work = fd->sidecar_files;
        while (work)
                {
                FileData *sfd = work->data;
-               
+
                if (!file_data_perform_ci(sfd)) ret = FALSE;
                work = work->next;
                }
-       
+
        if (!file_data_perform_ci(fd)) ret = FALSE;
-       
+
        return ret;
 }
 
@@ -2293,7 +2638,7 @@ gboolean file_data_apply_ci(FileData *fd)
                {
                DEBUG_1("planned change: applying %s -> %s", fd->change->dest, fd->path);
                file_data_planned_change_remove(fd);
-               
+
                if (g_hash_table_lookup(file_data_pool, fd->change->dest))
                        {
                        /* this change overwrites another file which is already known to other modules
@@ -2310,7 +2655,7 @@ gboolean file_data_apply_ci(FileData *fd)
                }
        file_data_increment_version(fd);
        file_data_send_notification(fd, NOTIFY_CHANGE);
-       
+
        return TRUE;
 }
 
@@ -2318,25 +2663,89 @@ gboolean file_data_sc_apply_ci(FileData *fd)
 {
        GList *work;
        FileDataChangeType type = fd->change->type;
-       
+
        if (!file_data_sc_check_ci(fd, type)) return FALSE;
 
        work = fd->sidecar_files;
        while (work)
                {
                FileData *sfd = work->data;
-               
+
                file_data_apply_ci(sfd);
                work = work->next;
                }
-       
+
        file_data_apply_ci(fd);
-       
+
+       return TRUE;
+}
+
+static gboolean file_data_list_contains_whole_group(GList *list, FileData *fd)
+{
+       GList *work;
+       if (fd->parent) fd = fd->parent;
+       if (!g_list_find(list, fd)) return FALSE;
+
+       work = fd->sidecar_files;
+       while (work)
+               {
+               if (!g_list_find(list, work->data)) return FALSE;
+               work = work->next;
+               }
        return TRUE;
 }
 
+GList *file_data_process_groups_in_selection(GList *list, gboolean ungroup, GList **ungrouped_list)
+{
+       GList *out = NULL;
+       GList *work = list;
+
+       /* change partial groups to independent files */
+       if (ungroup)
+               {
+               while (work)
+                       {
+                       FileData *fd = work->data;
+                       work = work->next;
+
+                       if (!file_data_list_contains_whole_group(list, fd))
+                               {
+                               file_data_disable_grouping(fd, TRUE);
+                               if (ungrouped_list)
+                                       {
+                                       *ungrouped_list = g_list_prepend(*ungrouped_list, file_data_ref(fd));
+                                       }
+                               }
+                       }
+               }
+
+       /* remove sidecars from the list,
+          they can be still acessed via main_fd->sidecar_files */
+       work = list;
+       while (work)
+               {
+               FileData *fd = work->data;
+               work = work->next;
+
+               if (!fd->parent ||
+                   (!ungroup && !file_data_list_contains_whole_group(list, fd)))
+                       {
+                       out = g_list_prepend(out, file_data_ref(fd));
+                       }
+               }
+
+       filelist_free(list);
+       out = g_list_reverse(out);
+
+       return out;
+}
+
+
+
+
+
 /*
- * notify other modules about the change described by FileFataChangeInfo
+ * notify other modules about the change described by FileDataChangeInfo
  */
 
 /* might use file_maint_ functions for now, later it should be changed to a system of callbacks
@@ -2344,6 +2753,12 @@ gboolean file_data_sc_apply_ci(FileData *fd)
    implementation in view_file_list.c */
 
 
+typedef struct _NotifyIdleData NotifyIdleData;
+
+struct _NotifyIdleData {
+       FileData *fd;
+       NotifyType type;
+};
 
 
 typedef struct _NotifyData NotifyData;
@@ -2369,7 +2784,20 @@ static gint file_data_notify_sort(gconstpointer a, gconstpointer b)
 gboolean file_data_register_notify_func(FileDataNotifyFunc func, gpointer data, NotifyPriority priority)
 {
        NotifyData *nd;
-       
+       GList *work = notify_func_list;
+
+       while (work)
+               {
+               NotifyData *nd = (NotifyData *)work->data;
+
+               if (nd->func == func && nd->data == data)
+                       {
+                       g_warning("Notify func already registered");
+                       return FALSE;
+                       }
+               work = work->next;
+               }
+
        nd = g_new(NotifyData, 1);
        nd->func = func;
        nd->data = data;
@@ -2377,18 +2805,18 @@ gboolean file_data_register_notify_func(FileDataNotifyFunc func, gpointer data,
 
        notify_func_list = g_list_insert_sorted(notify_func_list, nd, file_data_notify_sort);
        DEBUG_2("Notify func registered: %p", nd);
-       
+
        return TRUE;
 }
 
 gboolean file_data_unregister_notify_func(FileDataNotifyFunc func, gpointer data)
 {
        GList *work = notify_func_list;
-       
+
        while (work)
                {
                NotifyData *nd = (NotifyData *)work->data;
-       
+
                if (nd->func == func && nd->data == data)
                        {
                        notify_func_list = g_list_delete_link(notify_func_list, work);
@@ -2399,21 +2827,34 @@ gboolean file_data_unregister_notify_func(FileDataNotifyFunc func, gpointer data
                work = work->next;
                }
 
+       g_warning("Notify func not found");
        return FALSE;
 }
 
 
-void file_data_send_notification(FileData *fd, NotifyType type)
+gboolean file_data_send_notification_idle_cb(gpointer data)
 {
+       NotifyIdleData *nid = (NotifyIdleData *)data;
        GList *work = notify_func_list;
 
        while (work)
                {
                NotifyData *nd = (NotifyData *)work->data;
-               
-               nd->func(fd, type, nd->data);
+
+               nd->func(nid->fd, nid->type, nd->data);
                work = work->next;
                }
+       file_data_unref(nid->fd);
+       g_free(nid);
+       return FALSE;
+}
+
+void file_data_send_notification(FileData *fd, NotifyType type)
+{
+       NotifyIdleData *nid = g_new0(NotifyIdleData, 1);
+       nid->fd = file_data_ref(fd);
+       nid->type = type;
+       g_idle_add_full(G_PRIORITY_HIGH, file_data_send_notification_idle_cb, nid, NULL);
 }
 
 static GHashTable *file_data_monitor_pool = NULL;
@@ -2424,7 +2865,7 @@ static void realtime_monitor_check_cb(gpointer key, gpointer value, gpointer dat
        FileData *fd = key;
 
        file_data_check_changed_files(fd);
-       
+
        DEBUG_1("monitor %s", fd->path);
 }
 
@@ -2438,24 +2879,24 @@ static gboolean realtime_monitor_cb(gpointer data)
 gboolean file_data_register_real_time_monitor(FileData *fd)
 {
        gint count;
-       
+
        file_data_ref(fd);
-       
+
        if (!file_data_monitor_pool)
                file_data_monitor_pool = g_hash_table_new(g_direct_hash, g_direct_equal);
-       
+
        count = GPOINTER_TO_INT(g_hash_table_lookup(file_data_monitor_pool, fd));
 
        DEBUG_1("Register realtime %d %s", count, fd->path);
-       
+
        count++;
        g_hash_table_insert(file_data_monitor_pool, fd, GINT_TO_POINTER(count));
-       
+
        if (!realtime_monitor_id)
                {
                realtime_monitor_id = g_timeout_add(5000, realtime_monitor_cb, NULL);
                }
-       
+
        return TRUE;
 }
 
@@ -2464,29 +2905,29 @@ gboolean file_data_unregister_real_time_monitor(FileData *fd)
        gint count;
 
        g_assert(file_data_monitor_pool);
-       
+
        count = GPOINTER_TO_INT(g_hash_table_lookup(file_data_monitor_pool, fd));
-       
+
        DEBUG_1("Unregister realtime %d %s", count, fd->path);
-       
+
        g_assert(count > 0);
-       
+
        count--;
-       
+
        if (count == 0)
                g_hash_table_remove(file_data_monitor_pool, fd);
        else
                g_hash_table_insert(file_data_monitor_pool, fd, GINT_TO_POINTER(count));
 
        file_data_unref(fd);
-       
+
        if (g_hash_table_size(file_data_monitor_pool) == 0)
                {
                g_source_remove(realtime_monitor_id);
                realtime_monitor_id = 0;
                return FALSE;
                }
-       
+
        return TRUE;
 }
 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */