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
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))
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
830 // Both guards below run BEFORE the file list is built: the list is only ever released
831 // through dt_control_import_data_free() at the end, so anything returning between the two
832 // would have to free it by hand.
833 if(date[0] && !dt_datetime_entry_to_exif(datetime_override, sizeof(datetime_override), date))
834 {
835 dt_control_log(_("invalid date/time format for import"));
836 gtk_label_set_text(GTK_LABEL(d->test_path), _("Please check the date."));
837 return;
838 }
839
840 if(!dt_supported_image(d->path_file))
841 {
842 gtk_label_set_text(GTK_LABEL(d->test_path), _("Choose a file to see the result..."));
843 return;
844 }
845
846 GList *file = g_list_prepend(NULL, g_strdup(d->path_file));
847 gchar *basedir = dt_conf_get_string("session/base_directory_pattern");
848 dt_control_import_t data = {.imgs = file,
849 .datetime = dt_string_to_datetime(date),
850 .copy = 1,
851 .jobcode = dt_conf_get_string("ui_last/import_jobcode"),
852 .base_folder = basedir,
853 .target_subfolder_pattern = dt_conf_get_string("session/sub_directory_pattern"),
854 .target_file_pattern = dt_conf_get_string("session/filename_pattern"),
855 .target_dir = NULL,
856 .elements = 1,
857 .discarded = NULL,
858 };
859
860 gboolean free_after = FALSE;
861 if(IS_NULL_PTR(img))
862 {
863 img = dt_alloc_align(sizeof(dt_image_t)); // dt_image_t is 64-aligned, see #1212
864 dt_image_init(img);
865
866 // Generate file I/O only if the pattern is using EXIF variables.
867 // Otherwise, discard it since it's really expensive if the file is on external/remote storage.
868 // This is mandatory BEFORE expanding variables in pattern
869 if(strstr(data.target_file_pattern, "$(EXIF") != NULL
870 || strstr(data.target_subfolder_pattern, "$(EXIF") != NULL )
871 dt_exif_read(img, (const char*)file->data);
872
873 free_after = TRUE;
874 }
875
876 gchar *_path = dt_build_filename_from_pattern((const char *const)file->data, 1, img, &data);
877 gchar * cut = g_strdup(g_strrstr(basedir, G_DIR_SEPARATOR_S));
878 gchar *fake_path = g_strdup(g_strrstr(_path, cut));
879
880 if(free_after)
881 {
882 dt_free_align(img);
883 }
884
885 if(fake_path && fake_path[0] != 0)
886 _gtk_label_set_and_free(d->test_path, g_strdup_printf(_("...%s"), fake_path));
887 else
888 gtk_label_set_text(GTK_LABEL(d->test_path), _("Can't build a valid path."));
889
890 dt_free(cut);
891 dt_free(_path);
892 dt_free(fake_path);
894}
895
896// Words the selected-files label according to the active GUI filter (raw/raster get called out
897// explicitly; "All" is left exactly as before, per the user's request not to touch that case).
898// When scan_errors > 0, appends a warning so a permission-denied (or otherwise failed) subfolder
899// is visible right in the label instead of only in a transient dt_control_log() toast.
900static void _set_selected_files_label(dt_lib_import_t *d, const guint elements, const guint scan_errors)
901{
902 gchar *text;
903
904 if(elements == 0)
905 {
906 switch(d->last_filter_type)
907 {
909 text = g_strdup(_("No raw file selected"));
910 break;
912 text = g_strdup(_("No raster file selected"));
913 break;
914 default:
915 text = g_strdup(_("No file selected"));
916 break;
917 }
918 }
919 else
920 {
921 switch(d->last_filter_type)
922 {
924 text = g_strdup_printf(_("%i raw files selected"), elements);
925 break;
927 text = g_strdup_printf(_("%i raster files selected"), elements);
928 break;
929 default:
930 text = g_strdup_printf(_("%i files selected"), elements);
931 break;
932 }
933 }
934
935 if(scan_errors > 0)
936 {
937 gchar *warning = g_strdup_printf(ngettext(" -- %u folder could not be scanned (permission denied?)",
938 " -- %u folders could not be scanned (permission denied?)",
939 scan_errors), scan_errors);
940 gchar *combined = g_strconcat(text, warning, NULL);
941 dt_free(text);
942 dt_free(warning);
943 text = combined;
944 }
945
946 _gtk_label_set_and_free(d->selected_files, text);
947}
948
949static void _filelist_changed_callback(gpointer instance, GList *files, guint elements, guint finished, guint scan_errors, gpointer user_data)
950{
951 dt_lib_import_t *d = (dt_lib_import_t *)user_data;
952 if(IS_NULL_PTR(d) || d->closing || IS_NULL_PTR(d->selected_files)) return;
953
954 if(finished)
955 {
956 // Lock the thread to ensure we have the correct final number
957 dt_pthread_mutex_lock(&d->lock);
958 _set_selected_files_label(d, elements, scan_errors);
960 }
961 else
962 {
963 // We don't care for correctness, we just want to show user that we are still at it
964 const char *fmt;
965 switch(d->last_filter_type)
966 {
968 fmt = _("Detection in progress... (%i raw files found so far)");
969 break;
971 fmt = _("Detection in progress... (%i raster files found so far)");
972 break;
973 default:
974 fmt = _("Detection in progress... (%i files found so far)");
975 break;
976 }
977 _gtk_label_set_and_free(d->selected_files, g_strdup_printf(fmt, elements));
978 }
979}
980
982{
983 if(d->closing) return;
984 gtk_label_set_text(GTK_LABEL(d->selected_files), _("Detecting candidate files for import..."));
985
986 // Coalesce bursts of Gtk "selection-changed" signals while navigating file lists.
987 // A short delay avoids queueing redundant recursive scans for transient selections.
988 if(d->selection_scan_timeout_id > 0) g_source_remove(d->selection_scan_timeout_id);
989 d->selection_scan_timeout_id = g_timeout_add(120, _selection_changed_scan_trigger, d);
990}
991
1003static gboolean _selection_changed_scan_trigger(gpointer user_data)
1004{
1005 dt_lib_import_t *d = (dt_lib_import_t *)user_data;
1007 if(d->closing) return G_SOURCE_REMOVE;
1009 return G_SOURCE_REMOVE;
1010}
1011
1013{
1014 gboolean state = gtk_combo_box_get_active(GTK_COMBO_BOX(combobox));
1015 dt_conf_set_bool("ui_last/import_copy", state);
1016 gtk_widget_set_visible(GTK_WIDGET(d->grid), state);
1017 gtk_widget_set_visible(GTK_WIDGET(d->test_path), state);
1019 _set_test_path(d, NULL);
1020}
1021
1022static void _jobcode_changed(GtkFileChooserButton* widget, dt_lib_import_t *d)
1023{
1024 dt_conf_set_string("ui_last/import_jobcode", gtk_entry_get_text(GTK_ENTRY(widget)));
1025 _set_test_path(d, NULL);
1026}
1027
1028static void _base_dir_changed(GtkFileChooserButton* self, dt_lib_import_t *d)
1029{
1030 dt_conf_set_string("session/base_directory_pattern", gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(self)));
1031 _set_test_path(d, NULL);
1032}
1033
1035{
1036 dt_conf_set_string("session/sub_directory_pattern", gtk_entry_get_text(GTK_ENTRY(widget)));
1037 _set_test_path(d, NULL);
1038}
1039
1041{
1042 dt_conf_set_string("session/filename_pattern", gtk_entry_get_text(GTK_ENTRY(widget)));
1043 _set_test_path(d, NULL);
1044}
1045
1046static void _update_date(GtkCalendar *calendar, GtkWidget *entry)
1047{
1048 guint year, month, day;
1049 gtk_calendar_get_date(calendar, &year, &month, &day);
1050 GTimeZone *tz = g_time_zone_new_local();
1051
1052 // Again, GDateTime counts months from 1 but GtkCalendar from 0. Stupid.
1053 GDateTime *datetime = g_date_time_new(tz, year, month + 1, day, 0, 0, 0.);
1054 g_time_zone_unref(tz);
1055 gchar *date = g_date_time_format(datetime, "%F");
1056 gtk_entry_set_text(GTK_ENTRY(entry), date);
1057 dt_free(date);
1058 g_date_time_unref(datetime);
1059}
1060
1061/* Validate user input, aka check if date format respects ISO 8601*/
1062static void _datetime_changed_callback(GtkEntry *entry, dt_lib_import_t *d)
1063{
1064 const char *date = gtk_entry_get_text(entry);
1065 if(date[0])
1066 {
1067 char filtered[DT_DATETIME_LENGTH] = { 0 };
1068 gboolean valid = dt_datetime_entry_to_exif(filtered, sizeof(filtered), date);
1069 if(!valid)
1070 {
1071 gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_SECONDARY, "dialog-error");
1072 gtk_entry_set_icon_tooltip_text(entry, GTK_ENTRY_ICON_SECONDARY,
1073 _("Date should follow the ISO 8601 format, like :\n"
1074 "YYYY-MM-DD\n"
1075 "YYYY-MM-DD HH:mm\n"
1076 "YYYY-MM-DD HH:mm:ss\n"
1077 "YYYY-MM-DDTHH:mm:ss"));
1078 return;
1079 }
1080 else
1081 {
1082 gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_SECONDARY, "");
1083 gtk_entry_set_icon_tooltip_text(entry, GTK_ENTRY_ICON_SECONDARY, "");
1084 }
1085 }
1086 gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, NULL);
1087 _set_test_path(d, NULL);
1088}
1089
1090static void _file_activated(GtkFileChooser *chooser, GtkDialog *dialog)
1091{
1092 // If we double-click on image and we are not asking to duplicate files, let the filechooser
1093 // behave as a replacement of lighttable and directly open the image in darkroom.
1094 if(g_file_test(gtk_file_chooser_get_filename(chooser), G_FILE_TEST_IS_REGULAR)
1095 && !dt_conf_get_bool("ui_last/import_copy"))
1096 {
1097 gtk_dialog_response(dialog, GTK_RESPONSE_ACCEPT);
1098 }
1099}
1100
1101
1111static void _process_file_list(gpointer instance, GList *files, int elements, gboolean finished, guint scan_errors, gpointer user_data)
1112{
1113 if(!finished) return; // Should be fired only when we are done detecting stuff
1114
1115 dt_lib_import_t *d = (dt_lib_import_t *)user_data;
1116 if(IS_NULL_PTR(d) || d->closing) return;
1117
1118 if(elements > 0)
1119 {
1120 // Deep-copy the source list so import job owns an independent set of file path strings.
1121 dt_control_import_t data = {.imgs = g_list_copy_deep(files, (GCopyFunc)g_strdup, NULL),
1122 .datetime = dt_string_to_datetime(gtk_entry_get_text(GTK_ENTRY(d->datetime))),
1123 .copy = dt_conf_get_bool("ui_last/import_copy"),
1124 .jobcode = dt_conf_get_string("ui_last/import_jobcode"),
1125 .base_folder = dt_conf_get_string("session/base_directory_pattern"),
1126 .target_subfolder_pattern = dt_conf_get_string("session/sub_directory_pattern"),
1127 .target_file_pattern = dt_conf_get_string("session/filename_pattern"),
1128 .target_dir = NULL,
1129 .elements = elements,
1130 .discarded = NULL
1131 };
1132
1133 // Prepare to catch the end of import signal
1134 if(dt_control_import(data))
1135 dt_control_log(_("Could not start the import job."));
1136 }
1137 else
1138 dt_control_log(_("No files to import. Check your selection."));
1139
1141 gui_cleanup(d);
1142 _cleanup(d);
1143
1144 // Re-allocate focus to center widget
1146}
1147
1148void _file_chooser_response(GtkDialog *dialog, gint response_id, dt_lib_import_t *d)
1149{
1150 // Stop capturing the filelist changes for the in-popup label file counter.
1152
1153 switch(response_id)
1154 {
1155 case GTK_RESPONSE_ACCEPT:
1156 {
1157 if(d->selection_scan_timeout_id > 0)
1158 {
1159 g_source_remove(d->selection_scan_timeout_id);
1160 d->selection_scan_timeout_id = 0;
1161 }
1162
1163 // The next file list change will now only fire the importer job
1165
1166 // It would be swell if we could just re-use the file list computed on "select" callback.
1167 // However, it depends on the file filter used, and we can't refresh the list when
1168 // filter is changed (no callback to connect to).
1169 // To be safe, we need to start again here, from scratch.
1171
1172 // TODO: print "pending" message on modal window
1173 break;
1174 }
1175 case GTK_RESPONSE_CANCEL:
1176 default:
1177 gui_cleanup(d);
1178 _cleanup(d);
1179 break;
1180 }
1181}
1182
1183
1185{
1187
1188 d->dialog = gtk_dialog_new_with_buttons
1189 ( _("Ansel - Open pictures"), NULL, GTK_DIALOG_DESTROY_WITH_PARENT,
1190 _("Cancel"), GTK_RESPONSE_CANCEL,
1191 _("Import"), GTK_RESPONSE_ACCEPT,
1192 NULL);
1193 dt_gui_add_class(d->dialog, "dt_import_dialog");
1194
1195#ifdef GDK_WINDOWING_QUARTZ
1196// TODO: On MacOS (at least on version 13) the dialog windows doesn't behave as expected. The dialog
1197// needs to have a parent window. "set_parent_window" wasn't working, so set_transient_for is
1198// the way to go. Still the window manager isn't dealing with the dialog properly, when the dialog
1199// is shifted outside its parent. The dialog isn't visible any longer but still listed as a window
1200// of the app.
1202 gtk_window_set_position(GTK_WINDOW(d->dialog), GTK_WIN_POS_CENTER_ON_PARENT);
1203#endif
1204
1205 gtk_window_set_default_size(GTK_WINDOW(d->dialog),
1206 dt_conf_get_int("ui_last/import_dialog_width"),
1207 dt_conf_get_int("ui_last/import_dialog_height"));
1208 gtk_window_set_modal(GTK_WINDOW(d->dialog), FALSE);
1209 gtk_window_set_transient_for(GTK_WINDOW(d->dialog), GTK_WINDOW(dt_gui_main_window()));
1210 g_signal_connect(d->dialog, "response", G_CALLBACK(_file_chooser_response), d);
1211
1212 GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(d->dialog));
1213 g_signal_connect(d->dialog, "check-resize", G_CALLBACK(_resize_dialog), NULL);
1214
1215 /* Grid of options for copy/duplicate */
1216 d->grid = gtk_grid_new();
1217 GtkGrid *grid = GTK_GRID(d->grid);
1218 gtk_grid_set_column_spacing(grid, DT_GUI_BOX_SPACING / 2.);
1219 gtk_grid_set_row_spacing(grid, DT_GUI_BOX_SPACING / 2.);
1220 gtk_grid_set_column_homogeneous(grid, FALSE);
1221 gtk_grid_set_row_homogeneous(grid, FALSE);
1222
1223 /* BOTTOM PANEL */
1224 GtkWidget *rbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1225 gtk_box_pack_start(GTK_BOX(content), rbox, TRUE, TRUE, 0);
1226
1227 // File browser
1228 d->file_chooser = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN);
1229 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(d->file_chooser), TRUE);
1230 gtk_file_chooser_set_use_preview_label(GTK_FILE_CHOOSER(d->file_chooser), FALSE);
1231 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(d->file_chooser),
1232 dt_conf_get_string_const("ui_last/import_last_directory"));
1233 gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(d->file_chooser), FALSE);
1234 gtk_box_pack_start(GTK_BOX(rbox), d->file_chooser, TRUE, TRUE, 0);
1235 g_signal_connect(G_OBJECT(d->file_chooser), "current-folder-changed", G_CALLBACK(_update_directory), NULL);
1236 g_signal_connect(G_OBJECT(d->file_chooser), "file-activated", G_CALLBACK(_file_activated), GTK_DIALOG(d->dialog));
1237 g_signal_connect(G_OBJECT(d->file_chooser), "selection-changed", G_CALLBACK(_selection_changed), d);
1238 g_signal_connect(G_OBJECT(d->file_chooser), "update-preview", G_CALLBACK(update_preview_cb), d);
1239
1240 // file extension filters
1242
1243 // File browser toolbox (extra widgets)
1244 GtkWidget *toolbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1245 gtk_widget_set_halign(toolbox, GTK_ALIGN_END);
1246
1247 GtkWidget *select_all = gtk_button_new_with_label(_("Select all"));
1248 gtk_box_pack_start(GTK_BOX(toolbox), select_all, FALSE, FALSE, 0);
1249 g_signal_connect(select_all, "clicked", G_CALLBACK(_do_select_all_clicked), d);
1250
1251 GtkWidget *select_none = gtk_button_new_with_label(_("Select none"));
1252 gtk_box_pack_start(GTK_BOX(toolbox), select_none, FALSE, FALSE, 0);
1253 g_signal_connect(select_none, "clicked", G_CALLBACK(_do_select_none_clicked), d);
1254
1255 GtkWidget *select_new = gtk_button_new_with_label(_("Select new"));
1256 gtk_box_pack_start(GTK_BOX(toolbox), select_new, FALSE, FALSE, 0);
1257 g_signal_connect(select_new, "clicked", G_CALLBACK(_do_select_new_clicked), d);
1258 gtk_widget_set_tooltip_text(select_new,
1259 _("Selecting new files targets pictures that have never been added to the library. "
1260 "The lookup is done by searching for the original filename and date/time. "
1261 "It can detect files existing at another path, under a different name. "
1262 "False-positive can arise if two pictures have been taken at the same time with the same name."));
1263
1264 d->selected_files = gtk_label_new("");
1265 gtk_box_pack_start(GTK_BOX(toolbox), d->selected_files, FALSE, FALSE, 0);
1266
1267 gtk_file_chooser_set_extra_widget(GTK_FILE_CHOOSER(d->file_chooser), toolbox);
1268
1269 /* RIGHT PANEL */
1270 // File browser preview box
1271 // 1. Thumbnail
1272 GtkWidget *preview_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1273 d->preview = gtk_image_new();
1274 gtk_widget_set_size_request(d->preview, DT_PIXEL_APPLY_DPI(240), DT_PIXEL_APPLY_DPI(240));
1275 gtk_box_pack_start(GTK_BOX(preview_box), d->preview, TRUE, FALSE, 0);
1276
1277 // 2. Exif metadata
1278 d->exif = gtk_grid_new();
1279 gtk_grid_set_column_spacing(GTK_GRID(d->exif), DT_GUI_BOX_SPACING);
1280 _attach_aligned_grid_item(d->exif, 0, 0, _("Shot:"), GTK_ALIGN_END, FALSE, FALSE);
1281 _attach_grid_separator( d->exif, 1, 2);
1282 _attach_aligned_grid_item(d->exif, 2, 0, _("Camera:"), GTK_ALIGN_END, FALSE, FALSE);
1283 _attach_aligned_grid_item(d->exif, 3, 0, _("Brand:"), GTK_ALIGN_END, FALSE, FALSE);
1284 _attach_aligned_grid_item(d->exif, 4, 0, _("Lens:"), GTK_ALIGN_END, FALSE, FALSE);
1285 _attach_aligned_grid_item(d->exif, 5, 0, _("Focal:"), GTK_ALIGN_END, FALSE, FALSE);
1286 _attach_grid_separator( d->exif, 6, 2);
1287 // exposure trifecta
1288 _attach_grid_separator( d->exif, 8, 2);
1289
1290 GtkWidget *imported_label = gtk_label_new(_("Imported:"));
1291 GtkBox *help_box_inlib = attach_help_popover(
1292 imported_label,
1293 _("Images already in the library will not be imported again, selected or not. "
1294 "Remove them from the library first, or use the menu "
1295 "`Run \342\206\222 Resynchronize library and XMP` to update the local database from distant XMP.\n\n"
1296 "Ansel indexes images by their filename and parent folder (full path), "
1297 "not by their content. Therefore, renaming or moving images on the filesystem, "
1298 "or changing the mounting point of their external drive will make them "
1299 "look like new (unknown) images.\n\n"
1300 "If an XMP file is present alongside images, it will be imported as well, "
1301 "including the metadata and settings stored in it. If it is not what you want, "
1302 "you can reset metadata in the lighttable."));
1303 gtk_widget_set_halign(imported_label, GTK_ALIGN_END);
1304 gtk_grid_attach(GTK_GRID(d->exif), GTK_WIDGET(help_box_inlib), 0, EXIF_INLIB_FIELD, 1, 1);
1305 //_attach_aligned_grid_item(d->exif, 9, 0, _("Imported :"), GTK_ALIGN_END, FALSE, FALSE);
1306
1307 d->exif_info[EXIF_DATETIME_FIELD] = _attach_aligned_grid_item(d->exif, 0, 1, "", GTK_ALIGN_START, TRUE, FALSE);
1308 d->exif_info[EXIF_MODEL_FIELD] = _attach_aligned_grid_item(d->exif, 2, 1, "", GTK_ALIGN_START, TRUE, FALSE);
1309 d->exif_info[EXIF_MAKER_FIELD] = _attach_aligned_grid_item(d->exif, 3, 1, "", GTK_ALIGN_START, TRUE, FALSE);
1310 d->exif_info[EXIF_LENS_FIELD] = _attach_aligned_grid_item(d->exif, 4, 1, "", GTK_ALIGN_START, TRUE, FALSE);
1311 d->exif_info[EXIF_FOCAL_LENS_FIELD] = _attach_aligned_grid_item(d->exif, 5, 1, "", GTK_ALIGN_START, TRUE, FALSE);
1312 d->exif_info[EXIF_EXPOSURE_FIELD] = _attach_aligned_grid_item(d->exif, 7, 0, "", GTK_ALIGN_CENTER, TRUE, TRUE);
1313 d->exif_info[EXIF_INLIB_FIELD] = _attach_aligned_grid_item(d->exif, 9, 1, "", GTK_ALIGN_START, FALSE, TRUE);
1314 d->exif_info[EXIF_PATH_FIELD] = _attach_aligned_grid_item(d->exif, 10, 0, "", GTK_ALIGN_START, FALSE, TRUE);
1315 gtk_label_set_ellipsize(GTK_LABEL(d->exif_info[EXIF_PATH_FIELD]), PANGO_ELLIPSIZE_MIDDLE);
1316
1317 gtk_box_pack_start(GTK_BOX(preview_box), d->exif, TRUE, TRUE, 0);
1318 gtk_widget_show_all(d->exif);
1319
1320 gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(d->file_chooser), preview_box);
1321 /* BOTTOM PANEL */
1322
1323 GtkWidget *files = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1324 GtkWidget *file_handling = gtk_label_new("");
1325 gtk_label_set_markup(GTK_LABEL(file_handling), _("<b>File handling</b>"));
1326 gtk_box_pack_start(GTK_BOX(files), GTK_WIDGET(file_handling), FALSE, FALSE, 0);
1327
1328 GtkWidget *copy = gtk_combo_box_text_new();
1329 gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(copy), NULL, _("Add to library"));
1330 gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(copy), NULL, _("Copy to disk"));
1331 gtk_combo_box_set_active(GTK_COMBO_BOX(copy), dt_conf_get_bool("ui_last/import_copy"));
1332 gtk_box_pack_start(GTK_BOX(files), GTK_WIDGET(copy), FALSE, FALSE, 0);
1333 g_signal_connect(G_OBJECT(copy), "changed", G_CALLBACK(_copy_toggled_callback), (gpointer)d);
1334
1335 d->help_string = gtk_label_new("");
1336 _set_help_string(d, dt_conf_get_bool("ui_last/import_copy"));
1337 gtk_box_pack_start(GTK_BOX(files), GTK_WIDGET(d->help_string), FALSE, FALSE, 0);
1338
1339 gtk_box_pack_start(GTK_BOX(rbox), GTK_WIDGET(files), FALSE, FALSE, 0);
1340
1341 // Project date
1342 GtkWidget *calendar_label = gtk_label_new(_("Project date"));
1343 gtk_widget_set_halign(calendar_label, GTK_ALIGN_START);
1344 d->datetime = gtk_entry_new();
1346 gtk_entry_set_width_chars(GTK_ENTRY(d->datetime), 20);
1347 g_signal_connect(G_OBJECT(d->datetime), "changed", G_CALLBACK(_datetime_changed_callback), d);
1348
1349 // Date is inited as today by default
1350 GDateTime *now = g_date_time_new_now_local();
1351 gchar *now_string = g_date_time_format(now, "%F");
1352 gtk_entry_set_text(GTK_ENTRY(d->datetime), now_string);
1353 dt_free(now_string);
1354
1355 // Date chooser
1356 GtkWidget *calendar = gtk_calendar_new();
1357 // GtkCalendar uses monthes in [0:11]. Glib GDateTime returns monthes in [1:12]. Stupid.
1358 gtk_calendar_select_month(GTK_CALENDAR(calendar), g_date_time_get_month(now) - 1, g_date_time_get_year(now));
1359 const guint day = g_date_time_get_day_of_month(now);
1360 gtk_calendar_select_day(GTK_CALENDAR(calendar), day);
1361 gtk_calendar_mark_day(GTK_CALENDAR(calendar), day);
1362 GtkBox *box_calendar = attach_popover(d->datetime, "appointment-new-symbolic", calendar);
1363 g_signal_connect(G_OBJECT(calendar), "day-selected", G_CALLBACK(_update_date), d->datetime);
1364
1365 // free date
1366 g_date_time_unref(now);
1367
1368 // Base directory of projects
1369 GtkWidget *jobcode = gtk_entry_new();
1371 gtk_entry_set_text(GTK_ENTRY(jobcode), dt_conf_get_string_const("ui_last/import_jobcode"));
1372 gtk_widget_set_hexpand(jobcode, TRUE);
1373 g_signal_connect(G_OBJECT(jobcode), "changed", G_CALLBACK(_jobcode_changed), d);
1374
1375 GtkWidget *jobcode_label = gtk_label_new(_("Jobcode"));
1376 gtk_widget_set_halign(jobcode_label, GTK_ALIGN_START);
1377
1378 GtkWidget *base_label = gtk_label_new(_("Base directory of all projects"));
1379 gtk_widget_set_halign(base_label, GTK_ALIGN_START);
1380
1381 GtkWidget *dir_label = gtk_label_new(_("Project directory naming pattern"));
1382 gtk_widget_set_halign(dir_label, GTK_ALIGN_START);
1383
1384 GtkWidget *file_label = gtk_label_new(_("File naming pattern"));
1385 gtk_widget_set_halign(file_label, GTK_ALIGN_START);
1386
1387 GtkWidget *base_dir
1388 = gtk_file_chooser_button_new(_("Select a base directory"), GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER);
1389 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(base_dir),
1390 dt_conf_get_string_const("session/base_directory_pattern"));
1391 g_signal_connect(G_OBJECT(base_dir), "file-set", G_CALLBACK(_base_dir_changed), d);
1392 gtk_widget_set_hexpand(base_dir, TRUE);
1393
1394 GtkWidget *sep1 = gtk_label_new(G_DIR_SEPARATOR_S);
1395 GtkWidget *sep2 = gtk_label_new(G_DIR_SEPARATOR_S);
1396
1397 GtkWidget *project_dir = gtk_entry_new();
1399 gtk_entry_set_text(GTK_ENTRY(project_dir), dt_conf_get_string_const("session/sub_directory_pattern"));
1400 gtk_widget_set_hexpand(project_dir, TRUE);
1402 gtk_widget_set_tooltip_text(project_dir, _("Start typing `$(` to see available variables through auto-completion"));
1403 g_signal_connect(G_OBJECT(project_dir), "changed", G_CALLBACK(_project_dir_changed), d);
1404
1405 GtkWidget *file = gtk_entry_new();
1407 gtk_entry_set_text(GTK_ENTRY(file), dt_conf_get_string_const("session/filename_pattern"));
1408 gtk_widget_set_hexpand(file, TRUE);
1410 g_signal_connect(G_OBJECT(file), "changed", G_CALLBACK(_filename_changed), d);
1411
1412 GtkWidget *pattern_label = gtk_label_new(_("Pattern result"));
1413 gtk_widget_set_halign(pattern_label, GTK_ALIGN_START);
1414
1415 d->test_path = gtk_label_new(_("Choose a file to see the result..."));
1416 gtk_widget_set_halign(d->test_path, GTK_ALIGN_START);
1417 gtk_label_set_line_wrap(GTK_LABEL(d->test_path), TRUE);
1418 gtk_label_set_max_width_chars(GTK_LABEL(d->test_path), 60);
1419 _set_test_path(d, NULL);
1420
1421 /* Create the grid of import params when using duplication */
1422 int row = 0;
1423 _attach_grid_separator(GTK_WIDGET(grid), row, 5);
1424 row++;
1425
1426 // Row 0: labels for text entries
1427 gtk_grid_attach(grid, calendar_label, 0, row, 1, 1);
1428 gtk_grid_attach(grid, jobcode_label, 2, row, 1, 1);
1429 gtk_grid_attach(grid, pattern_label, 4, row, 1, 1);
1430 row++;
1431
1432 // Row 1: text entries
1433 gtk_grid_attach(grid, GTK_WIDGET(box_calendar), 0, row, 1, 1);
1434 gtk_grid_attach(grid, jobcode, 2, row, 1, 1);
1435 gtk_grid_attach(grid, d->test_path, 4, row, 1, 1);
1436 row++;
1437
1438 // Row 2: separator
1439 _attach_grid_separator(GTK_WIDGET(grid), row, 5);
1440 row++;
1441
1442 // Row 3: labels for text entries
1443 gtk_grid_attach(grid, base_label, 0, row, 1, 1);
1444 gtk_grid_attach(grid, dir_label, 2, row, 1, 1);
1445 gtk_grid_attach(grid, file_label, 4, row, 1, 1);
1446 row++;
1447
1448 // Row 4: text entries
1449 gtk_grid_attach(grid, base_dir, 0, row, 1, 1);
1450 gtk_grid_attach(grid, sep1, 1, row, 1, 1);
1451 gtk_grid_attach(grid, project_dir, 2, row, 1, 1);
1452 gtk_grid_attach(grid, sep2, 3, row, 1, 1);
1453 gtk_grid_attach(grid, file, 4, row, 1, 1);
1454 row++;
1455
1456 gtk_box_pack_start(GTK_BOX(rbox), GTK_WIDGET(grid), FALSE, FALSE, 0);
1457
1458 gtk_widget_show_all(d->dialog);
1459
1460 // Duplication parameters visible only if the option is set
1461 gtk_widget_set_visible(GTK_WIDGET(grid), dt_conf_get_bool("ui_last/import_copy"));
1462
1463 // Update the number of selected files string because Gtk forces a default selection at opening time
1465 G_CALLBACK(_filelist_changed_callback), d);
1466}
1467
1469{
1470 gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(d->file_chooser));
1471}
1472
1474{
1475 gtk_file_chooser_select_all(GTK_FILE_CHOOSER(d->file_chooser));
1476}
1477
1479{
1480 // Twisted Gtk doesn't let us select multiple files.
1481 // We need to select all then unselect what we don't want.
1483
1484 GtkFileChooser *chooser = GTK_FILE_CHOOSER(d->file_chooser);
1485 gchar *folder = gtk_file_chooser_get_current_folder(chooser);
1486 if(IS_NULL_PTR(folder)) return;
1487
1488 GFile *folder_file = g_file_new_for_path(folder);
1489 GFileEnumerator *files = NULL;
1490 if(IS_NULL_PTR(folder_file))
1491 goto end;
1492
1493 files = g_file_enumerate_children(
1494 folder_file, G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
1495 G_FILE_QUERY_INFO_NONE, NULL, NULL);
1496 g_object_unref(folder_file);
1497 if(IS_NULL_PTR(files))
1498 goto end;
1499
1500 // Get the file filter in use
1501 GtkFileFilter *filter = gtk_file_chooser_get_filter(chooser);
1502 if(IS_NULL_PTR(filter))
1503 {
1504 goto end;
1505 }
1506 const GtkFileFilterFlags filter_needed = gtk_file_filter_get_needed(filter);
1507
1508 GFile *file = NULL;
1509 while(g_file_enumerator_iterate(files, NULL, &file, NULL, NULL))
1510 {
1511 // g_file_enumerator_iterate returns FALSE only on errors, not on end of enumeration.
1512 // We need an ugly break here else infinite loop.
1513 if(IS_NULL_PTR(file)) break;
1514
1515 gchar *parse_name = g_file_get_parse_name(file);
1516 gchar *uri = g_file_get_uri(file);
1517 gchar *basename = g_file_get_basename(file);
1518 gchar *filepath = g_file_get_path(file);
1519 GtkFileFilterInfo filter_info = { filter_needed,
1520 parse_name,
1521 uri,
1522 parse_name, NULL };
1523
1524 const gboolean is_regular = !IS_NULL_PTR(filepath) && g_file_test(filepath, G_FILE_TEST_IS_REGULAR);
1525 const int is_path_in_lib = !IS_NULL_PTR(basename) ? _is_in_library_by_path(folder, basename) : -1;
1526 const int is_metadata_in_lib = _is_in_library_by_metadata(file);
1527 const gboolean is_in_lib = (is_path_in_lib > -1) || (is_metadata_in_lib > -1);
1528
1529 // We need to act only on files passing the file filter, aka being currently displayed on screen.
1530 // Unselecting files not displayed in the current list freezes the UI and introduces oddities.
1531 if(gtk_file_filter_filter(filter, &filter_info)
1532 && !(is_regular && !is_in_lib))
1533 {
1534 gtk_file_chooser_unselect_file(chooser, file);
1535 }
1536
1537 dt_free(parse_name);
1538 dt_free(uri);
1539 dt_free(basename);
1540 dt_free(filepath);
1541 // g_file_enumerator_iterate() returns transfer-none children owned by the enumerator.
1542 // Unref happens when the enumerator advances or is destroyed.
1543 file = NULL;
1544 }
1545
1546 end:
1547 if(!IS_NULL_PTR(files)) g_object_unref(files);
1548 dt_free(folder);
1549}
1550
1552{
1553 d->closing = TRUE;
1554
1555 if(d->selection_scan_timeout_id > 0)
1556 {
1557 g_source_remove(d->selection_scan_timeout_id);
1558 d->selection_scan_timeout_id = 0;
1559 }
1560
1561 // Disconnect callbacks that may enqueue async work while widgets are being destroyed.
1562 if(!IS_NULL_PTR(d->file_chooser))
1563 {
1564 g_signal_handlers_disconnect_by_func(G_OBJECT(d->file_chooser), G_CALLBACK(_selection_changed), d);
1565 g_signal_handlers_disconnect_by_func(G_OBJECT(d->file_chooser), G_CALLBACK(update_preview_cb), d);
1566 g_signal_handlers_disconnect_by_func(G_OBJECT(d->file_chooser), G_CALLBACK(_file_activated), GTK_DIALOG(d->dialog));
1567 g_signal_handlers_disconnect_by_func(G_OBJECT(d->file_chooser), G_CALLBACK(_update_directory), NULL);
1568 }
1569
1570 // Ensure the background recursive folder detection is finished before destroying widgets.
1571 // Reason is, if a job is still running, it might send its signal upon completion,
1572 // and then the widgets supposed to be updated in callback will be undefined (but not NULL... WTF Gtk ?)
1573 dt_pthread_mutex_lock(&d->lock);
1574 gtk_widget_destroy(d->dialog);
1575 d->dialog = NULL;
1576 d->file_chooser = NULL;
1577 d->preview = NULL;
1578 d->exif = NULL;
1579 d->grid = NULL;
1580 d->jobcode = NULL;
1581 d->help_string = NULL;
1582 d->test_path = NULL;
1583 d->selected_files = NULL;
1584 d->filter_all = NULL;
1585 d->filter_raw = NULL;
1586 d->filter_raster = NULL;
1587 for(int k = 0; k < EXIF_LAST_FIELD; k++) d->exif_info[k] = NULL;
1588 dt_pthread_mutex_unlock(&d->lock);
1589}
1590
1592{
1593 dt_lib_import_t *d = malloc(sizeof(dt_lib_import_t));
1594 d->closing = FALSE;
1595 d->selection_scan_timeout_id = 0;
1596 dt_pthread_mutex_init(&d->lock, NULL);
1597 d->path_file = NULL;
1598 d->scan_state = calloc(1, sizeof(dt_import_scan_state_t));
1599 dt_pthread_mutex_init(&d->scan_state->lock, NULL);
1600 d->scan_state->generation = 0;
1601 d->scan_state->refcount = 1;
1602 d->scan_state->closing = FALSE;
1603
1604 return d;
1605}
1606
1608{
1609 // Teardown can be entered from multiple control paths. Ensure no pending global signal
1610 // callback can still target this module state after memory is released.
1613
1614 if(!IS_NULL_PTR(d->scan_state))
1615 {
1616 gboolean release = FALSE;
1617 dt_pthread_mutex_lock(&d->scan_state->lock);
1618 d->scan_state->closing = TRUE;
1619 d->scan_state->generation++;
1620 if(d->scan_state->refcount > 0) d->scan_state->refcount--;
1621 release = (d->scan_state->refcount == 0);
1622 dt_pthread_mutex_unlock(&d->scan_state->lock);
1623 if(release)
1624 {
1625 dt_pthread_mutex_destroy(&d->scan_state->lock);
1626 dt_free(d->scan_state);
1627 }
1628 d->scan_state = NULL;
1629 }
1630
1632 dt_free(d->path_file);
1633 dt_free(d);
1634}
1635
1637{
1638 dt_lib_import_t *d = _init();
1639 gui_init(d);
1640}
1641
1642static dt_import_t * dt_import_init(dt_lib_import_t *d, const uint32_t generation)
1643{
1644 dt_import_t *import = g_malloc(sizeof(dt_import_t));
1645 import->generation = generation;
1646 import->files = NULL;
1647 import->elements = 0;
1648 import->scan_errors = 0;
1649 dt_pthread_mutex_init(&import->lock, NULL);
1650 import->scan_state = d->scan_state;
1651 dt_pthread_mutex_lock(&import->scan_state->lock);
1652 import->scan_state->refcount++;
1653 dt_pthread_mutex_unlock(&import->scan_state->lock);
1654
1655 dt_pthread_mutex_lock(&import->lock);
1656
1657 // selection is owned here and will need to be freed.
1658 import->selection = gtk_file_chooser_get_uris(GTK_FILE_CHOOSER(d->file_chooser));
1659
1660 // Snapshot the active file-type filter here, on the GUI thread, into a plain enum: the
1661 // recursive scan job runs off-thread and must never touch the live GtkFileFilter objects.
1662 GtkFileFilter *active_filter = gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(d->file_chooser));
1663 if(!IS_NULL_PTR(active_filter) && active_filter == d->filter_raw)
1664 import->filter_type = DT_IMPORT_FILTER_RAW;
1665 else if(!IS_NULL_PTR(active_filter) && active_filter == d->filter_raster)
1666 import->filter_type = DT_IMPORT_FILTER_RASTER;
1667 else
1668 import->filter_type = DT_IMPORT_FILTER_ALL;
1669
1670 // Mirror it onto d too: _filelist_changed_callback() only gets files/elements/finished from
1671 // the signal, not the dt_import_t this scan belongs to.
1672 d->last_filter_type = import->filter_type;
1673
1674 dt_pthread_mutex_unlock(&import->lock);
1675
1676 return import;
1677}
1678
1679static void dt_import_cleanup(void *data)
1680{
1681 // dt_import_t owns the recursive selection list for the whole detection job lifetime.
1682 // Signal receivers may inspect it, but must not release it.
1683 dt_import_t *import = (dt_import_t *)data;
1684 g_list_free_full(import->files, dt_free_gpointer);
1685 import->files = NULL;
1686 g_slist_free_full(import->selection, dt_free_gpointer);
1687 import->selection = NULL;
1688 if(!IS_NULL_PTR(import->scan_state))
1689 {
1690 gboolean release = FALSE;
1691 dt_pthread_mutex_lock(&import->scan_state->lock);
1692 if(import->scan_state->refcount > 0) import->scan_state->refcount--;
1693 release = (import->scan_state->refcount == 0);
1694 dt_pthread_mutex_unlock(&import->scan_state->lock);
1695 if(release)
1696 {
1697 dt_pthread_mutex_destroy(&import->scan_state->lock);
1698 dt_free(import->scan_state);
1699 }
1700 import->scan_state = NULL;
1701 }
1702 dt_pthread_mutex_destroy(&import->lock);
1703 dt_free(import);
1704}
1705
1706/* Shows what an import left behind. Lived in control/jobs/import_jobs.c -- the only GUI
1707 * code in that job file; the job invokes it through the registered handler and this
1708 * function owns freeing the enumerator params and import data, as the contract says. */
1710{
1711 dt_control_import_t *data = params->data;
1712
1713 // Create the window
1714 GtkWidget *dialog = gtk_dialog_new_with_buttons("Message",
1715 GTK_WINDOW(dt_gui_main_window()),
1716 GTK_DIALOG_DESTROY_WITH_PARENT,
1717 _("_OK"),
1718 GTK_RESPONSE_NONE,
1719 NULL);
1720 gtk_window_set_title(GTK_WINDOW(dialog), _("Some files have not been copied"));
1721 gtk_window_set_default_size(GTK_WINDOW(dialog), DT_PIXEL_APPLY_DPI(800), DT_PIXEL_APPLY_DPI(800));
1722
1723 // Create the label
1724 GtkWidget *label = gtk_label_new(_("The following source files have not been copied "
1725 "because similarly-named files already exist on the destination. "
1726 "This may be because the files have already been imported "
1727 "or the naming pattern leads to non-unique file names."));
1728 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
1729
1730 // Create the scrolled window internal container
1731 GtkWidget *scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1732 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window), GTK_POLICY_AUTOMATIC,
1733 GTK_POLICY_AUTOMATIC);
1734 gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(scrolled_window), TRUE);
1735
1736 // Create the treeview model from the list of discarded file pathes
1737 GtkListStore *store = gtk_list_store_new(1, G_TYPE_STRING);
1738 GtkTreeIter iter;
1739 for(GList *file = g_list_first(data->discarded); file; file = g_list_next(file))
1740 {
1741 if(file->data)
1742 {
1743 gtk_list_store_append(store, &iter);
1744 gtk_list_store_set(store, &iter, 0, (char *)file->data, -1);
1745 }
1746 }
1747
1748 // Create the treeview view. Sooooo verbose... it's only a flat list.
1749 GtkWidget *view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
1750 GtkTreeViewColumn *col = gtk_tree_view_column_new();
1751 gtk_tree_view_column_set_title(col, _("Origin path"));
1752 GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
1753 gtk_tree_view_column_pack_start(col, renderer, TRUE);
1754 gtk_tree_view_column_set_attributes(col, renderer, "text", 0, NULL);
1755 gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
1756 g_object_unref(store);
1757
1758 // Pack widgets to an unified box
1759 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1760 gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0);
1761 gtk_box_pack_start(GTK_BOX(box), scrolled_window, TRUE, TRUE, 0);
1762 dt_gui_add_class(scrolled_window, "dt_recessed_scroll");
1763 gtk_container_add(GTK_CONTAINER(scrolled_window), view);
1764
1765 // Pack the box to the dialog internal container
1766 GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
1767 gtk_container_add(GTK_CONTAINER(content_area), box);
1768 gtk_widget_show_all(dialog);
1769
1770#ifdef GDK_WINDOWING_QUARTZ
1772#endif
1773
1774 gtk_dialog_run(GTK_DIALOG(dialog));
1775 gtk_widget_destroy(dialog);
1776
1778 dt_free(data);
1780
1781 return 0;
1782}
1783
1784/* Runs on the import worker thread: hop to the GUI main loop, where the dialog lives.
1785 * The popup owns freeing params, per the handler contract. */
1787{
1788 g_main_context_invoke(NULL, (GSourceFunc)_import_discarded_files_popup, params);
1789}
1790
1795
1796// clang-format off
1797// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
1798// vim: shiftwidth=2 expandtab tabstop=2 cindent
1799// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
1800// 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:839
struct dt_control_t * dt_control_get_global(void)
Definition darktable.c:654
void dt_control_image_enumerator_cleanup(void *p)
void * dt_alloc_align(size_t size)
Allocate cacheline-aligned memory.
Definition darktable.c:511
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:237
@ ORIENTATION_ROTATE_CW_90_DEG
Definition image.h:238
@ ORIENTATION_ROTATE_180_DEG
Definition image.h:235
@ DT_IMAGEIO_OK
Definition image.h:98
dt_image_flags_t
Definition image.h:113
@ DT_IMAGE_RAW
Definition image.h:133
@ DT_IMAGE_HDR
Definition image.h:135
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:384
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:1679
static void _datetime_changed_callback(GtkEntry *entry, dt_lib_import_t *d)
Definition import.c:1062
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:1709
void _dt_check_basedir()
Definition import.c:515
static void _import_discarded_files_schedule(dt_control_image_enumerator_t *params)
Definition import.c:1786
static void _update_date(GtkCalendar *calendar, GtkWidget *entry)
Definition import.c:1046
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:1111
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:1040
static void _selection_changed(GtkWidget *filechooser, dt_lib_import_t *d)
Definition import.c:981
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:1551
void dt_gui_import_init_handlers(void)
Register the GUI-side import handlers (the discarded-files recap dialog).
Definition import.c:1791
static dt_import_t * dt_import_init(dt_lib_import_t *d, const uint32_t generation)
Definition import.c:1642
static void _set_selected_files_label(dt_lib_import_t *d, const guint elements, const guint scan_errors)
Definition import.c:900
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:1022
static void _copy_toggled_callback(GtkWidget *combobox, dt_lib_import_t *d)
Definition import.c:1012
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:1090
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:949
void dt_images_import()
Definition import.c:1636
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:1478
static void _base_dir_changed(GtkFileChooserButton *self, dt_lib_import_t *d)
Definition import.c:1028
static void _cleanup(dt_lib_import_t *d)
Definition import.c:1607
static dt_lib_import_t * _init()
Definition import.c:1591
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:1184
static void _project_dir_changed(GtkWidget *widget, dt_lib_import_t *d)
Definition import.c:1034
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:1473
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:1148
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:1003
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:1468
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:80
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:138
int dt_control_add_job(dt_control_t *control, dt_job_queue_t queue_id, _dt_job_t *job)
Definition jobs.c:419
void * dt_control_job_get_params(const _dt_job_t *job)
Definition jobs.c:132
void dt_control_job_set_params(_dt_job_t *job, void *params, dt_job_destroy_callback callback)
Definition jobs.c:115
void dt_control_job_dispose(_dt_job_t *job)
Definition jobs.c:156
@ DT_JOB_QUEUE_USER_BG
Definition jobs.h:57
static const dt_iop_drawlayer_params_t * params
Definition layers.h:80
@ 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:411
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
Definition signal.h:390
struct dt_control_signal_t * dt_control_signal_get_global(void)
Definition darktable.c:619
@ DT_SIGNAL_FILELIST_CHANGED
Raised when the recursive file crawler returns. no params, return :
Definition signal.h:333
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:400
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:376
float exif_iso
Definition image.h:379
float exif_aperture
Definition image.h:378
dt_image_orientation_t orientation
Definition image.h:375
float exif_focal_length
Definition image.h:380
char exif_maker[64]
Definition image.h:383
char exif_lens[128]
Definition image.h:385
char exif_model[64]
Definition image.h:384
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:800
char * dt_util_format_exposure(const float exposuretime)
Definition utility.c:891
#define DT_GUI_BOX_SPACING
#define DT_PIXEL_APPLY_DPI(value)
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)