diff --git a/src/libs/collect.c b/src/libs/collect.c index 4effcf23b906..dd8d278a1e04 100644 --- a/src/libs/collect.c +++ b/src/libs/collect.c @@ -2786,18 +2786,187 @@ void gui_reset(dt_lib_module_t *self) 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; + } + + 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); - gtk_entry_set_text(GTK_ENTRY(d->text), ""); + 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); - dt_lib_collect_t *c = get_collect(d); - c->active_rule = d->num; - const int property = _combo_get_active_collection(d->combo); + g_free(resolved_filmroll); if(property == DT_COLLECTION_PROP_FOLDERS || property == DT_COLLECTION_PROP_TAG @@ -2813,8 +2982,6 @@ static void combo_changed(GtkWidget *combo, gchar *order = NULL; if(c->active_rule == 0) { - const int prev_property = dt_conf_get_int("plugins/lighttable/collect/item0"); - if(prev_property != DT_COLLECTION_PROP_TAG && property == DT_COLLECTION_PROP_TAG) { @@ -3137,9 +3304,14 @@ static void collection_updated(gpointer instance, 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(query_change == DT_COLLECTION_CHANGE_RELOAD + 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 @@ -3159,6 +3331,19 @@ static void collection_updated(gpointer instance, 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); + } }