Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
import.c
Go to the documentation of this file.
1/*
2 This file is part of the Ansel project.
3 Copyright (C) 2023-2024 Alynx Zhou.
4 Copyright (C) 2023-2026 Aurélien PIERRE.
5 Copyright (C) 2023-2025 Guillaume Stutin.
6 Copyright (C) 2023 lologor.
7 Copyright (C) 2023 Luca Zulberti.
8
9 Ansel is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 3 of the License, or
12 (at your option) any later version.
13
14 Ansel is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
21*/
22
23#include "system/mem_alloc.h"
24#include "common/paths.h" // DT_PATH_MAX
25#include "common/film.h"
28#include "metadata/exif.h"
29#include "gui/import.h"
30#include "common/image.h"
31#include "caches/image_cache.h"
34#include "metadata/metadata.h"
35#include "common/datetime.h"
36#include "common/conf.h"
37#include "control/control.h"
38#include "control/signal.h"
39#include "control/jobs/control_jobs.h" // dt_control_image_enumerator_t
41
42#include "widgets/gtkentry.h"
43
44#include <gio/gio.h>
45
46#ifdef GDK_WINDOWING_QUARTZ
47#include "osx/osx.h"
48#endif
49#ifdef _WIN32
50//MSVCRT does not have strptime implemented
51#endif
52#include <librsvg/rsvg.h>
53#include "common/utility.h"
54#include "common/logging.h"
55#include "gui/application.h"
57#include "widgets/popup.h"
60// ugh, ugly hack. why do people break stuff all the time?
61#ifndef RSVG_CAIRO_H
62#include <librsvg/rsvg-cairo.h>
63#endif
64
65
67{
68 dt_pthread_mutex_t lock;
69 uint32_t generation;
70 uint32_t refcount;
71 gboolean closing;
73
74// dt_import_filter_type_t (gui/import.h) mirrors the 3 GtkFileFilter entries built by
75// _file_filters(). The recursive folder scan runs on a worker thread and must not touch
76// GtkFileFilter (Gtk objects are not thread-safe), so the active filter is resolved to one of
77// those plain values on the GUI thread, once, in dt_import_init().
78
79typedef struct dt_import_t {
80 // User-selected folders and files from the Gtk file chooser,
81 // referenced by basename.
82 GSList *selection;
83
84 // List of GFiles to import, built recursively by traversing the user selection
85 GList *files;
86
87 // Generation snapshot captured when this job starts.
88 uint32_t generation;
89
90 // Number of elements in the list
91 uint32_t elements;
92
93 // Job-local lock. Do not alias dialog state because the dialog can be destroyed
94 // while background jobs are still running.
95 dt_pthread_mutex_t lock;
96
98
99 // Active GUI file-type filter (All/Raw/Raster), snapshotted on the GUI thread.
101
102 // Number of folders that could not be enumerated (permission denied, I/O error, folder gone
103 // mid-scan...) during this recursive scan. See _recurse_folder(): GLib reports these through a
104 // GError that used to be silently discarded (NULL error arg), so a folder failure looked
105 // identical to "this folder is simply empty" -- no way to tell the user why an expected file
106 // didn't show up.
108
110
125
126
127typedef struct dt_lib_import_t
128{
132
138
143
144 // The 3 filters registered in the file chooser by _file_filters(), kept around so
145 // dt_import_init() can tell which one is currently active by pointer comparison.
146 GtkFileFilter *filter_all;
147 GtkFileFilter *filter_raw;
148 GtkFileFilter *filter_raster;
149
150 // Mirrors the last dt_import_t::filter_type resolved by dt_import_init(), so
151 // _filelist_changed_callback() (GUI thread, only gets files/elements/finished from the signal)
152 // can word the selected-files label according to the active filter.
154
155 gboolean closing;
156
157 dt_pthread_mutex_t lock;
158
160
162
164
165static dt_import_t *dt_import_init(dt_lib_import_t *d, const uint32_t generation);
166static void dt_import_cleanup(void *import);
167
168static dt_lib_import_t * _init();
169static void _cleanup(dt_lib_import_t *d);
170
171static void gui_init(dt_lib_import_t *d);
172static void gui_cleanup(dt_lib_import_t *d);
173
174static void _set_test_path(dt_lib_import_t *d, dt_image_t *img);
175
176static void _do_select_all(dt_lib_import_t *d);
177static void _do_select_none(dt_lib_import_t *d);
178static void _do_select_new(dt_lib_import_t *d);
179static gboolean _selection_changed_scan_trigger(gpointer user_data);
180
181static void _recurse_folder(GVfs *vfs, GFile *folder, dt_import_t *const import);
182
183static gboolean _scan_still_valid(dt_import_t *const import)
184{
185 gboolean valid = FALSE;
186 dt_pthread_mutex_lock(&import->scan_state->lock);
187 valid = !import->scan_state->closing && import->scan_state->generation == import->generation;
188 dt_pthread_mutex_unlock(&import->scan_state->lock);
189 return valid;
190}
191
192// one-liner to set GtkLabel text from non-constant text and free it straight away
193static void _gtk_label_set_and_free(GtkWidget *widget, gchar *label)
194{
195 gtk_label_set_text(GTK_LABEL(widget), label);
196 dt_free(label);
197}
198
199// dt_image_ext_is_gui_raw()/is_gui_raster() (common/image_extensions.c) are shared with
200// _file_filters() below so the GtkFileFilter patterns and this recursive-scan check can never
201// drift. Public (gui/import.h): the drag-and-drop folder-import path (gui/dtgtk/thumbtable.c)
202// reuses this exact function rather than re-deriving the same "does this file match" logic.
203gboolean dt_import_passes_filter(const dt_import_filter_type_t filter_type, const gchar *pathname)
204{
205 if(!dt_supported_image(pathname)) return FALSE;
206 if(filter_type == DT_IMPORT_FILTER_ALL) return TRUE;
207
208 const char *extension = g_strrstr(pathname, ".");
209 if(IS_NULL_PTR(extension)) return FALSE;
210 extension++;
211
214}
215
216static void _filter_document(GVfs *vfs, GFile *document, dt_import_t *import)
217{
218 if(!_scan_still_valid(import)) return;
219
220 gchar *pathname = g_file_get_path(document);
221
222 // Check that document is a real file (not directory) and it passes the type check defined by user in GUI filters.
223 // gtk_file_chooser_get_files() applies the filters on the first level of recursivity,
224 // so this test is only useful for the next levels if folders are selected at the first level.
225 // We must not call GtkFileFilter from worker threads because Gtk objects are not thread-safe,
226 // so import->filter_type (a plain enum snapshotted on the GUI thread, see dt_import_init())
227 // stands in for the live GtkFileFilter here.
228 if(pathname && g_file_test(pathname, G_FILE_TEST_IS_REGULAR) && dt_import_passes_filter(import->filter_type, pathname))
229 {
230 import->files = g_list_prepend(import->files, pathname);
231 // prepend is more efficient than append. Import control reorders alphabetically anyway.
232 pathname = NULL;
233 }
234 else if(pathname && g_file_test(pathname, G_FILE_TEST_IS_DIR))
235 {
236 _recurse_folder(vfs, document, import);
237 }
238
239 dt_free(pathname);
240}
241
242static void _report_scan_error(dt_import_t *const import, GFile *folder, GError *error)
243{
244 gchar *path = g_file_get_path(folder);
245 dt_print(DT_DEBUG_IMPORT, "[import] could not fully scan folder `%s': %s\n",
246 path ? path : "?", error->message);
247 dt_free(path);
248 g_error_free(error);
249 import->scan_errors++;
250}
251
252static void _recurse_folder(GVfs *vfs, GFile *folder, dt_import_t *const import)
253{
254 // Get subfolders and files from current folder
255 if(!_scan_still_valid(import)) return;
256
257 GError *error = NULL;
258 GFileEnumerator *files
259 = g_file_enumerate_children(folder, G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
260 G_FILE_QUERY_INFO_NONE, NULL, &error);
261 if(IS_NULL_PTR(files))
262 {
263 // e.g. permission denied, or the folder vanished mid-scan: previously silent (NULL error arg),
264 // indistinguishable from "this folder is simply empty".
265 if(error) _report_scan_error(import, folder, error);
266 return;
267 }
268
269 GFile *file = NULL;
270 GError *iter_error = NULL;
271 while(g_file_enumerator_iterate(files, NULL, &file, NULL, &iter_error))
272 {
273 // g_file_enumerator_iterate returns FALSE only on errors, not on end of enumeration.
274 // We need an ugly break here else infinite loop.
275 if(IS_NULL_PTR(file)) break;
276
277 // Shutdown ASAP
278 if(!_scan_still_valid(import))
279 {
280 g_object_unref(files);
281 return;
282 }
283
284 _filter_document(vfs, file, import);
285 // g_file_enumerator_iterate() returns transfer-none children owned by the enumerator.
286 // Unref happens when the enumerator advances or is destroyed.
287 file = NULL;
288 }
289
290 // A FALSE return above without a set error just means "iteration finished normally" -- only
291 // report when GLib actually set one.
292 if(iter_error) _report_scan_error(import, folder, iter_error);
293
294 g_object_unref(files);
295}
296
297static void _recurse_selection(GSList *selection, dt_import_t *const import)
298{
299 // Entry point of the file recursion : process user selection.
300 // GtkFileChooser gives us a GSList for selection, so we can't directly recurse from here
301 // since the import job expects a GList.
302
303 if(!_scan_still_valid(import) || IS_NULL_PTR(selection)) return;
304
305 GVfs *vfs = g_vfs_get_default();
306 for(GSList *uri = selection; uri; uri = g_slist_next(uri))
307 {
308 GFile *file = g_vfs_get_file_for_uri(vfs, (const char *)uri->data);
309 _filter_document(vfs, file, import);
310 g_object_unref(file);
311 }
312
313 import->files = g_list_sort(import->files, (GCompareFunc) g_strcmp0);
314}
315
316static int32_t dt_get_selected_files(dt_import_t *import)
317{
318 // Recurse through subfolders if any selected.
319 // Can be called directly from GUI thread without using a job,
320 // but that might freeze the GUI on large directories.
321
322 dt_pthread_mutex_lock(&import->lock);
323
324 // Get the new list
325 _recurse_selection(import->selection, import);
326 import->elements = (import->files) ? g_list_length(import->files) : 0;
327 gboolean valid = _scan_still_valid(import);
328
329 // If shutdown was triggered, we may already have no Gtk label widget to update through the callback.
330 // In that case, it will segfault. So don't raise the signal at all if shutdown was set.
331 if(valid)
332 {
333 dt_pthread_mutex_unlock(&import->lock);
334
335 // Raise even when import->files is empty (e.g. the active Raw/Raster filter matched
336 // nothing recursively): receivers need to know detection finished with zero results,
337 // otherwise the GUI label is stuck on "Detecting..." and clicking Import never fires
338 // _process_file_list, leaving the dialog looking frozen. scan_errors rides along so the
339 // label can report folders that failed to scan (permission denied, etc.) -- see
340 // _set_selected_files_label().
342 import->scan_errors);
343 // Signal receivers only observe this list. Ownership stays in dt_import_t and is released in dt_import_cleanup.
344 }
345 else if(import->files)
346 {
347 g_list_free_full(g_steal_pointer(&import->files), dt_free_gpointer);
348 import->files = NULL;
349 // no callback will be triggered. Free here.
350
351 dt_pthread_mutex_unlock(&import->lock);
352 }
353 else
354 {
355 dt_pthread_mutex_unlock(&import->lock);
356 }
357
358 return valid; // TRUE if completed without interruption
359}
360
365
366void dt_control_get_selected_files(dt_lib_import_t *d, gboolean destroy_window)
367{
368 if(d->closing || IS_NULL_PTR(d->scan_state)) return;
369
370 uint32_t generation = 0;
371 dt_pthread_mutex_lock(&d->scan_state->lock);
372 if(d->scan_state->closing)
373 {
374 dt_pthread_mutex_unlock(&d->scan_state->lock);
375 return;
376 }
377 d->scan_state->generation++;
378 generation = d->scan_state->generation;
379 dt_pthread_mutex_unlock(&d->scan_state->lock);
380
381 dt_job_t *job = dt_control_job_create(&_get_selected_files_job, "recursively detect files to import");
382 if(job)
383 {
384 dt_import_t *import = dt_import_init(d, generation);
385 if(IS_NULL_PTR(import))
386 {
388 return;
389 }
391 // Note : we don't free import->files. It's returned with the signal.
393 }
394}
395
396static GdkPixbuf *_import_get_thumbnail(const gchar *filename, const int width, const int height,
397 const gboolean valid_exif, dt_image_t *img)
398{
399 if(!filename || !g_file_test(filename, G_FILE_TEST_IS_REGULAR)) return NULL;
400
401 GdkPixbuf *pixbuf = NULL;
402 uint8_t *buffer = NULL;
403 int32_t th_width;
404 int32_t th_height;
405 char *mime_type = NULL;
406 const char *const extension = g_strrstr(filename, ".");
409 if(!dt_image_is_hdr(img)
410 && !dt_imageio_large_thumbnail(filename, &buffer, &th_width, &th_height, &color_space, width, height))
411 {
412 // Show the framing the camera recorded, not the wider frame it renders its previews from,
413 // so the import window matches what the lighttable and darkroom will show after import.
414 dt_boundingbox_t usercrop;
415 dt_image_get_usercrop(img, usercrop);
416 dt_imageio_crop_thumbnail(usercrop, buffer, &th_width, &th_height);
417
418 const float ratio = ((float)th_height) / ((float)th_width);
419
420 // Convert RGBa to RGB because GdkPixbuf doesn't do RGBa
422 th_width * th_height * 3 * sizeof(uint8_t),
423 0);
424 if(rgb)
425 {
427 for(size_t k = 0; k < th_width * th_height; k++)
428 {
429 const float alpha = buffer[k * 4 + 3] > 0 ? buffer[k * 4 + 3] / 255.0f : 1.0f;
430 rgb[k * 3] = CLAMP((int)roundf((buffer[k * 4] / 255.0f * alpha + (1.0f - alpha)) * 255.0f), 0, 255);
431 rgb[k * 3 + 1] = CLAMP((int)roundf((buffer[k * 4 + 1] / 255.0f * alpha + (1.0f - alpha)) * 255.0f), 0, 255);
432 rgb[k * 3 + 2] = CLAMP((int)roundf((buffer[k * 4 + 2] / 255.0f * alpha + (1.0f - alpha)) * 255.0f), 0, 255);
433 }
434
435 // Build the actual pixbuf object
436 GdkPixbuf *tmp = gdk_pixbuf_new_from_data(rgb, 0, FALSE, 8, th_width, th_height,
437 th_width * 3 * sizeof(uint8_t), NULL, NULL);
438 if(tmp)
439 {
440 pixbuf = gdk_pixbuf_scale_simple(tmp, roundf((float)width / ratio), height, GDK_INTERP_HYPER);
441 g_object_unref(tmp);
442 }
443 }
444
447 dt_free(mime_type);
448 }
449
450 if(IS_NULL_PTR(pixbuf))
451 {
452 const gboolean use_internal_loader = !(file_type & DT_IMAGE_RAW);
453
454 if(use_internal_loader)
455 {
456 dt_mipmap_buffer_t mipbuf = { 0 };
457
458 /* If embedded preview extraction failed, non-RAW files should still get a preview by
459 * decoding the real image through Ansel instead of relying on the desktop pixbuf stack.
460 * RAWs stay excluded here because the import dialog only wants a lightweight fallback. */
461 if(dt_imageio_open_standalone(img, filename, &mipbuf) == DT_IMAGEIO_OK
462 && !IS_NULL_PTR(mipbuf.buf) && mipbuf.width > 0 && mipbuf.height > 0)
463 {
464 const size_t pixels = (size_t)mipbuf.width * mipbuf.height;
465 uint8_t *rgb = dt_pixelpipe_cache_alloc_align_cache(pixels * 3 * sizeof(uint8_t), 0);
466 if(!IS_NULL_PTR(rgb))
467 {
468 const float *const in = (const float *const)mipbuf.buf;
470 for(size_t k = 0; k < pixels; k++)
471 {
472 const float alpha = in[k * 4 + 3] > 0.0f ? CLAMPF(in[k * 4 + 3], 0.0f, 1.0f) : 1.0f;
473 rgb[k * 3] = CLAMP((int)roundf((CLAMPF(in[k * 4], 0.0f, 1.0f) * alpha + (1.0f - alpha)) * 255.0f), 0, 255);
474 rgb[k * 3 + 1] = CLAMP((int)roundf((CLAMPF(in[k * 4 + 1], 0.0f, 1.0f) * alpha + (1.0f - alpha)) * 255.0f), 0, 255);
475 rgb[k * 3 + 2] = CLAMP((int)roundf((CLAMPF(in[k * 4 + 2], 0.0f, 1.0f) * alpha + (1.0f - alpha)) * 255.0f), 0, 255);
476 }
477
478 GdkPixbuf *tmp = gdk_pixbuf_new_from_data(rgb, 0, FALSE, 8, mipbuf.width, mipbuf.height,
479 mipbuf.width * 3 * sizeof(uint8_t), NULL, NULL);
480 if(!IS_NULL_PTR(tmp))
481 {
482 const float ratio = (float)mipbuf.height / (float)mipbuf.width;
483 pixbuf = gdk_pixbuf_scale_simple(tmp, roundf((float)width / ratio), height, GDK_INTERP_HYPER);
484 g_object_unref(tmp);
485 }
486
488 }
489 }
490
492 }
493 }
494
495 // Fallback to whatever Gtk found in the file
496 if(IS_NULL_PTR(pixbuf))
497 pixbuf = gdk_pixbuf_new_from_file_at_size(filename, width, height, NULL);
498
499 if(IS_NULL_PTR(pixbuf)) return NULL;
500
501 // Rotate the image to the correct orientation
502 GdkPixbuf *tmp = pixbuf;
504 tmp = gdk_pixbuf_rotate_simple(pixbuf, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE);
506 tmp = gdk_pixbuf_rotate_simple(pixbuf, GDK_PIXBUF_ROTATE_CLOCKWISE);
508 tmp = gdk_pixbuf_rotate_simple(pixbuf, GDK_PIXBUF_ROTATE_UPSIDEDOWN);
509
510 if(pixbuf != tmp) g_object_unref(pixbuf);
511
512 return tmp;
513}
514
516{
517 gchar basedir[DT_PATH_MAX] = { 0 };
518 g_strlcpy(basedir, dt_conf_get_string_const("session/base_directory_pattern"), sizeof(basedir));
519
520 if(*basedir == 0 && dt_get_user_pictures_dir(dt_loc_get_home_dir(NULL), basedir, sizeof(basedir)))
521 {
522 // Basedir is empty
523 dt_conf_set_string("session/base_directory_pattern", basedir);
524 }
525 else if(strstr(basedir, "$(") != NULL)
526 {
527 // Basedir contains a pattern to expand - remnant of Darktable's defaults
528 dt_variables_params_t *params;
530
531 gchar *file_expand = dt_variables_expand(params, basedir, FALSE);
532 dt_conf_set_string("session/base_directory_pattern", file_expand);
533
534 dt_free(file_expand);
536 }
537}
538
540{
542}
543
548
550{
552}
553
554
555static void _resize_dialog(GtkWidget *widget)
556{
557 GtkAllocation allocation;
558 gtk_widget_get_allocation(widget, &allocation);
559 dt_conf_set_int("ui_last/import_dialog_width", allocation.width);
560 dt_conf_set_int("ui_last/import_dialog_height", allocation.height);
561}
562
563static void _build_filter(GtkFileFilter *filter, const gchar *extension)
564{
565 gchar *text = g_strdup_printf("*.%s", extension);
566 gchar *TEXT = g_utf8_strup(text, -1); // uppercase variant
567 gtk_file_filter_add_pattern(filter, text);
568 gtk_file_filter_add_pattern(filter, TEXT);
569 dt_free(text);
570 dt_free(TEXT);
571}
572
573/* Add file extension patterns for file chooser filters
574* Bloody GTK doesn't support regex patterns so we need to unroll
575* every combination separately, for lowercase and uppercase.
576*/
578{
579 GtkWidget *file_chooser = d->file_chooser;
580 GtkFileFilter *filter;
581
582 // Enumerate every extension Ansel knows about (common/image_extensions.c) instead of
583 // hand-maintaining a copy here -- keeps this GUI filter and the recursive-scan check in
584 // dt_import_passes_filter() from ever drifting apart again.
585 const char *const *lists[] = { dt_image_ext_raw_list(), dt_image_ext_ldr_list(), dt_image_ext_hdr_list() };
586 const int n_lists = sizeof(lists) / sizeof(lists[0]);
587
588 /* ALL IMAGES */
589 filter = gtk_file_filter_new();
590 gtk_file_filter_set_name(filter, _("All image files"));
591 for(int l = 0; l < n_lists; l++)
592 for(const char *const *i = lists[l]; *i; i++)
593 _build_filter(filter, *i);
594
595 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(file_chooser), filter);
596
597 // Set ALL IMAGES as default
598 gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(file_chooser), filter);
599 d->filter_all = filter;
600
601 /* RAW ONLY */
602 filter = gtk_file_filter_new();
603 gtk_file_filter_set_name(filter, _("Raw image files"));
604 for(int l = 0; l < n_lists; l++)
605 for(const char *const *i = lists[l]; *i; i++)
607 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(file_chooser), filter);
608 d->filter_raw = filter;
609
610 /* RASTER ONLY */
611 filter = gtk_file_filter_new();
612 gtk_file_filter_set_name(filter, _("Raster image files"));
613 for(int l = 0; l < n_lists; l++)
614 for(const char *const *i = lists[l]; *i; i++)
616
617 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(file_chooser), filter);
618 d->filter_raster = filter;
619}
620
621static GtkWidget * _attach_aligned_grid_item(GtkWidget *grid, const int row, const int column,
622 const char *label, const GtkAlign align, const gboolean fixed_width,
623 const gboolean full_width)
624{
625 GtkWidget *w = gtk_label_new(label);
626 if(fixed_width)
627 gtk_label_set_max_width_chars(GTK_LABEL(w), 25);
628
629 gtk_label_set_ellipsize(GTK_LABEL(w), PANGO_ELLIPSIZE_END);
630 gtk_grid_attach(GTK_GRID(grid), w, column, row, full_width ? 2 : 1, 1);
631 gtk_label_set_xalign(GTK_LABEL(w), align);
632 gtk_widget_set_halign(w, align);
633 gtk_label_set_line_wrap(GTK_LABEL(w), TRUE);
634 return w;
635}
636
637static GtkWidget * _attach_grid_separator(GtkWidget *grid, const int row, const int length)
638{
639 GtkWidget *w = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
640 gtk_grid_attach(GTK_GRID(grid), w, 0, row, length, 1);
641 dt_gui_add_class(w, "grid-separator");
642 return w;
643}
644
645static int _is_in_library_by_path(const gchar *folder, const char *filename)
646{
647 int32_t filmroll_id = dt_film_get_id(folder);
648 int32_t image_id = dt_image_get_id(filmroll_id, filename);
649 return image_id;
650}
651
652static int _is_in_library_by_metadata(GFile *file)
653{
654 GError *error = NULL;
655 GFileInfo *info = g_file_query_info(file,
656 G_FILE_ATTRIBUTE_STANDARD_NAME ","
657 G_FILE_ATTRIBUTE_TIME_MODIFIED,
658 G_FILE_QUERY_INFO_NONE, NULL, &error);
659 if(IS_NULL_PTR(info))
660 {
661 if(error) g_error_free(error);
662 return 0;
663 }
664
665 const guint64 datetime = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
666 char dtid[DT_DATETIME_EXIF_LENGTH];
667 dt_datetime_unix_to_exif(dtid, sizeof(dtid), (const time_t *)&datetime);
668 const int res = dt_metadata_already_imported(g_file_info_get_name(info), dtid);
669 g_object_unref(info);
670 return res;
671}
672
673static void _exif_text_set_and_free(dt_lib_import_t *d, exif_fields_t field, gchar *label)
674{
675 _gtk_label_set_and_free(d->exif_info[field], label);
676}
677
678static void update_preview_cb(GtkFileChooser *file_chooser, gpointer userdata)
679{
680 dt_lib_import_t *d = (dt_lib_import_t *)userdata;
681 if(d->closing) return;
682 gchar *uri = gtk_file_chooser_get_preview_uri(file_chooser);
683 if(IS_NULL_PTR(uri))
684 {
685 gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
686 return; // nothing to do, nothing to free.
687 }
688
689 GVfs *vfs = g_vfs_get_default();
690 GFile *in = g_vfs_get_file_for_uri(vfs, (const char *)uri);
691 char *filename = g_file_get_path(in);
692
693 gboolean have_file = (!IS_NULL_PTR(filename)) && g_file_test(filename, G_FILE_TEST_IS_REGULAR);
694 gtk_file_chooser_set_preview_widget_active(file_chooser, have_file);
695
696 dt_image_t *img = NULL;
697 int valid_exif = 0;
698 if(have_file)
699 {
700 const char *const extension = g_strrstr(filename, ".");
702
703 dt_free(d->path_file);
704 d->path_file = g_strdup(filename);
705
706 img = dt_alloc_align(sizeof(dt_image_t)); // dt_image_t is 64-aligned, see #1212
707 dt_image_init(img);
708 if(!(file_type & DT_IMAGE_HDR))
709 valid_exif = dt_exif_read(img, filename);
710 else
711 valid_exif = 1;
712 _set_test_path(d, img);
713 }
714 else
715 {
716 g_object_unref(in);
717 dt_free(filename);
718 dt_free(uri);
719 return;
720 }
721
722 /* Get the thumbnail */
723 // 160x120 px seems a reasonably generic size for small thumbs from RAW files
724 if(!dt_conf_get_bool("import/disable_thumbnail"))
725 {
726 GdkPixbuf *pixbuf = _import_get_thumbnail(filename, (int) DT_PIXEL_APPLY_DPI(180), (int) DT_PIXEL_APPLY_DPI(180), valid_exif, img);
727 gtk_image_set_from_pixbuf(GTK_IMAGE(d->preview), pixbuf);
728 if(pixbuf) g_object_unref(pixbuf);
729 }
730
731 gtk_widget_show_all(d->preview);
732
733
734 // Reset everything
735 gtk_label_set_text(GTK_LABEL(d->exif_info[EXIF_DATETIME_FIELD]), "");
736 gtk_label_set_text(GTK_LABEL(d->exif_info[EXIF_MODEL_FIELD]), "");
737 gtk_label_set_text(GTK_LABEL(d->exif_info[EXIF_MAKER_FIELD]), "");
738 gtk_label_set_text(GTK_LABEL(d->exif_info[EXIF_LENS_FIELD]), "");
739 gtk_label_set_text(GTK_LABEL(d->exif_info[EXIF_FOCAL_LENS_FIELD]), "");
740 gtk_label_set_text(GTK_LABEL(d->exif_info[EXIF_EXPOSURE_FIELD]), "");
741 gtk_label_set_text(GTK_LABEL(d->exif_info[EXIF_INLIB_FIELD]), _("No"));
742 gtk_label_set_text(GTK_LABEL(d->exif_info[EXIF_PATH_FIELD]), "");
743
744 /* Do we already have this picture in library ? */
745 gchar *folder = dt_util_path_get_dirname(filename);
746 gchar *basename = g_file_get_basename(in);
747 const int is_path_in_lib = _is_in_library_by_path(folder, basename);
748 const int is_metadata_in_lib = _is_in_library_by_metadata(in);
749 g_object_unref(in);
750 const gboolean is_in_lib = (is_path_in_lib > -1) || (is_metadata_in_lib > -1);
752 dt_free(basename);
753
754 /* If alread imported, find out where */
755 int32_t imgid = UNKNOWN_IMAGE;
756 if(is_path_in_lib > -1)
757 imgid = is_path_in_lib;
758 else if(is_metadata_in_lib > -1)
759 imgid = is_metadata_in_lib;
760
761 char path[512] = { 0 };
762 if(imgid > UNKNOWN_IMAGE)
763 {
764 dt_image_t *lib_img = dt_image_cache_get(imgid, 'r');
765 if(lib_img)
766 {
767 dt_image_film_roll_directory(lib_img, path, sizeof(path));
769 }
770 }
771
772 /* Get EXIF info */
773 if(!valid_exif)
774 {
775 char datetime[200];
776 const gboolean valid = dt_datetime_img_to_local(datetime, sizeof(datetime), img, FALSE);
777 gchar *exposure = dt_util_format_exposure(img->exif_exposure);
778 gchar *exposure_field = g_strdup_printf("%.0f ISO - f/%.1f - %s", img->exif_iso, img->exif_aperture, exposure);
779 dt_free(exposure);
780 _exif_text_set_and_free(d, EXIF_DATETIME_FIELD, g_strdup_printf(" %s", valid ? datetime : "-"));
781 _exif_text_set_and_free(d, EXIF_MODEL_FIELD, g_strdup_printf(" %s", (img->exif_model[0] != '\0') ? img->exif_model : "-"));
782 _exif_text_set_and_free(d, EXIF_MAKER_FIELD, g_strdup_printf(" %s", (img->exif_maker[0] != '\0') ? img->exif_maker : "-"));
783 _exif_text_set_and_free(d, EXIF_LENS_FIELD, g_strdup_printf(" %s", (img->exif_lens[0] != '\0') ? img->exif_lens : "-"));
784 _exif_text_set_and_free(d, EXIF_FOCAL_LENS_FIELD, g_strdup_printf(" %0.f mm", img->exif_focal_length));
786 _exif_text_set_and_free(d, EXIF_INLIB_FIELD, (is_in_lib) ? g_strdup_printf(_(" Yes (ID %i), in"), imgid) : g_strdup_printf(_(" No")));
787
788 if(is_in_lib && path[0] != '\0') _exif_text_set_and_free(d, EXIF_PATH_FIELD, g_strdup_printf(_("%s"), path));
789 }
790
791 dt_free(filename);
792 dt_free(uri);
793 dt_free_align(img);
794}
795
796static void _update_directory(GtkWidget *file_chooser, dt_lib_import_t *d)
797{
798 gchar *path = gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(file_chooser));
799 dt_conf_set_string("ui_last/import_last_directory", path);
800 dt_free(path);
801}
802
803static void _set_help_string(dt_lib_import_t *d, gboolean copy)
804{
805 if(copy)
806 gtk_label_set_markup(
807 GTK_LABEL(d->help_string),
808 _("<i>The files will be copied to the selected destination. You can rename them in batch below:</i>"));
809 else
810 gtk_label_set_markup(
811 GTK_LABEL(d->help_string),
812 _("<i>The files will stay at their original location</i>"));
813}
814
816{
817 if(IS_NULL_PTR(d->path_file) || IS_NULL_PTR(d->path_file))
818 return;
819
820 const gboolean duplicate = dt_conf_get_bool("ui_last/import_copy");
821 if(!duplicate)
822 {
823 gtk_label_set_text(GTK_LABEL(d->test_path), _("No copy."));
824 return;
825 }
826
827 char datetime_override[DT_DATETIME_LENGTH] = { 0 };
828 const char *date = gtk_entry_get_text(GTK_ENTRY(d->datetime));
829 GList *file = g_list_prepend(NULL, g_strdup(d->path_file));
830
831 if(date[0] && !dt_datetime_entry_to_exif(datetime_override, sizeof(datetime_override), date))
832 {
833 dt_control_log(_("invalid date/time format for import"));
834 return;
835 }
836
837 if(IS_NULL_PTR(file->data) || !dt_supported_image(file->data))
838 {
839 gtk_label_set_text(GTK_LABEL(d->test_path), _("Choose a file to see the result..."));
840 return;
841 }
842 else
843 {
844 gchar *basedir = dt_conf_get_string("session/base_directory_pattern");
845 dt_control_import_t data = {.imgs = file,
846 .datetime = dt_string_to_datetime(date),
847 .copy = 1,
848 .jobcode = dt_conf_get_string("ui_last/import_jobcode"),
849 .base_folder = basedir,
850 .target_subfolder_pattern = dt_conf_get_string("session/sub_directory_pattern"),
851 .target_file_pattern = dt_conf_get_string("session/filename_pattern"),
852 .target_dir = NULL,
853 .elements = 1,
854 .discarded = NULL,
855 };
856
857 gboolean free_after = FALSE;
858 if(IS_NULL_PTR(img))
859 {
860 img = dt_alloc_align(sizeof(dt_image_t)); // dt_image_t is 64-aligned, see #1212
861 dt_image_init(img);
862
863 // Generate file I/O only if the pattern is using EXIF variables.
864 // Otherwise, discard it since it's really expensive if the file is on external/remote storage.
865 // This is mandatory BEFORE expanding variables in pattern
866 if(strstr(data.target_file_pattern, "$(EXIF") != NULL
867 || strstr(data.target_subfolder_pattern, "$(EXIF") != NULL )
868 dt_exif_read(img, (const char*)file->data);
869
870 free_after = TRUE;
871 }
872
873 gchar *_path = dt_build_filename_from_pattern((const char *const)file->data, 1, img, &data);
874 gchar * cut = g_strdup(g_strrstr(basedir, G_DIR_SEPARATOR_S));
875 gchar *fake_path = g_strdup(g_strrstr(_path, cut));
876
877 if(free_after)
878 {
879 dt_free_align(img);
880 }
881
882 if(fake_path && fake_path[0] != 0)
883 _gtk_label_set_and_free(d->test_path, g_strdup_printf(_("...%s"), fake_path));
884 else
885 gtk_label_set_text(GTK_LABEL(d->test_path), _("Can't build a valid path."));
886
887 dt_free(cut);
888 dt_free(_path);
889 dt_free(fake_path);
891 }
892}
893
894// Words the selected-files label according to the active GUI filter (raw/raster get called out
895// explicitly; "All" is left exactly as before, per the user's request not to touch that case).
896// When scan_errors > 0, appends a warning so a permission-denied (or otherwise failed) subfolder
897// is visible right in the label instead of only in a transient dt_control_log() toast.
898static void _set_selected_files_label(dt_lib_import_t *d, const guint elements, const guint scan_errors)
899{
900 gchar *text;
901
902 if(elements == 0)
903 {
904 switch(d->last_filter_type)
905 {
907 text = g_strdup(_("No raw file selected"));
908 break;
910 text = g_strdup(_("No raster file selected"));
911 break;
912 default:
913 text = g_strdup(_("No file selected"));
914 break;
915 }
916 }
917 else
918 {
919 switch(d->last_filter_type)
920 {
922 text = g_strdup_printf(_("%i raw files selected"), elements);
923 break;
925 text = g_strdup_printf(_("%i raster files selected"), elements);
926 break;
927 default:
928 text = g_strdup_printf(_("%i files selected"), elements);
929 break;
930 }
931 }
932
933 if(scan_errors > 0)
934 {
935 gchar *warning = g_strdup_printf(ngettext(" -- %u folder could not be scanned (permission denied?)",
936 " -- %u folders could not be scanned (permission denied?)",
937 scan_errors), scan_errors);
938 gchar *combined = g_strconcat(text, warning, NULL);
939 dt_free(text);
940 dt_free(warning);
941 text = combined;
942 }
943
944 _gtk_label_set_and_free(d->selected_files, text);
945}
946
947static void _filelist_changed_callback(gpointer instance, GList *files, guint elements, guint finished, guint scan_errors, gpointer user_data)
948{
949 dt_lib_import_t *d = (dt_lib_import_t *)user_data;
950 if(IS_NULL_PTR(d) || d->closing || IS_NULL_PTR(d->selected_files)) return;
951
952 if(finished)
953 {
954 // Lock the thread to ensure we have the correct final number
955 dt_pthread_mutex_lock(&d->lock);
956 _set_selected_files_label(d, elements, scan_errors);
958 }
959 else
960 {
961 // We don't care for correctness, we just want to show user that we are still at it
962 const char *fmt;
963 switch(d->last_filter_type)
964 {
966 fmt = _("Detection in progress... (%i raw files found so far)");
967 break;
969 fmt = _("Detection in progress... (%i raster files found so far)");
970 break;
971 default:
972 fmt = _("Detection in progress... (%i files found so far)");
973 break;
974 }
975 _gtk_label_set_and_free(d->selected_files, g_strdup_printf(fmt, elements));
976 }
977}
978
980{
981 if(d->closing) return;
982 gtk_label_set_text(GTK_LABEL(d->selected_files), _("Detecting candidate files for import..."));
983
984 // Coalesce bursts of Gtk "selection-changed" signals while navigating file lists.
985 // A short delay avoids queueing redundant recursive scans for transient selections.
986 if(d->selection_scan_timeout_id > 0) g_source_remove(d->selection_scan_timeout_id);
987 d->selection_scan_timeout_id = g_timeout_add(120, _selection_changed_scan_trigger, d);
988}
989
1001static gboolean _selection_changed_scan_trigger(gpointer user_data)
1002{
1003 dt_lib_import_t *d = (dt_lib_import_t *)user_data;
1005 if(d->closing) return G_SOURCE_REMOVE;
1007 return G_SOURCE_REMOVE;
1008}
1009
1011{
1012 gboolean state = gtk_combo_box_get_active(GTK_COMBO_BOX(combobox));
1013 dt_conf_set_bool("ui_last/import_copy", state);
1014 gtk_widget_set_visible(GTK_WIDGET(d->grid), state);
1015 gtk_widget_set_visible(GTK_WIDGET(d->test_path), state);
1017 _set_test_path(d, NULL);
1018}
1019
1020static void _jobcode_changed(GtkFileChooserButton* widget, dt_lib_import_t *d)
1021{
1022 dt_conf_set_string("ui_last/import_jobcode", gtk_entry_get_text(GTK_ENTRY(widget)));
1023 _set_test_path(d, NULL);
1024}
1025
1026static void _base_dir_changed(GtkFileChooserButton* self, dt_lib_import_t *d)
1027{
1028 dt_conf_set_string("session/base_directory_pattern", gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(self)));
1029 _set_test_path(d, NULL);
1030}
1031
1033{
1034 dt_conf_set_string("session/sub_directory_pattern", gtk_entry_get_text(GTK_ENTRY(widget)));
1035 _set_test_path(d, NULL);
1036}
1037
1039{
1040 dt_conf_set_string("session/filename_pattern", gtk_entry_get_text(GTK_ENTRY(widget)));
1041 _set_test_path(d, NULL);
1042}
1043
1044static void _update_date(GtkCalendar *calendar, GtkWidget *entry)
1045{
1046 guint year, month, day;
1047 gtk_calendar_get_date(calendar, &year, &month, &day);
1048 GTimeZone *tz = g_time_zone_new_local();
1049
1050 // Again, GDateTime counts months from 1 but GtkCalendar from 0. Stupid.
1051 GDateTime *datetime = g_date_time_new(tz, year, month + 1, day, 0, 0, 0.);
1052 g_time_zone_unref(tz);
1053 gchar *date = g_date_time_format(datetime, "%F");
1054 gtk_entry_set_text(GTK_ENTRY(entry), date);
1055 dt_free(date);
1056 g_date_time_unref(datetime);
1057}
1058
1059/* Validate user input, aka check if date format respects ISO 8601*/
1060static void _datetime_changed_callback(GtkEntry *entry, dt_lib_import_t *d)
1061{
1062 const char *date = gtk_entry_get_text(entry);
1063 if(date[0])
1064 {
1065 char filtered[DT_DATETIME_LENGTH] = { 0 };
1066 gboolean valid = dt_datetime_entry_to_exif(filtered, sizeof(filtered), date);
1067 if(!valid)
1068 {
1069 gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_SECONDARY, "dialog-error");
1070 gtk_entry_set_icon_tooltip_text(entry, GTK_ENTRY_ICON_SECONDARY,
1071 _("Date should follow the ISO 8601 format, like :\n"
1072 "YYYY-MM-DD\n"
1073 "YYYY-MM-DD HH:mm\n"
1074 "YYYY-MM-DD HH:mm:ss\n"
1075 "YYYY-MM-DDTHH:mm:ss"));
1076 return;
1077 }
1078 else
1079 {
1080 gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_SECONDARY, "");
1081 gtk_entry_set_icon_tooltip_text(entry, GTK_ENTRY_ICON_SECONDARY, "");
1082 }
1083 }
1084 gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, NULL);
1085 _set_test_path(d, NULL);
1086}
1087
1088static void _file_activated(GtkFileChooser *chooser, GtkDialog *dialog)
1089{
1090 // If we double-click on image and we are not asking to duplicate files, let the filechooser
1091 // behave as a replacement of lighttable and directly open the image in darkroom.
1092 if(g_file_test(gtk_file_chooser_get_filename(chooser), G_FILE_TEST_IS_REGULAR)
1093 && !dt_conf_get_bool("ui_last/import_copy"))
1094 {
1095 gtk_dialog_response(dialog, GTK_RESPONSE_ACCEPT);
1096 }
1097}
1098
1099
1109static void _process_file_list(gpointer instance, GList *files, int elements, gboolean finished, guint scan_errors, gpointer user_data)
1110{
1111 if(!finished) return; // Should be fired only when we are done detecting stuff
1112
1113 dt_lib_import_t *d = (dt_lib_import_t *)user_data;
1114 if(IS_NULL_PTR(d) || d->closing) return;
1115
1116 if(elements > 0)
1117 {
1118 // Deep-copy the source list so import job owns an independent set of file path strings.
1119 dt_control_import_t data = {.imgs = g_list_copy_deep(files, (GCopyFunc)g_strdup, NULL),
1120 .datetime = dt_string_to_datetime(gtk_entry_get_text(GTK_ENTRY(d->datetime))),
1121 .copy = dt_conf_get_bool("ui_last/import_copy"),
1122 .jobcode = dt_conf_get_string("ui_last/import_jobcode"),
1123 .base_folder = dt_conf_get_string("session/base_directory_pattern"),
1124 .target_subfolder_pattern = dt_conf_get_string("session/sub_directory_pattern"),
1125 .target_file_pattern = dt_conf_get_string("session/filename_pattern"),
1126 .target_dir = NULL,
1127 .elements = elements,
1128 .discarded = NULL
1129 };
1130
1131 // Prepare to catch the end of import signal
1132 if(dt_control_import(data))
1133 dt_control_log(_("Could not start the import job."));
1134 }
1135 else
1136 dt_control_log(_("No files to import. Check your selection."));
1137
1139 gui_cleanup(d);
1140 _cleanup(d);
1141
1142 // Re-allocate focus to center widget
1144}
1145
1146void _file_chooser_response(GtkDialog *dialog, gint response_id, dt_lib_import_t *d)
1147{
1148 // Stop capturing the filelist changes for the in-popup label file counter.
1150
1151 switch(response_id)
1152 {
1153 case GTK_RESPONSE_ACCEPT:
1154 {
1155 if(d->selection_scan_timeout_id > 0)
1156 {
1157 g_source_remove(d->selection_scan_timeout_id);
1158 d->selection_scan_timeout_id = 0;
1159 }
1160
1161 // The next file list change will now only fire the importer job
1163
1164 // It would be swell if we could just re-use the file list computed on "select" callback.
1165 // However, it depends on the file filter used, and we can't refresh the list when
1166 // filter is changed (no callback to connect to).
1167 // To be safe, we need to start again here, from scratch.
1169
1170 // TODO: print "pending" message on modal window
1171 break;
1172 }
1173 case GTK_RESPONSE_CANCEL:
1174 default:
1175 gui_cleanup(d);
1176 _cleanup(d);
1177 break;
1178 }
1179}
1180
1181
1183{
1185
1186 d->dialog = gtk_dialog_new_with_buttons
1187 ( _("Ansel - Open pictures"), NULL, GTK_DIALOG_DESTROY_WITH_PARENT,
1188 _("Cancel"), GTK_RESPONSE_CANCEL,
1189 _("Import"), GTK_RESPONSE_ACCEPT,
1190 NULL);
1191 dt_gui_add_class(d->dialog, "dt_import_dialog");
1192
1193#ifdef GDK_WINDOWING_QUARTZ
1194// TODO: On MacOS (at least on version 13) the dialog windows doesn't behave as expected. The dialog
1195// needs to have a parent window. "set_parent_window" wasn't working, so set_transient_for is
1196// the way to go. Still the window manager isn't dealing with the dialog properly, when the dialog
1197// is shifted outside its parent. The dialog isn't visible any longer but still listed as a window
1198// of the app.
1200 gtk_window_set_position(GTK_WINDOW(d->dialog), GTK_WIN_POS_CENTER_ON_PARENT);
1201#endif
1202
1203 gtk_window_set_default_size(GTK_WINDOW(d->dialog),
1204 dt_conf_get_int("ui_last/import_dialog_width"),
1205 dt_conf_get_int("ui_last/import_dialog_height"));
1206 gtk_window_set_modal(GTK_WINDOW(d->dialog), FALSE);
1207 gtk_window_set_transient_for(GTK_WINDOW(d->dialog), GTK_WINDOW(dt_gui_main_window()));
1208 g_signal_connect(d->dialog, "response", G_CALLBACK(_file_chooser_response), d);
1209
1210 GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(d->dialog));
1211 g_signal_connect(d->dialog, "check-resize", G_CALLBACK(_resize_dialog), NULL);
1212
1213 /* Grid of options for copy/duplicate */
1214 d->grid = gtk_grid_new();
1215 GtkGrid *grid = GTK_GRID(d->grid);
1216 gtk_grid_set_column_spacing(grid, DT_GUI_BOX_SPACING / 2.);
1217 gtk_grid_set_row_spacing(grid, DT_GUI_BOX_SPACING / 2.);
1218 gtk_grid_set_column_homogeneous(grid, FALSE);
1219 gtk_grid_set_row_homogeneous(grid, FALSE);
1220
1221 /* BOTTOM PANEL */
1222 GtkWidget *rbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1223 gtk_box_pack_start(GTK_BOX(content), rbox, TRUE, TRUE, 0);
1224
1225 // File browser
1226 d->file_chooser = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN);
1227 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(d->file_chooser), TRUE);
1228 gtk_file_chooser_set_use_preview_label(GTK_FILE_CHOOSER(d->file_chooser), FALSE);
1229 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(d->file_chooser),
1230 dt_conf_get_string_const("ui_last/import_last_directory"));
1231 gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(d->file_chooser), FALSE);
1232 gtk_box_pack_start(GTK_BOX(rbox), d->file_chooser, TRUE, TRUE, 0);
1233 g_signal_connect(G_OBJECT(d->file_chooser), "current-folder-changed", G_CALLBACK(_update_directory), NULL);
1234 g_signal_connect(G_OBJECT(d->file_chooser), "file-activated", G_CALLBACK(_file_activated), GTK_DIALOG(d->dialog));
1235 g_signal_connect(G_OBJECT(d->file_chooser), "selection-changed", G_CALLBACK(_selection_changed), d);
1236 g_signal_connect(G_OBJECT(d->file_chooser), "update-preview", G_CALLBACK(update_preview_cb), d);
1237
1238 // file extension filters
1240
1241 // File browser toolbox (extra widgets)
1242 GtkWidget *toolbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1243 gtk_widget_set_halign(toolbox, GTK_ALIGN_END);
1244
1245 GtkWidget *select_all = gtk_button_new_with_label(_("Select all"));
1246 gtk_box_pack_start(GTK_BOX(toolbox), select_all, FALSE, FALSE, 0);
1247 g_signal_connect(select_all, "clicked", G_CALLBACK(_do_select_all_clicked), d);
1248
1249 GtkWidget *select_none = gtk_button_new_with_label(_("Select none"));
1250 gtk_box_pack_start(GTK_BOX(toolbox), select_none, FALSE, FALSE, 0);
1251 g_signal_connect(select_none, "clicked", G_CALLBACK(_do_select_none_clicked), d);
1252
1253 GtkWidget *select_new = gtk_button_new_with_label(_("Select new"));
1254 gtk_box_pack_start(GTK_BOX(toolbox), select_new, FALSE, FALSE, 0);
1255 g_signal_connect(select_new, "clicked", G_CALLBACK(_do_select_new_clicked), d);
1256 gtk_widget_set_tooltip_text(select_new,
1257 _("Selecting new files targets pictures that have never been added to the library. "
1258 "The lookup is done by searching for the original filename and date/time. "
1259 "It can detect files existing at another path, under a different name. "
1260 "False-positive can arise if two pictures have been taken at the same time with the same name."));
1261
1262 d->selected_files = gtk_label_new("");
1263 gtk_box_pack_start(GTK_BOX(toolbox), d->selected_files, FALSE, FALSE, 0);
1264
1265 gtk_file_chooser_set_extra_widget(GTK_FILE_CHOOSER(d->file_chooser), toolbox);
1266
1267 /* RIGHT PANEL */
1268 // File browser preview box
1269 // 1. Thumbnail
1270 GtkWidget *preview_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1271 d->preview = gtk_image_new();
1272 gtk_widget_set_size_request(d->preview, DT_PIXEL_APPLY_DPI(240), DT_PIXEL_APPLY_DPI(240));
1273 gtk_box_pack_start(GTK_BOX(preview_box), d->preview, TRUE, FALSE, 0);
1274
1275 // 2. Exif metadata
1276 d->exif = gtk_grid_new();
1277 gtk_grid_set_column_spacing(GTK_GRID(d->exif), DT_GUI_BOX_SPACING);
1278 _attach_aligned_grid_item(d->exif, 0, 0, _("Shot:"), GTK_ALIGN_END, FALSE, FALSE);
1279 _attach_grid_separator( d->exif, 1, 2);
1280 _attach_aligned_grid_item(d->exif, 2, 0, _("Camera:"), GTK_ALIGN_END, FALSE, FALSE);
1281 _attach_aligned_grid_item(d->exif, 3, 0, _("Brand:"), GTK_ALIGN_END, FALSE, FALSE);
1282 _attach_aligned_grid_item(d->exif, 4, 0, _("Lens:"), GTK_ALIGN_END, FALSE, FALSE);
1283 _attach_aligned_grid_item(d->exif, 5, 0, _("Focal:"), GTK_ALIGN_END, FALSE, FALSE);
1284 _attach_grid_separator( d->exif, 6, 2);
1285 // exposure trifecta
1286 _attach_grid_separator( d->exif, 8, 2);
1287
1288 GtkWidget *imported_label = gtk_label_new(_("Imported:"));
1289 GtkBox *help_box_inlib = attach_help_popover(
1290 imported_label,
1291 _("Images already in the library will not be imported again, selected or not. "
1292 "Remove them from the library first, or use the menu "
1293 "`Run \342\206\222 Resynchronize library and XMP` to update the local database from distant XMP.\n\n"
1294 "Ansel indexes images by their filename and parent folder (full path), "
1295 "not by their content. Therefore, renaming or moving images on the filesystem, "
1296 "or changing the mounting point of their external drive will make them "
1297 "look like new (unknown) images.\n\n"
1298 "If an XMP file is present alongside images, it will be imported as well, "
1299 "including the metadata and settings stored in it. If it is not what you want, "
1300 "you can reset metadata in the lighttable."));
1301 gtk_widget_set_halign(imported_label, GTK_ALIGN_END);
1302 gtk_grid_attach(GTK_GRID(d->exif), GTK_WIDGET(help_box_inlib), 0, EXIF_INLIB_FIELD, 1, 1);
1303 //_attach_aligned_grid_item(d->exif, 9, 0, _("Imported :"), GTK_ALIGN_END, FALSE, FALSE);
1304
1305 d->exif_info[EXIF_DATETIME_FIELD] = _attach_aligned_grid_item(d->exif, 0, 1, "", GTK_ALIGN_START, TRUE, FALSE);
1306 d->exif_info[EXIF_MODEL_FIELD] = _attach_aligned_grid_item(d->exif, 2, 1, "", GTK_ALIGN_START, TRUE, FALSE);
1307 d->exif_info[EXIF_MAKER_FIELD] = _attach_aligned_grid_item(d->exif, 3, 1, "", GTK_ALIGN_START, TRUE, FALSE);
1308 d->exif_info[EXIF_LENS_FIELD] = _attach_aligned_grid_item(d->exif, 4, 1, "", GTK_ALIGN_START, TRUE, FALSE);
1309 d->exif_info[EXIF_FOCAL_LENS_FIELD] = _attach_aligned_grid_item(d->exif, 5, 1, "", GTK_ALIGN_START, TRUE, FALSE);
1310 d->exif_info[EXIF_EXPOSURE_FIELD] = _attach_aligned_grid_item(d->exif, 7, 0, "", GTK_ALIGN_CENTER, TRUE, TRUE);
1311 d->exif_info[EXIF_INLIB_FIELD] = _attach_aligned_grid_item(d->exif, 9, 1, "", GTK_ALIGN_START, FALSE, TRUE);
1312 d->exif_info[EXIF_PATH_FIELD] = _attach_aligned_grid_item(d->exif, 10, 0, "", GTK_ALIGN_START, FALSE, TRUE);
1313 gtk_label_set_ellipsize(GTK_LABEL(d->exif_info[EXIF_PATH_FIELD]), PANGO_ELLIPSIZE_MIDDLE);
1314
1315 gtk_box_pack_start(GTK_BOX(preview_box), d->exif, TRUE, TRUE, 0);
1316 gtk_widget_show_all(d->exif);
1317
1318 gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(d->file_chooser), preview_box);
1319 /* BOTTOM PANEL */
1320
1321 GtkWidget *files = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1322 GtkWidget *file_handling = gtk_label_new("");
1323 gtk_label_set_markup(GTK_LABEL(file_handling), _("<b>File handling</b>"));
1324 gtk_box_pack_start(GTK_BOX(files), GTK_WIDGET(file_handling), FALSE, FALSE, 0);
1325
1326 GtkWidget *copy = gtk_combo_box_text_new();
1327 gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(copy), NULL, _("Add to library"));
1328 gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(copy), NULL, _("Copy to disk"));
1329 gtk_combo_box_set_active(GTK_COMBO_BOX(copy), dt_conf_get_bool("ui_last/import_copy"));
1330 gtk_box_pack_start(GTK_BOX(files), GTK_WIDGET(copy), FALSE, FALSE, 0);
1331 g_signal_connect(G_OBJECT(copy), "changed", G_CALLBACK(_copy_toggled_callback), (gpointer)d);
1332
1333 d->help_string = gtk_label_new("");
1334 _set_help_string(d, dt_conf_get_bool("ui_last/import_copy"));
1335 gtk_box_pack_start(GTK_BOX(files), GTK_WIDGET(d->help_string), FALSE, FALSE, 0);
1336
1337 gtk_box_pack_start(GTK_BOX(rbox), GTK_WIDGET(files), FALSE, FALSE, 0);
1338
1339 // Project date
1340 GtkWidget *calendar_label = gtk_label_new(_("Project date"));
1341 gtk_widget_set_halign(calendar_label, GTK_ALIGN_START);
1342 d->datetime = gtk_entry_new();
1344 gtk_entry_set_width_chars(GTK_ENTRY(d->datetime), 20);
1345 g_signal_connect(G_OBJECT(d->datetime), "changed", G_CALLBACK(_datetime_changed_callback), d);
1346
1347 // Date is inited as today by default
1348 GDateTime *now = g_date_time_new_now_local();
1349 gchar *now_string = g_date_time_format(now, "%F");
1350 gtk_entry_set_text(GTK_ENTRY(d->datetime), now_string);
1351 dt_free(now_string);
1352
1353 // Date chooser
1354 GtkWidget *calendar = gtk_calendar_new();
1355 // GtkCalendar uses monthes in [0:11]. Glib GDateTime returns monthes in [1:12]. Stupid.
1356 gtk_calendar_select_month(GTK_CALENDAR(calendar), g_date_time_get_month(now) - 1, g_date_time_get_year(now));
1357 const guint day = g_date_time_get_day_of_month(now);
1358 gtk_calendar_select_day(GTK_CALENDAR(calendar), day);
1359 gtk_calendar_mark_day(GTK_CALENDAR(calendar), day);
1360 GtkBox *box_calendar = attach_popover(d->datetime, "appointment-new-symbolic", calendar);
1361 g_signal_connect(G_OBJECT(calendar), "day-selected", G_CALLBACK(_update_date), d->datetime);
1362
1363 // free date
1364 g_date_time_unref(now);
1365
1366 // Base directory of projects
1367 GtkWidget *jobcode = gtk_entry_new();
1369 gtk_entry_set_text(GTK_ENTRY(jobcode), dt_conf_get_string_const("ui_last/import_jobcode"));
1370 gtk_widget_set_hexpand(jobcode, TRUE);
1371 g_signal_connect(G_OBJECT(jobcode), "changed", G_CALLBACK(_jobcode_changed), d);
1372
1373 GtkWidget *jobcode_label = gtk_label_new(_("Jobcode"));
1374 gtk_widget_set_halign(jobcode_label, GTK_ALIGN_START);
1375
1376 GtkWidget *base_label = gtk_label_new(_("Base directory of all projects"));
1377 gtk_widget_set_halign(base_label, GTK_ALIGN_START);
1378
1379 GtkWidget *dir_label = gtk_label_new(_("Project directory naming pattern"));
1380 gtk_widget_set_halign(dir_label, GTK_ALIGN_START);
1381
1382 GtkWidget *file_label = gtk_label_new(_("File naming pattern"));
1383 gtk_widget_set_halign(file_label, GTK_ALIGN_START);
1384
1385 GtkWidget *base_dir
1386 = gtk_file_chooser_button_new(_("Select a base directory"), GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER);
1387 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(base_dir),
1388 dt_conf_get_string_const("session/base_directory_pattern"));
1389 g_signal_connect(G_OBJECT(base_dir), "file-set", G_CALLBACK(_base_dir_changed), d);
1390 gtk_widget_set_hexpand(base_dir, TRUE);
1391
1392 GtkWidget *sep1 = gtk_label_new(G_DIR_SEPARATOR_S);
1393 GtkWidget *sep2 = gtk_label_new(G_DIR_SEPARATOR_S);
1394
1395 GtkWidget *project_dir = gtk_entry_new();
1397 gtk_entry_set_text(GTK_ENTRY(project_dir), dt_conf_get_string_const("session/sub_directory_pattern"));
1398 gtk_widget_set_hexpand(project_dir, TRUE);
1400 gtk_widget_set_tooltip_text(project_dir, _("Start typing `$(` to see available variables through auto-completion"));
1401 g_signal_connect(G_OBJECT(project_dir), "changed", G_CALLBACK(_project_dir_changed), d);
1402
1403 GtkWidget *file = gtk_entry_new();
1405 gtk_entry_set_text(GTK_ENTRY(file), dt_conf_get_string_const("session/filename_pattern"));
1406 gtk_widget_set_hexpand(file, TRUE);
1408 g_signal_connect(G_OBJECT(file), "changed", G_CALLBACK(_filename_changed), d);
1409
1410 GtkWidget *pattern_label = gtk_label_new(_("Pattern result"));
1411 gtk_widget_set_halign(pattern_label, GTK_ALIGN_START);
1412
1413 d->test_path = gtk_label_new(_("Choose a file to see the result..."));
1414 gtk_widget_set_halign(d->test_path, GTK_ALIGN_START);
1415 gtk_label_set_line_wrap(GTK_LABEL(d->test_path), TRUE);
1416 gtk_label_set_max_width_chars(GTK_LABEL(d->test_path), 60);
1417 _set_test_path(d, NULL);
1418
1419 /* Create the grid of import params when using duplication */
1420 int row = 0;
1421 _attach_grid_separator(GTK_WIDGET(grid), row, 5);
1422 row++;
1423
1424 // Row 0: labels for text entries
1425 gtk_grid_attach(grid, calendar_label, 0, row, 1, 1);
1426 gtk_grid_attach(grid, jobcode_label, 2, row, 1, 1);
1427 gtk_grid_attach(grid, pattern_label, 4, row, 1, 1);
1428 row++;
1429
1430 // Row 1: text entries
1431 gtk_grid_attach(grid, GTK_WIDGET(box_calendar), 0, row, 1, 1);
1432 gtk_grid_attach(grid, jobcode, 2, row, 1, 1);
1433 gtk_grid_attach(grid, d->test_path, 4, row, 1, 1);
1434 row++;
1435
1436 // Row 2: separator
1437 _attach_grid_separator(GTK_WIDGET(grid), row, 5);
1438 row++;
1439
1440 // Row 3: labels for text entries
1441 gtk_grid_attach(grid, base_label, 0, row, 1, 1);
1442 gtk_grid_attach(grid, dir_label, 2, row, 1, 1);
1443 gtk_grid_attach(grid, file_label, 4, row, 1, 1);
1444 row++;
1445
1446 // Row 4: text entries
1447 gtk_grid_attach(grid, base_dir, 0, row, 1, 1);
1448 gtk_grid_attach(grid, sep1, 1, row, 1, 1);
1449 gtk_grid_attach(grid, project_dir, 2, row, 1, 1);
1450 gtk_grid_attach(grid, sep2, 3, row, 1, 1);
1451 gtk_grid_attach(grid, file, 4, row, 1, 1);
1452 row++;
1453
1454 gtk_box_pack_start(GTK_BOX(rbox), GTK_WIDGET(grid), FALSE, FALSE, 0);
1455
1456 gtk_widget_show_all(d->dialog);
1457
1458 // Duplication parameters visible only if the option is set
1459 gtk_widget_set_visible(GTK_WIDGET(grid), dt_conf_get_bool("ui_last/import_copy"));
1460
1461 // Update the number of selected files string because Gtk forces a default selection at opening time
1463 G_CALLBACK(_filelist_changed_callback), d);
1464}
1465
1467{
1468 gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(d->file_chooser));
1469}
1470
1472{
1473 gtk_file_chooser_select_all(GTK_FILE_CHOOSER(d->file_chooser));
1474}
1475
1477{
1478 // Twisted Gtk doesn't let us select multiple files.
1479 // We need to select all then unselect what we don't want.
1481
1482 GtkFileChooser *chooser = GTK_FILE_CHOOSER(d->file_chooser);
1483 gchar *folder = gtk_file_chooser_get_current_folder(chooser);
1484 if(IS_NULL_PTR(folder)) return;
1485
1486 GFile *folder_file = g_file_new_for_path(folder);
1487 GFileEnumerator *files = NULL;
1488 if(IS_NULL_PTR(folder_file))
1489 goto end;
1490
1491 files = g_file_enumerate_children(
1492 folder_file, G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
1493 G_FILE_QUERY_INFO_NONE, NULL, NULL);
1494 g_object_unref(folder_file);
1495 if(IS_NULL_PTR(files))
1496 goto end;
1497
1498 // Get the file filter in use
1499 GtkFileFilter *filter = gtk_file_chooser_get_filter(chooser);
1500 if(IS_NULL_PTR(filter))
1501 {
1502 goto end;
1503 }
1504 const GtkFileFilterFlags filter_needed = gtk_file_filter_get_needed(filter);
1505
1506 GFile *file = NULL;
1507 while(g_file_enumerator_iterate(files, NULL, &file, NULL, NULL))
1508 {
1509 // g_file_enumerator_iterate returns FALSE only on errors, not on end of enumeration.
1510 // We need an ugly break here else infinite loop.
1511 if(IS_NULL_PTR(file)) break;
1512
1513 gchar *parse_name = g_file_get_parse_name(file);
1514 gchar *uri = g_file_get_uri(file);
1515 gchar *basename = g_file_get_basename(file);
1516 gchar *filepath = g_file_get_path(file);
1517 GtkFileFilterInfo filter_info = { filter_needed,
1518 parse_name,
1519 uri,
1520 parse_name, NULL };
1521
1522 const gboolean is_regular = !IS_NULL_PTR(filepath) && g_file_test(filepath, G_FILE_TEST_IS_REGULAR);
1523 const int is_path_in_lib = !IS_NULL_PTR(basename) ? _is_in_library_by_path(folder, basename) : -1;
1524 const int is_metadata_in_lib = _is_in_library_by_metadata(file);
1525 const gboolean is_in_lib = (is_path_in_lib > -1) || (is_metadata_in_lib > -1);
1526
1527 // We need to act only on files passing the file filter, aka being currently displayed on screen.
1528 // Unselecting files not displayed in the current list freezes the UI and introduces oddities.
1529 if(gtk_file_filter_filter(filter, &filter_info)
1530 && !(is_regular && !is_in_lib))
1531 {
1532 gtk_file_chooser_unselect_file(chooser, file);
1533 }
1534
1535 dt_free(parse_name);
1536 dt_free(uri);
1537 dt_free(basename);
1538 dt_free(filepath);
1539 // g_file_enumerator_iterate() returns transfer-none children owned by the enumerator.
1540 // Unref happens when the enumerator advances or is destroyed.
1541 file = NULL;
1542 }
1543
1544 end:
1545 if(!IS_NULL_PTR(files)) g_object_unref(files);
1546 dt_free(folder);
1547}
1548
1550{
1551 d->closing = TRUE;
1552
1553 if(d->selection_scan_timeout_id > 0)
1554 {
1555 g_source_remove(d->selection_scan_timeout_id);
1556 d->selection_scan_timeout_id = 0;
1557 }
1558
1559 // Disconnect callbacks that may enqueue async work while widgets are being destroyed.
1560 if(!IS_NULL_PTR(d->file_chooser))
1561 {
1562 g_signal_handlers_disconnect_by_func(G_OBJECT(d->file_chooser), G_CALLBACK(_selection_changed), d);
1563 g_signal_handlers_disconnect_by_func(G_OBJECT(d->file_chooser), G_CALLBACK(update_preview_cb), d);
1564 g_signal_handlers_disconnect_by_func(G_OBJECT(d->file_chooser), G_CALLBACK(_file_activated), GTK_DIALOG(d->dialog));
1565 g_signal_handlers_disconnect_by_func(G_OBJECT(d->file_chooser), G_CALLBACK(_update_directory), NULL);
1566 }
1567
1568 // Ensure the background recursive folder detection is finished before destroying widgets.
1569 // Reason is, if a job is still running, it might send its signal upon completion,
1570 // and then the widgets supposed to be updated in callback will be undefined (but not NULL... WTF Gtk ?)
1571 dt_pthread_mutex_lock(&d->lock);
1572 gtk_widget_destroy(d->dialog);
1573 d->dialog = NULL;
1574 d->file_chooser = NULL;
1575 d->preview = NULL;
1576 d->exif = NULL;
1577 d->grid = NULL;
1578 d->jobcode = NULL;
1579 d->help_string = NULL;
1580 d->test_path = NULL;
1581 d->selected_files = NULL;
1582 d->filter_all = NULL;
1583 d->filter_raw = NULL;
1584 d->filter_raster = NULL;
1585 for(int k = 0; k < EXIF_LAST_FIELD; k++) d->exif_info[k] = NULL;
1586 dt_pthread_mutex_unlock(&d->lock);
1587}
1588
1590{
1591 dt_lib_import_t *d = malloc(sizeof(dt_lib_import_t));
1592 d->closing = FALSE;
1593 d->selection_scan_timeout_id = 0;
1594 dt_pthread_mutex_init(&d->lock, NULL);
1595 d->path_file = NULL;
1596 d->scan_state = calloc(1, sizeof(dt_import_scan_state_t));
1597 dt_pthread_mutex_init(&d->scan_state->lock, NULL);
1598 d->scan_state->generation = 0;
1599 d->scan_state->refcount = 1;
1600 d->scan_state->closing = FALSE;
1601
1602 return d;
1603}
1604
1606{
1607 // Teardown can be entered from multiple control paths. Ensure no pending global signal
1608 // callback can still target this module state after memory is released.
1611
1612 if(!IS_NULL_PTR(d->scan_state))
1613 {
1614 gboolean release = FALSE;
1615 dt_pthread_mutex_lock(&d->scan_state->lock);
1616 d->scan_state->closing = TRUE;
1617 d->scan_state->generation++;
1618 if(d->scan_state->refcount > 0) d->scan_state->refcount--;
1619 release = (d->scan_state->refcount == 0);
1620 dt_pthread_mutex_unlock(&d->scan_state->lock);
1621 if(release)
1622 {
1623 dt_pthread_mutex_destroy(&d->scan_state->lock);
1624 dt_free(d->scan_state);
1625 }
1626 d->scan_state = NULL;
1627 }
1628
1630 dt_free(d->path_file);
1631 dt_free(d);
1632}
1633
1635{
1636 dt_lib_import_t *d = _init();
1637 gui_init(d);
1638}
1639
1640static dt_import_t * dt_import_init(dt_lib_import_t *d, const uint32_t generation)
1641{
1642 dt_import_t *import = g_malloc(sizeof(dt_import_t));
1643 import->generation = generation;
1644 import->files = NULL;
1645 import->elements = 0;
1646 import->scan_errors = 0;
1647 dt_pthread_mutex_init(&import->lock, NULL);
1648 import->scan_state = d->scan_state;
1649 dt_pthread_mutex_lock(&import->scan_state->lock);
1650 import->scan_state->refcount++;
1651 dt_pthread_mutex_unlock(&import->scan_state->lock);
1652
1653 dt_pthread_mutex_lock(&import->lock);
1654
1655 // selection is owned here and will need to be freed.
1656 import->selection = gtk_file_chooser_get_uris(GTK_FILE_CHOOSER(d->file_chooser));
1657
1658 // Snapshot the active file-type filter here, on the GUI thread, into a plain enum: the
1659 // recursive scan job runs off-thread and must never touch the live GtkFileFilter objects.
1660 GtkFileFilter *active_filter = gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(d->file_chooser));
1661 if(!IS_NULL_PTR(active_filter) && active_filter == d->filter_raw)
1662 import->filter_type = DT_IMPORT_FILTER_RAW;
1663 else if(!IS_NULL_PTR(active_filter) && active_filter == d->filter_raster)
1664 import->filter_type = DT_IMPORT_FILTER_RASTER;
1665 else
1666 import->filter_type = DT_IMPORT_FILTER_ALL;
1667
1668 // Mirror it onto d too: _filelist_changed_callback() only gets files/elements/finished from
1669 // the signal, not the dt_import_t this scan belongs to.
1670 d->last_filter_type = import->filter_type;
1671
1672 dt_pthread_mutex_unlock(&import->lock);
1673
1674 return import;
1675}
1676
1677static void dt_import_cleanup(void *data)
1678{
1679 // dt_import_t owns the recursive selection list for the whole detection job lifetime.
1680 // Signal receivers may inspect it, but must not release it.
1681 dt_import_t *import = (dt_import_t *)data;
1682 g_list_free_full(import->files, dt_free_gpointer);
1683 import->files = NULL;
1684 g_slist_free_full(import->selection, dt_free_gpointer);
1685 import->selection = NULL;
1686 if(!IS_NULL_PTR(import->scan_state))
1687 {
1688 gboolean release = FALSE;
1689 dt_pthread_mutex_lock(&import->scan_state->lock);
1690 if(import->scan_state->refcount > 0) import->scan_state->refcount--;
1691 release = (import->scan_state->refcount == 0);
1692 dt_pthread_mutex_unlock(&import->scan_state->lock);
1693 if(release)
1694 {
1695 dt_pthread_mutex_destroy(&import->scan_state->lock);
1696 dt_free(import->scan_state);
1697 }
1698 import->scan_state = NULL;
1699 }
1700 dt_pthread_mutex_destroy(&import->lock);
1701 dt_free(import);
1702}
1703
1704/* Shows what an import left behind. Lived in control/jobs/import_jobs.c -- the only GUI
1705 * code in that job file; the job invokes it through the registered handler and this
1706 * function owns freeing the enumerator params and import data, as the contract says. */
1708{
1709 dt_control_import_t *data = params->data;
1710
1711 // Create the window
1712 GtkWidget *dialog = gtk_dialog_new_with_buttons("Message",
1713 GTK_WINDOW(dt_gui_main_window()),
1714 GTK_DIALOG_DESTROY_WITH_PARENT,
1715 _("_OK"),
1716 GTK_RESPONSE_NONE,
1717 NULL);
1718 gtk_window_set_title(GTK_WINDOW(dialog), _("Some files have not been copied"));
1719 gtk_window_set_default_size(GTK_WINDOW(dialog), DT_PIXEL_APPLY_DPI(800), DT_PIXEL_APPLY_DPI(800));
1720
1721 // Create the label
1722 GtkWidget *label = gtk_label_new(_("The following source files have not been copied "
1723 "because similarly-named files already exist on the destination. "
1724 "This may be because the files have already been imported "
1725 "or the naming pattern leads to non-unique file names."));
1726 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
1727
1728 // Create the scrolled window internal container
1729 GtkWidget *scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1730 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window), GTK_POLICY_AUTOMATIC,
1731 GTK_POLICY_AUTOMATIC);
1732 gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(scrolled_window), TRUE);
1733
1734 // Create the treeview model from the list of discarded file pathes
1735 GtkListStore *store = gtk_list_store_new(1, G_TYPE_STRING);
1736 GtkTreeIter iter;
1737 for(GList *file = g_list_first(data->discarded); file; file = g_list_next(file))
1738 {
1739 if(file->data)
1740 {
1741 gtk_list_store_append(store, &iter);
1742 gtk_list_store_set(store, &iter, 0, (char *)file->data, -1);
1743 }
1744 }
1745
1746 // Create the treeview view. Sooooo verbose... it's only a flat list.
1747 GtkWidget *view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
1748 GtkTreeViewColumn *col = gtk_tree_view_column_new();
1749 gtk_tree_view_column_set_title(col, _("Origin path"));
1750 GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
1751 gtk_tree_view_column_pack_start(col, renderer, TRUE);
1752 gtk_tree_view_column_set_attributes(col, renderer, "text", 0, NULL);
1753 gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
1754 g_object_unref(store);
1755
1756 // Pack widgets to an unified box
1757 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1758 gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0);
1759 gtk_box_pack_start(GTK_BOX(box), scrolled_window, TRUE, TRUE, 0);
1760 dt_gui_add_class(scrolled_window, "dt_recessed_scroll");
1761 gtk_container_add(GTK_CONTAINER(scrolled_window), view);
1762
1763 // Pack the box to the dialog internal container
1764 GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
1765 gtk_container_add(GTK_CONTAINER(content_area), box);
1766 gtk_widget_show_all(dialog);
1767
1768#ifdef GDK_WINDOWING_QUARTZ
1770#endif
1771
1772 gtk_dialog_run(GTK_DIALOG(dialog));
1773 gtk_widget_destroy(dialog);
1774
1776 dt_free(data);
1778
1779 return 0;
1780}
1781
1782/* Runs on the import worker thread: hop to the GUI main loop, where the dialog lives.
1783 * The popup owns freeing params, per the handler contract. */
1785{
1786 g_main_context_invoke(NULL, (GSourceFunc)_import_discarded_files_popup, params);
1787}
1788
1793
1794// clang-format off
1795// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
1796// vim: shiftwidth=2 expandtab tabstop=2 cindent
1797// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
1798// clang-format on
void dt_accels_disconnect_on_text_input(GtkWidget *widget)
Disconnect accels while a text or search entry has the focus, and reconnect them when it loses it....
Handle default and user-set shortcuts (accelerators)
GtkWidget * dt_gui_main_window(void)
void dt_gui_refocus_center()
static void error(char *msg)
Definition ashift_lsd.c:202
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
const char * extension(dt_imageio_module_data_t *data)
Definition avif.c:651
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
static dt_aligned_pixel_t rgb
static const int row
void dt_conf_set_bool(const char *name, int val)
int dt_conf_get_bool(const char *name)
gchar * dt_conf_get_string(const char *name)
Read the stored string for name as a private copy.
void dt_conf_set_int(const char *name, int val)
int dt_conf_get_int(const char *name)
Integer for name, clamped to the bounds declared in the XML.
void dt_conf_set_string(const char *name, const char *val)
const char * dt_conf_get_string_const(const char *name)
Borrow the stored string for name without copying it.
dt_image_flags_t dt_image_flags_from_extension(const char *extension)
int32_t dt_image_get_id(int32_t film_id, const gchar *filename)
gboolean dt_image_is_hdr(const dt_image_t *img)
void dt_image_init(dt_image_t *img)
void dt_image_film_roll_directory(const dt_image_t *img, char *pathname, size_t pathname_len)
gboolean dt_image_get_usercrop(const dt_image_t *img, dt_boundingbox_t box)
void dt_control_log(const char *msg,...)
Definition control.c:828
struct dt_control_t * dt_control_get_global(void)
Definition darktable.c:650
void dt_control_image_enumerator_cleanup(void *p)
void * dt_alloc_align(size_t size)
Allocate cacheline-aligned memory.
Definition darktable.c:507
GDateTime * dt_string_to_datetime(const char *string)
Definition datetime.c:321
gboolean dt_datetime_img_to_local(char *local, const size_t local_size, const dt_image_t *img, const gboolean msec)
Definition datetime.c:193
gboolean dt_datetime_unix_to_exif(char *exif, const size_t exif_size, const time_t *unix)
Definition datetime.c:212
gboolean dt_datetime_entry_to_exif(char *exif, const size_t exif_size, const char *entry)
Definition datetime.c:333
#define DT_DATETIME_LENGTH
Definition datetime.h:38
#define DT_DATETIME_EXIF_LENGTH
Definition datetime.h:39
GtkTreeStore * store
its model, owned by the view
GtkWidget * folder
destination folder chooser
GtkWidget * view
the tree view
const int res
Definition dtpthread.h:351
static int dt_pthread_mutex_unlock(dt_pthread_mutex_t *mutex) RELEASE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:127
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Initialise a mutex. With mutexattr NULL – which is how 54 of the 56 call sites in this tree spell it ...
Definition dtpthread.h:104
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
Definition dtpthread.h:132
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:117
int dt_exif_read(dt_image_t *img, const char *path)
Definition exif.cc:1994
What a photograph says about itself: the EXIF, IPTC and XMP tags a camera and a cataloguer write,...
gchar * dt_loc_get_home_dir(const gchar *user)
int32_t dt_film_get_id(const char *folder)
Definition film.c:108
void dt_gtkentry_setup_completion(GtkEntry *entry, const dt_gtkentry_completion_spec *compl_list, const char *trigger_char)
Definition gtkentry.c:176
const dt_gtkentry_completion_spec * dt_gtkentry_get_default_path_compl_list()
Definition gtkentry.c:200
#define UNKNOWN_IMAGE
Definition image.h:78
float dt_boundingbox_t[4]
Definition image.h:83
@ ORIENTATION_ROTATE_CCW_90_DEG
Definition image.h:228
@ ORIENTATION_ROTATE_CW_90_DEG
Definition image.h:229
@ ORIENTATION_ROTATE_180_DEG
Definition image.h:226
@ DT_IMAGEIO_OK
Definition image.h:92
dt_image_flags_t
Definition image.h:104
@ DT_IMAGE_RAW
Definition image.h:124
@ DT_IMAGE_HDR
Definition image.h:126
dt_image_t * dt_image_cache_get(const int32_t imgid, char mode)
void dt_image_cache_read_release(const dt_image_t *img)
const char *const * dt_image_ext_ldr_list(void)
gboolean dt_image_ext_is_gui_raw(const char *ext)
gboolean dt_image_ext_is_gui_raster(const char *ext)
const char *const * dt_image_ext_raw_list(void)
const char *const * dt_image_ext_hdr_list(void)
gboolean dt_supported_image(const gchar *filename)
Definition darktable.c:381
void dt_imageio_close_standalone(dt_mipmap_buffer_t *buf)
Release a buffer from dt_imageio_open_standalone(). NULL-safe, and safe to call on a buffer whose ope...
dt_imageio_retval_t dt_imageio_open_standalone(dt_image_t *img, const char *filename, dt_mipmap_buffer_t *buf)
Decode a file into a buffer the CALLER owns, with no cache involvement.
gboolean dt_imageio_crop_thumbnail(const dt_boundingbox_t box, uint8_t *const buffer, int32_t *width, int32_t *height)
Crop an 8-bit RGBA preview buffer in place to a normalized bounding box.
int dt_imageio_large_thumbnail(const char *filename, uint8_t **buffer, int32_t *th_width, int32_t *th_height, dt_colorspaces_color_profile_type_t *color_space, const int width, const int height)
Load the thumbnail embedded into a RAW file having at least the size MAX(width, height) x MAX(width,...
static void _resize_dialog(GtkWidget *widget)
Definition import.c:555
static void dt_import_cleanup(void *import)
Definition import.c:1677
static void _datetime_changed_callback(GtkEntry *entry, dt_lib_import_t *d)
Definition import.c:1060
static void _do_select_all_clicked(GtkWidget *widget, dt_lib_import_t *d)
Definition import.c:539
static int32_t _get_selected_files_job(dt_job_t *job)
Definition import.c:361
static int _import_discarded_files_popup(dt_control_image_enumerator_t *params)
Definition import.c:1707
void _dt_check_basedir()
Definition import.c:515
static void _import_discarded_files_schedule(dt_control_image_enumerator_t *params)
Definition import.c:1784
static void _update_date(GtkCalendar *calendar, GtkWidget *entry)
Definition import.c:1044
static void _process_file_list(gpointer instance, GList *files, int elements, gboolean finished, guint scan_errors, gpointer user_data)
Import a list of file by copying them or not, and adding them to database.
Definition import.c:1109
void dt_control_get_selected_files(dt_lib_import_t *d, gboolean destroy_window)
Definition import.c:366
static void _filename_changed(GtkWidget *widget, dt_lib_import_t *d)
Definition import.c:1038
static void _selection_changed(GtkWidget *filechooser, dt_lib_import_t *d)
Definition import.c:979
static int _is_in_library_by_path(const gchar *folder, const char *filename)
Definition import.c:645
static void gui_cleanup(dt_lib_import_t *d)
Definition import.c:1549
void dt_gui_import_init_handlers(void)
Register the GUI-side import handlers (the discarded-files recap dialog).
Definition import.c:1789
static dt_import_t * dt_import_init(dt_lib_import_t *d, const uint32_t generation)
Definition import.c:1640
static void _set_selected_files_label(dt_lib_import_t *d, const guint elements, const guint scan_errors)
Definition import.c:898
static void _gtk_label_set_and_free(GtkWidget *widget, gchar *label)
Definition import.c:193
static void _jobcode_changed(GtkFileChooserButton *widget, dt_lib_import_t *d)
Definition import.c:1020
static void _copy_toggled_callback(GtkWidget *combobox, dt_lib_import_t *d)
Definition import.c:1010
static gboolean _scan_still_valid(dt_import_t *const import)
Definition import.c:183
static void _file_activated(GtkFileChooser *chooser, GtkDialog *dialog)
Definition import.c:1088
static void _set_test_path(dt_lib_import_t *d, dt_image_t *img)
Definition import.c:815
static void _do_select_none_clicked(GtkWidget *widget, dt_lib_import_t *d)
Definition import.c:544
static void _file_filters(dt_lib_import_t *d)
Definition import.c:577
static void _filelist_changed_callback(gpointer instance, GList *files, guint elements, guint finished, guint scan_errors, gpointer user_data)
Definition import.c:947
void dt_images_import()
Definition import.c:1634
static void _do_select_new_clicked(GtkWidget *widget, dt_lib_import_t *d)
Definition import.c:549
static GdkPixbuf * _import_get_thumbnail(const gchar *filename, const int width, const int height, const gboolean valid_exif, dt_image_t *img)
Definition import.c:396
static void _do_select_new(dt_lib_import_t *d)
Definition import.c:1476
static void _base_dir_changed(GtkFileChooserButton *self, dt_lib_import_t *d)
Definition import.c:1026
static void _cleanup(dt_lib_import_t *d)
Definition import.c:1605
static dt_lib_import_t * _init()
Definition import.c:1589
static GtkWidget * _attach_aligned_grid_item(GtkWidget *grid, const int row, const int column, const char *label, const GtkAlign align, const gboolean fixed_width, const gboolean full_width)
Definition import.c:621
static int _is_in_library_by_metadata(GFile *file)
Definition import.c:652
static void gui_init(dt_lib_import_t *d)
Definition import.c:1182
static void _project_dir_changed(GtkWidget *widget, dt_lib_import_t *d)
Definition import.c:1032
exif_fields_t
Definition import.c:111
@ EXIF_EXPOSURE_FIELD
Definition import.c:119
@ EXIF_INLIB_FIELD
Definition import.c:121
@ EXIF_LENS_FIELD
Definition import.c:116
@ EXIF_DATETIME_FIELD
Definition import.c:112
@ EXIF_LAST_FIELD
Definition import.c:123
@ EXIF_SEPARATOR2_FIELD
Definition import.c:118
@ EXIF_MAKER_FIELD
Definition import.c:115
@ EXIF_SEPARATOR1_FIELD
Definition import.c:113
@ EXIF_FOCAL_LENS_FIELD
Definition import.c:117
@ EXIF_MODEL_FIELD
Definition import.c:114
@ EXIF_PATH_FIELD
Definition import.c:122
@ EXIF_SEPARATOR3_FIELD
Definition import.c:120
static void _build_filter(GtkFileFilter *filter, const gchar *extension)
Definition import.c:563
static int32_t dt_get_selected_files(dt_import_t *import)
Definition import.c:316
gboolean dt_import_passes_filter(const dt_import_filter_type_t filter_type, const gchar *pathname)
Definition import.c:203
static void _do_select_all(dt_lib_import_t *d)
Definition import.c:1471
static void _filter_document(GVfs *vfs, GFile *document, dt_import_t *import)
Definition import.c:216
static void _report_scan_error(dt_import_t *const import, GFile *folder, GError *error)
Definition import.c:242
void _file_chooser_response(GtkDialog *dialog, gint response_id, dt_lib_import_t *d)
Definition import.c:1146
static void _update_directory(GtkWidget *file_chooser, dt_lib_import_t *d)
Definition import.c:796
static gboolean _selection_changed_scan_trigger(gpointer user_data)
Trigger recursive import candidate detection after selection settle time.
Definition import.c:1001
static void update_preview_cb(GtkFileChooser *file_chooser, gpointer userdata)
Definition import.c:678
static void _exif_text_set_and_free(dt_lib_import_t *d, exif_fields_t field, gchar *label)
Definition import.c:673
static void _recurse_selection(GSList *selection, dt_import_t *const import)
Definition import.c:297
static void _do_select_none(dt_lib_import_t *d)
Definition import.c:1466
static void _recurse_folder(GVfs *vfs, GFile *folder, dt_import_t *const import)
Definition import.c:252
static GtkWidget * _attach_grid_separator(GtkWidget *grid, const int row, const int length)
Definition import.c:637
static void _set_help_string(dt_lib_import_t *d, gboolean copy)
Definition import.c:803
dt_import_filter_type_t
Definition import.h:46
@ DT_IMPORT_FILTER_ALL
Definition import.h:47
@ DT_IMPORT_FILTER_RASTER
Definition import.h:49
@ DT_IMPORT_FILTER_RAW
Definition import.h:48
gchar * dt_build_filename_from_pattern(const char *const filename, const int index, dt_image_t *img, dt_control_import_t *data)
Build a full path for a given image file, given a pattern.
Definition import_jobs.c:78
int dt_control_import(dt_control_import_t data)
Process a list of images to import with or without copying the files on an arbitrary hard-drive.
void dt_control_import_set_discarded_files_handler(dt_control_import_discarded_handler_t handler)
void dt_control_import_data_free(dt_control_import_t *data)
dt_job_t * dt_control_job_create(dt_job_execute_callback execute, const char *msg,...)
Definition jobs.c:137
int dt_control_add_job(dt_control_t *control, dt_job_queue_t queue_id, _dt_job_t *job)
Definition jobs.c:407
void * dt_control_job_get_params(const _dt_job_t *job)
Definition jobs.c:131
void dt_control_job_set_params(_dt_job_t *job, void *params, dt_job_destroy_callback callback)
Definition jobs.c:114
void dt_control_job_dispose(_dt_job_t *job)
Definition jobs.c:155
@ DT_JOB_QUEUE_USER_BG
Definition jobs.h:56
@ DT_DEBUG_IMPORT
Definition logging.h:77
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
float *const restrict const size_t k
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
#define CLAMPF(a, mn, mx)
Definition math.h:91
#define dt_free_align(ptr)
Release memory from dt_alloc_align() and set ptr to NULL.
Definition mem_alloc.h:214
static void dt_free_gpointer(gpointer ptr)
g_free() one pointer, with the signature GDestroyNotify wants.
Definition mem_alloc.h:184
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
int dt_metadata_already_imported(const char *filename, const char *datetime)
uint32_t width
Definition mipmap_cache.c:0
uint32_t height
Definition mipmap_cache.c:1
dt_colorspaces_color_profile_type_t color_space
Definition mipmap_cache.c:5
#define __OMP_PARALLEL_FOR__(...)
Definition openmp.h:95
void dt_osx_disallow_fullscreen(GtkWidget *widget)
Definition osx.mm:105
#define DT_PATH_MAX
Buffer size for a filesystem path anywhere in Ansel.
Definition paths.h:57
#define dt_pixelpipe_cache_alloc_align_cache(size, id)
#define dt_pixelpipe_cache_free_align(mem)
GtkBox * attach_help_popover(GtkWidget *widget, const char *label)
Definition popup.c:122
GtkBox * attach_popover(GtkWidget *widget, const char *icon, GtkWidget *content)
Definition popup.c:91
dt_colorspaces_color_profile_type_t
void copy(TYPE *dest, TYPE *source, size_t num_el)
Copy a flat buffer used to initialize test matrices.
#define DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(ctlsig, cb, user_data)
Definition signal.h:407
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
Definition signal.h:386
struct dt_control_signal_t * dt_control_signal_get_global(void)
Definition darktable.c:615
@ DT_SIGNAL_FILELIST_CHANGED
Raised when the recursive file crawler returns. no params, return :
Definition signal.h:332
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:396
char * dt_variables_expand(dt_variables_params_t *params, gchar *source, gboolean iterate)
void dt_variables_params_destroy(dt_variables_params_t *params)
gboolean dt_get_user_pictures_dir(const gchar *homedir, gchar *picdir, size_t picdir_size)
Gets the path to the current OS pictures directory.
void dt_variables_params_init(dt_variables_params_t **params)
const float uint32_t state[4]
static const char *const day[7]
Definition strptime.c:97
char * target_subfolder_pattern
Definition import_jobs.h:65
float exif_exposure
Definition image.h:367
float exif_iso
Definition image.h:370
float exif_aperture
Definition image.h:369
dt_image_orientation_t orientation
Definition image.h:366
float exif_focal_length
Definition image.h:371
char exif_maker[64]
Definition image.h:374
char exif_lens[128]
Definition image.h:376
char exif_model[64]
Definition image.h:375
uint32_t generation
Definition import.c:69
dt_pthread_mutex_t lock
Definition import.c:68
uint32_t elements
Definition import.c:91
GSList * selection
Definition import.c:82
uint32_t generation
Definition import.c:88
GList * files
Definition import.c:85
dt_pthread_mutex_t lock
Definition import.c:95
guint scan_errors
Definition import.c:107
dt_import_filter_type_t filter_type
Definition import.c:100
dt_import_scan_state_t * scan_state
Definition import.c:97
GtkWidget * jobcode
Definition import.c:137
GtkWidget * selected_files
Definition import.c:141
GtkWidget * grid
Definition import.c:136
GtkFileFilter * filter_all
Definition import.c:146
char * path_file
Definition import.c:159
GtkWidget * file_chooser
Definition import.c:129
dt_import_scan_state_t * scan_state
Definition import.c:161
gboolean closing
Definition import.c:155
GtkWidget * dialog
Definition import.c:135
GtkWidget * datetime
Definition import.c:134
GtkWidget * preview
Definition import.c:130
GtkFileFilter * filter_raster
Definition import.c:148
guint selection_scan_timeout_id
Definition import.c:142
GtkWidget * test_path
Definition import.c:140
dt_import_filter_type_t last_filter_type
Definition import.c:153
GtkWidget * help_string
Definition import.c:139
GtkWidget * exif
Definition import.c:131
dt_pthread_mutex_t lock
Definition import.c:157
GtkWidget * exif_info[EXIF_LAST_FIELD]
Definition import.c:133
GtkFileFilter * filter_raw
Definition import.c:147
gchar * dt_util_path_get_dirname(const gchar *filename)
Definition utility.c:777
char * dt_util_format_exposure(const float exposuretime)
Definition utility.c:868
#define DT_GUI_BOX_SPACING
#define DT_PIXEL_APPLY_DPI(value)
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)