most of the metadata options now works
[geeqie.git] / src / filedata.c
index 04d6dc2..4aca86d 100644 (file)
 
 #include "filefilter.h"
 #include "cache.h"
-#include "rcfile.h"
-#include "secure_save.h"
 #include "thumb_standard.h"
 #include "ui_fileops.h"
+#include "metadata.h"
 
 
+static GHashTable *file_data_pool = NULL;
+static GHashTable *file_data_planned_change_hash = NULL;
+
 static gint sidecar_file_priority(const gchar *path);
 
 
@@ -86,16 +88,16 @@ gchar *text_from_size_abrev(gint64 size)
                }
        if (size < (gint64)1048576)
                {
-               return g_strdup_printf(_("%.1f K"), (double)size / 1024.0);
+               return g_strdup_printf(_("%.1f K"), (gdouble)size / 1024.0);
                }
        if (size < (gint64)1073741824)
                {
-               return g_strdup_printf(_("%.1f MB"), (double)size / 1048576.0);
+               return g_strdup_printf(_("%.1f MB"), (gdouble)size / 1048576.0);
                }
 
-       /* to avoid overflowing the double, do division in two steps */
+       /* to avoid overflowing the gdouble, do division in two steps */
        size /= 1048576;
-       return g_strdup_printf(_("%.1f GB"), (double)size / 1024.0);
+       return g_strdup_printf(_("%.1f GB"), (gdouble)size / 1024.0);
 }
 
 /* note: returned string is valid until next call to text_from_time() */
@@ -142,15 +144,49 @@ void file_data_increment_version(FileData *fd)
        if (fd->parent) fd->parent->version++;
 }
 
+static void file_data_set_collate_keys(FileData *fd)
+{
+       gchar *caseless_name;
+
+       caseless_name = g_utf8_casefold(fd->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_nocase = g_utf8_collate_key(caseless_name, -1);
+#endif
+       g_free(caseless_name);
+}
 
 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);
+
+       g_free(fd->path);
+
+       if (fd->original_path)
+               {
+               g_hash_table_remove(file_data_pool, fd->original_path);
+               g_free(fd->original_path);
+               }
+
+       g_assert(!g_hash_table_lookup(file_data_pool, path));
+
+       fd->original_path = g_strdup(path);
+       g_hash_table_insert(file_data_pool, fd->original_path, fd);
 
        if (strcmp(path, G_DIR_SEPARATOR_S) == 0)
                {
                fd->path = g_strdup(path);
                fd->name = fd->path;
                fd->extension = fd->name + 1;
+               file_data_set_collate_keys(fd);
                return;
                }
 
@@ -165,6 +201,7 @@ static void file_data_set_path(FileData *fd, const gchar *path)
                g_free(dir);
                fd->name = "..";
                fd->extension = fd->name + 2;
+               file_data_set_collate_keys(fd);
                return;
                }
        else if (strcmp(fd->name, ".") == 0)
@@ -173,25 +210,33 @@ static void file_data_set_path(FileData *fd, const gchar *path)
                fd->path = remove_level_from_path(path);
                fd->name = ".";
                fd->extension = fd->name + 1;
+               file_data_set_collate_keys(fd);
                return;
                }
 
        fd->extension = extension_from_path(fd->path);
        if (fd->extension == NULL)
                fd->extension = fd->name + strlen(fd->name);
+
+       file_data_set_collate_keys(fd);
 }
 
-static void file_data_check_changed_files(FileData *fd, struct stat *st)
+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;
-               if (fd->pixbuf) g_object_unref(fd->pixbuf);
-               fd->pixbuf = NULL;
+               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_TYPE_REREAD);
+               ret = TRUE;
                }
 
        work = fd->sidecar_files;
@@ -199,18 +244,58 @@ static void file_data_check_changed_files(FileData *fd, struct stat *st)
                {
                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;
                        }
 
-               file_data_check_changed_files(sfd, &st);
-               work = work->next;
+               ret |= file_data_check_changed_files_recursive(sfd, &st);
                }
+       return ret;
 }
 
-static GHashTable *file_data_pool = NULL;
+
+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); /* this will group the sidecars back together */
+               file_data_send_notification(fd, NOTIFY_TYPE_REREAD);
+               }
+       else
+               {
+               ret |= file_data_check_changed_files_recursive(fd, &st);
+               }
+
+       return ret;
+}
 
 static FileData *file_data_new(const gchar *path_utf8, struct stat *st, gboolean check_sidecars)
 {
@@ -224,36 +309,73 @@ static FileData *file_data_new(const gchar *path_utf8, struct stat *st, gboolean
        fd = g_hash_table_lookup(file_data_pool, path_utf8);
        if (fd)
                {
-               file_data_check_changed_files(fd, st);
-               DEBUG_2("file_data_pool hit: '%s'", fd->path);
-               return file_data_ref(fd);
+               file_data_ref(fd);
+               }
+               
+       if (!fd && file_data_planned_change_hash)
+               {
+               fd = g_hash_table_lookup(file_data_planned_change_hash, path_utf8);
+               if (fd)
+                       {
+                       DEBUG_1("planned change: using %s -> %s", path_utf8, fd->path);
+                       file_data_ref(fd);
+                       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);
+               DEBUG_2("file_data_pool hit: '%s' %s", fd->path, changed ? "(changed)" : "");
+               
+               return fd;
                }
 
        fd = g_new0(FileData, 1);
+       
+       fd->path = NULL;
+       fd->name = NULL;
+       fd->collate_key_name = NULL;
+       fd->collate_key_name_nocase = NULL;
+       fd->original_path = NULL;
 
-       file_data_set_path(fd, path_utf8);
-
-       fd->original_path = g_strdup(path_utf8);
        fd->size = st->st_size;
        fd->date = st->st_mtime;
-       fd->pixbuf = NULL;
+       fd->mode = st->st_mode;
+       fd->thumb_pixbuf = NULL;
        fd->sidecar_files = NULL;
        fd->ref = 1;
        fd->magick = 0x12345678;
 
-       g_hash_table_insert(file_data_pool, fd->original_path, fd);
+       file_data_set_path(fd, path_utf8); /* set path, name, collate_key_*, original_path */
 
-       if (check_sidecars && sidecar_file_priority(fd->extension))
+       if (check_sidecars)
                file_data_check_sidecars(fd);
+
        return fd;
 }
 
 static void file_data_check_sidecars(FileData *fd)
 {
-       int base_len = fd->extension - fd->path;
-       GString *fname = g_string_new_len(fd->path, base_len);
+       gint base_len;
+       GString *fname;
        FileData *parent_fd = NULL;
-       GList *work = sidecar_ext_get_list();
+       GList *work;
+
+       if (fd->disable_grouping || !sidecar_file_priority(fd->extension))
+               return;
+
+       base_len = fd->extension - fd->path;
+       fname = g_string_new_len(fd->path, base_len);
+       work = sidecar_ext_get_list();
+
        while (work)
                {
                /* check for possible sidecar files;
@@ -263,8 +385,8 @@ static void file_data_check_sidecars(FileData *fd)
                */
 
                FileData *new_fd;
-
                gchar *ext = work->data;
+
                work = work->next;
 
                if (strcmp(ext, fd->extension) == 0)
@@ -281,6 +403,13 @@ static void file_data_check_sidecars(FileData *fd)
                                continue;
 
                        new_fd = file_data_new(fname->str, &nst, FALSE);
+                       
+                       if (new_fd->disable_grouping)
+                               {
+                               file_data_unref(new_fd);
+                               continue;
+                               }
+                       
                        new_fd->ref--; /* do not use ref here */
                        }
 
@@ -297,6 +426,7 @@ static FileData *file_data_new_local(const gchar *path, struct stat *st, gboolea
 {
        gchar *path_utf8 = path_to_utf8(path);
        FileData *ret = file_data_new(path_utf8, st, check_sidecars);
+
        g_free(path_utf8);
        return ret;
 }
@@ -317,8 +447,9 @@ FileData *file_data_new_simple(const gchar *path_utf8)
 FileData *file_data_add_sidecar_file(FileData *target, FileData *sfd)
 {
        sfd->parent = target;
-       if(!g_list_find(target->sidecar_files, sfd))
+       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;
 }
 
@@ -326,6 +457,7 @@ FileData *file_data_add_sidecar_file(FileData *target, FileData *sfd)
 FileData *file_data_merge_sidecar_files(FileData *target, FileData *source)
 {
        GList *work;
+       
        file_data_add_sidecar_file(target, source);
 
        work = source->sidecar_files;
@@ -340,18 +472,29 @@ FileData *file_data_merge_sidecar_files(FileData *target, FileData *source)
        source->sidecar_files = NULL;
 
        target->sidecar_files = filelist_sort(target->sidecar_files, SORT_NAME, TRUE);
+       
        return target;
 }
 
-
-
+#ifdef DEBUG_FILEDATA
+FileData *file_data_ref_debug(const gchar *file, gint line, FileData *fd)
+#else
 FileData *file_data_ref(FileData *fd)
+#endif
 {
        if (fd == NULL) return NULL;
-
-//     return g_memdup(fd, sizeof(FileData));
+#ifdef DEBUG_FILEDATA
+       if (fd->magick != 0x12345678)
+               DEBUG_0("fd magick mismatch at %s:%d", file, line);
+#endif
        g_assert(fd->magick == 0x12345678);
        fd->ref++;
+
+#ifdef DEBUG_FILEDATA
+       DEBUG_2("file_data_ref (%d): '%s' @ %s:%d", fd->ref, fd->path, file, line);
+#else
+       DEBUG_2("file_data_ref (%d): '%s'", fd->ref, fd->path);
+#endif
        return fd;
 }
 
@@ -364,8 +507,9 @@ static void file_data_free(FileData *fd)
 
        g_free(fd->path);
        g_free(fd->original_path);
-       if (fd->pixbuf) g_object_unref(fd->pixbuf);
-
+       g_free(fd->collate_key_name);
+       g_free(fd->collate_key_name_nocase);
+       if (fd->thumb_pixbuf) g_object_unref(fd->thumb_pixbuf);
 
        g_assert(fd->sidecar_files == NULL); /* sidecar files must be freed before calling this */
 
@@ -373,20 +517,31 @@ static void file_data_free(FileData *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
 {
        if (fd == NULL) return;
+#ifdef DEBUG_FILEDATA
+       if (fd->magick != 0x12345678)
+               DEBUG_0("fd magick mismatch @ %s:%d", file, line);
+#endif
        g_assert(fd->magick == 0x12345678);
-
+       
        fd->ref--;
-       DEBUG_2("file_data_unref (%d): '%s'", fd->ref, fd->path);
+#ifdef DEBUG_FILEDATA
+       DEBUG_2("file_data_unref (%d): '%s' @ %s:%d", fd->ref, fd->path, file, line);
 
+#else
+       DEBUG_2("file_data_unref (%d): '%s'", fd->ref, fd->path);
+#endif
        if (fd->ref == 0)
                {
-               FileData *parent = fd->parent ? fd->parent : fd;
-
                GList *work;
-
+               FileData *parent = fd->parent ? fd->parent : fd;
+               
                if (parent->ref > 0)
                        return;
 
@@ -401,7 +556,7 @@ void file_data_unref(FileData *fd)
 
                /* none of parent/children is referenced, we can free everything */
 
-               DEBUG_2("file_data_unref: deleting '%s', parent '%s'", fd->path, parent->path);
+               DEBUG_2("file_data_unref: deleting '%s', parent '%s'", fd->path, fd->parent ? parent->path : "-");
 
                work = parent->sidecar_files;
                while (work)
@@ -415,7 +570,6 @@ void file_data_unref(FileData *fd)
                parent->sidecar_files = NULL;
 
                file_data_free(parent);
-
                }
 }
 
@@ -423,18 +577,60 @@ FileData *file_data_disconnect_sidecar_file(FileData *target, FileData *sfd)
 {
        sfd->parent = target;
        g_assert(g_list_find(target->sidecar_files, sfd));
+       
+       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) {
+       if (sfd->ref == 0)
+               {
                file_data_free(sfd);
                return NULL;
-       }
+               }
 
        return 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)
+                       {
+                       FileData *parent = file_data_ref(fd->parent);
+                       file_data_disconnect_sidecar_file(parent, fd);
+                       file_data_send_notification(fd, NOTIFY_TYPE_INTERNAL);
+                       file_data_send_notification(parent, NOTIFY_TYPE_INTERNAL);
+                       file_data_unref(parent);
+                       }
+               else if (fd->sidecar_files)
+                       {
+                       GList *sidecar_files = filelist_copy(fd->sidecar_files);
+                       GList *work = sidecar_files;
+                       while (work)
+                               {
+                               FileData *sfd = work->data;
+                               work = work->next;
+                               file_data_disconnect_sidecar_file(fd, sfd);
+                               file_data_send_notification(sfd, NOTIFY_TYPE_INTERNAL);
+                               }
+                       file_data_send_notification(fd, NOTIFY_TYPE_INTERNAL);
+                       file_data_check_sidecars((FileData *)sidecar_files->data); /* this will group the sidecars back together */
+                       filelist_free(sidecar_files);
+                       }
+               }
+       else
+               {
+               file_data_check_sidecars(fd);
+               file_data_send_notification(fd, NOTIFY_TYPE_INTERNAL);
+               }
+}
+
 /* compare name without extension */
 gint file_data_compare_name_without_ext(FileData *fd1, FileData *fd2)
 {
@@ -444,30 +640,7 @@ gint file_data_compare_name_without_ext(FileData *fd1, FileData *fd2)
        if (len1 < len2) return -1;
        if (len1 > len2) return 1;
 
-       return strncmp(fd1->name, fd2->name, len1);
-}
-
-gboolean file_data_add_change_info(FileData *fd, FileDataChangeType type, const gchar *src, const gchar *dest)
-{
-
-       FileDataChangeInfo *fdci;
-
-       if (fd->change) return FALSE;
-
-       fdci = g_new0(FileDataChangeInfo, 1);
-
-       fdci->type = type;
-
-       if (src)
-               fdci->source = g_strdup(src);
-       else
-               fdci->source = g_strdup(fd->path);
-
-       if (dest)
-               fdci->dest = g_strdup(dest);
-
-       fd->change = fdci;
-       return TRUE;
+       return strncmp(fd1->name, fd2->name, len1); /* FIXME: utf8 */
 }
 
 void file_data_change_info_free(FileDataChangeInfo *fdci, FileData *fd)
@@ -500,9 +673,10 @@ void file_data_change_info_free(FileDataChangeInfo *fdci, FileData *fd)
 
 static gint sidecar_file_priority(const gchar *path)
 {
-       const char *extension = extension_from_path(path);
-       int i = 1;
+       const gchar *extension = extension_from_path(path);
+       gint i = 1;
        GList *work;
+
        if (extension == NULL)
                return 0;
 
@@ -510,6 +684,7 @@ static gint sidecar_file_priority(const gchar *path)
 
        while (work) {
                gchar *ext = work->data;
+               
                work = work->next;
                if (strcmp(extension, ext) == 0) return i;
                i++;
@@ -539,26 +714,31 @@ gint filelist_sort_compare_filedata(FileData *fa, FileData *fb)
 
        switch (filelist_sort_method)
                {
+               case SORT_NAME:
+                       break;
                case SORT_SIZE:
                        if (fa->size < fb->size) return -1;
                        if (fa->size > fb->size) return 1;
-                       return CASE_SORT(fa->name, fb->name); /* fall back to name */
+                       /* fall back to name */
                        break;
                case SORT_TIME:
                        if (fa->date < fb->date) return -1;
                        if (fa->date > fb->date) return 1;
-                       return CASE_SORT(fa->name, fb->name); /* fall back to name */
+                       /* fall back to name */
                        break;
 #ifdef HAVE_STRVERSCMP
                case SORT_NUMBER:
                        return strverscmp(fa->name, fb->name);
                        break;
 #endif
-               case SORT_NAME:
                default:
-                       return CASE_SORT(fa->name, fb->name);
                        break;
                }
+
+       if (options->file_sort.case_sensitive)
+               return strcmp(fa->collate_key_name, fb->collate_key_name);
+       else
+               return strcmp(fa->collate_key_name_nocase, fb->collate_key_name_nocase);
 }
 
 gint filelist_sort_compare_filedata_full(FileData *fa, FileData *fb, SortType method, gint ascend)
@@ -568,7 +748,7 @@ gint filelist_sort_compare_filedata_full(FileData *fa, FileData *fb, SortType me
        return filelist_sort_compare_filedata(fa, fb);
 }
 
-static gint filelist_sort_file_cb(void *a, void *b)
+static gint filelist_sort_file_cb(gpointer a, gpointer b)
 {
        return filelist_sort_compare_filedata(a, b);
 }
@@ -580,7 +760,7 @@ GList *filelist_sort_full(GList *list, SortType method, gint ascend, GCompareFun
        return g_list_sort(list, cb);
 }
 
-GList *filelist_insert_sort_full(GList *list, void *data, SortType method, gint ascend, GCompareFunc cb)
+GList *filelist_insert_sort_full(GList *list, gpointer data, SortType method, gint ascend, GCompareFunc cb)
 {
        filelist_sort_method = method;
        filelist_sort_ascend = ascend;
@@ -606,6 +786,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);
@@ -613,24 +794,25 @@ static GList *filelist_filter_out_sidecars(GList *flist)
                        flist_filtered = g_list_prepend(flist_filtered, fd);
                }
        g_list_free(flist);
+
        return flist_filtered;
 }
 
-static gint filelist_read_real(const gchar *path, GList **files, GList **dirs, gint follow_symlinks)
+static gint filelist_read_real(FileData *dir_fd, GList **files, GList **dirs, gint follow_symlinks)
 {
        DIR *dp;
        struct dirent *dir;
        gchar *pathl;
        GList *dlist = NULL;
        GList *flist = NULL;
-       int (*stat_func)(const char *path, struct stat *buf);
+       gint (*stat_func)(const gchar *path, struct stat *buf);
 
        g_assert(files || dirs);
 
        if (files) *files = NULL;
        if (dirs) *dirs = NULL;
 
-       pathl = path_from_utf8(path);
+       pathl = path_from_utf8(dir_fd->path);
        if (!pathl) return FALSE;
 
        dp = opendir(pathl);
@@ -690,14 +872,14 @@ static gint filelist_read_real(const gchar *path, GList **files, GList **dirs, g
        return TRUE;
 }
 
-gint filelist_read(const gchar *path, GList **files, GList **dirs)
+gint filelist_read(FileData *dir_fd, GList **files, GList **dirs)
 {
-       return filelist_read_real(path, files, dirs, TRUE);
+       return filelist_read_real(dir_fd, files, dirs, TRUE);
 }
 
-gint filelist_read_lstat(const gchar *path, GList **files, GList **dirs)
+gint filelist_read_lstat(FileData *dir_fd, GList **files, GList **dirs)
 {
-       return filelist_read_real(path, files, dirs, FALSE);
+       return filelist_read_real(dir_fd, files, dirs, FALSE);
 }
 
 void filelist_free(GList *list)
@@ -790,15 +972,13 @@ GList *filelist_filter(GList *list, gint is_dir_list)
                                                       strcmp(name, GQ_CACHE_LOCAL_METADATA) == 0)) )
                        {
                        GList *link = work;
-                       work = work->next;
+                       
                        list = g_list_remove_link(list, link);
                        file_data_unref(fd);
                        g_list_free(link);
                        }
-               else
-                       {
-                       work = work->next;
-                       }
+       
+               work = work->next;
                }
 
        return list;
@@ -828,11 +1008,10 @@ static void filelist_recursive_append(GList **list, GList *dirs)
        while (work)
                {
                FileData *fd = (FileData *)(work->data);
-               const gchar *path = fd->path;
-               GList *f = NULL;
-               GList *d = NULL;
+               GList *f;
+               GList *d;
 
-               if (filelist_read(path, &f, &d))
+               if (filelist_read(fd, &f, &d))
                        {
                        f = filelist_filter(f, FALSE);
                        f = filelist_sort_path(f);
@@ -848,12 +1027,12 @@ static void filelist_recursive_append(GList **list, GList *dirs)
                }
 }
 
-GList *filelist_recursive(const gchar *path)
+GList *filelist_recursive(FileData *dir_fd)
 {
-       GList *list = NULL;
-       GList *d = NULL;
+       GList *list;
+       GList *d;
 
-       if (!filelist_read(path, &list, &d)) return NULL;
+       if (!filelist_read(dir_fd, &list, &d)) return NULL;
        list = filelist_filter(list, FALSE);
        list = filelist_sort_path(list);
 
@@ -866,6 +1045,82 @@ GList *filelist_recursive(const gchar *path)
 }
 
 
+/*
+ * marks and orientation
+ */
+
+
+gboolean file_data_get_mark(FileData *fd, gint n)
+{
+       return !!(fd->marks & (1 << n));
+}
+
+guint file_data_get_marks(FileData *fd)
+{
+       return fd->marks;
+}
+
+void file_data_set_mark(FileData *fd, gint n, gboolean value)
+{
+       guint old = fd->marks;
+       if (!value == !(fd->marks & (1 << n))) return;
+
+       fd->marks = fd->marks ^ (1 << n);
+       
+       if (old && !fd->marks) /* keep files with non-zero marks in memory */
+               {
+               file_data_unref(fd);
+               }
+       else if (!old && fd->marks)
+               {
+               file_data_ref(fd);
+               }
+       
+       file_data_increment_version(fd);
+       file_data_send_notification(fd, NOTIFY_TYPE_INTERNAL);
+}
+
+gboolean file_data_filter_marks(FileData *fd, guint filter)
+{
+       return ((fd->marks & filter) == filter);
+}
+
+GList *file_data_filter_marks_list(GList *list, guint filter)
+{
+       GList *work;
+
+       work = list;
+       while (work)
+               {
+               FileData *fd = work->data;
+               GList *link = work;
+               work = work->next;
+
+               if (!file_data_filter_marks(fd, filter))
+                       {
+                       list = g_list_remove_link(list, link);
+                       file_data_unref(fd);
+                       g_list_free(link);
+                       }
+               }
+
+       return list;
+}
+
+gint file_data_get_user_orientation(FileData *fd)
+{
+       return fd->user_orientation;
+}
+
+void file_data_set_user_orientation(FileData *fd, gint value)
+{
+       if (fd->user_orientation == value) return;
+
+       fd->user_orientation = value;
+       file_data_increment_version(fd);
+       file_data_send_notification(fd, NOTIFY_TYPE_INTERNAL);
+}
+
 
 /*
  * file_data    - operates on the given fd
@@ -883,6 +1138,7 @@ gchar *file_data_sc_list_to_string(FileData *fd)
        while (work)
                {
                FileData *sfd = work->data;
+
                result = g_string_append(result, "+ ");
                result = g_string_append(result, sfd->extension);
                work = work->next;
@@ -893,41 +1149,31 @@ gchar *file_data_sc_list_to_string(FileData *fd)
 }
 
 
-/* disables / enables grouping for particular file, sends UPDATE notification */
-void file_data_disable_grouping(FileData *fd); // now file_data_disconnect_sidecar_file, broken
-void file_data_disable_grouping(FileData *fd);
-
-/* runs stat on a file and sends UPDATE notification if it has been changed */
-void file_data_sc_update(FileData *fd);
-
-
 
-
-/* 
- * add FileDataChangeInfo (see typedefs.h) for the given operation 
+/*
+ * add FileDataChangeInfo (see typedefs.h) for the given operation
  * uses file_data_add_change_info
  *
  * fails if the fd->change already exists - change operations can't run in parallel
  * fd->change_info works as a lock
  *
  * dest can be NULL - in this case the current name is used for now, it will
- * be changed later 
+ * be changed later
  */
 
 /*
    FileDataChangeInfo types:
    COPY
-   MOVE - patch is changed, name may be changed too
+   MOVE   - path is changed, name may be changed too
    RENAME - path remains unchanged, name is changed
             extension should remain (FIXME should we allow editing extension? it will make problems wth grouping)
            sidecar names are changed too, extensions are not changed
    DELETE
-   UPDATE - file size, date or grouping has been changed 
+   UPDATE - file size, date or grouping has been changed
 */
 
 gboolean file_data_add_ci(FileData *fd, FileDataChangeType type, const gchar *src, const gchar *dest)
 {
-
        FileDataChangeInfo *fdci;
 
        if (fd->change) return FALSE;
@@ -949,6 +1195,27 @@ gboolean file_data_add_ci(FileData *fd, FileDataChangeType type, const gchar *sr
        return TRUE;
 }
 
+static void file_data_planned_change_remove(FileData *fd)
+{
+       if (file_data_planned_change_hash &&
+           (fd->change->type == FILEDATA_CHANGE_MOVE || fd->change->type == FILEDATA_CHANGE_RENAME))
+               {
+               if (g_hash_table_lookup(file_data_planned_change_hash, fd->change->dest) == fd)
+                       {
+                       DEBUG_1("planned change: removing %s -> %s", fd->change->dest, fd->path);
+                       g_hash_table_remove(file_data_planned_change_hash, fd->change->dest);
+                       file_data_unref(fd);
+                       if (g_hash_table_size(file_data_planned_change_hash) == 0)
+                               {
+                               g_hash_table_destroy(file_data_planned_change_hash);
+                               file_data_planned_change_hash = NULL;
+                               DEBUG_1("planned change: empty");
+                               }
+                       }
+               }
+}
+
+
 void file_data_free_ci(FileData *fd)
 {
        FileDataChangeInfo *fdci = fd->change;
@@ -956,6 +1223,8 @@ void file_data_free_ci(FileData *fd)
        if (!fdci)
                return;
 
+       file_data_planned_change_remove(fd);
+
        g_free(fdci->source);
        g_free(fdci->dest);
 
@@ -964,17 +1233,20 @@ void file_data_free_ci(FileData *fd)
        fd->change = NULL;
 }
 
+
 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;
                }
@@ -985,28 +1257,31 @@ static gboolean file_data_sc_add_ci(FileData *fd, FileDataChangeType type)
        while (work)
                {
                FileData *sfd = work->data;
+               
                file_data_add_ci(sfd, type, NULL, NULL);
                work = work->next;
                }
                
-       return TRUE;    
+       return TRUE;
 }
 
 static gboolean file_data_sc_check_ci(FileData *fd, FileDataChangeType type)
 {
        GList *work;
+       
        if (fd->parent) fd = fd->parent;
        
-       if (!fd->change) return FALSE;
-       if (fd->change->type != type) return FALSE;
+       if (!fd->change || fd->change->type != type) return FALSE;
+       
        work = fd->sidecar_files;
        while (work)
                {
                FileData *sfd = work->data;
-               if (!sfd->change) return FALSE;
-               if (sfd->change->type != type) return FALSE;
+
+               if (!sfd->change || sfd->change->type != type) return FALSE;
                work = work->next;
                }
+
        return TRUE;
 }
 
@@ -1044,9 +1319,15 @@ gboolean file_data_sc_add_ci_unspecified(FileData *fd, const gchar *dest_path)
        return TRUE;
 }
 
+gboolean file_data_add_ci_write_metadata(FileData *fd)
+{
+       return file_data_add_ci(fd, FILEDATA_CHANGE_WRITE_METADATA, NULL, NULL);
+}
+
 void file_data_sc_free_ci(FileData *fd)
 {
        GList *work;
+
        if (fd->parent) fd = fd->parent;
        
        file_data_free_ci(fd);
@@ -1055,6 +1336,7 @@ void file_data_sc_free_ci(FileData *fd)
        while (work)
                {
                FileData *sfd = work->data;
+       
                file_data_free_ci(sfd);
                work = work->next;
                }
@@ -1064,101 +1346,177 @@ gboolean file_data_sc_add_ci_delete_list(GList *fd_list)
 {
        GList *work;
        gboolean ret = TRUE;
+
        work = fd_list;
        while (work)
                {
                FileData *fd = work->data;
+       
                if (!file_data_sc_add_ci_delete(fd)) ret = FALSE;
                work = work->next;
                }
+
        return ret;
 }
 
-gboolean file_data_sc_add_ci_copy_list(GList *fd_list, const gchar *dest)
+static void file_data_sc_revert_ci_list(GList *fd_list)
 {
        GList *work;
-       gboolean ret = TRUE;
+       
        work = fd_list;
        while (work)
                {
                FileData *fd = work->data;
-               if (!file_data_sc_add_ci_copy(fd, dest)) ret = FALSE;
-               work = work->next;
+               
+               file_data_sc_free_ci(fd);
+               work = work->prev;
                }
-       return ret;
 }
 
-gboolean file_data_sc_add_ci_move_list(GList *fd_list, const gchar *dest)
+static gboolean file_data_sc_add_ci_list_call_func(GList *fd_list, const gchar *dest, gboolean (*func)(FileData *, const gchar *))
 {
        GList *work;
-       gboolean ret = TRUE;
+       
        work = fd_list;
        while (work)
                {
                FileData *fd = work->data;
-               if (!file_data_sc_add_ci_move(fd, dest)) ret = FALSE;
+               
+               if (!func(fd, dest))
+                       {
+                       file_data_sc_revert_ci_list(work->prev);
+                       return FALSE;
+                       }
                work = work->next;
                }
-       return ret;
+       
+       return TRUE;
+}
+
+gboolean file_data_sc_add_ci_copy_list(GList *fd_list, const gchar *dest)
+{
+       return file_data_sc_add_ci_list_call_func(fd_list, dest, file_data_sc_add_ci_copy);
+}
+
+gboolean file_data_sc_add_ci_move_list(GList *fd_list, const gchar *dest)
+{
+       return file_data_sc_add_ci_list_call_func(fd_list, dest, file_data_sc_add_ci_move);
 }
 
 gboolean file_data_sc_add_ci_rename_list(GList *fd_list, const gchar *dest)
+{
+       return file_data_sc_add_ci_list_call_func(fd_list, dest, file_data_sc_add_ci_rename);
+}
+
+gboolean file_data_sc_add_ci_unspecified_list(GList *fd_list, const gchar *dest)
+{
+       return file_data_sc_add_ci_list_call_func(fd_list, dest, file_data_sc_add_ci_unspecified);
+}
+
+gboolean file_data_add_ci_write_metadata_list(GList *fd_list)
 {
        GList *work;
        gboolean ret = TRUE;
+
        work = fd_list;
        while (work)
                {
                FileData *fd = work->data;
-               if (!file_data_sc_add_ci_rename(fd, dest)) ret = FALSE;
+       
+               if (!file_data_add_ci_write_metadata(fd)) ret = FALSE;
                work = work->next;
                }
+
        return ret;
 }
 
-gboolean file_data_sc_add_ci_unspecified_list(GList *fd_list, const gchar *dest)
+void file_data_free_ci_list(GList *fd_list)
 {
        GList *work;
-       gboolean ret = TRUE;
+       
        work = fd_list;
        while (work)
                {
                FileData *fd = work->data;
-               if (!file_data_sc_add_ci_unspecified(fd, dest)) ret = FALSE;
+               
+               file_data_free_ci(fd);
                work = work->next;
                }
-       return ret;
 }
 
 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;
                }
 }
 
-/* 
+/*
  * update existing fd->change, it will be used from dialog callbacks for interactive editing
  * fails if fd->change does not exist or the change type does not match
  */
 
+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);
+                       g_hash_table_remove(file_data_planned_change_hash, old_path);
+                       file_data_unref(fd);
+                       }
+
+               ofd = g_hash_table_lookup(file_data_planned_change_hash, new_path);
+               if (ofd != fd)
+                       {
+                       if (ofd)
+                               {
+                               DEBUG_1("planned change: replacing %s -> %s", new_path, ofd->path);
+                               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);
+                       }
+               }
+}
+
 static void file_data_update_ci_dest(FileData *fd, const gchar *dest_path)
 {
-       g_free(fd->change->dest);
+       gchar *old_path = fd->change->dest;
+
        fd->change->dest = g_strdup(dest_path);
+       file_data_update_planned_change_hash(fd, old_path, fd->change->dest);
+       g_free(old_path);
 }
 
 static void file_data_update_ci_dest_preserve_ext(FileData *fd, const gchar *dest_path)
 {
-       const char *extension = extension_from_path(fd->change->source);
+       const gchar *extension = extension_from_path(fd->change->source);
        gchar *base = remove_extension_from_path(dest_path);
-       g_free(fd->change->dest);
-       fd->change->dest = g_strdup_printf("%s%s", base, extension);
+       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);
 }
 
@@ -1166,128 +1524,414 @@ 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;
+       if (fd->parent) fd = fd->parent;
        
-       if (!strchr(dest_path, G_DIR_SEPARATOR)) /* we got only filename, not a full path */
+       if (!dest_path)
+               {
+               dest_path = fd->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;
                }
-       
-       if (isdir(dest_path))
+       else if (fd->change->type != FILEDATA_CHANGE_RENAME && isdir(dest_path)) /* rename should not move files between directories */
                {
                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);
 }
 
-gint file_data_sc_update_ci_copy(FileData *fd, const gchar *dest_path)
+static gint file_data_sc_check_update_ci(FileData *fd, const gchar *dest_path, FileDataChangeType type)
 {
-       if (!file_data_sc_check_ci(fd, FILEDATA_CHANGE_COPY)) return FALSE;
+       if (!file_data_sc_check_ci(fd, type)) return FALSE;
        file_data_sc_update_ci(fd, dest_path);
        return TRUE;
 }
+
+gint 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);
+}
        
 gint file_data_sc_update_ci_move(FileData *fd, const gchar *dest_path)
 {
-       if (!file_data_sc_check_ci(fd, FILEDATA_CHANGE_MOVE)) return FALSE;
-       file_data_sc_update_ci(fd, dest_path);
-       return TRUE;
+       return file_data_sc_check_update_ci(fd, dest_path, FILEDATA_CHANGE_MOVE);
 }
 
 gint file_data_sc_update_ci_rename(FileData *fd, const gchar *dest_path)
 {
-       if (!file_data_sc_check_ci(fd, FILEDATA_CHANGE_RENAME)) return FALSE;
-       file_data_sc_update_ci(fd, dest_path);
-       return TRUE;
+       return file_data_sc_check_update_ci(fd, dest_path, FILEDATA_CHANGE_RENAME);
 }
 
 gint file_data_sc_update_ci_unspecified(FileData *fd, const gchar *dest_path)
 {
-       if (!file_data_sc_check_ci(fd, FILEDATA_CHANGE_UNSPECIFIED)) return FALSE;
-       file_data_sc_update_ci(fd, dest_path);
-       return TRUE;
+       return file_data_sc_check_update_ci(fd, dest_path, FILEDATA_CHANGE_UNSPECIFIED);
 }
 
-
-gboolean file_data_sc_update_ci_move_list(GList *fd_list, const gchar *dest)
+static gboolean file_data_sc_update_ci_list_call_func(GList *fd_list,
+                                                     const gchar *dest,
+                                                     gboolean (*func)(FileData *, const gchar *))
 {
        GList *work;
        gboolean ret = TRUE;
+       
        work = fd_list;
        while (work)
                {
                FileData *fd = work->data;
-               if (!file_data_sc_update_ci_move(fd, dest)) ret = FALSE;
+               
+               if (!func(fd, dest)) ret = FALSE;
                work = work->next;
                }
+       
        return ret;
 }
 
+gboolean file_data_sc_update_ci_move_list(GList *fd_list, const gchar *dest)
+{
+       return file_data_sc_update_ci_list_call_func(fd_list, dest, file_data_sc_update_ci_move);
+}
+
 gboolean file_data_sc_update_ci_copy_list(GList *fd_list, const gchar *dest)
 {
-       GList *work;
-       gboolean ret = TRUE;
-       work = fd_list;
-       while (work)
+       return file_data_sc_update_ci_list_call_func(fd_list, dest, file_data_sc_update_ci_copy);
+}
+
+gboolean file_data_sc_update_ci_unspecified_list(GList *fd_list, const gchar *dest)
+{
+       return file_data_sc_update_ci_list_call_func(fd_list, dest, file_data_sc_update_ci_unspecified);
+}
+
+
+/*
+ * verify source and dest paths - dest image exists, etc.
+ * it should detect all possible problems with the planned operation
+ */
+
+gint file_data_verify_ci(FileData *fd)
+{
+       gint ret = CHANGE_OK;
+       gchar *dir;
+       
+       if (!fd->change)
                {
-               FileData *fd = work->data;
-               if (!file_data_sc_update_ci_copy(fd, dest)) ret = FALSE;
-               work = work->next;
+               DEBUG_1("Change checked: no change info: %s", fd->path);
+               return ret;
+               }
+
+       if (!isname(fd->path) && 
+           !filter_file_class(fd->extension, FORMAT_CLASS_META)) /* xmp sidecar can be eventually created from scratch */
+               {
+               /* this probably should not happen */
+               ret |= CHANGE_NO_SRC;
+               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_WRITE_METADATA &&
+           !access_file(fd->path, R_OK))
+               {
+               ret |= CHANGE_NO_READ_PERM;
+               DEBUG_1("Change checked: no read permission: %s", fd->path);
+               }
+       else if ((fd->change->type == FILEDATA_CHANGE_DELETE || fd->change->type == FILEDATA_CHANGE_MOVE) &&
+                !access_file(dir, W_OK))
+               {
+               ret |= CHANGE_NO_WRITE_PERM_DIR;
+               DEBUG_1("Change checked: source dir is readonly: %s", fd->path);
+               }
+       else if (fd->change->type != FILEDATA_CHANGE_COPY &&
+                fd->change->type != FILEDATA_CHANGE_UNSPECIFIED &&
+                fd->change->type != FILEDATA_CHANGE_WRITE_METADATA &&
+                !access_file(fd->path, W_OK))
+               {
+               ret |= CHANGE_WARN_NO_WRITE_PERM;
+               DEBUG_1("Change checked: no write permission: %s", fd->path);
+               }
+       /* WRITE_METADATA is special because it can be configured to silently write to ~/.geeqie/...
+          - that means that there are no hard errors and warnings can be disabled
+       */
+       else if (fd->change->type == FILEDATA_CHANGE_WRITE_METADATA && 
+                options->metadata.save_in_image_file && options->metadata.warn_on_write_problems)
+               {
+               if (isname(fd->path) && !access_file(fd->path, W_OK))
+                       {
+                       ret |= CHANGE_WARN_NO_WRITE_PERM;
+                       DEBUG_1("Change checked: file is readonly: %s", fd->path);
+                       }
+               
+               else if (!access_file(dir, W_OK))
+                       {
+                       ret |= CHANGE_WARN_NO_WRITE_PERM;
+                       DEBUG_1("Change checked: dir is readonly: %s", fd->path);
+                       }
+               }
+               
+       if (fd->change->dest)
+               {
+               gboolean same;
+               gchar *dest_dir;
+                       
+               same = (strcmp(fd->path, fd->change->dest) == 0);
+
+               if (!same)
+                       {
+                       const gchar *dest_ext = extension_from_path(fd->change->dest);
+                       if (!dest_ext) dest_ext = "";
+
+                       if (strcasecmp(fd->extension, dest_ext) != 0)
+                               {
+                               ret |= CHANGE_WARN_CHANGED_EXT;
+                               DEBUG_1("Change checked: source and destination have different extensions: %s -> %s", fd->path, fd->change->dest);
+                               }
+                       }
+               else
+                       {
+                       if (fd->change->type != FILEDATA_CHANGE_UNSPECIFIED) /* FIXME this is now needed for running editors */
+                               {
+                               ret |= CHANGE_WARN_SAME;
+                               DEBUG_1("Change checked: source and destination are the same: %s -> %s", fd->path, fd->change->dest);
+                               }
+                       }
+
+               dest_dir = remove_level_from_path(fd->change->dest);
+
+               if (!isdir(dest_dir))
+                       {
+                       ret |= CHANGE_NO_DEST_DIR;
+                       DEBUG_1("Change checked: destination dir does not exist: %s -> %s", fd->path, fd->change->dest);
+                       }
+               else if (!access_file(dest_dir, W_OK))
+                       {
+                       ret |= CHANGE_NO_WRITE_PERM_DEST_DIR;
+                       DEBUG_1("Change checked: destination dir is readonly: %s -> %s", fd->path, fd->change->dest);
+                       }
+               else if (!same)
+                       {
+                       if (isfile(fd->change->dest))
+                               {
+                               if (!access_file(fd->change->dest, W_OK))
+                                       {
+                                       ret |= CHANGE_NO_WRITE_PERM_DEST;
+                                       DEBUG_1("Change checked: destination file exists and is readonly: %s -> %s", fd->path, fd->change->dest);
+                                       }
+                               else
+                                       {
+                                       ret |= CHANGE_WARN_DEST_EXISTS;
+                                       DEBUG_1("Change checked: destination exists: %s -> %s", fd->path, fd->change->dest);
+                                       }
+                               }
+                       else if (isdir(fd->change->dest))
+                               {
+                               ret |= CHANGE_DEST_EXISTS;
+                               DEBUG_1("Change checked: destination exists: %s -> %s", fd->path, fd->change->dest);
+                               }
+                       }
+
+               g_free(dest_dir);
                }
+               
+       fd->change->error = ret;
+       if (ret == 0) DEBUG_1("Change checked: OK: %s", fd->path);
+
+       g_free(dir);
        return ret;
 }
 
-gboolean file_data_sc_update_ci_unspecified_list(GList *fd_list, const gchar *dest)
+
+gint file_data_sc_verify_ci(FileData *fd)
 {
        GList *work;
-       gboolean ret = TRUE;
-       work = fd_list;
+       gint ret;
+
+       ret = file_data_verify_ci(fd);
+
+       work = fd->sidecar_files;
        while (work)
                {
-               FileData *fd = work->data;
-               if (!file_data_sc_update_ci_unspecified(fd, dest)) ret = FALSE;
+               FileData *sfd = work->data;
+
+               ret |= file_data_verify_ci(sfd);
                work = work->next;
                }
+
        return ret;
 }
 
-
-/*
- * check dest paths - dest image exists, etc.
- * returns FIXME
- * it should detect all possible problems with the planned operation
- */
-gint file_data_sc_check_ci_dest(FileData *fd)
+gchar *file_data_get_error_string(gint error)
 {
+       GString *result = g_string_new("");
+
+       if (error & CHANGE_NO_SRC)
+               {
+               if (result->len > 0) g_string_append(result, ", ");
+               g_string_append(result, _("file or directory does not exist"));
+               }
+
+       if (error & CHANGE_DEST_EXISTS)
+               {
+               if (result->len > 0) g_string_append(result, ", ");
+               g_string_append(result, _("destination already exists"));
+               }
+
+       if (error & CHANGE_NO_WRITE_PERM_DEST)
+               {
+               if (result->len > 0) g_string_append(result, ", ");
+               g_string_append(result, _("destination can't be overwritten"));
+               }
+
+       if (error & CHANGE_NO_WRITE_PERM_DEST_DIR)
+               {
+               if (result->len > 0) g_string_append(result, ", ");
+               g_string_append(result, _("destination directory is not writable"));
+               }
+
+       if (error & CHANGE_NO_DEST_DIR)
+               {
+               if (result->len > 0) g_string_append(result, ", ");
+               g_string_append(result, _("destination directory does not exist"));
+               }
+
+       if (error & CHANGE_NO_WRITE_PERM_DIR)
+               {
+               if (result->len > 0) g_string_append(result, ", ");
+               g_string_append(result, _("source directory is not writable"));
+               }
+
+       if (error & CHANGE_NO_READ_PERM)
+               {
+               if (result->len > 0) g_string_append(result, ", ");
+               g_string_append(result, _("no read permission"));
+               }
+
+       if (error & CHANGE_WARN_NO_WRITE_PERM)
+               {
+               if (result->len > 0) g_string_append(result, ", ");
+               g_string_append(result, _("file is readonly"));
+               }
+
+       if (error & CHANGE_WARN_DEST_EXISTS)
+               {
+               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, ", ");
+               g_string_append(result, _("source and destination are the same"));
+               }
+
+       if (error & CHANGE_WARN_CHANGED_EXT)
+               {
+               if (result->len > 0) g_string_append(result, ", ");
+               g_string_append(result, _("source and destination have different extension"));
+               }
+
+       return g_string_free(result, FALSE);
 }
 
+gint file_data_verify_ci_list(GList *list, gchar **desc, gboolean with_sidecars)
+{
+       GList *work;
+       gint all_errors = 0;
+       gint common_errors = ~0;
+       gint num;
+       gint *errors;
+       gint i;
+       
+       if (!list) return 0;
+       
+       num = g_list_length(list);
+       errors = g_new(int, num);
+       work = list;
+       i = 0;
+       while (work)
+               {
+               FileData *fd;
+               gint error;
+
+               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);
+                       g_string_append(result, str);
+                       g_string_append(result, "\n");
+                       g_free(str);
+                       }
+               
+               work = list;
+               i = 0;
+               while (work)
+                       {
+                       FileData *fd;
+                       gint error;
+
+                       fd = work->data;
+                       work = work->next;
+                       
+                       error = errors[i] & ~common_errors;
+                       
+                       if (error)
+                               {
+                               gchar *str = file_data_get_error_string(error);
+                               g_string_append_printf(result, "%s: %s\n", fd->name, str);
+                               g_free(str);
+                               }
+                       i++;
+                       }
+               *desc = g_string_free(result, FALSE);
+               }
 
+       g_free(errors);
+       return all_errors;
+}
 
 
 /*
  * perform the change described by FileFataChangeInfo
- * it is used for internal operations, 
+ * it is used for internal operations,
  * this function actually operates with files on the filesystem
  * it should implement safe delete
  */
+
 static gboolean file_data_perform_move(FileData *fd)
 {
        g_assert(!strcmp(fd->change->source, fd->path));
@@ -1302,10 +1946,13 @@ static gboolean file_data_perform_copy(FileData *fd)
 
 static gboolean file_data_perform_delete(FileData *fd)
 {
-       return unlink_file(fd->path);
+       if (isdir(fd->path) && !islink(fd->path))
+               return rmdir_utf8(fd->path);
+       else
+               return unlink_file(fd->path);
 }
 
-static gboolean file_data_perform_ci(FileData *fd)
+gboolean file_data_perform_ci(FileData *fd)
 {
        FileDataChangeType type = fd->change->type;
        switch (type)
@@ -1318,6 +1965,8 @@ static gboolean file_data_perform_ci(FileData *fd)
                        return file_data_perform_move(fd); /* the same as move */
                case FILEDATA_CHANGE_DELETE:
                        return file_data_perform_delete(fd);
+               case FILEDATA_CHANGE_WRITE_METADATA:
+                       return metadata_write_perform(fd);
                case FILEDATA_CHANGE_UNSPECIFIED:
                        /* nothing to do here */
                        break;
@@ -1332,67 +1981,231 @@ 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;
 }
 
 /*
  * updates FileData structure according to FileDataChangeInfo
  */
-static void file_data_apply_ci(FileData *fd)
+
+gint file_data_apply_ci(FileData *fd)
 {
        FileDataChangeType type = fd->change->type;
+
        /* FIXME delete ?*/
        if (type == FILEDATA_CHANGE_MOVE || type == FILEDATA_CHANGE_RENAME)
                {
-               g_free(fd->path);
-               g_hash_table_remove(file_data_pool, fd->original_path);
-               g_free(fd->original_path);
-               file_data_set_path(fd, fd->change->dest);
-               fd->original_path = g_strdup(fd->change->dest);
-               g_hash_table_insert(file_data_pool, fd->original_path, 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
+                          renaming fd would create duplicate FileData structure
+                          the best thing we can do is nothing
+                          FIXME: maybe we could copy stuff like marks
+                       */
+                       DEBUG_1("can't rename fd, target exists %s -> %s", fd->change->dest, fd->path);
+                       }
+               else
+                       {
+                       file_data_set_path(fd, fd->change->dest);
+                       }
                }
        file_data_increment_version(fd);
+       file_data_send_notification(fd, NOTIFY_TYPE_CHANGE);
+       
+       return TRUE;
 }
 
 gint 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;
 }
 
-
 /*
  * notify other modules about the change described by FileFataChangeInfo
  */
+
 /* might use file_maint_ functions for now, later it should be changed to a system of callbacks
    FIXME do we need the ignore_list? It looks like a workaround for ineffective
    implementation in view_file_list.c */
 
-void file_data_sc_send_notification(FileData *fd)
+
+
+
+typedef struct _NotifyData NotifyData;
+
+struct _NotifyData {
+       FileDataNotifyFunc func;
+       gpointer data;
+       NotifyPriority priority;
+       };
+
+static GList *notify_func_list = NULL;
+
+static gint file_data_notify_sort(gconstpointer a, gconstpointer b)
+{
+       NotifyData *nda = (NotifyData *)a;
+       NotifyData *ndb = (NotifyData *)b;
+
+       if (nda->priority < ndb->priority) return -1;
+       if (nda->priority > ndb->priority) return 1;
+       return 0;
+}
+
+gint file_data_register_notify_func(FileDataNotifyFunc func, gpointer data, NotifyPriority priority)
+{
+       NotifyData *nd;
+       
+       nd = g_new(NotifyData, 1);
+       nd->func = func;
+       nd->data = data;
+       nd->priority = priority;
+
+       notify_func_list = g_list_insert_sorted(notify_func_list, nd, file_data_notify_sort);
+       DEBUG_1("Notify func registered: %p", nd);
+       
+       return TRUE;
+}
+
+gint 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);
+                       g_free(nd);
+                       DEBUG_1("Notify func unregistered: %p", nd);
+                       return TRUE;
+                       }
+               work = work->next;
+               }
+
+       return FALSE;
+}
+
+
+void file_data_send_notification(FileData *fd, NotifyType type)
+{
+       GList *work = notify_func_list;
+
+       while (work)
+               {
+               NotifyData *nd = (NotifyData *)work->data;
+               
+               DEBUG_1("Notify func calling: %p %s", nd, fd->path);
+               nd->func(fd, type, nd->data);
+               work = work->next;
+               }
+}
+
+static GHashTable *file_data_monitor_pool = NULL;
+static gint realtime_monitor_id = -1;
+
+static void realtime_monitor_check_cb(gpointer key, gpointer value, gpointer data)
 {
+       FileData *fd = key;
+
+       file_data_check_changed_files(fd);
+       
+       DEBUG_1("monitor %s", fd->path);
+}
+
+static gboolean realtime_monitor_cb(gpointer data)
+{
+       if (!options->update_on_time_change) return TRUE;
+       g_hash_table_foreach(file_data_monitor_pool, realtime_monitor_check_cb, NULL);
+       return TRUE;
+}
+
+gint 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 == -1)
+               {
+               realtime_monitor_id = g_timeout_add(5000, realtime_monitor_cb, NULL);
+               }
+       
+       return TRUE;
 }
 
+gint 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 = -1;
+               return FALSE;
+               }
+       
+       return TRUE;
+}
+/* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */