/* 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/collection.h" #include "common/debug.h" #include "common/map_locations.h" #include "control/conf.h" #include "control/control.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "libs/lib.h" #include #include #include #include #include // map position module uses the tag dictionary with dt_geo_tag_root as a prefix. // Synonym field is used to store positions coordinates in ascii format. static void _signal_location_change(dt_lib_module_t *self); static void _show_location(dt_lib_module_t *self); DT_MODULE(1) const char *name(dt_lib_module_t *self) { return _("locations"); } dt_view_type_flags_t views(dt_lib_module_t *self) { return DT_VIEW_MAP; } uint32_t container(dt_lib_module_t *self) { return DT_UI_CONTAINER_PANEL_RIGHT_CENTER; } typedef struct dt_lib_map_locations_t { GtkWidget *shape_button; gulong shape_button_handler; GtkWidget *new_button; GtkWidget *show_all_button; GtkWidget *hide_button; GtkWidget *view; GtkCellRenderer *renderer; GtkTreeSelection *selection; GtkTreeViewColumn *name_col; GList *polygons; } dt_lib_map_locations_t; typedef struct dt_loc_op_t { char *newtagname; char *oldtagname; } dt_loc_op_t; int position(const dt_lib_module_t *self) { return 995; } typedef enum dt_map_positions_cols_t { DT_MAP_LOCATION_COL_ID = 0, DT_MAP_LOCATION_COL_TAG, DT_MAP_LOCATION_COL_PATH, DT_MAP_LOCATION_COL_COUNT, DT_MAP_LOCATION_NUM_COLS } dt_map_positions_cols_t; typedef enum dt_map_position_name_sort_id { DT_MAP_POSITION_SORT_NAME_ID, } dt_map_position_name_sort_id; const DTGTKCairoPaintIconFunc location_shapes[] = { dtgtk_cairo_paint_masks_circle, // MAP_LOCATION_SHAPE_ELLIPSE dtgtk_cairo_paint_rect_landscape, // MAP_LOCATION_SHAPE_RECTANGLE dtgtk_cairo_paint_polygon}; // MAP_LOCATION_SHAPE_POLYGONS // find a tag on the tree static gboolean _find_tag_iter_id(GtkTreeModel *model, GtkTreeIter *iter, const guint locid) { gboolean found = FALSE; if(locid <= 0) return found; guint id; do { gtk_tree_model_get(model, iter, DT_MAP_LOCATION_COL_ID, &id, -1); found = id == locid; if(found) return found; GtkTreeIter child, parent = *iter; if(gtk_tree_model_iter_children(model, &child, &parent)) { found = _find_tag_iter_id(model, &child, locid); if(found) { *iter = child; return found; } } } while(gtk_tree_model_iter_next(model, iter)); return found; } static void _locations_tree_update(dt_lib_module_t *self, const guint locid) { dt_lib_map_locations_t *d = self->data; GList *tags = dt_map_location_get_locations_by_path("", TRUE); GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->view)); g_object_ref(model); gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), NULL); gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING); gtk_tree_store_clear(GTK_TREE_STORE(model)); if(tags) { char **last_tokens = NULL; int last_tokens_length = 0; GtkTreeIter last_parent = { 0 }; GList *sorted_tags = dt_sort_tag(tags, 0); // ordered by full tag name tags = sorted_tags; for(GList *stag = tags; stag; stag = g_list_next(stag)) { GtkTreeIter iter; const gchar *tag = ((dt_map_location_t *)stag->data)->tag; if(tag == NULL) continue; char **tokens; tokens = g_strsplit(tag, "|", -1); if(tokens) { // find the number of common parts at the beginning of tokens and last_tokens GtkTreeIter parent = last_parent; const int tokens_length = g_strv_length(tokens); int common_length = 0; if(last_tokens) { while(tokens[common_length] && last_tokens[common_length] && !g_strcmp0(tokens[common_length], last_tokens[common_length])) { common_length++; } // point parent iter to where the entries should be added for(int i = common_length; i < last_tokens_length; i++) { gtk_tree_model_iter_parent(GTK_TREE_MODEL(model), &parent, &last_parent); last_parent = parent; } } // insert everything from tokens past the common part char *pth = NULL; for(int i = 0; i < common_length; i++) dt_util_str_cat(&pth, "%s|", tokens[i]); for(char **token = &tokens[common_length]; *token; token++) { dt_util_str_cat(&pth, "%s|", *token); gchar *pth2 = g_strdup(pth); pth2[strlen(pth2) - 1] = '\0'; gtk_tree_store_insert_with_values(GTK_TREE_STORE(model), &iter, common_length > 0 ? &parent : NULL, -1, DT_MAP_LOCATION_COL_TAG, *token, DT_MAP_LOCATION_COL_ID, (token == &tokens[tokens_length-1]) ? ((dt_map_location_t *)stag->data)->id : 0, DT_MAP_LOCATION_COL_PATH, pth2, DT_MAP_LOCATION_COL_COUNT, (token == &tokens[tokens_length-1]) ? ((dt_map_location_t *)stag->data)->count : 0, -1); common_length++; parent = iter; g_free(pth2); } g_free(pth); // remember things for the next round if(last_tokens) g_strfreev(last_tokens); last_tokens = tokens; last_parent = parent; last_tokens_length = tokens_length; } } g_strfreev(last_tokens); dt_map_location_free_result(&tags); } gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), model); g_object_unref(model); gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model), DT_MAP_POSITION_SORT_NAME_ID, GTK_SORT_ASCENDING); if(locid) { // try to select the right record GtkTreeIter iter; GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view)); if(gtk_tree_model_get_iter_first(model, &iter)) { if(_find_tag_iter_id(model, &iter, locid)) { gtk_tree_selection_select_iter(selection, &iter); GtkTreePath *path = gtk_tree_model_get_path(model, &iter); gtk_tree_view_expand_to_path(GTK_TREE_VIEW(d->view), path); gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(d->view), path, NULL, TRUE, 0.5, 0.5); gtk_tree_view_set_cursor(GTK_TREE_VIEW(d->view), path, d->name_col, FALSE); gtk_tree_path_free(path); } } } } static void _display_buttons(dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; GtkTreeIter iter; GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view)); GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->view)); if(gtk_tree_selection_get_selected(selection, &model, &iter)) { gtk_label_set_text(GTK_LABEL(gtk_bin_get_child(GTK_BIN(d->new_button))), _("new sub-location")); } else { gtk_label_set_text(GTK_LABEL(gtk_bin_get_child(GTK_BIN(d->new_button))), _("new location")); } } static void _tree_name_show(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer data) { guint locid; gchar *name; gchar *path; guint count; gchar *coltext; gtk_tree_model_get(model, iter, DT_MAP_LOCATION_COL_ID, &locid, DT_MAP_LOCATION_COL_TAG, &name, DT_MAP_LOCATION_COL_COUNT, &count, DT_MAP_LOCATION_COL_PATH, &path, -1); if(count < 1) { coltext = g_markup_printf_escaped(locid ? "%s" : "%s", name); } else { coltext = g_markup_printf_escaped(locid ? "%s (%d)" : "%s (%d)", name, count); } g_object_set(renderer, "markup", coltext, NULL); g_free(coltext); g_free(name); g_free(path); } static void _new_button_clicked(GtkButton *button, dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; GtkTreeIter iter, parent; GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view)); GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->view)); char *name = NULL; char *path = NULL; if(gtk_tree_selection_get_selected(selection, &model, &iter)) { gtk_tree_model_get(model, &iter, DT_MAP_LOCATION_COL_PATH, &path, -1); parent = iter; } name = path ? g_strconcat(path, "|", NULL) : g_strdup(""); const int base_len = strlen(name); int i = 1; dt_util_str_cat(&name, "%s", _("new location")); char *new_name = g_strdup(name); while(dt_map_location_name_exists(new_name)) { g_free(new_name); new_name = g_strdup_printf("%s %d", name, i); i++; } // add the new record to the tree gtk_tree_store_insert_with_values(GTK_TREE_STORE(model), &iter, path ? &parent : NULL, -1, DT_MAP_LOCATION_COL_TAG, &new_name[base_len], DT_MAP_LOCATION_COL_ID, -1, DT_MAP_LOCATION_COL_PATH, new_name, DT_MAP_LOCATION_COL_COUNT, 0, -1); g_free(new_name); g_free(name); g_free(path); // set the new record editable g_object_set(G_OBJECT(d->renderer), "editable", TRUE, NULL); GtkTreePath *path2 = gtk_tree_model_get_path(model, &iter); gtk_tree_view_expand_to_path(GTK_TREE_VIEW(d->view), path2); gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(d->view), path2, NULL, TRUE, 0.5, 0.5); gtk_tree_view_set_cursor(GTK_TREE_VIEW(d->view), path2, d->name_col, TRUE); gtk_tree_path_free(path2); } static void _shape_button_clicked(GtkButton *button, dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; int shape = dt_conf_get_int("plugins/map/locationshape"); shape++; if((shape > G_N_ELEMENTS(location_shapes) - 1) || (!d->polygons && shape == MAP_LOCATION_SHAPE_POLYGONS)) shape = 0; dt_conf_set_int("plugins/map/locationshape", shape); g_signal_handler_block (d->shape_button, d->shape_button_handler); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->shape_button), FALSE); dtgtk_togglebutton_set_paint((GtkDarktableToggleButton *)d->shape_button, location_shapes[shape], 0, NULL); g_signal_handler_unblock (d->shape_button, d->shape_button_handler); } static void _show_all_button_clicked(GtkButton *button, dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; dt_conf_set_bool("plugins/map/showalllocations", gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->show_all_button))); dt_view_map_location_action(darktable.view_manager, MAP_LOCATION_ACTION_UPDATE_OTHERS); } // delete a path of the tag tree static void _delete_tree_path(GtkTreeModel *model, GtkTreeIter *iter, gboolean root) { GtkTreeIter child, parent = *iter; gboolean valid = TRUE; do { if(gtk_tree_model_iter_children(model, &child, &parent)) _delete_tree_path(model, &child, FALSE); GtkTreeIter tobedel = parent; valid = gtk_tree_model_iter_next(model, &parent); char *path = NULL; gtk_tree_model_get(model, &tobedel, DT_MAP_LOCATION_COL_PATH, &path, -1); g_free(path); gtk_tree_store_remove(GTK_TREE_STORE(model), &tobedel); } while(!root && valid); } static gboolean _update_tag_name_per_name(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, dt_loc_op_t *to) { char *tagname; char *newtagname = to->newtagname; char *oldtagname = to->oldtagname; gtk_tree_model_get(model, iter, DT_MAP_LOCATION_COL_PATH, &tagname, -1); if(g_str_has_prefix(tagname, oldtagname)) { if(strlen(tagname) == strlen(oldtagname)) { // rename the tag itself char *subtag = g_strrstr(to->newtagname, "|"); subtag = (!subtag) ? newtagname : subtag + 1; gtk_tree_store_set(GTK_TREE_STORE(model), iter, DT_MAP_LOCATION_COL_PATH, newtagname, DT_MAP_LOCATION_COL_TAG, subtag, -1); } else if(strlen(tagname) > strlen(oldtagname) && tagname[strlen(oldtagname)] == '|') { // rename similar path char *newpath = g_strconcat(newtagname, &tagname[strlen(oldtagname)] , NULL); gtk_tree_store_set(GTK_TREE_STORE(model), iter, DT_MAP_LOCATION_COL_PATH, newpath, -1); g_free(newpath); } } g_free(tagname); return FALSE; } static void _view_map_geotag_changed(gpointer instance, GList *imgs, const int newlocid, dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; // one of the other location has been clicked on the map if(newlocid) { GtkTreeIter iter; GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->view)); if(gtk_tree_model_get_iter_first(model, &iter)) { if(_find_tag_iter_id(model, &iter, newlocid)) { GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view)); gtk_tree_selection_select_iter(selection, &iter); GtkTreePath *path = gtk_tree_model_get_path(model, &iter); gtk_tree_view_expand_to_path(GTK_TREE_VIEW(d->view), path); gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(d->view), path, NULL, TRUE, 0.5, 0.5); gtk_tree_view_set_cursor(GTK_TREE_VIEW(d->view), path, d->name_col, FALSE); gtk_tree_path_free(path); _show_location(self); _display_buttons(self); } } } else { for(GList* img = imgs; img; img = g_list_next(img)) { // find new locations for that image GList *tags = dt_map_location_find_locations(GPOINTER_TO_INT(img->data)); // update locations for that image dt_map_location_update_locations(GPOINTER_TO_INT(img->data), tags); g_list_free(tags); } // update count on the treeview GList *locs = dt_map_location_get_locations_by_path("", TRUE); GtkTreeIter iter; GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->view)); if(gtk_tree_model_get_iter_first(model, &iter)) { for(GList *loc = locs; loc; loc = g_list_next(loc)) { const guint locid = ((dt_map_location_t *)loc->data)->id; GtkTreeIter iter2 = iter; if(_find_tag_iter_id(model, &iter2, locid)) { gtk_tree_store_set(GTK_TREE_STORE(model), &iter2, DT_MAP_LOCATION_COL_COUNT, ((dt_map_location_t *)loc->data)->count, -1); } } } dt_map_location_free_result(&locs); } } static void _view_map_location_changed(gpointer instance, GList *polygons, dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; const int shape = dt_conf_get_int("plugins/map/locationshape"); if((shape == MAP_LOCATION_SHAPE_POLYGONS) && !polygons) { g_signal_handler_block (d->shape_button, d->shape_button_handler); dtgtk_togglebutton_set_paint((GtkDarktableToggleButton *)d->shape_button, location_shapes[MAP_LOCATION_SHAPE_ELLIPSE], 0, NULL); g_signal_handler_unblock (d->shape_button, d->shape_button_handler); dt_conf_set_int("plugins/map/locationshape", MAP_LOCATION_SHAPE_ELLIPSE); } d->polygons = polygons; } static void _signal_location_change(dt_lib_module_t *self) { dt_control_signal_block_by_func(darktable.signals, G_CALLBACK(_view_map_geotag_changed), self); DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_GEOTAG_CHANGED, (GList *)NULL, 0); dt_control_signal_unblock_by_func(darktable.signals, G_CALLBACK(_view_map_geotag_changed), self); } static void _name_editing_done(GtkCellEditable *editable, dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; gboolean canceled = TRUE; g_object_get(editable, "editing-canceled", &canceled, NULL); const gchar *name = gtk_entry_get_text(GTK_ENTRY(editable)); const gboolean reset = name[0] ? FALSE : TRUE; GtkTreeIter iter; GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view)); GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->view)); if(gtk_tree_selection_get_selected(selection, &model, &iter)) { char *path = NULL; char *leave = NULL; guint locid; gtk_tree_model_get(model, &iter, DT_MAP_LOCATION_COL_ID, &locid, DT_MAP_LOCATION_COL_PATH, &path, DT_MAP_LOCATION_COL_TAG, &leave, -1); if(reset && locid) canceled = TRUE; // empty name for a location is not allowed if(!canceled) { const gboolean isroot = !strcmp(path, leave); const int path_len = strlen(path); char *new_path; if(!isroot) { const int leave_len = strlen(leave); const char letter = path[path_len - leave_len]; path[path_len - leave_len] = '\0'; new_path = g_strconcat(path, name, NULL); path[path_len - leave_len] = letter; } else new_path = g_strdup((char *)name); gboolean new_exists = FALSE; GList *new_existing = NULL; if(!reset) // in case of reset we rely on dt_tag_rename to be safe new_existing = dt_map_location_get_locations_by_path(new_path, FALSE); if(new_existing) { dt_map_location_free_result(&new_existing); new_exists = TRUE; } if(!new_exists) { // new name is free, we can work if(locid == -1) { // new location - create and show it locid = dt_map_location_new(new_path); if(locid != -1) { // add the location on the map dt_map_location_data_t g; g.shape = dt_conf_get_int("plugins/map/locationshape"); g.lon = g.lat = NAN; g.delta1 = g.delta2 = 0.0; g.polygons = d->polygons; dt_view_map_add_location(darktable.view_manager, &g, locid); const int count = dt_map_location_get_images_count(locid); if(g_strstr_len(name, -1, "|")) { // the user wants to insert some group(s). difficult to handle the tree => reset _locations_tree_update(self, locid); } else { gtk_tree_store_set(GTK_TREE_STORE(model), &iter, DT_MAP_LOCATION_COL_ID, locid, DT_MAP_LOCATION_COL_PATH, new_path, DT_MAP_LOCATION_COL_TAG, name, DT_MAP_LOCATION_COL_COUNT, count, -1); } } else canceled = TRUE; } else { // existing location - rename it GList *children = dt_map_location_get_locations_by_path(path, FALSE); for(GList *tag = children; tag; tag = g_list_next(tag)) { // reset on leave is not possible. should be safe const char *new_part = &((dt_map_location_t *)tag->data)->tag[path_len + (reset ? 1 :0)]; char *new_name = g_strconcat(new_path, new_part, NULL); dt_map_location_rename(((dt_map_location_t *)tag->data)->id, new_name); g_free(new_name); } dt_map_location_free_result(&children); // update the store if(reset || g_strstr_len(name, -1, "|")) { // the user wants to insert some group(s). difficult to handle the tree => reset _locations_tree_update(self, locid); } else { dt_loc_op_t to; to.oldtagname = path; to.newtagname = new_path; gint sort_column; GtkSortType sort_order; gtk_tree_sortable_get_sort_column_id(GTK_TREE_SORTABLE(model), &sort_column, &sort_order); gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING); gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)_update_tag_name_per_name, &to); gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model), sort_column, sort_order); } _signal_location_change(self); } } else { dt_control_log(_("location name \'%s\' already exists"), new_path); canceled = TRUE; } g_free(new_path); } if(canceled) { if(locid == -1) { // if new we have to remove the new location from tree _delete_tree_path(model, &iter, TRUE); gtk_tree_selection_unselect_all(selection); } } g_free(path); g_free(leave); } g_object_set(G_OBJECT(d->renderer), "editable", FALSE, NULL); _display_buttons(self); } static void _name_start_editing(GtkCellRenderer *renderer, GtkCellEditable *editable, char *path, dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; if(GTK_IS_ENTRY(editable)) { // set up the editable with name (without number) GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->view)); GtkTreeIter iter; GtkTreePath *new_path = gtk_tree_path_new_from_string(path); if(gtk_tree_model_get_iter(model, &iter, new_path)) { char *name = NULL; gtk_tree_model_get(model, &iter, DT_MAP_LOCATION_COL_TAG, &name, -1); gtk_entry_set_text(GTK_ENTRY(editable), name); g_free(name); } gtk_tree_path_free(new_path); g_signal_connect(G_OBJECT(editable), "editing-done", G_CALLBACK(_name_editing_done), self); } } static gint _sort_position_names_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, dt_lib_module_t *self) { char *tag_a = NULL; char *tag_b = NULL; gtk_tree_model_get(model, a, DT_MAP_LOCATION_COL_PATH, &tag_a, -1); gtk_tree_model_get(model, b, DT_MAP_LOCATION_COL_PATH, &tag_b, -1); if(tag_a == NULL) tag_a = g_strdup(""); if(tag_b == NULL) tag_b = g_strdup(""); const gboolean sort = g_ascii_strncasecmp(tag_a, tag_b, -1); g_free(tag_a); g_free(tag_b); return sort; } static void _pop_menu_edit_location(GSimpleAction *action, GVariant *parameter, gpointer user_data) { dt_lib_module_t *self = (dt_lib_module_t *)user_data; dt_lib_map_locations_t *d = self->data; GtkTreeIter iter; GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view)); GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->view)); if(gtk_tree_selection_get_selected(selection, &model, &iter)) { GtkTreePath *path = gtk_tree_model_get_path(model, &iter); g_object_set(G_OBJECT(d->renderer), "editable", TRUE, NULL); gtk_tree_view_set_cursor(GTK_TREE_VIEW(d->view), path, d->name_col, TRUE); gtk_tree_path_free(path); _display_buttons(self); } } static void _pop_menu_delete_location(GSimpleAction *action, GVariant *parameter, gpointer user_data) { dt_lib_module_t *self = (dt_lib_module_t *)user_data; dt_lib_map_locations_t *d = self->data; GtkTreeIter iter; GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view)); GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->view)); if(gtk_tree_selection_get_selected(selection, &model, &iter)) { guint locid = 0; gtk_tree_model_get(model, &iter, DT_MAP_LOCATION_COL_ID, &locid, -1); if(locid > 0) { dt_view_map_location_action(darktable.view_manager, MAP_LOCATION_ACTION_REMOVE); dt_map_location_delete(locid); _signal_location_change(self); } // update the treeview GtkTreeIter parent; if(gtk_tree_model_iter_parent(model, &parent, &iter)) { guint parentid; gtk_tree_model_get(model, &parent, DT_MAP_LOCATION_COL_ID, &parentid, -1); if(parentid > 0) { _delete_tree_path(model, &iter, TRUE); gtk_tree_selection_unselect_all(selection); } else { // parent is a node (not a location). reset treeview _locations_tree_update(self, 0); } } else { _delete_tree_path(model, &iter, TRUE); gtk_tree_selection_unselect_all(selection); } } _display_buttons(self); } static void _show_location(dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view)); GtkTreeIter iter; GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->view)); if(gtk_tree_selection_get_selected(selection, &model, &iter)) { guint locid; gtk_tree_model_get(model, &iter, DT_MAP_LOCATION_COL_ID, &locid, -1); if(locid) { dt_map_location_data_t *p = dt_map_location_get_data(locid); dt_view_map_add_location(darktable.view_manager, p, locid); g_free(p); } else { // this is not a location (only a parent). remove location from map if any dt_view_map_location_action(darktable.view_manager, MAP_LOCATION_ACTION_REMOVE); } } } static gboolean _set_location_collection(dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; GtkTreeIter iter; GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->view)); GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view)); if(gtk_tree_selection_get_selected(selection, &model, &iter)) { char *name; gtk_tree_model_get(model, &iter, DT_MAP_LOCATION_COL_PATH, &name, -1); char *collection = g_strdup_printf("1:0:%d:%s|%s$", DT_COLLECTION_PROP_GEOTAGGING, _("tagged"), name); dt_collection_deserialize(collection, FALSE); g_free(collection); g_free(name); return TRUE; } return FALSE; } static void _pop_menu_update_filmstrip(GSimpleAction *action, GVariant *parameter, gpointer user_data) { dt_lib_module_t *self = (dt_lib_module_t *)user_data; _set_location_collection(self); } static void _pop_menu_goto_collection(GSimpleAction *action, GVariant *parameter, gpointer user_data) { dt_lib_module_t *self = (dt_lib_module_t *)user_data; if(_set_location_collection(self)) dt_ctl_switch_mode_to("lighttable"); } static void _pop_menu_view(GtkWidget *view, GdkEventButton *event, dt_lib_module_t *self) { GActionGroup *action_group = gtk_widget_get_action_group(view, "map_locations"); if(action_group == NULL) { GActionEntry action_entries[] = { { "edit", _pop_menu_edit_location, NULL, NULL }, { "delete", _pop_menu_delete_location, NULL, NULL }, { "update", _pop_menu_update_filmstrip, NULL, NULL }, { "goto", _pop_menu_goto_collection, NULL, NULL }, }; action_group = G_ACTION_GROUP(g_simple_action_group_new()); g_action_map_add_action_entries(G_ACTION_MAP(action_group), action_entries, G_N_ELEMENTS(action_entries), self); gtk_widget_insert_action_group(view, "map_locations", G_ACTION_GROUP(action_group)); g_object_unref(action_group); } GMenu *menu = g_menu_new(); GMenu *section = menu; GtkTreeIter iter; GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(view)); GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view)); if(gtk_tree_selection_get_selected(selection, &model, &iter)) { guint locid = 0; gtk_tree_model_get(model, &iter, DT_MAP_LOCATION_COL_ID, &locid, -1); GtkTreeIter child, parent = iter; const gboolean children = gtk_tree_model_iter_children(model, &child, &parent); g_menu_append(section, _("edit location"), "map_locations.edit"); g_menu_append(section, _("delete location"), "map_locations.delete"); GAction *item_action = g_action_map_lookup_action(G_ACTION_MAP(action_group), "delete"); g_simple_action_set_enabled(G_SIMPLE_ACTION(item_action), !children); section = g_menu_new(); g_menu_append_section(menu, NULL, G_MENU_MODEL(section)); g_menu_append(section, _("update filmstrip"), "map_locations.update"); g_menu_append(section, _("go to collection (lighttable)"), "map_locations.goto"); item_action = g_action_map_lookup_action(G_ACTION_MAP(action_group), "update"); g_simple_action_set_enabled(G_SIMPLE_ACTION(item_action), locid); GdkRectangle rect; GtkTreePath *path = gtk_tree_model_get_path(model, &iter); GtkTreeViewColumn *column = gtk_tree_view_get_column(GTK_TREE_VIEW(view), 0); gtk_tree_view_get_background_area(GTK_TREE_VIEW(view), path, column, &rect); // popup the menu GtkWidget *popover_menu = dt_gui_popover_menu_from_model(view, menu); g_object_unref(menu); gtk_popover_set_pointing_to(GTK_POPOVER(popover_menu), &rect); gtk_popover_popup(GTK_POPOVER(popover_menu)); } } static gboolean _force_selection_changed(dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; gtk_tree_selection_unselect_all(d->selection); return FALSE; } static void _selection_changed(GtkTreeSelection *selection, dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; GtkTreeIter iter; GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->view)); if(gtk_tree_selection_get_selected(selection, &model, &iter)) { _show_location(self); } else { dt_view_map_location_action(darktable.view_manager, MAP_LOCATION_ACTION_REMOVE); } _display_buttons(self); } static void _click_on_view(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_lib_module_t *self) { dt_lib_map_locations_t *d = self->data; GtkWidget *view = dt_gui_get_widget(gesture); gboolean editing; g_object_get(G_OBJECT(d->renderer), "editing", &editing, NULL); if(editing) { dt_control_log(_("terminate edit (press enter or escape) before selecting another location")); return; } const guint button_pressed = gtk_gesture_single_get_current_button(gesture); const gboolean ctrl_pressed = dt_key_modifier_state() & GDK_CONTROL_MASK; if(button_pressed == GDK_BUTTON_SECONDARY || button_pressed == GDK_BUTTON_PRIMARY) { GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view)); GtkTreePath *path = NULL; // Get tree path for row that was clicked if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view), (gint)x, (gint)y, &path, NULL, NULL, NULL)) { if(button_pressed == GDK_BUTTON_SECONDARY) { gtk_tree_selection_select_path(selection, path); _pop_menu_view(view, NULL, self); gtk_tree_path_free(path); _display_buttons(self); return; } else if(button_pressed == GDK_BUTTON_PRIMARY && !ctrl_pressed) { if(gtk_tree_selection_path_is_selected(selection, path)) g_timeout_add(100, (GSourceFunc)_force_selection_changed, self); gtk_tree_path_free(path); return; } else if(button_pressed == GDK_BUTTON_PRIMARY && ctrl_pressed) { gtk_tree_selection_select_path(selection, path); g_object_set(G_OBJECT(d->renderer), "editable", TRUE, NULL); gtk_tree_view_set_cursor(GTK_TREE_VIEW(d->view), path, d->name_col, TRUE); gtk_tree_path_free(path); _display_buttons(self); return; } } else { g_timeout_add(10, (GSourceFunc)_force_selection_changed, self); return; } } } void gui_init(dt_lib_module_t *self) { dt_lib_map_locations_t *d = g_malloc0(sizeof(dt_lib_map_locations_t)); self->data = d; self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); GtkTreeView *view = GTK_TREE_VIEW(gtk_tree_view_new()); d->view = GTK_WIDGET(view); gtk_box_pack_start(GTK_BOX(self->widget), dt_ui_resize_wrap(d->view, 100, "plugins/map/heightlocationwindow"), TRUE, TRUE, 0); gtk_tree_view_set_headers_visible(view, FALSE); GtkTreeStore *treestore = gtk_tree_store_new(DT_MAP_LOCATION_NUM_COLS, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_UINT); gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(treestore), DT_MAP_POSITION_SORT_NAME_ID, (GtkTreeIterCompareFunc)_sort_position_names_func, self, NULL); GtkTreeViewColumn *col = gtk_tree_view_column_new(); gtk_tree_view_append_column(view, col); gtk_tree_view_set_expander_column(view, col); d->name_col = col; GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); gtk_tree_view_column_pack_start(col, renderer, TRUE); gtk_tree_view_column_add_attribute(col, renderer, "text", DT_MAP_LOCATION_COL_TAG); gtk_tree_view_column_set_cell_data_func(col, renderer, _tree_name_show, (gpointer)self, NULL); // g_object_set(renderer, "editable", TRUE, NULL); g_signal_connect(G_OBJECT(renderer), "editing-started", G_CALLBACK(_name_start_editing), self); d->renderer = renderer; GtkTreeSelection *selection = gtk_tree_view_get_selection(view); d->selection = selection; gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE); gtk_tree_view_set_model(view, GTK_TREE_MODEL(treestore)); g_object_unref(treestore); dt_gui_connect_click(view, _click_on_view, NULL, self); gtk_widget_set_tooltip_text(GTK_WIDGET(view), _("list of user locations," "\nclick to show or hide a location on the map:" "\n - wheel scroll inside the shape to resize it" "\n - or scroll to modify the width or the height" "\n - click inside the shape and drag it to change its position" "\n - ctrl+click to move an image from inside the location" "\nctrl+click to edit a location name" "\n - a pipe \'|\' symbol breaks the name into several levels" "\n - to remove a group of locations clear its name" "\n - press enter to validate the new name, escape to cancel the edit" "\nright-click for other actions: delete location and go to collection")); // buttons GtkBox *hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0)); int shape = dt_conf_get_int("plugins/map/locationshape"); if(shape == MAP_LOCATION_SHAPE_POLYGONS) { shape = MAP_LOCATION_SHAPE_ELLIPSE; dt_conf_set_int("plugins/map/locationshape", shape); } d->shape_button = dtgtk_togglebutton_new(location_shapes[shape], 0, NULL); gtk_box_pack_start(hbox, d->shape_button, FALSE, TRUE, 0); d->shape_button_handler = g_signal_connect_data(G_OBJECT(d->shape_button), "clicked", G_CALLBACK(_shape_button_clicked), self, NULL, 0); gtk_widget_set_tooltip_text(GTK_WIDGET(d->shape_button ), _("select the shape of the location\'s limits on the map, circle or rectangle" "\nor even polygon if available (select first a polygon place in 'find location' module)")); d->new_button = dt_action_button_new(self, N_("new location"), _new_button_clicked, self, _("add a new location on the center of the visible map"), 0, 0); gtk_box_pack_start(hbox, d->new_button, TRUE, TRUE, 0); dt_conf_set_bool("plugins/map/showalllocations", FALSE); d->show_all_button = gtk_check_button_new_with_label(_("show all")); gtk_label_set_ellipsize(GTK_LABEL(gtk_bin_get_child(GTK_BIN(d->show_all_button))), PANGO_ELLIPSIZE_END); gtk_widget_set_tooltip_text(d->show_all_button, _("show all locations which are on the visible map")); gtk_box_pack_end(hbox, d->show_all_button, FALSE, FALSE, 8); g_signal_connect(G_OBJECT(d->show_all_button), "clicked", G_CALLBACK(_show_all_button_clicked), self); gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(hbox), FALSE, TRUE, 0); _locations_tree_update(self,0); _display_buttons(self); g_signal_connect(G_OBJECT(selection), "changed", G_CALLBACK(_selection_changed), self); // connect geotag changed signal DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_GEOTAG_CHANGED, _view_map_geotag_changed); // connect location changed signal DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_LOCATION_CHANGED, _view_map_location_changed); } void gui_cleanup(dt_lib_module_t *self) { 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