summaryrefslogtreecommitdiff
path: root/src/window.c
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-20 19:46:42 +0300
committerPaul Buetow <paul@buetow.org>2026-07-20 19:46:42 +0300
commitde9ceaeb0cc7040153c3ba1288180bd38767f639 (patch)
tree6a3fd48200ac87622659f9cea013da3ad70ad27b /src/window.c
parentb69ed3923e42f122910242e724ee9dc43208d7b4 (diff)
grid: mark multi-selection (v / Ctrl+a / Esc) and a ? shortcuts overlay
The navigator already had a mark API and the grid rendered mark badges, but nothing could set marks — so multi-file ops (D, Ctrl+c, m) only ever acted on the single current image. Wire up marking in the UI: - v toggles a mark on the highlighted grid cell (or the current image in large view). navigator_toggle_mark does not emit "changed", so the cell's badge is updated in place via ggaze_grid_update_mark_badge (no reflow / re-decode). - Ctrl+a marks all; Esc now clears marks first (contextual), then keeps its existing back/quit behavior. - The window title appends "N marked" so multi-selection is visible. - A GtkCssProvider styles .ggaze-marked (accent border + tint) — the class was set but had no styling, so badges were invisible. Also add a ? -> win.shortcuts action that presents a GtkShortcutsWindow (grouped: navigation, view, selection, files, zoom) built from an inline GtkBuilder UI string, transient to the window. tests/test_shortcut.c full-table test updated for the three new bindings (v, Ctrl+a, ?) -> 23 rows, 19 actions.
Diffstat (limited to 'src/window.c')
-rw-r--r--src/window.c211
1 files changed, 211 insertions, 0 deletions
diff --git a/src/window.c b/src/window.c
index e28ca0a..50a6e04 100644
--- a/src/window.c
+++ b/src/window.c
@@ -253,6 +253,173 @@ _action_toggle_view(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
}
}
+/* Toggle a mark on the highlighted grid cell (grid view) or the current image
+ * (large view). Marks are ggaze's multi-selection: D / Ctrl+c / m act on the
+ * marked set. Toggle does not emit navigator "changed", so the grid cell's
+ * badge is updated in place (no reflow/re-decode). */
+static void
+_action_mark(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
+ (void)p_a;
+ (void)p_v;
+ GgazeWindow *p_win = GGAZE_WINDOW(p_data);
+ if (p_win->p_nav == NULL) {
+ return;
+ }
+ GFile *p_target = NULL;
+ const char *c_cur =
+ gtk_stack_get_visible_child_name(GTK_STACK(p_win->p_stack));
+ if (g_strcmp0(c_cur, "grid") == 0 && p_win->p_grid != NULL) {
+ p_target = ggaze_grid_get_selected_file(p_win->p_grid);
+ }
+ if (p_target == NULL) {
+ p_target = navigator_get_current(p_win->p_nav);
+ }
+ if (p_target == NULL) {
+ return;
+ }
+ navigator_toggle_mark(p_win->p_nav, p_target);
+ if (p_win->p_grid != NULL) {
+ ggaze_grid_update_mark_badge(p_win->p_grid, p_target);
+ }
+ _update_header(p_win);
+}
+
+static void
+_action_mark_all(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
+ (void)p_a;
+ (void)p_v;
+ GgazeWindow *p_win = GGAZE_WINDOW(p_data);
+ if (p_win->p_nav == NULL) {
+ return;
+ }
+ navigator_mark_all(p_win->p_nav); /* emits "changed" -> grid refresh */
+ _update_header(p_win);
+}
+
+/* GtkBuilder UI for the shortcuts overlay (?). Accel strings use gtk
+ * accelerator syntax: "h Left" means h OR Left triggers it. */
+static const char *SHORTCUTS_UI =
+ "<interface>"
+ " <object class=\"GtkShortcutsWindow\" id=\"shortcuts\">"
+ " <property name=\"modal\">True</property>"
+ " <property name=\"section-name\">shortcuts</property>"
+ " <child>"
+ " <object class=\"GtkShortcutsSection\" id=\"sec\">"
+ " <property name=\"section-name\">shortcuts</property>"
+ " <property name=\"title\">ggaze</property>"
+ " <child>"
+ " <object class=\"GtkShortcutsGroup\">"
+ " <property name=\"title\">Navigation</property>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">h Left</property>"
+ " <property name=\"title\">Previous "
+ "image</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">l Right</property>"
+ " <property name=\"title\">Next "
+ "image</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">g</property>"
+ " <property name=\"title\">First "
+ "image</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">Shift+G</property>"
+ " <property name=\"title\">Last "
+ "image</property></object></child>"
+ " </object></child>"
+ " <child>"
+ " <object class=\"GtkShortcutsGroup\">"
+ " <property name=\"title\">View</property>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">t</property>"
+ " <property name=\"title\">Toggle large / "
+ "grid</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">f</property>"
+ " <property "
+ "name=\"title\">Fullscreen</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">Shift+S</property>"
+ " <property "
+ "name=\"title\">Slideshow</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">i</property>"
+ " <property name=\"title\">Info "
+ "overlay</property></object></child>"
+ " </object></child>"
+ " <child>"
+ " <object class=\"GtkShortcutsGroup\">"
+ " <property name=\"title\">Selection (marks)</property>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">v</property>"
+ " <property name=\"title\">Toggle mark on "
+ "highlighted</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">Ctrl+a</property>"
+ " <property name=\"title\">Mark all</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">Escape</property>"
+ " <property name=\"title\">Clear marks / "
+ "back</property></object></child>"
+ " </object></child>"
+ " <child>"
+ " <object class=\"GtkShortcutsGroup\">"
+ " <property name=\"title\">Files</property>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">o</property>"
+ " <property name=\"title\">Open</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">d</property>"
+ " <property name=\"title\">Trash</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">Shift+D</property>"
+ " <property name=\"title\">Delete "
+ "permanently</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">u</property>"
+ " <property name=\"title\">Undo</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">q</property>"
+ " <property name=\"title\">Quit</property></object></child>"
+ " </object></child>"
+ " <child>"
+ " <object class=\"GtkShortcutsGroup\">"
+ " <property name=\"title\">Zoom</property>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">plus equal</property>"
+ " <property name=\"title\">Zoom in</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">minus underscore</property>"
+ " <property name=\"title\">Zoom out</property></object></child>"
+ " <child><object class=\"GtkShortcutsShortcut\">"
+ " <property name=\"accelerator\">question</property>"
+ " <property name=\"title\">Show this "
+ "help</property></object></child>"
+ " </object></child>"
+ " </object></child>"
+ " </child>"
+ " </object></child>"
+ "</interface>";
+
+static void
+_action_shortcuts(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
+ (void)p_a;
+ (void)p_v;
+ GgazeWindow *p_win = GGAZE_WINDOW(p_data);
+ GtkBuilder *p_b = gtk_builder_new_from_string(SHORTCUTS_UI, -1);
+ GtkShortcutsWindow *p_w =
+ GTK_SHORTCUTS_WINDOW(gtk_builder_get_object(p_b, "shortcuts"));
+ if (p_w == NULL) {
+ g_object_unref(p_b);
+ return;
+ }
+ gtk_window_set_transient_for(GTK_WINDOW(p_w), GTK_WINDOW(p_win));
+ gtk_window_set_title(GTK_WINDOW(p_w), "ggaze — keyboard shortcuts");
+ /* Keep the builder alive for the window's lifetime, drop it on close. */
+ g_signal_connect_swapped(p_w, "destroy", G_CALLBACK(g_object_unref), p_b);
+ gtk_window_present(GTK_WINDOW(p_w));
+}
+
static void
_action_zoom_in(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
(void)p_a;
@@ -330,6 +497,12 @@ _action_back(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
if (p_win->b_fullscreen) {
gtk_window_unfullscreen(GTK_WINDOW(p_win));
p_win->b_fullscreen = FALSE;
+ } else if (p_win->p_nav != NULL &&
+ navigator_get_mark_count(p_win->p_nav) > 0) {
+ /* Contextual Esc: clear marks before backing out (docs/ui-and-
+ * interactions.md marks). Emits "changed" -> grid refreshes badges. */
+ navigator_clear_marks(p_win->p_nav);
+ _update_header(p_win);
} else {
const char *c_cur =
gtk_stack_get_visible_child_name(GTK_STACK(p_win->p_stack));
@@ -398,6 +571,9 @@ static const GActionEntry ACTIONS[] = {
{.name = "delete", .activate = _action_delete},
{.name = "undo", .activate = _action_undo},
{.name = "toggle-view", .activate = _action_toggle_view},
+ {.name = "mark", .activate = _action_mark},
+ {.name = "mark-all", .activate = _action_mark_all},
+ {.name = "shortcuts", .activate = _action_shortcuts},
{.name = "zoom-in", .activate = _action_zoom_in},
{.name = "zoom-out", .activate = _action_zoom_out},
{.name = "fullscreen", .activate = _action_fullscreen},
@@ -615,6 +791,14 @@ _update_header(GgazeWindow *p_win) {
}
g_free(c_name);
}
+ /* Append the marked count so multi-selection is visible in the title. */
+ guint u_marks = navigator_get_mark_count(p_win->p_nav);
+ if (u_marks > 0 && c_title != NULL) {
+ char *c_tmp =
+ g_strdup_printf("%s \u00b7 %u marked", c_title, u_marks);
+ g_free(c_title);
+ c_title = c_tmp;
+ }
}
if (c_title == NULL) {
c_title = g_strdup("ggaze");
@@ -665,8 +849,35 @@ ggaze_window_class_init(GgazeWindowClass *p_klass) {
p_obj_class->dispose = ggaze_window_dispose;
}
+/* Load the small ggaze stylesheet once (mark badge styling — the navigator's
+ * mark API has no visual representation without it). */
+static void
+_ensure_css(void) {
+ static gboolean b_done = FALSE;
+ if (b_done) {
+ return;
+ }
+ b_done = TRUE;
+ GtkCssProvider *p_css = gtk_css_provider_new();
+ gtk_css_provider_load_from_string(
+ p_css, "/* marked-thumbnail badge (multi-selection). */\n"
+ ".ggaze-marked {\n"
+ " border: 2px solid #3584e4;\n"
+ " border-radius: 4px;\n"
+ " background-color: rgba(53, 132, 228, 0.15);\n"
+ "}\n");
+ GdkDisplay *p_disp = gdk_display_get_default();
+ if (p_disp != NULL) {
+ gtk_style_context_add_provider_for_display(
+ p_disp, GTK_STYLE_PROVIDER(p_css),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+ g_object_unref(p_css);
+}
+
static void
ggaze_window_init(GgazeWindow *p_win) {
+ _ensure_css();
p_win->p_cancel = g_cancellable_new();
p_win->p_prefetch_cancel = g_cancellable_new();
p_win->p_cache = texturecache_new(4);