Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
crawler.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2014, 2016 Roman Lebedev.
4 Copyright (C) 2014-2016, 2020 Tobias Ellinghaus.
5 Copyright (C) 2017 parafin.
6 Copyright (C) 2018 Peter Budai.
7 Copyright (C) 2019, 2021-2023, 2025-2026 Aurélien PIERRE.
8 Copyright (C) 2020 esq4.
9 Copyright (C) 2020-2021 Hubert Kowalski.
10 Copyright (C) 2020-2022 Pascal Obry.
11 Copyright (C) 2020 Philippe Weyland.
12 Copyright (C) 2021 Hanno Schwalm.
13 Copyright (C) 2021 Marco.
14 Copyright (C) 2021 Marco Carrarini.
15 Copyright (C) 2021 Miloš Komarčević.
16 Copyright (C) 2021 Ralf Brown.
17 Copyright (C) 2022 Martin Bařinka.
18 Copyright (C) 2023 Luca Zulberti.
19
20 darktable is free software: you can redistribute it and/or modify
21 it under the terms of the GNU General Public License as published by
22 the Free Software Foundation, either version 3 of the License, or
23 (at your option) any later version.
24
25 darktable is distributed in the hope that it will be useful,
26 but WITHOUT ANY WARRANTY; without even the implied warranty of
27 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 GNU General Public License for more details.
29
30 You should have received a copy of the GNU General Public License
31 along with darktable. If not, see <http://www.gnu.org/licenses/>.
32*/
33
34#include <glib.h>
35#include "common/paths.h" // DT_PATH_MAX
36#include <gio/gio.h>
37#include <gtk/gtk.h>
38#include <glib/gstdio.h>
39#include <stdio.h>
40#include <string.h>
41
42#include "common/logging.h"
44#include "system/macros.h"
45#include "system/mem_alloc.h"
46#include "caches/image_cache.h"
47#include "control/control.h"
48#include "control/jobs.h"
50#include "common/image.h"
51#include "crawler.h"
52#include "gui/application.h"
55#ifdef GDK_WINDOWING_QUARTZ
56#include "osx/osx.h"
57#endif
58
59
73
81
83{
84 dt_free(entry->image_path);
85 dt_free(entry->xmp_path);
86 entry->image_path = entry->xmp_path = NULL;
87}
88
89static void _free_crawler_results(GList *results)
90{
91 for(GList *l = results; !IS_NULL_PTR(l); l = g_list_next(l))
93 g_list_free_full(results, dt_free_gpointer);
94}
95
96static void _set_modification_time(char *filename,
97 const time_t timestamp)
98{
99 GFile *gfile = g_file_new_for_path(filename);
100
101 GFileInfo *info = g_file_query_info(
102 gfile,
103 G_FILE_ATTRIBUTE_TIME_MODIFIED "," G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC,
104 G_FILE_QUERY_INFO_NONE,
105 NULL,
106 NULL);
107
108 // For reference, we could use the following lines but for some
109 // reasons there is a deprecated message raised even though this
110 // routine is not marked as deprecated in the documentation.
111 //
112 // GDateTime *datetime = g_date_time_new_from_unix_local(timestamp);
113 // g_file_info_set_modification_date_time(info, datetime);
114
115 if(info)
116 {
117 g_file_info_set_attribute_uint64
118 (info,
119 G_FILE_ATTRIBUTE_TIME_MODIFIED,
120 timestamp);
121
122 g_file_set_attributes_from_info(
123 gfile,
124 info,
125 G_FILE_QUERY_INFO_NONE,
126 NULL,
127 NULL);
128 }
129
130 g_object_unref(gfile);
131 if(info) g_clear_object(&info);
132}
133
134/* A folder's contents as one lookup table: basename -> modification time.
135 *
136 * The crawler asks up to six questions about every image -- does the image still exist, does
137 * its XMP exist and when was it last written, is there a .txt/.TXT/.wav/.WAV beside it -- and
138 * used to answer each one with its own stat(). On a network filesystem the round-trip, not the
139 * work, is the entire cost: measured at 8.1 ms per stat() on a GVFS/SMB share, a 1969-image
140 * library spent 102 s in dt_control_crawler_run() before the main window was ever built.
141 *
142 * One directory listing answers all six questions for every image in that folder, and carries
143 * the modification times with it -- SMB returns them in the listing itself, so the XMP
144 * timestamp costs nothing beyond the listing. Same library, same share: 1.1 s for 3967 entries
145 * across 18 folders.
146 *
147 * Do NOT "improve" this by parallelising the per-file lookups instead. That was measured on
148 * the same share and does not work: gvfsd-fuse multiplexes every FUSE request through a single
149 * daemon, so 4 threads gained 4% (inside the noise) and 64 threads ran twice as slow as one.
150 * What this path needs is fewer round-trips, not overlapping ones.
151 */
152/* Windows and macOS resolve a filename without regard to case, and so does an SMB server:
153 * stat() found `IMG.NEF.XMP' when asked for `IMG.NEF.xmp', and an exact lookup in a listing
154 * does not. So a folder carries a second index, of casefolded names, consulted only when the
155 * exact name misses and built on that first miss -- a library whose names all agree with the
156 * database never pays for it.
157 *
158 * That index is a FILTER, not an answer. It says a file exists under some other spelling;
159 * whether the database's own spelling resolves to it is the filesystem's call, so a stat() of
160 * that spelling decides -- the very question the per-file code asked, put to the same
161 * filesystem. Found where the filesystem folds case; "missing" where it does not (ext4), and
162 * rightly: there the database's name really names no file, and taking the other one would
163 * hand this image another file's sidecar and companions. That costs one stat() per name that
164 * exists only under another spelling, which on an agreeing library is none.
165 *
166 * The key is Unicode-normalised before it is folded: macOS stores names decomposed, and a
167 * database may carry the composed spelling of the same name. The stat() is what makes a
168 * generous key safe -- at worst it asks the filesystem once for nothing. */
170{
171 gchar *path; // the folder itself, for the stat() confirming a casefold hit
172 GHashTable *exact; // basename -> guint64 *mtime, owns both
173 GHashTable *folded; // set of normalised, casefolded basenames; NULL until needed
175
176static void _free_folder(gpointer p)
177{
179 if(!IS_NULL_PTR(folder->folded)) g_hash_table_destroy(folder->folded);
180 g_hash_table_destroy(folder->exact);
181 dt_free(folder->path);
183}
184
185/* The spelling two names share when only case or Unicode composition sets them apart. */
186static gchar *_folded_key(const char *name)
187{
188 // a name that is not UTF-8 -- possible on a POSIX filesystem -- has no case to fold
189 if(!g_utf8_validate(name, -1, NULL)) return g_strdup(name);
190
191 gchar *normalised = g_utf8_normalize(name, -1, G_NORMALIZE_DEFAULT);
192 gchar *key = g_utf8_casefold(normalised, -1);
193 dt_free(normalised);
194 return key;
195}
196
197typedef struct dt_crawler_walk_t
198{
199 GList **result;
200 GHashTable *folders; // dirname -> dt_crawler_folder_t *
201 dt_job_t *job; // NULL for a crawl the user asked for from the menu: it runs to its end
202 int images; // rows walked, for the one line this prints at the end
203 int listings; // directories enumerated; a re-listing past the cache cap counts twice
205
206/* A crawl run as a job stops when the job is cancelled, and when Ansel quits. Nothing cancels a
207 * running job on the way out -- dt_control_quit() and dt_control_shutdown() only clear
208 * `running`, then join the workers -- so `running` is the flag that says so, and a crawl that
209 * ignored it would hold the quit up until the last image: the full mount timeout per folder
210 * on a share that has gone away. */
211static gboolean _job_cancelled(dt_job_t *job)
212{
213 return !IS_NULL_PTR(job)
215}
216
217/* The walk visits folders in film-roll order -- the query orders by f.id -- so one folder is
218 * live at a time and this cache is a window onto the library, not a copy of it. The cap keeps
219 * it a window even if that order ever changes: past it the cache is dropped wholesale rather
220 * than growing with the collection, which costs a re-listing at worst and never a wrong
221 * answer. */
222#define DT_CRAWLER_FOLDER_CACHE_MAX 32
223
224static dt_crawler_folder_t *_crawler_folder(dt_crawler_walk_t *walk, const char *dirname)
225{
226 GHashTable *folders = walk->folders;
227 dt_crawler_folder_t *folder = (dt_crawler_folder_t *)g_hash_table_lookup(folders, dirname);
228 if(!IS_NULL_PTR(folder)) return folder;
229
230 if(g_hash_table_size(folders) >= DT_CRAWLER_FOLDER_CACHE_MAX)
231 g_hash_table_remove_all(folders);
232
233 folder = (dt_crawler_folder_t *)g_malloc0(sizeof(dt_crawler_folder_t));
234 folder->path = g_strdup(dirname);
235 folder->exact = g_hash_table_new_full(g_str_hash, g_str_equal,
237 walk->listings++;
238
239 /* GIO rather than readdir()/stat(): it is the one spelling that works on all three
240 * platforms, and on Windows it takes the UTF-8 path this database stores and does the
241 * UTF-16 conversion itself -- which is exactly what the hand-rolled _wstati64() branch
242 * removed from _crawl_image() was there to do. */
243 GFile *dir = g_file_new_for_path(dirname);
244 GFileEnumerator *entries = g_file_enumerate_children(dir,
245 G_FILE_ATTRIBUTE_STANDARD_NAME ","
246 G_FILE_ATTRIBUTE_TIME_MODIFIED,
247 G_FILE_QUERY_INFO_NONE, NULL, NULL);
248 if(!IS_NULL_PTR(entries))
249 {
250 GError *error = NULL;
251 while(!_job_cancelled(walk->job))
252 {
253 GFileInfo *info = g_file_enumerator_next_file(entries, NULL, &error);
254 if(IS_NULL_PTR(info)) break;
255
256 const char *name = g_file_info_get_name(info);
257 if(!IS_NULL_PTR(name))
258 {
259 guint64 *mtime = (guint64 *)g_malloc(sizeof(guint64));
260 *mtime = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
261 g_hash_table_insert(folder->exact, g_strdup(name), mtime);
262 }
263 g_object_unref(info);
264 }
265
266 /* A listing that fails part-way -- a share dropping mid-read -- is discarded whole. Kept as
267 * far as it got, it would read an image whose .txt came after the break as having none and
268 * clear its flag; empty, it reads every image here as missing, which is the answer an
269 * unreadable folder gets below. */
270 if(!IS_NULL_PTR(error))
271 {
272 dt_print(DT_DEBUG_CONTROL, "[crawler] listing `%s' failed part-way: %s\n", dirname,
273 error->message);
274 g_hash_table_remove_all(folder->exact);
275 g_error_free(error);
276 }
277 g_object_unref(entries);
278 }
279 else
280 dt_print(DT_DEBUG_CONTROL, "[crawler] cannot list `%s'.\n", dirname);
281
282 g_object_unref(dir);
283
284 /* A folder we could not read memoises as an EMPTY listing, not as "not looked at yet":
285 * every image in it then reads as missing, which is exactly what the per-file stat()
286 * answered for an unreachable folder -- and we do not ask again once per image. That is
287 * the unplugged external drive and the offline share, at one failed call per folder
288 * instead of six per image. */
289 g_hash_table_insert(folders, g_strdup(dirname), folder);
290 return folder;
291}
292
293/* TRUE if the folder holds `name`; its modification time goes to `mtime` when one is wanted. */
294static gboolean _folder_holds(dt_crawler_folder_t *folder, const char *name, time_t *mtime)
295{
296 const guint64 *found = (const guint64 *)g_hash_table_lookup(folder->exact, name);
297 if(!IS_NULL_PTR(found))
298 {
299 if(!IS_NULL_PTR(mtime)) *mtime = (time_t)*found;
300 return TRUE;
301 }
302
303 if(IS_NULL_PTR(folder->folded))
304 {
305 folder->folded = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, NULL);
306 GHashTableIter iter;
307 gpointer listed = NULL;
308 g_hash_table_iter_init(&iter, folder->exact);
309 while(g_hash_table_iter_next(&iter, &listed, NULL))
310 g_hash_table_add(folder->folded, _folded_key((const char *)listed));
311 }
312
313 gchar *key = _folded_key(name);
314 const gboolean elsewhere = g_hash_table_contains(folder->folded, key);
315 dt_free(key);
316 if(!elsewhere) return FALSE;
317
318 // a file answers to this name under another spelling; the filesystem decides if this one does
319 gchar *path = g_build_filename(folder->path, name, NULL);
320 GStatBuf st;
321 const gboolean resolves = (g_stat(path, &st) == 0);
322 dt_free(path);
323 if(resolves && !IS_NULL_PTR(mtime)) *mtime = st.st_mtime;
324 return resolves;
325}
326
327/* `name` with its extension replaced by the three characters `ext`: the sibling-file spelling
328 * the per-file lookups built by hand. It finds the dot in the file name, where they searched
329 * the whole path, and the two agree for every name that has an extension. They part ways for
330 * a name with none in a folder whose path has a dot: the old spelling then pointed beside the
331 * folder, outside the image's own directory, where this one stays inside it. A name with no
332 * '.' at all comes out as its first character followed by `ext` -- no companion file is
333 * spelled that way, so such an image simply has none. */
334static gchar *_sibling_name(const char *name, const char *ext)
335{
336 size_t len = strlen(name);
337 const char *c = name + len;
338 while((c > name) && (*c != '.')) c--;
339 len = c - name + 1;
340
341 // g_strndup always allocates n + 1 bytes and NUL-pads, so writing [len .. len + 2] is in
342 // bounds even when `name`'s own extension is shorter than three characters.
343 gchar *sibling = g_strndup(name, len + 3);
344 memcpy(sibling + len, ext, 3);
345 return sibling;
346}
347
348/* One row of the library walk: everything below used to be the body of a cursor loop over
349 * main.images joined to main.film_rolls, with a second statement writing the flags back. */
350static gboolean _crawl_image(const int32_t id,
351 const int64_t timestamp,
352 const int version,
353 const char *image_path,
354 const int flags,
355 void *user_data)
356{
357 dt_crawler_walk_t *walk = (dt_crawler_walk_t *)user_data;
358 if(_job_cancelled(walk->job)) return FALSE;
359
360 walk->images++;
361
362 gboolean go_on = TRUE;
363 gchar *dirname = g_path_get_dirname(image_path);
364 gchar *filename = g_path_get_basename(image_path);
366
367 /* Checked again after the listing, which is where the time goes: a cancel landing during it
368 * leaves a listing cut short, and a name missing from it would read as a file missing from
369 * disk -- clearing the companion flags of every image whose .txt was not listed yet. */
370 if(_job_cancelled(walk->job))
371 {
372 go_on = FALSE;
373 goto done;
374 }
375
376 // if the image is missing we ignore it.
377 if(!_folder_holds(folder, filename, NULL))
378 {
379 dt_print(DT_DEBUG_CONTROL, "[crawler] `%s' (id: %d) is missing.\n", image_path, id);
380 goto done;
381 }
382
383 {
384 // construct the xmp filename for this image
385 gchar xmp_name[DT_PATH_MAX] = { 0 };
386 g_strlcpy(xmp_name, filename, sizeof(xmp_name));
387 dt_image_path_append_version_no_db(version, xmp_name, sizeof(xmp_name));
388 g_strlcat(xmp_name, ".xmp", sizeof(xmp_name));
389
390 time_t xmp_timestamp = 0;
391 if(!_folder_holds(folder, xmp_name, &xmp_timestamp))
392 goto done; // TODO: shall we report these?
393
394 // step 1: check if the xmp is newer than our db entry
395 // FIXME: allow for a few seconds difference?
396 if(timestamp < xmp_timestamp)
397 {
400 item->id = id;
401 item->timestamp_xmp = xmp_timestamp;
402 item->timestamp_db = timestamp;
403 item->image_path = g_strdup(image_path);
404 item->xmp_path = g_build_filename(dirname, xmp_name, NULL);
405
406 *walk->result = g_list_prepend(*walk->result, item);
408 "[crawler] `%s' (id: %d) is a newer XMP file.\n", item->xmp_path, id);
409 }
410 // older timestamps are the case for all images after the db
411 // upgrade. better not report these
412 }
413
414 {
415 // step 2: check if the image has associated files (.txt, .wav)
416 // Both spellings of each, in the order the per-file lookups tried them.
417 gchar *txt_lower = _sibling_name(filename, "txt");
418 gchar *txt_upper = _sibling_name(filename, "TXT");
419 const gboolean has_txt = _folder_holds(folder, txt_lower, NULL)
420 || _folder_holds(folder, txt_upper, NULL);
421 dt_free(txt_lower);
422 dt_free(txt_upper);
423
424 gchar *wav_lower = _sibling_name(filename, "wav");
425 gchar *wav_upper = _sibling_name(filename, "WAV");
426 const gboolean has_wav = _folder_holds(folder, wav_lower, NULL)
427 || _folder_holds(folder, wav_upper, NULL);
428 dt_free(wav_lower);
429 dt_free(wav_upper);
430
431 // TODO: decide if we want to remove the flag for images that lost
432 // their extra file. currently we do (the else cases)
433 const int mask = DT_IMAGE_HAS_TXT | DT_IMAGE_HAS_WAV;
434 int value = 0;
435 if(has_txt) value |= DT_IMAGE_HAS_TXT;
436 if(has_wav) value |= DT_IMAGE_HAS_WAV;
437
438 /* The row is not the only copy of this word. An image the user has looked at also has an
439 * image-cache entry holding its own dt_image_t, and releasing that entry writes the whole
440 * struct back -- which is how a rating or a colour label reaches the database at all
441 * (metadata/ratings.c's _ratings_apply_to_image()). So when the image has an entry, the
442 * entry is what we compare against AND what we edit: it owns the struct, and its write lock
443 * is the one _ratings_apply_to_image() takes, which is what serialises the two. A rating set
444 * meanwhile either lands before us and is read here, or lands after us and sees our bits.
445 *
446 * Compare against the entry, not the row. The two can disagree on these very bits -- a row
447 * written behind the entry earlier, say -- and a guard reading the row would then find
448 * nothing to do, leave the entry stale, and let its next release write the stale bits back.
449 *
450 * get_existing(), not testget(). testget() returns NULL for an entry someone holds this
451 * instant as well as for no entry, and writing the row in the first case writes behind a
452 * live entry whose release then reverts it. get_existing() waits for that entry instead --
453 * and, like testget(), never creates one, so a crawl over the whole library does not pull
454 * the whole library into the cache on its way past.
455 *
456 * RELAXED rather than SAFE: the release writes the row, but must not queue an XMP write
457 * from the one job whose entire purpose is to find out whether the sidecars are in sync.
458 * MINIMAL when nothing changed: that gives the lock back without writing anything. */
459 dt_image_t *cached = dt_image_cache_get_existing(id, 'w');
460 if(!IS_NULL_PTR(cached))
461 {
462 if((cached->flags & mask) != value)
463 {
464 cached->flags = (cached->flags & ~mask) | value;
466 }
467 else
469 }
470 /* No entry, so the row is the only copy and comparing against it is sound. `flags` was read
471 * from it before this folder was listed -- a filesystem round-trip, up to a second on a
472 * network share -- so the write is masked: a rating set in that window lives in the same
473 * word and must survive. The two bits compared are written only by the crawl and by the
474 * import, so the stale read costs at worst one redundant UPDATE. */
475 else if((flags & mask) != value)
477 }
478
479done:
480 dt_free(dirname);
481 dt_free(filename);
482 return go_on;
483}
484
485static GList *_crawler_run(dt_job_t *job)
486{
487 GList *result = NULL;
488 const gint64 started = g_get_monotonic_time();
490 = { .result = &result,
491 .folders = g_hash_table_new_full(g_str_hash, g_str_equal,
493 .job = job };
494
495 /* NO transaction around this walk, deliberately -- it used to carry one, inherited from the
496 * days when the crawl ran before the main window existed and nothing else could touch the
497 * database. It cannot stay now that this runs as a background job:
498 *
499 * - dt_database_start_transaction() takes the module-wide _db_lock as a WRITER and holds it
500 * until the matching release, so every other thread that opens a transaction -- the GUI
501 * thread does so constantly -- would block for the whole crawl;
502 * - the module owns ONE sqlite3 connection, and a transaction belongs to the connection
503 * rather than to the thread, so any statement the GUI thread issues outside a transaction
504 * of its own would silently execute inside OURS: not durable until we commit, and gone if
505 * anything rolled us back;
506 * - and what it spanned is not database work at all. _crawler_folder() lists a directory
507 * from inside the callback, so the lock would be held across every filesystem round-trip
508 * -- 1.1 s on the measured SMB share, and the full mount timeout when a share is gone.
509 *
510 * Nothing is lost by dropping it. The walk is a read; the only writes are the rare
511 * dt_image_repository_set_flags_masked() calls for an image whose .txt/.wav sibling appeared or
512 * disappeared, and the database runs `synchronous = OFF` with `journal_mode = MEMORY`
513 * (dt_database_open), so a commit costs no disk sync and batching them buys nothing.
514 */
516
517 g_hash_table_destroy(walk.folders);
518
519 /* One line, at the end, for the one thing the job traces cannot say. [run_job-] reports that
520 * this function returned, not that it walked anything: a crawl that stopped at its first
521 * check -- cancelled, or a library whose folders are all unreachable -- prints exactly the
522 * same pair of brackets as one that visited every image. Per folder would be 18 lines on the
523 * library this was measured against and per image 1969, which is itself enough I/O to move
524 * the number it would be reporting. */
526 "[crawler] %s: %d images, %d folder listings, %d to report, %.2f s\n",
527 _job_cancelled(job) ? "cancelled" : "done", walk.images, walk.listings,
528 g_list_length(result), (double)(g_get_monotonic_time() - started) / 1.0e6);
529
530 return g_list_reverse(result); // list was built in reverse order, so un-reverse it
531}
532
534{
535 return _crawler_run(NULL);
536}
537
538/* The crawl is I/O-latency bound and its cost scales with the library, so it does not belong
539 * on the startup path at all: it used to run to completion before dt_control_init(), i.e.
540 * before the main window was built. It runs as a background job instead, and posts its popup
541 * to the GUI thread if and when it finds anything.
542 */
543static gboolean _crawler_show_results(gpointer user_data)
544{
545 // takes ownership of the list and frees it
546 dt_control_crawler_show_image_list((GList *)user_data);
547 return G_SOURCE_REMOVE;
548}
549
550static int32_t _crawler_job_run(dt_job_t *job)
551{
552 GList *changed_xmp_files = _crawler_run(job);
553
554 /* A cancelled walk stopped part-way, so its list is a fraction of the answer -- and after a
555 * quit, the main loop that would show it is on its way out. Neither is worth a popup. */
556 if(_job_cancelled(job))
557 {
558 _free_crawler_results(changed_xmp_files);
559 return 0;
560 }
561
562 // the popup is GTK and this runs on a worker thread
563 if(!IS_NULL_PTR(changed_xmp_files))
564 g_main_context_invoke(NULL, _crawler_show_results, changed_xmp_files);
565
566 return 0;
567}
568
570{
571 /* dt_control_add_job() runs a job synchronously on the calling thread when the scheduler is
572 * not up, and reports that as success -- which is the one outcome this function exists to
573 * avoid. No crawl at all is the right answer then: it is a consistency check between the
574 * database and the sidecars, not something a session depends on. dt_init() calls this well
575 * after dt_control_init() starts the workers, so it cannot fire today; this keeps that
576 * ordering from being a silent precondition. */
577 if(!dt_control_running()) return;
578
579 dt_job_t *job = dt_control_job_create(&_crawler_job_run, "crawl XMP files");
580 if(IS_NULL_PTR(job)) return;
581
582 // SYSTEM_BG, not SYSTEM_FG: the queue a job may not be pushed back out of. Dropping the
583 // crawl would leave the database out of sync with the sidecars with nothing said about it.
585}
586
587
588/********************* the gui stuff *********************/
589
598
599// close the window and clean up
601 const gint response_id,
602 gpointer user_data)
603{
605 g_object_unref(G_OBJECT(gui->model));
606 gtk_widget_destroy(dialog);
607 dt_free(gui);
608}
609
610
612{
613 GList *rr_list = gui->rows_to_remove;
614 GtkTreeModel *model = gui->model;
615
616 // Remove TreeView rows from rr_list. It needs to be populated before
617 for(GList *node = rr_list; !IS_NULL_PTR(node); node = g_list_next(node))
618 {
619 GtkTreePath *path = gtk_tree_row_reference_get_path((GtkTreeRowReference*)node->data);
620
621 if(path)
622 {
623 GtkTreeIter iter;
624 if(gtk_tree_model_get_iter(model, &iter, path))
625 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
626 }
627 }
628
629 // Cleanup the list of rows
630 g_list_foreach(rr_list, (GFunc) gtk_tree_row_reference_free, NULL);
631 g_list_free(rr_list);
632 rr_list = NULL;
633}
634
635
636static void _select_all_callback(GtkButton *button,
637 gpointer user_data)
638{
640 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
641 gtk_tree_selection_select_all(selection);
642}
643
644
645static void _select_none_callback(GtkButton *button, gpointer user_data)
646{
648 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
649 gtk_tree_selection_unselect_all(selection);
650}
651
652
653static void _select_invert_callback(GtkButton *button, gpointer user_data)
654{
656 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
657
658 GtkTreeIter iter;
659 gboolean valid = gtk_tree_model_get_iter_first(gui->model, &iter);
660 while(valid)
661 {
662 if(gtk_tree_selection_iter_is_selected(selection, &iter))
663 gtk_tree_selection_unselect_iter(selection, &iter);
664 else
665 gtk_tree_selection_select_iter(selection, &iter);
666
667 valid = gtk_tree_model_iter_next(gui->model, &iter);
668 }
669}
670
671
672static void _get_crawler_entry_from_model(GtkTreeModel *model,
673 GtkTreeIter *iter,
675{
676 gtk_tree_model_get(model, iter,
682}
683
684
685static void _append_row_to_remove(GtkTreeModel *model,
686 GtkTreePath *path,
687 GList **rowref_list)
688{
689 // append TreeModel rows to the list to remove
690 GtkTreeRowReference *rowref = gtk_tree_row_reference_new(model, path);
691 *rowref_list = g_list_append(*rowref_list, rowref);
692}
693
695 gchar *pattern,
696 gchar *filepath)
697{
698 gchar *message = pattern;
699 gboolean to_free = FALSE;
700
701 if(!IS_NULL_PTR(filepath))
702 {
703 message = g_strdup_printf(pattern, filepath);
704 to_free = TRUE;
705 }
706
707 // add a new line in the log TreeView
708 GtkTreeIter iter_log;
709 GtkTreeModel *model_log = gtk_tree_view_get_model(GTK_TREE_VIEW(gui->log));
710 gtk_list_store_append(GTK_LIST_STORE(model_log), &iter_log);
711 gtk_list_store_set(GTK_LIST_STORE(model_log), &iter_log,
712 0, message,
713 -1);
714
715 if(to_free)
716 {
717 dt_free(message);
718 }
719}
720
721
722static void sync_xmp_to_db(GtkTreeModel *model,
723 GtkTreePath *path,
724 GtkTreeIter *iter,
725 gpointer user_data)
726{
728 dt_control_crawler_result_t entry = { 0 };
730 // the DB writing timestamp becomes the XMP file's
732
733 const int error =
734 dt_history_load_and_apply_on_image(entry.id, entry.xmp_path, 0); // success = 0, fail = 1
735
736 if(error)
737 {
738 _log_synchronization(gui, _("ERROR: %s NOT synced XMP \342\206\222 DB"), entry.image_path);
739 _log_synchronization(gui, _("ERROR: cannot write the database."
740 " the destination may be full, offline or read-only."),
741 NULL);
742 }
743 else
744 {
746 _log_synchronization(gui, _("SUCCESS: %s synced XMP \342\206\222 DB"), entry.image_path);
747 }
748
749 _free_crawler_result(&entry);
750}
751
752
753static void sync_db_to_xmp(GtkTreeModel *model,
754 GtkTreePath *path,
755 GtkTreeIter *iter,
756 gpointer user_data)
757{
759 dt_control_crawler_result_t entry = { 0 };
761
763
764 if(result == DT_IMAGE_WRITE_SIDECAR_OK)
765 {
768 _log_synchronization(gui, _("SUCCESS: %s synced DB \342\206\222 XMP"), entry.image_path);
769 }
770 else
771 {
772 _log_synchronization(gui, _("ERROR: %s NOT synced DB \342\206\222 XMP"), entry.image_path);
774 _("ERROR: cannot write %s \nthe destination may be full,"
775 " offline or read-only."), entry.xmp_path);
776 }
777
778 _free_crawler_result(&entry);
779}
780
781static void sync_newest_to_oldest(GtkTreeModel *model,
782 GtkTreePath *path,
783 GtkTreeIter *iter,
784 gpointer user_data)
785{
787 dt_control_crawler_result_t entry = { 0 };
789
790 int error = 0;
791
792 if(entry.timestamp_xmp > entry.timestamp_db)
793 {
794 // WRITE XMP in DB
795 // the DB writing timestamp becomes the XMP file's
798 if(error)
799 {
801 (gui,
802 _("ERROR: %s NOT synced new (XMP) \342\206\222 old (DB)"), entry.image_path);
804 (gui,
805 _("ERROR: cannot write the database. the destination may be full,"
806 " offline or read-only."), NULL);
807 }
808 else
809 {
811 (gui,
812 _("SUCCESS: %s synced new (XMP) \342\206\222 old (DB)"), entry.image_path);
813 }
814 }
815 else if(entry.timestamp_xmp < entry.timestamp_db)
816 {
817 // write the XMP and make sure it get the last modified timestamp of the db
819 error = (xres != DT_IMAGE_WRITE_SIDECAR_OK) ? 1 : 0;
821
822 if(error)
823 {
825 (gui,
826 _("ERROR: %s NOT synced new (DB) \342\206\222 old (XMP)"), entry.image_path);
828 (gui,
829 _("ERROR: cannot write %s \nthe destination may be full, offline or read-only."),
830 entry.xmp_path);
831 }
832 else
833 {
834 _log_synchronization(gui, _("SUCCESS: %s synced new (DB) \342\206\222 old (XMP)"),
835 entry.image_path);
836 }
837 }
838 else
839 {
840 // we should never reach that part of the code
841 // if both timestamps are equal, they should not be in this list in the first place
842 error = 1;
843 _log_synchronization(gui, _("EXCEPTION: %s has inconsistent timestamps"),
844 entry.image_path);
845 }
846
848
849 _free_crawler_result(&entry);
850}
851
852
853static void sync_oldest_to_newest(GtkTreeModel *model,
854 GtkTreePath *path,
855 GtkTreeIter *iter,
856 gpointer user_data)
857{
859 dt_control_crawler_result_t entry = { 0 };
861 int error = 0;
862
863 if(entry.timestamp_xmp < entry.timestamp_db)
864 {
865 // WRITE XMP in DB
866 // the DB writing timestamp becomes the XMP file's
869 if(error)
870 {
872 _("ERROR: %s NOT synced old (XMP) \342\206\222 new (DB)"),
873 entry.image_path);
875 _("ERROR: cannot write the database."
876 " the destination may be full, offline or read-only."), NULL);
877 }
878 else
879 {
881 _("SUCCESS: %s synced old (XMP) \342\206\222 new (DB)"),
882 entry.image_path);
883 }
884 }
885 else if(entry.timestamp_xmp > entry.timestamp_db)
886 {
887 // WRITE DB in XMP
889 error = (xres != DT_IMAGE_WRITE_SIDECAR_OK) ? 1 : 0;
891 if(error)
892 {
894 _("ERROR: %s NOT synced old (DB) \342\206\222 new (XMP)"),
895 entry.image_path);
897 _("ERROR: cannot write %s \nthe destination may be full,"
898 " offline or read-only."), entry.xmp_path);
899 }
900 else
901 {
903 _("SUCCESS: %s synced old (DB) \342\206\222 new (XMP)"),
904 entry.image_path);
905 }
906 }
907 else
908 {
909 // we should never reach that part of the code
910 // if both timestamps are equal, they should not be in this list in the first place
911 error = 1;
913 _("EXCEPTION: %s has inconsistent timestamps"),
914 entry.image_path);
915 }
916
917 if(!error)
919
920 _free_crawler_result(&entry);
921}
922
923// overwrite database with xmp
924static void _reload_button_clicked(GtkButton *button, gpointer user_data)
925{
927 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
928 gui->rows_to_remove = NULL;
929 gtk_spinner_start(GTK_SPINNER(gui->spinner));
930 gtk_tree_selection_selected_foreach(selection, sync_xmp_to_db, gui);
932 gtk_spinner_stop(GTK_SPINNER(gui->spinner));
933}
934
935// overwrite xmp with database
936void _overwrite_button_clicked(GtkButton *button, gpointer user_data)
937{
939 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
940 gui->rows_to_remove = NULL;
941 gtk_spinner_start(GTK_SPINNER(gui->spinner));
942 gtk_tree_selection_selected_foreach(selection, sync_db_to_xmp, gui);
944 gtk_spinner_stop(GTK_SPINNER(gui->spinner));
945}
946
947// overwrite the oldest with the newest
948static void _newest_button_clicked(GtkButton *button, gpointer user_data)
949{
951 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
952 gui->rows_to_remove = NULL;
953 gtk_spinner_start(GTK_SPINNER(gui->spinner));
954 gtk_tree_selection_selected_foreach(selection, sync_newest_to_oldest, gui);
956 gtk_spinner_stop(GTK_SPINNER(gui->spinner));
957}
958
959// overwrite the newest with the oldest
960static void _oldest_button_clicked(GtkButton *button, gpointer user_data)
961{
963 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
964 gui->rows_to_remove = NULL;
965 gtk_spinner_start(GTK_SPINNER(gui->spinner));
966 gtk_tree_selection_selected_foreach(selection, sync_oldest_to_newest, gui);
968 gtk_spinner_stop(GTK_SPINNER(gui->spinner));
969}
970
971static gchar* str_time_delta(const int time_delta)
972{
973 // display the time difference as a legible string
974 int seconds = time_delta;
975
976 int minutes = seconds / 60;
977 seconds -= 60 * minutes;
978
979 int hours = minutes / 60;
980 minutes -= 60 * hours;
981
982 const int days = hours / 24;
983 hours -= 24 * days;
984
985 return g_strdup_printf(_("%id %02dh %02dm %02ds"), days, hours, minutes, seconds);
986}
987
988// show a popup window with a list of updated images/xmp files and allow the user to tell dt what to do about them
990{
991 if(IS_NULL_PTR(images)) return;
992
995
996 // a list with all the images
997 GtkTreeViewColumn *column;
998 GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
999 gtk_widget_set_vexpand(scroll, TRUE);
1000 GtkListStore *store = gtk_list_store_new(DT_CONTROL_CRAWLER_NUM_COLS,
1001 G_TYPE_INT, // id
1002 G_TYPE_STRING, // image path
1003 G_TYPE_STRING, // xmp path
1004 G_TYPE_STRING, // timestamp from xmp
1005 G_TYPE_STRING, // timestamp from db
1006 G_TYPE_INT, // timestamp to db
1007 G_TYPE_INT,
1008 G_TYPE_STRING, // report: newer version
1009 G_TYPE_STRING);// time delta
1010
1011 gui->model = GTK_TREE_MODEL(store);
1012
1013 for(GList *list_iter = images; list_iter; list_iter = g_list_next(list_iter))
1014 {
1015 GtkTreeIter iter;
1016 dt_control_crawler_result_t *item = list_iter->data;
1017 char timestamp_db[64], timestamp_xmp[64];
1018 struct tm tm_stamp;
1019 strftime(timestamp_db, sizeof(timestamp_db),
1020 "%c", localtime_r(&item->timestamp_db, &tm_stamp));
1021 strftime(timestamp_xmp, sizeof(timestamp_xmp),
1022 "%c", localtime_r(&item->timestamp_xmp, &tm_stamp));
1023
1024 const time_t time_delta = llabs(item->timestamp_db - item->timestamp_xmp);
1025 gchar *timestamp_delta = str_time_delta(time_delta);
1026
1027 gtk_list_store_append(store, &iter);
1028 gtk_list_store_set
1029 (store, &iter,
1033 DT_CONTROL_CRAWLER_COL_TS_XMP, timestamp_xmp,
1034 DT_CONTROL_CRAWLER_COL_TS_DB, timestamp_db,
1038 ? _("XMP")
1039 : _("database"),
1040 DT_CONTROL_CRAWLER_COL_TIME_DELTA, timestamp_delta,
1041 -1);
1043 dt_free(timestamp_delta);
1044 }
1045 g_list_free_full(images, dt_free_gpointer);
1046 images = NULL;
1047
1048 GtkWidget *tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
1049 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
1050 gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
1051
1052 gui->tree = GTK_TREE_VIEW(tree); // FIXME: do we need to free that later ?
1053
1054 GtkCellRenderer *renderer_text = gtk_cell_renderer_text_new();
1055 column = gtk_tree_view_column_new_with_attributes
1056 (_("path"), renderer_text, "text",
1058 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
1059 gtk_tree_view_column_set_expand(column, TRUE);
1060 gtk_tree_view_column_set_resizable(column, TRUE);
1061 gtk_tree_view_column_set_min_width(column, DT_PIXEL_APPLY_DPI(200));
1062 g_object_set(renderer_text, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, NULL);
1063
1064 column = gtk_tree_view_column_new_with_attributes
1065 (_("XMP timestamp"), gtk_cell_renderer_text_new(), "text",
1067 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
1068
1069 column = gtk_tree_view_column_new_with_attributes
1070 (_("database timestamp"), gtk_cell_renderer_text_new(), "text",
1072 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
1073
1074 column = gtk_tree_view_column_new_with_attributes
1075 (_("newest"), gtk_cell_renderer_text_new(), "text",
1077 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
1078
1079 GtkCellRenderer *renderer_date = gtk_cell_renderer_text_new();
1080 column = gtk_tree_view_column_new_with_attributes
1081 (_("time difference"), renderer_date, "text",
1083 g_object_set(renderer_date, "xalign", 1., NULL);
1084 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
1085
1086 dt_gui_add_class(scroll, "dt_recessed_scroll");
1087 gtk_container_add(GTK_CONTAINER(scroll), tree);
1088 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
1089 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
1090
1091 // build a dialog window that contains the list of images
1093 GtkWidget *dialog = gtk_dialog_new_with_buttons
1094 (_("updated XMP sidecar files found"), GTK_WINDOW(win),
1095 GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, _("_close"),
1096 GTK_RESPONSE_CLOSE, NULL);
1097
1098#ifdef GDK_WINDOWING_QUARTZ
1100#endif
1101 gtk_widget_set_size_request(dialog, -1, DT_PIXEL_APPLY_DPI(400));
1102 gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(win));
1103 GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
1104
1105 GtkWidget *content_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1106 gtk_container_add(GTK_CONTAINER(content_area), content_box);
1107
1108 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1109 gtk_box_pack_start(GTK_BOX(content_box), box, FALSE, FALSE, 0);
1110 GtkWidget *select_all = gtk_button_new_with_label(_("select all"));
1111 GtkWidget *select_none = gtk_button_new_with_label(_("select none"));
1112 GtkWidget *select_invert = gtk_button_new_with_label(_("invert selection"));
1113 gtk_box_pack_start(GTK_BOX(box), select_all, FALSE, FALSE, 0);
1114 gtk_box_pack_start(GTK_BOX(box), select_none, FALSE, FALSE, 0);
1115 gtk_box_pack_start(GTK_BOX(box), select_invert, FALSE, FALSE, 0);
1116 g_signal_connect(select_all, "clicked", G_CALLBACK(_select_all_callback), gui);
1117 g_signal_connect(select_none, "clicked", G_CALLBACK(_select_none_callback), gui);
1118 g_signal_connect(select_invert, "clicked", G_CALLBACK(_select_invert_callback), gui);
1119
1120 gtk_box_pack_start(GTK_BOX(content_box), scroll, TRUE, TRUE, 0);
1121
1122 box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1123 gtk_box_pack_start(GTK_BOX(content_box), box, FALSE, FALSE, 1);
1124 GtkWidget *label = gtk_label_new_with_mnemonic(_("on the selection:"));
1125 GtkWidget *reload_button = gtk_button_new_with_label(_("keep the XMP edit"));
1126 GtkWidget *overwrite_button = gtk_button_new_with_label(_("keep the database edit"));
1127 GtkWidget *newest_button = gtk_button_new_with_label(_("keep the newest edit"));
1128 GtkWidget *oldest_button = gtk_button_new_with_label(_("keep the oldest edit"));
1129 gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0);
1130 gtk_box_pack_start(GTK_BOX(box), reload_button, FALSE, FALSE, 0);
1131 gtk_box_pack_start(GTK_BOX(box), overwrite_button, FALSE, FALSE, 0);
1132 gtk_box_pack_start(GTK_BOX(box), newest_button, FALSE, FALSE, 0);
1133 gtk_box_pack_start(GTK_BOX(box), oldest_button, FALSE, FALSE, 0);
1134 g_signal_connect(reload_button, "clicked", G_CALLBACK(_reload_button_clicked), gui);
1135 g_signal_connect(overwrite_button, "clicked", G_CALLBACK(_overwrite_button_clicked), gui);
1136 g_signal_connect(newest_button, "clicked", G_CALLBACK(_newest_button_clicked), gui);
1137 g_signal_connect(oldest_button, "clicked", G_CALLBACK(_oldest_button_clicked), gui);
1138
1139 /* Feedback spinner in case synch happens over network and stales */
1140 gui->spinner = gtk_spinner_new();
1141 gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(gui->spinner), FALSE, FALSE, 0);
1142
1143 /* Log report */
1144 scroll = gtk_scrolled_window_new(NULL, NULL);
1145 gui->log = gtk_tree_view_new();
1146 gtk_box_pack_start(GTK_BOX(content_box), scroll, TRUE, TRUE, 0);
1147 dt_gui_add_class(scroll, "dt_recessed_scroll");
1148 gtk_container_add(GTK_CONTAINER(scroll), gui->log);
1149 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
1150 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
1151
1152 gtk_tree_view_insert_column_with_attributes
1153 (GTK_TREE_VIEW(gui->log), -1,
1154 _("synchronization log"), renderer_text,
1155 "text", 0, NULL);
1156
1157 GtkListStore *store_log = gtk_list_store_new (1, G_TYPE_STRING);
1158 GtkTreeModel *model_log = GTK_TREE_MODEL(store_log);
1159 gtk_tree_view_set_model(GTK_TREE_VIEW(gui->log), model_log);
1160 g_object_unref(model_log);
1161
1162 gtk_widget_show_all(dialog);
1163
1164 g_signal_connect(dialog, "response",
1165 G_CALLBACK(dt_control_crawler_response_callback), gui);
1166}
1167
1168// clang-format off
1169// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
1170// vim: shiftwidth=2 expandtab tabstop=2 cindent
1171// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
1172// clang-format on
GtkWidget * dt_gui_main_window(void)
static void error(char *msg)
Definition ashift_lsd.c:202
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
void dt_image_path_append_version_no_db(int version, char *pathname, size_t pathname_len)
dt_image_write_sidecar_result_t dt_image_write_sidecar_file_forced(const int32_t imgid)
int dt_control_running()
Definition control.c:442
struct dt_control_t * dt_control_get_global(void)
Definition darktable.c:651
void dt_control_crawler_show_image_list(GList *images)
Definition crawler.c:989
void _overwrite_button_clicked(GtkButton *button, gpointer user_data)
Definition crawler.c:936
static void sync_db_to_xmp(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
Definition crawler.c:753
static void _free_crawler_results(GList *results)
Definition crawler.c:89
dt_control_crawler_cols_t
Definition crawler.c:61
@ DT_CONTROL_CRAWLER_COL_XMP_PATH
Definition crawler.c:64
@ DT_CONTROL_CRAWLER_COL_TS_XMP
Definition crawler.c:65
@ DT_CONTROL_CRAWLER_COL_TS_DB_INT
Definition crawler.c:68
@ DT_CONTROL_CRAWLER_NUM_COLS
Definition crawler.c:71
@ DT_CONTROL_CRAWLER_COL_REPORT
Definition crawler.c:69
@ DT_CONTROL_CRAWLER_COL_IMAGE_PATH
Definition crawler.c:63
@ DT_CONTROL_CRAWLER_COL_TIME_DELTA
Definition crawler.c:70
@ DT_CONTROL_CRAWLER_COL_TS_DB
Definition crawler.c:66
@ DT_CONTROL_CRAWLER_COL_ID
Definition crawler.c:62
@ DT_CONTROL_CRAWLER_COL_TS_XMP_INT
Definition crawler.c:67
static void _append_row_to_remove(GtkTreeModel *model, GtkTreePath *path, GList **rowref_list)
Definition crawler.c:685
static void _select_invert_callback(GtkButton *button, gpointer user_data)
Definition crawler.c:653
static void _log_synchronization(dt_control_crawler_gui_t *gui, gchar *pattern, gchar *filepath)
Definition crawler.c:694
static GList * _crawler_run(dt_job_t *job)
Definition crawler.c:485
static gboolean _crawler_show_results(gpointer user_data)
Definition crawler.c:543
static gchar * _folded_key(const char *name)
Definition crawler.c:186
static void _oldest_button_clicked(GtkButton *button, gpointer user_data)
Definition crawler.c:960
static gboolean _crawl_image(const int32_t id, const int64_t timestamp, const int version, const char *image_path, const int flags, void *user_data)
Definition crawler.c:350
static void _free_crawler_result(dt_control_crawler_result_t *entry)
Definition crawler.c:82
static void _free_folder(gpointer p)
Definition crawler.c:176
static gchar * str_time_delta(const int time_delta)
Definition crawler.c:971
#define DT_CRAWLER_FOLDER_CACHE_MAX
Definition crawler.c:222
static void sync_oldest_to_newest(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
Definition crawler.c:853
void dt_control_crawler_run_in_background(void)
Definition crawler.c:569
static gboolean _folder_holds(dt_crawler_folder_t *folder, const char *name, time_t *mtime)
Definition crawler.c:294
static void _set_modification_time(char *filename, const time_t timestamp)
Definition crawler.c:96
static void _reload_button_clicked(GtkButton *button, gpointer user_data)
Definition crawler.c:924
static gchar * _sibling_name(const char *name, const char *ext)
Definition crawler.c:334
static dt_crawler_folder_t * _crawler_folder(dt_crawler_walk_t *walk, const char *dirname)
Definition crawler.c:224
static void _select_all_callback(GtkButton *button, gpointer user_data)
Definition crawler.c:636
static void sync_xmp_to_db(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
Definition crawler.c:722
static void _newest_button_clicked(GtkButton *button, gpointer user_data)
Definition crawler.c:948
GList * dt_control_crawler_run(void)
Definition crawler.c:533
static void _delete_selected_rows(dt_control_crawler_gui_t *gui)
Definition crawler.c:611
static gboolean _job_cancelled(dt_job_t *job)
Definition crawler.c:211
static void _get_crawler_entry_from_model(GtkTreeModel *model, GtkTreeIter *iter, dt_control_crawler_result_t *entry)
Definition crawler.c:672
static void dt_control_crawler_response_callback(GtkWidget *dialog, const gint response_id, gpointer user_data)
Definition crawler.c:600
static int32_t _crawler_job_run(dt_job_t *job)
Definition crawler.c:550
static void _select_none_callback(GtkButton *button, gpointer user_data)
Definition crawler.c:645
static void sync_newest_to_oldest(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
Definition crawler.c:781
GtkTreeStore * store
its model, owned by the view
GtkWidget * folder
destination folder chooser
int dt_history_load_and_apply_on_image(int32_t imgid, gchar *filename, int history_only)
@ DT_IMAGE_HAS_WAV
Definition image.h:138
@ DT_IMAGE_HAS_TXT
Definition image.h:136
dt_image_write_sidecar_result_t
Definition image.h:752
@ DT_IMAGE_WRITE_SIDECAR_OK
Definition image.h:753
dt_image_t * dt_image_cache_get_existing(const int32_t imgid, char mode)
void dt_image_cache_write_release(dt_image_t *img, dt_image_cache_write_mode_t mode)
@ DT_IMAGE_CACHE_RELAXED
Definition image_cache.h:50
@ DT_IMAGE_CACHE_MINIMAL
Definition image_cache.h:53
gboolean dt_image_repository_set_flags_masked(const int32_t imgid, const int mask, const int value)
Write only mask's bits of imgid's flags, taking them from value.
gboolean dt_image_repository_set_write_timestamp(const int32_t imgid, const int64_t timestamp)
Set write_timestamp of imgid to timestamp (seconds since the epoch). Bound as a 64-bit integer; the c...
void dt_image_repository_foreach_with_path(dt_image_repository_path_row_cb cb, void *user_data)
Walk every image in the library, film roll by film roll, filename within each.
Reading and writing one dt_image_t to and from the library database. The SQL half of what used to be ...
const char * model
dt_job_state_t dt_control_job_get_state(_dt_job_t *job)
Definition jobs.c:105
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
@ DT_JOB_QUEUE_SYSTEM_BG
Definition jobs.h:58
@ DT_JOB_STATE_CANCELLED
Definition jobs.h:47
@ DT_DEBUG_CONTROL
Definition logging.h:52
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.
#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
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
char * key
dt_mipmap_buffer_dsc_flags flags
Definition mipmap_cache.c:4
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
const char * name
Definition pdf.h:90
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
GtkTreeModel * model
Definition crawler.c:593
GtkTreeView * tree
Definition crawler.c:592
GHashTable * exact
Definition crawler.c:172
GHashTable * folded
Definition crawler.c:173
GList ** result
Definition crawler.c:199
dt_job_t * job
Definition crawler.c:201
GHashTable * folders
Definition crawler.c:200
int32_t flags
Definition image.h:401
GHashTable * entries
Definition supervisor.c:120
typedef double((*spd)(unsigned long int wavelength, double TempK))
#define DT_GUI_BOX_SPACING
#define DT_PIXEL_APPLY_DPI(value)
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)