Ctrl K
ComunidadGuías…

src/libs/import.c

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/*
    This file is part of darktable,
    Copyright (C) 2011-2026 darktable developers.

    darktable 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 3 of the License, or
    (at your option) any later version.

    darktable 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 darktable.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "common/collection.h"
#include "common/darktable.h"
#include "common/file_location.h"
#include "common/exif.h"
#include "common/metadata.h"
#include "common/datetime.h"
#include "control/conf.h"
#include "control/control.h"

#ifdef HAVE_GPHOTO2
#include "control/jobs/camera_jobs.h"
#endif

#include "dtgtk/button.h"
#include "gui/accelerators.h"
#include "gui/gtk.h"
#include "gui/draw.h"
#include "gui/import_metadata.h"
#include "gui/preferences.h"
#include "imageio/imageio_common.h"  // for dt_imageio_is_raw_by_extension()
#include "libs/lib.h"
#include "libs/lib_api.h"

#ifdef GDK_WINDOWING_QUARTZ
#include "osx/osx.h"
#endif

#include <strings.h>
#include <librsvg/rsvg.h>

// ugh, ugly hack. why do people break stuff all the time?
#ifndef RSVG_CAIRO_H
#include <librsvg/rsvg-cairo.h>
#endif

#ifdef USE_LUA
#include "lua/widget/widget.h"
#endif

DT_MODULE(1)

static void _import_from_dialog_new(dt_lib_module_t* self);
static void _import_from_dialog_run(dt_lib_module_t* self);
static void _import_from_dialog_free(dt_lib_module_t* self);
static void _do_select_all(dt_lib_module_t* self);
static void _do_select_none(dt_lib_module_t* self);
static uint32_t _do_select_new(dt_lib_module_t* self);
static void _update_places_list(dt_lib_module_t* self);
static gboolean _update_files_list(dt_lib_module_t* self);
static void _update_folders_list(dt_lib_module_t* self);
static void _update_images_number(dt_lib_module_t* self,
                                  const guint nb_sel);
static void _lib_import_select_folder(GtkWidget *widget,
                                      dt_lib_module_t *self);
static void _remove_place(gchar *folder,
                          GtkTreeIter iter,
                          dt_lib_module_t* self);
static GList* _get_custom_places();

typedef enum dt_import_cols_t
{
  DT_IMPORT_SEL_THUMB = 0,      // active / inactive thumbnails
  DT_IMPORT_THUMB,              // thumbnail
  DT_IMPORT_UI_FILENAME,        // displayed filename
  DT_IMPORT_FILENAME,           // filename
  DT_IMPORT_UI_DATETIME,        // displayed datetime
  DT_IMPORT_UI_EXISTS,          // whether the image is already imported
  DT_IMPORT_DATETIME,           // file datetime
  DT_IMPORT_NUM_COLS
} dt_import_cols_t;

typedef enum dt_folder_cols_t
{
  DT_FOLDER_PATH = 0,
  DT_FOLDER_NAME,
  DT_FOLDER_EXPANDED,
  DT_FOLDER_NUM_COLS
} dt_folder_cols_t;

typedef enum dt_places_cols_t
{
  DT_PLACES_NAME = 0,
  DT_PLACES_PATH,
  DT_PLACES_TYPE,
  DT_PLACES_NUM_COLS
} dt_places_cols_t;

typedef enum dt_places_type_t
{
  DT_TYPE_HOME = 1,
  DT_TYPE_PIC,
  DT_TYPE_MOUNT,
  DT_TYPE_CUSTOM,
} dt_places_type_t;

typedef enum dt_import_case_t
{
  DT_IMPORT_INPLACE = 0,
  DT_IMPORT_COPY,
  DT_IMPORT_CAMERA,
  DT_IMPORT_TETHER
} dt_import_case_t;

static const char *const _import_text[] =
{
  [DT_IMPORT_INPLACE] = N_("add to library"),
  [DT_IMPORT_COPY   ] = N_("copy & import"),
  [DT_IMPORT_CAMERA ] = N_("copy & import from camera"),
  [DT_IMPORT_TETHER ] = N_("tethered shoot"),
};

typedef struct dt_lib_import_t
{
#ifdef HAVE_GPHOTO2
  dt_camera_t *camera;
#endif
  GtkButton *import_inplace;
  GCancellable *cancel_iter;

  GtkWidget *ignore_exif, *rating, *apply_metadata, *recursive;
  GtkWidget *import_new;
  dt_import_metadata_t metadata;
  GtkBox *devices;
  dt_import_case_t import_case;
  struct
  {
    GtkWidget *dialog;
    GtkListStore *store;
    GtkWidget *w;
    GtkTreeView *treeview;
    GtkWidget *thumbs;
    GtkTreeView *folderview;
    GtkTreeViewColumn *foldercol;
    GtkTreeIter iter;
    guint event;
    guint nb;
    GdkPixbuf *eye;
    GtkTreeViewColumn *pixcol;
    GtkWidget *img_nb;
    GtkGrid *patterns;
    GtkWidget *datetime;
    dt_gui_collapsible_section_t cs;
    guint fn_line;
    GtkWidget *info;
  } from;
  GtkListStore *placesModel;
  GtkWidget *placesView;
  GtkTreeSelection *placesSelection;
  GtkWidget *remove_place_button;
  GtkWidget *select_all;
  GtkWidget *select_new;
  GtkWidget *select_none;
  GtkWidget *do_select_none;

  dt_gui_collapsible_section_t cs;

  gboolean is_importing;
  GList *to_be_visited;

#ifdef USE_LUA
  GtkWidget *extra_lua_widgets;
#endif
} dt_lib_import_t;

const char *name(dt_lib_module_t *self)
{
  return _("import");
}

gboolean has_preset_label(dt_lib_module_t *self)
{
  return TRUE;
}

dt_view_type_flags_t views(dt_lib_module_t *self)
{
  return DT_VIEW_LIGHTTABLE;
}

uint32_t container(dt_lib_module_t *self)
{
  return DT_UI_CONTAINER_PANEL_LEFT_CENTER;
}

int position(const dt_lib_module_t *self)
{
  return 999;
}

#ifdef HAVE_GPHOTO2

/* show import from camera dialog */
static void _lib_import_from_camera_callback(GtkButton *button,
                                             dt_lib_module_t *self)
{
  dt_camctl_t *camctl = (dt_camctl_t *)darktable.camctl;
  camctl->import_ui = TRUE;

  dt_lib_import_t *d = self->data;
  d->import_case = DT_IMPORT_CAMERA;
  _import_from_dialog_new(self);
  _import_from_dialog_run(self);
  _import_from_dialog_free(self);
  camctl->import_ui = FALSE;
}

/* enter tethering mode for camera */
static void _lib_import_tethered_callback(GtkToggleButton *button,
                                          gpointer data)
{
  /* select camera to work with before switching mode */
  dt_camctl_select_camera(darktable.camctl, (dt_camera_t *)data);
  dt_ctl_switch_mode_to("tethering");
}

static void _lib_import_mount_callback(GtkToggleButton *button,
                                       gpointer data)
{
  dt_camera_unused_t *camera = (dt_camera_unused_t *)data;
  camera->trymount = TRUE;
  dt_camctl_t *camctl = (dt_camctl_t *)darktable.camctl;
  camctl->tickmask = 3;
}

static void _lib_import_unmount_callback(GtkToggleButton *button,
                                         gpointer data)
{
  dt_camera_t *camera = (dt_camera_t *)data;
  camera->unmount = TRUE;
  dt_camctl_t *camctl = (dt_camctl_t *)darktable.camctl;
  camctl->tickmask = 3;
}

/** update the device list */
void gui_update(dt_lib_module_t *self)
{
  // TODO the (shortcuts to) the buttons directly pass a pointer to camera
  // which might already be deleted in a race with the polling thread.
  // Also shortcuts always refer to the last camera in the list.
  // This should really be changed to have a fixed set of (un)hidden buttons
  // that work on the "selected" camera (in a, resizable, treeview).
  dt_camctl_t *camctl = (dt_camctl_t *)darktable.camctl;
  dt_pthread_mutex_lock(&camctl->lock);

  if(camctl->changed_camera)
  {
    dt_lib_import_t *d = self->data;

    /* cleanup of widgets in devices container*/
    dt_gui_container_remove_children(GTK_CONTAINER(d->devices));
    GtkWidget *button = NULL;

    // Add detected supported devices
    char buffer[512] = { 0 };
    for(GList *citem = camctl->cameras; citem; citem = g_list_next(citem))
    {
      dt_camera_t *camera = citem->data;

      /* add camera label */
      GtkWidget *label = dt_ui_section_label_new(_(camera->model));
      gtk_box_pack_start(GTK_BOX(d->devices), label, TRUE, TRUE, 0);

      /* set camera summary if available */
      if(*camera->summary.text)
      {
        gtk_widget_set_tooltip_text(label, camera->summary.text);
      }
      else
      {
        snprintf(buffer, sizeof(buffer),
                 _("device \"%s\" connected on port \"%s\"."),
                 camera->model,
                 camera->port);
        gtk_widget_set_tooltip_text(label, buffer);
      }

      /* add camera actions buttons */
      GtkWidget *vbx = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
      if(camera->can_import)
      {
        button = dt_action_button_new(self, _import_text[DT_IMPORT_CAMERA],
                                      _lib_import_from_camera_callback, self, NULL, 0, 0);
        gtk_box_pack_start(GTK_BOX(vbx), button, FALSE, FALSE, 0);
        d->camera = camera;
        gtk_widget_set_halign(gtk_bin_get_child(GTK_BIN(button)), GTK_ALIGN_CENTER);
        dt_gui_add_help_link(button, "import_camera");
      }
      if(camera->can_tether)
      {
        button = dt_action_button_new(self, _import_text[DT_IMPORT_TETHER],
                                      _lib_import_tethered_callback, camera, NULL, 0, 0);
        gtk_box_pack_start(GTK_BOX(vbx), button, FALSE, FALSE, 0);
        gtk_widget_set_halign(gtk_bin_get_child(GTK_BIN(button)), GTK_ALIGN_CENTER);
        dt_gui_add_help_link(button, "import_camera");
      }

      button = dt_action_button_new(self, N_("unmount camera"),
                                    _lib_import_unmount_callback, camera, NULL, 0, 0);
      gtk_box_pack_start(GTK_BOX(vbx), button, FALSE, FALSE, 0);
      gtk_widget_set_halign(gtk_bin_get_child(GTK_BIN(button)), GTK_ALIGN_CENTER);
      dt_gui_add_help_link(button, "mount_camera");

      gtk_box_pack_start(GTK_BOX(d->devices), vbx, FALSE, FALSE, 0);
    }

    // Add list of locked cameras
    for(GList *citem = camctl->unused_cameras; citem; citem = g_list_next(citem))
    {
      dt_camera_unused_t *camera = citem->data;
      GtkWidget *label = dt_ui_section_label_new(_(camera->model));
      gtk_box_pack_start(GTK_BOX(d->devices), label, FALSE, FALSE, 0);

      if(camera->used)
        gtk_widget_set_tooltip_text
          (label,
           _("camera is locked by another application\n"
             "make sure it is no longer mounted\nor quit the locking application"));
      else if(camera->boring)
        gtk_widget_set_tooltip_text
          (label,
           _("tethering and importing is disabled for this camera"));

      button = dt_action_button_new(self, N_("mount camera"),
                                    _lib_import_mount_callback, camera, NULL, 0, 0);
      gtk_box_pack_start(GTK_BOX(d->devices), button, FALSE, FALSE, 0);
      gtk_widget_set_halign(gtk_bin_get_child(GTK_BIN(button)), GTK_ALIGN_CENTER);
      dt_gui_add_help_link(button, "mount_camera");
    }
    gtk_widget_show_all(GTK_WIDGET(d->devices));

    camctl->changed_camera = FALSE;
  }
  camctl->import_ui = FALSE;
  dt_pthread_mutex_unlock(&camctl->lock);
}

static void _free_camera_files(gpointer data)
{
  dt_camera_files_t *file = (dt_camera_files_t *)data;
  g_free(file->filename);
  g_free(file);
}

static guint _import_from_camera_set_file_list(dt_lib_module_t *self)
{
  dt_lib_import_t *d = self->data;

  GList *imgs = dt_camctl_get_images_list(darktable.camctl, d->camera);
  int nb = 0;
  const gboolean include_nonraws = !dt_conf_get_bool("ui_last/import_ignore_nonraws");
  for(GList *img = imgs; img; img = g_list_next(img))
  {
    dt_camera_files_t *file = img->data;
    const char *ext = g_strrstr(file->filename, ".");
    // Either we are not rejecting non-raw, or the file extension
    // belongs to the raw format
    if(include_nonraws ||
       (ext && ((dt_imageio_is_raw_by_extension(ext)) ||
                !g_ascii_strncasecmp(ext, ".dng", sizeof(".dng")))))
    {
      const time_t datetime = file->timestamp;
      GDateTime *dt_datetime = g_date_time_new_from_unix_local(datetime);
      gchar *dt_txt = g_date_time_format(dt_datetime, "%x %X");
      gchar *basename = g_path_get_basename(file->filename);
      char dtid[DT_DATETIME_EXIF_LENGTH];
      dt_datetime_unix_to_exif(dtid, sizeof(dtid), &datetime);
      const gboolean already_imported = dt_metadata_already_imported(basename, dtid);
      g_free(basename);
      gtk_list_store_insert_with_values(d->from.store, NULL, -1,
                         DT_IMPORT_UI_EXISTS, already_imported ? "✔" : " ",
                         DT_IMPORT_UI_FILENAME, file->filename,
                         DT_IMPORT_FILENAME, file->filename,
                         DT_IMPORT_UI_DATETIME, dt_txt,
                         DT_IMPORT_DATETIME, datetime,
                         DT_IMPORT_THUMB, d->from.eye, -1);
      nb++;
      g_free(dt_txt);
      g_date_time_unref(dt_datetime);
    }
  }
  g_list_free_full(imgs, _free_camera_files);
  return nb;
}

#endif // HAVE_GPHOTO2

#ifdef USE_LUA
static void reset_child(GtkWidget* child, gpointer user_data)
{
  dt_lua_async_call_alien(dt_lua_widget_trigger_callback,
      0,NULL,NULL,
      LUA_ASYNC_TYPENAME,"lua_widget",child, // the GtkWidget is an alias for the lua_widget
      LUA_ASYNC_TYPENAME,"const char*","reset",
      LUA_ASYNC_DONE);
}

// remove the extra portion from the filechooser before destroying it
static void detach_lua_widgets(GtkWidget *extra_lua_widgets)
{
  GtkWidget *parent = gtk_widget_get_parent(extra_lua_widgets);
  gtk_container_remove(GTK_CONTAINER(parent), extra_lua_widgets);
}
#endif

// maybe this should be (partly) in imageio/imageio.[c|h]?
static GdkPixbuf *_import_get_thumbnail(const gchar *filename)
{
  GdkPixbuf *pixbuf = NULL;
  gboolean have_preview = FALSE, no_preview_fallback = FALSE;

  if(!filename || !g_file_test(filename, G_FILE_TEST_IS_REGULAR))
  {
    no_preview_fallback = TRUE;
  }

  // Step 1: try to check whether the image contains embedded thumbnail
  // In case it has, we'll use that thumbnail to show on the dialog
  if(!no_preview_fallback)
  {
    uint8_t *buffer = NULL;
    size_t size = 0;
    char *mime_type = NULL;
    if(!dt_exif_get_thumbnail(filename, &buffer, &size, &mime_type))
    {
      // Scale the image to the correct size
      GdkPixbuf *tmp;
      GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
      if(!gdk_pixbuf_loader_write(loader, buffer, size, NULL)) goto cleanup;
      // Calling gdk_pixbuf_loader_close forces the data to be parsed by the
      // loader. We must do this before calling gdk_pixbuf_loader_get_pixbuf.
      if(!gdk_pixbuf_loader_close(loader, NULL)) goto cleanup;
      if(!(tmp = gdk_pixbuf_loader_get_pixbuf(loader))) goto cleanup;
      const float ratio = 1.0 * gdk_pixbuf_get_height(tmp) / gdk_pixbuf_get_width(tmp);
      const int width = 128;
      const int height = 128 * ratio;
      pixbuf = gdk_pixbuf_scale_simple(tmp, width, height, GDK_INTERP_BILINEAR);

      have_preview = TRUE;

    cleanup:
      gdk_pixbuf_loader_close(loader, NULL);
      free(mime_type);
      free(buffer);
      g_object_unref(loader); // This should clean up tmp as well
    }
  }

  // Step 2: if we were not able to get a thumbnail at step 1,
  // read the whole file to get a small size thumbnail
  if(!have_preview && !no_preview_fallback)
  {
    pixbuf = gdk_pixbuf_new_from_file_at_size(filename, 128, 128, NULL);
    if(pixbuf != NULL) have_preview = TRUE;
  }

  // If we got a thumbnail (either embedded or reading the file directly)
  // we need to find out the rotation as well
  if(have_preview && !no_preview_fallback)
  {
    // get image orientation
    dt_image_t img = { 0 };
    (void)dt_exif_read(&img, filename);

    // Rotate the image to the correct orientation
    GdkPixbuf *tmp = pixbuf;

    if(img.orientation == ORIENTATION_ROTATE_CCW_90_DEG)
    {
      tmp = gdk_pixbuf_rotate_simple(pixbuf, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE);
    }
    else if(img.orientation == ORIENTATION_ROTATE_CW_90_DEG)
    {
      tmp = gdk_pixbuf_rotate_simple(pixbuf, GDK_PIXBUF_ROTATE_CLOCKWISE);
    }
    else if(img.orientation == ORIENTATION_ROTATE_180_DEG)
    {
      tmp = gdk_pixbuf_rotate_simple(pixbuf, GDK_PIXBUF_ROTATE_UPSIDEDOWN);
    }

    if(pixbuf != tmp)
    {
      g_object_unref(pixbuf);
      pixbuf = tmp;
    }
  }

  // if no thumbanail found or read failed for whatever reason
  // just display the default darktable logo
  if(!have_preview || no_preview_fallback)
  {
    /* load the dt logo as a background */
    cairo_surface_t *surface = dt_util_get_logo(128.0);
    if(surface)
    {
      guint8 *image_buffer = cairo_image_surface_get_data(surface);
      const int image_width = cairo_image_surface_get_width(surface);
      const int image_height = cairo_image_surface_get_height(surface);

      pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, image_width, image_height);

      cairo_surface_destroy(surface);
      free(image_buffer);

      have_preview = TRUE;
    }
  }

return pixbuf;
}

static void _thumb_set_in_listview(GtkTreeModel *model,
                                   GtkTreeIter *iter,
                                   const gboolean thumb_sel,
                                   dt_lib_module_t *self)
{
  dt_lib_import_t *d = self->data;
  gchar *filename;
  gchar *fullname;
  gtk_tree_model_get(model, iter,
                     DT_IMPORT_UI_FILENAME, &filename,
                     DT_IMPORT_FILENAME, &fullname,
                     -1);
  GdkPixbuf *pixbuf = NULL;
#ifdef HAVE_GPHOTO2
  if(d->import_case == DT_IMPORT_CAMERA)
    pixbuf = thumb_sel ?
      dt_camctl_get_thumbnail(darktable.camctl, d->camera, filename)
      : d->from.eye;
  else
#endif
  {
    pixbuf = thumb_sel ? _import_get_thumbnail(fullname) : d->from.eye;
  }
  gtk_list_store_set(d->from.store, iter, DT_IMPORT_SEL_THUMB, thumb_sel,
                                          DT_IMPORT_THUMB, pixbuf, -1);

  if(pixbuf)
    g_object_ref(pixbuf);

  g_free(filename);
  g_free(fullname);
}

static void _files_button_press_cb(GtkGestureSingle *gesture, int n_press,
                                       double x, double y,
                                       dt_lib_module_t *self)
{
  GtkWidget *view = dt_gui_get_widget(gesture);
  dt_lib_import_t *d = self->data;
  /* gesture coordinates are relative to the widget allocation, while the
   * treeview coordinate APIs expect bin-window coordinates */
  gint bin_x, bin_y;
  gtk_tree_view_convert_widget_to_bin_window_coords(GTK_TREE_VIEW(view),
                                                    (gint)x, (gint)y, &bin_x, &bin_y);
  if(n_press == 1)
  {
    GtkTreePath *path = NULL;
    GtkTreeViewColumn *column = NULL;
    // Get tree path for row that was clicked
    if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view),
                                     bin_x, bin_y,
                                     &path, &column, NULL, NULL))
    {
      if(column == d->from.pixcol)
      {
        GtkTreeIter iter;
        GtkTreeModel *model = GTK_TREE_MODEL(d->from.store);
        gtk_tree_model_get_iter(model, &iter, path);
        gboolean thumb_sel;
        gtk_tree_model_get(model, &iter, DT_IMPORT_SEL_THUMB, &thumb_sel, -1);
        _thumb_set_in_listview(model, &iter, !thumb_sel, self);
        gtk_tree_path_free(path);
        return;
      }
    }
    gtk_tree_path_free(path);
  }
  else if(n_press >= 2)
  {
    GtkTreePath *path = NULL;
    // Get tree path for row that was clicked
    if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view),
                                     bin_x, bin_y,
                                     &path, NULL, NULL, NULL))
    {
      GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
      gtk_tree_selection_unselect_all(selection);
      gtk_tree_selection_select_path(selection, path);
      gtk_dialog_response(GTK_DIALOG(d->from.dialog), GTK_RESPONSE_ACCEPT);
      gtk_tree_path_free(path);
      return;
    }
  }
}

static gboolean _thumb_set(dt_lib_module_t *self)
{
  dt_lib_import_t *d = self->data;

  if(d->from.event)
  {
    GtkTreeModel *model = GTK_TREE_MODEL(d->from.store);
    gboolean thumb_sel;
    gtk_tree_model_get(model, &d->from.iter, DT_IMPORT_SEL_THUMB, &thumb_sel, -1);
    if(!thumb_sel)
      _thumb_set_in_listview(model, &d->from.iter, TRUE, self);
    if(d->from.event && gtk_tree_model_iter_next(model, &d->from.iter))
      return TRUE;
  }
  d->from.event = 0;
  return FALSE;
}

static void _all_thumb_toggled(GtkTreeViewColumn *column,
                               dt_lib_module_t *self)
{
  GtkWidget *toggle = gtk_tree_view_column_get_widget(column);
  const gboolean thumb_sel = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), thumb_sel);

  dt_lib_import_t *d = self->data;
  if(!thumb_sel)
  {
    // remove the thumbnails
    d->from.event = 0;
    GtkTreeModel *model = GTK_TREE_MODEL(d->from.store);
    GtkTreeIter iter;
    for(gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
        valid;
        valid = gtk_tree_model_iter_next(model, &iter))
    {
      _thumb_set_in_listview(model, &iter, FALSE, self);
    }
  }
  else if(!d->from.event)
  {
    // if the display is not yet started, start it
    GtkTreeModel *model = GTK_TREE_MODEL(d->from.store);
    if(gtk_tree_model_get_iter_first(model, &d->from.iter))
      d->from.event = g_timeout_add_full(G_PRIORITY_LOW, 100, (GSourceFunc)_thumb_set, self, NULL);
  }
}

static void _show_all_thumbs(dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  const gboolean thumb_sel =
    gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->from.thumbs));
  if(!d->from.event && thumb_sel)
  {
    // if the display is not yet started, start it
    GtkTreeModel *model = GTK_TREE_MODEL(d->from.store);
    if(gtk_tree_model_get_iter_first(model, &d->from.iter))
      d->from.event = g_timeout_add_full(G_PRIORITY_LOW, 100, (GSourceFunc)_thumb_set, self, NULL);
  }
}

#define FILE_REQUEST_BLOCK 50

static void _import_set_file_list(const gchar *folder,
                                  dt_lib_module_t *self);

static void _import_cancel(dt_lib_module_t *self)
{
  dt_lib_import_t *d = self->data;
  if(d->cancel_iter)
    g_cancellable_cancel(d->cancel_iter);

  d->from.nb = 0;
  _update_images_number(self, 0);
}

static void _import_active(dt_lib_module_t *self,
                           const gboolean active,
                           const uint32_t nb_sel)
{
  dt_lib_import_t *d = self->data;

  gtk_widget_set_sensitive(d->select_all, active);
  gtk_widget_set_sensitive(d->select_none, active);
  gtk_widget_set_sensitive(d->select_new, active);

  if(!active || nb_sel > 0)
    gtk_dialog_set_response_sensitive(GTK_DIALOG(d->from.dialog),
                                      GTK_RESPONSE_ACCEPT, active);
}

static void _import_add_file_callback(GObject *direnum,
                                      GAsyncResult *result,
                                      gpointer user_data)
{
  dt_lib_module_t* self = (dt_lib_module_t *)user_data;
  dt_lib_import_t *d = self->data;

  GError *error = NULL;
  GFileEnumerator *dir_files = G_FILE_ENUMERATOR(direnum);

  GList *file_list = g_file_enumerator_next_files_finish(dir_files, result, &error);
  GFile* gfolder = g_file_enumerator_get_container(dir_files);

  if(error)
  {
    g_file_enumerator_close(dir_files, NULL, NULL);
    g_object_unref(gfolder);
    g_object_unref(direnum);
    g_list_free_full(file_list, g_object_unref);
    if(error->code != G_IO_ERROR_CANCELLED)
      dt_print(DT_DEBUG_ALWAYS,
             "[_import_add_file_callback] error: %s", error->message);
    g_error_free(error);
    return;
  }
  else if(file_list == NULL)
  {
    // Done listing on this iterator

    g_object_unref(gfolder);
    g_file_enumerator_close(dir_files, NULL, NULL);
    g_object_unref(direnum);

    _update_images_number(self, 0);

    // Do we have more to parse

    if(d->to_be_visited)
    {
      GList *l = g_list_first(d->to_be_visited);
      char *dirname = (char *)l->data;
      d->to_be_visited = g_list_remove_link(d->to_be_visited, l);
      _import_set_file_list(dirname, user_data);
      g_free(dirname);
    }
    else
    {
      // Nothing more to parse, do select the images
      // according to the preference.
      uint32_t count_sel = 0;

      if(dt_conf_get_bool("ui_last/import_select_new"))
      {
        count_sel = _do_select_new(self);
      }
      else
      {
        _do_select_all(self);
        count_sel = d->from.nb;
      }
      d->is_importing = FALSE;
      _import_active(self, TRUE, count_sel);
      _update_images_number(self, count_sel);

      gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->from.store),
                                           DT_IMPORT_DATETIME, GTK_SORT_ASCENDING);
    }

    return;
  }
  else
  {
    g_file_enumerator_next_files_async(G_FILE_ENUMERATOR(direnum),
                                       FILE_REQUEST_BLOCK,
                                       G_PRIORITY_LOW,
                                       d->cancel_iter,
                                       _import_add_file_callback,
                                       user_data);

    const gboolean recursive = dt_conf_get_bool("ui_last/import_recursive");
    const gboolean include_nonraws = !dt_conf_get_bool("ui_last/import_ignore_nonraws");

    char *folder = g_file_get_path(gfolder);

    // if folder is root, consider one folder separator less
    const int offset = g_path_skip_root(folder)[0]
#ifdef WIN32
      // .. but for Windows UNC there will be a folder separator anyway
      || dt_util_path_is_UNC(folder)
#endif
      ? strlen(folder) + 1
      : strlen(folder);

    const dt_filmid_t filmroll_id = dt_film_get_id(folder);
    for(GList *node = file_list;
        node;
        node = node->next)
    {
      GFileInfo *info = node->data;

      const char *filename = g_file_info_get_display_name(info);

      if(!filename)
        continue;

      const GFileType filetype = g_file_info_get_file_type(info);
      const time_t datetime =
        g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED);

      /* g_file_info_get_is_hidden() always returns 0 on macOS, so we
         check if the filename starts with a '.' */
      const gboolean is_hidden = g_file_info_get_is_hidden(info) || filename[0] ==  '.';
      if(is_hidden)
        continue;

      gchar *fullname = g_build_filename(folder, filename, NULL);

      if(recursive && filetype == G_FILE_TYPE_DIRECTORY)
      {
        if(g_file_test(fullname, G_FILE_TEST_IS_SYMLINK))
        {
          dt_print(DT_DEBUG_CONTROL, "[import] skip symlink %s", fullname);
        }
        else
        {
          d->to_be_visited = g_list_prepend(d->to_be_visited, g_strdup(fullname));
        }
      }
      // Before adding to the import list, check that the format is
      // supported by this build of dt
      else if(filetype != G_FILE_TYPE_DIRECTORY && dt_supported_image(filename))
      {
        const char *ext = g_strrstr(filename, ".");
        // Either we are not rejecting non-raw, or the file extension
        // belongs to the raw format
        if(include_nonraws ||
           (ext && ((dt_imageio_is_raw_by_extension(ext)) ||
                    !g_ascii_strncasecmp(ext, ".dng", sizeof(".dng")))))
        {
          gboolean already_imported = FALSE;
          if(d->import_case == DT_IMPORT_INPLACE)
          {
            /* check if image is already imported, using previously
             * fetched filmroll id */
            if(filmroll_id != -1)
              already_imported = dt_image_get_id(filmroll_id, filename) == NO_IMGID
                ? FALSE : TRUE;
          }
          else
          {
            gchar *basename = g_path_get_basename(filename);
            char dtid[DT_DATETIME_EXIF_LENGTH];
            dt_datetime_unix_to_exif(dtid, sizeof(dtid), &datetime);
            already_imported = dt_metadata_already_imported(basename, dtid);
            g_free(basename);
          }

          GDateTime *dt_datetime = g_date_time_new_from_unix_local(datetime);
          gchar *dt_txt = g_date_time_format(dt_datetime, "%x %X");

          gtk_list_store_insert_with_values(d->from.store, NULL, -1,
                             DT_IMPORT_UI_EXISTS, already_imported ? "✔" : " ",
                             DT_IMPORT_UI_FILENAME, &fullname[offset],
                             DT_IMPORT_FILENAME, fullname,
                             DT_IMPORT_UI_DATETIME, dt_txt,
                             DT_IMPORT_DATETIME, datetime,
                             DT_IMPORT_THUMB, d->from.eye,
                             -1);
          d->from.nb++;
          g_free(dt_txt);
        }

        g_free(fullname);
      }
      g_object_unref(info);
    }

    g_free(folder);
  }
  g_list_free(file_list);
}

static void _import_enum_callback(GObject* source_object,
                                  GAsyncResult* res,
                                  gpointer user_data)
{
  dt_lib_module_t* self = (dt_lib_module_t *)user_data;
  dt_lib_import_t *d = self->data;

  GError *error = NULL;
  GFile *file = G_FILE (source_object);
  GFileEnumerator *dir_files = g_file_enumerate_children_finish(file, res, &error);

  if(error)
  {
    if(error->code != G_IO_ERROR_CANCELLED)
    {
      dt_print(DT_DEBUG_ALWAYS,
               "[_import_enum_callback] unable to create iterator,"
               " code: %d, error: %s\n", error->code, error->message);
    }
    g_error_free(error);
    return;
  }


  g_file_enumerator_next_files_async(dir_files,
                                     FILE_REQUEST_BLOCK,
                                     G_PRIORITY_LOW,
                                     d->cancel_iter,
                                     _import_add_file_callback,
                                     self);
}

static void _import_set_file_list(const gchar *folder,
                                  dt_lib_module_t *self)
{
  dt_lib_import_t *d = self->data;

  GFile *gfolder = g_file_new_for_path(folder);

  g_file_enumerate_children_async(gfolder,
                                  G_FILE_ATTRIBUTE_STANDARD_NAME ","
                                  G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
                                  G_FILE_ATTRIBUTE_TIME_MODIFIED ","
                                  G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
                                  G_FILE_ATTRIBUTE_STANDARD_TYPE,
                                  G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                  G_PRIORITY_LOW,
                                  d->cancel_iter,
                                  _import_enum_callback,
                                  self);
}

static void _import_set_file_list_start(const gchar *folder,
                                        dt_lib_module_t *self)
{
  dt_lib_import_t *d = self->data;

  _import_cancel(self);
  d->cancel_iter = g_cancellable_new();

  d->from.nb = 0;
  d->to_be_visited = NULL;
  d->is_importing = TRUE;

  // new selected folder, disable the thumb display button
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->from.thumbs), FALSE);

  const gboolean recursive = dt_conf_get_bool("ui_last/import_recursive");

  if(recursive)
  {
    // disable sorting as it is very costly for large set of files
    gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->from.store),
                                         GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
                                         GTK_SORT_ASCENDING);
  }

  _import_active(self, FALSE, 0);

  _import_set_file_list(folder, self);
}

static void _update_images_number(dt_lib_module_t* self,
                                  const guint nb_sel)
{
  dt_lib_import_t *d = self->data;
  GtkWidget *label = d->from.img_nb;
  char text[256] = { 0 };
  snprintf(text, sizeof(text),
           ngettext("%d image out of %d selected", "%d images out of %d selected",
                    nb_sel), nb_sel, d->from.nb);
  gtk_label_set_text(GTK_LABEL(label), text);
}

static void _import_from_selection_changed(GtkTreeSelection *selection,
                                           dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  const guint nb_sel = gtk_tree_selection_count_selected_rows(selection);
  _update_images_number(self, nb_sel);
  if(!d->is_importing)
    gtk_dialog_set_response_sensitive(GTK_DIALOG(d->from.dialog),
                                      GTK_RESPONSE_ACCEPT, nb_sel ? TRUE : FALSE);
}

static void _update_layout(dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  const gboolean usefn = dt_conf_get_bool("session/use_filename");
  for(int j = 0; j < 2 ; j++)
  {
    GtkWidget *w = gtk_grid_get_child_at(GTK_GRID(d->from.patterns), j, d->from.fn_line);
    if(GTK_IS_WIDGET(w))
      gtk_widget_set_sensitive(w, !usefn);
  }
}

static void _usefn_toggled(GtkWidget *widget,
                           dt_lib_module_t* self)
{
  _update_layout(self);
}

static gboolean _update_files_list(dt_lib_module_t *self)
{
  dt_lib_import_t *d = self->data;
  // clear parallel thumb refresh
  d->from.event = 0;
  GtkTreeModel *model = GTK_TREE_MODEL(d->from.store);
  g_object_ref(model);
  gtk_tree_view_set_model(d->from.treeview, NULL);
  gtk_list_store_clear(d->from.store);
  gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model),
                                       GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
                                       GTK_SORT_ASCENDING);
#ifdef HAVE_GPHOTO2
  if(d->import_case == DT_IMPORT_CAMERA)
  {
    d->from.nb = _import_from_camera_set_file_list(self);
    gtk_widget_hide(d->from.info);
    gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model),
                                         DT_IMPORT_FILENAME, GTK_SORT_ASCENDING);
  }
  else
#endif
  {
    char *folder = dt_conf_get_path("ui_last/import_last_directory");
    if(folder[0])
      _import_set_file_list_start(folder, self);
    g_free(folder);
  }
  gtk_tree_view_set_model(d->from.treeview, model);
  g_object_unref(model);

  if(dt_conf_get_bool("ui_last/import_select_new"))
    _do_select_new(self);
  else
    _do_select_all(self);

  return FALSE;
}

static void _import_new_toggled(GtkWidget *widget,
                                dt_lib_module_t* self)
{
  if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)))
    _do_select_new(self);
}

static void _ignore_nonraws_toggled(GtkWidget *widget,
                                    dt_lib_module_t* self)
{
  _update_files_list(self);
  _show_all_thumbs(self);
}

static void _recursive_toggled(GtkWidget *widget,
                               dt_lib_module_t* self)
{
  _update_files_list(self);
  _show_all_thumbs(self);
}

static void _do_select_all_clicked(GtkWidget *widget,
                                   dt_lib_module_t* self)
{
  _do_select_all(self);
}
static void _do_select_none_clicked(GtkWidget *widget,
                                    dt_lib_module_t* self)
{
  _do_select_none(self);
}

static void _do_select_new_clicked(GtkWidget *widget,
                                   dt_lib_module_t* self)
{
  _do_select_new(self);
}

static void _expander_create(dt_gui_collapsible_section_t *cs,
                             GtkBox *parent,
                             const char *label,
                             const char *pref_key,
                             dt_lib_module_t *self)
{
  dt_gui_new_collapsible_section
    (cs,
     pref_key,
     label,
     parent,
     DT_ACTION(self));
}

static gboolean _find_iter_folder(GtkTreeModel *model,
                                  GtkTreeIter *iter,
                                  const char *folder)
{
  gboolean found = FALSE;
  if(!folder) return found;
  char *path;
  do
  {
    gtk_tree_model_get(model, iter, DT_FOLDER_PATH, &path, -1);
    found = !g_strcmp0(folder, path);
    g_free(path);
    if(found) return found;
    GtkTreeIter child, parent = *iter;
    if(gtk_tree_model_iter_children(model, &child, &parent))
    {
      found = _find_iter_folder(model, &child, folder);
      if(found)
      {
        *iter = child;
        return found;
      }
    }
  } while(gtk_tree_model_iter_next(model, iter));
  return found;
}

static void _get_folders_list(GtkTreeStore *store,
                              GtkTreeIter *parent,
                              const gchar *folder,
                              const char *selected)
{
  if(!folder[0])
    return;

  // each time a new folder is added, it is set as not expanded and
  // assigned a fake child when expanded, the children are added and
  // the fake child is reused
  GError *error = NULL;
  GFile *gfolder = g_file_new_for_path(folder);
  GFileEnumerator *dir_files = g_file_enumerate_children(gfolder,
                                  G_FILE_ATTRIBUTE_STANDARD_NAME ","
                                  G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
                                  G_FILE_ATTRIBUTE_STANDARD_TYPE ","
                                  G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
                                  G_FILE_ATTRIBUTE_ACCESS_CAN_READ,
                                  G_FILE_QUERY_INFO_NONE, NULL, &error);

  gboolean expanded = FALSE;
  GtkTreeIter iter;
  GtkTreeIter parent2;
  if(!parent)
  {
    gchar *basename = g_path_get_basename(folder);
    gtk_tree_store_insert_with_values(store, &parent2, NULL, -1,
                                      DT_FOLDER_NAME, basename,
                                      DT_FOLDER_PATH, folder,
                                      DT_FOLDER_EXPANDED, FALSE, -1);
    // fake child
    gtk_tree_store_insert_with_values(store, &iter, &parent2, -1,
                                      DT_FOLDER_EXPANDED, FALSE, -1);
    g_free(basename);
  }
  else
  {
    parent2 = *parent;
    gtk_tree_model_get(GTK_TREE_MODEL(store), &parent2, DT_FOLDER_EXPANDED, &expanded, -1);
  }
  GFileInfo *info = NULL;
  gint i = 0;
  if(dir_files)
  {
    while((info = g_file_enumerator_next_file(dir_files, NULL, &error)))
    {
      const char *filename = g_file_info_get_name(info);
      if(!filename)
        continue;
      const gboolean ishidden =
        g_file_info_get_attribute_boolean(info, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN);
      const gboolean canread =
        g_file_info_get_attribute_boolean(info, G_FILE_ATTRIBUTE_ACCESS_CAN_READ);
      const GFileType filetype = g_file_info_get_file_type(info);
      if(filetype == G_FILE_TYPE_DIRECTORY && !ishidden && canread)
      {
        gchar *fullname = g_build_filename(folder, filename, NULL);
        if(!expanded)
        {
          GtkTreeIter child;
          const char *uifilename = g_file_info_get_display_name(info);
          gchar *uifullname = g_build_filename(folder, uifilename, NULL);
          gchar *basename = g_path_get_basename(uifullname);
          if(!i)
            gtk_tree_model_iter_children(GTK_TREE_MODEL(store), &iter, &parent2);
          else
            gtk_tree_store_append(store, &iter, &parent2);
          gtk_tree_store_set(store, &iter, DT_FOLDER_NAME, basename,
                                           DT_FOLDER_PATH, fullname,
                                           DT_FOLDER_EXPANDED, FALSE, -1);
          // fake child
          gtk_tree_store_append(store, &child, &iter);
          gtk_tree_store_set(store, &iter, DT_FOLDER_EXPANDED, FALSE, -1);
          g_free(uifullname);
          g_free(basename);
        }
        else
        {
          iter = parent2;
          if(!_find_iter_folder(GTK_TREE_MODEL(store), &iter, fullname))
          {
            g_free(fullname);
            g_object_unref(info);
            break;
          }
        }
        if(selected[0] && g_str_has_prefix(selected, fullname))
          _get_folders_list(store, &iter, fullname, selected);
        g_free(fullname);
        i++;
      }
      gtk_tree_store_set(store, &parent2, DT_FOLDER_EXPANDED, TRUE, -1);
      g_object_unref(info);
    }
    g_file_enumerator_close(dir_files, NULL, NULL);
    g_object_unref(dir_files);
  }
  if(!i)
  {
    // remove the fake child as there is no child
    gtk_tree_model_iter_children(GTK_TREE_MODEL(store), &iter, &parent2);
    gtk_tree_store_remove(store, &iter);
  }
}

// workaround to erase parasitic selection when click on expander or
// empty part of the view
static gboolean _clear_parasitic_selection(dt_lib_module_t *self)
{
  if(dt_conf_is_equal("ui_last/import_last_directory", ""))
  {
    dt_lib_import_t *d = self->data;
    GtkTreeSelection *selection = gtk_tree_view_get_selection(d->from.folderview);
    if(gtk_tree_selection_count_selected_rows(selection))
      gtk_tree_selection_unselect_all(selection);
  }
  return FALSE;
}

static void _remove_selected_place(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_import_t *d = (dt_lib_import_t *)self->data;

  GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->placesView));
  GtkTreeSelection *place_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->placesView));
  GList *path = gtk_tree_selection_get_selected_rows(place_selection, &model);

  if(!path)
    return;

  GtkTreeIter iter;
  gtk_tree_model_get_iter(model, &iter, path->data);
  char *folder_path;
  gtk_tree_model_get(model, &iter, 1, &folder_path, -1);
  _remove_place(folder_path, iter, self);

  dt_conf_set_string("ui_last/import_last_place", "");
  dt_conf_set_string("ui_last/import_last_directory", "");

  _update_folders_list(self);
  _update_files_list(self);
}

static void _places_button_press_cb(GtkGestureSingle *gesture, int n_press,
                                        double x, double y,
                                        dt_lib_module_t *self)
{
  GtkWidget *view = dt_gui_get_widget(gesture);
  GtkTreePath *path = NULL;
  /* gesture coordinates are relative to the widget allocation, while the
   * treeview coordinate APIs expect bin-window coordinates */
  gint bin_x, bin_y;
  gtk_tree_view_convert_widget_to_bin_window_coords(GTK_TREE_VIEW(view),
                                                    (gint)x, (gint)y, &bin_x, &bin_y);
  if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view),
                                   bin_x, bin_y, &path, NULL, NULL, NULL))
  {
    GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
    GtkTreeIter iter;
    gtk_tree_model_get_iter(model, &iter, path);

    char *folder_name, *folder_path;
    gtk_tree_model_get(model, &iter, 0, &folder_name, 1, &folder_path, -1);

    // left-click: set as new root
    if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY)
    {
      GtkTreeSelection *place_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
      gtk_tree_selection_select_path(place_selection, path);
      dt_conf_set_string("ui_last/import_last_place", folder_path);
      dt_conf_set_string("ui_last/import_last_directory", "");
      _import_cancel(self);
      _update_folders_list(self);
      _update_files_list(self);
    }

    g_free(folder_name);
    g_free(folder_path);
  }

  gtk_tree_path_free(path);
}

static void _folders_button_press_cb(GtkGestureSingle *gesture, int n_press,
                                         double x, double y,
                                         dt_lib_module_t *self)
{
  GtkWidget *view = dt_gui_get_widget(gesture);
  dt_lib_import_t *d = self->data;

  /* gesture coordinates are relative to the widget allocation, while the
   * treeview coordinate APIs expect bin-window coordinates */
  gint bin_x, bin_y;
  gtk_tree_view_convert_widget_to_bin_window_coords(GTK_TREE_VIEW(view),
                                                    (gint)x, (gint)y, &bin_x, &bin_y);

  const GdkModifierType state =
    dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture));

  if(n_press == 1
     && gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY
     && !dt_modifier_is(state, GDK_SHIFT_MASK | GDK_CONTROL_MASK))
  {
    GtkTreePath *path = NULL;
    if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view),
                                     bin_x, bin_y, &path, NULL, NULL, NULL))
    {
      GdkRectangle rect;
      gtk_tree_view_get_cell_area(GTK_TREE_VIEW(view), path, d->from.foldercol, &rect);
      const gboolean blank = gtk_tree_view_is_blank_at_pos(GTK_TREE_VIEW(view),
                                                           bin_x, bin_y,
                                                           NULL, NULL, NULL, NULL);
      // select and save new folder only if not click on expander
      if(blank || (bin_x > rect.x))
      {
        GtkTreeSelection *selection = gtk_tree_view_get_selection(d->from.folderview);
        gtk_tree_selection_select_path(selection, path);
        GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
        GtkTreeIter iter;
        gtk_tree_model_get_iter(model, &iter, path);
        char *folder;
        gtk_tree_model_get(model, &iter, DT_FOLDER_PATH, &folder, -1);
        dt_conf_set_string("ui_last/import_last_directory", folder);
        g_free(folder);
        _update_files_list(self);
        _show_all_thumbs(self);
      }
    }
    gtk_tree_path_free(path);
  }
  else if(n_press >= 2 && gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY)
  {
    GtkTreePath *path = NULL;
    gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view),
                                  bin_x, bin_y, &path, NULL, NULL, NULL);
    if(path)
    {
      if(gtk_tree_view_row_expanded(d->from.folderview, path))
        gtk_tree_view_collapse_row(d->from.folderview, path);
      else
        gtk_tree_view_expand_row(d->from.folderview, path, FALSE);
      gtk_tree_path_free(path);
    }
  }

  g_timeout_add_full(G_PRIORITY_DEFAULT_IDLE, 100, (GSourceFunc)_clear_parasitic_selection, self, NULL);
}

static void _folder_order_clicked(GtkTreeViewColumn *column,
                                  dt_lib_module_t *self)
{
  dt_conf_set_bool("ui_last/import_last_folder_descending",
                  !dt_conf_get_bool("ui_last/import_last_folder_descending"));
}

static void _row_expanded(GtkTreeView *view,
                          GtkTreeIter *iter,
                          GtkTreePath *path,
                          dt_lib_module_t *self)
{
  GtkTreeModel *model = gtk_tree_view_get_model(view);
  gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model),
                                       GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
                                       GTK_SORT_ASCENDING);
  char *fullname;
  gtk_tree_model_get(model, iter, DT_FOLDER_PATH, &fullname, -1);
  _get_folders_list(GTK_TREE_STORE(model), iter, fullname, "");
  g_free(fullname);
  gtk_tree_sortable_set_sort_column_id
    (GTK_TREE_SORTABLE(model), DT_FOLDER_PATH,
     dt_conf_get_bool("ui_last/import_last_folder_descending")
     ? GTK_SORT_DESCENDING : GTK_SORT_ASCENDING);
}

static void _paned_position_changed(GtkWidget *widget, GParamSpec *pspec, gpointer user_data)
{
  const gint position = gtk_paned_get_position(GTK_PANED(widget));
  dt_conf_set_int("ui_last/import_dialog_paned_pos", position);
}

static void _paned_places_position_changed(GtkWidget *widget, GParamSpec *pspec, gpointer user_data)
{
  const gint position = gtk_paned_get_position(GTK_PANED(widget));
  dt_conf_set_int("ui_last/import_dialog_paned_places_pos", position);
}

static void _places_reset_callback(GtkWidget *widget,
                                   dt_lib_module_t* self)
{
  dt_conf_set_bool("ui_last/import_dialog_show_home", TRUE);
  dt_conf_set_bool("ui_last/import_dialog_show_pictures", TRUE);
  dt_conf_set_bool("ui_last/import_dialog_show_mounted", TRUE);

  _update_places_list(self);
}

static void _set_places_list(GtkWidget *places_paned,
                             dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  d->placesModel = gtk_list_store_new(DT_PLACES_NUM_COLS, G_TYPE_STRING,
                                      G_TYPE_STRING, G_TYPE_INT);
  d->placesView = gtk_tree_view_new_with_model(GTK_TREE_MODEL(d->placesModel));

  GtkWidget *places_top_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
  GtkWidget *places_header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
  gtk_widget_set_tooltip_text(places_header,
                              _("choose the root of the folder tree below"));

  GtkWidget *places_label = gtk_label_new(NULL);
  gchar *markup = g_strdup_printf("<b>  %s</b>",_("places"));
  gtk_label_set_markup(GTK_LABEL(places_label), markup);
  g_free(markup);
  gtk_box_pack_start(GTK_BOX(places_header), places_label, FALSE, FALSE, 0);

  GtkWidget *places_reset = dtgtk_button_new_full(dtgtk_cairo_paint_reset, 0, NULL,
      &(dtgtk_button_config_t){
        .tooltip = _("restore all default places you have removed"),
        .clicked_cb = G_CALLBACK(_places_reset_callback),
        .clicked_data = self,
      });
  gtk_box_pack_end(GTK_BOX(places_header), places_reset, FALSE, FALSE, 0);

  d->remove_place_button = dtgtk_button_new_full(dtgtk_cairo_paint_minus_simple, 0, NULL,
      &(dtgtk_button_config_t){
        .tooltip = _("remove the selected custom place"),
        .clicked_cb = G_CALLBACK(_remove_selected_place),
        .clicked_data = self,
      });
  gtk_box_pack_end(GTK_BOX(places_header), d->remove_place_button, FALSE, FALSE, 0);

  GtkWidget *places_add = dtgtk_button_new_full(dtgtk_cairo_paint_plus_simple, 0, NULL,
      &(dtgtk_button_config_t){
        .tooltip = _("add a custom place"),
        .clicked_cb = G_CALLBACK(_lib_import_select_folder),
        .clicked_data = self,
      });
  gtk_box_pack_end(GTK_BOX(places_header), places_add, FALSE, FALSE, 0);

  gtk_box_pack_start(GTK_BOX(places_top_box), places_header, FALSE, FALSE, 0);

  GtkWidget *placesWindow = dt_gui_scroll_wrap(d->placesView);
  gtk_widget_set_tooltip_text(placesWindow,
                              _("you can add custom places using the plus icon"));

  gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(d->placesView),FALSE);
  gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(d->placesView), DT_PLACES_PATH);

  GtkTreeViewColumn* placesColumn =
    gtk_tree_view_column_new_with_attributes("", gtk_cell_renderer_text_new(),
                                             "text", 0, NULL);
  gtk_tree_view_append_column(GTK_TREE_VIEW(d->placesView), placesColumn);

  gtk_box_pack_start(GTK_BOX(places_top_box), placesWindow, TRUE, TRUE, 0);
  gtk_paned_pack1(GTK_PANED(places_paned), places_top_box, TRUE, TRUE);


  dt_gui_connect_click(d->placesView, _places_button_press_cb, NULL, self);
}

static void _set_folders_list(GtkWidget *places_paned, dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  GtkTreeStore *store = gtk_tree_store_new(DT_FOLDER_NUM_COLS, G_TYPE_STRING,
                                           G_TYPE_STRING, G_TYPE_BOOLEAN);
  d->from.folderview = GTK_TREE_VIEW(gtk_tree_view_new());
  GtkWidget *w = dt_gui_scroll_wrap(GTK_WIDGET(d->from.folderview));
  gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_AUTOMATIC,
                                 GTK_POLICY_ALWAYS);
  gtk_widget_set_tooltip_text(GTK_WIDGET(d->from.folderview),
                              _("select a folder to see the content"));

  GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
  GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(_("folders"),
                                                                       renderer, "text",
                                                    DT_FOLDER_NAME, NULL);
  gtk_tree_view_append_column(d->from.folderview, column);
  gtk_tree_view_column_set_expand(column, TRUE);
  gtk_tree_view_column_set_resizable(column, TRUE);
  gtk_tree_view_set_expander_column(d->from.folderview, column);
  g_signal_connect(d->from.folderview, "row-expanded", G_CALLBACK(_row_expanded), self);
  dt_gui_connect_click(d->from.folderview, _folders_button_press_cb, NULL, self);
  gtk_tree_view_column_set_sort_column_id(column, DT_FOLDER_PATH);
  gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(store), DT_FOLDER_PATH,
                                dt_conf_get_bool("ui_last/import_last_folder_descending")
                                ? GTK_SORT_DESCENDING : GTK_SORT_ASCENDING);
  g_signal_connect(column, "clicked", G_CALLBACK(_folder_order_clicked), self);
  gtk_tree_view_column_set_min_width(column, DT_PIXEL_APPLY_DPI(200));
  d->from.foldercol = column;
  gtk_scrolled_window_set_min_content_width(GTK_SCROLLED_WINDOW(w),
                                            DT_PIXEL_APPLY_DPI(200));
  gtk_tree_view_set_model(d->from.folderview, GTK_TREE_MODEL(store));
  gtk_tree_view_set_headers_visible(d->from.folderview, TRUE);
  gtk_paned_pack2(GTK_PANED(places_paned), w, TRUE, TRUE);
}

static void _expand_folder(const char *folder,
                           const gboolean select,
                           dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  if(folder && folder[0])
  {
    GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->from.folderview));
    GtkTreeIter iter;
    if(gtk_tree_model_get_iter_first(model, &iter))
    {
      if(_find_iter_folder(model, &iter, folder))
      {
        // except for root expand only to parent
        GtkTreeIter parent;
        if(!gtk_tree_model_iter_parent(model, &parent, &iter))
          parent = iter;
        GtkTreePath *parent_path = gtk_tree_model_get_path(model, &parent);
        GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
        gtk_tree_view_expand_to_path(d->from.folderview, parent_path);
        gtk_tree_view_scroll_to_cell(d->from.folderview, path, NULL, TRUE, 0.5, 0.5);
        gtk_tree_path_free(path);
        gtk_tree_path_free(parent_path);
        if(select)
        {
          GtkTreeSelection *selection = gtk_tree_view_get_selection(d->from.folderview);
          gtk_tree_selection_select_iter(selection, &iter);
        }
      }
    }
  }
}

static void _update_places_list(dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  gtk_list_store_clear(d->placesModel);

  // add default folders

  GtkTreeIter iter, current_iter;
  d->placesSelection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->placesView));
  char *last_place = dt_conf_get_string("ui_last/import_last_place");
  char *current_place = NULL;

  if(dt_conf_get_bool("ui_last/import_dialog_show_home"))
  {
    gchar *homedir = dt_loc_get_home_dir(NULL);
    if(homedir)
    {
      current_place = homedir;
      gtk_list_store_insert_with_values(d->placesModel, &iter, -1,
                                        DT_PLACES_NAME, _("home"), DT_PLACES_PATH,
                                        current_place, DT_PLACES_TYPE, DT_TYPE_HOME, -1);
      if(!g_strcmp0(current_place, last_place))
        gtk_tree_selection_select_iter(d->placesSelection, &iter);
      current_iter = iter;
    }
  }

  if(dt_conf_get_bool("ui_last/import_dialog_show_pictures")
     && g_get_user_special_dir(G_USER_DIRECTORY_PICTURES))
  {
    g_free(current_place);
    current_place = g_strdup(g_get_user_special_dir(G_USER_DIRECTORY_PICTURES));
    gtk_list_store_insert_with_values(d->placesModel, &iter, -1,
                                      DT_PLACES_NAME, _("pictures"), DT_PLACES_PATH,
                                      current_place, DT_PLACES_TYPE, DT_TYPE_PIC, -1);
    if(!g_strcmp0(current_place, last_place))
      gtk_tree_selection_select_iter(d->placesSelection, &iter);
    current_iter = iter;
  }

  // set home/pictures as default
  if(last_place[0] == '\0' && current_place)
  {
    dt_conf_set_string("ui_last/import_last_place", current_place);
    gtk_tree_selection_select_iter(d->placesSelection, &current_iter);
  }
  g_free(current_place);

  // add mounted drives
  if(dt_conf_get_bool("ui_last/import_dialog_show_mounted"))
  {
    GVolumeMonitor *placesMonitor = g_volume_monitor_get();
    GList *drives, *drive, *volumes, *volume;
    drives = g_volume_monitor_get_connected_drives(placesMonitor);

    for(drive = drives; drive; drive = drive->next)
    {
      volumes = g_drive_get_volumes(drive->data);
      for(volume = volumes; volume; volume = volume->next)
      {
        GMount *placesMount = g_volume_get_mount(volume->data);
        if(placesMount)
        {
          GFile *placesFile = g_mount_get_root(placesMount);
          g_object_unref(placesMount);

          gchar *volname = g_volume_get_name(volume->data);
          gchar *path = g_file_get_path(placesFile);
          gtk_list_store_insert_with_values(d->placesModel, &iter, -1,
                                            DT_PLACES_NAME, volname,
                                            DT_PLACES_PATH, path,
                                            DT_PLACES_TYPE, DT_TYPE_MOUNT, -1);
          g_free(volname);
          g_free(path);

          if(!g_strcmp0(g_file_get_path(placesFile), last_place))
            gtk_tree_selection_select_iter(d->placesSelection, &iter);
        }
      }
      g_list_free(volumes);
    }
    g_list_free(drives);
  }

  // add folders added by user
  GList *places = _get_custom_places();

  for(GList *places_iter = places; places_iter; places_iter = places_iter->next)
  {
    gchar *basename = g_path_get_basename(places_iter->data);

#ifdef WIN32
    // special case: root folder shall keep the drive letter in the basename
    if(basename[0] == G_DIR_SEPARATOR && !basename[1])
    {
      g_free(basename);
      basename = g_strdup(places_iter->data);
    }
#endif

    gtk_list_store_insert_with_values(d->placesModel, &iter, -1,
                                      DT_PLACES_NAME, basename,
                                      DT_PLACES_PATH, (char *)places_iter->data,
                                      DT_PLACES_TYPE, DT_TYPE_CUSTOM, -1);
    g_free(basename);
    if(!g_strcmp0(places_iter->data, last_place))
      gtk_tree_selection_select_iter(d->placesSelection, &iter);
  }
  g_free(last_place);
  // the list returned by _get_custom_places references a single
  // string that has been split on commas.  Release it by freeing the
  // data of the list's first node
  if(places)
    g_free(places->data);
  g_list_free(places);

  _update_folders_list(self);
  _update_files_list(self);
}

static void _update_folders_list(dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->from.folderview));
  g_object_ref(model);
  gtk_tree_view_set_model(d->from.folderview, NULL);
  gtk_tree_store_clear(GTK_TREE_STORE(model));
  const char *last_place = dt_conf_get_string_const("ui_last/import_last_place");
  char *folder = dt_conf_get_path("ui_last/import_last_directory");
  gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model),
                                       GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
                                       GTK_SORT_ASCENDING);
  gtk_widget_set_sensitive(d->remove_place_button, last_place[0]);
  _get_folders_list(GTK_TREE_STORE(model), NULL, last_place, folder);
  gtk_tree_sortable_set_sort_column_id
    (GTK_TREE_SORTABLE(model), DT_FOLDER_PATH,
     dt_conf_get_bool("ui_last/import_last_folder_descending")
     ? GTK_SORT_DESCENDING : GTK_SORT_ASCENDING);
  gtk_tree_view_set_model(d->from.folderview, model);
  g_object_unref(model);

  if(folder[0] && strncmp(folder, last_place, strlen(last_place)) == 0)
    _expand_folder(folder, TRUE, self);
  else
    _expand_folder(last_place, FALSE, self);

  g_free(folder);
}

static void _escape_place_name_comma(char *name)
{
  for(int i = 0; name && i < strlen(name); i++)
    if(name[i] == ',') name[i] = '\1';
}

static void _restore_place_name_comma(char *name)
{
  for(int i = 0; name && i < strlen(name); i++)
    if(name[i] == '\1') name[i] = ',';
}

static gboolean _find_iter_place(GtkTreeModel *model,
                                 GtkTreeIter *iter,
                                 const char *place)
{
  gboolean found = FALSE;
  if(!place) return found;
  char *path;
  do
  {
    gtk_tree_model_get(model, iter, DT_PLACES_PATH, &path, -1);
    found = !g_strcmp0(place, path);
    g_free(path);
    if(found) return found;
  } while(gtk_tree_model_iter_next(model, iter));
  return found;
}

static void _add_custom_place(gchar *place,
                              dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;

  GtkTreeIter iter;
  gboolean found = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(d->placesModel), &iter);
  if(found)
    found = _find_iter_place(GTK_TREE_MODEL(d->placesModel), &iter, place);

  if(!found)
  {
    const char *current_places = dt_conf_get_string_const("ui_last/import_custom_places");
    _escape_place_name_comma(place);
    gchar *places = g_strdup_printf("%s%s,", current_places, place);
    dt_conf_set_string("ui_last/import_custom_places", places);
    g_free(places);

    _restore_place_name_comma(place);
    gchar *basename = g_path_get_basename(place);
#ifdef WIN32
    // special case: root folder shall keep the drive letter in the basename
    if(G_IS_DIR_SEPARATOR(basename[0]) && !basename[1])
    {
      g_free(basename);
      basename = g_strdup(place);
    }
#endif
    gtk_list_store_insert_with_values(d->placesModel, &iter, -1,
                                      DT_PLACES_NAME, basename,
                                      DT_PLACES_PATH, (char *)place,
                                      DT_PLACES_TYPE, DT_TYPE_CUSTOM, -1);
    g_free(basename);
  }

  dt_conf_set_string("ui_last/import_last_place", place);
  gtk_tree_selection_select_iter(d->placesSelection, &iter);
}

static void _remove_place(gchar *place, GtkTreeIter iter, dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  _escape_place_name_comma(place);
  const char *current_places = dt_conf_get_string_const("ui_last/import_custom_places");
  int type = 0;
  gtk_tree_model_get(GTK_TREE_MODEL(d->placesModel), &iter,
                     DT_PLACES_TYPE, &type, -1);

  if(type == DT_TYPE_HOME)
    dt_conf_set_bool("ui_last/import_dialog_show_home", FALSE);
  if(type == DT_TYPE_PIC)
    dt_conf_set_bool("ui_last/import_dialog_show_pictures", FALSE);
  if(type == DT_TYPE_MOUNT)
    dt_conf_set_bool("ui_last/import_dialog_show_mounted", FALSE);
  if(type == DT_TYPE_CUSTOM)
  {
    gchar *pattern = g_strdup_printf("%s,", place);
    gchar *places = dt_util_str_replace(current_places, pattern, "");
    dt_conf_set_string("ui_last/import_custom_places", places);
    g_free(pattern);
    g_free(places);
  }

  _update_places_list(self);
}

static GList* _get_custom_places()
{
  GList *places = NULL;
  gchar *saved = dt_conf_get_string("ui_last/import_custom_places");
  const int nb_saved = saved[0] ? dt_util_str_occurence(saved, ",") + 1 : 0;

  for(int i = 0; i < nb_saved; i++)
  {
    char *next = g_strstr_len(saved, strlen(saved), ",");
    if(next)
      next[0] = '\0';
    if(saved[0])
    {
      places = g_list_append(places, saved);
      _restore_place_name_comma(saved);
      if(next)
        saved = next + 1;
    }
  }

  return places;
}

static void _lib_import_select_folder(GtkWidget *widget,
                                      dt_lib_module_t *self)
{
  dt_lib_import_t *d = self->data;
  GtkWidget *win = dt_ui_main_window(darktable.gui->ui);

  GtkFileChooserNative *filechooser = gtk_file_chooser_native_new(
      _("select directory"), GTK_WINDOW(win),
      GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, _("_open"), _("_cancel"));

  // run the native dialog
  dt_conf_get_folder_to_file_chooser("ui_last/import_last_place",
                                     GTK_FILE_CHOOSER(filechooser));
  if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT)
  {
    char *dirname = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));
    _add_custom_place(dirname, self);
    g_free(dirname);
  }
  g_object_unref(filechooser);

  dt_conf_set_string("ui_last/import_last_directory", "");
  dt_conf_set_bool("ui_last/import_recursive", FALSE);
  dt_gui_preferences_bool_update(d->recursive);
  _update_folders_list(self);
  _update_files_list(self);
}

static void _set_files_list(GtkWidget *rbox, dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  d->from.store = gtk_list_store_new(DT_IMPORT_NUM_COLS, G_TYPE_BOOLEAN, GDK_TYPE_PIXBUF,
                                     G_TYPE_STRING, G_TYPE_STRING,
                                     G_TYPE_STRING, G_TYPE_STRING,
                                     G_TYPE_UINT64);
  d->from.eye = dt_draw_paint_to_pixbuf(GTK_WIDGET(d->from.dialog), 13, 0,
                                        dtgtk_cairo_paint_eye);

  // Create the treview with list model data store
  d->from.treeview = GTK_TREE_VIEW(gtk_tree_view_new());
  d->from.w = dt_gui_scroll_wrap(GTK_WIDGET(d->from.treeview));
  gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(d->from.w), GTK_POLICY_NEVER,
                                 GTK_POLICY_ALWAYS);

  GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
  GtkTreeViewColumn *column =
    gtk_tree_view_column_new_with_attributes("✔", renderer, "text",
                                             DT_IMPORT_UI_EXISTS, NULL);
  g_object_set(renderer, "xalign", 0.5, NULL);
  gtk_tree_view_append_column(d->from.treeview, column);
  gtk_tree_view_column_set_alignment(column, 0.5);
  gtk_tree_view_column_set_min_width(column, DT_PIXEL_APPLY_DPI(25));
  GtkWidget *header = gtk_tree_view_column_get_button(column);
  gtk_widget_set_tooltip_text(header, _("mark already imported images"));

  renderer = gtk_cell_renderer_text_new();
  column = gtk_tree_view_column_new_with_attributes(_("name"), renderer, "text",
                                                    DT_IMPORT_UI_FILENAME, NULL);
  gtk_tree_view_append_column(d->from.treeview, column);

  gtk_tree_view_column_set_expand(column, TRUE);
  gtk_tree_view_column_set_resizable(column, TRUE);
  gtk_tree_view_column_set_min_width(column, DT_PIXEL_APPLY_DPI(200));
  g_object_set(renderer, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, NULL);
  gtk_tree_view_column_set_sort_column_id(column, DT_IMPORT_FILENAME);

  renderer = gtk_cell_renderer_text_new();
  column = gtk_tree_view_column_new_with_attributes(_("modified"), renderer, "text",
                                                    DT_IMPORT_UI_DATETIME, NULL);
  gtk_tree_view_append_column(d->from.treeview, column);
  gtk_tree_view_column_set_sort_column_id(column, DT_IMPORT_DATETIME);
  header = gtk_tree_view_column_get_button(column);
  gtk_widget_set_tooltip_text
    (header,
     _("file 'modified date/time', may be different from 'Exif date/time'"));
  gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->from.store),
                                       DT_IMPORT_DATETIME, GTK_SORT_ASCENDING);

  renderer = gtk_cell_renderer_pixbuf_new();
  column = gtk_tree_view_column_new_with_attributes("", renderer, "pixbuf",
                                                    DT_IMPORT_THUMB, NULL);
  gtk_tree_view_append_column(d->from.treeview, column);
  GtkWidget *button = dtgtk_togglebutton_new(dtgtk_cairo_paint_eye, 0, NULL);
  dt_gui_add_class(button, "dt_transparent_background");
  gtk_widget_show(button);
  header = gtk_tree_view_column_get_button(column);
  gtk_widget_set_tooltip_text(header, _("show/hide thumbnails"));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), FALSE);
  gtk_tree_view_column_set_widget(column, button);
  g_signal_connect(column, "clicked", G_CALLBACK(_all_thumb_toggled), self);
  d->from.thumbs = button;
  gtk_tree_view_column_set_alignment(column, 0.5);
  gtk_tree_view_column_set_clickable(column, TRUE);
  gtk_tree_view_column_set_min_width(column, DT_PIXEL_APPLY_DPI(128));
  d->from.pixcol = column;
  dt_gui_connect_click(d->from.treeview, _files_button_press_cb, NULL, self);

  GtkTreeSelection *selection = gtk_tree_view_get_selection(d->from.treeview);
  gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
  g_signal_connect(G_OBJECT(selection), "changed",
                   G_CALLBACK(_import_from_selection_changed), self);

  gtk_tree_view_set_model(d->from.treeview, GTK_TREE_MODEL(d->from.store));
  gtk_tree_view_set_headers_visible(d->from.treeview, TRUE);

  gtk_box_pack_start(GTK_BOX(rbox), GTK_WIDGET(d->from.w), TRUE, TRUE, 0);
}

static void _browse_basedir_clicked(GtkWidget *widget,
                                    GtkEntry *basedir)
{
  GtkWidget *topwindow = gtk_widget_get_toplevel(widget);
  if(!GTK_IS_WINDOW(topwindow))
  {
    topwindow = dt_ui_main_window(darktable.gui->ui);
  }

  GtkFileChooserNative *filechooser = gtk_file_chooser_native_new(
      _("select directory"), GTK_WINDOW(topwindow),
      GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, _("_open"), _("_cancel"));

  gchar *old = g_strdup(gtk_entry_get_text(basedir));
  char *c = g_strstr_len(old, -1, "$");
  if(c) *c = '\0';
  gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(filechooser), old);
  g_free(old);
  if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT)
  {
    gchar *dir = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));

    // dir can now contain '\': on Windows it's the path separator,
    // on other platforms it can be part of a regular folder name.
    // This would later clash with variable substitution, so we have to escape them
    gchar *escaped = dt_util_str_replace(dir, "\\", "\\\\");

    gtk_entry_set_text(basedir, escaped); // the signal handler will write this to conf
    gtk_editable_set_position(GTK_EDITABLE(basedir), strlen(escaped));
    g_free(dir);
    g_free(escaped);
  }
  g_object_unref(filechooser);
}

static void _set_expander_content(GtkWidget *rbox,
                                  dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  // separator
  dt_gui_add_class(GTK_WIDGET(d->from.w), "dt_section_label");
  // job code
  GtkWidget *import_patterns = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
  GtkGrid *grid = GTK_GRID(gtk_grid_new());
  gtk_grid_set_column_spacing(grid, DT_PIXEL_APPLY_DPI(5));
  gint line = 0;
  dt_gui_preferences_string(grid, "ui_last/import_jobcode", 0, line++);
  gtk_box_pack_start(GTK_BOX(import_patterns), GTK_WIDGET(grid), FALSE, FALSE, 0);

  // collapsible section
  _expander_create(&d->from.cs, GTK_BOX(import_patterns),
                   _("naming rules"), "ui_last/session_expander_import", NULL);

  // import patterns
  grid = GTK_GRID(gtk_grid_new());
  gtk_grid_set_column_spacing(grid, DT_PIXEL_APPLY_DPI(5));
  d->from.datetime = dt_gui_preferences_string
    (grid, "ui_last/import_datetime_override", 0, line++);
  GtkWidget *basedir = dt_gui_preferences_string
    (grid, "session/base_directory_pattern", 0, line++);

  GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
  g_object_ref(basedir);
  gtk_container_remove(GTK_CONTAINER(grid), basedir);
  gtk_box_pack_start(GTK_BOX(hbox), basedir, TRUE, TRUE, 0);
  g_object_unref(basedir);
  GtkWidget *browsedir = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL,
      &(dtgtk_button_config_t){
        .tooltip = _("select directory"),
        .clicked_cb = G_CALLBACK(_browse_basedir_clicked),
        .clicked_data = basedir,
      });
  gtk_widget_set_name(browsedir, "non-flat");

  gtk_box_pack_start(GTK_BOX(hbox), browsedir, FALSE, FALSE, 0);
  gtk_grid_attach_next_to(grid, hbox, gtk_grid_get_child_at(grid, 0, line - 1),
                          GTK_POS_RIGHT, 1, 1);

  dt_gui_preferences_string(grid, "session/sub_directory_pattern", 0, line++);
  GtkWidget *usefn = dt_gui_preferences_bool(grid, "session/use_filename", 0,
                                             line++, FALSE);
  d->from.fn_line = line;
  dt_gui_preferences_string(grid, "session/filename_pattern", 0, line++);
  gtk_box_pack_start(GTK_BOX(d->from.cs.container), GTK_WIDGET(grid), FALSE, FALSE, 0);
  d->from.patterns = grid;
  _update_layout(self);
  g_signal_connect(usefn, "toggled", G_CALLBACK(_usefn_toggled), self);
  gtk_box_pack_start(GTK_BOX(rbox), import_patterns, FALSE, FALSE, 0);

  GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
  grid = GTK_GRID(gtk_grid_new());
  gtk_grid_set_column_spacing(grid, DT_PIXEL_APPLY_DPI(5));
  dt_gui_preferences_bool(grid, "ui_last/import_keep_open", 0, 0, TRUE);
  gtk_box_pack_end(GTK_BOX(box), GTK_WIDGET(grid), FALSE, FALSE, 0);
  gtk_box_pack_start(GTK_BOX(rbox), GTK_WIDGET(box), FALSE, FALSE, 0);
}

static void _import_from_dialog_new(dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  GtkWidget *win = dt_ui_main_window(darktable.gui->ui);

  d->from.dialog = gtk_dialog_new_with_buttons
    (_(_import_text[d->import_case]), NULL,
     GTK_DIALOG_MODAL,
     _("_cancel"), GTK_RESPONSE_CANCEL,
     _(_import_text[d->import_case]), GTK_RESPONSE_ACCEPT,
     NULL);
  gtk_dialog_set_default_response(GTK_DIALOG(d->from.dialog), GTK_RESPONSE_ACCEPT);
  dt_gui_dialog_add_help(GTK_DIALOG(d->from.dialog), "import_dialog");

#ifdef GDK_WINDOWING_QUARTZ
  dt_osx_disallow_fullscreen(d->from.dialog);
#endif
  dt_gui_dialog_restore_size(GTK_DIALOG(d->from.dialog), "import");
  gtk_window_set_transient_for(GTK_WINDOW(d->from.dialog), GTK_WINDOW(win));
  dt_gui_connect_key(d->from.dialog, dt_handle_dialog_enter, self);

  // images numbers in action-box
  GtkWidget *box = dt_gui_container_first_child(GTK_CONTAINER(d->from.dialog));
  box = dt_gui_container_first_child(GTK_CONTAINER(box)); // action-box

  d->select_all = gtk_button_new_with_label(_("select all"));
  gtk_box_pack_start(GTK_BOX(box), d->select_all, FALSE, FALSE, 2);
  g_signal_connect(d->select_all, "clicked", G_CALLBACK(_do_select_all_clicked), self);

  d->select_none = gtk_button_new_with_label(_("select none"));
  gtk_box_pack_start(GTK_BOX(box), d->select_none, FALSE, FALSE, 2);
  g_signal_connect(d->select_none, "clicked", G_CALLBACK(_do_select_none_clicked), self);

  d->select_new = gtk_button_new_with_label(_("select new"));
  gtk_box_pack_start(GTK_BOX(box), d->select_new, FALSE, FALSE, 2);
  g_signal_connect(d->select_new, "clicked", G_CALLBACK(_do_select_new_clicked), self);

  d->from.img_nb = gtk_label_new("");
  gtk_widget_set_halign(d->from.img_nb, GTK_ALIGN_END);
  gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(d->from.img_nb), TRUE, TRUE, 0);

  GtkWidget *paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
  gtk_paned_set_position(GTK_PANED(paned),
                         dt_conf_get_int("ui_last/import_dialog_paned_pos"));

  // right pane
  GtkWidget *rbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
  gtk_paned_pack2(GTK_PANED(paned), rbox, TRUE, FALSE);
  dt_gui_dialog_add(GTK_DIALOG(d->from.dialog), paned);

  guint line = 0;
  guint col = 0;
  GtkGrid *grid = GTK_GRID(gtk_grid_new());
  gtk_grid_set_column_spacing(grid, DT_PIXEL_APPLY_DPI(5));

  d->import_new = dt_gui_preferences_bool(grid, "ui_last/import_select_new", col++,
                                          line, TRUE);
  gtk_widget_set_hexpand(gtk_grid_get_child_at(grid, col++, line), TRUE);
  g_signal_connect(G_OBJECT(d->import_new), "toggled",
                   G_CALLBACK(_import_new_toggled), self);

  if(d->import_case != DT_IMPORT_CAMERA)
  {
    d->recursive = dt_gui_preferences_bool(grid, "ui_last/import_recursive", col++,
                                           line, TRUE);
    gtk_widget_set_hexpand(gtk_grid_get_child_at(grid, col++, line), TRUE);
    g_signal_connect(G_OBJECT(d->recursive), "toggled",
                     G_CALLBACK(_recursive_toggled), self);
  }
  GtkWidget *ignore_nonraws =
    dt_gui_preferences_bool(grid, "ui_last/import_ignore_nonraws", col++, line, TRUE);
  gtk_widget_set_hexpand(gtk_grid_get_child_at(grid, col++, line++), TRUE);
  g_signal_connect(G_OBJECT(ignore_nonraws), "toggled",
                   G_CALLBACK(_ignore_nonraws_toggled), self);
  gtk_box_pack_start(GTK_BOX(rbox), GTK_WIDGET(grid), FALSE, FALSE, 8);

  // files list
  _set_files_list(rbox, self);
  g_timeout_add_full(G_PRIORITY_LOW, 100, (GSourceFunc)_update_files_list, self, NULL);

#ifdef HAVE_GPHOTO2
  if(d->import_case == DT_IMPORT_CAMERA)
  {
    d->from.info = dt_ui_label_new
      (_("please wait while prefetching the list of images from camera..."));
    gtk_label_set_single_line_mode(GTK_LABEL(d->from.info), FALSE);
    gtk_box_pack_start(GTK_BOX(rbox), d->from.info, FALSE, FALSE, 0);
  }
  else
#endif
  {
    // left pane
    g_signal_connect(paned, "notify::position", G_CALLBACK(_paned_position_changed), self);
    GtkWidget *lbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
    gtk_paned_pack1(GTK_PANED(paned), lbox, TRUE, FALSE);

    GtkWidget *places_paned = gtk_paned_new(GTK_ORIENTATION_VERTICAL);

    gtk_paned_set_position(GTK_PANED(places_paned),
                           dt_conf_get_int("ui_last/import_dialog_paned_places_pos"));
    g_signal_connect(places_paned, "notify::position",
                     G_CALLBACK(_paned_places_position_changed), self);

    _set_places_list(places_paned, self);
    _set_folders_list(places_paned, self);
    gtk_box_pack_start(GTK_BOX(lbox), places_paned, TRUE, TRUE, 0);

    _update_places_list(self);
    _update_folders_list(self);
  }

  // patterns expander
  if(d->import_case != DT_IMPORT_INPLACE)
  {
    _set_expander_content(rbox, self);
    gtk_widget_show_all(d->from.dialog);
    dt_gui_update_collapsible_section(&d->from.cs);
  }
  else
  {
    gtk_widget_show_all(d->from.dialog);
  }

  // make sure no buttons focused, so default button is marked
  gtk_window_set_focus(GTK_WINDOW(d->from.dialog), NULL);
}

static void _import_set_collection(const char *dirname)
{
  if(dirname)
  {
    dt_collection_properties_t property =
      dt_conf_get_int("plugins/lighttable/collect/item0");

    if(property != DT_COLLECTION_PROP_FOLDERS
       && property != DT_COLLECTION_PROP_FILMROLL)
    {
      // the current collection is not based on filmrolls or folders
      // fallback to DT_COLLECTION_PROP_FILMROLL. Otherwise we keep
      // the current property of the collection.
      property = DT_COLLECTION_PROP_FILMROLL;
    }
    dt_conf_set_int("plugins/lighttable/collect/num_rules", 1);
    dt_conf_set_int("plugins/lighttable/collect/item0", property);
    dt_conf_set_string("plugins/lighttable/collect/string0", dirname);
    dt_collection_update_query(darktable.collection,
                               DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF,
                               NULL);
  }
}

static void _do_select_all(dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;

  GtkTreeSelection *selection = gtk_tree_view_get_selection(d->from.treeview);
  gtk_tree_selection_select_all(selection);
}

static void _do_select_none(dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;

  GtkTreeSelection *selection = gtk_tree_view_get_selection(d->from.treeview);
  gtk_tree_selection_unselect_all(selection);
}

static uint32_t _do_select_new(dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;

  GtkTreeIter iter;
  GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->from.treeview));
  GtkTreeSelection *selection = gtk_tree_view_get_selection(d->from.treeview);
  uint32_t count = 0;

  gtk_tree_selection_unselect_all(selection);

  if(gtk_tree_model_get_iter_first(model, &iter))
  {
    do
    {
      gchar *sel = NULL;
      gtk_tree_model_get(model, &iter, DT_IMPORT_UI_EXISTS, &sel, -1);
      if(sel && !strcmp(sel, " "))
      {
        gtk_tree_selection_select_iter(selection, &iter);
        count++;
      }
    }
    while(gtk_tree_model_iter_next(model, &iter));
  }

  return count;
}

static void _import_from_dialog_run(dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;

  while(gtk_dialog_run(GTK_DIALOG(d->from.dialog)) == GTK_RESPONSE_ACCEPT)
  {
    // reset filter so that view isn't empty
    dt_view_filtering_reset(darktable.view_manager, TRUE);
    GList *imgs = NULL;
    GtkTreeModel *model = GTK_TREE_MODEL(d->from.store);
    GtkTreeSelection *selection = gtk_tree_view_get_selection(d->from.treeview);
    GList *paths = gtk_tree_selection_get_selected_rows(selection, &model);
    char *folder = (d->import_case == DT_IMPORT_CAMERA)
      ? g_strdup("")
      : dt_conf_get_path("ui_last/import_last_directory");

    for(GList *path = paths; path; path = g_list_next(path))
    {
      GtkTreeIter iter;
      gtk_tree_model_get_iter(model, &iter, (GtkTreePath *)path->data);
      char *fullname;
      gtk_tree_model_get(model, &iter, DT_IMPORT_FILENAME, &fullname, -1);
      imgs = g_list_prepend(imgs, fullname);
    }
    g_list_free_full(paths, (GDestroyNotify)gtk_tree_path_free);

    if(imgs)
    {
      const gboolean unique = !imgs->next;
      imgs = g_list_reverse(imgs);
      char datetime_override[DT_DATETIME_LENGTH] = {0};
      if(d->import_case != DT_IMPORT_INPLACE)
      {
        const char *entry = gtk_entry_get_text(GTK_ENTRY(d->from.datetime));
        if(entry[0]
           && !dt_datetime_entry_to_exif(datetime_override,
                                         sizeof(datetime_override), entry))
        {
          dt_control_log(_("invalid override date/time format"));
          break;
        }
        dt_gui_preferences_string_reset(d->from.datetime);
      }
#ifdef HAVE_GPHOTO2
      if(d->import_case == DT_IMPORT_CAMERA)
      {
        dt_control_add_job(DT_JOB_QUEUE_USER_BG,
                           dt_camera_import_job_create(imgs, d->camera, datetime_override));
      }
      else
#endif
        dt_control_import(imgs, datetime_override, d->import_case == DT_IMPORT_INPLACE);

      if(d->import_case == DT_IMPORT_INPLACE)
      {
        if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->recursive)))
          dt_util_str_cat(&folder, "%%");
        _import_set_collection(folder);
        const dt_imgid_t imgid = dt_conf_get_int("ui_last/import_last_image");
        if(unique && dt_is_valid_imgid(imgid))
        {
          dt_control_set_mouse_over_id(imgid);
          dt_ctl_switch_mode_to("darkroom");
        }
      }
    }
    gtk_tree_selection_unselect_all(selection);
    if((d->import_case == DT_IMPORT_INPLACE)
       || !dt_conf_get_bool("ui_last/import_keep_open"))
    {
      g_free(folder);
      break;
    }
    g_free(folder);
  }

  _import_cancel(self);
}

static void _import_from_dialog_free(dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  d->from.event = 0;
  g_object_unref(d->from.eye);
  g_object_unref(d->from.store);
  if(d->import_case != DT_IMPORT_CAMERA)
  {
    GtkTreeModel *model = gtk_tree_view_get_model(d->from.folderview);
    g_object_unref(GTK_TREE_STORE(model));
  }
  // Destroy and quit
  gtk_widget_destroy(d->from.dialog);
}

static void _lib_import_from_callback(GtkWidget *widget, dt_lib_module_t* self)
{
  dt_lib_import_t *d = self->data;
  d->import_case = (widget == GTK_WIDGET(d->import_inplace))
    ? DT_IMPORT_INPLACE : DT_IMPORT_COPY;

  if(d->import_case == DT_IMPORT_COPY)
  {
    const gboolean setup = dt_conf_get_bool("setup_import_directory");
    if(setup == FALSE)
    {
      const gboolean understood = dt_gui_show_standalone_yes_no_dialog
        (_("import base directory"),
         _("before copying images to the darktable base directory make sure it is defined as you prefer."
           "\nfurther information can be found in the darktable manual."
           "\n\ninspect darktable preferences -> import."
           "\ncheck and possibly correct the 'base directory naming pattern'"),
         _("_come back & check"), _("_understood & done"));
      if(understood)
        dt_conf_set_bool("setup_import_directory", TRUE);
      else
        return;
    }
  }

#ifdef HAVE_GPHOTO2
  // on some systems, GPhoto2 is somewhat prone to crashing while
  // scanning for new devices; this manifests as a crash during long
  // import sessions.  So disable the scan while we're importing, even
  // though we aren't using GPhoto2 to do the importing
  struct dt_camctl_t *camctl = (dt_camctl_t *)darktable.camctl;
  camctl->import_ui = TRUE;
#endif
  _import_from_dialog_new(self);
  _import_from_dialog_run(self);
  _import_from_dialog_free(self);
#ifdef HAVE_GPHOTO2
  // OK to resume periodic scans for new GPhoto2 devices
  camctl->import_ui = FALSE;
#endif
}

#ifdef HAVE_GPHOTO2
static void _camera_detected(gpointer instance, gpointer self)
{
  /* update gui with detected devices */
  dt_lib_gui_queue_update(self);
}
#endif
#ifdef USE_LUA
static int lua_register_widget(lua_State *L)
{
  dt_lib_module_t *self = lua_touserdata(L, lua_upvalueindex(1));
  dt_lib_import_t *d = self->data;
  lua_widget widget;
  luaA_to(L,lua_widget,&widget,1);
  dt_lua_widget_bind(L,widget);
  gtk_box_pack_start(GTK_BOX(d->extra_lua_widgets),widget->widget, TRUE, TRUE, 0);
  return 0;
}

void init(dt_lib_module_t *self)
{
  lua_State *L = darktable.lua_state.state;
  int my_type = dt_lua_module_entry_get_type(L, "lib", self->plugin_name);
  lua_pushlightuserdata(L,self);
  lua_pushcclosure(L, lua_register_widget,1);
  dt_lua_gtk_wrap(L);
  lua_pushcclosure(L, dt_lua_type_member_common, 1);
  dt_lua_type_register_const_type(L, my_type, "register_widget");
}
#endif

void gui_init(dt_lib_module_t *self)
{
  /* initialize ui widgets */
  dt_lib_import_t *d = g_malloc0(sizeof(dt_lib_import_t));
  self->data = (void *)d;

  // add import buttons
  GtkWidget *widget = dt_action_button_new(self, N_("add to library..."),
                                           _lib_import_from_callback, self,
                                           _("add existing images to the library"), 0, 0);
  d->import_inplace = GTK_BUTTON(widget);
  gtk_widget_set_can_focus(widget, TRUE);
  gtk_widget_set_receives_default(widget, TRUE);

  widget = dt_action_button_new
    (self, N_("copy & import..."),
     _lib_import_from_callback, self,
     _("copy and optionally rename images before adding them to the library"
       "\npatterns can be defined to rename the images and specify the destination folders"),
     GDK_KEY_i, GDK_CONTROL_MASK | GDK_SHIFT_MASK);
  gtk_widget_set_can_focus(widget, TRUE);
  gtk_widget_set_receives_default(widget, TRUE);

  self->widget = dt_gui_vbox(dt_gui_hbox(d->import_inplace, widget));
  
#ifdef HAVE_GPHOTO2
  /* add devices container for cameras */
  d->devices = GTK_BOX(dt_gui_vbox());
  dt_gui_box_add(self->widget, d->devices);

  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_CAMERA_DETECTED, _camera_detected);
  dt_action_define(DT_ACTION(self), NULL, _import_text[DT_IMPORT_CAMERA], NULL, &dt_action_def_button);
  dt_action_define(DT_ACTION(self), NULL, _import_text[DT_IMPORT_TETHER], NULL, &dt_action_def_button);
  dt_action_define(DT_ACTION(self), NULL, N_("mount camera"), NULL, &dt_action_def_button);
  dt_action_define(DT_ACTION(self), NULL, N_("unmount camera"), NULL, &dt_action_def_button);
#endif

  // collapsible section

  _expander_create(&d->cs, GTK_BOX(self->widget),
                   _("parameters"), "ui_last/expander_import", self);

  GtkGrid *grid = GTK_GRID(gtk_grid_new());
  gtk_grid_set_column_spacing(grid, DT_PIXEL_APPLY_DPI(5));
  guint line = 0;
  d->ignore_exif = dt_gui_preferences_bool(grid, "ui_last/ignore_exif_rating", 0,
                                           line++, FALSE);
  d->rating = dt_gui_preferences_int(grid, "ui_last/import_initial_rating", 0, line++);
  d->apply_metadata = dt_gui_preferences_bool(grid, "ui_last/import_apply_metadata", 0,
                                              line++, FALSE);
  d->metadata.apply_metadata = d->apply_metadata;
  dt_gui_box_add(d->cs.container, grid);
  d->metadata.box = GTK_WIDGET(d->cs.container);
  dt_import_metadata_init(&d->metadata);

#ifdef USE_LUA
  /* initialize the lua area and make sure it survives its parent's destruction*/
  d->extra_lua_widgets = gtk_box_new(GTK_ORIENTATION_VERTICAL,5);
  g_object_ref_sink(d->extra_lua_widgets);
  dt_gui_box_add(d->cs.container, d->extra_lua_widgets);
  gtk_container_foreach(GTK_CONTAINER(d->extra_lua_widgets), reset_child, NULL);
#endif

  gtk_widget_show_all(self->widget);
  gtk_widget_set_no_show_all(self->widget, TRUE);

  dt_gui_update_collapsible_section(&d->cs);
}

void gui_cleanup(dt_lib_module_t *self)
{
  dt_lib_import_t *d = self->data;
#ifdef USE_LUA
  detach_lua_widgets(d->extra_lua_widgets);
#endif
  dt_import_metadata_cleanup(&d->metadata);
  /* cleanup mem */
  g_free(self->data);
  self->data = NULL;
}

const struct
{
  char *key;
  char *name;
  int type;
} _pref[] = {
  {"ui_last/import_ignore_nonraws",     "ignore_nonraws",     DT_BOOL},
  {"ui_last/import_apply_metadata",     "apply_metadata",     DT_BOOL},
  {"ui_last/import_recursive",          "recursive",          DT_BOOL},
  {"ui_last/ignore_exif_rating",        "ignore_exif_rating", DT_BOOL},
  {"session/use_filename",              "use_filename",       DT_BOOL},
  {"session/base_directory_pattern",    "base_pattern",       DT_STRING},
  {"session/sub_directory_pattern",     "sub_pattern",        DT_STRING},
  {"session/filename_pattern",          "filename_pattern",   DT_STRING},
  {"ui_last/import_initial_rating",     "rating",             DT_INT}
};
static const guint pref_n = G_N_ELEMENTS(_pref);

static int _get_key_index(const char *name)
{
  if(!name || !name[0]) return -1;
  for(int i = 0; i < pref_n; i++)
    if(!g_strcmp0(name, _pref[i].name))
      return i;
  return -1;
}

static void _set_default_preferences(dt_lib_module_t *self)
{
  for(int i = 0; i < pref_n; i++)
  {
    switch(_pref[i].type)
    {
      case DT_BOOL:
      {
        const gboolean default_bool = dt_confgen_get_bool(_pref[i].key, DT_DEFAULT);
        dt_conf_set_bool(_pref[i].key, default_bool);
        break;
      }
      case DT_INT:
      {
        const int default_int = dt_confgen_get_int(_pref[i].key, DT_DEFAULT);
        dt_conf_set_int(_pref[i].key, default_int);
        break;
      }
      case DT_STRING:
      {
        const char *default_char = dt_confgen_get(_pref[i].key, DT_DEFAULT);
        dt_conf_set_string(_pref[i].key, default_char);
        break;
      }
    }
  }
  // metadata
  dt_pthread_mutex_lock(&darktable.metadata_threadsafe);
  for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next)
  {
    dt_metadata_t *metadata = iter->data;
    if(!metadata->internal)
    {
      const char *metadata_name = dt_metadata_get_tag_subkey(metadata->tagname);
      char *setting = g_strdup_printf("plugins/lighttable/metadata/%s_flag", metadata_name);
      const uint32_t flag = (dt_conf_get_int(setting) | DT_METADATA_FLAG_IMPORTED);
      dt_conf_set_int(setting, flag);
      g_free(setting);
      setting = g_strdup_printf("ui_last/import_last_%s", metadata_name);
      dt_conf_set_string(setting, "");
      g_free(setting);
    }
  }
  dt_pthread_mutex_unlock(&darktable.metadata_threadsafe);

  // tags
  {
    dt_conf_set_bool("ui_last/import_last_tags_imported", TRUE);
    dt_conf_set_string("ui_last/import_last_tags", "");
  }
}

static char *_get_current_configuration(dt_lib_module_t *self)
{
  char *pref = NULL;
  for(int i = 0; i < pref_n; i++)
  {
    if(_pref[i].type == DT_BOOL)
    {
      dt_util_str_cat(&pref, "%s=%d,", _pref[i].name,
                             dt_conf_get_bool(_pref[i].key) ? 1 : 0);
    }
    else if(_pref[i].type == DT_INT)
    {
      dt_util_str_cat(&pref, "%s=%d,", _pref[i].name,
                             dt_conf_get_int(_pref[i].key));
    }
    else if(_pref[i].type == DT_STRING)
    {
      const char *s = dt_conf_get_string_const(_pref[i].key);
      dt_util_str_cat(&pref, "%s=%s,", _pref[i].name, s);
    }
  }

  dt_pthread_mutex_lock(&darktable.metadata_threadsafe);
  for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next)
  {
    dt_metadata_t *metadata = iter->data;

    if(!metadata->internal)
    {
      const char *metadata_name = dt_metadata_get_tag_subkey(metadata->tagname);
      gchar *setting = g_strdup_printf("plugins/lighttable/metadata/%s_flag",
                                       metadata_name);
      const gboolean imported = dt_conf_get_int(setting) & DT_METADATA_FLAG_IMPORTED;
      g_free(setting);

      setting = g_strdup_printf("ui_last/import_last_%s", metadata_name);
      const char *metadata_value = dt_conf_get_string_const(setting);
      dt_util_str_cat(&pref, "%s=%d%s,", metadata_name,
                             imported ? 1 : 0, metadata_value);
      g_free(setting);
    }
  }
  dt_pthread_mutex_unlock(&darktable.metadata_threadsafe);

  // must be the last (comma separated list)
  const gboolean imported = dt_conf_get_bool("ui_last/import_last_tags_imported");
  const char *tags_value = dt_conf_get_string_const("ui_last/import_last_tags");
  dt_util_str_cat(&pref, "%s=%d%s,", "tags", imported ? 1 : 0, tags_value);
  if(pref && *pref)
    pref[strlen(pref) - 1] = '\0';

  return pref;
}

static void _apply_preferences(const char *pref,
                               dt_lib_module_t *self)
{
  if(!pref || !pref[0]) return;
  _set_default_preferences(self);

  // set the presets
  // TODO: splitting on "," fails if the user has entered a metadata value with a comma!
  GList *prefs = dt_util_str_to_glist(",", pref);
  for(GList *iter = prefs; iter; iter = g_list_next(iter))
  {
    char *value = g_strstr_len(iter->data, -1, "=");
    if(!value) continue;
    value[0] = '\0';
    value++;
    char *metadata_name = (char *)iter->data;
    const int i = _get_key_index(metadata_name);
    if(i != -1)
    {
      // standard preferences
      if(_pref[i].type == DT_BOOL)
      {
        dt_conf_set_bool(_pref[i].key, (value[0] == '1') ? TRUE : FALSE);
      }
      else if(_pref[i].type == DT_INT)
      {
        dt_conf_set_int(_pref[i].key, (int)atol(value));
      }
      else if(_pref[i].type == DT_STRING)
      {
        dt_conf_set_string(_pref[i].key, value);
      }
    }
    else if(g_strcmp0(metadata_name, "tags"))
    {
      // metadata
      gchar *setting = g_strdup_printf("plugins/lighttable/metadata/%s_flag",
                                       metadata_name);
      const uint32_t flag = (dt_conf_get_int(setting) & ~DT_METADATA_FLAG_IMPORTED) |
                            ((value[0] == '1') ? DT_METADATA_FLAG_IMPORTED : 0);
      dt_conf_set_int(setting, flag);
      g_free(setting);
      value++;
      setting = g_strdup_printf("ui_last/import_last_%s", metadata_name);
      dt_conf_set_string(setting, value);
      g_free(setting);
    }
    else
    {
      // tags
      if(value[0] == '0' || value[0] == '1')
      {
        dt_conf_set_bool("ui_last/import_last_tags_imported", (value[0] == '1'));
        value++;
      }
      else
        dt_conf_set_bool("ui_last/import_last_tags_imported", TRUE);
      // get all the tags back - ugly but allow to keep readable presets
      char *tags = g_strdup(value);
      for(GList *iter2 = g_list_next(iter); iter2; iter2 = g_list_next(iter2))
      {
        if(strlen((char *)iter2->data))
          dt_util_str_cat(&tags, ",%s", (char *)iter2->data);
      }
      dt_conf_set_string("ui_last/import_last_tags", tags);
      g_free(tags);
      break;  // must be the last setting
    }
  }
  g_list_free_full(prefs, g_free);

  dt_lib_import_t *d = self->data;
  dt_gui_preferences_bool_update(d->ignore_exif);
  dt_gui_preferences_int_update(d->rating);
  dt_gui_preferences_bool_update(d->apply_metadata);
  dt_import_metadata_update(&d->metadata);
}

void init_presets(dt_lib_module_t *self)
{
}

void *get_params(dt_lib_module_t *self,
                 int *size)
{
  *size = 0;
  char *params = _get_current_configuration(self);
  if(params)
    *size =strlen(params) + 1;
  return params;
}

int set_params(dt_lib_module_t *self,
               const void *params,
               int size)
{
  if(!params) return 1;
  _apply_preferences(params, self);
  return 0;
}

// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
// clang-format on