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 <glib/gstdio.h>
36#include <sqlite3.h>
37#include <stdio.h>
38#include <string.h>
39
40#include "common/darktable.h"
41#include "common/database.h"
42#include "common/debug.h"
43#include "common/history.h"
44#include "common/image.h"
45#include "control/conf.h"
46#include "control/control.h"
47#include "crawler.h"
48#include "gui/gtk.h"
49#ifdef GDK_WINDOWING_QUARTZ
50#include "osx/osx.h"
51#endif
52
53
67
75
77{
78 dt_free(entry->image_path);
79 dt_free(entry->xmp_path);
80 entry->image_path = entry->xmp_path = NULL;
81}
82
83static void _set_modification_time(char *filename,
84 const time_t timestamp)
85{
86 GFile *gfile = g_file_new_for_path(filename);
87
88 GFileInfo *info = g_file_query_info(
89 gfile,
90 G_FILE_ATTRIBUTE_TIME_MODIFIED "," G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC,
91 G_FILE_QUERY_INFO_NONE,
92 NULL,
93 NULL);
94
95 // For reference, we could use the following lines but for some
96 // reasons there is a deprecated message raised even though this
97 // routine is not marked as deprecated in the documentation.
98 //
99 // GDateTime *datetime = g_date_time_new_from_unix_local(timestamp);
100 // g_file_info_set_modification_date_time(info, datetime);
101
102 if(info)
103 {
104 g_file_info_set_attribute_uint64
105 (info,
106 G_FILE_ATTRIBUTE_TIME_MODIFIED,
107 timestamp);
108
109 g_file_set_attributes_from_info(
110 gfile,
111 info,
112 G_FILE_QUERY_INFO_NONE,
113 NULL,
114 NULL);
115 }
116
117 g_object_unref(gfile);
118 if(info) g_clear_object(&info);
119}
120
122{
123 sqlite3_stmt *stmt, *inner_stmt;
124 GList *result = NULL;
125
126 // clang-format off
127 sqlite3_prepare_v2(dt_database_get(darktable.db),
128 "SELECT i.id, write_timestamp, version,"
129 " folder || '" G_DIR_SEPARATOR_S "' || filename, flags"
130 " FROM main.images i, main.film_rolls f"
131 " ON i.film_id = f.id"
132 " ORDER BY f.id, filename",
133 -1, &stmt, NULL);
134 // clang-format on
135 sqlite3_prepare_v2(dt_database_get(darktable.db),
136 "UPDATE main.images SET flags = ?1 WHERE id = ?2", -1,
137 &inner_stmt, NULL);
138
139 // let's wrap this into a transaction, it might make it a little faster.
141
142 while(sqlite3_step(stmt) == SQLITE_ROW)
143 {
144 const int id = sqlite3_column_int(stmt, 0);
145 const time_t timestamp = sqlite3_column_int(stmt, 1);
146 const int version = sqlite3_column_int(stmt, 2);
147 const gchar *image_path = (char *)sqlite3_column_text(stmt, 3);
148 int flags = sqlite3_column_int(stmt, 4);
149
150 // if the image is missing we ignore it.
151 if(!g_file_test(image_path, G_FILE_TEST_EXISTS))
152 {
153 dt_print(DT_DEBUG_CONTROL, "[crawler] `%s' (id: %d) is missing.\n", image_path, id);
154 continue;
155 }
156
157 // construct the xmp filename for this image
158 gchar xmp_path[PATH_MAX] = { 0 };
159 g_strlcpy(xmp_path, image_path, sizeof(xmp_path));
160 dt_image_path_append_version_no_db(version, xmp_path, sizeof(xmp_path));
161 size_t len = strlen(xmp_path);
162 if(len + 4 >= PATH_MAX) continue;
163 xmp_path[len++] = '.';
164 xmp_path[len++] = 'x';
165 xmp_path[len++] = 'm';
166 xmp_path[len++] = 'p';
167 xmp_path[len] = '\0';
168
169 // on Windows the encoding might not be UTF8
170 gchar *xmp_path_locale = dt_util_normalize_path(xmp_path);
171 int stat_res = -1;
172#ifdef _WIN32
173 // UTF8 paths fail in this context, but converting to UTF16 works
174 struct _stati64 statbuf;
175 if(xmp_path_locale) // in Windows dt_util_normalize_path returns
176 // NULL if file does not exist
177 {
178 wchar_t *wfilename = g_utf8_to_utf16(xmp_path_locale, -1, NULL, NULL, NULL);
179 stat_res = _wstati64(wfilename, &statbuf);
180 dt_free(wfilename);
181 }
182#else
183 struct stat statbuf;
184 stat_res = stat(xmp_path_locale, &statbuf);
185#endif
186 dt_free(xmp_path_locale);
187 if(stat_res) continue; // TODO: shall we report these?
188
189 // step 1: check if the xmp is newer than our db entry
190 // FIXME: allow for a few seconds difference?
191 if(timestamp < statbuf.st_mtime)
192 {
195 item->id = id;
196 item->timestamp_xmp = statbuf.st_mtime;
197 item->timestamp_db = timestamp;
198 item->image_path = g_strdup(image_path);
199 item->xmp_path = g_strdup(xmp_path);
200
201 result = g_list_prepend(result, item);
203 "[crawler] `%s' (id: %d) is a newer XMP file.\n", xmp_path, id);
204 }
205 // older timestamps are the case for all images after the db
206 // upgrade. better not report these
207
208 // step 2: check if the image has associated files (.txt, .wav)
209 len = strlen(image_path);
210 const char *c = image_path + len;
211 while((c > image_path) && (*c != '.')) c--;
212 len = c - image_path + 1;
213
214 char *txt_path = dt_image_get_text_path_from_path(image_path);
215 gboolean has_txt = !IS_NULL_PTR(txt_path);
216 dt_free(txt_path);
217
218 char *extra_path = (char *)calloc(len + 3 + 1, sizeof(char));
219 g_strlcpy(extra_path, image_path, len + 1);
220
221 extra_path[len] = 'w';
222 extra_path[len + 1] = 'a';
223 extra_path[len + 2] = 'v';
224 gboolean has_wav = g_file_test(extra_path, G_FILE_TEST_EXISTS);
225
226 if(!has_wav)
227 {
228 extra_path[len] = 'W';
229 extra_path[len + 1] = 'A';
230 extra_path[len + 2] = 'V';
231 has_wav = g_file_test(extra_path, G_FILE_TEST_EXISTS);
232 }
233
234 // TODO: decide if we want to remove the flag for images that lost
235 // their extra file. currently we do (the else cases)
236 int new_flags = flags;
237 if(has_txt)
238 new_flags |= DT_IMAGE_HAS_TXT;
239 else
240 new_flags &= ~DT_IMAGE_HAS_TXT;
241 if(has_wav)
242 new_flags |= DT_IMAGE_HAS_WAV;
243 else
244 new_flags &= ~DT_IMAGE_HAS_WAV;
245 if(flags != new_flags)
246 {
247 sqlite3_bind_int(inner_stmt, 1, new_flags);
248 sqlite3_bind_int(inner_stmt, 2, id);
249 sqlite3_step(inner_stmt);
250 sqlite3_reset(inner_stmt);
251 sqlite3_clear_bindings(inner_stmt);
252 }
253
254 dt_free(extra_path);
255 }
256
258
259 sqlite3_finalize(stmt);
260 sqlite3_finalize(inner_stmt);
261
262 return g_list_reverse(result); // list was built in reverse order, so un-reverse it
263}
264
265
266/********************* the gui stuff *********************/
267
276
277// close the window and clean up
279 const gint response_id,
280 gpointer user_data)
281{
283 g_object_unref(G_OBJECT(gui->model));
284 gtk_widget_destroy(dialog);
285 dt_free(gui);
286}
287
288
290{
291 GList *rr_list = gui->rows_to_remove;
292 GtkTreeModel *model = gui->model;
293
294 // Remove TreeView rows from rr_list. It needs to be populated before
295 for(GList *node = rr_list; !IS_NULL_PTR(node); node = g_list_next(node))
296 {
297 GtkTreePath *path = gtk_tree_row_reference_get_path((GtkTreeRowReference*)node->data);
298
299 if(path)
300 {
301 GtkTreeIter iter;
302 if(gtk_tree_model_get_iter(model, &iter, path))
303 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
304 }
305 }
306
307 // Cleanup the list of rows
308 g_list_foreach(rr_list, (GFunc) gtk_tree_row_reference_free, NULL);
309 g_list_free(rr_list);
310 rr_list = NULL;
311}
312
313
314static void _select_all_callback(GtkButton *button,
315 gpointer user_data)
316{
318 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
319 gtk_tree_selection_select_all(selection);
320}
321
322
323static void _select_none_callback(GtkButton *button, gpointer user_data)
324{
326 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
327 gtk_tree_selection_unselect_all(selection);
328}
329
330
331static void _select_invert_callback(GtkButton *button, gpointer user_data)
332{
334 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
335
336 GtkTreeIter iter;
337 gboolean valid = gtk_tree_model_get_iter_first(gui->model, &iter);
338 while(valid)
339 {
340 if(gtk_tree_selection_iter_is_selected(selection, &iter))
341 gtk_tree_selection_unselect_iter(selection, &iter);
342 else
343 gtk_tree_selection_select_iter(selection, &iter);
344
345 valid = gtk_tree_model_iter_next(gui->model, &iter);
346 }
347}
348
349
350static void _db_update_timestamp(const int id, const time_t timestamp)
351{
352 // Update DB writing timestamp with XMP file timestamp
353 sqlite3_stmt *stmt;
356 "UPDATE main.images"
357 " SET write_timestamp = ?2"
358 " WHERE id = ?1", -1, &stmt, NULL);
359 DT_DEBUG_SQLITE3_BIND_INT(stmt, 1, id);
360 DT_DEBUG_SQLITE3_BIND_INT(stmt, 2, timestamp);
361 sqlite3_step(stmt);
362 sqlite3_finalize(stmt);
363}
364
365
366static void _get_crawler_entry_from_model(GtkTreeModel *model,
367 GtkTreeIter *iter,
369{
370 gtk_tree_model_get(model, iter,
376}
377
378
379static void _append_row_to_remove(GtkTreeModel *model,
380 GtkTreePath *path,
381 GList **rowref_list)
382{
383 // append TreeModel rows to the list to remove
384 GtkTreeRowReference *rowref = gtk_tree_row_reference_new(model, path);
385 *rowref_list = g_list_append(*rowref_list, rowref);
386}
387
389 gchar *pattern,
390 gchar *filepath)
391{
392 gchar *message = pattern;
393 gboolean to_free = FALSE;
394
395 if(!IS_NULL_PTR(filepath))
396 {
397 message = g_strdup_printf(pattern, filepath);
398 to_free = TRUE;
399 }
400
401 // add a new line in the log TreeView
402 GtkTreeIter iter_log;
403 GtkTreeModel *model_log = gtk_tree_view_get_model(GTK_TREE_VIEW(gui->log));
404 gtk_list_store_append(GTK_LIST_STORE(model_log), &iter_log);
405 gtk_list_store_set(GTK_LIST_STORE(model_log), &iter_log,
406 0, message,
407 -1);
408
409 if(to_free)
410 {
411 dt_free(message);
412 }
413}
414
415
416static void sync_xmp_to_db(GtkTreeModel *model,
417 GtkTreePath *path,
418 GtkTreeIter *iter,
419 gpointer user_data)
420{
422 dt_control_crawler_result_t entry = { 0 };
425
426 const int error =
427 dt_history_load_and_apply_on_image(entry.id, entry.xmp_path, 0); // success = 0, fail = 1
428
429 if(error)
430 {
431 _log_synchronization(gui, _("ERROR: %s NOT synced XMP \342\206\222 DB"), entry.image_path);
432 _log_synchronization(gui, _("ERROR: cannot write the database."
433 " the destination may be full, offline or read-only."),
434 NULL);
435 }
436 else
437 {
439 _log_synchronization(gui, _("SUCCESS: %s synced XMP \342\206\222 DB"), entry.image_path);
440 }
441
442 _free_crawler_result(&entry);
443}
444
445
446static void sync_db_to_xmp(GtkTreeModel *model,
447 GtkTreePath *path,
448 GtkTreeIter *iter,
449 gpointer user_data)
450{
452 dt_control_crawler_result_t entry = { 0 };
454
455 // write the XMP and make sure it get the last modified timestamp of the db
456 const int error = dt_image_write_sidecar_file(entry.id); // success = 0, fail = 1
458
459 if(error)
460 {
461 _log_synchronization(gui, _("ERROR: %s NOT synced DB \342\206\222 XMP"), entry.image_path);
463 _("ERROR: cannot write %s \nthe destination may be full,"
464 " offline or read-only."), entry.xmp_path);
465 }
466 else
467 {
469 _log_synchronization(gui, _("SUCCESS: %s synced DB \342\206\222 XMP"), entry.image_path);
470 }
471
472 _free_crawler_result(&entry);
473}
474
475static void sync_newest_to_oldest(GtkTreeModel *model,
476 GtkTreePath *path,
477 GtkTreeIter *iter,
478 gpointer user_data)
479{
481 dt_control_crawler_result_t entry = { 0 };
483
484 int error = 0;
485
486 if(entry.timestamp_xmp > entry.timestamp_db)
487 {
488 // WRITE XMP in DB
491 if(error)
492 {
494 (gui,
495 _("ERROR: %s NOT synced new (XMP) \342\206\222 old (DB)"), entry.image_path);
497 (gui,
498 _("ERROR: cannot write the database. the destination may be full,"
499 " offline or read-only."), NULL);
500 }
501 else
502 {
504 (gui,
505 _("SUCCESS: %s synced new (XMP) \342\206\222 old (DB)"), entry.image_path);
506 }
507 }
508 else if(entry.timestamp_xmp < entry.timestamp_db)
509 {
510 // write the XMP and make sure it get the last modified timestamp of the db
513
514 fprintf(stdout, "%s synced DB (new) \342\206\222 XMP (old)\n", entry.image_path);
515 if(error)
516 {
518 (gui,
519 _("ERROR: %s NOT synced new (DB) \342\206\222 old (XMP)"), entry.image_path);
521 (gui,
522 _("ERROR: cannot write %s \nthe destination may be full, offline or read-only."),
523 entry.xmp_path);
524 }
525 else
526 {
527 _log_synchronization(gui, _("SUCCESS: %s synced new (DB) \342\206\222 old (XMP)"),
528 entry.image_path);
529 }
530 }
531 else
532 {
533 // we should never reach that part of the code
534 // if both timestamps are equal, they should not be in this list in the first place
535 error = 1;
536 _log_synchronization(gui, _("EXCEPTION: %s has inconsistent timestamps"),
537 entry.image_path);
538 }
539
541
542 _free_crawler_result(&entry);
543}
544
545
546static void sync_oldest_to_newest(GtkTreeModel *model,
547 GtkTreePath *path,
548 GtkTreeIter *iter,
549 gpointer user_data)
550{
552 dt_control_crawler_result_t entry = { 0 };
554 int error = 0;
555
556 if(entry.timestamp_xmp < entry.timestamp_db)
557 {
558 // WRITE XMP in DB
561 if(error)
562 {
564 _("ERROR: %s NOT synced old (XMP) \342\206\222 new (DB)"),
565 entry.image_path);
567 _("ERROR: cannot write the database."
568 " the destination may be full, offline or read-only."), NULL);
569 }
570 else
571 {
573 _("SUCCESS: %s synced old (XMP) \342\206\222 new (DB)"),
574 entry.image_path);
575 }
576 }
577 else if(entry.timestamp_xmp > entry.timestamp_db)
578 {
579 // WRITE DB in XMP
582 if(error)
583 {
585 _("ERROR: %s NOT synced old (DB) \342\206\222 new (XMP)"),
586 entry.image_path);
588 _("ERROR: cannot write %s \nthe destination may be full,"
589 " offline or read-only."), entry.xmp_path);
590 }
591 else
592 {
594 _("SUCCESS: %s synced old (DB) \342\206\222 new (XMP)"),
595 entry.image_path);
596 }
597 }
598 else
599 {
600 // we should never reach that part of the code
601 // if both timestamps are equal, they should not be in this list in the first place
602 error = 1;
604 _("EXCEPTION: %s has inconsistent timestamps"),
605 entry.image_path);
606 }
607
608 if(!error)
610
611 _free_crawler_result(&entry);
612}
613
614// overwrite database with xmp
615static void _reload_button_clicked(GtkButton *button, gpointer user_data)
616{
618 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
619 gui->rows_to_remove = NULL;
620 gtk_spinner_start(GTK_SPINNER(gui->spinner));
621 gtk_tree_selection_selected_foreach(selection, sync_xmp_to_db, gui);
623 gtk_spinner_stop(GTK_SPINNER(gui->spinner));
624}
625
626// overwrite xmp with database
627void _overwrite_button_clicked(GtkButton *button, gpointer user_data)
628{
630 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
631 gui->rows_to_remove = NULL;
632 gtk_spinner_start(GTK_SPINNER(gui->spinner));
633 gtk_tree_selection_selected_foreach(selection, sync_db_to_xmp, gui);
635 gtk_spinner_stop(GTK_SPINNER(gui->spinner));
636}
637
638// overwrite the oldest with the newest
639static void _newest_button_clicked(GtkButton *button, gpointer user_data)
640{
642 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
643 gui->rows_to_remove = NULL;
644 gtk_spinner_start(GTK_SPINNER(gui->spinner));
645 gtk_tree_selection_selected_foreach(selection, sync_newest_to_oldest, gui);
647 gtk_spinner_stop(GTK_SPINNER(gui->spinner));
648}
649
650// overwrite the newest with the oldest
651static void _oldest_button_clicked(GtkButton *button, gpointer user_data)
652{
654 GtkTreeSelection *selection = gtk_tree_view_get_selection(gui->tree);
655 gui->rows_to_remove = NULL;
656 gtk_spinner_start(GTK_SPINNER(gui->spinner));
657 gtk_tree_selection_selected_foreach(selection, sync_oldest_to_newest, gui);
659 gtk_spinner_stop(GTK_SPINNER(gui->spinner));
660}
661
662static gchar* str_time_delta(const int time_delta)
663{
664 // display the time difference as a legible string
665 int seconds = time_delta;
666
667 int minutes = seconds / 60;
668 seconds -= 60 * minutes;
669
670 int hours = minutes / 60;
671 minutes -= 60 * hours;
672
673 const int days = hours / 24;
674 hours -= 24 * days;
675
676 return g_strdup_printf(_("%id %02dh %02dm %02ds"), days, hours, minutes, seconds);
677}
678
679// show a popup window with a list of updated images/xmp files and allow the user to tell dt what to do about them
681{
682 if(IS_NULL_PTR(images)) return;
683
686
687 // a list with all the images
688 GtkTreeViewColumn *column;
689 GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
690 gtk_widget_set_vexpand(scroll, TRUE);
691 GtkListStore *store = gtk_list_store_new(DT_CONTROL_CRAWLER_NUM_COLS,
692 G_TYPE_INT, // id
693 G_TYPE_STRING, // image path
694 G_TYPE_STRING, // xmp path
695 G_TYPE_STRING, // timestamp from xmp
696 G_TYPE_STRING, // timestamp from db
697 G_TYPE_INT, // timestamp to db
698 G_TYPE_INT,
699 G_TYPE_STRING, // report: newer version
700 G_TYPE_STRING);// time delta
701
702 gui->model = GTK_TREE_MODEL(store);
703
704 for(GList *list_iter = images; list_iter; list_iter = g_list_next(list_iter))
705 {
706 GtkTreeIter iter;
707 dt_control_crawler_result_t *item = list_iter->data;
708 char timestamp_db[64], timestamp_xmp[64];
709 struct tm tm_stamp;
710 strftime(timestamp_db, sizeof(timestamp_db),
711 "%c", localtime_r(&item->timestamp_db, &tm_stamp));
712 strftime(timestamp_xmp, sizeof(timestamp_xmp),
713 "%c", localtime_r(&item->timestamp_xmp, &tm_stamp));
714
715 const time_t time_delta = llabs(item->timestamp_db - item->timestamp_xmp);
716 gchar *timestamp_delta = str_time_delta(time_delta);
717
718 gtk_list_store_append(store, &iter);
719 gtk_list_store_set
720 (store, &iter,
724 DT_CONTROL_CRAWLER_COL_TS_XMP, timestamp_xmp,
725 DT_CONTROL_CRAWLER_COL_TS_DB, timestamp_db,
729 ? _("XMP")
730 : _("database"),
731 DT_CONTROL_CRAWLER_COL_TIME_DELTA, timestamp_delta,
732 -1);
734 dt_free(timestamp_delta);
735 }
736 g_list_free_full(images, dt_free_gpointer);
737 images = NULL;
738
739 GtkWidget *tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
740 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
741 gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
742
743 gui->tree = GTK_TREE_VIEW(tree); // FIXME: do we need to free that later ?
744
745 GtkCellRenderer *renderer_text = gtk_cell_renderer_text_new();
746 column = gtk_tree_view_column_new_with_attributes
747 (_("path"), renderer_text, "text",
749 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
750 gtk_tree_view_column_set_expand(column, TRUE);
751 gtk_tree_view_column_set_resizable(column, TRUE);
752 gtk_tree_view_column_set_min_width(column, DT_PIXEL_APPLY_DPI(200));
753 g_object_set(renderer_text, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, NULL);
754
755 column = gtk_tree_view_column_new_with_attributes
756 (_("XMP timestamp"), gtk_cell_renderer_text_new(), "text",
758 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
759
760 column = gtk_tree_view_column_new_with_attributes
761 (_("database timestamp"), gtk_cell_renderer_text_new(), "text",
763 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
764
765 column = gtk_tree_view_column_new_with_attributes
766 (_("newest"), gtk_cell_renderer_text_new(), "text",
768 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
769
770 GtkCellRenderer *renderer_date = gtk_cell_renderer_text_new();
771 column = gtk_tree_view_column_new_with_attributes
772 (_("time difference"), renderer_date, "text",
774 g_object_set(renderer_date, "xalign", 1., NULL);
775 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
776
777 dt_gui_add_class(scroll, "dt_recessed_scroll");
778 gtk_container_add(GTK_CONTAINER(scroll), tree);
779 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
780 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
781
782 // build a dialog window that contains the list of images
784 GtkWidget *dialog = gtk_dialog_new_with_buttons
785 (_("updated XMP sidecar files found"), GTK_WINDOW(win),
786 GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, _("_close"),
787 GTK_RESPONSE_CLOSE, NULL);
788
789#ifdef GDK_WINDOWING_QUARTZ
791#endif
792 gtk_widget_set_size_request(dialog, -1, DT_PIXEL_APPLY_DPI(400));
793 gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(win));
794 GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
795
796 GtkWidget *content_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
797 gtk_container_add(GTK_CONTAINER(content_area), content_box);
798
799 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
800 gtk_box_pack_start(GTK_BOX(content_box), box, FALSE, FALSE, 0);
801 GtkWidget *select_all = gtk_button_new_with_label(_("select all"));
802 GtkWidget *select_none = gtk_button_new_with_label(_("select none"));
803 GtkWidget *select_invert = gtk_button_new_with_label(_("invert selection"));
804 gtk_box_pack_start(GTK_BOX(box), select_all, FALSE, FALSE, 0);
805 gtk_box_pack_start(GTK_BOX(box), select_none, FALSE, FALSE, 0);
806 gtk_box_pack_start(GTK_BOX(box), select_invert, FALSE, FALSE, 0);
807 g_signal_connect(select_all, "clicked", G_CALLBACK(_select_all_callback), gui);
808 g_signal_connect(select_none, "clicked", G_CALLBACK(_select_none_callback), gui);
809 g_signal_connect(select_invert, "clicked", G_CALLBACK(_select_invert_callback), gui);
810
811 gtk_box_pack_start(GTK_BOX(content_box), scroll, TRUE, TRUE, 0);
812
813 box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
814 gtk_box_pack_start(GTK_BOX(content_box), box, FALSE, FALSE, 1);
815 GtkWidget *label = gtk_label_new_with_mnemonic(_("on the selection:"));
816 GtkWidget *reload_button = gtk_button_new_with_label(_("keep the XMP edit"));
817 GtkWidget *overwrite_button = gtk_button_new_with_label(_("keep the database edit"));
818 GtkWidget *newest_button = gtk_button_new_with_label(_("keep the newest edit"));
819 GtkWidget *oldest_button = gtk_button_new_with_label(_("keep the oldest edit"));
820 gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0);
821 gtk_box_pack_start(GTK_BOX(box), reload_button, FALSE, FALSE, 0);
822 gtk_box_pack_start(GTK_BOX(box), overwrite_button, FALSE, FALSE, 0);
823 gtk_box_pack_start(GTK_BOX(box), newest_button, FALSE, FALSE, 0);
824 gtk_box_pack_start(GTK_BOX(box), oldest_button, FALSE, FALSE, 0);
825 g_signal_connect(reload_button, "clicked", G_CALLBACK(_reload_button_clicked), gui);
826 g_signal_connect(overwrite_button, "clicked", G_CALLBACK(_overwrite_button_clicked), gui);
827 g_signal_connect(newest_button, "clicked", G_CALLBACK(_newest_button_clicked), gui);
828 g_signal_connect(oldest_button, "clicked", G_CALLBACK(_oldest_button_clicked), gui);
829
830 /* Feedback spinner in case synch happens over network and stales */
831 gui->spinner = gtk_spinner_new();
832 gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(gui->spinner), FALSE, FALSE, 0);
833
834 /* Log report */
835 scroll = gtk_scrolled_window_new(NULL, NULL);
836 gui->log = gtk_tree_view_new();
837 gtk_box_pack_start(GTK_BOX(content_box), scroll, TRUE, TRUE, 0);
838 dt_gui_add_class(scroll, "dt_recessed_scroll");
839 gtk_container_add(GTK_CONTAINER(scroll), gui->log);
840 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
841 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
842
843 gtk_tree_view_insert_column_with_attributes
844 (GTK_TREE_VIEW(gui->log), -1,
845 _("synchronization log"), renderer_text,
846 "text", 0, NULL);
847
848 GtkListStore *store_log = gtk_list_store_new (1, G_TYPE_STRING);
849 GtkTreeModel *model_log = GTK_TREE_MODEL(store_log);
850 gtk_tree_view_set_model(GTK_TREE_VIEW(gui->log), model_log);
851 g_object_unref(model_log);
852
853 gtk_widget_show_all(dialog);
854
855 g_signal_connect(dialog, "response",
856 G_CALLBACK(dt_control_crawler_response_callback), gui);
857}
858
859// clang-format off
860// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
861// vim: shiftwidth=2 expandtab tabstop=2 cindent
862// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
863// clang-format on
static void error(char *msg)
Definition ashift_lsd.c:202
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
int dt_image_write_sidecar_file(const int32_t imgid)
char * dt_image_get_text_path_from_path(const char *image_path)
void dt_image_path_append_version_no_db(int version, char *pathname, size_t pathname_len)
void dt_control_crawler_show_image_list(GList *images)
Definition crawler.c:680
void _overwrite_button_clicked(GtkButton *button, gpointer user_data)
Definition crawler.c:627
static void sync_db_to_xmp(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
Definition crawler.c:446
dt_control_crawler_cols_t
Definition crawler.c:55
@ DT_CONTROL_CRAWLER_COL_XMP_PATH
Definition crawler.c:58
@ DT_CONTROL_CRAWLER_COL_TS_XMP
Definition crawler.c:59
@ DT_CONTROL_CRAWLER_COL_TS_DB_INT
Definition crawler.c:62
@ DT_CONTROL_CRAWLER_NUM_COLS
Definition crawler.c:65
@ DT_CONTROL_CRAWLER_COL_REPORT
Definition crawler.c:63
@ DT_CONTROL_CRAWLER_COL_IMAGE_PATH
Definition crawler.c:57
@ DT_CONTROL_CRAWLER_COL_TIME_DELTA
Definition crawler.c:64
@ DT_CONTROL_CRAWLER_COL_TS_DB
Definition crawler.c:60
@ DT_CONTROL_CRAWLER_COL_ID
Definition crawler.c:56
@ DT_CONTROL_CRAWLER_COL_TS_XMP_INT
Definition crawler.c:61
static void _append_row_to_remove(GtkTreeModel *model, GtkTreePath *path, GList **rowref_list)
Definition crawler.c:379
static void _select_invert_callback(GtkButton *button, gpointer user_data)
Definition crawler.c:331
static void _log_synchronization(dt_control_crawler_gui_t *gui, gchar *pattern, gchar *filepath)
Definition crawler.c:388
static void _oldest_button_clicked(GtkButton *button, gpointer user_data)
Definition crawler.c:651
static void _free_crawler_result(dt_control_crawler_result_t *entry)
Definition crawler.c:76
static gchar * str_time_delta(const int time_delta)
Definition crawler.c:662
static void sync_oldest_to_newest(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
Definition crawler.c:546
static void _set_modification_time(char *filename, const time_t timestamp)
Definition crawler.c:83
static void _reload_button_clicked(GtkButton *button, gpointer user_data)
Definition crawler.c:615
static void _select_all_callback(GtkButton *button, gpointer user_data)
Definition crawler.c:314
static void sync_xmp_to_db(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
Definition crawler.c:416
static void _newest_button_clicked(GtkButton *button, gpointer user_data)
Definition crawler.c:639
GList * dt_control_crawler_run(void)
Definition crawler.c:121
static void _delete_selected_rows(dt_control_crawler_gui_t *gui)
Definition crawler.c:289
static void _get_crawler_entry_from_model(GtkTreeModel *model, GtkTreeIter *iter, dt_control_crawler_result_t *entry)
Definition crawler.c:366
static void _db_update_timestamp(const int id, const time_t timestamp)
Definition crawler.c:350
static void dt_control_crawler_response_callback(GtkWidget *dialog, const gint response_id, gpointer user_data)
Definition crawler.c:278
static void _select_none_callback(GtkButton *button, gpointer user_data)
Definition crawler.c:323
static void sync_newest_to_oldest(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
Definition crawler.c:475
darktable_t darktable
Definition darktable.c:181
void dt_print(dt_debug_thread_t thread, const char *msg,...)
Definition darktable.c:1542
@ DT_DEBUG_CONTROL
Definition darktable.h:716
static void dt_free_gpointer(gpointer ptr)
Definition darktable.h:463
#define dt_free(ptr)
Definition darktable.h:456
#define PATH_MAX
Definition darktable.h:1062
#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 darktable.h:281
sqlite3 * dt_database_get(const dt_database_t *db)
Definition database.c:3646
#define dt_database_start_transaction(db)
Definition database.h:77
#define dt_database_release_transaction(db)
Definition database.h:78
#define DT_DEBUG_SQLITE3_PREPARE_V2(a, b, c, d, e)
Definition debug.h:107
#define DT_DEBUG_SQLITE3_BIND_INT(a, b, c)
Definition debug.h:115
int store(dt_imageio_module_storage_t *self, dt_imageio_module_data_t *sdata, const int32_t imgid, dt_imageio_module_format_t *format, dt_imageio_module_data_t *fdata, const int num, const int total, const gboolean high_quality, const gboolean export_masks, dt_colorspaces_color_profile_type_t icc_type, const gchar *icc_filename, dt_iop_color_intent_t icc_intent, dt_export_metadata_t *metadata)
Definition disk.c:252
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)
Definition gtk.c:133
GtkWidget * dt_ui_main_window(dt_ui_t *ui)
get the main window widget
#define DT_GUI_BOX_SPACING
Definition gtk.h:109
#define DT_PIXEL_APPLY_DPI(value)
Definition gtk.h:90
int dt_history_load_and_apply_on_image(int32_t imgid, gchar *filename, int history_only)
@ DT_IMAGE_HAS_WAV
Definition image.h:125
@ DT_IMAGE_HAS_TXT
Definition image.h:123
const char * model
dt_mipmap_buffer_dsc_flags flags
Definition mipmap_cache.c:4
void dt_osx_disallow_fullscreen(GtkWidget *widget)
Definition osx.mm:104
struct _GtkWidget GtkWidget
Definition splash.h:29
struct dt_gui_gtk_t * gui
Definition darktable.h:775
const struct dt_database_t * db
Definition darktable.h:779
GtkTreeModel * model
Definition crawler.c:271
GtkTreeView * tree
Definition crawler.c:270
dt_ui_t * ui
Definition gtk.h:164
gchar * dt_util_normalize_path(const gchar *_input)
Definition utility.c:680