/*
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 .
*/
#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
#include
// ugh, ugly hack. why do people break stuff all the time?
#ifndef RSVG_CAIRO_H
#include
#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(" %s",_("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, ¤t_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