/* This file is part of darktable, Copyright (C) 2010-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 "libs/collect.h" #include "bauhaus/bauhaus.h" #include "common/collection.h" #include "common/collection_navigation.h" #include "common/darktable.h" #include "common/datetime.h" #include "common/debug.h" #include "common/film.h" #include "common/map_locations.h" #include "common/metadata.h" #include "common/utility.h" #include "control/conf.h" #include "control/control.h" #include "dtgtk/button.h" #include "dtgtk/thumbtable.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "gui/preferences_dialogs.h" #include "libs/lib.h" #include "libs/lib_api.h" #include "views/view.h" #include #include #include #ifndef _WIN32 #include #endif #ifdef GDK_WINDOWING_QUARTZ #include "osx/osx.h" #endif DT_MODULE(3) #define MAX_RULES 10 #define PARAM_STRING_SIZE 256 // FIXME: is this enough !? typedef struct _datetime_range_t { GTimeSpan nb1; GTimeSpan nb2; char *operator; GtkTreePath *path1; GtkTreePath *path2; } _datetime_range_t; typedef struct dt_lib_collect_rule_t { int num; GtkWidget *hbox; GtkWidget *combo; GtkWidget *text; GtkWidget *button; gboolean typing; gchar *searchstring; gboolean startwildcard; gboolean sensitive; _datetime_range_t datetime_range; GtkTreePath *expanded_path; } dt_lib_collect_rule_t; typedef struct dt_lib_collect_t { dt_lib_collect_rule_t rule[MAX_RULES]; int active_rule; int nb_rules; GtkTreeView *view; int view_rule; GtkTreeModel *treefilter; GtkTreeModel *listfilter; gboolean singleclick; struct dt_lib_collect_params_t *params; #ifdef _WIN32 GVolumeMonitor *vmonitor; #else GUnixMountMonitor *vmonitor; #endif gboolean inited; GtkWidget *history_box, *history_button; GtkWidget *back, *forward; GSimpleActionGroup *action_group; } dt_lib_collect_t; typedef struct dt_lib_collect_params_rule_t { uint32_t item : 16; uint32_t mode : 16; char string[PARAM_STRING_SIZE]; } dt_lib_collect_params_rule_t; typedef struct dt_lib_collect_params_t { uint32_t rules; dt_lib_collect_params_rule_t rule[MAX_RULES]; } dt_lib_collect_params_t; typedef enum dt_lib_collect_cols_t { DT_LIB_COLLECT_COL_TEXT = 0, DT_LIB_COLLECT_COL_ID, DT_LIB_COLLECT_COL_TOOLTIP, DT_LIB_COLLECT_COL_PATH, DT_LIB_COLLECT_COL_VISIBLE, DT_LIB_COLLECT_COL_UNREACHABLE, DT_LIB_COLLECT_COL_COUNT, DT_LIB_COLLECT_COL_INDEX, DT_LIB_COLLECT_NUM_COLS } dt_lib_collect_cols_t; typedef struct _range_t { gchar *start; gchar *stop; GtkTreePath *path1; GtkTreePath *path2; } _range_t; typedef struct filmroll_row_t { char *folder; char *value; int id; int count; int status; } filmroll_row_t; static void _lib_collect_gui_update(dt_lib_module_t *self); static void _lib_folders_update_collection(const gchar *filmroll); static void entry_changed(GtkEntry *entry, dt_lib_collect_rule_t *dr); static void collection_updated(gpointer instance, dt_collection_change_t query_change, dt_collection_properties_t changed_property, gpointer imgs, int next, gpointer self); static void row_activated_with_event(GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *col, GdkEventButton *event, dt_lib_collect_t *d); static int _is_time_property(const int property); static void _populate_collect_combo(GtkWidget *w); static gint _sort_filmroll_by_display_name(gconstpointer a, gconstpointer b) { const filmroll_row_t *ra = a; const filmroll_row_t *rb = b; return g_ascii_strcasecmp(ra->folder, rb->folder); } static gint _sort_filmroll_rows(gconstpointer a, gconstpointer b) { const filmroll_row_t *ra = a; const filmroll_row_t *rb = b; return g_ascii_strcasecmp(ra->value, rb->value); } static gint _sort_filmroll_by_id(gconstpointer a, gconstpointer b) { const filmroll_row_t *ra = a; const filmroll_row_t *rb = b; return ra->id - rb->id; } int last_state = 0; const char *name(dt_lib_module_t *self) { return _("collections"); } const char *description(dt_lib_module_t *self) { return _("define search criteria for images\n" "to be displayed or edited"); } void *legacy_params(struct dt_lib_module_t *self, const void *const old_params, const size_t old_params_size, const int old_version, int *new_version, size_t *new_size) { if(old_version == 1) { /* from v1 to v2 we have reordered the filters */ dt_lib_collect_params_t *o = (dt_lib_collect_params_t *)old_params; if(o->rules > MAX_RULES) /* preset is corrupted, return NULL and drop the preset */ return NULL; dt_lib_collect_params_t *n = malloc(old_params_size); const int table[DT_COLLECTION_PROP_LAST] = { DT_COLLECTION_PROP_FILMROLL, DT_COLLECTION_PROP_FOLDERS, DT_COLLECTION_PROP_CAMERA, DT_COLLECTION_PROP_TAG, DT_COLLECTION_PROP_DAY, DT_COLLECTION_PROP_TIME, DT_COLLECTION_PROP_HISTORY, DT_COLLECTION_PROP_COLORLABEL, // spaces for the metadata, see metadata.h DT_COLLECTION_PROP_COLORLABEL + 1, DT_COLLECTION_PROP_COLORLABEL + 2, DT_COLLECTION_PROP_COLORLABEL + 3, DT_COLLECTION_PROP_COLORLABEL + 4, DT_COLLECTION_PROP_COLORLABEL + 5, DT_COLLECTION_PROP_LENS, DT_COLLECTION_PROP_FOCAL_LENGTH, DT_COLLECTION_PROP_ISO, DT_COLLECTION_PROP_APERTURE, DT_COLLECTION_PROP_EXPOSURE, DT_COLLECTION_PROP_ASPECT_RATIO, DT_COLLECTION_PROP_FILENAME, DT_COLLECTION_PROP_GEOTAGGING, DT_COLLECTION_PROP_GROUPING, DT_COLLECTION_PROP_LOCAL_COPY, DT_COLLECTION_PROP_MODULE, DT_COLLECTION_PROP_ORDER }; n->rules = o->rules; for(int r=0; rrules; r++) { n->rule[r].item = table[o->rule[r].item]; n->rule[r].mode = o->rule[r].mode; memcpy(n->rule[r].string, o->rule[r].string, PARAM_STRING_SIZE); } *new_size = old_params_size; *new_version = 2; return (void *)n; } else if(old_version == 2) { /* from v2 to v3 we have added 4 new timestamp filters and 2 metadata filters */ dt_lib_collect_params_t *old = (dt_lib_collect_params_t *)old_params; if(old->rules > MAX_RULES) /* preset is corrupted, return NULL and drop the preset */ return NULL; dt_lib_collect_params_t *new = malloc(old_params_size); const int table[DT_COLLECTION_PROP_LAST] = { DT_COLLECTION_PROP_FILMROLL, DT_COLLECTION_PROP_FOLDERS, DT_COLLECTION_PROP_FILENAME, DT_COLLECTION_PROP_CAMERA, DT_COLLECTION_PROP_LENS, DT_COLLECTION_PROP_APERTURE, DT_COLLECTION_PROP_EXPOSURE, DT_COLLECTION_PROP_FOCAL_LENGTH, DT_COLLECTION_PROP_ISO, DT_COLLECTION_PROP_DAY, DT_COLLECTION_PROP_TIME, DT_COLLECTION_PROP_GEOTAGGING, DT_COLLECTION_PROP_ASPECT_RATIO, DT_COLLECTION_PROP_TAG, DT_COLLECTION_PROP_COLORLABEL, // spaces for the metadata, see metadata.h DT_COLLECTION_PROP_COLORLABEL + 1, DT_COLLECTION_PROP_COLORLABEL + 2, DT_COLLECTION_PROP_COLORLABEL + 3, DT_COLLECTION_PROP_COLORLABEL + 4, DT_COLLECTION_PROP_COLORLABEL + 5, DT_COLLECTION_PROP_GROUPING, DT_COLLECTION_PROP_LOCAL_COPY, DT_COLLECTION_PROP_HISTORY, DT_COLLECTION_PROP_MODULE, DT_COLLECTION_PROP_ORDER }; new->rules = old->rules; for(int r = 0; r < old->rules; r++) { new->rule[r].item = table[old->rule[r].item]; new->rule[r].mode = old->rule[r].mode; memcpy(new->rule[r].string, old->rule[r].string, PARAM_STRING_SIZE); } *new_size = old_params_size; *new_version = 3; return (void *)new; } return NULL; } void init_presets(dt_lib_module_t *self) { } /* Update the params struct with active ruleset */ static void _lib_collect_update_params(dt_lib_collect_t *d) { /* reset params */ dt_lib_collect_params_t *p = d->params; memset(p, 0, sizeof(dt_lib_collect_params_t)); /* for each active rule set update params */ const int _a = dt_conf_get_int("plugins/lighttable/collect/num_rules") - 1; const int active = CLAMP(_a, 0, (MAX_RULES - 1)); char confname[200] = { 0 }; for(int i = 0; i <= active; i++) { /* get item */ snprintf(confname, sizeof(confname), "plugins/lighttable/collect/item%1d", i); p->rule[i].item = dt_conf_get_int(confname); /* get mode */ snprintf(confname, sizeof(confname), "plugins/lighttable/collect/mode%1d", i); p->rule[i].mode = dt_conf_get_int(confname); /* get string */ snprintf(confname, sizeof(confname), "plugins/lighttable/collect/string%1d", i); const char *string = dt_conf_get_string_const(confname); if(string != NULL) { g_strlcpy(p->rule[i].string, string, PARAM_STRING_SIZE); } } p->rules = active + 1; } void *get_params(dt_lib_module_t *self, int *size) { _lib_collect_update_params(self->data); /* allocate a copy of params to return, freed by caller */ *size = sizeof(dt_lib_collect_params_t); void *p = malloc(*size); memcpy(p, ((dt_lib_collect_t *)self->data)->params, *size); return p; } int set_params(dt_lib_module_t *self, const void *params, const int size) { /* update conf settings from params */ dt_lib_collect_t *d = (dt_lib_collect_t *)self->data; dt_lib_collect_params_t *p = (dt_lib_collect_params_t *)params; char confname[200] = { 0 }; gboolean reset_view_filter = FALSE; for(uint32_t i = 0; i < p->rules; i++) { /* set item */ snprintf(confname, sizeof(confname), "plugins/lighttable/collect/item%1u", i); dt_conf_set_int(confname, p->rule[i].item); /* set mode */ snprintf(confname, sizeof(confname), "plugins/lighttable/collect/mode%1u", i); dt_conf_set_int(confname, p->rule[i].mode); /* set string */ snprintf(confname, sizeof(confname), "plugins/lighttable/collect/string%1u", i); dt_conf_set_string(confname, p->rule[i].string); /* if one of the rules is a rating filter, the view rating filter * will be reset to all */ if(p->rule[i].item == DT_COLLECTION_PROP_RATING) { reset_view_filter = TRUE; } } if(reset_view_filter) { dt_view_filtering_reset(darktable.view_manager, FALSE); } /* set number of rules */ g_strlcpy(confname, "plugins/lighttable/collect/num_rules", sizeof(confname)); dt_conf_set_int(confname, p->rules); d->view_rule = -1; /* update internal params */ _lib_collect_update_params(self->data); /* update ui */ _lib_collect_gui_update(self); /* update view */ dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, NULL); return 0; } dt_view_type_flags_t views(dt_lib_module_t *self) { return DT_VIEW_LIGHTTABLE | DT_VIEW_MAP | DT_VIEW_PRINT; } uint32_t container(dt_lib_module_t *self) { return DT_UI_CONTAINER_PANEL_LEFT_CENTER; } static void _view_popup_menu_onSearchFilmroll(GSimpleAction *action, GVariant *parameter, gpointer user_data) { GtkTreeView *treeview = GTK_TREE_VIEW(user_data); GtkWidget *win = dt_ui_main_window(darktable.gui->ui); GtkTreeSelection *selection; GtkTreeIter iter, child; GtkTreeModel *model; gchar *tree_path = NULL; gchar *new_path = NULL; model = gtk_tree_view_get_model(treeview); selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview)); if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return; child = iter; gtk_tree_model_iter_parent(model, &iter, &child); gtk_tree_model_get(model, &child, DT_LIB_COLLECT_COL_PATH, &tree_path, -1); GtkFileChooserNative *filechooser = gtk_file_chooser_native_new (_("update path to files"), GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, _("_open"), _("_cancel")); if(tree_path != NULL) gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(filechooser), tree_path); else goto error; // run the dialog if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT) { gint id = -1; sqlite3_stmt *stmt; gchar *query = NULL; gchar *uri = NULL; uri = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(filechooser)); new_path = g_filename_from_uri(uri, NULL, NULL); g_free(uri); if(new_path) { gchar *old = NULL; gchar *q_tree_path = g_strdup_printf("%s%%", tree_path); query = "SELECT id, folder FROM main.film_rolls WHERE folder LIKE ?1"; DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL); DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, q_tree_path, -1, SQLITE_TRANSIENT); g_free(q_tree_path); q_tree_path = NULL; query = NULL; while(sqlite3_step(stmt) == SQLITE_ROW) { id = sqlite3_column_int(stmt, 0); old = (gchar *)sqlite3_column_text(stmt, 1); query = g_strdup("UPDATE main.film_rolls SET folder=?1 WHERE id=?2"); gchar trailing[1024] = { 0 }; gchar final[1024] = { 0 }; if(g_strcmp0(old, tree_path)) { g_strlcpy(trailing, old + strlen(tree_path) + 1, sizeof(trailing)); g_snprintf(final, sizeof(final), "%s/%s", new_path, trailing); } else { g_strlcpy(final, new_path, sizeof(final)); } sqlite3_stmt *stmt2; DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt2, NULL); DT_DEBUG_SQLITE3_BIND_TEXT(stmt2, 1, final, -1, SQLITE_STATIC); DT_DEBUG_SQLITE3_BIND_INT(stmt2, 2, id); sqlite3_step(stmt2); sqlite3_finalize(stmt2); } sqlite3_finalize(stmt); g_free(query); /* reset filter so that view isn't empty */ dt_view_filtering_reset(darktable.view_manager, FALSE); // refresh the folders status dt_film_set_folder_status(); /* update collection to view missing filmroll */ _lib_folders_update_collection(new_path); DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_FILMROLLS_CHANGED); } else goto error; } g_free(tree_path); g_free(new_path); g_object_unref(filechooser); return; error: /* Something wrong happened */ g_object_unref(filechooser); dt_control_log(_("problem selecting new path for the filmroll in %s"), tree_path); g_free(tree_path); g_free(new_path); } static void _view_popup_menu_onRemove(GSimpleAction *action, GVariant *parameter, gpointer user_data) { GtkTreeView *treeview = GTK_TREE_VIEW(user_data); GtkTreeSelection *selection; GtkTreeIter iter, model_iter; GtkTreeModel *model; /* Get info about the filmroll (or parent) selected */ model = gtk_tree_view_get_model(treeview); selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview)); if(gtk_tree_selection_get_selected(selection, &model, &iter)) { gchar *filmroll_path = NULL; gchar *fullq = NULL; gtk_tree_model_get(model, &iter, DT_LIB_COLLECT_COL_PATH, &filmroll_path, -1); char *escaped_filmroll_path = sqlite3_mprintf("%q", filmroll_path); /* Clean selected images, and add to the table those which are going to be deleted */ DT_DEBUG_SQLITE3_EXEC(dt_database_get(darktable.db), "DELETE FROM main.selected_images", NULL, NULL, NULL); // clang-format off fullq = g_strdup_printf ("INSERT INTO main.selected_images (imgid)" " SELECT id" " FROM main.images" " WHERE film_id IN (SELECT id FROM main.film_rolls WHERE folder LIKE '%s%%')", escaped_filmroll_path); // clang-format on DT_DEBUG_SQLITE3_EXEC(dt_database_get(darktable.db), fullq, NULL, NULL, NULL); g_free(filmroll_path); sqlite3_free(escaped_filmroll_path); if(dt_control_remove_images()) { gtk_tree_model_filter_convert_iter_to_child_iter (GTK_TREE_MODEL_FILTER(model), &model_iter, &iter); if(gtk_tree_model_get_flags(model) == GTK_TREE_MODEL_LIST_ONLY) { gtk_list_store_remove (GTK_LIST_STORE(gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model))), &model_iter); } else { gtk_tree_store_remove (GTK_TREE_STORE(gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model))), &model_iter); } } g_free(fullq); } } static void _view_popup_menu(GtkWidget *treeview, GtkTreePath *path, dt_lib_collect_t *d) { GActionGroup *action_group = gtk_widget_get_action_group(treeview, "collections"); if(action_group == NULL) { GActionEntry action_entries[] = { { "update", _view_popup_menu_onSearchFilmroll, NULL, NULL }, { "remove", _view_popup_menu_onRemove, 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), treeview); gtk_widget_insert_action_group(treeview, "collections", G_ACTION_GROUP(action_group)); g_object_unref(action_group); } GMenu *menu = g_menu_new(); g_menu_append(menu, _("update path to files..."), "collections.update"); g_menu_append(menu, _("remove..."), "collections.remove"); // popup the menu GtkWidget *popover_menu = dt_gui_popover_menu_from_model(GTK_WIDGET(treeview), menu); g_object_unref(menu); if(path) { GdkRectangle rect; GtkTreeViewColumn *column = gtk_tree_view_get_column(GTK_TREE_VIEW(treeview), 0); gtk_tree_view_get_background_area(GTK_TREE_VIEW(treeview), path, column, &rect); gtk_popover_set_pointing_to(GTK_POPOVER(popover_menu), &rect); } gtk_popover_popup(GTK_POPOVER(popover_menu)); } /* Claim the press only when the collections handler fully owns it, so a * plain click still reaches the treeview's internal bubble-phase gesture: * single-click expander toggles, focus grab and cursor placement keep * working (the pre-migration button-press-event handler returned FALSE for * those presses). The presses that must claim are the ones the handler * resolves itself and that the internal gesture would fight: modifier * presses (ctrl/shift selection, shift-range, ctrl+shift view switch), * single-click mode, the MONTH rule's single-press activation, and the * folder/filmroll context menu. Same selective pattern as in * gui/accelerators.c. */ static void _gesture_begin_claim(GtkGesture *gesture, GdkEventSequence *sequence, gpointer user_data) { const dt_lib_collect_t *d = user_data; const GdkModifierType state = dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); const gboolean modifier = dt_modifier_is(state, GDK_SHIFT_MASK) || dt_modifier_is(state, GDK_CONTROL_MASK) || dt_modifier_is(state, GDK_SHIFT_MASK | GDK_CONTROL_MASK); const guint button = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture)); const gboolean folder_menu = (d->view_rule == DT_COLLECTION_PROP_FOLDERS || d->view_rule == DT_COLLECTION_PROP_FILMROLL) && button == GDK_BUTTON_SECONDARY && !modifier; if(modifier || d->singleclick || d->view_rule == DT_COLLECTION_PROP_MONTH || folder_menu) gtk_gesture_set_sequence_state(gesture, sequence, GTK_EVENT_SEQUENCE_CLAIMED); } /* Replicates GTK's private coords_are_over_arrow(): TRUE when the bin-window * coordinates hit the expander arrow of a parent row. Needed to tell apart * a double-click on the arrow (the treeview's internal gesture toggles it on * every primary release, so ours must claim the second press and not add a * third toggle) from a double-click on the row body (the internal gesture * does not toggle there, ours must). */ static gboolean _coords_are_over_arrow(GtkTreeView *view, gint bin_x, gint bin_y) { GtkTreePath *path = NULL; GtkTreeViewColumn *column = NULL; gint cell_x, cell_y; if(!gtk_tree_view_get_path_at_pos(view, bin_x, bin_y, &path, &column, &cell_x, &cell_y)) return FALSE; GtkTreeIter iter; GtkTreeModel *model = gtk_tree_view_get_model(view); const gboolean over_parent = gtk_tree_model_get_iter(model, &iter, path) && gtk_tree_model_iter_has_child(model, &iter); const gboolean in_expander_column = column == gtk_tree_view_get_expander_column(view); if(!over_parent || !in_expander_column) { gtk_tree_path_free(path); return FALSE; } gboolean indent_expanders = TRUE; gint expander_size = 12, horizontal_separator = 0; gtk_widget_style_get(GTK_WIDGET(view), "indent-expanders", &indent_expanders, "horizontal-separator", &horizontal_separator, "expander-size", &expander_size, NULL); /* GTK draws the arrow one expander width in per tree level below the * top level when indent-expanders is set: gtk_tree_view_get_arrow_xrange * offsets by expander_size * (rbtree_depth - 1) (the root rbtree has * depth 0), and the rbtree depth is path depth - 1. */ const gint depth = gtk_tree_path_get_depth(path); const gint x = indent_expanders ? expander_size * (depth - 1) : 0; const gint x1 = x + horizontal_separator / 2; const gint x2 = x + expander_size; gtk_tree_path_free(path); return cell_x >= x1 && cell_x < x2; } static void view_onButtonPressed_cb(GtkGestureSingle *gesture, int n_press, double x, double y, dt_lib_collect_t *d) { GtkWidget *treeview = dt_gui_get_widget(gesture); static GdkModifierType last_mod_state = 0; const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); /* gesture coordinates are relative to the widget allocation, while * gtk_tree_view_get_path_at_pos() expects bin-window coordinates */ gint bin_x, bin_y; gtk_tree_view_convert_widget_to_bin_window_coords(GTK_TREE_VIEW(treeview), (gint)x, (gint)y, &bin_x, &bin_y); /* Get tree path for row that was clicked */ GtkTreePath *path = NULL; const gboolean get_path = gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(treeview), bin_x, bin_y, &path, NULL, NULL, NULL); const GdkModifierType mod_state = dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); const guint button = gtk_gesture_single_get_current_button(gesture); const gboolean modifier = dt_modifier_is(mod_state, GDK_SHIFT_MASK) || dt_modifier_is(mod_state, GDK_CONTROL_MASK) || dt_modifier_is(mod_state, GDK_SHIFT_MASK | GDK_CONTROL_MASK); const gboolean plain_primary = button == GDK_BUTTON_PRIMARY && !modifier && !d->singleclick && d->view_rule != DT_COLLECTION_PROP_MONTH; const gboolean over_arrow = path && _coords_are_over_arrow(GTK_TREE_VIEW(treeview), bin_x, bin_y); /* the treeview's internal gesture toggles the expander on every primary * release over the arrow, so it owns all arrow toggles; ours must not add * a toggle for any n_press >= 2 primary press on the arrow (a double * click would otherwise get release1 + press2 + release2 = 3 toggles). * Only the second press of a fast pair is claimed, so the internal * gesture never sees it: a double-click then toggles exactly once (on the * first release) with no flicker, while every later press of a longer * burst is still handled by the internal gesture and keeps responding. */ const gboolean arrow_second_press = n_press == 2 && plain_primary && over_arrow; if(arrow_second_press) { GdkEventSequence *sequence = gtk_gesture_single_get_current_sequence(GTK_GESTURE_SINGLE(gesture)); gtk_gesture_set_sequence_state(GTK_GESTURE(gesture), sequence, GTK_EVENT_SEQUENCE_CLAIMED); } if((n_press >= 2 || d->singleclick) && path && !(n_press >= 2 && plain_primary && over_arrow)) { if(mod_state == last_mod_state) { if(gtk_tree_view_row_expanded(GTK_TREE_VIEW(treeview), path)) gtk_tree_view_collapse_row(GTK_TREE_VIEW(treeview), path); else gtk_tree_view_expand_row(GTK_TREE_VIEW(treeview), path, FALSE); } last_mod_state = mod_state; } // case of a range selection GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview)); if(get_path && dt_modifier_is(mod_state, GDK_SHIFT_MASK) && gtk_tree_selection_count_selected_rows(selection) > 0 && (d->view_rule == DT_COLLECTION_PROP_DAY || d->view_rule == DT_COLLECTION_PROP_MONTH || _is_time_property(d->view_rule) || d->view_rule == DT_COLLECTION_PROP_APERTURE || d->view_rule == DT_COLLECTION_PROP_FOCAL_LENGTH || d->view_rule == DT_COLLECTION_PROP_ISO || d->view_rule == DT_COLLECTION_PROP_EXPOSURE || d->view_rule == DT_COLLECTION_PROP_ASPECT_RATIO || d->view_rule == DT_COLLECTION_PROP_RATING)) { // range selection GList *sels = gtk_tree_selection_get_selected_rows(selection, NULL); GtkTreePath *path2 = (GtkTreePath *)sels->data; gtk_tree_selection_unselect_all(selection); if(gtk_tree_path_compare(path, path2) > 0) gtk_tree_selection_select_range(selection, path, path2); else gtk_tree_selection_select_range(selection, path2, path); g_list_free_full(sels, (GDestroyNotify)gtk_tree_path_free); row_activated_with_event(GTK_TREE_VIEW(treeview), path, NULL, (GdkEventButton *)event, d); gtk_tree_path_free(path); return; } if(path) { gtk_tree_selection_unselect_all(selection); gtk_tree_selection_select_path(selection, path); } // case of a context-menu (folder/filmroll) if(((d->view_rule == DT_COLLECTION_PROP_FOLDERS) || (d->view_rule == DT_COLLECTION_PROP_FILMROLL)) && (n_press == 1 && button == GDK_BUTTON_SECONDARY) && !(dt_modifier_is(mod_state, GDK_SHIFT_MASK) || dt_modifier_is(mod_state, GDK_CONTROL_MASK))) { row_activated_with_event(GTK_TREE_VIEW(treeview), path, NULL, (GdkEventButton *)event, d); _view_popup_menu(treeview, path, d); if(path) gtk_tree_path_free(path); return; } // case of a activation if((!d->singleclick && n_press >= 2 && button == GDK_BUTTON_PRIMARY) || (d->singleclick && n_press == 1 && button == GDK_BUTTON_PRIMARY) || (!d->singleclick && n_press == 1 && button == GDK_BUTTON_PRIMARY && modifier) || (d->view_rule == DT_COLLECTION_PROP_MONTH && n_press == 1 && button == GDK_BUTTON_PRIMARY)) { row_activated_with_event(GTK_TREE_VIEW(treeview), path, NULL, (GdkEventButton *)event, d); if(path) gtk_tree_path_free(path); return; } if(path) gtk_tree_path_free(path); } static gboolean view_onPopupMenu(GtkWidget *treeview, dt_lib_collect_t *d) { if(d->view_rule != DT_COLLECTION_PROP_FOLDERS) { return FALSE; } _view_popup_menu(treeview, NULL, d); return TRUE; /* we handled this */ } static dt_lib_collect_t *get_collect(dt_lib_collect_rule_t *r) { dt_lib_collect_t *d = (dt_lib_collect_t *)(((char *)r) - r->num * sizeof(dt_lib_collect_rule_t)); return d; } static void _scroll_to_position(GtkTreeView *treeview, GtkTreePath *path, GtkTreeViewColumn *column, gboolean use_align, float row_align, float col_align) { gtk_tree_view_scroll_to_cell(treeview, path, column, use_align, row_align, col_align); // Force value refresh of the vertical scrollbar. // This is a workaround for a (possible) GTK bug. When returning from darkroom // the treeview is not scrolled to the previous position. g_signal_emit_by_name(gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(treeview)), "value-changed"); } static gboolean list_select(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { dt_lib_collect_rule_t *dr = (dt_lib_collect_rule_t *)data; dt_lib_collect_t *d = get_collect(dr); gchar *str = NULL; gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, -1); gchar *haystack = g_utf8_strdown(str, -1); gchar *needle = g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(dr->text)), -1); if(strcmp(haystack, needle) == 0) { gtk_tree_selection_select_path(gtk_tree_view_get_selection(d->view), path); _scroll_to_position(d->view, path, NULL, FALSE, 0.2, 0); } g_free(haystack); g_free(needle); g_free(str); return FALSE; } static gboolean range_select(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { _range_t *range = (_range_t *)data; gchar *str = NULL; gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, -1); gchar *haystack = g_utf8_strdown(str, -1); gchar *needle; if(range->path1) needle = g_utf8_strdown(range->stop, -1); else needle = g_utf8_strdown(range->start, -1); if(strcmp(haystack, needle) == 0) { if(range->path1) { range->path2 = gtk_tree_path_copy(path); g_free(haystack); g_free(needle); g_free(str); return TRUE; } else range->path1 = gtk_tree_path_copy(path); } g_free(haystack); g_free(needle); g_free(str); return FALSE; } static gboolean _datetime_range_select(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { dt_lib_collect_rule_t *dr = (dt_lib_collect_rule_t *)data; gchar *str; gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, -1); const GTimeSpan nb = dt_datetime_exif_to_gtimespan(str); g_free(str); GTimeSpan target = dr->datetime_range.path1 == NULL ? dr->datetime_range.nb1 : dr->datetime_range.nb2; if(nb >= target) { if(dr->datetime_range.path1) return TRUE; else dr->datetime_range.path1 = gtk_tree_path_copy(path); } dr->datetime_range.path2 = gtk_tree_path_copy(path); return FALSE; } int _combo_get_active_collection(GtkWidget *combo) { return GPOINTER_TO_UINT(dt_bauhaus_combobox_get_data(combo)) - 1; } gboolean _combo_set_active_collection(GtkWidget *combo, const int property) { const gboolean found = dt_bauhaus_combobox_set_from_value(combo, property + 1); // make sure we have a valid collection if(!found) dt_bauhaus_combobox_set_from_value(combo, DT_COLLECTION_PROP_FILMROLL + 1); return found; } static gboolean tree_expand(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { dt_lib_collect_rule_t *dr = (dt_lib_collect_rule_t *)data; gchar *str = NULL; gboolean expanded = FALSE; gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, -1); gchar *haystack = dr->sensitive ? g_strdup(str) : g_utf8_strdown(str, -1); gchar *needle = g_strdup(dr->searchstring); int collection = _combo_get_active_collection(dr->combo); switch(collection) { case DT_COLLECTION_PROP_TAG: case DT_COLLECTION_PROP_GEOTAGGING: if(g_str_has_suffix(needle, "*")) needle[strlen(needle) - 1] = '\0'; if(g_str_has_suffix(needle, "|")) needle[strlen(needle) - 1] = '\0'; if(g_str_has_suffix(haystack, "|")) haystack[strlen(haystack) - 1] = '\0'; break; case DT_COLLECTION_PROP_FOLDERS: if(g_str_has_suffix(needle, "*")) needle[strlen(needle) - 1] = '\0'; if(g_str_has_suffix(needle, "/")) needle[strlen(needle) - 1] = '\0'; if(g_str_has_suffix(haystack, "/")) haystack[strlen(haystack) - 1] = '\0'; break; case DT_COLLECTION_PROP_DAY: case DT_COLLECTION_PROP_TIME: case DT_COLLECTION_PROP_IMPORT_TIMESTAMP: case DT_COLLECTION_PROP_CHANGE_TIMESTAMP: case DT_COLLECTION_PROP_EXPORT_TIMESTAMP: case DT_COLLECTION_PROP_PRINT_TIMESTAMP: if(g_str_has_suffix(needle, ":")) needle[strlen(needle) - 1] = '\0'; if(g_str_has_suffix(haystack, ":")) haystack[strlen(haystack) - 1] = '\0'; break; } if(!needle || !needle[0]) { //nothing to do, we keep the tree collapsed } else if((dr->startwildcard && g_strrstr(haystack, needle)) || (!dr->startwildcard && g_str_has_prefix(haystack, needle))) { dr->expanded_path = gtk_tree_path_copy(path); expanded = TRUE; } g_free(haystack); g_free(needle); g_free(str); return expanded; // if we expanded the path, we can stop iteration (saves half on average) } static gboolean list_match_string(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { dt_lib_collect_rule_t *dr = (dt_lib_collect_rule_t *)data; gchar *str = NULL; gboolean visible = FALSE; gboolean was_visible; gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, DT_LIB_COLLECT_COL_VISIBLE, &was_visible, -1); gchar *haystack = g_utf8_strdown(str, -1); const gchar *needle = dr->searchstring; const int property = _combo_get_active_collection(dr->combo); if(property == DT_COLLECTION_PROP_APERTURE || property == DT_COLLECTION_PROP_FOCAL_LENGTH || property == DT_COLLECTION_PROP_ISO || property == DT_COLLECTION_PROP_RATING) { // handle of numeric value, which can have some operator before the text visible = TRUE; gchar *operator, *number, *number2; dt_collection_split_operator_number(needle, &number, &number2, &operator); if(number) { const float nb1 = g_strtod(number, NULL); const float nb2 = g_strtod(haystack, NULL); if(operator && strcmp(operator, ">") == 0) { visible = (nb2 > nb1); } else if(operator && strcmp(operator, ">=") == 0) { visible = (nb2 >= nb1); } else if(operator && strcmp(operator, "<") == 0) { visible = (nb2 < nb1); } else if(operator && strcmp(operator, "<=") == 0) { visible = (nb2 <= nb1); } else if(operator && strcmp(operator, "<>") == 0) { visible = (nb1 != nb2); } else if(operator && number2 && strcmp(operator, "[]") == 0) { const float nb3 = g_strtod(number2, NULL); visible = (nb2 >= nb1 && nb2 <= nb3); } else { visible = (nb1 == nb2); } } g_free(operator); g_free(number); g_free(number2); } else if(property == DT_COLLECTION_PROP_FILENAME && strchr(needle,',') != NULL) { GList *list = dt_util_str_to_glist(",", needle); for(const GList *l = list; l; l = g_list_next(l)) { const char *name = (char *)l->data; if((visible = (g_strrstr(haystack, name + (name[0]=='%')) != NULL))) break; } g_list_free_full(list, g_free); } else { if(needle[0] == '%') needle++; if(!needle[0]) { // empty search string matches all visible = TRUE; } else if(!needle[1]) { // single-char search, use faster strchr instead of strstr visible = (strchr(haystack, needle[0]) != NULL); } else { visible = (g_strrstr(haystack, needle) != NULL); } } g_free(haystack); g_free(str); if(visible != was_visible) gtk_list_store_set(GTK_LIST_STORE(model), iter, DT_LIB_COLLECT_COL_VISIBLE, visible, -1); return FALSE; } static gboolean tree_match_string(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { dt_lib_collect_rule_t *dr = (dt_lib_collect_rule_t *)data; gchar *str = NULL; gboolean cur_state, visible; gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_PATH, &str, DT_LIB_COLLECT_COL_VISIBLE, &cur_state, -1); if(dr->typing == FALSE && !cur_state) { visible = TRUE; } else { gchar *haystack = dr->sensitive ? g_strdup(str) : g_utf8_strdown(str, -1); const int property = _combo_get_active_collection(dr->combo); if(_is_time_property(property) || property == DT_COLLECTION_PROP_DAY) { // handle of numeric value, which can have some operator before the text visible = TRUE; if(dr->datetime_range.nb1) { const GTimeSpan nb = dt_datetime_exif_to_gtimespan(haystack); if(!dr->datetime_range.operator) visible = (nb >= dr->datetime_range.nb1 && nb <= dr->datetime_range.nb2); else if(strcmp(dr->datetime_range.operator, ">") == 0) visible = (nb > dr->datetime_range.nb1); else if(strcmp(dr->datetime_range.operator, ">=") == 0) visible = (nb >= dr->datetime_range.nb1); else if(strcmp(dr->datetime_range.operator, "<") == 0) visible = (nb < dr->datetime_range.nb1); else if(strcmp(dr->datetime_range.operator, "<=") == 0) visible = (nb <= dr->datetime_range.nb1); else if(strcmp(dr->datetime_range.operator, "<>") == 0) visible = (nb != dr->datetime_range.nb1); else if(dr->datetime_range.nb2) visible = (nb >= dr->datetime_range.nb1 && nb <= dr->datetime_range.nb2); } } else if(!dr->searchstring || !dr->searchstring[0]) visible = TRUE; else if(dr->startwildcard) visible = g_strrstr(haystack, dr->searchstring) != NULL; else visible = g_str_has_prefix(haystack, dr->searchstring); g_free(haystack); } g_free(str); gtk_tree_store_set(GTK_TREE_STORE(model), iter, DT_LIB_COLLECT_COL_VISIBLE, visible, -1); return FALSE; } static gboolean tree_reveal_func(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { gboolean state; GtkTreeIter parent, child = *iter; gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_VISIBLE, &state, -1); if(!state) return FALSE; while(gtk_tree_model_iter_parent(model, &parent, &child)) { gtk_tree_model_get(model, &parent, DT_LIB_COLLECT_COL_VISIBLE, &state, -1); gtk_tree_store_set(GTK_TREE_STORE(model), &parent, DT_LIB_COLLECT_COL_VISIBLE, TRUE, -1); child = parent; } return FALSE; } static void tree_set_visibility(GtkTreeModel *model, gpointer data) { gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)tree_match_string, data); gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)tree_reveal_func, NULL); } static void _lib_folders_update_collection(const gchar *filmroll) { // remove from selected images where not in this query. sqlite3_stmt *stmt = NULL; const gchar *cquery = dt_collection_get_query(darktable.collection); if(cquery && cquery[0] != '\0') { gchar *complete_query = g_strdup_printf( "DELETE FROM main.selected_images WHERE imgid NOT IN (%s)", cquery); DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), complete_query, -1, &stmt, NULL); DT_DEBUG_SQLITE3_BIND_INT(stmt, 1, 0); DT_DEBUG_SQLITE3_BIND_INT(stmt, 2, -1); sqlite3_step(stmt); sqlite3_finalize(stmt); /* free allocated strings */ g_free(complete_query); } /* raise signal of collection change, only if this is an original */ if(!darktable.collection->clone) { dt_collection_memory_update(); DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_COLLECTION_CHANGED, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, (GList *)NULL, -1); } } static void set_properties(dt_lib_collect_rule_t *dr) { const int property = _combo_get_active_collection(dr->combo); const gchar *text = gtk_entry_get_text(GTK_ENTRY(dr->text)); char confname[200] = { 0 }; snprintf(confname, sizeof(confname), "plugins/lighttable/collect/string%1d", dr->num); dt_conf_set_string(confname, text); snprintf(confname, sizeof(confname), "plugins/lighttable/collect/item%1d", dr->num); dt_conf_set_int(confname, property); } static GtkTreeModel *_create_filtered_model(GtkTreeModel *model, dt_lib_collect_rule_t *dr) { GtkTreeModel *filter = NULL; GtkTreePath *path = NULL; if(_combo_get_active_collection(dr->combo) == DT_COLLECTION_PROP_FOLDERS) { // we search a common path to all the folders // we'll use it as root GtkTreeIter child, iter; int level = 0; while(gtk_tree_model_iter_n_children(model, level > 0 ? &iter : NULL) > 0) { if(level > 0) { sqlite3_stmt *stmt = NULL; gchar *pth = NULL; int id = -1; // Check if this path also matches a filmroll gtk_tree_model_get(model, &iter, DT_LIB_COLLECT_COL_PATH, &pth, -1); DT_DEBUG_SQLITE3_PREPARE_V2 (dt_database_get(darktable.db), "SELECT id FROM main.film_rolls WHERE folder LIKE ?1", -1, &stmt, NULL); DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, pth, -1, SQLITE_TRANSIENT); if(sqlite3_step(stmt) == SQLITE_ROW) id = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); g_free(pth); if(id != -1) { // we go back to the parent, in order to show this folder if(!gtk_tree_model_iter_parent(model, &child, &iter)) level = 0; iter = child; break; } } if(gtk_tree_model_iter_n_children(model, level > 0 ? &iter : NULL) != 1) break; gtk_tree_model_iter_children(model, &child, level > 0 ? &iter : NULL); iter = child; level++; } if(level > 0) { if(gtk_tree_model_iter_n_children(model, &iter) == 0 && gtk_tree_model_iter_parent(model, &child, &iter)) { path = gtk_tree_model_get_path(model, &child); } else { path = gtk_tree_model_get_path(model, &iter); } } } // Create filter and set virtual root filter = gtk_tree_model_filter_new(model, path); gtk_tree_path_free(path); gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(filter), DT_LIB_COLLECT_COL_VISIBLE); return filter; } static int string_array_length(char **list) { int length = 0; for(; *list; list++) length++; return length; } // returns a NULL terminated array of path components static char **split_path(const char *path) { if(!path || !*path) return NULL; char **result; char **tokens = g_strsplit(path, G_DIR_SEPARATOR_S, -1); #ifdef _WIN32 if(! (g_ascii_isalpha(tokens[0][0]) && tokens[0][strlen(tokens[0]) - 1] == ':') ) { g_strfreev(tokens); tokens = NULL; } result = tokens; #else // there are size + 1 elements in tokens -- the final NULL! we want to ignore it. const unsigned int size = g_strv_length(tokens); result = malloc(sizeof(char *) * size); for(unsigned int i = 0; i < size; i++) result[i] = tokens[i + 1]; g_free(tokens[0]); g_free(tokens); #endif return result; } typedef struct name_key_tuple_t { char *name, *collate_key; int count, status; } name_key_tuple_t; static void free_tuple(gpointer data) { name_key_tuple_t *tuple = (name_key_tuple_t *)data; g_free(tuple->name); g_free(tuple->collate_key); free(tuple); } static gint _sort_folder_tag(gconstpointer a, gconstpointer b) { const name_key_tuple_t *tuple_a = (const name_key_tuple_t *)a; const name_key_tuple_t *tuple_b = (const name_key_tuple_t *)b; return g_strcmp0(tuple_a->collate_key, tuple_b->collate_key); } // create a key such that _("not tagged") & "darktable|" are coming first, // and the rest is ordered such that sub tags are coming directly behind their parent static char *tag_collate_key(char *tag) { const size_t len = strlen(tag); char *result = g_malloc(len + 2); if(!g_strcmp0(tag, _("not tagged"))) *result = ' '; // 1 else if(g_str_has_prefix(tag, "darktable|")) *result = '!'; // 2 else *result = '"'; // 3 memcpy(result + 1, tag, len + 1); for(char *iter = result + 1; *iter; iter++) if(*iter == '|') *iter = ' '; return result; } void tree_count_show(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer data) { gchar *name; guint count; gtk_tree_model_get(model, iter, DT_LIB_COLLECT_COL_TEXT, &name, DT_LIB_COLLECT_COL_COUNT, &count, -1); if(!count) { g_object_set(renderer, "text", name, NULL); } else { gchar *coltext = g_strdup_printf("%s (%u)", name, count); g_object_set(renderer, "text", coltext, NULL); g_free(coltext); } g_free(name); } static void _expand_select_tree_path(GtkTreePath *path1, GtkTreePath *path2, dt_lib_collect_t *d) { GtkTreePath *p1 = gtk_tree_model_filter_convert_child_path_to_path(GTK_TREE_MODEL_FILTER(d->treefilter), path1); if(!p1) return; GtkTreePath *p2 = path2 ? gtk_tree_model_filter_convert_child_path_to_path(GTK_TREE_MODEL_FILTER(d->treefilter), path2) : NULL; GtkTreePath *p3 = NULL; GtkTreeIter iter; if(gtk_tree_model_get_iter(d->treefilter, &iter, p1)) { GtkTreeIter parent; if(gtk_tree_model_iter_parent(d->treefilter, &parent, &iter)) p3 = gtk_tree_model_get_path(d->treefilter, &parent); } gtk_tree_view_expand_to_path(d->view, p3 ? p3 : p1); _scroll_to_position(d->view, p1, NULL, TRUE, 0.5, 0.5); if(path2) gtk_tree_selection_select_range(gtk_tree_view_get_selection(d->view), p1, p2); else gtk_tree_selection_select_path(gtk_tree_view_get_selection(d->view), p1); gtk_tree_path_free(p1); if(p2) gtk_tree_path_free(p2); if(p3) gtk_tree_path_free(p3); } static const char *UNCATEGORIZED_TAG = N_("uncategorized"); static void _update_parent_count(GtkTreeModel *model, GtkTreeIter *iter, const int count) { GtkTreeIter parent = *iter; GtkTreeIter child; guint parentcount; do { gtk_tree_model_get(model, &parent, DT_LIB_COLLECT_COL_COUNT, &parentcount, -1); gtk_tree_store_set(GTK_TREE_STORE(model), &parent, DT_LIB_COLLECT_COL_COUNT, parentcount + count, -1); child = parent; } while (gtk_tree_model_iter_parent(model, &parent, &child)); } static void _tree_view(dt_lib_collect_rule_t *dr) { // update related list dt_lib_collect_t *d = get_collect(dr); const int property = _combo_get_active_collection(dr->combo); char *format_separator = ""; switch(property) { case DT_COLLECTION_PROP_FOLDERS: format_separator = "%s" G_DIR_SEPARATOR_S; break; case DT_COLLECTION_PROP_TAG: case DT_COLLECTION_PROP_GEOTAGGING: format_separator = "%s|"; break; case DT_COLLECTION_PROP_DAY: case DT_COLLECTION_PROP_MONTH: case DT_COLLECTION_PROP_TIME: case DT_COLLECTION_PROP_IMPORT_TIMESTAMP: case DT_COLLECTION_PROP_CHANGE_TIMESTAMP: case DT_COLLECTION_PROP_EXPORT_TIMESTAMP: case DT_COLLECTION_PROP_PRINT_TIMESTAMP: format_separator = "%s:"; break; } set_properties(dr); GtkTreeModel *model = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(d->treefilter)); // since we'll be inserting elements in the same order that we want // them displayed, there is no need for the overhead of having the // tree view sort itself gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING); if(d->view_rule != property) { // tree creation/recreation sqlite3_stmt *stmt; GtkTreeIter uncategorized = { 0 }; GtkTreeIter temp; g_object_ref(model); g_object_unref(d->treefilter); gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), NULL); gtk_tree_store_clear(GTK_TREE_STORE(model)); gtk_widget_hide(GTK_WIDGET(d->view)); /* query construction */ gchar *where_ext = dt_collection_get_extended_where(darktable.collection, dr->num); gchar *query = NULL; switch(property) { case DT_COLLECTION_PROP_FOLDERS: // clang-format off query = g_strdup_printf ("SELECT folder, film_rolls_id, COUNT(*) AS count, status" " FROM main.images AS mi" " JOIN (SELECT fr.id AS film_rolls_id, folder, status" " FROM main.film_rolls AS fr" " JOIN memory.film_folder AS ff" " ON fr.id = ff.id)" " ON film_id = film_rolls_id " " WHERE %s" " GROUP BY folder, film_rolls_id", where_ext); // clang-format on break; case DT_COLLECTION_PROP_TAG: { const gboolean is_insensitive = dt_conf_is_equal("plugins/lighttable/tagging/case_sensitivity", "insensitive"); if(is_insensitive) // clang-format off query = g_strdup_printf ("SELECT name, 1 AS tagid, SUM(count) AS count" " FROM (SELECT tagid, COUNT(*) as count" " FROM main.images AS mi" " JOIN main.tagged_images" " ON id = imgid " " WHERE %s" " GROUP BY tagid)" " JOIN (SELECT lower(name) AS name, id AS tag_id FROM data.tags)" " ON tagid = tag_id" " GROUP BY name", where_ext); // clang-format on else // clang-format off query = g_strdup_printf ("SELECT name, tagid, count" " FROM (SELECT tagid, COUNT(*) AS count" " FROM main.images AS mi" " JOIN main.tagged_images" " ON id = imgid " " WHERE %s" " GROUP BY tagid)" " JOIN (SELECT name, id AS tag_id FROM data.tags)" " ON tagid = tag_id" , where_ext); // clang-format on // clang-format off dt_util_str_cat (&query, " UNION ALL " "SELECT '%s' AS name, 0 as id, COUNT(*) AS count " "FROM main.images AS mi " "WHERE mi.id NOT IN" " (SELECT DISTINCT imgid FROM main.tagged_images AS ti" " WHERE ti.tagid NOT IN memory.darktable_tags)", _("not tagged")); // clang-format on } break; case DT_COLLECTION_PROP_GEOTAGGING: // clang-format off query = g_strdup_printf ("SELECT " " CASE WHEN mi.longitude IS NULL" " OR mi.latitude IS null THEN \'%s\'" " ELSE CASE WHEN ta.imgid IS NULL THEN \'%s\'" " ELSE \'%s\' || ta.tagname" " END" " END AS name," " ta.tagid AS tag_id, COUNT(*) AS count" " FROM main.images AS mi" " LEFT JOIN (SELECT imgid, t.id AS tagid, SUBSTR(t.name, %d) AS tagname" " FROM main.tagged_images AS ti" " JOIN data.tags AS t" " ON ti.tagid = t.id" " JOIN data.locations AS l" " ON l.tagid = t.id" " ) AS ta ON ta.imgid = mi.id" " WHERE %s" " GROUP BY name, tag_id", _("not tagged"), _("tagged"), _("tagged"), (int)strlen(dt_map_location_data_tag_root()) + 1, where_ext); // clang-format on break; case DT_COLLECTION_PROP_DAY: // clang-format off query = g_strdup_printf ("SELECT (datetime_taken / 86400000000) * 86400000000 AS date, 1," " COUNT(*) AS count" " FROM main.images AS mi" " WHERE datetime_taken IS NOT NULL AND datetime_taken <> 0" " AND %s" " GROUP BY date", where_ext); // clang-format on break; case DT_COLLECTION_PROP_MONTH: // clang-format off query = g_strdup_printf ("SELECT CAST(strftime('%%m', datetime_taken / 86400000000.0" " + julianday('0001-01-01')) AS INTEGER) AS month_num," " 1, COUNT(*) AS count" " FROM main.images AS mi" " WHERE datetime_taken IS NOT NULL AND datetime_taken <> 0" " AND %s" " GROUP BY month_num" " ORDER BY month_num", where_ext); // clang-format on break; case DT_COLLECTION_PROP_TIME: case DT_COLLECTION_PROP_IMPORT_TIMESTAMP: case DT_COLLECTION_PROP_CHANGE_TIMESTAMP: case DT_COLLECTION_PROP_EXPORT_TIMESTAMP: case DT_COLLECTION_PROP_PRINT_TIMESTAMP: { const int local_property = property; char *colname = NULL; switch(local_property) { case DT_COLLECTION_PROP_TIME: colname = "datetime_taken"; break; case DT_COLLECTION_PROP_IMPORT_TIMESTAMP: colname = "import_timestamp"; break; case DT_COLLECTION_PROP_CHANGE_TIMESTAMP: colname = "change_timestamp"; break; case DT_COLLECTION_PROP_EXPORT_TIMESTAMP: colname = "export_timestamp"; break; case DT_COLLECTION_PROP_PRINT_TIMESTAMP: colname = "print_timestamp"; break; } // clang-format off query = g_strdup_printf("SELECT %s AS date, 1, COUNT(*) AS count" " FROM main.images AS mi" " WHERE %s IS NOT NULL AND %s <> 0" " AND %s" " GROUP BY date", colname, colname, colname, where_ext); // clang-format on break; } } g_free(where_ext); DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL); char **last_tokens = NULL; int last_tokens_length = 0; GtkTreeIter last_parent = { 0 }; // we need to sort the names ourselves and not let sqlite handle this // because it knows nothing about path separators. GList *sorted_names = NULL; guint index = 0; while(sqlite3_step(stmt) == SQLITE_ROW) { char *name; if(_is_time_property(property) || property == DT_COLLECTION_PROP_DAY) { char sdt[DT_DATETIME_EXIF_LENGTH] = {0}; dt_datetime_gtimespan_to_exif(sdt, sizeof(sdt), sqlite3_column_int64(stmt, 0)); if(property == DT_COLLECTION_PROP_DAY) sdt[10] = '\0'; name = g_strdup(sdt); } else if(property == DT_COLLECTION_PROP_MONTH) { const int month_num = sqlite3_column_int(stmt, 0); if(month_num >= 1 && month_num <= 12) name = g_strdup_printf("%02d - %s", month_num, _(dt_month_names[month_num - 1])); else name = g_strdup(_("unknown")); } else { const char* sqlite_name = (const char *)sqlite3_column_text(stmt, 0); name = sqlite_name == NULL ? g_strdup("") : g_strdup(sqlite_name); } gchar *collate_key = NULL; const int count = sqlite3_column_int(stmt, 2); if(property == DT_COLLECTION_PROP_FOLDERS) { char *name_folded = g_utf8_casefold(name, -1); char *name_folded_slash = g_strconcat(name_folded, G_DIR_SEPARATOR_S, NULL); collate_key = g_utf8_collate_key_for_filename(name_folded_slash, -1); g_free(name_folded_slash); g_free(name_folded); } else { char *tck = tag_collate_key(name); collate_key = g_utf8_collate_key_for_filename(tck, -1); g_free(tck); } name_key_tuple_t *tuple = malloc(sizeof(name_key_tuple_t)); tuple->name = name; tuple->collate_key = collate_key; tuple->count = count; tuple->status = property == DT_COLLECTION_PROP_FOLDERS ? sqlite3_column_int(stmt, 3) : -1; sorted_names = g_list_prepend(sorted_names, tuple); } sqlite3_finalize(stmt); g_free(query); // this order should not be altered. the right feeding of the tree relies on it. sorted_names = g_list_sort(sorted_names, _sort_folder_tag); const gboolean sort_descend = dt_conf_get_bool("plugins/collect/descending"); if(!sort_descend) sorted_names = g_list_reverse(sorted_names); const gboolean no_uncategorized = (property == DT_COLLECTION_PROP_TAG) ? dt_conf_get_bool("plugins/lighttable/tagging/no_uncategorized") : TRUE; for(GList *names = sorted_names; names; names = g_list_next(names)) { name_key_tuple_t *tuple = (name_key_tuple_t *)names->data; char *name = tuple->name; const int count = tuple->count; const int status = tuple->status; if(name == NULL) continue; // safeguard against degenerated db entries // this is just for tags gboolean uncategorized_found = FALSE; if(!no_uncategorized && strchr(name, '|') == NULL) { char *next_name = g_strdup(names->next ? ((name_key_tuple_t *)names->next->data)->name : ""); if(strlen(next_name) >= strlen(name) + 1 && next_name[strlen(name)] == '|') next_name[strlen(name)] = '\0'; if(g_strcmp0(next_name, name) && g_strcmp0(name, _("not tagged"))) { /* add uncategorized root iter if not exists */ if(!uncategorized.stamp) { gtk_tree_store_insert_with_values (GTK_TREE_STORE(model), &uncategorized, NULL, -1, DT_LIB_COLLECT_COL_TEXT, _(UNCATEGORIZED_TAG), DT_LIB_COLLECT_COL_PATH, "", DT_LIB_COLLECT_COL_VISIBLE, TRUE, DT_LIB_COLLECT_COL_INDEX, index, -1); index++; } /* adding an uncategorized tag */ gtk_tree_store_insert_with_values (GTK_TREE_STORE(model), &temp, &uncategorized, 0, DT_LIB_COLLECT_COL_TEXT, name, DT_LIB_COLLECT_COL_PATH, name, DT_LIB_COLLECT_COL_VISIBLE, TRUE, DT_LIB_COLLECT_COL_COUNT, count, DT_LIB_COLLECT_COL_INDEX, index, -1); uncategorized_found = TRUE; index++; } g_free(next_name); } if(!uncategorized_found) { char **tokens; if(property == DT_COLLECTION_PROP_FOLDERS) tokens = split_path(name); else if(property == DT_COLLECTION_PROP_DAY) tokens = g_strsplit(name, ":", -1); else if(_is_time_property(property)) tokens = g_strsplit_set(name, ": ", 4); else tokens = g_strsplit(name, "|", -1); if(tokens != NULL) { // find the number of common parts at the beginning of tokens and // last_tokens GtkTreeIter parent = last_parent; const int tokens_length = string_array_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(model, &parent, &last_parent); last_parent = parent; } } // insert everything from tokens past the common part char *pth = NULL; #ifndef _WIN32 if(property == DT_COLLECTION_PROP_FOLDERS) pth = g_strdup("/"); #endif for(int i = 0; i < common_length; i++) dt_util_str_cat(&pth, format_separator, tokens[i]); // if we are at the end of the path, and this is a folder, we need to // add the count to all parents. common_length == 0 means 'parent' was // never set to a valid node, and there is no node to update anyway. if(common_length > 0 && !tokens[common_length] && property == DT_COLLECTION_PROP_FOLDERS) { _update_parent_count(model, &parent, count); } for(char **token = &tokens[common_length]; *token; token++) { GtkTreeIter iter; dt_util_str_cat(&pth, format_separator, *token); if(_is_time_property(property) && !*(token + 1)) pth[10] = ' '; 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, 0, DT_LIB_COLLECT_COL_TEXT, *token, DT_LIB_COLLECT_COL_PATH, pth2, DT_LIB_COLLECT_COL_VISIBLE, TRUE, DT_LIB_COLLECT_COL_COUNT, (*(token + 1)?0:count), DT_LIB_COLLECT_COL_INDEX, index, DT_LIB_COLLECT_COL_UNREACHABLE, (*(token + 1) ? 0 : !status), -1); index++; // also add the item count to parents if((property == DT_COLLECTION_PROP_DAY || property == DT_COLLECTION_PROP_FOLDERS || _is_time_property(property)) && !*(token + 1)) { GtkTreeIter p; // a top-level node has no parent: gtk_tree_model_iter_parent // then returns FALSE and leaves 'p' uninitialized, so passing it // on would cause a crash when dereferenced later if(gtk_tree_model_iter_parent(model, &p, &iter)) _update_parent_count(model, &p, count); } 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_list_free_full(sorted_names, free_tuple); gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(d->view), DT_LIB_COLLECT_COL_TOOLTIP); d->treefilter = _create_filtered_model(model, dr); GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view)); if(property == DT_COLLECTION_PROP_DAY || _is_time_property(property)) { gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE); } else { gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE); } gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), d->treefilter); gtk_widget_set_no_show_all(GTK_WIDGET(d->view), FALSE); gtk_widget_show_all(GTK_WIDGET(d->view)); g_object_unref(model); g_strfreev(last_tokens); d->view_rule = property; } // we update tree expansion and selection gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(d->view)); gtk_tree_view_collapse_all(d->view); // prepare datetime entry data if(_is_time_property(property) || property == DT_COLLECTION_PROP_DAY) { gchar *number1, *number2; dt_collection_split_operator_datetime(gtk_entry_get_text(GTK_ENTRY(dr->text)), &number1, &number2, &dr->datetime_range.operator); if(number1 && number1[strlen(number1) - 1] == '%') number1[strlen(number1) - 1] = '\0'; dr->datetime_range.nb1 = number1 ? dt_datetime_exif_to_gtimespan(number1) : 0; dr->datetime_range.nb2 = number2 ? dt_datetime_exif_to_gtimespan(number2) : 0; g_free(number1); g_free(number2); } dr->sensitive = (property == DT_COLLECTION_PROP_TAG && dt_conf_is_equal("plugins/lighttable/tagging/case_sensitivity", "sensitive")); gchar *needle = dr->sensitive ? g_strdup(gtk_entry_get_text(GTK_ENTRY(dr->text))) : g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(dr->text)), -1); if(g_str_has_suffix(needle, "%")) needle[strlen(needle) - 1] = '\0'; dr->startwildcard = needle[0] == '%'; dr->searchstring = needle[0] == '%' ? &needle[1] : needle; dr->expanded_path = NULL; // if needed, we restrict the tree to matching entries if(dr->typing) tree_set_visibility(model, dr); if(!needle[0]) {} // do nothing // select/expand items corresponding to entry else if(property == DT_COLLECTION_PROP_DAY || _is_time_property(property)) { if(strcmp(dr->datetime_range.operator, "[]") == 0) { dr->datetime_range.path1 = NULL; dr->datetime_range.path2 = NULL; gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)_datetime_range_select, dr); if(dr->datetime_range.path1 && dr->datetime_range.path2) _expand_select_tree_path(dr->datetime_range.path1, dr->datetime_range.path2, d); if(dr->datetime_range.path1) gtk_tree_path_free(dr->datetime_range.path1); if(dr->datetime_range.path2) gtk_tree_path_free(dr->datetime_range.path2); } else gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)tree_expand, dr); g_free(dr->datetime_range.operator); } else gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)tree_expand, dr); if(dr->expanded_path) { _expand_select_tree_path(dr->expanded_path, NULL, d); gtk_tree_path_free(dr->expanded_path); } dr->searchstring = NULL; g_free(needle); } static void _list_view(dt_lib_collect_rule_t *dr) { // update related list dt_lib_collect_t *d = get_collect(dr); const int property = _combo_get_active_collection(dr->combo); set_properties(dr); GtkTreeModel *model = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(d->listfilter)); if(d->view_rule != property) { const gboolean sort_descending = dt_conf_get_bool("plugins/collect/descending"); sqlite3_stmt *stmt; g_object_unref(d->listfilter); g_object_ref(model); gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), NULL); gtk_list_store_clear(GTK_LIST_STORE(model)); gtk_widget_hide(GTK_WIDGET(d->view)); gchar *where_ext = dt_collection_get_extended_where(darktable.collection, dr->num); char query[1024] = { 0 }; switch(property) { case DT_COLLECTION_PROP_CAMERA:; // camera // clang-format off g_snprintf(query, sizeof(query), "SELECT TRIM(cm.maker || ' ' || cm.model) AS camera," " 1, COUNT(*) AS count" " FROM main.images AS mi, main.cameras AS cm" " WHERE mi.camera_id = cm.id" " AND %s " " GROUP BY LOWER(camera)" " ORDER BY LOWER(camera) %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_HISTORY: // History // images without history are counted as if they were basic // clang-format off g_snprintf(query, sizeof(query), "SELECT CASE" " WHEN basic_hash == current_hash THEN '%s'" " WHEN auto_hash == current_hash THEN '%s'" " WHEN current_hash IS NOT NULL THEN '%s'" " ELSE '%s'" " END as altered, 1, COUNT(*) AS count," " CASE" " WHEN basic_hash == current_hash THEN 0" " WHEN auto_hash == current_hash THEN 1" " WHEN current_hash IS NOT NULL THEN 3" " ELSE 2" " END AS force_order" " FROM main.images AS mi" " LEFT JOIN (SELECT DISTINCT imgid, basic_hash, auto_hash, current_hash" " FROM main.history_hash) ON id = imgid" " WHERE %s" " GROUP BY force_order" " ORDER BY force_order %s", _("basic"), _("auto applied"), _("altered"), _("basic"), where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_LOCAL_COPY: // local copy, 2 hardcoded alternatives // clang-format off g_snprintf(query, sizeof(query), "SELECT CASE " " WHEN (flags & %d) THEN '%s'" " ELSE '%s'" " END as lcp, 1, COUNT(*) AS count," " CASE " " WHEN (flags & %d) THEN 0" " ELSE 1" " END as force_order" " FROM main.images AS mi " " WHERE %s" " GROUP BY force_order ORDER BY force_order %s", DT_IMAGE_LOCAL_COPY, _("copied locally"), _("not copied locally"), DT_IMAGE_LOCAL_COPY, where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_ASPECT_RATIO: // aspect ratio, 3 hardcoded alternatives // clang-format off g_snprintf(query, sizeof(query), "SELECT ROUND(aspect_ratio,2), 1, COUNT(*) AS count" " FROM main.images AS mi " " WHERE %s" " GROUP BY ROUND(aspect_ratio,2)" " ORDER BY ROUND(aspect_ratio,2) %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_COLORLABEL: // colorlabels // clang-format off g_snprintf(query, sizeof(query), "SELECT CASE color" " WHEN 0 THEN '%s'" " WHEN 1 THEN '%s'" " WHEN 2 THEN '%s'" " WHEN 3 THEN '%s'" " WHEN 4 THEN '%s'" " ELSE ''" " END, color, COUNT(*) AS count" " FROM main.images AS mi" " JOIN " " (SELECT imgid AS color_labels_id, color FROM main.color_labels)" " ON id = color_labels_id " " WHERE %s" " GROUP BY color" " ORDER BY color %s", _("red"), _("yellow"), _("green"), _("blue"), _("purple"), where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_LENS: // lens // clang-format off g_snprintf(query, sizeof(query), "SELECT CASE LOWER(TRIM(ln.name))" " WHEN 'n/a' THEN ''" " ELSE ln.name" " END AS lens, 1, COUNT(*) AS count" " FROM main.images AS mi, main.lens AS ln" " WHERE mi.lens_id = ln.id" " AND %s" " GROUP BY LOWER(lens)" " ORDER BY LOWER(lens) %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_WHITEBALANCE: // white balance // clang-format off g_snprintf(query, sizeof(query), "SELECT wb.name AS whitebalance, 1, COUNT(*) AS count" " FROM main.images AS mi, main.whitebalance AS wb" " WHERE mi.whitebalance_id = wb.id" " AND %s" " GROUP BY LOWER(whitebalance)" " ORDER BY LOWER(whitebalance) %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_FLASH: // white balance // clang-format off g_snprintf(query, sizeof(query), "SELECT fl.name AS flash, 1, COUNT(*) AS count" " FROM main.images AS mi, main.flash AS fl" " WHERE mi.flash_id = fl.id" " AND %s" " GROUP BY LOWER(flash)" " ORDER BY LOWER(flash) %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_EXPOSURE_PROGRAM: // exposure program // clang-format off g_snprintf(query, sizeof(query), "SELECT ep.name AS exposure_program, 1, COUNT(*) AS count" " FROM main.images AS mi, main.exposure_program AS ep" " WHERE mi.exposure_program_id = ep.id" " AND %s" " GROUP BY LOWER(exposure_program)" " ORDER BY LOWER(exposure_program) %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_METERING_MODE: // metering mode // clang-format off g_snprintf(query, sizeof(query), "SELECT mm.name AS metering_mode, 1, COUNT(*) AS count" " FROM main.images AS mi, main.metering_mode AS mm" " WHERE mi.metering_mode_id = mm.id" " AND %s" " GROUP BY LOWER(metering_mode)" " ORDER BY LOWER(metering_mode) %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_FOCAL_LENGTH: // focal length // clang-format off g_snprintf(query, sizeof(query), "SELECT CAST(focal_length AS INTEGER) AS focal_length, 1," " COUNT(*) AS count" " FROM main.images AS mi" " WHERE %s" " GROUP BY CAST(focal_length AS INTEGER)" " ORDER BY CAST(focal_length AS INTEGER) %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_ISO: // iso // clang-format off g_snprintf(query, sizeof(query), "SELECT CAST(iso AS INTEGER) AS iso, 1, COUNT(*) AS count" " FROM main.images AS mi" " WHERE %s" " GROUP BY iso" " ORDER BY iso %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_APERTURE: // aperture // clang-format off g_snprintf(query, sizeof(query), "SELECT ROUND(aperture,1) AS aperture, 1, COUNT(*) AS count" " FROM main.images AS mi" " WHERE %s" " GROUP BY aperture" " ORDER BY aperture %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_EXPOSURE: // exposure // clang-format off g_snprintf(query, sizeof(query), "SELECT CASE" " WHEN (exposure < 0.4) THEN '1/' || CAST(1/exposure + 0.9 AS INTEGER) " " ELSE ROUND(exposure,2) || '\"'" " END AS _exposure, 1, COUNT(*) AS count" " FROM main.images AS mi" " WHERE %s" " GROUP BY _exposure" " ORDER BY exposure %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_EXPOSURE_BIAS: // exposure bias // clang-format off g_snprintf(query, sizeof(query), "SELECT ROUND(exposure_bias,2), 1, COUNT(*) AS count" " FROM main.images AS mi" " WHERE %s" " GROUP BY ROUND(exposure_bias,2)" " ORDER BY ROUND(exposure_bias,2) %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_FILENAME: // filename // clang-format off g_snprintf(query, sizeof(query), "SELECT filename, 1, COUNT(*) AS count" " FROM main.images AS mi" " WHERE %s" " GROUP BY lower(filename)" " ORDER BY lower(filename) %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_GROUP_ID: // group_id // clang-format off g_snprintf(query, sizeof(query), "SELECT mi.group_id, 1, COUNT(*) AS count" " FROM main.images AS mi" " WHERE %s" " GROUP BY group_id" " HAVING COUNT(*) > 1" " ORDER BY group_id %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_MODULE: // module // clang-format off snprintf(query, sizeof(query), "SELECT m.name AS module_name, 1, COUNT(*) AS count" " FROM main.images AS mi" " JOIN (SELECT DISTINCT imgid, operation" " FROM main.history" " WHERE enabled = 1) AS h" " ON h.imgid = mi.id" " JOIN memory.darktable_iop_names AS m" " ON m.operation = h.operation" " WHERE %s" " GROUP BY lower(module_name)" " ORDER BY lower(module_name) %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; case DT_COLLECTION_PROP_ORDER: // modules order { char *orders = NULL; for(int i = 0; i < DT_IOP_ORDER_LAST; i++) { dt_util_str_cat(&orders, "WHEN mo.version = %d THEN '%s' ", i, _(dt_iop_order_string(i))); } dt_util_str_cat(&orders, "ELSE '%s' ", _("none")); // clang-format off snprintf(query, sizeof(query), "SELECT CASE %s END as ver, 1, COUNT(*) AS count" " FROM main.images AS mi" " LEFT JOIN (SELECT imgid, version FROM main.module_order) mo" " ON mo.imgid = mi.id" " WHERE %s" " GROUP BY ver" " ORDER BY ver %s", orders, where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on g_free(orders); } break; case DT_COLLECTION_PROP_RATING: // image rating { // clang-format off g_snprintf(query, sizeof(query), "SELECT CASE WHEN (flags & 8) == 8" " THEN -1 ELSE (flags & 7) END AS rating, 1," " COUNT(*) AS count" " FROM main.images AS mi" " WHERE %s" " GROUP BY rating" " ORDER BY rating %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on } break; case DT_COLLECTION_PROP_MONTH: // capture month // clang-format off g_snprintf(query, sizeof(query), "SELECT CAST(strftime('%%m', datetime_taken / 86400000000.0" " + julianday('0001-01-01')) AS INTEGER) AS month_num," " 1, COUNT(*) AS count" " FROM main.images AS mi" " WHERE datetime_taken IS NOT NULL AND datetime_taken <> 0" " AND %s" " GROUP BY month_num" " ORDER BY month_num %s", where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on break; default: if(property >= DT_COLLECTION_PROP_METADATA_OFFSET) { const int keyid = property - DT_COLLECTION_PROP_METADATA_OFFSET; // clang-format off snprintf(query, sizeof(query), "SELECT" " CASE WHEN value IS NULL THEN '%s' ELSE value END AS value," " 1, COUNT(*) AS count," " CASE WHEN value IS NULL THEN 0 ELSE 1 END AS force_order" " FROM main.images AS mi" " LEFT JOIN (SELECT id AS meta_data_id, value" " FROM main.meta_data" " WHERE key = %d)" " ON id = meta_data_id" " WHERE %s" " GROUP BY lower(value)" " ORDER BY force_order ASC, lower(value) %s", _("not defined"), keyid, where_ext, sort_descending ? "DESC" : "ASC"); // clang-format on } else // filmroll { gchar *order_by = NULL; const gboolean sort_by_import_time = dt_conf_is_equal("plugins/collect/filmroll_sort", "import time"); if(sort_by_import_time) { order_by = g_strdup(sort_descending ? "film_rolls_id DESC" : "film_rolls_id ASC"); } else { order_by = g_strdup(sort_descending ? "lower(folder) DESC" : "lower(folder) ASC"); } // clang-format off g_snprintf(query, sizeof(query), "SELECT folder, film_rolls_id, COUNT(*) AS count, status" " FROM main.images AS mi" " JOIN (SELECT fr.id AS film_rolls_id, folder, status" " FROM main.film_rolls AS fr" " JOIN memory.film_folder AS ff" " ON ff.id = fr.id)" " ON film_id = film_rolls_id " " WHERE %s" " GROUP BY folder" " ORDER BY %s", where_ext, order_by); // clang-format on g_free(order_by); } break; } g_free(where_ext); if(strlen(query) > 0) { DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL); GList *rows = NULL; while(sqlite3_step(stmt) == SQLITE_ROW) { const gchar *value = (gchar *)sqlite3_column_text(stmt, 0); const char *folder = (const char *)sqlite3_column_text(stmt, 0); const int count = sqlite3_column_int(stmt, 2); if(folder == NULL) continue; // safeguard against degenerated db entries int status = 0; if(property == DT_COLLECTION_PROP_FILMROLL) { folder = dt_image_film_roll_name(folder); status = !sqlite3_column_int(stmt, 3); filmroll_row_t *r = g_malloc(sizeof(*r)); r->folder = g_strdup(folder); r->value = g_strdup(value); r->id = sqlite3_column_int(stmt, 1); r->count = count; r->status = status; rows = g_list_prepend(rows, r); continue; } else if(property == DT_COLLECTION_PROP_RATING) { const int rating = sqlite3_column_int(stmt, 0); switch(rating) { case -1: folder = _("rejected"); break; case 0: folder = _("not rated"); break; case 1: folder = "★"; break; case 2: folder = "★★"; break; case 3: folder = "★★★"; break; case 4: folder = "★★★★"; break; case 5: folder = "★★★★★"; break; } } else if(property == DT_COLLECTION_PROP_MONTH) { const int month_num = sqlite3_column_int(stmt, 0); if(month_num >= 1 && month_num <= 12) folder = _(dt_month_names[month_num - 1]); else folder = _("unknown"); } // check if name is empty string gboolean only_spaces = TRUE; for(int k=0; k < strlen(folder); k++) { if(folder[k] != ' ' && folder[k] != '\t') { only_spaces = FALSE; break; } } if(only_spaces) { folder = _("unnamed"); value = _("unnamed"); } // replace invalid utf8 characters if any gchar *text = g_strdup(value); gchar *ptr = text; while(!g_utf8_validate(ptr, -1, (const gchar **)&ptr)) ptr[0] = '?'; gchar *escaped_text = g_markup_escape_text(text, -1); gtk_list_store_insert_with_values(GTK_LIST_STORE(model), NULL, -1, DT_LIB_COLLECT_COL_TEXT, folder, DT_LIB_COLLECT_COL_ID, sqlite3_column_int(stmt, 1), DT_LIB_COLLECT_COL_TOOLTIP, escaped_text, DT_LIB_COLLECT_COL_PATH, value, DT_LIB_COLLECT_COL_VISIBLE, TRUE, DT_LIB_COLLECT_COL_COUNT, count, DT_LIB_COLLECT_COL_UNREACHABLE, status, -1); g_free(text); g_free(escaped_text); } if(property == DT_COLLECTION_PROP_FILMROLL) { const gboolean sort_by_import_time = dt_conf_is_equal("plugins/collect/filmroll_sort", "import time"); const gboolean sort_by_folder_name = dt_conf_is_equal("plugins/collect/filmroll_sort", "folder name"); if(sort_by_import_time) { rows = g_list_sort(rows, _sort_filmroll_by_id); } else if(sort_by_folder_name) { rows = g_list_sort(rows, _sort_filmroll_rows); } else { rows = g_list_sort(rows, _sort_filmroll_by_display_name); } if(sort_descending) rows = g_list_reverse(rows); for(GList *l = rows; l; l = l->next) { filmroll_row_t *r = l->data; gchar *text = g_strdup(r->value); gchar *ptr = text; while(!g_utf8_validate(ptr, -1, (const gchar **)&ptr)) ptr[0] = '?'; gchar *escaped_text = g_markup_escape_text(text, -1); gtk_list_store_insert_with_values(GTK_LIST_STORE(model), NULL, -1, DT_LIB_COLLECT_COL_TEXT, r->folder, DT_LIB_COLLECT_COL_ID, r->id, DT_LIB_COLLECT_COL_TOOLTIP, escaped_text, DT_LIB_COLLECT_COL_PATH, r->value, DT_LIB_COLLECT_COL_VISIBLE, TRUE, DT_LIB_COLLECT_COL_COUNT, r->count, DT_LIB_COLLECT_COL_UNREACHABLE, r->status, -1); g_free(text); g_free(escaped_text); g_free(r->folder); g_free(r->value); g_free(r); } g_list_free(rows); rows = NULL; } sqlite3_finalize(stmt); } gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(d->view), DT_LIB_COLLECT_COL_TOOLTIP); d->listfilter = _create_filtered_model(model, dr); GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(d->view)); if(property == DT_COLLECTION_PROP_APERTURE || property == DT_COLLECTION_PROP_FOCAL_LENGTH || property == DT_COLLECTION_PROP_ISO || property == DT_COLLECTION_PROP_EXPOSURE || property == DT_COLLECTION_PROP_ASPECT_RATIO || property == DT_COLLECTION_PROP_RATING) { gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE); } else { gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE); } gtk_tree_view_set_model(GTK_TREE_VIEW(d->view), d->listfilter); gtk_widget_set_no_show_all(GTK_WIDGET(d->view), FALSE); gtk_widget_show_all(GTK_WIDGET(d->view)); g_object_unref(model); d->view_rule = property; } // if needed, we restrict the tree to matching entries if(dr->typing && (property == DT_COLLECTION_PROP_CAMERA || property == DT_COLLECTION_PROP_FILENAME || property == DT_COLLECTION_PROP_FILMROLL || property == DT_COLLECTION_PROP_LENS || property == DT_COLLECTION_PROP_APERTURE || property == DT_COLLECTION_PROP_FOCAL_LENGTH || property == DT_COLLECTION_PROP_ISO || property == DT_COLLECTION_PROP_MODULE || property == DT_COLLECTION_PROP_ORDER || property == DT_COLLECTION_PROP_RATING || property >= DT_COLLECTION_PROP_METADATA_OFFSET)) { gchar *needle = g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(dr->text)), -1); if(g_str_has_suffix(needle, "%")) needle[strlen(needle) - 1] = '\0'; dr->searchstring = needle; gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)list_match_string, dr); dr->searchstring = NULL; g_free(needle); } // we update list selection gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(d->view)); if(property == DT_COLLECTION_PROP_APERTURE || property == DT_COLLECTION_PROP_FOCAL_LENGTH || property == DT_COLLECTION_PROP_ISO || property == DT_COLLECTION_PROP_EXPOSURE || property == DT_COLLECTION_PROP_ASPECT_RATIO || property == DT_COLLECTION_PROP_RATING) { // test selection range [xxx;xxx] GRegex *regex; GMatchInfo *match_info; regex = g_regex_new("^\\s*\\[\\s*(.*)\\s*;\\s*(.*)\\s*\\]\\s*$", 0, 0, NULL); g_regex_match_full(regex, gtk_entry_get_text(GTK_ENTRY(dr->text)), -1, 0, 0, &match_info, NULL); const int match_count = g_match_info_get_match_count(match_info); if(match_count == 3) { _range_t *range = calloc(1, sizeof(_range_t)); range->start = g_match_info_fetch(match_info, 1); range->stop = g_match_info_fetch(match_info, 2); gtk_tree_model_foreach(d->listfilter, (GtkTreeModelForeachFunc)range_select, range); if(range->path1 && range->path2) { gtk_tree_selection_select_range(gtk_tree_view_get_selection(d->view), range->path1, range->path2); } g_free(range->start); g_free(range->stop); gtk_tree_path_free(range->path1); gtk_tree_path_free(range->path2); free(range); } else gtk_tree_model_foreach(d->listfilter, (GtkTreeModelForeachFunc)list_select, dr); g_match_info_free(match_info); g_regex_unref(regex); } else gtk_tree_model_foreach(d->listfilter, (GtkTreeModelForeachFunc)list_select, dr); } static void update_view(dt_lib_collect_rule_t *dr) { const int property = _combo_get_active_collection(dr->combo); if(property == DT_COLLECTION_PROP_FOLDERS || property == DT_COLLECTION_PROP_TAG || property == DT_COLLECTION_PROP_GEOTAGGING || property == DT_COLLECTION_PROP_DAY || _is_time_property(property)) _tree_view(dr); else _list_view(dr); } static void _set_tooltip(dt_lib_collect_rule_t *d) { const int property = _combo_get_active_collection(d->combo); if(property == DT_COLLECTION_PROP_APERTURE || property == DT_COLLECTION_PROP_FOCAL_LENGTH || property == DT_COLLECTION_PROP_ISO || property == DT_COLLECTION_PROP_ASPECT_RATIO || property == DT_COLLECTION_PROP_EXPOSURE) { gtk_widget_set_tooltip_text(d->text, _("use <, <=, >, >=, <>, =, [;] as operators")); } else if(property == DT_COLLECTION_PROP_RATING) { gtk_widget_set_tooltip_text(d->text, _("use <, <=, >, >=, <>, =, [;] as operators\n" "star rating: 0-5\n" "rejected images: -1")); } else if(property == DT_COLLECTION_PROP_DAY || _is_time_property(property)) { gtk_widget_set_tooltip_text (d->text, _("use <, <=, >, >=, <>, =, [;] as operators\n" "type dates in the form: YYYY:MM:DD hh:mm:ss.sss (only the year is mandatory)")); } else if(property == DT_COLLECTION_PROP_FILENAME) { gtk_widget_set_tooltip_text(d->text, /* xgettext:no-c-format */ _("use `%' as wildcard and `,' to separate values")); } else if(property == DT_COLLECTION_PROP_TAG) { gtk_widget_set_tooltip_text (d->text, /* xgettext:no-c-format */ _("use `%' as wildcard\n" "click to include hierarchy + sub-hierarchies (suffix `*')\n" "shift+click to include only the current hierarchy (no suffix)\n" "ctrl+click to include only sub-hierarchies (suffix `|%')")); } else if(property == DT_COLLECTION_PROP_GEOTAGGING) { gtk_widget_set_tooltip_text (d->text, /* xgettext:no-c-format */ _("use `%' as wildcard\n" "click to include location + sub-locations (suffix `*')\n" "shift+click to include only the current location (no suffix)\n" "ctrl+click to include only sub-locations (suffix `|%')")); } else if(property == DT_COLLECTION_PROP_FOLDERS) { gtk_widget_set_tooltip_text (d->text, /* xgettext:no-c-format */ _("use `%' as wildcard\n" "click to include current + sub-folders (suffix `*')\n" "shift+click to include only the current folder (no suffix)\n" "ctrl+click to include only sub-folders (suffix `|%')")); } else { /* xgettext:no-c-format */ gtk_widget_set_tooltip_text(d->text, _("use `%' as wildcard")); } //set the combobox tooltip as well gchar *tip = gtk_widget_get_tooltip_text(d->text); gtk_widget_set_tooltip_text(GTK_WIDGET(d->combo), tip); g_free(tip); } void view_enter(struct dt_lib_module_t *self, struct dt_view_t *old_view, struct dt_view_t *new_view) { dt_lib_collect_t *d = self->data; const gboolean hide = dt_lib_is_visible(darktable.view_manager->proxy.module_recentcollect.module); gtk_widget_set_visible(d->history_button, !hide); } void gui_update(dt_lib_module_t *self) { dt_lib_collect_t *d = self->data; update_view(d->rule + d->active_rule); dt_gui_widget_reallocate_now(GTK_WIDGET(d->view)); } static void _lib_collect_gui_update(dt_lib_module_t *self) { dt_lib_collect_t *const d = self->data; // we check if something has changed since last call if(d->view_rule != -1) return; DT_ENTER_GUI_UPDATE(); const int _a = dt_conf_get_int("plugins/lighttable/collect/num_rules") - 1; const int active = CLAMP(_a, 0, (MAX_RULES - 1)); d->nb_rules = active + 1; char confname[200] = { 0 }; gtk_widget_set_no_show_all(GTK_WIDGET(d->view), TRUE); for(int i = 0; i < MAX_RULES; i++) { gtk_widget_set_no_show_all(d->rule[i].hbox, TRUE); gtk_widget_set_visible(d->rule[i].hbox, FALSE); } for(int i = 0; i <= active; i++) { gtk_widget_set_no_show_all(d->rule[i].hbox, FALSE); gtk_widget_set_visible(d->rule[i].hbox, TRUE); gtk_widget_show_all(d->rule[i].hbox); snprintf(confname, sizeof(confname), "plugins/lighttable/collect/item%1d", i); _combo_set_active_collection(d->rule[i].combo, dt_conf_get_int(confname)); snprintf(confname, sizeof(confname), "plugins/lighttable/collect/string%1d", i); const char *text = dt_conf_get_string_const(confname); if(text) { g_signal_handlers_block_matched(d->rule[i].text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL); gtk_entry_set_text(GTK_ENTRY(d->rule[i].text), text); gtk_editable_set_position(GTK_EDITABLE(d->rule[i].text), -1); g_signal_handlers_unblock_matched(d->rule[i].text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL); d->rule[i].typing = FALSE; } GtkDarktableButton *button = DTGTK_BUTTON(d->rule[i].button); if(i == MAX_RULES - 1) { // only clear button->icon = dtgtk_cairo_paint_cancel; gtk_widget_set_tooltip_text(GTK_WIDGET(button), _("clear this rule")); } else if(i == active) { gtk_widget_set_tooltip_text(GTK_WIDGET(button), _("clear this rule or add new rules")); dtgtk_button_set_paint(button, dtgtk_cairo_paint_solid_arrow, CPF_DIRECTION_DOWN, NULL); } else { snprintf(confname, sizeof(confname), "plugins/lighttable/collect/mode%1d", i + 1); const int mode = dt_conf_get_int(confname); if(mode == DT_LIB_COLLECT_MODE_AND) button->icon = dtgtk_cairo_paint_intersection; else if(mode == DT_LIB_COLLECT_MODE_OR) button->icon = dtgtk_cairo_paint_union; else if(mode == DT_LIB_COLLECT_MODE_AND_NOT) button->icon = dtgtk_cairo_paint_andnot; gtk_widget_set_tooltip_text(GTK_WIDGET(button), _("clear this rule")); } _set_tooltip(d->rule + i); } // update list of proposals if the module's contents are visible d->active_rule = active; dt_lib_gui_queue_update(self); DT_LEAVE_GUI_UPDATE(); } void gui_reset(dt_lib_module_t *self) { dt_conf_set_int("plugins/lighttable/collect/num_rules", 1); dt_conf_set_int("plugins/lighttable/collect/item0", DT_COLLECTION_PROP_FILMROLL); dt_conf_set_int("plugins/lighttable/collect/mode0", 0); dt_conf_set_string("plugins/lighttable/collect/string0", ""); dt_lib_collect_t *d = self->data; d->active_rule = 0; d->view_rule = -1; dt_collection_set_query_flags(darktable.collection, COLLECTION_QUERY_FULL); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, NULL); } // Returns TRUE if the folder still corresponds to at least one film roll, // either directly or as an ancestor of one. This mirrors the folders tree, // where a node exists as long as a film roll lives at or below it. static gboolean _folder_exists(const char *folder) { if(!folder || !*folder) return FALSE; gboolean exists = FALSE; sqlite3_stmt *stmt = NULL; gchar *child = g_strconcat(folder, G_DIR_SEPARATOR_S, "%", NULL); DT_DEBUG_SQLITE3_PREPARE_V2 (dt_database_get(darktable.db), "SELECT 1 FROM main.film_rolls WHERE folder = ?1 OR folder LIKE ?2 LIMIT 1", -1, &stmt, NULL); DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, folder, -1, SQLITE_TRANSIENT); DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 2, child, -1, SQLITE_TRANSIENT); if(sqlite3_step(stmt) == SQLITE_ROW) exists = TRUE; sqlite3_finalize(stmt); g_free(child); return exists; } // Resolve a folder path to the film roll that best corresponds to it, so // that switching from the folders to the film roll view keeps the user near // where they were instead of forcing them to retraverse the tree. // Returns a newly allocated film roll folder path, or NULL if none exists. // Preference order: an exact match (1:1), else the closest film roll that // contains the folder (nearest ancestor), else the closest film roll // contained in the folder (nearest descendant). static gchar *_filmroll_for_folder(const char *folder) { if(!folder || !*folder) return NULL; gchar *result = NULL; sqlite3_stmt *stmt = NULL; // 1:1 exact match DT_DEBUG_SQLITE3_PREPARE_V2 (dt_database_get(darktable.db), "SELECT folder FROM main.film_rolls WHERE folder = ?1", -1, &stmt, NULL); DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, folder, -1, SQLITE_TRANSIENT); if(sqlite3_step(stmt) == SQLITE_ROW) result = g_strdup((const char *)sqlite3_column_text(stmt, 0)); sqlite3_finalize(stmt); if(result) return result; // closest containing film roll (nearest ancestor) DT_DEBUG_SQLITE3_PREPARE_V2 (dt_database_get(darktable.db), "SELECT folder FROM main.film_rolls" " WHERE ?1 LIKE folder || ?2" " ORDER BY LENGTH(folder) DESC LIMIT 1", -1, &stmt, NULL); DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, folder, -1, SQLITE_TRANSIENT); DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 2, G_DIR_SEPARATOR_S "%", -1, SQLITE_TRANSIENT); if(sqlite3_step(stmt) == SQLITE_ROW) result = g_strdup((const char *)sqlite3_column_text(stmt, 0)); sqlite3_finalize(stmt); if(result) return result; // closest contained film roll (nearest descendant) gchar *child_pattern = g_strconcat(folder, G_DIR_SEPARATOR_S, "%", NULL); DT_DEBUG_SQLITE3_PREPARE_V2 (dt_database_get(darktable.db), "SELECT folder FROM main.film_rolls" " WHERE folder LIKE ?1" " ORDER BY LENGTH(folder) ASC LIMIT 1", -1, &stmt, NULL); DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, child_pattern, -1, SQLITE_TRANSIENT); if(sqlite3_step(stmt) == SQLITE_ROW) result = g_strdup((const char *)sqlite3_column_text(stmt, 0)); sqlite3_finalize(stmt); g_free(child_pattern); return result; } // Strip the folders-collection suffixes (`*`, `|%`, trailing separator or // `%`) from a rule value to recover a plain filesystem path. Returns a // newly allocated string. static gchar *_folder_plain_path(const char *text) { gchar *folder = g_strdup(text ? text : ""); size_t len = strlen(folder); if(len && folder[len - 1] == '*') folder[--len] = '\0'; if(len >= 2 && g_str_has_suffix(folder, "|%")) { len -= 2; folder[len] = '\0'; } while(len && (folder[len - 1] == G_DIR_SEPARATOR || folder[len - 1] == '%')) folder[--len] = '\0'; return folder; } // When a filesystem operation (move/delete/erase) removes the film roll or // folder the active rule points at, repoint it at the closest still existing // entry instead of letting the collection module collapse back to the tree // root. The selection is left untouched while its target still exists. // Returns TRUE if the rule's stored value was changed. static gboolean _fixup_stale_selection(dt_lib_collect_t *d) { const int active = d->active_rule; const int property = _combo_get_active_collection(d->rule[active].combo); if(property != DT_COLLECTION_PROP_FILMROLL && property != DT_COLLECTION_PROP_FOLDERS) return FALSE; char confname[200] = { 0 }; snprintf(confname, sizeof(confname), "plugins/lighttable/collect/string%1d", active); gchar *text = dt_conf_get_string(confname); if(!text || !text[0]) { g_free(text); return FALSE; } // a restored rule may have changed property before the combobox refreshes char itemname[200]; snprintf(itemname, sizeof(itemname), "plugins/lighttable/collect/item%1d", active); if(property != dt_conf_get_int(itemname)) { g_free(text); return FALSE; } gchar *folder = _folder_plain_path(text); gboolean changed = FALSE; // a folder node survives as long as a film roll lives at or under it, so we // only repoint when it has vanished entirely; film rolls are an exact match // and _filmroll_for_folder() returns the path unchanged while it exists. if(property != DT_COLLECTION_PROP_FOLDERS || !_folder_exists(folder)) { gchar *closest = _filmroll_for_folder(folder); if(g_strcmp0(closest, folder) != 0) { dt_conf_set_string(confname, closest ? closest : ""); changed = TRUE; } g_free(closest); } g_free(folder); g_free(text); return changed; } static void combo_changed(GtkWidget *combo, dt_lib_collect_rule_t *d) { DT_GUARD_GUI_UPDATE(); dt_lib_collect_t *c = get_collect(d); c->active_rule = d->num; const int property = _combo_get_active_collection(d->combo); // When switching between the film roll and folders views, both store the // full folder path as their value, so we carry the current selection over // instead of forcing the user to retraverse the tree to find where they // were. // - film roll -> folders: keep the path verbatim (same exact-match query, // the displayed collection is left unchanged and the tree opens there). // - folders -> film roll: resolve the folder to the closest film roll // (1:1 match, else nearest containing/contained film roll). char confname[200] = { 0 }; snprintf(confname, sizeof(confname), "plugins/lighttable/collect/item%1d", d->num); const int prev_property = dt_conf_get_int(confname); const gchar *cur_text = gtk_entry_get_text(GTK_ENTRY(d->text)); const gboolean has_text = cur_text && cur_text[0]; const gboolean keep_path = prev_property == DT_COLLECTION_PROP_FILMROLL && property == DT_COLLECTION_PROP_FOLDERS && has_text; gchar *resolved_filmroll = NULL; if(prev_property == DT_COLLECTION_PROP_FOLDERS && property == DT_COLLECTION_PROP_FILMROLL && has_text) { gchar *folder = _folder_plain_path(cur_text); resolved_filmroll = _filmroll_for_folder(folder); g_free(folder); } g_signal_handlers_block_matched(d->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL); if(resolved_filmroll) gtk_entry_set_text(GTK_ENTRY(d->text), resolved_filmroll); else if(!keep_path) gtk_entry_set_text(GTK_ENTRY(d->text), ""); g_signal_handlers_unblock_matched(d->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL); g_free(resolved_filmroll); if(property == DT_COLLECTION_PROP_FOLDERS || property == DT_COLLECTION_PROP_TAG || property == DT_COLLECTION_PROP_GEOTAGGING || property == DT_COLLECTION_PROP_DAY || _is_time_property(property)) { d->typing = FALSE; } _set_tooltip(d); gchar *order = NULL; if(c->active_rule == 0) { if(prev_property != DT_COLLECTION_PROP_TAG && property == DT_COLLECTION_PROP_TAG) { // save global order char buf[4096] = { 0 }; dt_collection_sort_serialize(buf, sizeof(buf)); dt_conf_set_string("plugins/lighttable/collect/lastorder", buf); } else if(prev_property == DT_COLLECTION_PROP_TAG && property != DT_COLLECTION_PROP_TAG) { // restore global order order = dt_conf_get_string("plugins/lighttable/collect/lastorder"); dt_collection_set_tag_id((dt_collection_t *)darktable.collection, 0); } } set_properties(d); c->view_rule = -1; if(order) DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_IMAGES_ORDER_CHANGE, order); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, NULL); } static void row_activated_with_event(GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *col, GdkEventButton *event, dt_lib_collect_t *d) { GtkTreeIter iter; GtkTreeModel *model = NULL; GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view)); if(gtk_tree_selection_count_selected_rows(selection) < 1) return; GList *sels = gtk_tree_selection_get_selected_rows(selection, &model); GtkTreePath *path1 = (GtkTreePath *)sels->data; if(!gtk_tree_model_get_iter(model, &iter, path1)) { g_list_free_full(sels, (GDestroyNotify)gtk_tree_path_free); return; } gchar *text; gchar *order = NULL; const int active = d->active_rule; d->rule[active].typing = FALSE; gboolean force_update_view = FALSE; const int item = _combo_get_active_collection(d->rule[active].combo); gtk_tree_model_get(model, &iter, DT_LIB_COLLECT_COL_PATH, &text, -1); if(text && strlen(text) > 0) { if(dt_modifier_is(dt_gdk_event_get_state(event), GDK_SHIFT_MASK | GDK_CONTROL_MASK)) { if(item == DT_COLLECTION_PROP_FILMROLL) { // go to corresponding folder collection _combo_set_active_collection(d->rule[active].combo, DT_COLLECTION_PROP_FOLDERS); } else if(item == DT_COLLECTION_PROP_FOLDERS) { // go to corresponding filmroll collection _combo_set_active_collection(d->rule[active].combo, DT_COLLECTION_PROP_FILMROLL); force_update_view = TRUE; } } else if(gtk_tree_selection_count_selected_rows(selection) > 1 && (item == DT_COLLECTION_PROP_DAY || _is_time_property(item) || item == DT_COLLECTION_PROP_APERTURE || item == DT_COLLECTION_PROP_FOCAL_LENGTH || item == DT_COLLECTION_PROP_ISO || item == DT_COLLECTION_PROP_EXPOSURE || item == DT_COLLECTION_PROP_ASPECT_RATIO || item == DT_COLLECTION_PROP_RATING)) { /* this is a range selection */ GtkTreeIter iter2; GtkTreePath *path2 = (GtkTreePath *)g_list_last(sels)->data; if(!gtk_tree_model_get_iter(model, &iter2, path2)) return; gchar *text2; gtk_tree_model_get(model, &iter2, DT_LIB_COLLECT_COL_PATH, &text2, -1); gchar *n_text; const gboolean sort_descending = dt_conf_get_bool("plugins/collect/descending"); if(sort_descending) n_text = g_strdup_printf("[%s;%s]", text2, text); else n_text = g_strdup_printf("[%s;%s]", text, text2); g_free(text); g_free(text2); text = n_text; } else if(item == DT_COLLECTION_PROP_TAG || item == DT_COLLECTION_PROP_GEOTAGGING || item == DT_COLLECTION_PROP_FOLDERS) { if(gtk_tree_model_iter_has_child(model, &iter)) { /* if a tag has children, ctrl-clicking on a parent node * should display all images under this hierarchy. */ if(dt_modifier_is(dt_gdk_event_get_state(event), GDK_CONTROL_MASK)) { gchar *n_text = g_strconcat(text, "|%", NULL); g_free(text); text = n_text; } /* if a tag has children, left-clicking on a parent node * should display all images in and under this hierarchy. */ else if(!dt_modifier_is(dt_gdk_event_get_state(event), GDK_SHIFT_MASK)) { gchar *n_text = g_strconcat(text, "*", NULL); g_free(text); text = n_text; } } else if(active == 0 && g_strcmp0(text, _("not tagged"))) { // // first filter is tag and the row is a leave const uint32_t tagid = dt_tag_get_tag_id_by_name(text); if(tagid) dt_collection_set_tag_id((dt_collection_t *)darktable.collection, tagid); else dt_collection_set_tag_id((dt_collection_t *)darktable.collection, 0); } } } g_list_free_full(sels, (GDestroyNotify)gtk_tree_path_free); g_signal_handlers_block_matched(d->rule[active].text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL); gtk_entry_set_text(GTK_ENTRY(d->rule[active].text), text); gtk_editable_set_position(GTK_EDITABLE(d->rule[active].text), -1); g_signal_handlers_unblock_matched(d->rule[active].text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL); g_free(text); if(item == DT_COLLECTION_PROP_TAG || (item == DT_COLLECTION_PROP_FOLDERS && !force_update_view) || item == DT_COLLECTION_PROP_DAY || _is_time_property(item) || item == DT_COLLECTION_PROP_COLORLABEL || item == DT_COLLECTION_PROP_GEOTAGGING || item == DT_COLLECTION_PROP_HISTORY || item == DT_COLLECTION_PROP_LOCAL_COPY) { set_properties(d->rule + active); // we just have to set the selection } else update_view(d->rule + active); // we have to update visible items too dt_control_signal_block_by_func(darktable.signals, G_CALLBACK(collection_updated), darktable.view_manager->proxy.module_collect.module); if(order) DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_IMAGES_ORDER_CHANGE, order); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, NULL); dt_control_signal_unblock_by_func(darktable.signals, G_CALLBACK(collection_updated), darktable.view_manager->proxy.module_collect.module); gtk_widget_grab_focus(dt_ui_center(darktable.gui->ui)); dt_control_queue_redraw_center(); } static void entry_activated(GtkWidget *entry, dt_lib_collect_rule_t *d) { GtkTreeView *view; GtkTreeModel *model; int rows; update_view(d); dt_lib_collect_t *c = get_collect(d); const int property = _combo_get_active_collection(d->combo); if(property != DT_COLLECTION_PROP_FOLDERS && property != DT_COLLECTION_PROP_TAG && property != DT_COLLECTION_PROP_GEOTAGGING && property != DT_COLLECTION_PROP_DAY && !_is_time_property(property)) { view = c->view; model = gtk_tree_view_get_model(GTK_TREE_VIEW(view)); rows = gtk_tree_model_iter_n_children(model, NULL); // if only one row, press enter in entry box to fill it with the row value if(rows == 1) { GtkTreeIter iter; if(gtk_tree_model_get_iter_first(model, &iter)) { gchar *text; gtk_tree_model_get(model, &iter, DT_LIB_COLLECT_COL_PATH, &text, -1); g_signal_handlers_block_matched(d->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL); gtk_entry_set_text(GTK_ENTRY(d->text), text); gtk_editable_set_position(GTK_EDITABLE(d->text), -1); g_signal_handlers_unblock_matched(d->text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL); g_free(text); update_view(d); } } } dt_control_signal_block_by_func(darktable.signals, G_CALLBACK(collection_updated), darktable.view_manager->proxy.module_collect.module); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, NULL); dt_control_signal_unblock_by_func(darktable.signals, G_CALLBACK(collection_updated), darktable.view_manager->proxy.module_collect.module); d->typing = FALSE; dt_control_queue_redraw_center(); } static void entry_changed(GtkEntry *entry, dt_lib_collect_rule_t *dr) { dr->typing = TRUE; update_view(dr); } int position(const dt_lib_module_t *self) { return 400; } static void entry_focus_in_callback(GtkEventController *controller, dt_lib_collect_rule_t *d) { dt_lib_collect_t *c = get_collect(d); if(c->active_rule != d->num) { c->active_rule = d->num; update_view(c->rule + c->active_rule); } } static gboolean _process_variant_params(GVariant *parameter, gpointer userdata, dt_lib_collect_mode_t *mode, dt_lib_collect_rule_t **d) { const gsize nb = g_variant_n_children(parameter); if(nb != 2) return FALSE; dt_lib_collect_t *m = (dt_lib_collect_t *)userdata; GVariant *v = g_variant_get_child_value(parameter, 0); *mode = g_variant_get_int32(v); g_variant_unref(v); v = g_variant_get_child_value(parameter, 1); const int rule_index = g_variant_get_int32(v); g_variant_unref(v); *d = &m->rule[rule_index]; return TRUE; } static void _menuitem_mode(GSimpleAction *action, GVariant *parameter, gpointer userdata) { dt_lib_collect_mode_t mode; dt_lib_collect_rule_t *d = NULL; if(!_process_variant_params(parameter, userdata, &mode, &d)) return; // add next row with and operator const int _a = dt_conf_get_int("plugins/lighttable/collect/num_rules"); const int active = CLAMP(_a, 1, MAX_RULES); if(active < MAX_RULES) { char confname[200] = { 0 }; snprintf(confname, sizeof(confname), "plugins/lighttable/collect/mode%1d", active); dt_conf_set_int(confname, mode); snprintf(confname, sizeof(confname), "plugins/lighttable/collect/string%1d", active); dt_conf_set_string(confname, ""); dt_conf_set_int("plugins/lighttable/collect/num_rules", active + 1); dt_lib_collect_t *c = get_collect(d); c->active_rule = active; c->view_rule = -1; } dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, NULL); } static void _menuitem_mode_change(GSimpleAction *action, GVariant *parameter, gpointer userdata) { dt_lib_collect_mode_t mode; dt_lib_collect_rule_t *d = NULL; if(!_process_variant_params(parameter, userdata, &mode, &d)) return; // add next row with and operator const int num = d->num + 1; if(num < MAX_RULES && num > 0) { char confname[200] = { 0 }; snprintf(confname, sizeof(confname), "plugins/lighttable/collect/mode%1d", num); dt_conf_set_int(confname, mode); } dt_lib_collect_t *c = get_collect(d); c->view_rule = -1; dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, NULL); } static void _navigation_update(dt_lib_collect_t *d) { gtk_widget_set_sensitive(d->back, dt_collection_navigation_available(-1)); gtk_widget_set_sensitive(d->forward, dt_collection_navigation_available(1)); } static void _navigation_changed(gpointer instance, const dt_collection_change_t query_change, const dt_collection_properties_t changed_property, gpointer imgs, const int next, gpointer self) { _navigation_update(((dt_lib_module_t *)self)->data); } static void _navigation_back(GtkWidget *widget, gpointer user_data) { dt_collection_navigation_go(-1); _navigation_update(((dt_lib_module_t *)user_data)->data); } static void _navigation_forward(GtkWidget *widget, gpointer user_data) { dt_collection_navigation_go(1); _navigation_update(((dt_lib_module_t *)user_data)->data); } static void collection_updated(gpointer instance, const dt_collection_change_t query_change, const dt_collection_properties_t changed_property, gpointer imgs, const int next, gpointer self) { dt_lib_module_t *dm = (dt_lib_module_t *)self; dt_lib_collect_t *d = dm->data; // update tree d->view_rule = -1; d->rule[d->active_rule].typing = FALSE; // if a move/delete/erase removed the selected film roll/folder, repoint the // rule at the closest survivor so we don't collapse back to the tree root const gboolean repointed = _fixup_stale_selection(d); // determine if we want to refresh the tree or not gboolean refresh = TRUE; if(!repointed && query_change == DT_COLLECTION_CHANGE_RELOAD && changed_property != DT_COLLECTION_PROP_UNDEF) { // if we only reload the collection, that means that we don't // change the query itself so we only rebuild the treeview if a // used property has changed refresh = FALSE; for(int i = 0; i <= d->active_rule; i++) { const int item = _combo_get_active_collection(d->rule[i].combo); if(item == changed_property) { refresh = TRUE; break; } } } if(refresh) _lib_collect_gui_update(self); if(repointed) { // the collection was queried from the now-stale rule; re-run it from the // corrected value so the displayed images follow the new selection. Block // ourselves to avoid recursing through this same handler. dt_control_signal_block_by_func(darktable.signals, G_CALLBACK(collection_updated), darktable.view_manager->proxy.module_collect.module); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, NULL); dt_control_signal_unblock_by_func(darktable.signals, G_CALLBACK(collection_updated), darktable.view_manager->proxy.module_collect.module); } } static void filmrolls_updated(gpointer instance, gpointer self) { // TODO: We should update the count of images here _lib_collect_gui_update(self); } static void filmrolls_imported(gpointer instance, const int film_id, gpointer self) { dt_lib_module_t *dm = (dt_lib_module_t *)self; dt_lib_collect_t *d = dm->data; // update tree d->view_rule = -1; d->rule[d->active_rule].typing = FALSE; _lib_collect_gui_update(self); } static void preferences_changed(gpointer instance, gpointer self) { dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF, NULL); } static void filmrolls_removed(gpointer instance, gpointer self) { dt_lib_module_t *dm = (dt_lib_module_t *)self; dt_lib_collect_t *d = dm->data; // update tree if(d->view_rule != DT_COLLECTION_PROP_FOLDERS) { d->view_rule = -1; } d->rule[d->active_rule].typing = FALSE; _lib_collect_gui_update(self); } static void tag_changed(gpointer instance, gpointer self) { dt_lib_module_t *dm = (dt_lib_module_t *)self; dt_lib_collect_t *d = dm->data; // update tree if(_combo_get_active_collection(d->rule[d->active_rule].combo) == DT_COLLECTION_PROP_TAG) { d->view_rule = -1; d->rule[d->active_rule].typing = FALSE; // need to reload collection since we have tags as active collection filter dt_control_signal_block_by_func(darktable.signals, G_CALLBACK(collection_updated), darktable.view_manager->proxy.module_collect.module); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_TAG, NULL); dt_control_signal_unblock_by_func(darktable.signals, G_CALLBACK(collection_updated), darktable.view_manager->proxy.module_collect.module); } else { // currently tag filter isn't the one selected but it might be in // one of rules. needs check gboolean needs_update = FALSE; for(int i = 0; i < d->nb_rules && !needs_update; i++) { needs_update = needs_update || _combo_get_active_collection(d->rule[i].combo) == DT_COLLECTION_PROP_TAG; } if(needs_update) { // we have tags as one of rules, needs reload. dt_control_signal_block_by_func (darktable.signals, G_CALLBACK(collection_updated), darktable.view_manager->proxy.module_collect.module); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_TAG, NULL); dt_control_signal_unblock_by_func (darktable.signals, G_CALLBACK(collection_updated), darktable.view_manager->proxy.module_collect.module); } } } static void _geotag_changed(gpointer instance, GList *imgs, const int locid, gpointer self) { // if locid <> NULL this event doesn't concern collect module if(!locid) { dt_lib_module_t *dm = (dt_lib_module_t *)self; dt_lib_collect_t *d = dm->data; // update tree if(_combo_get_active_collection(d->rule[d->active_rule].combo) == DT_COLLECTION_PROP_GEOTAGGING) { d->view_rule = -1; d->rule[d->active_rule].typing = FALSE; _lib_collect_gui_update(self); //need to reload collection since we have geotags as active collection filter dt_control_signal_block_by_func (darktable.signals, G_CALLBACK(collection_updated), darktable.view_manager->proxy.module_collect.module); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_GEOTAGGING, NULL); dt_control_signal_unblock_by_func (darktable.signals, G_CALLBACK(collection_updated), darktable.view_manager->proxy.module_collect.module); } } } static void _metadata_changed(gpointer instance, int type, gpointer self) { dt_lib_module_t *dm = (dt_lib_module_t *)self; dt_lib_collect_t *d = dm->data; if(type == DT_METADATA_SIGNAL_PREF_CHANGED) { // metadata preferences have changed - update the collection list for(int i = 0; i < MAX_RULES; i++) { g_signal_handlers_block_matched(d->rule[i].combo, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, combo_changed, NULL); const int property = _combo_get_active_collection(d->rule[i].combo); dt_bauhaus_combobox_clear(d->rule[i].combo); _populate_collect_combo(d->rule[i].combo); if(property != -1 && !_combo_set_active_collection(d->rule[i].combo, property)) { // this one has been hidden - remove entry g_signal_handlers_block_matched(d->rule[i].text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL); gtk_entry_set_text(GTK_ENTRY(d->rule[i].text), ""); g_signal_handlers_unblock_matched(d->rule[i].text, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, entry_changed, NULL); d->rule[i].typing = FALSE; set_properties(&d->rule[i]); } g_signal_handlers_unblock_matched(d->rule[i].combo, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, combo_changed, NULL); } } // update collection if metadata have been hidden or a metadata collection is active const int prop = _combo_get_active_collection(d->rule[d->active_rule].combo); if(type == DT_METADATA_SIGNAL_PREF_CHANGED || prop >= DT_COLLECTION_PROP_METADATA_OFFSET) { dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_METADATA, NULL); } } static void _menuitem_clear(GSimpleAction *simple, GVariant *parameter, gpointer userdata) { dt_lib_collect_t *m = (dt_lib_collect_t*) userdata; const int index = g_variant_get_int32(parameter); dt_lib_collect_rule_t *d = &m->rule[index]; // remove this row, or if 1st, clear text entry box const int _a = dt_conf_get_int("plugins/lighttable/collect/num_rules"); const int active = CLAMP(_a, 1, MAX_RULES); dt_lib_collect_t *c = get_collect(d); if(active > 1) { dt_conf_set_int("plugins/lighttable/collect/num_rules", active - 1); if(c->active_rule >= active - 1) c->active_rule = active - 2; } else { dt_conf_set_int("plugins/lighttable/collect/mode0", DT_LIB_COLLECT_MODE_AND); dt_conf_set_int("plugins/lighttable/collect/item0", 0); dt_conf_set_string("plugins/lighttable/collect/string0", ""); d->typing = FALSE; } // move up all still active rules by one. for(int i = d->num; i < MAX_RULES - 1; i++) { char confname[200] = { 0 }; snprintf(confname, sizeof(confname), "plugins/lighttable/collect/mode%1d", i + 1); const int mode = dt_conf_get_int(confname); snprintf(confname, sizeof(confname), "plugins/lighttable/collect/item%1d", i + 1); const int item = dt_conf_get_int(confname); snprintf(confname, sizeof(confname), "plugins/lighttable/collect/string%1d", i + 1); gchar *string = dt_conf_get_string(confname); if(string) { snprintf(confname, sizeof(confname), "plugins/lighttable/collect/mode%1d", i); dt_conf_set_int(confname, mode); snprintf(confname, sizeof(confname), "plugins/lighttable/collect/item%1d", i); dt_conf_set_int(confname, item); snprintf(confname, sizeof(confname), "plugins/lighttable/collect/string%1d", i); dt_conf_set_string(confname, string); g_free(string); } } c->view_rule = -1; dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, NULL); } static gboolean _popup_menu_idle(gpointer user_data) { gtk_popover_popup(GTK_POPOVER(user_data)); return G_SOURCE_REMOVE; } static void popup_button_callback_cb(GtkGestureSingle *gesture, int n_press, double x, double y, dt_lib_collect_rule_t *d) { if(gtk_gesture_single_get_current_button(gesture) != GDK_BUTTON_PRIMARY) return; GMenu *menu = g_menu_new(); const int _a = dt_conf_get_int("plugins/lighttable/collect/num_rules"); const int active = CLAMP(_a, 1, MAX_RULES); gchar *action; action = g_strdup_printf("collect.clear(%d)", d->num); g_menu_append(menu, _("clear this rule"), action); g_free(action); if(d->num == active - 1) { const char *fmt = "collect.mode((%d,%d))"; action = g_strdup_printf(fmt, DT_LIB_COLLECT_MODE_AND, d->num); g_menu_append(menu, _("narrow down search"), action); g_free(action); action = g_strdup_printf(fmt, DT_LIB_COLLECT_MODE_OR, d->num); g_menu_append(menu, _("add more images"), action); g_free(action); action = g_strdup_printf(fmt, DT_LIB_COLLECT_MODE_AND_NOT, d->num); g_menu_append(menu, _("exclude images"), action); g_free(action); } else if(d->num < active - 1) { const char *fmt = "collect.modechange((%d,%d))"; action = g_strdup_printf(fmt, DT_LIB_COLLECT_MODE_AND, d->num); g_menu_append(menu, _("change to: and"), action); g_free(action); action = g_strdup_printf(fmt, DT_LIB_COLLECT_MODE_OR, d->num); g_menu_append(menu, _("change to: or"), action); g_free(action); action = g_strdup_printf(fmt, DT_LIB_COLLECT_MODE_AND_NOT, d->num); g_menu_append(menu, _("change to: except"), action); g_free(action); } GtkWidget *popover_menu = dt_gui_popover_menu_from_model(d->button, menu); g_object_unref(menu); // the click gesture still holds the implicit grab while this handler runs, // which would keep the popover from taking its own grab; defer the popup // to the next main loop iteration g_idle_add(_popup_menu_idle, popover_menu); } static void view_set_click(gpointer instance, dt_lib_module_t *self) { dt_lib_collect_t *d = self->data; d->singleclick = dt_conf_get_bool("plugins/lighttable/collect/single-click"); } static void _populate_collect_combo(GtkWidget *w) { #define ADD_COLLECT_ENTRY(value) \ dt_bauhaus_combobox_add_full(w, dt_collection_name(value), \ DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT, \ GUINT_TO_POINTER(value + 1), NULL, TRUE) dt_bauhaus_combobox_add_section(w, _("files")); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FILMROLL); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FOLDERS); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FILENAME); dt_bauhaus_combobox_add_section(w, _("metadata")); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_TAG); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_RATING); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_COLORLABEL); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_GEOTAGGING); dt_pthread_mutex_lock(&darktable.metadata_threadsafe); for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next) { dt_metadata_t *metadata = iter->data; if(!metadata->internal && metadata->visible) // metadata name is user defined, so no localization here dt_bauhaus_combobox_add_full(w, metadata->name, DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT, GUINT_TO_POINTER(DT_COLLECTION_PROP_METADATA_OFFSET + metadata->key + 1), NULL, TRUE); } dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); dt_bauhaus_combobox_add_section(w, _("times")); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_DAY); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_MONTH); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_TIME); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_IMPORT_TIMESTAMP); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_CHANGE_TIMESTAMP); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_EXPORT_TIMESTAMP); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_PRINT_TIMESTAMP); dt_bauhaus_combobox_add_section(w, _("capture details")); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_CAMERA); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_LENS); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_APERTURE); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_EXPOSURE); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_EXPOSURE_BIAS); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FOCAL_LENGTH); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_ISO); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_ASPECT_RATIO); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_WHITEBALANCE); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FLASH); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_EXPOSURE_PROGRAM); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_METERING_MODE); dt_bauhaus_combobox_add_section(w, _("darktable")); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_GROUP_ID); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_LOCAL_COPY); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_HISTORY); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_MODULE); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_ORDER); #undef ADD_COLLECT_ENTRY } static void _menuitem_preferences(GSimpleAction *action, GVariant *parameter, gpointer user_data) { GtkWidget *win = dt_ui_main_window(darktable.gui->ui); GtkWidget *dialog = gtk_dialog_new_with_buttons (_("collections settings"), GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT, _("_cancel"), GTK_RESPONSE_NONE, _("_save"), GTK_RESPONSE_ACCEPT, NULL); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); dt_prefs_init_dialog_collect(dialog); dt_gui_connect_key(dialog, dt_handle_dialog_enter, NULL); #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); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, NULL); } void set_preferences(GMenu *menu, GActionGroup *action_group, dt_lib_module_t *self) { GSimpleAction *action = g_simple_action_new("preferences", NULL); g_signal_connect(action, "activate", G_CALLBACK(_menuitem_preferences), self); g_action_map_add_action(G_ACTION_MAP(action_group), G_ACTION(action)); g_menu_append(menu, _("preferences..."), "presets.preferences"); } static gint _sort_model_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, dt_lib_module_t *self) { gint ia, ib; gtk_tree_model_get(model, a, DT_LIB_COLLECT_COL_INDEX, &ia, -1); gtk_tree_model_get(model, b, DT_LIB_COLLECT_COL_INDEX, &ib, -1); return ib - ia; } #ifdef _WIN32 static void _mount_changed(GVolumeMonitor *volume_monitor, GMount *mount, dt_lib_module_t *self) #else static void _mount_changed(GUnixMountMonitor *monitor, dt_lib_module_t *self) #endif { dt_lib_collect_t *d = self->data; dt_film_set_folder_status(); // very rough update (rebuild the view). As these events are not too // many that remains acceptable adding film_id to treeview and // listview would be cleaner to update just the parameter // "reachable" dt_lib_collect_rule_t *dr = d->rule + d->active_rule; const int property = _combo_get_active_collection(dr->combo); if(property == DT_COLLECTION_PROP_FOLDERS) { d->rule[d->active_rule].typing = FALSE; d->view_rule = -1; _tree_view(dr); } else if(property == DT_COLLECTION_PROP_FILMROLL) { d->rule[d->active_rule].typing = FALSE; d->view_rule = -1; _list_view(dr); } } static void _history_apply(GSimpleAction *action, GVariant *parameter, gpointer user_data) { const int hid = g_variant_get_int32(parameter); if(hid < 0 || hid >= dt_conf_get_int("plugins/lighttable/collect/history_max")) return; char confname[200]; snprintf(confname, sizeof(confname), "plugins/lighttable/collect/history_pos%1d", hid); const int pos = dt_conf_get_int(confname); snprintf(confname, sizeof(confname), "plugins/lighttable/collect/history%1d", hid); const char *line = dt_conf_get_string_const(confname); if(line && line[0] != '\0') { // we store the wanted offset which will be set by thumbtable on // collection_change signal dt_conf_set_int("plugins/lighttable/collect/history_next_pos", pos); const int prev_property = dt_conf_get_int("plugins/lighttable/collect/item0"); dt_collection_deserialize(line, FALSE); // for the tag property we need to adjust the order accordingly const int new_property = dt_conf_get_int("plugins/lighttable/collect/item0"); gchar *order = NULL; if(prev_property != DT_COLLECTION_PROP_TAG && new_property == DT_COLLECTION_PROP_TAG) { // save global order char buf[4096] = { 0 }; dt_collection_sort_serialize(buf, sizeof(buf)); dt_conf_set_string("plugins/lighttable/collect/lastorder", buf); } else if(prev_property == DT_COLLECTION_PROP_TAG && new_property != DT_COLLECTION_PROP_TAG) { // restore global order order = dt_conf_get_string("plugins/lighttable/collect/lastorder"); dt_collection_set_tag_id((dt_collection_t *)darktable.collection, 0); } if(order) DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_IMAGES_ORDER_CHANGE, order); } } static void _history_pretty_print(const char *buf, char *out, size_t outsize) { memset(out, 0, outsize); if(!buf || buf[0] == '\0') return; int num_rules = 0; char str[400] = { 0 }; int mode, item; int c; sscanf(buf, "%d", &num_rules); while(buf[0] != '\0' && buf[0] != ':') buf++; if(buf[0] == ':') buf++; for(int k = 0; k < num_rules; k++) { const int n = sscanf(buf, "%d:%d:%399[^$]", &mode, &item, str); if(n == 3) { if(k > 0) { c = g_strlcpy(out, " ", outsize); out += c; outsize -= c; switch(mode) { case DT_LIB_COLLECT_MODE_AND: c = g_strlcpy(out, _("AND"), outsize); out += c; outsize -= c; break; case DT_LIB_COLLECT_MODE_OR: c = g_strlcpy(out, _("OR"), outsize); out += c; outsize -= c; break; default: // case DT_LIB_COLLECT_MODE_AND_NOT: c = g_strlcpy(out, _("BUT NOT"), outsize); out += c; outsize -= c; break; } c = g_strlcpy(out, " ", outsize); out += c; outsize -= c; } int i = 0; while(str[i] != '\0' && str[i] != '$') i++; if(str[i] == '$') str[i] = '\0'; gchar *pretty = NULL; if(!g_strcmp0(str, "%")) pretty = g_strdup(_("all")); else if(item == DT_COLLECTION_PROP_FILMROLL) pretty = g_strdup(dt_image_film_roll_name(str)); else pretty = g_strdup(str); c = snprintf(out, outsize, "%s: %s", item < DT_COLLECTION_PROP_LAST ? dt_collection_name(item) : "???", pretty); g_free(pretty); out += c; outsize -= c; } while(buf[0] != '$' && buf[0] != '\0') buf++; if(buf[0] == '$') buf++; } } static void _history_show(GtkWidget *widget, dt_lib_module_t *self) { GActionGroup *action_group = gtk_widget_get_action_group(widget, "history"); if(action_group == NULL) { GActionEntry action_entries[] = { { "activate", _history_apply, "i", 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(widget, "history", G_ACTION_GROUP(action_group)); g_object_unref(action_group); } // we show a popup with all the history entries GMenu *menu = g_menu_new(); const int maxitems = dt_conf_get_int("plugins/lighttable/collect/history_max"); for(int i = 0; i < maxitems; i++) { char confname[200]; snprintf(confname, sizeof(confname), "plugins/lighttable/collect/history%1d", i); const char *line = dt_conf_get_string_const(confname); if(line && line[0] != '\0') { char str[2048] = { 0 }; _history_pretty_print(line, str, sizeof(str)); gchar *action = g_strdup_printf("history.activate(%i)", i); gchar *label = dt_str_unmnemonic(str); g_menu_append(menu, label, action); g_free(label); g_free(action); } else break; } // popup the menu GtkWidget *popover_menu = dt_gui_popover_menu_from_model(GTK_WIDGET(widget), menu); g_object_unref(menu); gtk_popover_popup(GTK_POPOVER(popover_menu)); } static void _history_previous(dt_action_t *action) { const char *line = dt_conf_get_string_const("plugins/lighttable/collect/history1"); if(line && g_strcmp0(line, "")) { dt_collection_deserialize(line, FALSE); } } static void _sort_reverse_changed(GtkDarktableToggleButton *widget, gpointer user_data) { const gboolean reverse = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)); if(reverse) dtgtk_togglebutton_set_paint(DTGTK_TOGGLEBUTTON(widget), dtgtk_cairo_paint_sortby, CPF_DIRECTION_DOWN, NULL); else dtgtk_togglebutton_set_paint(DTGTK_TOGGLEBUTTON(widget), dtgtk_cairo_paint_sortby, CPF_DIRECTION_UP, NULL); dt_conf_set_bool("plugins/collect/descending", reverse); gtk_widget_queue_draw((GtkWidget*)widget); dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, NULL); } GtkWidget *gui_tool_box(dt_lib_module_t *self) { // specific header action const gboolean sort_descend = dt_conf_get_bool("plugins/collect/descending"); GtkWidget *sortb = dtgtk_togglebutton_new(dtgtk_cairo_paint_sortby, sort_descend ? CPF_DIRECTION_DOWN : CPF_DIRECTION_UP, NULL); gtk_widget_set_tooltip_text(sortb, _("toggle collection sort order ascending/descending")); dt_gui_add_class(sortb, "dt_ignore_fg_state"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(sortb), sort_descend); g_signal_connect(G_OBJECT(sortb), "toggled", G_CALLBACK(_sort_reverse_changed), self); return sortb; } void gui_init(dt_lib_module_t *self) { dt_lib_collect_t *d = self->data = calloc(1, sizeof(dt_lib_collect_t)); self->widget = dt_gui_vbox(); dt_gui_add_class(self->widget, "dt_spacing_sw"); // setup the actions for this module const GActionEntry entries[] = { { "clear", _menuitem_clear, "i" }, { "mode", _menuitem_mode, "(ii)" }, { "modechange", _menuitem_mode_change, "(ii)" } }; d->action_group = g_simple_action_group_new(); g_action_map_add_action_entries(G_ACTION_MAP(d->action_group), entries, G_N_ELEMENTS(entries), d); gtk_widget_insert_action_group(self->widget, "collect", G_ACTION_GROUP(d->action_group)); d->active_rule = 0; d->nb_rules = 0; d->params = (dt_lib_collect_params_t *)malloc(sizeof(dt_lib_collect_params_t)); view_set_click(NULL, self); gboolean has_iop_name_rule = FALSE; for(int i = 0; i < MAX_RULES; i++) { d->rule[i].num = i; d->rule[i].typing = FALSE; GtkWidget *w = NULL; d->rule[i].combo = w = dt_bauhaus_combobox_new(NULL); dt_bauhaus_combobox_set_selected_text_align(w, DT_BAUHAUS_COMBOBOX_ALIGN_LEFT); _populate_collect_combo(w); dt_bauhaus_combobox_mute_scrolling(w); if(_combo_get_active_collection(w) == DT_COLLECTION_PROP_MODULE) has_iop_name_rule = TRUE; g_signal_connect(G_OBJECT(w), "value-changed", G_CALLBACK(combo_changed), d->rule + i); d->rule[i].text = w = dt_ui_entry_new(10); gtk_widget_add_events(w, GDK_FOCUS_CHANGE_MASK | GDK_KEY_PRESS_MASK); dt_gui_connect_focus(w, entry_focus_in_callback, NULL, d->rule + i); g_signal_connect(G_OBJECT(w), "changed", G_CALLBACK(entry_changed), d->rule + i); g_signal_connect(G_OBJECT(w), "activate", G_CALLBACK(entry_activated), d->rule + i); gtk_entry_set_width_chars(GTK_ENTRY(w), 5); d->rule[i].button = w = dtgtk_button_new(dtgtk_cairo_paint_presets, 0, NULL); dt_gui_add_class(GTK_WIDGET(w), "dt_big_btn_canvas"); dt_gui_connect_click(w, popup_button_callback_cb, NULL, d->rule + i); d->rule[i].hbox = dt_gui_hbox(d->rule[i].combo, dt_gui_expand(d->rule[i].text), d->rule[i].button); gtk_widget_set_name(d->rule[i].hbox, "lib-dtbutton"); dt_gui_box_add(self->widget, d->rule[i].hbox); } GtkTreeView *view = GTK_TREE_VIEW(gtk_tree_view_new()); d->view_rule = -1; d->view = view; gtk_tree_view_set_headers_visible(view, FALSE); /* the treeview owns an internal bubble-phase GtkGestureMultiPress that * would fight our own selection handling: use a CAPTURE-phase gesture * that claims the sequence, replicating the event consumption of the * pre-migration button-press-event handler */ GtkGesture *gesture = gtk_gesture_multi_press_new(GTK_WIDGET(view)); gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(gesture), GTK_PHASE_CAPTURE); dt_gui_add_controller(GTK_WIDGET(view), gesture); gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(gesture), 0); g_signal_connect(gesture, "pressed", G_CALLBACK(view_onButtonPressed_cb), d); g_signal_connect(gesture, "begin", G_CALLBACK(_gesture_begin_claim), d); g_signal_connect(G_OBJECT(view), "popup-menu", G_CALLBACK(view_onPopupMenu), d); GtkTreeViewColumn *col = gtk_tree_view_column_new(); gtk_tree_view_append_column(view, col); GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); gtk_tree_view_column_pack_start(col, renderer, TRUE); gtk_tree_view_column_set_cell_data_func(col, renderer, tree_count_show, NULL, NULL); g_object_set(renderer, "strikethrough", TRUE, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, (gchar *)0); gtk_tree_view_column_add_attribute(col, renderer, "strikethrough-set", DT_LIB_COLLECT_COL_UNREACHABLE); GtkTreeModel *listmodel = GTK_TREE_MODEL (gtk_list_store_new (DT_LIB_COLLECT_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_UINT, G_TYPE_UINT)); gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(listmodel), DT_LIB_COLLECT_COL_INDEX, (GtkTreeIterCompareFunc)_sort_model_func, self, NULL); d->listfilter = gtk_tree_model_filter_new(listmodel, NULL); gtk_tree_model_filter_set_visible_column (GTK_TREE_MODEL_FILTER(d->listfilter), DT_LIB_COLLECT_COL_VISIBLE); GtkTreeModel *treemodel = GTK_TREE_MODEL (gtk_tree_store_new(DT_LIB_COLLECT_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_UINT, G_TYPE_UINT)); d->treefilter = gtk_tree_model_filter_new(treemodel, NULL); gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(d->treefilter), DT_LIB_COLLECT_COL_VISIBLE); g_object_unref(treemodel); // the bottom buttons for the rules. the actions keep their names for // shortcuts; the buttons show the symbols, which need no translation d->back = dt_action_button_new(self, N_("back"), G_CALLBACK(_navigation_back), self, _("go back to the previous collection"), 0, 0); gtk_button_set_label(GTK_BUTTON(d->back), "<"); d->forward = dt_action_button_new(self, N_("forward"), G_CALLBACK(_navigation_forward), self, _("go forward to the next collection"), 0, 0); gtk_button_set_label(GTK_BUTTON(d->forward), ">"); dt_collection_navigation_record_current(); _navigation_update(d); d->history_button = dt_action_button_new(self, N_("history"), G_CALLBACK(_history_show), self, _("revert to a previous set of rules"), GDK_KEY_k, GDK_CONTROL_MASK); // homogeneous halves align the history button with those in filtering lib d->history_box = dt_gui_hbox(dt_gui_hbox(d->back, d->forward), d->history_button); gtk_box_set_homogeneous(GTK_BOX(d->history_box), TRUE); gtk_widget_show_all(d->history_box); gtk_widget_set_no_show_all(d->history_button, TRUE); dt_gui_box_add(self->widget, dt_ui_resize_wrap(GTK_WIDGET(view), 200, "plugins/lighttable/collect/windowheight"), d->history_box); /* setup proxy */ darktable.view_manager->proxy.module_collect.module = self; darktable.view_manager->proxy.module_collect.update = _lib_collect_gui_update; _lib_collect_gui_update(self); if(_combo_get_active_collection(d->rule[0].combo) == DT_COLLECTION_PROP_TAG) { const char *tag = dt_conf_get_string_const("plugins/lighttable/collect/string0"); dt_collection_set_tag_id((dt_collection_t *)darktable.collection, dt_tag_get_tag_id_by_name(tag)); } #ifdef _WIN32 d->vmonitor = g_volume_monitor_get(); g_signal_connect(G_OBJECT(d->vmonitor), "mount-changed", G_CALLBACK(_mount_changed), self); g_signal_connect(G_OBJECT(d->vmonitor), "mount-added", G_CALLBACK(_mount_changed), self); #else d->vmonitor = g_unix_mount_monitor_get(); g_signal_connect(G_OBJECT(d->vmonitor), "mounts-changed", G_CALLBACK(_mount_changed), self); #endif // force redraw collection images because of late update of the // table memory.darktable_iop_names if(has_iop_name_rule) dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_MODULE, NULL); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_COLLECTION_CHANGED, collection_updated); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_COLLECTION_CHANGED, _navigation_changed); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_FILMROLLS_CHANGED, filmrolls_updated); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_PREFERENCES_CHANGE, preferences_changed); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_FILMROLLS_IMPORTED, filmrolls_imported); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_FILMROLLS_REMOVED, filmrolls_removed); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_TAG_CHANGED, tag_changed); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_GEOTAG_CHANGED, _geotag_changed); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_METADATA_CHANGED, _metadata_changed); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_PREFERENCES_CHANGE, view_set_click); dt_action_register(DT_ACTION(self), N_("jump back to previous collection"), _history_previous, GDK_KEY_k, GDK_CONTROL_MASK | GDK_SHIFT_MASK); } void gui_cleanup(dt_lib_module_t *self) { dt_lib_collect_t *d = self->data; darktable.view_manager->proxy.module_collect.module = NULL; free(d->params); /* cleanup mem */ g_object_unref(d->treefilter); g_object_unref(d->listfilter); g_object_unref(d->vmonitor); g_object_unref(d->action_group); /* TODO: Make sure we are cleaning up all allocations */ free(self->data); self->data = NULL; } static int _is_time_property(int property) { return (property == DT_COLLECTION_PROP_TIME || property == DT_COLLECTION_PROP_IMPORT_TIMESTAMP || property == DT_COLLECTION_PROP_CHANGE_TIMESTAMP || property == DT_COLLECTION_PROP_EXPORT_TIMESTAMP || property == DT_COLLECTION_PROP_PRINT_TIMESTAMP); } #ifdef USE_LUA static int new_rule_cb(lua_State*L) { dt_lib_collect_params_rule_t rule; memset(&rule, 0, sizeof(dt_lib_collect_params_rule_t)); luaA_push(L, dt_lib_collect_params_rule_t, &rule); return 1; } static int filter_cb(lua_State *L) { dt_lib_module_t *self = lua_touserdata(L, lua_upvalueindex(1)); int size; dt_lib_collect_params_t *p = get_params(self, &size); // put it in stack so memory is not lost if a lua exception is raised if(lua_gettop(L) > 0) { luaL_checktype(L, 1, LUA_TTABLE); dt_lib_collect_params_t *new_p = get_params(self, &size); new_p->rules = 0; do { lua_pushinteger(L, new_p->rules + 1); lua_gettable(L, 1); if(lua_isnil(L, -1)) break; luaA_to(L, dt_lib_collect_params_rule_t, &new_p->rule[new_p->rules], -1); new_p->rules++; } while(new_p->rules < MAX_RULES); if(new_p->rules == MAX_RULES) { lua_pushinteger(L, new_p->rules + 1); lua_gettable(L, 1); if(!lua_isnil(L, -1)) { luaL_error(L, "Number of rules given exceeds max allowed (%d)", MAX_RULES); } } set_params(self, new_p, size); free(new_p); } lua_newtable(L); for(int i = 0; i < p->rules; i++) { luaA_push(L, dt_lib_collect_params_rule_t, &p->rule[i]); lua_seti(L, -2, i + 1); // lua tables are 1 based } free(p); return 1; } static int mode_member(lua_State *L) { dt_lib_collect_params_rule_t *rule = luaL_checkudata(L, 1, "dt_lib_collect_params_rule_t"); if(lua_gettop(L) > 2) { dt_lib_collect_mode_t value; luaA_to(L, dt_lib_collect_mode_t, &value, 3); rule->mode = value; return 0; } const dt_lib_collect_mode_t tmp = rule->mode; // temp buffer because // of bitfield in the // original struct luaA_push(L, dt_lib_collect_mode_t, &tmp); return 1; } static int item_member(lua_State *L) { dt_lib_collect_params_rule_t *rule = luaL_checkudata(L, 1, "dt_lib_collect_params_rule_t"); if(lua_gettop(L) > 2) { dt_collection_properties_t value; luaA_to(L, dt_collection_properties_t, &value, 3); rule->item = value; return 0; } const dt_collection_properties_t tmp = rule->item; // temp buffer // because of // bitfield in // the original // struct luaA_push(L, dt_collection_properties_t, &tmp); return 1; } static int data_member(lua_State *L) { dt_lib_collect_params_rule_t *rule = luaL_checkudata(L, 1, "dt_lib_collect_params_rule_t"); if(lua_gettop(L) > 2) { size_t tgt_size; const char*data = luaL_checklstring(L, 3, &tgt_size); if(tgt_size > PARAM_STRING_SIZE) { return luaL_error(L, "string '%s' too long (max is %d)", data, PARAM_STRING_SIZE); } g_strlcpy(rule->string, data, sizeof(rule->string)); return 0; } lua_pushstring(L, rule->string); return 1; } void init(struct dt_lib_module_t *self) { lua_State *L = darktable.lua_state.state; int my_type = dt_lua_module_entry_get_type(L, "lib", self->plugin_name); lua_pushlightuserdata(L, self); lua_pushcclosure(L, filter_cb, 1); dt_lua_gtk_wrap(L); lua_pushcclosure(L, dt_lua_type_member_common, 1); dt_lua_type_register_const_type(L, my_type, "filter"); lua_pushcfunction(L, new_rule_cb); lua_pushcclosure(L, dt_lua_type_member_common, 1); dt_lua_type_register_const_type(L, my_type, "new_rule"); dt_lua_init_type(L, dt_lib_collect_params_rule_t); lua_pushcfunction(L, mode_member); dt_lua_type_register(L, dt_lib_collect_params_rule_t, "mode"); lua_pushcfunction(L, item_member); dt_lua_type_register(L, dt_lib_collect_params_rule_t, "item"); lua_pushcfunction(L, data_member); dt_lua_type_register(L, dt_lib_collect_params_rule_t, "data"); luaA_enum(L, dt_lib_collect_mode_t); luaA_enum_value(L, dt_lib_collect_mode_t, DT_LIB_COLLECT_MODE_AND); luaA_enum_value(L, dt_lib_collect_mode_t, DT_LIB_COLLECT_MODE_OR); luaA_enum_value(L, dt_lib_collect_mode_t, DT_LIB_COLLECT_MODE_AND_NOT); luaA_enum(L, dt_collection_properties_t); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_FILMROLL); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_FOLDERS); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_CAMERA); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_TAG); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_DAY); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_TIME); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_IMPORT_TIMESTAMP); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_CHANGE_TIMESTAMP); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_EXPORT_TIMESTAMP); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_PRINT_TIMESTAMP); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_HISTORY); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_RATING); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_COLORLABEL); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_WHITEBALANCE); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_FLASH); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_EXPOSURE_PROGRAM); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_METERING_MODE); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_GROUP_ID); dt_pthread_mutex_lock(&darktable.metadata_threadsafe); for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next) { dt_metadata_t *metadata = iter->data; if(!metadata->internal && metadata->visible) luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_METADATA_OFFSET + metadata->key); } dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_LENS); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_FOCAL_LENGTH); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_ISO); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_APERTURE); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_ASPECT_RATIO); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_EXPOSURE); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_EXPOSURE_BIAS); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_FILENAME); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_GEOTAGGING); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_LOCAL_COPY); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_MODULE); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_ORDER); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_DUPLICATES); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_MONTH); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_DIMENSIONS); } #endif #undef MAX_RULES // 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