Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 192 additions & 7 deletions src/libs/collect.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
{
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}


Expand Down
Loading