summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-20 20:16:25 +0300
committerPaul Buetow <paul@buetow.org>2026-07-20 20:16:25 +0300
commitdc127b5089656944dab7ab72799d0755ca122803 (patch)
treee72e2ee8b30b0c4ef831b01cf6a1e3b0ca9a5b6b
parent0747e161dd4fd089346b0c14d38229d612e53a5f (diff)
grid: j/k move the cursor down/up one row; stop blanking thumbnails on nav
_on_nav_changed rebuilt every cell on every navigator "changed" signal, so a single h/l/j/k press destroyed and recreated all 613 GtkPicture cells (each re-requesting its thumbnail) and the grid flashed blank. Now it only does a full ggaze_grid_refresh when the file count actually changed (rescan/trash/move); a mere current/mark change just refreshes mark badges in place and re-selects the cursor. Add j/k row navigation: ggaze_grid_move_cursor finds the cell in the adjacent row closest to the current column via gtk_widget_compute_bounds and sets navigator.current to it (so the header / large-view preview track the move and the light nav-changed path re-selects it). h/l stay linear via the existing global win.next/prev. Also document j/k in the ? shortcuts overlay.
-rw-r--r--src/gridview.c109
-rw-r--r--src/gridview.h7
-rw-r--r--src/window.c20
3 files changed, 132 insertions, 4 deletions
diff --git a/src/gridview.c b/src/gridview.c
index 3275333..69a62c1 100644
--- a/src/gridview.c
+++ b/src/gridview.c
@@ -32,7 +32,8 @@ struct _GgazeGrid {
int i_size;
gboolean b_hide_trashed;
guint u_nav_handler;
- GCancellable *p_cancel; /* cancels pending thumbnails on dispose */
+ guint u_last_count; /* navigator count at last full refresh */
+ GCancellable *p_cancel; /* cancels pending thumbnails on dispose */
};
G_DEFINE_TYPE(GgazeGrid, ggaze_grid, GTK_TYPE_WIDGET)
@@ -177,6 +178,16 @@ _on_flow_key(GtkEventControllerKey *p_key, guint u_kv, guint u_kc,
g_signal_emit(p_grid, u_activate_signal, 0);
return (TRUE);
}
+ if (u_kv == GDK_KEY_j) {
+ /* Move the cursor down one row (vim-style); h/l stay linear via the
+ * global win.next/win.prev shortcuts. */
+ ggaze_grid_move_cursor(p_grid, 1);
+ return (TRUE);
+ }
+ if (u_kv == GDK_KEY_k) {
+ ggaze_grid_move_cursor(p_grid, -1);
+ return (TRUE);
+ }
if (u_kv == GDK_KEY_d) {
/* `d` in the grid trashes the current file (window handles via action).
*/
@@ -240,6 +251,60 @@ ggaze_grid_sync_current(GgazeGrid *p_grid) {
return (navigator_set_current_file(p_grid->p_nav, p_file));
}
+/* Move the grid cursor one row down (i_dy = +1) or up (i_dy = -1), selecting
+ * the cell in the adjacent row closest to the current column. Updates
+ * navigator.current so the header and large-view preview track the move (the
+ * "changed" emission re-selects via _on_nav_changed). */
+void
+ggaze_grid_move_cursor(GgazeGrid *p_grid, int i_dy) {
+ g_return_if_fail(GGAZE_IS_GRID(p_grid));
+ if (p_grid->p_nav == NULL || p_grid->p_flow == NULL) {
+ return;
+ }
+ GList *p_sel =
+ gtk_flow_box_get_selected_children(GTK_FLOW_BOX(p_grid->p_flow));
+ if (p_sel == NULL) {
+ return;
+ }
+ GtkWidget *p_cur = GTK_WIDGET(p_sel->data);
+ g_list_free(p_sel);
+ graphene_rect_t r_cur;
+ if (!gtk_widget_compute_bounds(p_cur, p_grid->p_flow, &r_cur) ||
+ r_cur.size.width == 0) {
+ return; /* not laid out yet */
+ }
+ GtkWidget *p_best = NULL;
+ float best_dy = 0;
+ float best_dx = 0;
+ GtkWidget *p_child = gtk_widget_get_first_child(p_grid->p_flow);
+ while (p_child != NULL) {
+ if (p_child != p_cur) {
+ graphene_rect_t r;
+ if (gtk_widget_compute_bounds(p_child, p_grid->p_flow, &r) &&
+ r.size.width != 0) {
+ float dy = r.origin.y - r_cur.origin.y;
+ if ((i_dy > 0 && dy > 0) || (i_dy < 0 && dy < 0)) {
+ float ady = ABS(dy);
+ float adx = ABS(r.origin.x - r_cur.origin.x);
+ if (p_best == NULL || ady < best_dy ||
+ (ady == best_dy && adx < best_dx)) {
+ p_best = p_child;
+ best_dy = ady;
+ best_dx = adx;
+ }
+ }
+ }
+ }
+ p_child = gtk_widget_get_next_sibling(p_child);
+ }
+ if (p_best != NULL) {
+ GFile *p_f = (GFile *)g_object_get_data(G_OBJECT(p_best), "file");
+ if (p_f != NULL) {
+ navigator_set_current_file(p_grid->p_nav, p_f);
+ }
+ }
+}
+
/* Borrowed pointer to the selected cell's file (NULL if nothing selected). */
GFile *
ggaze_grid_get_selected_file(GgazeGrid *p_grid) {
@@ -304,13 +369,53 @@ ggaze_grid_refresh(GgazeGrid *p_grid) {
GtkWidget *p_child = _make_cell(p_grid, p_file);
gtk_flow_box_append(GTK_FLOW_BOX(p_grid->p_flow), p_child);
}
+ p_grid->u_last_count = u_n;
_select_current(p_grid);
}
+/* Update every cell's "ggaze-marked" badge from the navigator's mark set
+ * in place (no rebuild), so mark changes and navigation don't blank the grid
+ * by recreating cells. */
+void
+ggaze_grid_refresh_mark_badges(GgazeGrid *p_grid) {
+ g_return_if_fail(GGAZE_IS_GRID(p_grid));
+ if (p_grid->p_nav == NULL) {
+ return;
+ }
+ GtkWidget *p_child = gtk_widget_get_first_child(p_grid->p_flow);
+ while (p_child != NULL) {
+ GFile *p_f = (GFile *)g_object_get_data(G_OBJECT(p_child), "file");
+ GtkWidget *p_box =
+ gtk_flow_box_child_get_child(GTK_FLOW_BOX_CHILD(p_child));
+ if (p_f != NULL && p_box != NULL) {
+ if (navigator_is_marked(p_grid->p_nav, p_f)) {
+ gtk_widget_add_css_class(p_box, "ggaze-marked");
+ } else {
+ gtk_widget_remove_css_class(p_box, "ggaze-marked");
+ }
+ }
+ p_child = gtk_widget_get_next_sibling(p_child);
+ }
+}
+
static void
_on_nav_changed(Navigator *p_nav, gpointer p_data) {
(void)p_nav;
- ggaze_grid_refresh(GGAZE_GRID(p_data));
+ GgazeGrid *p_grid = GGAZE_GRID(p_data);
+ if (p_grid->p_nav == NULL) {
+ return;
+ }
+ /* Only a structural change (files added/removed via rescan/trash/move)
+ * needs a full rebuild. A mere current/mark change just moves the
+ * selection and refreshes badges in place — rebuilding on every
+ * navigation keypress would recreate every GtkPicture and blank the grid
+ * while each cell re-requested its thumbnail. */
+ if (navigator_get_count(p_grid->p_nav) != p_grid->u_last_count) {
+ ggaze_grid_refresh(p_grid);
+ return;
+ }
+ ggaze_grid_refresh_mark_badges(p_grid);
+ _select_current(p_grid);
}
/* --- GObject ------------------------------------------------------------- */
diff --git a/src/gridview.h b/src/gridview.h
index bfbf816..f8a203f 100644
--- a/src/gridview.h
+++ b/src/gridview.h
@@ -34,10 +34,17 @@ void ggaze_grid_set_hide_trashed(GgazeGrid *p_grid, gboolean b_hide);
/* Rebuild the cells from the navigator (call after structural changes). */
void ggaze_grid_refresh(GgazeGrid *p_grid);
+/* Update each cell's mark badge in place from the navigator (no rebuild). */
+void ggaze_grid_refresh_mark_badges(GgazeGrid *p_grid);
+
/* Sync navigator.current to the flowbox's currently-selected cell, so leaving
* the grid (Enter / toggle-to-large) opens the highlighted image. */
gboolean ggaze_grid_sync_current(GgazeGrid *p_grid);
+/* Move the grid cursor one row down (i_dy = +1) or up (i_dy = -1), updating
+ * navigator.current so the header / large-view preview track the move. */
+void ggaze_grid_move_cursor(GgazeGrid *p_grid, int i_dy);
+
/* Borrowed pointer to the currently-selected cell's file (NULL if none). The
* pointer is owned by the cell; only valid while the cell lives. */
GFile *ggaze_grid_get_selected_file(GgazeGrid *p_grid);
diff --git a/src/window.c b/src/window.c
index 8e74db8..2810d0e 100644
--- a/src/window.c
+++ b/src/window.c
@@ -324,6 +324,20 @@ static const char *SHORTCUTS_UI =
" </child>\n"
" <child>\n"
" <object class=\"GtkShortcutsShortcut\">\n"
+ " <property name=\"accelerator\">j</property>\n"
+ " <property name=\"title\">Cursor down one row (grid)\n"
+ " </property>\n"
+ " </object>\n"
+ " </child>\n"
+ " <child>\n"
+ " <object class=\"GtkShortcutsShortcut\">\n"
+ " <property name=\"accelerator\">k</property>\n"
+ " <property name=\"title\">Cursor up one row (grid)\n"
+ " </property>\n"
+ " </object>\n"
+ " </child>\n"
+ " <child>\n"
+ " <object class=\"GtkShortcutsShortcut\">\n"
" <property name=\"accelerator\">g</property>\n"
" <property name=\"title\">First image</property>\n"
" </object>\n"
@@ -371,7 +385,8 @@ static const char *SHORTCUTS_UI =
" <child>\n"
" <object class=\"GtkShortcutsShortcut\">\n"
" <property name=\"accelerator\">v</property>\n"
- " <property name=\"title\">Toggle mark on highlighted</property>\n"
+ " <property name=\"title\">Toggle mark on "
+ "highlighted</property>\n"
" </object>\n"
" </child>\n"
" <child>\n"
@@ -434,7 +449,8 @@ static const char *SHORTCUTS_UI =
" </child>\n"
" <child>\n"
" <object class=\"GtkShortcutsShortcut\">\n"
- " <property name=\"accelerator\">minus underscore</property>\n"
+ " <property name=\"accelerator\">minus "
+ "underscore</property>\n"
" <property name=\"title\">Zoom out</property>\n"
" </object>\n"
" </child>\n"