diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-12 21:13:45 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-12 21:13:45 +0300 |
| commit | 89727590e718d3628a0847689182e896df646b8a (patch) | |
| tree | 3106397d1b041d95ecfc04762548261f9d660d62 /src/navigator.c | |
| parent | 198b0e8112905bc003f954fc9159cc3da2dfce09 (diff) | |
walkdir: navigator, shortcuts, window wiring, drop target, tests (ht0)
M2 (task ht0): walk the directory.
- src/navigator.{c,h}: GObject (no GtkWidget) holding the folder listing —
image-MIME filter + extension fallback, hide-RAW-sidecars (decision #33),
sort name/time/size, cursor + prev/next/wrap, path-based marks (survive
re-sort, pruned on rescan/monitor, cleared on remove), nearest-fallback,
GFileMonitor debounced 250ms (decision #28), 'changed' signal. 92% gcov.
- src/shortcuts.{c,h}: GtkShortcutController (MANAGED) binding h/l/Left/Right/
g/G/o/q to win.* actions; one table all milestones add to.
- src/window.{c,h}: open file->parent+current / folder->first; h/l/g/G actions
-> nav -> load -> viewer; single GCancellable last-write-wins (g_file_equal
guard for M3 async); header title 'filename · n/total'; GtkDropTarget
(GDK_TYPE_FILE_LIST, many->first, decision Z); GtkFileDialog open.
- data/gschema: add hide-raw-sidecars key.
- tests: unit test_navigator (12 subtests), integration test_walk_folder
(temp fixtures walk + ./sample-images skip-if-absent). 8/8 green; ASan clean
(GIO_USE_VFS=local in fixtures_env).
Fixed during this milestone: a UAF (borrowed c_name used after unref), a
cancellable double-ref leak, time::modified UINT64 vs INT64, and several
test-harness leaks. Sub-agent review issues addressed (mark pruning on
rescan/monitor, remove-before-cursor test, recursive temp-dir cleanup,
callback _cb naming, connect-before-mutate).
Diffstat (limited to 'src/navigator.c')
| -rw-r--r-- | src/navigator.c | 691 |
1 files changed, 691 insertions, 0 deletions
diff --git a/src/navigator.c b/src/navigator.c new file mode 100644 index 0000000..cb47575 --- /dev/null +++ b/src/navigator.c @@ -0,0 +1,691 @@ +/*:* + * ggaze — directory navigator + * + * GObject (no GtkWidget) holding the current folder listing: filter to + * image MIME, sort (name/time/size), cursor + path-based marks, GFileMonitor + * with debounce, nearest-fallback on current-file removal. Emits "changed". + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "navigator.h" + +#include <gio/gio.h> +#include <glib-object.h> +#include <glib.h> +#include <string.h> + +/* Extensions (lowercase, no dot). */ +static const char *IMAGE_EXTS[] = {"jpg", "jpeg", "png", "gif", "webp", + "tif", "tiff", "ico", "jxl", "avif", + "heif", "heic", "bmp", "svg", NULL}; +static const char *RAW_EXTS[] = {"raf", "cr2", "cr3", "nef", "arw", + "dng", "orf", "rw2", "pef", "srw", + "3fr", "x3f", NULL}; + +#define GGAZE_DEFAULT_DEBOUNCE_MS 250 + +typedef struct { + GFile *file; /* owned ref */ + gchar *name; /* owned (basename) */ + gint64 mtime; + gint64 size; +} Entry; + +struct _Navigator { + GObject parent_instance; + GFile *p_dir; /* owned */ + GPtrArray *p_files; /* GFile* (owned refs), sorted/filtered */ + gint i_current; /* -1 if empty */ + GgazeSort e_sort; + gboolean b_wrap; + gboolean b_hide_raw; + GHashTable *p_marks; /* GFile* (owned refs) -> presence */ + GFileMonitor *p_monitor; + guint u_debounce_ms; + guint u_debounce_id; /* 0 = none pending */ +}; + +G_DEFINE_TYPE(Navigator, navigator, G_TYPE_OBJECT) + +static guint u_changed_signal = 0; + +/* --- helpers -------------------------------------------------------------- */ + +static const char * +_ext_of(const char *c_name) { + const char *c_dot = strrchr(c_name, '.'); + if (c_dot == NULL || c_dot == c_name) { + return (NULL); + } + return (c_dot + 1); +} + +static gboolean +_str_in_set(const char *c_s, const char **set) { + if (c_s == NULL) { + return (FALSE); + } + for (gsize u_i = 0; set[u_i] != NULL; u_i++) { + if (g_ascii_strcasecmp(c_s, set[u_i]) == 0) { + return (TRUE); + } + } + return (FALSE); +} + +static gboolean +_is_raw_ext(const char *c_ext) { + return (_str_in_set(c_ext, RAW_EXTS)); +} + +static gboolean +_is_image_file(const char *c_name, const char *c_ct) { + if (c_ct != NULL && g_str_has_prefix(c_ct, "image/")) { + return (TRUE); + } + const char *c_ext = _ext_of(c_name); + return (_str_in_set(c_ext, IMAGE_EXTS) || _str_in_set(c_ext, RAW_EXTS)); +} + +static gboolean +_is_jpeg_ext(const char *c_ext) { + return (c_ext != NULL && (g_ascii_strcasecmp(c_ext, "jpg") == 0 || + g_ascii_strcasecmp(c_ext, "jpeg") == 0)); +} + +/* Owned stem (basename without extension, lowercased for compare). */ +static char * +_stem_lower(const char *c_name) { + const char *c_dot = strrchr(c_name, '.'); + gsize u_len = (c_dot != NULL && c_dot != c_name) ? (gsize)(c_dot - c_name) + : strlen(c_name); + char *c_stem = g_strndup(c_name, u_len); + for (char *p = c_stem; *p != '\0'; p++) { + *p = (char)g_ascii_tolower((gint)*p); + } + return (c_stem); +} + +static void +_entry_free(gpointer p_void) { + Entry *p_e = (Entry *)p_void; + g_clear_object(&p_e->file); + g_free(p_e->name); + g_free(p_e); +} + +static gint +_compare_entries(gconstpointer p_a, gconstpointer p_b, gpointer p_data) { + const Entry *p_ea = *(Entry *const *)p_a; + const Entry *p_eb = *(Entry *const *)p_b; + GgazeSort e_sort = (GgazeSort)GPOINTER_TO_INT(p_data); + + switch (e_sort) { + case GGAZE_SORT_TIME: + if (p_ea->mtime < p_eb->mtime) { + return (-1); + } + if (p_ea->mtime > p_eb->mtime) { + return (1); + } + break; + case GGAZE_SORT_SIZE: + if (p_ea->size < p_eb->size) { + return (-1); + } + if (p_ea->size > p_eb->size) { + return (1); + } + break; + case GGAZE_SORT_NAME: + default: + break; + } + /* Tie-break (and the NAME case) by collated basename. */ + return (g_utf8_collate(p_ea->name, p_eb->name)); +} + +static gint +_find_index_by_file(Navigator *p_nav, GFile *p_file) { + if (p_file == NULL) { + return (-1); + } + for (guint u_i = 0; u_i < p_nav->p_files->len; u_i++) { + if (g_file_equal(p_file, + (GFile *)g_ptr_array_index(p_nav->p_files, u_i))) { + return ((gint)u_i); + } + } + return (-1); +} + +/* Re-read the directory, filter, hide-raw, sort, and commit; keep the current + * file by path, falling back to the nearest by position if it is gone. */ +static void +_relist(Navigator *p_nav) { + GError *p_err = NULL; + GFileEnumerator *p_enum = g_file_enumerate_children( + p_nav->p_dir, + "standard::name,standard::type,standard::content-type," + "time::modified,standard::size", + G_FILE_QUERY_INFO_NONE, NULL, &p_err); + if (p_enum == NULL) { + if (p_err != NULL) { + g_warning("navigator: enumerate failed: %s", p_err->message); + g_error_free(p_err); + } + return; + } + + GPtrArray *p_entries = g_ptr_array_new_with_free_func(_entry_free); + GHashTable *p_jpeg_stems = + g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + + GFileInfo *p_info = NULL; + while ((p_info = g_file_enumerator_next_file(p_enum, NULL, NULL)) != NULL) { + const char *c_name = g_file_info_get_name(p_info); + if (c_name == NULL || c_name[0] == '.') { + g_object_unref(p_info); + continue; + } + if (g_file_info_get_file_type(p_info) != G_FILE_TYPE_REGULAR) { + g_object_unref(p_info); + continue; + } + const char *c_ct = g_file_info_get_content_type(p_info); + if (!_is_image_file(c_name, c_ct)) { + g_object_unref(p_info); + continue; + } + Entry *p_e = g_new(Entry, 1); + p_e->file = g_file_get_child(p_nav->p_dir, c_name); + p_e->name = g_strdup(c_name); + p_e->mtime = (gint64)g_file_info_get_attribute_uint64( + p_info, G_FILE_ATTRIBUTE_TIME_MODIFIED); + p_e->size = (gint64)g_file_info_get_size(p_info); + g_object_unref(p_info); + + const char *c_ext = _ext_of(p_e->name); /* p_e->name is owned; c_name + * was borrowed from p_info */ + if (_is_jpeg_ext(c_ext)) { + char *c_stem = _stem_lower(p_e->name); + g_hash_table_add(p_jpeg_stems, c_stem); + } + g_ptr_array_add(p_entries, p_e); + } + g_object_unref(p_enum); + + /* Drop RAW sidecars that have a JPEG twin (decision #33). */ + if (p_nav->b_hide_raw) { + for (gsize u_i = p_entries->len; u_i > 0; u_i--) { + Entry *p_e = (Entry *)g_ptr_array_index(p_entries, u_i - 1); + const char *c_ext = _ext_of(p_e->name); + if (_is_raw_ext(c_ext)) { + char *c_stem = _stem_lower(p_e->name); + if (g_hash_table_contains(p_jpeg_stems, c_stem)) { + g_ptr_array_remove_index(p_entries, u_i - 1); + } + g_free(c_stem); + } + } + } + g_hash_table_unref(p_jpeg_stems); + + g_ptr_array_sort_with_data(p_entries, _compare_entries, + GINT_TO_POINTER((gint)p_nav->e_sort)); + + /* Remember current by path, then commit. */ + GFile *p_keep = NULL; + if (p_nav->i_current >= 0 && (guint)p_nav->i_current < p_nav->p_files->len) { + p_keep = g_object_ref( + g_ptr_array_index(p_nav->p_files, (guint)p_nav->i_current)); + } + gint i_old = p_nav->i_current; + + g_ptr_array_set_size(p_nav->p_files, 0); + for (gsize u_i = 0; u_i < p_entries->len; u_i++) { + Entry *p_e = (Entry *)g_ptr_array_index(p_entries, u_i); + g_ptr_array_add(p_nav->p_files, g_object_ref(p_e->file)); + } + + if (p_keep != NULL) { + gint i_found = _find_index_by_file(p_nav, p_keep); + if (i_found >= 0) { + p_nav->i_current = i_found; + } else if (p_nav->p_files->len == 0) { + p_nav->i_current = -1; + } else { + p_nav->i_current = CLAMP(i_old, 0, (gint)p_nav->p_files->len - 1); + } + g_object_unref(p_keep); + } else if (p_nav->p_files->len == 0) { + p_nav->i_current = -1; + } else if (p_nav->i_current < 0) { + p_nav->i_current = 0; + } + + /* Prune marks whose file is no longer in the listing (external delete via + * the monitor, or a rescan). Keeps marks consistent with the live folder. */ + GHashTableIter iter; + gpointer p_key; + g_hash_table_iter_init(&iter, p_nav->p_marks); + while (g_hash_table_iter_next(&iter, &p_key, NULL)) { + if (_find_index_by_file(p_nav, (GFile *)p_key) < 0) { + g_hash_table_iter_remove(&iter); + } + } + + g_ptr_array_unref(p_entries); +} + +static void +_emit_changed(Navigator *p_nav) { + g_signal_emit(p_nav, u_changed_signal, 0); +} + +/* --- monitor / debounce -------------------------------------------------- */ + +static gboolean +_debounce_fire(gpointer p_data) { + Navigator *p_nav = (Navigator *)p_data; + p_nav->u_debounce_id = 0; + _relist(p_nav); + _emit_changed(p_nav); + return (G_SOURCE_REMOVE); +} + +static void +_schedule_debounce(Navigator *p_nav) { + if (p_nav->u_debounce_id != 0) { + return; /* already pending; the single rescan picks up all changes */ + } + p_nav->u_debounce_id = + g_timeout_add(p_nav->u_debounce_ms, _debounce_fire, p_nav); +} + +static void +monitor_changed_cb(GFileMonitor *p_mon, GFile *p_other, GFile *p_src, + GFileMonitorEvent e_ev, gpointer p_data) { + (void)p_mon; + (void)p_other; + (void)p_src; + switch (e_ev) { + case G_FILE_MONITOR_EVENT_CREATED: + case G_FILE_MONITOR_EVENT_DELETED: + case G_FILE_MONITOR_EVENT_MOVED_IN: + case G_FILE_MONITOR_EVENT_MOVED_OUT: + case G_FILE_MONITOR_EVENT_RENAMED: + _schedule_debounce((Navigator *)p_data); + break; + default: + break; + } +} + +/* --- GObject ------------------------------------------------------------- */ + +static void +navigator_dispose(GObject *p_obj) { + Navigator *p_nav = (Navigator *)p_obj; + if (p_nav->u_debounce_id != 0) { + g_source_remove(p_nav->u_debounce_id); + p_nav->u_debounce_id = 0; + } + if (p_nav->p_monitor != NULL) { + g_file_monitor_cancel(p_nav->p_monitor); + g_clear_object(&p_nav->p_monitor); + } + g_clear_pointer(&p_nav->p_marks, g_hash_table_unref); + g_clear_pointer(&p_nav->p_files, g_ptr_array_unref); + g_clear_object(&p_nav->p_dir); + G_OBJECT_CLASS(navigator_parent_class)->dispose(p_obj); +} + +static void +navigator_class_init(NavigatorClass *p_klass) { + GObjectClass *p_oc = G_OBJECT_CLASS(p_klass); + p_oc->dispose = navigator_dispose; + u_changed_signal = + g_signal_new("changed", G_TYPE_FROM_CLASS(p_klass), G_SIGNAL_RUN_LAST, 0, + NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 0); +} + +static void +navigator_init(Navigator *p_nav) { + p_nav->p_files = + g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); + p_nav->p_marks = + g_hash_table_new_full((GHashFunc)g_file_hash, (GEqualFunc)g_file_equal, + (GDestroyNotify)g_object_unref, NULL); + p_nav->i_current = -1; + p_nav->e_sort = GGAZE_SORT_NAME; + p_nav->b_wrap = TRUE; + p_nav->b_hide_raw = TRUE; + p_nav->u_debounce_ms = GGAZE_DEFAULT_DEBOUNCE_MS; +} + +/* --- public API ---------------------------------------------------------- */ + +Navigator * +navigator_new(GFile *p_dir, GgazeSort e_sort, gboolean b_wrap, + gboolean b_hide_raw) { + g_return_val_if_fail(G_IS_FILE(p_dir), NULL); + Navigator *p_nav = (Navigator *)g_object_new(GGAZE_TYPE_NAVIGATOR, NULL); + p_nav->p_dir = (GFile *)g_object_ref(p_dir); + p_nav->e_sort = e_sort; + p_nav->b_wrap = b_wrap; + p_nav->b_hide_raw = b_hide_raw; + _relist(p_nav); + if (p_nav->i_current < 0 && p_nav->p_files->len > 0) { + p_nav->i_current = 0; + } + + GError *p_err = NULL; + p_nav->p_monitor = g_file_monitor_directory( + p_nav->p_dir, G_FILE_MONITOR_WATCH_MOVES, NULL, &p_err); + if (p_nav->p_monitor != NULL) { + g_signal_connect(p_nav->p_monitor, "changed", + G_CALLBACK(monitor_changed_cb), p_nav); + } else { + if (p_err != NULL) { + g_warning("navigator: monitor failed: %s", p_err->message); + g_error_free(p_err); + } + } + return (p_nav); +} + +void +navigator_delete(Navigator *p_nav) { + if (p_nav != NULL) { + g_object_unref(p_nav); + } +} + +GFile * +navigator_get_dir(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), NULL); + return (p_nav->p_dir); +} + +guint +navigator_get_count(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), 0); + return (p_nav->p_files->len); +} + +GFile * +navigator_get_file(Navigator *p_nav, guint u_index) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), NULL); + g_return_val_if_fail(u_index < p_nav->p_files->len, NULL); + return ((GFile *)g_ptr_array_index(p_nav->p_files, u_index)); +} + +gint +navigator_get_current_index(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), -1); + return (p_nav->i_current); +} + +GFile * +navigator_get_current(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), NULL); + if (p_nav->i_current < 0 || (guint)p_nav->i_current >= p_nav->p_files->len) { + return (NULL); + } + return ((GFile *)g_ptr_array_index(p_nav->p_files, (guint)p_nav->i_current)); +} + +guint +navigator_get_remaining(Navigator *p_nav) { + return (navigator_get_count(p_nav)); +} + +gboolean +navigator_set_current(Navigator *p_nav, guint u_index) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), FALSE); + if (u_index >= p_nav->p_files->len) { + return (FALSE); + } + if (p_nav->i_current == (gint)u_index) { + return (FALSE); + } + p_nav->i_current = (gint)u_index; + _emit_changed(p_nav); + return (TRUE); +} + +gboolean +navigator_set_current_file(Navigator *p_nav, GFile *p_file) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), FALSE); + gint i = _find_index_by_file(p_nav, p_file); + if (i < 0) { + return (FALSE); + } + if (p_nav->i_current == i) { + return (FALSE); + } + p_nav->i_current = i; + _emit_changed(p_nav); + return (TRUE); +} + +static gboolean +_advance(Navigator *p_nav, gint i_delta) { + if (p_nav->p_files->len == 0) { + return (FALSE); + } + gint i_new = p_nav->i_current + i_delta; + if (i_new < 0 || i_new >= (gint)p_nav->p_files->len) { + if (!p_nav->b_wrap) { + return (FALSE); + } + i_new = (i_new < 0) ? (gint)p_nav->p_files->len - 1 : 0; + } + if (i_new == p_nav->i_current) { + return (FALSE); + } + p_nav->i_current = i_new; + _emit_changed(p_nav); + return (TRUE); +} + +gboolean +navigator_prev(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), FALSE); + return (_advance(p_nav, -1)); +} + +gboolean +navigator_next(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), FALSE); + return (_advance(p_nav, 1)); +} + +gboolean +navigator_first(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), FALSE); + return (navigator_set_current(p_nav, 0)); +} + +gboolean +navigator_last(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), FALSE); + if (p_nav->p_files->len == 0) { + return (FALSE); + } + return (navigator_set_current(p_nav, p_nav->p_files->len - 1)); +} + +GgazeSort +navigator_get_sort(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), GGAZE_SORT_NAME); + return (p_nav->e_sort); +} + +void +navigator_set_sort(Navigator *p_nav, GgazeSort e_sort) { + g_return_if_fail(GGAZE_IS_NAVIGATOR(p_nav)); + if (p_nav->e_sort == e_sort) { + return; + } + p_nav->e_sort = e_sort; + _relist(p_nav); + _emit_changed(p_nav); +} + +gboolean +navigator_get_wrap(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), FALSE); + return (p_nav->b_wrap); +} + +void +navigator_set_wrap(Navigator *p_nav, gboolean b_wrap) { + g_return_if_fail(GGAZE_IS_NAVIGATOR(p_nav)); + p_nav->b_wrap = b_wrap; +} + +gboolean +navigator_get_hide_raw(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), FALSE); + return (p_nav->b_hide_raw); +} + +void +navigator_set_hide_raw(Navigator *p_nav, gboolean b_hide_raw) { + g_return_if_fail(GGAZE_IS_NAVIGATOR(p_nav)); + if (p_nav->b_hide_raw == b_hide_raw) { + return; + } + p_nav->b_hide_raw = b_hide_raw; + _relist(p_nav); + _emit_changed(p_nav); +} + +/* --- marks --------------------------------------------------------------- */ + +gboolean +navigator_is_marked(Navigator *p_nav, GFile *p_file) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), FALSE); + return (g_hash_table_contains(p_nav->p_marks, p_file)); +} + +void +navigator_toggle_mark(Navigator *p_nav, GFile *p_file) { + g_return_if_fail(GGAZE_IS_NAVIGATOR(p_nav)); + g_return_if_fail(G_IS_FILE(p_file)); + if (g_hash_table_contains(p_nav->p_marks, p_file)) { + g_hash_table_remove(p_nav->p_marks, p_file); + } else { + g_hash_table_add(p_nav->p_marks, g_object_ref(p_file)); + } +} + +static void +_mark_index(Navigator *p_nav, guint u_index) { + if (u_index >= p_nav->p_files->len) { + return; + } + GFile *p_file = (GFile *)g_ptr_array_index(p_nav->p_files, u_index); + if (!g_hash_table_contains(p_nav->p_marks, p_file)) { + g_hash_table_add(p_nav->p_marks, g_object_ref(p_file)); + } +} + +void +navigator_mark_range(Navigator *p_nav, GFile *p_from, GFile *p_to) { + g_return_if_fail(GGAZE_IS_NAVIGATOR(p_nav)); + gint i_a = _find_index_by_file(p_nav, p_from); + gint i_b = _find_index_by_file(p_nav, p_to); + if (i_a < 0 || i_b < 0) { + return; + } + gint i_lo = MIN(i_a, i_b); + gint i_hi = MAX(i_a, i_b); + for (gint i = i_lo; i <= i_hi; i++) { + _mark_index(p_nav, (guint)i); + } + _emit_changed(p_nav); +} + +void +navigator_mark_all(Navigator *p_nav) { + g_return_if_fail(GGAZE_IS_NAVIGATOR(p_nav)); + for (guint u_i = 0; u_i < p_nav->p_files->len; u_i++) { + _mark_index(p_nav, u_i); + } + _emit_changed(p_nav); +} + +void +navigator_clear_marks(Navigator *p_nav) { + g_return_if_fail(GGAZE_IS_NAVIGATOR(p_nav)); + g_hash_table_remove_all(p_nav->p_marks); + _emit_changed(p_nav); +} + +guint +navigator_get_mark_count(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), 0); + return (g_hash_table_size(p_nav->p_marks)); +} + +GList * +navigator_get_marks(Navigator *p_nav) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), NULL); + GList *p_out = NULL; + GList *p_keys = g_hash_table_get_keys(p_nav->p_marks); + for (GList *p_it = p_keys; p_it != NULL; p_it = p_it->next) { + p_out = g_list_prepend(p_out, g_object_ref((GFile *)p_it->data)); + } + g_list_free(p_keys); + return (g_list_reverse(p_out)); +} + +/* --- mutations ----------------------------------------------------------- */ + +void +navigator_rescan(Navigator *p_nav) { + g_return_if_fail(GGAZE_IS_NAVIGATOR(p_nav)); + _relist(p_nav); + _emit_changed(p_nav); +} + +gboolean +navigator_remove(Navigator *p_nav, GFile *p_file) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), FALSE); + g_return_val_if_fail(G_IS_FILE(p_file), FALSE); + gint i = _find_index_by_file(p_nav, p_file); + if (i < 0) { + return (FALSE); + } + /* Clear its mark first (decision Q) while the file ref is still valid. */ + g_hash_table_remove(p_nav->p_marks, p_file); + g_ptr_array_remove_index(p_nav->p_files, (guint)i); + if (p_nav->p_files->len == 0) { + p_nav->i_current = -1; + } else if (p_nav->i_current == i) { + /* Was current: fall back to nearest (stay at this index -> the next file, + * or the new last if we removed the tail). */ + p_nav->i_current = MIN(i, (gint)p_nav->p_files->len - 1); + } else if (p_nav->i_current > i) { + p_nav->i_current--; + } + _emit_changed(p_nav); + return (TRUE); +} + +void +navigator_set_debounce_ms(Navigator *p_nav, guint u_ms) { + g_return_if_fail(GGAZE_IS_NAVIGATOR(p_nav)); + p_nav->u_debounce_ms = u_ms; +} + +void +navigator_emit_changed(Navigator *p_nav) { + g_return_if_fail(GGAZE_IS_NAVIGATOR(p_nav)); + _emit_changed(p_nav); +}
\ No newline at end of file |
