/* This file is part of darktable, Copyright (C) 2012-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. darktable is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with darktable. If not, see . */ #include "common/gdk_event_utils.h" #include "common/debug.h" #include "common/file_location.h" #include "common/image_cache.h" #include "common/collection.h" #include "common/selection.h" #include "common/gpx.h" #include "common/geo.h" #include "common/datetime.h" #include "control/conf.h" #include "dtgtk/button.h" #include "control/jobs.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "libs/lib_api.h" #ifdef HAVE_MAP #include "views/view.h" #endif #ifdef GDK_WINDOWING_QUARTZ #include "osx/osx.h" #endif #include DT_MODULE(1) typedef struct tz_tuple_t { char *name, *display; } tz_tuple_t; #define DT_GEOTAG_PARTS_NB 7 typedef struct dt_lib_datetime_t { GtkWidget *widget[DT_GEOTAG_PARTS_NB]; GtkWidget *sign; } dt_lib_datetime_t; #ifdef HAVE_MAP typedef struct dt_lib_tracks_data_t { GObject *track; dt_map_box_t map_box; } dt_lib_tracks_data_t; typedef struct dt_lib_tracks_t { dt_lib_tracks_data_t td[1]; } dt_lib_tracks_t; typedef enum dt_tracks_cols_t { DT_GEO_TRACKS_ACTIVE = 0, // active / deactivated track DT_GEO_TRACKS_DATETIME, // displayed start datetime DT_GEO_TRACKS_POINTS, // nb points DT_GEO_TRACKS_IMAGES, // nb images DT_GEO_TRACKS_SEGID, // id track segment DT_GEO_TRACKS_TOOLTIP, // datetime details DT_GEO_TRACKS_NUM_COLS } dt_tracks_cols_t; #endif typedef struct dt_lib_geotagging_t { dt_lib_datetime_t dt; dt_lib_datetime_t dt0; dt_lib_datetime_t of; GDateTime *datetime; GDateTime *datetime0; GTimeSpan offset; gboolean editing; dt_imgid_t imgid; GList* imgs; int nb_imgs; GtkWidget *apply_offset; GtkWidget *lock_offset; GtkWidget *apply_datetime; GtkWidget *timezone; GList *timezones; GtkWidget *timezone_changed; GtkWidget *gpx_button; GTimeZone *tz_camera; #ifdef HAVE_MAP struct { gboolean view; GtkWidget *gpx_button, *gpx_file, *gpx_view; struct dt_gpx_t *gpx; dt_lib_tracks_t *tracks; dt_map_box_t map_box; int nb_tracks, nb_imgs; GtkWidget *gpx_section, *preview_button, *apply_gpx_button, *select_button, *nb_imgs_label; GtkTreeViewColumn *sel_tracks; } map; #endif } dt_lib_geotagging_t; typedef struct dt_sel_img_t { dt_imgid_t imgid; uint32_t segid; gchar dt[DT_DATETIME_LENGTH]; gboolean counted; dt_image_geoloc_t gl; GObject *image; } dt_sel_img_t; static void _datetime_entry_changed(GtkWidget *entry, dt_lib_module_t *self); static void _setup_selected_images_list(dt_lib_module_t *self); static void free_tz_tuple(gpointer data) { tz_tuple_t *tz_tuple = (tz_tuple_t *)data; g_free(tz_tuple->display); #ifdef _WIN32 g_free(tz_tuple->name); // on non-Windows both point to the same string #endif free(tz_tuple); } const char *name(dt_lib_module_t *self) { return _("geotagging"); } const char *description(dt_lib_module_t *self) { return _("set geolocation information for\n" "the currently selected images"); } dt_view_type_flags_t views(dt_lib_module_t *self) { #ifdef HAVE_MAP return DT_VIEW_LIGHTTABLE | DT_VIEW_MAP; #else return DT_VIEW_LIGHTTABLE; #endif } uint32_t container(dt_lib_module_t *self) { return DT_UI_CONTAINER_PANEL_RIGHT_CENTER; } int position(const dt_lib_module_t *self) { return 450; } // modify the datetime_taken field in the db/cache of selected images static void _apply_offset_callback(GtkWidget *widget, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; if(d->offset) dt_control_datetime(d->offset, NULL, NULL); } // modify the datetime_taken field in the db/cache of selected images static void _apply_datetime_callback(GtkWidget *widget, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; if(d->datetime) { char dt[DT_DATETIME_LENGTH]; dt_datetime_gdatetime_to_exif(dt, sizeof(dt), d->datetime); dt_control_datetime(0, dt, NULL); } } static gboolean _lib_geotagging_filter_gpx(const GtkFileFilterInfo *filter_info, gpointer data) { if(!g_ascii_strcasecmp(filter_info->mime_type, "application/gpx+xml")) return TRUE; const gchar *filename = filter_info->filename; const char *cc = filename + strlen(filename); for(; *cc != '.' && cc > filename; cc--) ; if(!g_ascii_strcasecmp(cc, ".gpx")) return TRUE; return FALSE; } static GtkWidget *_set_up_label(const char *name, const int align, GtkWidget *grid, const int col, const int line, const int ellipsize) { GtkWidget *label = gtk_label_new(name); gtk_label_set_ellipsize(GTK_LABEL(label), ellipsize); if(ellipsize != PANGO_ELLIPSIZE_NONE) gtk_widget_set_visible(label, TRUE); gtk_widget_set_halign(label, align); gtk_widget_set_hexpand(label, TRUE); gtk_grid_attach(GTK_GRID(grid), label, col, line, 1, 1); return label; } static gchar *_utc_timeval_to_localtime_text(GDateTime *utc_dt, GTimeZone *tz_camera, const gboolean full) { GDateTime *local_dt = g_date_time_to_timezone(utc_dt, tz_camera); gchar *dts = g_date_time_format(local_dt, full ? "%Y:%m:%d %H:%M:%S" : "%H:%M:%S"); g_date_time_unref(local_dt); return dts; } static GDateTime *_localtime_text_to_utc_timeval(const char *date_time, GTimeZone *tz_camera, GTimeZone *tz_utc, GTimeSpan offset) { GDateTime *exif_time = dt_datetime_exif_to_gdatetime(date_time, tz_camera); GDateTime *dt_offset = g_date_time_add(exif_time, offset); GDateTime *utc_time = g_date_time_to_timezone(dt_offset, tz_utc); g_date_time_unref(exif_time); g_date_time_unref(dt_offset); return utc_time; } static int _count_images_per_track(dt_gpx_track_segment_t *t, dt_gpx_track_segment_t *n, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; int nb_imgs = 0; for(GList *i = d->imgs; i; i = g_list_next(i)) { dt_sel_img_t *im = i->data; if(im->segid == -1) { GDateTime *dt = _localtime_text_to_utc_timeval(im->dt, d->tz_camera, darktable.utc_tz, d->offset); if((g_date_time_compare(dt, t->start_dt) >= 0 && g_date_time_compare(dt, t->end_dt) <= 0) || (n && g_date_time_compare(dt, t->end_dt) >= 0 && g_date_time_compare(dt, n->start_dt) <= 0)) { nb_imgs++; im->segid = t->id; } g_date_time_unref(dt); } } return nb_imgs; } #ifdef HAVE_MAP static gchar *_utc_timeval_to_utc_text(GDateTime *utc_dt, const gboolean full) { gchar *dts = g_date_time_format(utc_dt, full ? "%Y:%m:%d %H:%M:%S" : "%H:%M:%S"); return dts; } static gchar *_datetime_tooltip(GDateTime *start, GDateTime *end, GTimeZone *tz) { gchar *dtsl = _utc_timeval_to_localtime_text(start, tz, FALSE); gchar *dtel = _utc_timeval_to_localtime_text(end, tz, FALSE); gchar *dtsu = _utc_timeval_to_utc_text(start, FALSE); gchar *dteu = _utc_timeval_to_utc_text(end, FALSE); gchar *res = g_strdup_printf("%s -> %s LT\n%s -> %s UTC", dtsl, dtel, dtsu, dteu); g_free(dtsl); g_free(dtel); g_free(dtsu); g_free(dteu); return res; } static void _remove_images_from_map(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; for(GList *i = d->imgs; i; i = g_list_next(i)) { dt_sel_img_t *im = i->data; if(im->image) { dt_view_map_remove_marker(darktable.view_manager, MAP_DISPLAY_THUMB, im->image); im->image = NULL; } } } static void _refresh_images_displayed_on_track(const int segid, const gboolean active, dt_lib_module_t *self) { dt_gui_cursor_set_busy(); dt_lib_geotagging_t *d = self->data; for(GList *i = d->imgs; i; i = g_list_next(i)) { dt_sel_img_t *im = i->data; if(im->segid == segid && active) { GDateTime *dt = _localtime_text_to_utc_timeval(im->dt, d->tz_camera, darktable.utc_tz, d->offset); if(!dt_gpx_get_location(d->map.gpx, dt, &im->gl)) im->gl.latitude = NAN; g_date_time_unref(dt); } else if(im->segid == segid && !active && im->image) { dt_view_map_remove_marker(darktable.view_manager, MAP_DISPLAY_THUMB, im->image); im->image = NULL; im->gl.latitude = NAN; } } int count = 0; for(GList *i = d->imgs; active && i; i = g_list_next(i)) { dt_sel_img_t *im = i->data; if(im->segid == segid && im->gl.latitude != NAN) { count++; dt_sel_img_t *next = i->next ? (dt_sel_img_t *)i->next->data : NULL; if(!im->image && (!next || !((next->gl.latitude == im->gl.latitude) && (next->gl.longitude == im->gl.longitude)))) { struct {dt_imgid_t imgid; float latitude; float longitude; int count;} p; p.imgid = im->imgid; p.latitude = im->gl.latitude; p.longitude = im->gl.longitude; p.count = count == 1 ? 0 : count; GList *img = g_list_prepend(NULL, &p); im->image = dt_view_map_add_marker(darktable.view_manager, MAP_DISPLAY_THUMB, img); g_list_free(img); count = 0; } } } dt_gui_cursor_clear_busy(); } static void _update_nb_images(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view)); GtkTreeIter iter; gboolean valid = gtk_tree_model_get_iter_first(model, &iter); int nb_imgs = 0; for(int segid = 0; valid && segid < d->map.nb_tracks; segid++) { gboolean active; int nb; gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_ACTIVE, &active, DT_GEO_TRACKS_IMAGES, &nb, -1); if(active) nb_imgs += nb; valid = gtk_tree_model_iter_next(model, &iter); } d->map.nb_imgs = nb_imgs; gchar *nb = g_strdup_printf("%d/%d", nb_imgs, d->nb_imgs); gtk_label_set_text(GTK_LABEL(d->map.nb_imgs_label), nb); g_free(nb); } static void _update_buttons(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; gtk_widget_set_sensitive(d->map.preview_button, d->map.nb_tracks); GtkWidget *label = gtk_bin_get_child(GTK_BIN(d->map.apply_gpx_button)); gtk_label_set_text(GTK_LABEL(label), d->offset ? _("apply offset and geo-location") : _("apply geo-location")); gtk_widget_set_tooltip_text(d->map.apply_gpx_button, d->offset ? _("apply offset and geo-location to matching images" "\ndouble operation: two ctrl+z to undo") : _("apply geo-location to matching images")); gtk_widget_set_sensitive(d->map.apply_gpx_button, d->map.nb_imgs); gtk_widget_set_sensitive(d->map.select_button, d->map.nb_imgs && d->map.nb_imgs != d->nb_imgs); } static GList *_get_images_on_active_tracks(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view)); GtkTreeIter iter; int segid = 0; GList *imgs = NULL; gboolean valid = gtk_tree_model_get_iter_first(model, &iter); while(valid) { gboolean active; gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_ACTIVE, &active, -1); if(active) { for(GList *i = d->imgs; i; i = g_list_next(i)) { dt_sel_img_t *im = i->data; if(im->segid == segid) imgs = g_list_prepend(imgs, GINT_TO_POINTER(im->imgid)); } } valid = gtk_tree_model_iter_next(model, &iter); segid++; } return imgs; } static void _refresh_displayed_images(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view)); GtkTreeIter iter; const gboolean preview = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->map.preview_button)); gboolean valid = gtk_tree_model_get_iter_first(model, &iter); for(int segid = 0; valid && segid < d->map.nb_tracks; segid++) { gboolean active; gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_ACTIVE, &active, -1); _refresh_images_displayed_on_track(segid, active && preview, self); valid = gtk_tree_model_iter_next(model, &iter); } } static gboolean _update_map_box(const guint segid, GList *pts, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; // box for this track if(pts) { d->map.tracks->td[segid].map_box.lon1 = 180.0; d->map.tracks->td[segid].map_box.lon2 = -180.0; d->map.tracks->td[segid].map_box.lat1 = -90.0; d->map.tracks->td[segid].map_box.lat2 = 90.0; for(GList *pt = pts; pt; pt = g_list_next(pt)) { dt_geo_map_display_point_t *p = pt->data; if(p->lon < d->map.tracks->td[segid].map_box.lon1) d->map.tracks->td[segid].map_box.lon1 = MAX(-180.0, p->lon); if(p->lon > d->map.tracks->td[segid].map_box.lon2) d->map.tracks->td[segid].map_box.lon2 = MIN(180.0, p->lon); if(p->lat > d->map.tracks->td[segid].map_box.lat1) d->map.tracks->td[segid].map_box.lat1 = MIN(90.0, p->lat); if(p->lat < d->map.tracks->td[segid].map_box.lat2) d->map.tracks->td[segid].map_box.lat2 = MAX(-90.0, p->lat); } } // box for all tracks float lon1 = 180.0; float lon2 = -180.0; float lat1 = -90.0; float lat2 = 90.0; for(int i = 0; i < d->map.nb_tracks; i++) { if(d->map.tracks->td[i].track) { if(d->map.tracks->td[i].map_box.lon1 < lon1) lon1 = d->map.tracks->td[i].map_box.lon1; if(d->map.tracks->td[i].map_box.lon2 > lon2) lon2 = d->map.tracks->td[i].map_box.lon2; if(d->map.tracks->td[i].map_box.lat1 > lat1) lat1 = d->map.tracks->td[i].map_box.lat1; if(d->map.tracks->td[i].map_box.lat2 < lat2) lat2 = d->map.tracks->td[i].map_box.lat2; } } const gboolean grow = lon1 < d->map.map_box.lon1 || lon2 > d->map.map_box.lon1 || lat1 > d->map.map_box.lat1 || lat2 < d->map.map_box.lat2; d->map.map_box.lon1 = lon1; d->map.map_box.lon2 = lon2; d->map.map_box.lat1 = lat1; d->map.map_box.lat2 = lat2; return grow; } static void _remove_tracks_from_map(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; if(d->map.tracks) { for(int i = 0; i < d->map.nb_tracks; i++) { if(d->map.tracks->td[i].track) { dt_view_map_remove_marker(darktable.view_manager, MAP_DISPLAY_TRACK, d->map.tracks->td[i].track); d->map.tracks->td[i].track = NULL; } } g_free(d->map.tracks); d->map.tracks = NULL; } if(d->map.gpx) { dt_gpx_destroy(d->map.gpx); d->map.gpx = NULL; } } GdkRGBA color[] = {(GdkRGBA){.red = 1.0, .green = 0.0, .blue = 0.0, .alpha = 0.5 }, (GdkRGBA){.red = 0.0, .green = 1.0, .blue = 1.0, .alpha = 0.5 }, (GdkRGBA){.red = 0.0, .green = 0.0, .blue = 1.0, .alpha = 0.5 }, (GdkRGBA){.red = 1.0, .green = 1.0, .blue = 0.0, .alpha = 0.5 }, (GdkRGBA){.red = 0.0, .green = 1.0, .blue = 0.0, .alpha = 0.5 }, (GdkRGBA){.red = 1.0, .green = 0.0, .blue = 1.0, .alpha = 0.5 }}; static gboolean _refresh_display_track(const gboolean active, const int segid, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; gboolean grow = FALSE; if(active) { GList *pts = dt_gpx_get_trkpts(d->map.gpx, segid); if(!d->map.tracks->td[segid].track) d->map.tracks->td[segid].track = dt_view_map_add_marker(darktable.view_manager, MAP_DISPLAY_TRACK, pts); osm_gps_map_track_set_color((OsmGpsMapTrack *)d->map.tracks->td[segid].track, &color[segid % 6]); grow = _update_map_box(segid, pts, self); g_list_free_full(pts, g_free); } else { if(d->map.tracks->td[segid].track != NULL) dt_view_map_remove_marker(darktable.view_manager, MAP_DISPLAY_TRACK, d->map.tracks->td[segid].track); d->map.tracks->td[segid].track = NULL; _update_map_box(segid, NULL, self); } return grow; } static void _refresh_display_all_tracks(GtkWidget *widget, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view)); GtkTreeIter iter; gboolean valid = gtk_tree_model_get_iter_first(model, &iter); gboolean grow = FALSE; for(int segid = 0; valid && segid < d->map.nb_tracks; segid++) { gboolean active; gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_ACTIVE, &active, -1); grow = _refresh_display_track(active, segid, self) ? TRUE : grow; valid = gtk_tree_model_iter_next(model, &iter); } if(grow) { dt_view_map_center_on_bbox(darktable.view_manager, d->map.map_box.lon1, d->map.map_box.lat1, d->map.map_box.lon2, d->map.map_box.lat2); } _refresh_displayed_images(self); } static void _click_for_entire_track_pressed(GtkGestureSingle *gesture, int n_press, double x, double y, dt_lib_module_t *self) { if(n_press == 2) { _refresh_display_all_tracks(NULL, self); } } static void _track_seg_toggled(GtkCellRendererToggle *cell_renderer, gchar *path_str, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view)); GtkTreeIter iter; GtkTreePath *path = gtk_tree_path_new_from_string(path_str); gboolean active; uint32_t segid; gtk_tree_model_get_iter(model, &iter, path); gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_ACTIVE, &active, DT_GEO_TRACKS_SEGID, &segid, -1); gtk_list_store_set(GTK_LIST_STORE(model), &iter, DT_GEO_TRACKS_ACTIVE, !active, -1); gtk_tree_path_free(path); active = !active; if(_refresh_display_track(active, segid, self)) { dt_view_map_center_on_bbox(darktable.view_manager, d->map.map_box.lon1, d->map.map_box.lat1, d->map.map_box.lon2, d->map.map_box.lat2); } const gboolean preview = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->map.preview_button)); _refresh_images_displayed_on_track(segid, active && preview, self); _update_nb_images(self); _update_buttons(self); } static void _all_tracks_toggled(GtkTreeViewColumn *column, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; GtkWidget *toggle = gtk_tree_view_column_get_widget(column); gboolean active = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), active); GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view)); GtkTreeIter iter; gboolean valid = gtk_tree_model_get_iter_first(model, &iter); gboolean grow = FALSE; for(int segid = 0; valid && segid < d->map.nb_tracks; segid++) { gtk_list_store_set(GTK_LIST_STORE(model), &iter, DT_GEO_TRACKS_ACTIVE, active,-1); grow = _refresh_display_track(active, segid, self) ? TRUE : grow; valid = gtk_tree_model_iter_next(model, &iter); } if(active && grow) { dt_view_map_center_on_bbox(darktable.view_manager, d->map.map_box.lon1, d->map.map_box.lat1, d->map.map_box.lon2, d->map.map_box.lat2); } _refresh_displayed_images(self); _update_nb_images(self); _update_buttons(self); } static void _select_images(GtkWidget *widget, dt_lib_module_t *self) { GList *imgs = _get_images_on_active_tracks(self); dt_selection_clear(darktable.selection); dt_selection_select_list(darktable.selection, imgs); g_list_free(imgs); } static void _images_preview_toggled(GtkToggleButton *button, dt_lib_module_t *self) { _refresh_displayed_images(self); } static void _refresh_track_list(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; if(!d->map.gpx) return; GList *trkseg = dt_gpx_get_trkseg(d->map.gpx); _remove_images_from_map(self); for(GList *i = d->imgs; i; i = g_list_next(i)) ((dt_sel_img_t *)i->data)->segid = -1; GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view)); GtkTreeIter iter; gboolean valid = gtk_tree_model_get_iter_first(model, &iter); for(GList *ts = trkseg; ts && valid; ts = g_list_next(ts)) { dt_gpx_track_segment_t *t = ts->data; gchar *dts = _utc_timeval_to_localtime_text(t->start_dt, d->tz_camera, TRUE); const int nb_imgs = _count_images_per_track(t, ts->next ? ts->next->data : NULL, self); gboolean active; gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_ACTIVE, &active, -1); gchar *tooltip = _datetime_tooltip(t->start_dt, t->end_dt, d->tz_camera); gtk_list_store_set(GTK_LIST_STORE(model), &iter, DT_GEO_TRACKS_DATETIME, dts, DT_GEO_TRACKS_POINTS, t->nb_trkpt, DT_GEO_TRACKS_IMAGES, nb_imgs, DT_GEO_TRACKS_TOOLTIP, tooltip, -1); g_free(dts); g_free(tooltip); valid = gtk_tree_model_iter_next(model, &iter); } _update_nb_images(self); _refresh_displayed_images(self); _update_buttons(self); } static void _show_gpx_tracks(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; _remove_tracks_from_map(self); d->map.gpx = dt_gpx_new(gtk_label_get_text(GTK_LABEL(d->map.gpx_file))); GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view)); g_object_ref(model); gtk_tree_view_set_model(GTK_TREE_VIEW(d->map.gpx_view), NULL); gtk_list_store_clear(GTK_LIST_STORE(model)); GList *trkseg = dt_gpx_get_trkseg(d->map.gpx); d->map.nb_tracks = g_list_length(trkseg); d->map.tracks = g_try_malloc0(sizeof(dt_lib_tracks_data_t) * d->map.nb_tracks); if(!d->map.tracks) { d->map.nb_tracks = 0; dt_print(DT_DEBUG_ALWAYS, "[geotagging] unable to allocate storage for tracks"); } _remove_images_from_map(self); for(GList *i = d->imgs; i; i = g_list_next(i)) ((dt_sel_img_t *)i->data)->segid = -1; int segid = 0; const gboolean active = gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON(gtk_tree_view_column_get_widget(d->map.sel_tracks))); for(GList *ts = trkseg; ts; ts = g_list_next(ts)) { dt_gpx_track_segment_t *t = ts->data; gchar *dts = _utc_timeval_to_localtime_text(t->start_dt, d->tz_camera, TRUE); gchar *tooltip = _datetime_tooltip(t->start_dt, t->end_dt, d->tz_camera); const int nb_imgs = _count_images_per_track(t, ts->next ? ts->next->data : NULL, self); gtk_list_store_insert_with_values(GTK_LIST_STORE(model), NULL, -1, DT_GEO_TRACKS_ACTIVE, active, DT_GEO_TRACKS_DATETIME, dts, DT_GEO_TRACKS_POINTS, t->nb_trkpt, DT_GEO_TRACKS_IMAGES, nb_imgs, DT_GEO_TRACKS_SEGID, segid, DT_GEO_TRACKS_TOOLTIP, tooltip, -1); segid++; g_free(dts); g_free(tooltip); } gtk_tree_view_set_model(GTK_TREE_VIEW(d->map.gpx_view), model); g_object_unref(model); gtk_tree_view_column_set_clickable(d->map.sel_tracks, TRUE); _update_nb_images(self); _update_buttons(self); _refresh_display_all_tracks(NULL, self); } static void _apply_gpx(GtkWidget *widget, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; gchar *tz = dt_conf_get_string("plugins/lighttable/geotagging/tz"); GList *imgs = _get_images_on_active_tracks(self); if(imgs) { if(d->offset) { GList *imgs2 = g_list_copy(imgs); dt_control_datetime(d->offset, NULL, imgs2); } dt_control_gpx_apply(gtk_label_get_text(GTK_LABEL(d->map.gpx_file)), -1, tz, imgs); } g_free(tz); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->map.preview_button), FALSE); } static void _update_layout(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; gtk_widget_set_visible(d->gpx_button, !d->map.view); gtk_widget_set_visible(d->map.gpx_section, d->map.view); } static void _view_changed(gpointer instance, dt_view_t *old_view, dt_view_t *new_view, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; if(old_view != new_view) { d->map.view = !g_strcmp0(new_view->module_name, "map"); if(d->map.view) { _setup_selected_images_list(self); _refresh_track_list(self); } _update_layout(self); } } static void _geotag_changed(gpointer instance, GList *imgs, const int locid, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; if(d->map.view && !locid) { _refresh_displayed_images(self); _update_nb_images(self); _update_buttons(self); } } static void _refresh_selected_images_datetime(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; for(GList *i = d->imgs; i; i = g_list_next(i)) { dt_sel_img_t *img = i->data; const dt_image_t *cimg = dt_image_cache_get(img->imgid, 'r'); if(!cimg) continue; dt_datetime_img_to_exif(img->dt, sizeof(img->dt), cimg); dt_image_cache_read_release(cimg); } } static gboolean _row_tooltip_setup(GtkWidget *view, gint x, gint y, gboolean kb_mode, GtkTooltip* tooltip, dt_lib_module_t *self) { gboolean res = FALSE; GtkTreePath *path = NULL; GtkTreeViewColumn *column = NULL; // Get view path mouse position gint tx, ty; gtk_tree_view_convert_widget_to_bin_window_coords(GTK_TREE_VIEW(view), x, y, &tx, &ty); if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view), tx, ty, &path, &column, NULL, NULL)) { GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(view)); GtkTreeIter iter; if(gtk_tree_model_get_iter(model, &iter, path)) { char *text = NULL; gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_TOOLTIP, &text, -1); if(text && text[0] != '\0') { gtk_tooltip_set_text(tooltip, text); res = TRUE; } g_free(text); } } gtk_tree_path_free(path); return res; } #endif // HAVE_MAP static void _preview_gpx_file(GtkWidget *widget, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; GtkWidget *win = dt_ui_main_window(darktable.gui->ui); GtkWidget *dialog = gtk_dialog_new_with_buttons(_("GPX file track segments"), GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT, _("_done"), GTK_RESPONSE_CANCEL, NULL); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL); gchar *filedir = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); struct dt_gpx_t *gpx = dt_gpx_new(filedir); g_free(filedir); GtkWidget *grid = gtk_grid_new(); gtk_grid_set_column_spacing(GTK_GRID(grid), DT_PIXEL_APPLY_DPI(10)); int line = 0; GList *trkseg = dt_gpx_get_trkseg(gpx); _set_up_label(_("name"), GTK_ALIGN_START, grid, 0, line, PANGO_ELLIPSIZE_NONE); _set_up_label(_("start time"), GTK_ALIGN_START, grid, 1, line, PANGO_ELLIPSIZE_NONE); _set_up_label(_("end time"), GTK_ALIGN_START, grid, 2, line, PANGO_ELLIPSIZE_NONE); _set_up_label(_("points"), GTK_ALIGN_CENTER, grid, 3, line, PANGO_ELLIPSIZE_NONE); _set_up_label(_("images"), GTK_ALIGN_CENTER, grid, 4, line, PANGO_ELLIPSIZE_NONE); for(GList *i = d->imgs; i; i = g_list_next(i)) ((dt_sel_img_t *)i->data)->segid = -1; int total_imgs = 0; int total_pts = 0; line++; for(GList *ts = trkseg; ts; ts = g_list_next(ts)) { dt_gpx_track_segment_t *t = ts->data; gchar *dts = _utc_timeval_to_localtime_text(t->start_dt, d->tz_camera, TRUE); gchar *dte = _utc_timeval_to_localtime_text(t->end_dt, d->tz_camera, TRUE); const int nb_imgs = _count_images_per_track(t, ts->next ? ts->next->data : NULL,self); total_imgs += nb_imgs; _set_up_label(t->name, GTK_ALIGN_START, grid, 0, line, PANGO_ELLIPSIZE_NONE); _set_up_label(dts, GTK_ALIGN_START, grid, 1, line, PANGO_ELLIPSIZE_NONE); _set_up_label(dte, GTK_ALIGN_START, grid, 2, line, PANGO_ELLIPSIZE_NONE); char *nb = g_strdup_printf("%u", t->nb_trkpt); _set_up_label(nb, GTK_ALIGN_CENTER, grid, 3, line, PANGO_ELLIPSIZE_NONE); g_free(nb); nb = g_strdup_printf("%d", nb_imgs); _set_up_label(nb, GTK_ALIGN_CENTER, grid, 4, line, PANGO_ELLIPSIZE_NONE); g_free(nb); line++; total_pts += t->nb_trkpt; g_free(dts); g_free(dte); } char *nb = g_strdup_printf("%d", total_pts); _set_up_label(nb, GTK_ALIGN_CENTER, grid, 3, line, PANGO_ELLIPSIZE_NONE); g_free(nb); nb = g_strdup_printf("%d / %d", total_imgs, d->nb_imgs); _set_up_label(nb, GTK_ALIGN_CENTER, grid, 4, line, PANGO_ELLIPSIZE_NONE); g_free(nb); if(gpx) { dt_gpx_destroy(gpx); gpx = NULL; } GtkWidget *w = dt_gui_scroll_wrap(grid); gtk_widget_set_size_request(w, -1, DT_PIXEL_APPLY_DPI(100)); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_NEVER, GTK_POLICY_NEVER); dt_gui_dialog_add(GTK_DIALOG(dialog), w); #ifdef GDK_WINDOWING_QUARTZ dt_osx_disallow_fullscreen(dialog); #endif gtk_widget_show_all(dialog); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); } static void _setup_selected_images_list(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; if(d->imgs) { #ifdef HAVE_MAP _remove_images_from_map(self); #endif g_list_free_full(d->imgs, g_free); } d->imgs = NULL; d->nb_imgs = 0; sqlite3_stmt *stmt; DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), "SELECT imgid FROM main.selected_images", -1, &stmt, NULL); while(sqlite3_step(stmt) == SQLITE_ROW) { const dt_imgid_t imgid = sqlite3_column_int(stmt, 0); const dt_image_t *cimg = dt_image_cache_get(imgid, 'r'); char dt[DT_DATETIME_LENGTH]; if(!cimg) continue; dt_datetime_img_to_exif(dt, sizeof(dt), cimg); dt_image_cache_read_release(cimg); dt_sel_img_t *img = g_try_malloc0(sizeof(dt_sel_img_t)); if(!img) continue; memcpy(img->dt, dt, DT_DATETIME_LENGTH); img->imgid = imgid; d->imgs = g_list_prepend(d->imgs, img); d->nb_imgs++; } sqlite3_finalize(stmt); } static void _choose_gpx_callback(GtkWidget *widget, dt_lib_module_t *self) { // bring a filechooser to select the gpx file to apply to selection dt_lib_geotagging_t *d = self->data; GtkWidget *win = dt_ui_main_window(darktable.gui->ui); GtkWidget *filechooser = gtk_file_chooser_dialog_new( _("open GPX file"), GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_OPEN, _("preview"), GTK_RESPONSE_ACCEPT, _("_cancel"), GTK_RESPONSE_CANCEL, _("_open"), GTK_RESPONSE_OK, (char *)NULL); #ifdef GDK_WINDOWING_QUARTZ dt_osx_disallow_fullscreen(filechooser); #endif dt_conf_get_folder_to_file_chooser("ui_last/gpx_last_directory", GTK_FILE_CHOOSER(filechooser)); GtkFileFilter *filter; filter = GTK_FILE_FILTER(gtk_file_filter_new()); gtk_file_filter_add_custom(filter, GTK_FILE_FILTER_FILENAME | GTK_FILE_FILTER_MIME_TYPE, _lib_geotagging_filter_gpx, NULL, NULL); gtk_file_filter_set_name(filter, _("GPS data exchange format")); gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(filechooser), filter); filter = GTK_FILE_FILTER(gtk_file_filter_new()); gtk_file_filter_add_pattern(filter, "*"); gtk_file_filter_set_name(filter, _("all files")); gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(filechooser), filter); if(!d->imgs) _setup_selected_images_list(self); int res = gtk_dialog_run(GTK_DIALOG(filechooser)); while(res == GTK_RESPONSE_ACCEPT) { _preview_gpx_file(filechooser, self); res = gtk_dialog_run(GTK_DIALOG(filechooser)); } if(res == GTK_RESPONSE_OK) { dt_conf_set_folder_from_file_chooser("ui_last/gpx_last_directory", GTK_FILE_CHOOSER(filechooser)); gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser)); #ifdef HAVE_MAP if(d->map.view) { gtk_label_set_text(GTK_LABEL(d->map.gpx_file), filename); _show_gpx_tracks(self); gtk_widget_set_visible(d->map.gpx_view, d->map.view); } else #endif { gchar *tz = dt_conf_get_string("plugins/lighttable/geotagging/tz"); dt_control_gpx_apply(filename, -1, tz, NULL); g_free(tz); g_list_free_full(d->imgs, g_free); d->imgs = NULL; d->nb_imgs = 0; } g_free(filename); } gtk_widget_destroy(filechooser); // dt_control_queue_redraw_center(); } static int _sort_timezones(gconstpointer a, gconstpointer b) { const tz_tuple_t *tz_a = (tz_tuple_t *)a; const tz_tuple_t *tz_b = (tz_tuple_t *)b; #ifdef _WIN32 gboolean utc_neg_a = g_str_has_prefix(tz_a->display, "(UTC-"); gboolean utc_neg_b = g_str_has_prefix(tz_b->display, "(UTC-"); gboolean utc_pos_a = g_str_has_prefix(tz_a->display, "(UTC+"); gboolean utc_pos_b = g_str_has_prefix(tz_b->display, "(UTC+"); if(utc_neg_a && utc_neg_b) { char *iter_a = tz_a->display + strlen("(UTC-"); char *iter_b = tz_b->display + strlen("(UTC-"); while(((*iter_a >= '0' && *iter_a <= '9') || *iter_a == ':') && ((*iter_b >= '0' && *iter_b <= '9') || *iter_b == ':')) { if(*iter_a != *iter_b) return *iter_b - *iter_a; iter_a++; iter_b++; } } else if(utc_neg_a && utc_pos_b) return -1; else if(utc_pos_a && utc_neg_b) return 1; #endif return g_strcmp0(tz_a->display, tz_b->display); } // create a list of possible time zones static GList *_lib_geotagging_get_timezones(void) { GList *timezones = NULL; #ifndef _WIN32 // possible locations for zone.tab: // - /usr/share/zoneinfo // - /usr/lib/zoneinfo // - getenv("TZDIR") // - apparently on solaris there is no zones.tab. we need to collect the information ourselves like this: // /bin/grep -h ^Zone /usr/share/lib/zoneinfo/src/* | /bin/awk '{print "??\t+9999+99999\t" $2}' #define MAX_LINE_LENGTH 256 FILE *fp; char line[MAX_LINE_LENGTH]; // find the file using known possible locations gchar *zone_tab = g_strdup("/usr/share/zoneinfo/zone.tab"); if(!g_file_test(zone_tab, G_FILE_TEST_IS_REGULAR)) { g_free(zone_tab); zone_tab = g_strdup("/usr/lib/zoneinfo/zone.tab"); if(!g_file_test(zone_tab, G_FILE_TEST_IS_REGULAR)) { g_free(zone_tab); zone_tab = g_build_filename(g_getenv("TZDIR"), "zone.tab", NULL); if(!g_file_test(zone_tab, G_FILE_TEST_IS_REGULAR)) { g_free(zone_tab); char datadir[PATH_MAX] = { 0 }; dt_loc_get_datadir(datadir, sizeof(datadir)); zone_tab = g_build_filename(datadir, "zone.tab", NULL); if(!g_file_test(zone_tab, G_FILE_TEST_IS_REGULAR)) { g_free(zone_tab); // TODO: Solaris test return NULL; } } } } // parse zone.tab and put all time zone descriptions into timezones fp = g_fopen(zone_tab, "r"); g_free(zone_tab); if(!fp) return NULL; while(fgets(line, MAX_LINE_LENGTH, fp)) { if(line[0] == '#' || line[0] == '\0') continue; gchar **tokens = g_strsplit_set(line, " \t\n", 0); // sometimes files are not separated by single tabs but multiple spaces, resulting in empty strings in tokens // so we have to look for the 3rd non-empty entry int n_found = -1, i; for(i = 0; tokens[i] && n_found < 2; i++) if(*tokens[i]) n_found++; if(n_found != 2) { g_strfreev(tokens); continue; } gchar *name = g_strdup(tokens[i - 1]); g_strfreev(tokens); if(name[0] == '\0') { g_free(name); continue; } size_t last_char = strlen(name) - 1; if(name[last_char] == '\n') name[last_char] = '\0'; tz_tuple_t *tz_tuple = malloc(sizeof(tz_tuple_t)); if(tz_tuple) { tz_tuple->display = name; tz_tuple->name = name; timezones = g_list_prepend(timezones, tz_tuple); } } fclose(fp); // sort timezones timezones = g_list_sort(timezones, _sort_timezones); tz_tuple_t *utc = malloc(sizeof(tz_tuple_t)); if(utc) { utc->display = g_strdup("UTC"); utc->name = utc->display; timezones = g_list_prepend(timezones, utc); } #undef MAX_LINE_LENGTH #else // !_WIN32 // on Windows we have to grab the time zones from the registry char *keypath = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\"; HKEY hKey; if(RegOpenKeyExA(HKEY_LOCAL_MACHINE, keypath, 0, KEY_READ, &hKey) == ERROR_SUCCESS) { DWORD n_subkeys, max_subkey_len; if(RegQueryInfoKey(hKey, NULL, NULL, NULL, &n_subkeys, &max_subkey_len, NULL, NULL, NULL, NULL, NULL, NULL) == ERROR_SUCCESS) { wchar_t *subkeyname = malloc(sizeof(wchar_t) * (max_subkey_len + 1)); for(DWORD i = 1; i < n_subkeys; i++) { DWORD subkeyname_length = max_subkey_len + 1; if(RegEnumKeyExW(hKey, i, subkeyname, &subkeyname_length, NULL, NULL, NULL, NULL) == ERROR_SUCCESS) { DWORD buffer_size; char *subkeyname_utf8 = g_utf16_to_utf8(subkeyname, -1, NULL, NULL, NULL); char *subkeypath_utf8 = g_strconcat(keypath, "\\", subkeyname_utf8, NULL); wchar_t *subkeypath = g_utf8_to_utf16(subkeypath_utf8, -1, NULL, NULL, NULL); if(RegGetValueW(HKEY_LOCAL_MACHINE, subkeypath, L"Display", RRF_RT_ANY, NULL, NULL, &buffer_size) == ERROR_SUCCESS) { wchar_t *display_name = malloc(buffer_size); if(RegGetValueW(HKEY_LOCAL_MACHINE, subkeypath, L"Display", RRF_RT_ANY, NULL, display_name, &buffer_size) == ERROR_SUCCESS) { tz_tuple_t *tz = malloc(sizeof(tz_tuple_t)); if(tz) { tz->name = subkeyname_utf8; tz->display = g_utf16_to_utf8(display_name, -1, NULL, NULL, NULL); timezones = g_list_prepend(timezones, tz); } subkeyname_utf8 = NULL; // to not free it later } free(display_name); } g_free(subkeyname_utf8); g_free(subkeypath_utf8); g_free(subkeypath); } } free(subkeyname); } } RegCloseKey(hKey); timezones = g_list_sort(timezones, _sort_timezones); #endif // !_WIN32 return timezones; } static void _display_offset(const GTimeSpan offset_int, const gboolean valid, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; GTimeSpan off2 = 0; if(valid) { const gboolean neg = offset_int < 0; gtk_label_set_text(GTK_LABEL(d->of.sign), neg ? "- " : ""); char text[5]; GTimeSpan off = neg ? -offset_int : offset_int; off2 = off / 1000; // skip microseconds off = off2; off2 = off / 1000; snprintf(text, sizeof(text), "%03d", (int)(off - off2 * 1000)); gtk_entry_set_text(GTK_ENTRY(d->of.widget[6]), text); off = off2; off2 = off / 60; snprintf(text, sizeof(text), "%02d", (int)(off - off2 * 60)); gtk_entry_set_text(GTK_ENTRY(d->of.widget[5]), text); off = off2; off2 = off / 60; snprintf(text, sizeof(text), "%02d", (int)(off - off2 * 60)); gtk_entry_set_text(GTK_ENTRY(d->of.widget[4]), text); off = off2; off2 = off / 24; snprintf(text, sizeof(text), "%02d", (int)(off - off2 * 24)); gtk_entry_set_text(GTK_ENTRY(d->of.widget[3]), text); off = off2; off2 = off / 10000; snprintf(text, sizeof(text), "%04d", (int)(off - off2 * 10000)); gtk_entry_set_text(GTK_ENTRY(d->of.widget[2]), text); } if(!valid || off2) { gtk_label_set_text(GTK_LABEL(d->of.sign), ""); for(int i = 2; i < DT_GEOTAG_PARTS_NB; i++) gtk_entry_set_text(GTK_ENTRY(d->of.widget[i]), "-"); } const gboolean locked = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->lock_offset)); gtk_widget_set_sensitive(d->apply_offset, d->imgid && valid && !off2 && offset_int); gtk_widget_set_sensitive(d->lock_offset, locked || (d->imgid && valid && !off2 && offset_int)); gtk_widget_set_sensitive(d->apply_datetime, d->imgid && !locked); #ifdef HAVE_MAP _update_buttons(self); #endif } static void _display_datetime(dt_lib_datetime_t *dtw, GDateTime *datetime, const gboolean lock, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; for(int i = 0; lock && i < DT_GEOTAG_PARTS_NB; i++) g_signal_handlers_block_by_func(d->dt.widget[i], _datetime_entry_changed, self); if(datetime) { char value[8] = {0}; snprintf(value, sizeof(value), "%04d", g_date_time_get_year(datetime)); gtk_entry_set_text(GTK_ENTRY(dtw->widget[0]), value); snprintf(value, sizeof(value), "%02d", g_date_time_get_month(datetime)); gtk_entry_set_text(GTK_ENTRY(dtw->widget[1]), value); snprintf(value, sizeof(value), "%02d", g_date_time_get_day_of_month(datetime)); gtk_entry_set_text(GTK_ENTRY(dtw->widget[2]), value); snprintf(value, sizeof(value), "%02d", g_date_time_get_hour(datetime)); gtk_entry_set_text(GTK_ENTRY(dtw->widget[3]), value); snprintf(value, sizeof(value), "%02d", g_date_time_get_minute(datetime)); gtk_entry_set_text(GTK_ENTRY(dtw->widget[4]),value); snprintf(value, sizeof(value), "%02d", g_date_time_get_second(datetime)); gtk_entry_set_text(GTK_ENTRY(dtw->widget[5]), value); snprintf(value, sizeof(value), "%03d", (int)(g_date_time_get_microsecond(datetime) * 0.001)); gtk_entry_set_text(GTK_ENTRY(dtw->widget[6]), value); } else { for(int i = 0; i < DT_GEOTAG_PARTS_NB; i++) gtk_entry_set_text(GTK_ENTRY(dtw->widget[i]), "-"); } for(int i = 0; lock && i < DT_GEOTAG_PARTS_NB; i++) g_signal_handlers_unblock_by_func(d->dt.widget[i], _datetime_entry_changed, self); } // read the current date/time and make correction (field under/overflow) static GDateTime *_read_datetime_entry(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; const int year = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[0]))); const int month = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[1]))); const int day = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[2]))); const int hour = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[3]))); const int minute = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[4]))); const int second = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[5]))); const int millisecond = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[6]))); const gdouble second2 = (gdouble)second + (gdouble)millisecond * 0.001; return g_date_time_new(darktable.utc_tz, year, month, day, hour, minute, second2); } static void _new_datetime(GDateTime *datetime, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; if(datetime) { _display_datetime(&d->dt, datetime, TRUE, self); if(d->datetime) g_date_time_unref(d->datetime); d->datetime = datetime; d->offset = g_date_time_difference(d->datetime, d->datetime0); _display_offset(d->offset, d->datetime != NULL, self); #ifdef HAVE_MAP if(d->map.view) _refresh_track_list(self); #endif } } static void _datetime_entry_changed(GtkWidget *entry, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; if(!d->editing) { GDateTime *datetime = _read_datetime_entry(self); _new_datetime(datetime, self); } } static GDateTime *_get_image_datetime(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; GList *selected = dt_collection_get_selected(darktable.collection, 1); const int selid = selected ? GPOINTER_TO_INT(selected->data) : 0; const dt_imgid_t imgid = dt_act_on_get_main_image(); GDateTime *datetime = NULL; if((selid != 0) || ((selid == 0) && (dt_is_valid_imgid(imgid)))) { // consider act on only if no selected char datetime_s[DT_DATETIME_LENGTH]; dt_image_get_datetime(selid ? selid : imgid, datetime_s); if(datetime_s[0] != '\0') datetime = dt_datetime_exif_to_gdatetime(datetime_s, darktable.utc_tz); else datetime = NULL; } d->imgid = selid; return datetime; } static void _refresh_image_datetime(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; const gboolean locked = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->lock_offset)); GDateTime *datetime = _get_image_datetime(self); if(d->datetime0) g_date_time_unref(d->datetime0); d->datetime0 = datetime; _display_datetime(&d->dt0, datetime, FALSE, self); if(locked) { if(datetime) { GDateTime *datetime2 = g_date_time_add(datetime, d->offset); _new_datetime(datetime2, self); } } else { _display_offset(d->offset = 0, datetime != NULL, self); if(datetime) { g_date_time_ref(datetime); _new_datetime(datetime, self); } } } static void _image_info_changed(gpointer instance, gpointer imgs, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; for(GList *i = imgs; i; i = g_list_next(i)) { if(GPOINTER_TO_INT(i->data) == d->imgid) { _refresh_image_datetime(self); break; } } #ifdef HAVE_MAP if(d->map.view) { _refresh_selected_images_datetime(self); _refresh_track_list(self); } #endif } static void _mouse_over_image_callback(gpointer instance, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; if(!d->imgid) _refresh_image_datetime(self); } static void _selection_changed_callback(gpointer instance, dt_lib_module_t *self) { _refresh_image_datetime(self); #ifdef HAVE_MAP dt_lib_geotagging_t *d = self->data; if(d->map.view) { _setup_selected_images_list(self); _refresh_track_list(self); } #endif } static void _datetime_scroll_over(GtkEventControllerScroll *controller, double dx, double dy, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; if(!d->editing) { GtkWidget* w = dt_gui_get_widget(controller); int i = 0; for(i = 0; i < DT_GEOTAG_PARTS_NB; i++) if(w == d->dt.widget[i]) break; int increment = CLAMPF(-dy, -1, 1); if(dt_modifier_eq(controller, GDK_SHIFT_MASK)) increment *= 10; GDateTime *datetime; switch(i) { case 0: datetime = g_date_time_add_years(d->datetime, increment); break; case 1: datetime = g_date_time_add_months(d->datetime, increment); break; case 2: datetime = g_date_time_add_days(d->datetime, increment); break; case 3: datetime = g_date_time_add_hours(d->datetime, increment); break; case 4: datetime = g_date_time_add_minutes(d->datetime, increment); break; case 5: datetime = g_date_time_add_seconds(d->datetime, increment); break; case 6: datetime = g_date_time_add(d->datetime, increment * 1000); break; default: datetime = NULL; } _new_datetime(datetime, self); } } // type 0 date/time, 1 original date/time, 2 offset static GtkWidget *_gui_init_datetime(gchar *text, dt_lib_datetime_t *dt, const int type, dt_lib_module_t *self, GtkSizeGroup *group, GtkWidget *button, gchar *tip) { GtkWidget *flow = gtk_flow_box_new(); gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(flow), 2); GtkWidget *t = dt_ui_label_new(text); gtk_size_group_add_widget(group, t); gtk_container_add(GTK_CONTAINER(flow), t); gtk_widget_set_tooltip_text(flow, tip); GtkWidget *flow2 = gtk_flow_box_new(); gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(flow2), TRUE); // otherwise weird behavior gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(flow2), 2); gtk_container_add(GTK_CONTAINER(flow), flow2); GtkBox *box = NULL; for(int i = 0; i < DT_GEOTAG_PARTS_NB; i++) { if(!box) box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0)); if(i == 0 && type == 2) { gtk_box_set_homogeneous(box, TRUE); // creates some spacing gtk_box_pack_start(box, button, TRUE, TRUE, 0); dt->sign = gtk_label_new(""); gtk_box_pack_start(box, dt->sign, FALSE, FALSE, 0); } if(i >= 2 || type != 2) { // 1st field of dates (offset line is excluded by "if" above) is year, so 4 digits dt->widget[i] = dt_ui_entry_new(i == 0 ? 4 // 7th field (optional, may be hidden) is msec, so 3 digits : i == 6 ? 3 // 2nd widget of offset line should accommodate offsets up to 9999 days, so 4 digits // all other fields for 2 digit numbers : ((type == 2) && (i == 2)) ? 4 : 2); gtk_entry_set_alignment(GTK_ENTRY(dt->widget[i]), 0.5); gtk_box_pack_start(box, dt->widget[i], FALSE, FALSE, 0); if(type == 0) { dt_action_define(DT_ACTION(self), NULL, i <= 2 ? N_("date") : N_("time"), dt->widget[i], &dt_action_def_entry); gtk_widget_add_events(dt->widget[i], darktable.gui->scroll_mask); } else { gtk_widget_set_sensitive(dt->widget[i], FALSE); } } if(i == 2 || i == 6) { gtk_widget_set_halign(GTK_WIDGET(box), GTK_ALIGN_END); gtk_widget_set_hexpand(GTK_WIDGET(box), TRUE); gtk_container_add(GTK_CONTAINER(flow2), GTK_WIDGET(box)); box = NULL; } else if(i > 2 || type != 2) { GtkWidget *label = gtk_label_new(i < 2 ? "-" : i == 5 ? "." :":"); if(i == 5) g_object_set_data(G_OBJECT(dt->widget[i]), "msec_label", label); gtk_box_pack_start(box, label, FALSE, FALSE, 0); } } gtk_container_foreach(GTK_CONTAINER(flow2), (GtkCallback)gtk_widget_set_can_focus, GINT_TO_POINTER(FALSE)); return flow; } static gboolean _datetime_key_pressed(GtkEventControllerKey *controller, guint keyval, guint keycode, GdkModifierType state, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; switch(keyval) { case GDK_KEY_Escape: { // reset _refresh_image_datetime(self); #ifdef HAVE_MAP if(d->map.view) _refresh_track_list(self); #endif gtk_window_set_focus(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)), NULL); d->editing = FALSE; return FALSE; } // allow 0 .. 9, left/right/home/end movement using arrow keys and del/backspace case GDK_KEY_0: case GDK_KEY_KP_0: case GDK_KEY_1: case GDK_KEY_KP_1: case GDK_KEY_2: case GDK_KEY_KP_2: case GDK_KEY_3: case GDK_KEY_KP_3: case GDK_KEY_4: case GDK_KEY_KP_4: case GDK_KEY_5: case GDK_KEY_KP_5: case GDK_KEY_6: case GDK_KEY_KP_6: case GDK_KEY_7: case GDK_KEY_KP_7: case GDK_KEY_8: case GDK_KEY_KP_8: case GDK_KEY_9: case GDK_KEY_KP_9: case GDK_KEY_Delete: case GDK_KEY_KP_Delete: case GDK_KEY_BackSpace: case GDK_KEY_Left: case GDK_KEY_Right: case GDK_KEY_Home: case GDK_KEY_KP_Home: case GDK_KEY_End: case GDK_KEY_KP_End: d->editing = TRUE; return FALSE; case GDK_KEY_Tab: case GDK_KEY_KP_Tab: case GDK_KEY_ISO_Left_Tab: case GDK_KEY_Return: case GDK_KEY_KP_Enter: d->editing = FALSE; g_signal_emit_by_name(d->dt.widget[0], "changed"); return FALSE; default: return TRUE; } } static void _timezone_save(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; const gchar *tz = gtk_entry_get_text(GTK_ENTRY(d->timezone)); gchar *name = NULL; for(GList *iter = d->timezones; iter; iter = g_list_next(iter)) { tz_tuple_t *tz_tuple = (tz_tuple_t *)iter->data; if(!strcmp(tz_tuple->display, tz)) name = tz_tuple->name; } if(d->tz_camera) g_time_zone_unref(d->tz_camera); d->tz_camera = !name ? g_time_zone_new_utc() : g_time_zone_new(name); dt_conf_set_string("plugins/lighttable/geotagging/tz", name ? name : "UTC"); gtk_entry_set_text(GTK_ENTRY(d->timezone), name ? name : "UTC"); gtk_label_set_text(GTK_LABEL (d->timezone_changed), ""); gtk_window_set_focus(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)), NULL); #ifdef HAVE_MAP if(d->map.view) _refresh_track_list(self); #endif } static gboolean _timezone_key_pressed(GtkEventControllerKey *controller, guint keyval, guint keycode, GdkModifierType state, dt_lib_module_t *self) { switch(keyval) { case GDK_KEY_Return: case GDK_KEY_KP_Enter: case GDK_KEY_Tab: _timezone_save(self); return TRUE; case GDK_KEY_Escape: gtk_window_set_focus(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)), NULL); return TRUE; default: ; dt_lib_geotagging_t *d = self->data; gtk_label_set_text(GTK_LABEL (d->timezone_changed), " *"); break; } return FALSE; } static void _timezone_focus_out(GtkEventController *controller, dt_lib_module_t *self) { _timezone_save(self); } static gboolean _completion_match_func(GtkEntryCompletion *completion, const gchar *key, GtkTreeIter *iter, gpointer user_data) { gboolean res = FALSE; GtkEditable *e = (GtkEditable *)gtk_entry_completion_get_entry(completion); if(!GTK_IS_EDITABLE(e)) return FALSE; GtkTreeModel *model = gtk_entry_completion_get_model(completion); const int column = gtk_entry_completion_get_text_column(completion); if(gtk_tree_model_get_column_type(model, column) != G_TYPE_STRING) return FALSE; char *tag = NULL; gtk_tree_model_get(model, iter, column, &tag, -1); if(tag) { char *normalized = g_utf8_normalize(tag, -1, G_NORMALIZE_ALL); if(normalized) { char *casefold = g_utf8_casefold(normalized, -1); if(casefold) { res = g_strstr_len(casefold, -1, key) != NULL; } g_free(casefold); } g_free(normalized); g_free(tag); } return res; } static void _toggle_lock_button_callback(GtkToggleButton *button, dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; const gboolean locked = gtk_toggle_button_get_active(button); for(int i = 0; i < DT_GEOTAG_PARTS_NB; i++) { gtk_widget_set_sensitive(d->dt.widget[i], !locked); } gtk_widget_set_sensitive(d->apply_datetime, d->imgid && !locked); } GtkTreeViewColumn *_new_tree_text_column(const char *name, const gboolean expand, const float xalign, const int m_col, const int ellipsize) { GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); GtkTreeViewColumn *column = gtk_tree_view_column_new(); gtk_tree_view_column_pack_start(column, renderer, TRUE); gtk_tree_view_column_set_attributes(column, renderer, "text", m_col, NULL); gtk_tree_view_column_set_expand(column, expand); GtkWidget *label = gtk_label_new(name); gtk_widget_show(label); gtk_tree_view_column_set_widget(column, label); gtk_label_set_ellipsize(GTK_LABEL(label), expand ? PANGO_ELLIPSIZE_MIDDLE : PANGO_ELLIPSIZE_NONE); g_object_set(renderer, "xalign", xalign, "ellipsize", ellipsize, NULL); return column; } static void _show_milliseconds(dt_lib_geotagging_t *d) { const gboolean milliseconds = dt_conf_get_bool("lighttable/ui/milliseconds"); gtk_widget_set_visible(d->dt.widget[6], milliseconds); gtk_widget_set_visible(d->dt0.widget[6], milliseconds); gtk_widget_set_visible(d->of.widget[6], milliseconds); gtk_widget_set_visible(g_object_get_data(G_OBJECT(d->dt.widget[5]), "msec_label"), milliseconds); gtk_widget_set_visible(g_object_get_data(G_OBJECT(d->dt0.widget[5]), "msec_label"), milliseconds); gtk_widget_set_visible(g_object_get_data(G_OBJECT(d->of.widget[5]), "msec_label"), milliseconds); } static void _dt_pref_change_callback(gpointer instance, dt_lib_module_t *self) { _show_milliseconds(self->data); } void gui_reset(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->lock_offset), FALSE); _refresh_image_datetime(self); #ifdef HAVE_MAP if(d->map.view) { gtk_label_set_text(GTK_LABEL(d->map.gpx_file), ""); _remove_tracks_from_map(self); gtk_widget_set_visible(d->map.gpx_view, FALSE); } #endif } void gui_init(dt_lib_module_t *self) { dt_lib_geotagging_t *d = g_malloc0(sizeof(dt_lib_geotagging_t)); self->data = (void *)d; d->timezones = _lib_geotagging_get_timezones(); self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); GtkGrid *grid = GTK_GRID(gtk_grid_new()); gtk_grid_set_column_spacing(grid, DT_PIXEL_APPLY_DPI(5)); int line = 0; GtkSizeGroup *group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); GtkWidget *box = _gui_init_datetime(_("date/time"), &d->dt, 0, self, group, NULL, _("enter the new date/time (YYYY:MM:DD hh:mm:ss[.sss])" "\nkey in the new numbers or scroll over the cell")); gtk_grid_attach(grid, box, 0, line++, 4, 1); box = _gui_init_datetime(_("original date/time"), &d->dt0, 1, self, group, NULL, NULL); gtk_grid_attach(grid, box, 0, line++, 4, 1); d->lock_offset = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_lock, 0, NULL, &(dtgtk_button_config_t){ .tooltip = _("lock date/time offset value to apply it onto another selection"), .clicked_cb = G_CALLBACK(_toggle_lock_button_callback), .clicked_data = (gpointer)self, }); gtk_widget_set_halign(d->lock_offset, GTK_ALIGN_START); box = _gui_init_datetime(_("date/time offset"), &d->of, 2, self, group, d->lock_offset, _("offset or difference ([-]dd hh:mm:ss[.sss])")); gtk_grid_attach(grid, box, 0, line++, 4, 1); // apply d->apply_offset = dt_action_button_new(self, N_("apply offset"), _apply_offset_callback, self, _("apply offset to selected images"), 0, 0); gtk_grid_attach(grid, d->apply_offset , 0, line, 2, 1); d->apply_datetime = dt_action_button_new(self, N_("apply date/time"), _apply_datetime_callback, self, _("apply the same date/time to selected images"), 0, 0); gtk_grid_attach(grid, d->apply_datetime , 2, line++, 2, 1); // time zone entry GtkWidget *label = dt_ui_label_new(_(dt_confgen_get_label("plugins/lighttable/geotagging/tz"))); gtk_widget_set_tooltip_markup(label, _(dt_confgen_get_tooltip("plugins/lighttable/geotagging/tz"))); gtk_grid_attach(grid, label, 0, line, 2, 1); d->timezone = dt_ui_entry_new(0); gtk_widget_set_tooltip_text(d->timezone, _("start typing to show a list of permitted values and select your timezone.\n" "press enter to confirm, so that the asterisk * disappears")); d->timezone_changed = dt_ui_label_new(""); GtkWidget *timezone_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); gtk_box_pack_start(GTK_BOX(timezone_box), d->timezone, TRUE, TRUE, 0); gtk_box_pack_end(GTK_BOX(timezone_box), d->timezone_changed, FALSE, FALSE, 0); gtk_grid_attach(grid, timezone_box, 2, line++, 2, 1); GtkCellRenderer *renderer; GtkListStore *model = gtk_list_store_new(2, G_TYPE_STRING /*display*/, G_TYPE_STRING /*name*/); GtkWidget *tz_selection = gtk_combo_box_new_with_model(GTK_TREE_MODEL(model)); renderer = gtk_cell_renderer_text_new(); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(tz_selection), renderer, FALSE); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(tz_selection), renderer, "text", 0, NULL); gchar *tz = dt_conf_get_string("plugins/lighttable/geotagging/tz"); d->tz_camera = (tz == NULL) ? g_time_zone_new_utc() : g_time_zone_new(tz); for(GList *iter = d->timezones; iter; iter = g_list_next(iter)) { tz_tuple_t *tz_tuple = (tz_tuple_t *)iter->data; gtk_list_store_insert_with_values(model, NULL, -1, 0, tz_tuple->display, 1, tz_tuple->name, -1); if(!strcmp(tz_tuple->name, tz)) gtk_entry_set_text(GTK_ENTRY(d->timezone), tz_tuple->display); } g_free(tz); // add entry completion GtkEntryCompletion *completion = gtk_entry_completion_new(); gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(model)); gtk_entry_completion_set_text_column(completion, 0); gtk_entry_completion_set_inline_completion(completion, TRUE); gtk_entry_completion_set_popup_set_width(completion, FALSE); gtk_entry_completion_set_match_func(completion, _completion_match_func, NULL, NULL); gtk_entry_completion_set_minimum_key_length(completion, 0); gtk_entry_set_completion(GTK_ENTRY(d->timezone), completion); dt_gui_connect_key(d->timezone, _timezone_key_pressed, self); dt_gui_connect_focus(d->timezone, NULL, _timezone_focus_out, self); // gpx d->gpx_button = dt_action_button_new(self, N_("apply GPX track file..."), _choose_gpx_callback, self, _("parses a GPX file and updates location of selected images"), 0, 0); gtk_grid_attach(grid, d->gpx_button, 0, line++, 4, 1); gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(grid), TRUE, TRUE, 0); #ifdef HAVE_MAP grid = GTK_GRID(gtk_grid_new()); gtk_grid_set_column_spacing(grid, DT_PIXEL_APPLY_DPI(5)); line = 0; d->map.gpx_section = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(d->map.gpx_section), TRUE, TRUE, 0); label = dt_ui_section_label_new(C_("section", "GPX file")); gtk_grid_attach(grid, label, 0, line++, 4, 1); d->map.gpx_button = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL, &(dtgtk_button_config_t){ .tooltip = _("select a GPX track file..."), .clicked_cb = G_CALLBACK(_choose_gpx_callback), .clicked_data = self, }); gtk_widget_set_hexpand(d->map.gpx_button, FALSE); gtk_widget_set_halign(d->map.gpx_button, GTK_ALIGN_START); gtk_widget_set_name(d->map.gpx_button, "non-flat"); gtk_grid_attach(grid, d->map.gpx_button, 0, line, 1, 1); d->map.gpx_file = dt_ui_label_new(""); gtk_label_set_ellipsize(GTK_LABEL(d->map.gpx_file ), PANGO_ELLIPSIZE_MIDDLE); gtk_widget_set_hexpand(d->map.gpx_file, TRUE); gtk_grid_attach(grid, d->map.gpx_file, 1, line++, 3, 1); gtk_box_pack_start(GTK_BOX(d->map.gpx_section), GTK_WIDGET(grid), TRUE, TRUE, 0); model = gtk_list_store_new(DT_GEO_TRACKS_NUM_COLS, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING); d->map.gpx_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(model)); g_object_unref(model); gtk_widget_set_name(d->map.gpx_view, "gpx_list"); gtk_widget_set_tooltip_text(GTK_WIDGET(d->map.gpx_view), _("list of track segments in the GPX file, for each segment:" "\n- the start date/time in local time (LT)" "\n- the number of track points" "\n- the number of matching images" " based on images date/time, offset and time zone" "\n- more detailed time information hovering the row")); renderer = gtk_cell_renderer_toggle_new(); g_signal_connect(renderer, "toggled", G_CALLBACK(_track_seg_toggled), self); GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("", renderer, "active", DT_GEO_TRACKS_ACTIVE, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(d->map.gpx_view), column); d->map.sel_tracks = column; GtkWidget *button = gtk_check_button_new(); gtk_widget_show(button); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), FALSE); gtk_tree_view_column_set_widget(column, button); gtk_tree_view_column_set_alignment(column, 0.5); g_signal_connect(column, "clicked", G_CALLBACK(_all_tracks_toggled), self); column = _new_tree_text_column(_("start time"), TRUE, 0.0, DT_GEO_TRACKS_DATETIME, PANGO_ELLIPSIZE_START); gtk_tree_view_append_column(GTK_TREE_VIEW(d->map.gpx_view), column); column = _new_tree_text_column(_("points"), FALSE, 1.0, DT_GEO_TRACKS_POINTS, PANGO_ELLIPSIZE_NONE); gtk_tree_view_append_column(GTK_TREE_VIEW(d->map.gpx_view), column); column = _new_tree_text_column(_("images"), FALSE, 1.0, DT_GEO_TRACKS_IMAGES, PANGO_ELLIPSIZE_NONE); gtk_tree_view_append_column(GTK_TREE_VIEW(d->map.gpx_view), column); g_object_set(G_OBJECT(d->map.gpx_view), "has-tooltip", TRUE, NULL); g_signal_connect(G_OBJECT(d->map.gpx_view), "query-tooltip", G_CALLBACK(_row_tooltip_setup), self); dt_gui_connect_click(d->map.gpx_view, _click_for_entire_track_pressed, NULL, self); // avoid ugly console pixman messages due to headers gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(d->map.gpx_view), FALSE); GtkWidget *w = dt_ui_resize_wrap(GTK_WIDGET(d->map.gpx_view), 100, "plugins/lighttable/geotagging/heighttracklist"); gtk_widget_set_size_request(w, -1, 100); gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(d->map.gpx_view), TRUE); // the gpx_view_button is invisible and the label and tooltip of cannot be displayed on GUI. // but label is use in "shortcuts" dialog. GtkWidget *gpx_view_button = dt_action_button_new(self, N_("view entire track"), _refresh_display_all_tracks, self, _("refresh map to view entire selected track segments"), 0, 0); gtk_widget_set_opacity(gpx_view_button, 0); gtk_widget_set_valign(gpx_view_button, GTK_ALIGN_START); gtk_widget_set_margin_start(gpx_view_button, DT_PIXEL_APPLY_DPI(25)); gtk_widget_set_size_request(gpx_view_button, -1, DT_PIXEL_APPLY_DPI(25)); GtkWidget *overlay = gtk_overlay_new(); gtk_container_add(GTK_CONTAINER(overlay), w); gtk_overlay_add_overlay(GTK_OVERLAY(overlay), GTK_WIDGET(gpx_view_button)); gtk_box_pack_start(GTK_BOX(d->map.gpx_section), overlay, TRUE, TRUE, 0); grid = GTK_GRID(gtk_grid_new()); gtk_grid_set_column_spacing(grid, DT_PIXEL_APPLY_DPI(5)); line = 0; d->map.preview_button = gtk_check_button_new_with_label(_("preview images")); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->map.preview_button), TRUE); gtk_widget_set_sensitive(d->map.preview_button, FALSE); gtk_label_set_ellipsize(GTK_LABEL(gtk_bin_get_child(GTK_BIN(d->map.preview_button))), PANGO_ELLIPSIZE_END); gtk_grid_attach(grid, d->map.preview_button, 0, line, 1, 1); gtk_widget_set_tooltip_text(d->map.preview_button, _("show on map matching images")); g_signal_connect(GTK_TOGGLE_BUTTON(d->map.preview_button), "toggled", G_CALLBACK(_images_preview_toggled), self); d->map.select_button = dt_action_button_new(self, N_("select images"), _select_images, self, _("select matching images"), 0, 0); gtk_widget_set_hexpand(d->map.select_button, TRUE); gtk_widget_set_sensitive(d->map.select_button, FALSE); gtk_grid_attach(grid, d->map.select_button, 1, line, 1, 1); d->map.nb_imgs_label = dt_ui_label_new("0/0"); gtk_widget_set_halign(d->map.nb_imgs_label, GTK_ALIGN_END); gtk_widget_set_tooltip_text(GTK_WIDGET(d->map.nb_imgs_label), _("number of matching images versus selected images")); gtk_grid_attach(grid, d->map.nb_imgs_label, 2, line++, 1, 1); d->map.apply_gpx_button = dt_action_button_new(self, N_("apply geo-location"), _apply_gpx, self, _("apply geo-location to matching images"), 0, 0); gtk_widget_set_hexpand(d->map.apply_gpx_button, TRUE); gtk_widget_set_sensitive(d->map.apply_gpx_button, FALSE); gtk_grid_attach(grid, d->map.apply_gpx_button, 0, line++, 3, 1); gtk_box_pack_start(GTK_BOX(d->map.gpx_section), GTK_WIDGET(grid), TRUE, TRUE, 0); d->map.view = FALSE; gtk_widget_show_all(self->widget); gtk_widget_set_no_show_all(self->widget, TRUE); _update_layout(self); #endif d->imgid = NO_IMGID; d->datetime = d->datetime0 = _get_image_datetime(self); if(d->datetime) g_date_time_ref(d->datetime); _display_datetime(&d->dt0, d->datetime0, FALSE, self); _display_datetime(&d->dt, d->datetime, TRUE, self); d->offset = 0; _display_offset(d->offset, TRUE, self); for(int i = 0; i < DT_GEOTAG_PARTS_NB; i++) { g_signal_connect(d->dt.widget[i], "changed", G_CALLBACK(_datetime_entry_changed), self); dt_gui_connect_key(d->dt.widget[i], _datetime_key_pressed, self); dt_gui_connect_scroll(d->dt.widget[i], GTK_EVENT_CONTROLLER_SCROLL_VERTICAL | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE, _datetime_scroll_over, self); } DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_SELECTION_CHANGED, _selection_changed_callback); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE, _mouse_over_image_callback); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_IMAGE_INFO_CHANGED, _image_info_changed); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_PREFERENCES_CHANGE, _dt_pref_change_callback); #ifdef HAVE_MAP DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_VIEWMANAGER_VIEW_CHANGED, _view_changed); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_GEOTAG_CHANGED, _geotag_changed); #endif _show_milliseconds(d); gtk_widget_show_all(self->widget); gtk_widget_set_no_show_all(self->widget, TRUE); } void gui_cleanup(dt_lib_module_t *self) { dt_lib_geotagging_t *d = self->data; g_list_free_full(d->timezones, free_tz_tuple); d->timezones = NULL; g_time_zone_unref(d->tz_camera); if(d->datetime) g_date_time_unref(d->datetime); if(d->datetime0) g_date_time_unref(d->datetime0); if(d->imgs) { #ifdef HAVE_MAP _remove_images_from_map(self); #endif g_list_free_full(d->imgs, g_free); } d->imgs = NULL; d->imgs = 0; #ifdef HAVE_MAP _remove_tracks_from_map(self); #endif free(self->data); self->data = NULL; } // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; // clang-format on