Include a Other Software section in Help file
[geeqie.git] / src / filedata.c
index e0d70f0..f2c8795 100644 (file)
@@ -1,16 +1,24 @@
 /*
- * Geeqie
- * (C) 2006 John Ellis
- * Copyright (C) 2008 - 2012 The Geeqie Team
+ * Copyright (C) 2006 John Ellis
+ * Copyright (C) 2008 - 2016 The Geeqie Team
  *
  * Author: John Ellis
  *
- * This software is released under the GNU General Public License (GNU GPL).
- * Please read the included file COPYING for more information.
- * This software comes with no warranty of any kind, use at your own risk!
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
 
-
 #include "main.h"
 #include "filedata.h"
 
 #include "metadata.h"
 #include "trash.h"
 #include "histogram.h"
+#include "secure_save.h"
 
 #include "exif.h"
+#include "misc.h"
 
 #include <errno.h>
+#include <grp.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;
@@ -52,7 +67,7 @@ gchar *text_from_size(gint64 size)
        /* what I would like to use is printf("%'d", size)
         * BUT: not supported on every libc :(
         */
-       if (size > G_MAXUINT)
+       if (size > G_MAXINT)
                {
                /* the %lld conversion is not valid in all libcs, so use a simple work-around */
                a = g_strdup_printf("%d%09d", (guint)(size / 1000000000), (guint)(size % 1000000000));
@@ -98,16 +113,16 @@ gchar *text_from_size_abrev(gint64 size)
                }
        if (size < (gint64)1048576)
                {
-               return g_strdup_printf(_("%.1f K"), (gdouble)size / 1024.0);
+               return g_strdup_printf(_("%.1f KiB"), (gdouble)size / 1024.0);
                }
        if (size < (gint64)1073741824)
                {
-               return g_strdup_printf(_("%.1f MB"), (gdouble)size / 1048576.0);
+               return g_strdup_printf(_("%.1f MiB"), (gdouble)size / 1048576.0);
                }
 
        /* to avoid overflowing the gdouble, do division in two steps */
        size /= 1048576;
-       return g_strdup_printf(_("%.1f GB"), (gdouble)size / 1024.0);
+       return g_strdup_printf(_("%.1f GiB"), (gdouble)size / 1024.0);
 }
 
 /* note: returned string is valid until next call to text_from_time() */
@@ -161,6 +176,7 @@ static gboolean file_data_check_changed_single_file(FileData *fd, struct stat *s
                {
                fd->size = st->st_size;
                fd->date = st->st_mtime;
+               fd->cdate = st->st_ctime;
                fd->mode = st->st_mode;
                if (fd->thumb_pixbuf) g_object_unref(fd->thumb_pixbuf);
                fd->thumb_pixbuf = NULL;
@@ -266,8 +282,21 @@ static void file_data_set_collate_keys(FileData *fd)
        g_free(fd->collate_key_name);
        g_free(fd->collate_key_name_nocase);
 
+#if GTK_CHECK_VERSION(2, 8, 0)
+       if (options->file_sort.natural)
+               {
+               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(valid_name, -1);
+               fd->collate_key_name_nocase = g_utf8_collate_key(caseless_name, -1);
+               }
+#else
        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);
@@ -343,6 +372,8 @@ static void file_data_set_path(FileData *fd, const gchar *path)
 static FileData *file_data_new(const gchar *path_utf8, struct stat *st, gboolean disable_sidecars)
 {
        FileData *fd;
+       struct passwd *user;
+       struct group *group;
 
        DEBUG_2("file_data_new: '%s' %d", path_utf8, disable_sidecars);
 
@@ -363,8 +394,15 @@ static FileData *file_data_new(const gchar *path_utf8, struct stat *st, gboolean
                if (fd)
                        {
                        DEBUG_1("planned change: using %s -> %s", path_utf8, fd->path);
-                       file_data_ref(fd);
-                       file_data_apply_ci(fd);
+                       if (!isfile(fd->path))
+                               {
+                               file_data_ref(fd);
+                               file_data_apply_ci(fd);
+                               }
+                       else
+                               {
+                               fd = NULL;
+                               }
                        }
                }
 
@@ -383,12 +421,44 @@ static FileData *file_data_new(const gchar *path_utf8, struct stat *st, gboolean
                }
 
        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->cdate = st->st_ctime;
        fd->mode = st->st_mode;
        fd->ref = 1;
        fd->magick = FD_MAGICK;
+       fd->exifdate = 0;
+       fd->rating = STAR_RATING_NOT_READ;
+       fd->format_class = filter_file_get_class(path_utf8);
+       fd->page_num = 0;
+       fd->page_total = 0;
+
+       user = getpwuid(st->st_uid);
+       if (!user)
+               {
+               fd->owner = g_strdup_printf("%u", st->st_uid);
+               }
+       else
+               {
+               fd->owner = g_strdup(user->pw_name);
+               }
+
+       group = getgrgid(st->st_gid);
+       if (!group)
+               {
+               fd->group = g_strdup_printf("%u", st->st_gid);
+               }
+       else
+               {
+               fd->group = g_strdup(group->gr_name);
+               }
+
+       fd->sym_link = get_symbolic_link(path_utf8);
 
        if (disable_sidecars) fd->disable_grouping = TRUE;
 
@@ -406,19 +476,25 @@ static FileData *file_data_new_local(const gchar *path, struct stat *st, gboolea
        return ret;
 }
 
-void init_exif_time_data(GList *files)
+FileData *file_data_new_simple(const gchar *path_utf8)
 {
-       FileData *file;
-       DEBUG_1("%s init_exif_time_data: ...", get_exec_time());
-       while (files)
-               {
-               file = files->data;
+       struct stat st;
+       FileData *fd;
 
-               if (file)
-                       file->exifdate = 0;
+       if (!stat_utf8(path_utf8, &st))
+               {
+               st.st_size = 0;
+               st.st_mtime = 0;
+               }
 
-               files = files->next;
+       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);
                }
+
+       return fd;
 }
 
 void read_exif_time_data(FileData *file)
@@ -429,7 +505,10 @@ void read_exif_time_data(FileData *file)
                return;
                }
 
-       file->exif = exif_read_fd(file);
+       if (!file->exif)
+               {
+               exif_read_fd(file);
+               }
 
        if (file->exif)
                {
@@ -456,6 +535,60 @@ void read_exif_time_data(FileData *file)
                }
 }
 
+void read_exif_time_digitized_data(FileData *file)
+{
+       if (file->exifdate_digitized > 0)
+               {
+               DEBUG_1("%s set_exif_time_digitized_data: Already exists for %s", get_exec_time(), file->path);
+               return;
+               }
+
+       if (!file->exif)
+               {
+               exif_read_fd(file);
+               }
+
+       if (file->exif)
+               {
+               gchar *tmp = exif_get_data_as_text(file->exif, "Exif.Photo.DateTimeDigitized");
+               DEBUG_2("%s set_exif_time_digitized_data: reading %p %s", get_exec_time(), file, file->path);
+
+               if (tmp)
+                       {
+                       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_digitized = mktime(&time_str);
+                       g_free(tmp);
+                       }
+               }
+}
+
+void read_rating_data(FileData *file)
+{
+       gchar *rating_str;
+
+       rating_str = metadata_read_string(file, RATING_KEY, METADATA_PLAIN);
+       if (rating_str)
+               {
+               file->rating = atoi(rating_str);
+               g_free(rating_str);
+               }
+       else
+               {
+               file->rating = 0;
+               }
+}
+
 void set_exif_time_data(GList *files)
 {
        DEBUG_1("%s set_exif_time_data: ...", get_exec_time());
@@ -469,6 +602,37 @@ void set_exif_time_data(GList *files)
                }
 }
 
+void set_exif_time_digitized_data(GList *files)
+{
+       DEBUG_1("%s set_exif_time_digitized_data: ...", get_exec_time());
+
+       while (files)
+               {
+               FileData *file = files->data;
+
+               read_exif_time_digitized_data(file);
+               files = files->next;
+               }
+}
+
+void set_rating_data(GList *files)
+{
+       gchar *rating_str;
+       DEBUG_1("%s set_rating_data: ...", get_exec_time());
+
+       while (files)
+               {
+               FileData *file = files->data;
+               rating_str = metadata_read_string(file, RATING_KEY, METADATA_PLAIN);
+               if (rating_str )
+                       {
+                       file->rating = atoi(rating_str);
+                       g_free(rating_str);
+                       }
+               files = files->next;
+               }
+}
+
 FileData *file_data_new_no_grouping(const gchar *path_utf8)
 {
        struct stat st;
@@ -513,9 +677,9 @@ FileData *file_data_ref(FileData *fd)
        if (fd == NULL) return NULL;
        if (fd->magick != FD_MAGICK)
 #ifdef DEBUG_FILEDATA
-               DEBUG_0("fd magick mismatch @ %s:%d  fd=%p", file, line, fd);
+               log_printf("Error: fd magick mismatch @ %s:%d  fd=%p", file, line, fd);
 #else
-               DEBUG_0("fd magick mismatch fd=%p", fd);
+               log_printf("Error: fd magick mismatch fd=%p", fd);
 #endif
        g_assert(fd->magick == FD_MAGICK);
        fd->ref++;
@@ -532,6 +696,12 @@ static void file_data_free(FileData *fd)
 {
        g_assert(fd->magick == FD_MAGICK);
        g_assert(fd->ref == 0);
+       g_assert(!fd->locked);
+
+#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);
@@ -540,15 +710,69 @@ static void file_data_free(FileData *fd)
        g_free(fd->original_path);
        g_free(fd->collate_key_name);
        g_free(fd->collate_key_name_nocase);
+       g_free(fd->extended_extension);
        if (fd->thumb_pixbuf) g_object_unref(fd->thumb_pixbuf);
        histmap_free(fd->histmap);
-
+       g_free(fd->owner);
+       g_free(fd->group);
+       g_free(fd->sym_link);
        g_assert(fd->sidecar_files == NULL); /* sidecar files must be freed before calling this */
 
        file_data_change_info_free(NULL, fd);
        g_free(fd);
 }
 
+/**
+ * @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)
+{
+       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
 void file_data_unref_debug(const gchar *file, gint line, FileData *fd)
 #else
@@ -558,53 +782,100 @@ void file_data_unref(FileData *fd)
        if (fd == NULL) return;
        if (fd->magick != FD_MAGICK)
 #ifdef DEBUG_FILEDATA
-               DEBUG_0("fd magick mismatch @ %s:%d  fd=%p", file, line, fd);
+               log_printf("Error: fd magick mismatch @ %s:%d  fd=%p", file, line, fd);
 #else
-               DEBUG_0("fd magick mismatch fd=%p", fd);
+               log_printf("Error: fd magick mismatch fd=%p", fd);
 #endif
        g_assert(fd->magick == FD_MAGICK);
 
        fd->ref--;
 #ifdef DEBUG_FILEDATA
-       DEBUG_2("file_data_unref fd=%p (%d): '%s' @ %s:%d", fd, 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 fd=%p (%d): '%s'", fd, 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)
-               {
-               GList *work;
-               FileData *parent = fd->parent ? fd->parent : fd;
 
-               if (parent->ref > 0) return;
+       // Free FileData if it's no longer ref'd
+       file_data_consider_free(fd);
+}
 
-               work = parent->sidecar_files;
-               while (work)
-                       {
-                       FileData *sfd = work->data;
-                       if (sfd->ref > 0) return;
-                       work = work->next;
-                       }
+/**
+ * @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) log_printf("Error: fd magick mismatch fd=%p", fd);
 
-               /* none of parent/children is referenced, we can free everything */
+       g_assert(fd->magick == FD_MAGICK);
+       fd->locked = TRUE;
 
-               DEBUG_2("file_data_unref: deleting '%s', parent '%s'", fd->path, fd->parent ? parent->path : "-");
+       DEBUG_2("file_data_ref fd=%p (%d): '%s'", fd, fd->ref, fd->path);
+}
 
-               work = parent->sidecar_files;
-               while (work)
-                       {
-                       FileData *sfd = work->data;
-                       file_data_free(sfd);
-                       work = work->next;
-                       }
+/**
+ * @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) log_printf("Error: fd magick mismatch fd=%p", fd);
 
-               g_list_free(parent->sidecar_files);
-               parent->sidecar_files = NULL;
+       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;
 
-               file_data_free(parent);
+       work = list;
+       while (work)
+               {
+               FileData *fd = work->data;
+               work = work->next;
+               file_data_lock(fd);
                }
 }
 
+/**
+ * @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);
+               }
+}
 
 /*
  *-----------------------------------------------------------------------------
@@ -769,6 +1040,8 @@ static void file_data_disconnect_sidecar_file(FileData *target, FileData *sfd)
 
        target->sidecar_files = g_list_remove(target->sidecar_files, sfd);
        sfd->parent = NULL;
+       g_free(sfd->extended_extension);
+       sfd->extended_extension = NULL;
 
        file_data_unref(target);
        file_data_unref(sfd);
@@ -864,11 +1137,31 @@ gint filelist_sort_compare_filedata(FileData *fa, FileData *fb)
                        if (fa->date > fb->date) return 1;
                        /* fall back to name */
                        break;
+               case SORT_CTIME:
+                       if (fa->cdate < fb->cdate) return -1;
+                       if (fa->cdate > fb->cdate) 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;
+               case SORT_EXIFTIMEDIGITIZED:
+                       if (fa->exifdate_digitized < fb->exifdate_digitized) return -1;
+                       if (fa->exifdate_digitized > fb->exifdate_digitized) return 1;
+                       /* fall back to name */
+                       break;
+               case SORT_RATING:
+                       if (fa->rating < fb->rating) return -1;
+                       if (fa->rating > fb->rating) return 1;
+                       /* fall back to name */
+                       break;
+               case SORT_CLASS:
+                       if (fa->format_class < fb->format_class) return -1;
+                       if (fa->format_class > fb->format_class) return 1;
+                       /* fall back to name */
+                       break;
 #ifdef HAVE_STRVERSCMP
                case SORT_NUMBER:
                        ret = strverscmp(fa->name, fb->name);
@@ -920,10 +1213,6 @@ 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);
 }
 
@@ -951,6 +1240,37 @@ static GList * file_data_basename_hash_insert(GHashTable *basename_hash, FileDat
 
        list = g_hash_table_lookup(basename_hash, basename);
 
+       if (!list)
+               {
+               DEBUG_1("TG: basename_hash not found for %s",fd->path);
+               const gchar *parent_extension = registered_extension_from_path(basename);
+
+               if (parent_extension)
+                       {
+                       DEBUG_1("TG: parent extension %s",parent_extension);
+                       gchar *parent_basename = g_strndup(basename, parent_extension - basename);
+                       DEBUG_1("TG: parent basename %s",parent_basename);
+                       FileData *parent_fd = g_hash_table_lookup(file_data_pool, basename);
+                       if (parent_fd)
+                               {
+                               DEBUG_1("TG: parent fd found");
+                               list = g_hash_table_lookup(basename_hash, parent_basename);
+                               if (!g_list_find(list, parent_fd))
+                                       {
+                                       DEBUG_1("TG: parent fd doesn't fit");
+                                       g_free(parent_basename);
+                                       list = NULL;
+                                       }
+                               else
+                                       {
+                                       g_free(basename);
+                                       basename = parent_basename;
+                                       fd->extended_extension = g_strconcat(parent_extension, fd->extension, NULL);
+                                       }
+                               }
+                       }
+               }
+
        if (!g_list_find(list, fd))
                {
                list = g_list_insert_sorted(list, file_data_ref(fd), file_data_sort_by_ext);
@@ -963,6 +1283,11 @@ static GList * file_data_basename_hash_insert(GHashTable *basename_hash, FileDat
        return list;
 }
 
+static void file_data_basename_hash_insert_cb(gpointer fd, gpointer basename_hash)
+{
+       file_data_basename_hash_insert((GHashTable *)basename_hash, (FileData *)fd);
+}
+
 static void file_data_basename_hash_remove_list(gpointer key, gpointer value, gpointer data)
 {
        filelist_free((GList *)value);
@@ -1027,6 +1352,7 @@ static gboolean filelist_read_real(const gchar *dir_path, GList **files, GList *
        gchar *pathl;
        GList *dlist = NULL;
        GList *flist = NULL;
+       GList *xmp_files = NULL;
        gint (*stat_func)(const gchar *path, struct stat *buf);
        GHashTable *basename_hash = NULL;
 
@@ -1084,7 +1410,10 @@ static gboolean filelist_read_real(const gchar *dir_path, GList **files, GList *
                                        flist = g_list_prepend(flist, fd);
                                        if (fd->sidecar_priority && !fd->disable_grouping)
                                                {
-                                               file_data_basename_hash_insert(basename_hash, fd);
+                                               if (strcmp(fd->extension, ".xmp") != 0)
+                                                       file_data_basename_hash_insert(basename_hash, fd);
+                                               else
+                                                       xmp_files = g_list_append(xmp_files, fd);
                                                }
                                        }
                                }
@@ -1103,6 +1432,12 @@ static gboolean filelist_read_real(const gchar *dir_path, GList **files, GList *
 
        g_free(pathl);
 
+       if (xmp_files)
+               {
+               g_list_foreach(xmp_files,file_data_basename_hash_insert_cb,basename_hash);
+               g_list_free(xmp_files);
+               }
+
        if (dirs) *dirs = dlist;
 
        if (files)
@@ -1113,9 +1448,6 @@ static gboolean filelist_read_real(const gchar *dir_path, GList **files, GList *
                }
        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;
 }
 
@@ -1150,8 +1482,11 @@ FileData *file_data_new_group(const gchar *path_utf8)
        filelist_read_real(dir, &files, NULL, TRUE);
 
        fd = g_hash_table_lookup(file_data_pool, path_utf8);
-       g_assert(fd);
-       file_data_ref(fd);
+       if (!fd) fd = file_data_new(path_utf8, &st, TRUE);
+       if (fd)
+               {
+               file_data_ref(fd);
+               }
 
        filelist_free(files);
        g_free(dir);
@@ -1304,6 +1639,33 @@ static void filelist_recursive_append(GList **list, GList *dirs)
                }
 }
 
+static void filelist_recursive_append_full(GList **list, GList *dirs, SortType method, gboolean ascend)
+{
+       GList *work;
+
+       work = dirs;
+       while (work)
+               {
+               FileData *fd = (FileData *)(work->data);
+               GList *f;
+               GList *d;
+
+               if (filelist_read(fd, &f, &d))
+                       {
+                       f = filelist_filter(f, FALSE);
+                       f = filelist_sort_full(f, method, ascend, (GCompareFunc) filelist_sort_file_cb);
+                       *list = g_list_concat(*list, f);
+
+                       d = filelist_filter(d, TRUE);
+                       d = filelist_sort_path(d);
+                       filelist_recursive_append_full(list, d, method, ascend);
+                       filelist_free(d);
+                       }
+
+               work = work->next;
+               }
+}
+
 GList *filelist_recursive(FileData *dir_fd)
 {
        GList *list;
@@ -1321,6 +1683,23 @@ GList *filelist_recursive(FileData *dir_fd)
        return list;
 }
 
+GList *filelist_recursive_full(FileData *dir_fd, SortType method, gboolean ascend)
+{
+       GList *list;
+       GList *d;
+
+       if (!filelist_read(dir_fd, &list, &d)) return NULL;
+       list = filelist_filter(list, FALSE);
+       list = filelist_sort_full(list, method, ascend, (GCompareFunc) filelist_sort_file_cb);
+
+       d = filelist_filter(d, TRUE);
+       d = filelist_sort_path(d);
+       filelist_recursive_append_full(&list, d, method, ascend);
+       filelist_free(d);
+
+       return list;
+}
+
 /*
  *-----------------------------------------------------------------------------
  * file modification support
@@ -1360,22 +1739,29 @@ gchar *file_data_get_sidecar_path(FileData *fd, gboolean existing_only)
        if (!file_data_can_write_sidecar(fd)) return NULL;
 
        work = fd->parent ? fd->parent->sidecar_files : fd->sidecar_files;
+       gchar *extended_extension = g_strconcat(fd->parent ? fd->parent->extension : fd->extension, ".xmp", NULL);
        while (work)
                {
                FileData *sfd = work->data;
                work = work->next;
-               if (g_ascii_strcasecmp(sfd->extension, ".xmp") == 0)
+               if (g_ascii_strcasecmp(sfd->extension, ".xmp") == 0 || g_ascii_strcasecmp(sfd->extension, extended_extension) == 0)
                        {
                        sidecar_path = g_strdup(sfd->path);
                        break;
                        }
                }
+       g_free(extended_extension);
 
        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);
+               if (options->metadata.sidecar_extended_name)
+                       sidecar_path = g_strconcat(fd->path, ".xmp", NULL);
+               else
+                       {
+                       gchar *base = g_strndup(fd->path, fd->extension - fd->path);
+                       sidecar_path = g_strconcat(base, ".xmp", NULL);
+                       g_free(base);
+                       }
                }
 
        return sidecar_path;
@@ -1481,6 +1867,73 @@ GList *file_data_filter_marks_list(GList *list, guint filter)
        return list;
 }
 
+gboolean file_data_filter_file_filter(FileData *fd, GRegex *filter)
+{
+       return g_regex_match(filter, fd->name, 0, NULL);
+}
+
+GList *file_data_filter_file_filter_list(GList *list, GRegex *filter)
+{
+       GList *work;
+
+       work = list;
+       while (work)
+               {
+               FileData *fd = work->data;
+               GList *link = work;
+               work = work->next;
+
+               if (!file_data_filter_file_filter(fd, filter))
+                       {
+                       list = g_list_remove_link(list, link);
+                       file_data_unref(fd);
+                       g_list_free(link);
+                       }
+               }
+
+       return list;
+}
+
+static gboolean file_data_filter_class(FileData *fd, guint filter)
+{
+       gint i;
+
+       for (i = 0; i < FILE_FORMAT_CLASSES; i++)
+               {
+               if (filter & (1 << i))
+                       {
+                       if ((FileFormatClass)i == filter_file_get_class(fd->path))
+                               {
+                               return TRUE;
+                               }
+                       }
+               }
+
+       return FALSE;
+}
+
+GList *file_data_filter_class_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_class(fd, filter))
+                       {
+                       list = g_list_remove_link(list, link);
+                       file_data_unref(fd);
+                       g_list_free(link);
+                       }
+               }
+
+       return list;
+}
+
 static void file_data_notify_mark_func(gpointer key, gpointer value, gpointer user_data)
 {
        FileData *fd = value;
@@ -1499,7 +1952,7 @@ gboolean file_data_register_mark_func(gint n, FileDataGetMarkFunc get_mark_func,
         file_data_mark_func_data[n] = data;
         file_data_destroy_mark_func[n] = notify;
 
-        if (get_mark_func)
+       if (get_mark_func && file_data_pool)
                {
                /* this effectively changes all known files */
                g_hash_table_foreach(file_data_pool, file_data_notify_mark_func, NULL);
@@ -1574,7 +2027,7 @@ gchar *file_data_sc_list_to_string(FileData *fd)
    COPY
    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)
+            extension should remain (FIXME should we allow editing extension? it will make problems with grouping)
            sidecar names are changed too, extensions are not changed
    DELETE
    UPDATE - file size, date or grouping has been changed
@@ -1924,11 +2377,11 @@ static void file_data_update_ci_dest(FileData *fd, const gchar *dest_path)
 
 static void file_data_update_ci_dest_preserve_ext(FileData *fd, const gchar *dest_path)
 {
-       const gchar *extension = extension_from_path(fd->change->source);
+       const gchar *extension = registered_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);
+       fd->change->dest = g_strconcat(base, fd->extended_extension ? fd->extended_extension : extension, NULL);
        file_data_update_planned_change_hash(fd, old_path, fd->change->dest);
 
        g_free(old_path);
@@ -2041,10 +2494,12 @@ gboolean file_data_sc_update_ci_unspecified_list(GList *fd_list, const gchar *de
  * it should detect all possible problems with the planned operation
  */
 
-gint file_data_verify_ci(FileData *fd)
+gint file_data_verify_ci(FileData *fd, GList *list)
 {
        gint ret = CHANGE_OK;
        gchar *dir;
+       GList *work = NULL;
+       FileData *fd1 = NULL;
 
        if (!fd->change)
                {
@@ -2199,18 +2654,20 @@ gint file_data_verify_ci(FileData *fd)
 
                if (!same)
                        {
-                       const gchar *dest_ext = extension_from_path(fd->change->dest);
+                       const gchar *dest_ext = registered_extension_from_path(fd->change->dest);
                        if (!dest_ext) dest_ext = "";
-
-                       if (g_ascii_strcasecmp(fd->extension, dest_ext) != 0)
+                       if (!options->file_filter.disable_file_extension_checks)
                                {
-                               ret |= CHANGE_WARN_CHANGED_EXT;
-                               DEBUG_1("Change checked: source and destination have different extensions: %s -> %s", fd->path, fd->change->dest);
+                               if (g_ascii_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 */
+                       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);
@@ -2254,6 +2711,28 @@ gint file_data_verify_ci(FileData *fd)
                g_free(dest_dir);
                }
 
+       /* During a rename operation, check if another planned destination file has
+        * the same filename
+        */
+       if(fd->change->type == FILEDATA_CHANGE_RENAME ||
+                               fd->change->type == FILEDATA_CHANGE_COPY ||
+                               fd->change->type == FILEDATA_CHANGE_MOVE)
+               {
+               work = list;
+               while (work)
+                       {
+                       fd1 = work->data;
+                       work = work->next;
+                       if (fd1 != NULL && fd != fd1 )
+                               {
+                               if (!strcmp(fd->change->dest, fd1->change->dest))
+                                       {
+                                       ret |= CHANGE_DUPLICATE_DEST;
+                                       }
+                               }
+                       }
+               }
+
        fd->change->error = ret;
        if (ret == 0) DEBUG_1("Change checked: OK: %s", fd->path);
 
@@ -2262,19 +2741,19 @@ gint file_data_verify_ci(FileData *fd)
 }
 
 
-gint file_data_sc_verify_ci(FileData *fd)
+gint file_data_sc_verify_ci(FileData *fd, GList *list)
 {
        GList *work;
        gint ret;
 
-       ret = file_data_verify_ci(fd);
+       ret = file_data_verify_ci(fd, list);
 
        work = fd->sidecar_files;
        while (work)
                {
                FileData *sfd = work->data;
 
-               ret |= file_data_verify_ci(sfd);
+               ret |= file_data_verify_ci(sfd, list);
                work = work->next;
                }
 
@@ -2357,6 +2836,12 @@ gchar *file_data_get_error_string(gint error)
                g_string_append(result, _("there are unsaved metadata changes for the file"));
                }
 
+       if (error & CHANGE_DUPLICATE_DEST)
+               {
+               if (result->len > 0) g_string_append(result, ", ");
+               g_string_append(result, _("another destination file has the same filename"));
+               }
+
        return g_string_free(result, FALSE);
 }
 
@@ -2383,7 +2868,7 @@ 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);
+               error = with_sidecars ? file_data_sc_verify_ci(fd, list) : file_data_verify_ci(fd, list);
                all_errors |= error;
                common_errors &= error;
 
@@ -2518,7 +3003,7 @@ gboolean file_data_apply_ci(FileData *fd)
 {
        FileDataChangeType type = fd->change->type;
 
-       /* FIXME delete ?*/
+       /** @FIXME delete ?*/
        if (type == FILEDATA_CHANGE_MOVE || type == FILEDATA_CHANGE_RENAME)
                {
                DEBUG_1("planned change: applying %s -> %s", fd->change->dest, fd->path);
@@ -2529,7 +3014,8 @@ gboolean file_data_apply_ci(FileData *fd)
                        /* 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
+                       */
+                       /**  @FIXME maybe we could copy stuff like marks
                        */
                        DEBUG_1("can't rename fd, target exists %s -> %s", fd->change->dest, fd->path);
                        }
@@ -2605,7 +3091,7 @@ GList *file_data_process_groups_in_selection(GList *list, gboolean ungroup, GLis
                }
 
        /* remove sidecars from the list,
-          they can be still acessed via main_fd->sidecar_files */
+          they can be still accessed via main_fd->sidecar_files */
        work = list;
        while (work)
                {
@@ -2633,8 +3119,8 @@ GList *file_data_process_groups_in_selection(GList *list, gboolean ungroup, GLis
  * 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
-   FIXME do we need the ignore_list? It looks like a workaround for ineffective
+/* 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 */
 
 
@@ -2736,10 +3222,21 @@ gboolean file_data_send_notification_idle_cb(gpointer data)
 
 void file_data_send_notification(FileData *fd, NotifyType type)
 {
+       GList *work = notify_func_list;
+
+       while (work)
+               {
+               NotifyData *nd = (NotifyData *)work->data;
+
+               nd->func(fd, type, nd->data);
+               work = work->next;
+               }
+    /*
        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;
@@ -2815,4 +3312,178 @@ gboolean file_data_unregister_real_time_monitor(FileData *fd)
 
        return TRUE;
 }
+
+/*
+ *-----------------------------------------------------------------------------
+ * Saving marks list, clearing marks
+ * Uses file_data_pool
+ *-----------------------------------------------------------------------------
+ */
+
+static void marks_get_files(gpointer key, gpointer value, gpointer userdata)
+{
+       gchar *file_name = key;
+       GString *result = userdata;
+       FileData *fd;
+
+       if (isfile(file_name))
+               {
+               fd = value;
+               if (fd && fd->marks > 0)
+                       {
+                       g_string_append_printf(result, "%s,%i\n", fd->path, fd->marks);
+                       }
+               }
+}
+
+gboolean marks_list_load(const gchar *path)
+{
+       FILE *f;
+       gchar s_buf[1024];
+       gchar *pathl;
+       gchar *file_path;
+       gchar *marks_value;
+
+       pathl = path_from_utf8(path);
+       f = fopen(pathl, "r");
+       g_free(pathl);
+       if (!f) return FALSE;
+
+       /* first line must start with Marks comment */
+       if (!fgets(s_buf, sizeof(s_buf), f) ||
+                                       strncmp(s_buf, "#Marks", 6) != 0)
+               {
+               fclose(f);
+               return FALSE;
+               }
+
+       while (fgets(s_buf, sizeof(s_buf), f))
+               {
+               if (s_buf[0]=='#') continue;
+                       file_path = strtok(s_buf, ",");
+                       marks_value = strtok(NULL, ",");
+                       if (isfile(file_path))
+                               {
+                               FileData *fd = file_data_new_no_grouping(file_path);
+                               file_data_ref(fd);
+                               gint n = 0;
+                               while (n <= 9)
+                                       {
+                                       gint mark_no = 1 << n;
+                                       if (atoi(marks_value) & mark_no)
+                                               {
+                                               file_data_set_mark(fd, n , 1);
+                                               }
+                                       n++;
+                                       }
+                               }
+               }
+
+       fclose(f);
+       return TRUE;
+}
+
+gboolean marks_list_save(gchar *path, gboolean save)
+{
+       SecureSaveInfo *ssi;
+       gchar *pathl;
+       GString  *marks = g_string_new("");
+
+       pathl = path_from_utf8(path);
+       ssi = secure_open(pathl);
+       g_free(pathl);
+       if (!ssi)
+               {
+               log_printf(_("Error: Unable to write marks lists to: %s\n"), path);
+               return FALSE;
+               }
+
+       secure_fprintf(ssi, "#Marks lists\n");
+
+       if (save)
+               {
+               g_hash_table_foreach(file_data_pool, marks_get_files, marks);
+               }
+       secure_fprintf(ssi, "%s", marks->str);
+       g_string_free(marks, FALSE);
+
+       secure_fprintf(ssi, "#end\n");
+       return (secure_close(ssi) == 0);
+}
+
+static void marks_clear(gpointer key, gpointer value, gpointer userdata)
+{
+       gchar *file_name = key;
+       gint mark_no;
+       gint n;
+       FileData *fd;
+
+       if (isfile(file_name))
+               {
+               fd = value;
+               if (fd && fd->marks > 0)
+                       {
+                       n = 0;
+                       while (n <= 9)
+                               {
+                               mark_no = 1 << n;
+                               if (fd->marks & mark_no)
+                                       {
+                                       file_data_set_mark(fd, n , 0);
+                                       }
+                               n++;
+                               }
+                       }
+               }
+}
+
+void marks_clear_all()
+{
+       g_hash_table_foreach(file_data_pool, marks_clear, NULL);
+}
+
+void file_data_set_page_num(FileData *fd, gint page_num)
+{
+       if (fd->page_total > 1 && page_num < 0)
+               {
+               fd->page_num = fd->page_total - 1;
+               }
+       else if (fd->page_total > 1 && page_num <= fd->page_total)
+               {
+               fd->page_num = page_num - 1;
+               }
+       else
+               {
+               fd->page_num = 0;
+               }
+       file_data_send_notification(fd, NOTIFY_REREAD);
+}
+
+void file_data_inc_page_num(FileData *fd)
+{
+       if (fd->page_total > 0 && fd->page_num < fd->page_total - 1)
+               {
+               fd->page_num = fd->page_num + 1;
+               }
+       else if (fd->page_total == 0)
+               {
+               fd->page_num = fd->page_num + 1;
+               }
+       file_data_send_notification(fd, NOTIFY_REREAD);
+}
+
+void file_data_dec_page_num(FileData *fd)
+{
+       if (fd->page_num > 0)
+               {
+               fd->page_num = fd->page_num - 1;
+               }
+       file_data_send_notification(fd, NOTIFY_REREAD);
+}
+
+void file_data_set_page_total(FileData *fd, gint page_total)
+{
+       fd->page_total = page_total;
+}
+
 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */