diff options
Diffstat (limited to 'src/gridview.c')
| -rw-r--r-- | src/gridview.c | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/src/gridview.c b/src/gridview.c new file mode 100644 index 0000000..c8336e8 --- /dev/null +++ b/src/gridview.c @@ -0,0 +1,383 @@ +/*:* + * ggaze — thumbnail grid view + * + * GgazeGrid wraps a GtkFlowBox (in a GtkScrolledWindow): one cell per + * navigator file, thumbnails loaded lazily (async, on realize) from the + * thumbnail cache. Removed (trashed/deleted) cells are dimmed; marked cells + * get a check badge. Resize (+/-) updates the cell size and reflows. Enter or + * double-click emits "activate". Cursor follows navigator.current. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "gridview.h" + +#include <glib.h> + +/* Per-cell data, attached to the GtkPicture via qdata. */ +typedef struct { + GFile *p_file; /* owned ref */ + GFile *p_expected; /* the file this thumbnail request is for (owned) */ +} CellData; + +static const char *CELL_DATA_KEY = "ggaze-cell-data"; + +struct _GgazeGrid { + GtkWidget parent_instance; + Navigator *p_nav; + Thumbnail *p_thumb; + GtkWidget *p_flow; /* GtkFlowBox */ + GtkWidget *p_scrolled; /* GtkScrolledWindow */ + int i_size; + gboolean b_hide_trashed; + guint u_nav_handler; + GCancellable *p_cancel; /* cancels pending thumbnails on dispose */ +}; + +G_DEFINE_TYPE(GgazeGrid, ggaze_grid, GTK_TYPE_WIDGET) + +static guint u_activate_signal = 0; + +static void +_cell_data_free(gpointer p_void) { + CellData *p_d = (CellData *)p_void; + g_clear_object(&p_d->p_file); + g_clear_object(&p_d->p_expected); + g_free(p_d); +} + +static CellData * +_cell_data(GtkWidget *p_pic) { + return ((CellData *)g_object_get_data(G_OBJECT(p_pic), CELL_DATA_KEY)); +} + +/* --- thumbnail request --------------------------------------------------- */ + +static void +_thumb_finish_cb(GObject *p_src, GAsyncResult *p_res, gpointer p_data) { + (void)p_src; + GtkWidget *p_pic = (GtkWidget *)p_data; + CellData *p_d = _cell_data(p_pic); + if (p_d == NULL) { + g_object_unref(p_pic); + return; + } + GError *p_err = NULL; + GdkTexture *p_tex = thumbnail_get_finish(NULL, p_res, &p_err); + if (p_tex != NULL) { + /* Only apply if this request is still for this cell's file. */ + if (p_d->p_expected != NULL && + g_file_equal(p_d->p_expected, p_d->p_file)) { + gtk_picture_set_paintable(GTK_PICTURE(p_pic), (GdkPaintable *)p_tex); + } + g_object_unref(p_tex); + } else { + g_clear_error(&p_err); + } + g_clear_object(&p_d->p_expected); + g_object_unref(p_pic); +} + +static void +_request_thumbnail(GtkWidget *p_pic) { + CellData *p_d = _cell_data(p_pic); + if (p_d == NULL || p_d->p_file == NULL) { + return; + } + if (p_d->p_expected != NULL) { + return; /* already in flight */ + } + p_d->p_expected = (GFile *)g_object_ref(p_d->p_file); + GgazeGrid *p_grid = + GGAZE_GRID(gtk_widget_get_ancestor(p_pic, GGAZE_TYPE_GRID)); + if (p_grid == NULL || p_grid->p_thumb == NULL) { + g_clear_object(&p_d->p_expected); + return; + } + thumbnail_get_async(p_grid->p_thumb, p_d->p_file, p_grid->i_size, + p_grid->p_cancel, _thumb_finish_cb, g_object_ref(p_pic)); +} + +static void +_on_pic_map(GtkWidget *p_pic, gpointer p_data) { + (void)p_data; + _request_thumbnail(p_pic); +} + +/* --- cell construction --------------------------------------------------- */ + +static GtkWidget * +_make_picture(GgazeGrid *p_grid, GFile *p_file) { + GtkWidget *p_pic = gtk_picture_new(); + gtk_widget_set_size_request(p_pic, p_grid->i_size, p_grid->i_size); + gtk_picture_set_content_fit(GTK_PICTURE(p_pic), GTK_CONTENT_FIT_CONTAIN); + CellData *p_d = g_new(CellData, 1); + p_d->p_file = (GFile *)g_object_ref(p_file); + p_d->p_expected = NULL; + g_object_set_data_full(G_OBJECT(p_pic), CELL_DATA_KEY, p_d, _cell_data_free); + g_signal_connect(p_pic, "map", G_CALLBACK(_on_pic_map), NULL); + return (p_pic); +} + +static GtkWidget * +_make_cell(GgazeGrid *p_grid, GFile *p_file) { + GtkWidget *p_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + gtk_widget_set_size_request(p_box, p_grid->i_size + 4, p_grid->i_size + 18); + GtkWidget *p_pic = _make_picture(p_grid, p_file); + gtk_box_append(GTK_BOX(p_box), p_pic); + char *c_name = g_file_get_basename(p_file); + GtkWidget *p_label = gtk_label_new(c_name); + g_free(c_name); + gtk_label_set_max_width_chars(GTK_LABEL(p_label), 20); + gtk_label_set_ellipsize(GTK_LABEL(p_label), PANGO_ELLIPSIZE_END); + gtk_widget_add_css_class(p_label, "caption"); + gtk_box_append(GTK_BOX(p_box), p_label); + + /* mark badge */ + if (navigator_is_marked(p_grid->p_nav, p_file)) { + gtk_widget_add_css_class(p_box, "ggaze-marked"); + } + /* dim removed */ + if (navigator_is_removed(p_grid->p_nav, p_file)) { + gtk_widget_set_opacity(p_box, 0.35); + gtk_widget_add_css_class(p_box, "ggaze-removed"); + } + + GtkWidget *p_child = gtk_flow_box_child_new(); + gtk_flow_box_child_set_child(GTK_FLOW_BOX_CHILD(p_child), p_box); + g_object_set_data_full(G_OBJECT(p_child), "file", g_object_ref(p_file), + (GDestroyNotify)g_object_unref); + return (p_child); +} + +static void +_on_child_activated(GtkFlowBox *p_flow, GtkFlowBoxChild *p_child, + gpointer p_data) { + (void)p_flow; + GgazeGrid *p_grid = GGAZE_GRID(p_data); + GFile *p_file = (GFile *)g_object_get_data(G_OBJECT(p_child), "file"); + if (p_file != NULL) { + navigator_set_current_file(p_grid->p_nav, p_file); + } + g_signal_emit(p_grid, u_activate_signal, 0); +} + +static gboolean +_on_flow_key(GtkEventControllerKey *p_key, guint u_kv, guint u_kc, + GdkModifierType e_st, gpointer p_data) { + (void)p_key; + (void)u_kc; + (void)e_st; + GgazeGrid *p_grid = GGAZE_GRID(p_data); + if (u_kv == GDK_KEY_Return || u_kv == GDK_KEY_KP_Enter) { + g_signal_emit(p_grid, u_activate_signal, 0); + return (TRUE); + } + if (u_kv == GDK_KEY_d) { + /* `d` in the grid trashes the current file (window handles via action). + */ + return (FALSE); + } + return (FALSE); +} + +/* --- refresh / rebuild --------------------------------------------------- */ + +static void +_clear_flow(GgazeGrid *p_grid) { + GtkWidget *p_child = gtk_widget_get_first_child(p_grid->p_flow); + while (p_child != NULL) { + GtkWidget *p_next = gtk_widget_get_next_sibling(p_child); + gtk_flow_box_remove(GTK_FLOW_BOX(p_grid->p_flow), p_child); + p_child = p_next; + } +} + +static void +_select_current(GgazeGrid *p_grid) { + GFile *p_cur = navigator_get_current(p_grid->p_nav); + if (p_cur == 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"); + if (p_f != NULL && g_file_equal(p_f, p_cur)) { + GtkFlowBoxChild *p_fc = GTK_FLOW_BOX_CHILD(p_child); + gtk_flow_box_select_child(GTK_FLOW_BOX(p_grid->p_flow), p_fc); + gtk_widget_grab_focus(p_child); + return; + } + p_child = gtk_widget_get_next_sibling(p_child); + } +} + +void +ggaze_grid_refresh(GgazeGrid *p_grid) { + g_return_if_fail(GGAZE_IS_GRID(p_grid)); + _clear_flow(p_grid); + + guint u_n = navigator_get_count(p_grid->p_nav); + for (guint u_i = 0; u_i < u_n; u_i++) { + GFile *p_file = navigator_get_file(p_grid->p_nav, u_i); + if (p_file == NULL) { + continue; + } + if (p_grid->b_hide_trashed && + navigator_is_removed(p_grid->p_nav, p_file)) { + continue; + } + GtkWidget *p_child = _make_cell(p_grid, p_file); + gtk_flow_box_append(GTK_FLOW_BOX(p_grid->p_flow), p_child); + } + _select_current(p_grid); +} + +static void +_on_nav_changed(Navigator *p_nav, gpointer p_data) { + (void)p_nav; + ggaze_grid_refresh(GGAZE_GRID(p_data)); +} + +/* --- GObject ------------------------------------------------------------- */ + +static void +ggaze_grid_dispose(GObject *p_obj) { + GgazeGrid *p_grid = GGAZE_GRID(p_obj); + if (p_grid->p_cancel != NULL) { + g_cancellable_cancel(p_grid->p_cancel); + g_clear_object(&p_grid->p_cancel); + } + if (p_grid->p_nav != NULL && p_grid->u_nav_handler != 0) { + g_signal_handler_disconnect(p_grid->p_nav, p_grid->u_nav_handler); + p_grid->u_nav_handler = 0; + } + p_grid->p_nav = NULL; /* detached; don't touch it after this */ + g_clear_pointer(&p_grid->p_scrolled, gtk_widget_unparent); + G_OBJECT_CLASS(ggaze_grid_parent_class)->dispose(p_obj); +} + +static void +ggaze_grid_finalize(GObject *p_obj) { + GgazeGrid *p_grid = GGAZE_GRID(p_obj); + /* p_nav/p_thumb are borrowed (owned by the window). */ + (void)p_grid; + G_OBJECT_CLASS(ggaze_grid_parent_class)->finalize(p_obj); +} + +static void +ggaze_grid_class_init(GgazeGridClass *p_klass) { + GObjectClass *p_oc = G_OBJECT_CLASS(p_klass); + p_oc->dispose = ggaze_grid_dispose; + p_oc->finalize = ggaze_grid_finalize; + u_activate_signal = + g_signal_new("activate", G_TYPE_FROM_CLASS(p_klass), G_SIGNAL_RUN_LAST, 0, + NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 0); +} + +static void +ggaze_grid_init(GgazeGrid *p_grid) { + p_grid->i_size = 128; + p_grid->b_hide_trashed = FALSE; + p_grid->p_scrolled = gtk_scrolled_window_new(); + gtk_widget_set_parent(p_grid->p_scrolled, GTK_WIDGET(p_grid)); + gtk_widget_set_hexpand(p_grid->p_scrolled, TRUE); + gtk_widget_set_vexpand(p_grid->p_scrolled, TRUE); + p_grid->p_flow = gtk_flow_box_new(); + gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(p_grid->p_flow), TRUE); + gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(p_grid->p_flow), + FALSE); + g_signal_connect(p_grid->p_flow, "child-activated", + G_CALLBACK(_on_child_activated), p_grid); + GtkEventController *p_key = gtk_event_controller_key_new(); + g_signal_connect(p_key, "key-pressed", G_CALLBACK(_on_flow_key), p_grid); + gtk_widget_add_controller(p_grid->p_flow, p_key); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(p_grid->p_scrolled), + p_grid->p_flow); +} + +/* --- public ------------------------------------------------------------- */ + +GtkWidget * +ggaze_grid_new(Navigator *p_nav, Thumbnail *p_thumb, int i_size, + gboolean b_hide_trashed) { + g_return_val_if_fail(GGAZE_IS_NAVIGATOR(p_nav), NULL); + GgazeGrid *p_grid = GGAZE_GRID(g_object_new(GGAZE_TYPE_GRID, NULL)); + p_grid->p_nav = p_nav; + p_grid->p_thumb = p_thumb; + p_grid->i_size = (i_size <= 0) ? 128 : i_size; + p_grid->b_hide_trashed = b_hide_trashed; + p_grid->u_nav_handler = + g_signal_connect(p_nav, "changed", G_CALLBACK(_on_nav_changed), p_grid); + ggaze_grid_refresh(p_grid); + return (GTK_WIDGET(p_grid)); +} + +void +ggaze_grid_set_thumbnail_size(GgazeGrid *p_grid, int i_size) { + g_return_if_fail(GGAZE_IS_GRID(p_grid)); + if (i_size <= 0) { + i_size = 128; + } + if (p_grid->i_size == i_size) { + return; + } + p_grid->i_size = i_size; + /* Update the size request of every cell's picture and reflow. */ + GtkWidget *p_child = gtk_widget_get_first_child(p_grid->p_flow); + while (p_child != NULL) { + GtkWidget *p_box = + gtk_flow_box_child_get_child(GTK_FLOW_BOX_CHILD(p_child)); + if (p_box != NULL) { + GtkWidget *p_pic = gtk_widget_get_first_child(p_box); + if (p_pic != NULL) { + gtk_widget_set_size_request(p_pic, i_size, i_size); + } + gtk_widget_set_size_request(p_box, i_size + 4, i_size + 18); + } + p_child = gtk_widget_get_next_sibling(p_child); + } + gtk_widget_queue_resize(p_grid->p_flow); +} + +void +ggaze_grid_set_hide_trashed(GgazeGrid *p_grid, gboolean b_hide) { + g_return_if_fail(GGAZE_IS_GRID(p_grid)); + if (p_grid->b_hide_trashed == b_hide) { + return; + } + p_grid->b_hide_trashed = b_hide; + ggaze_grid_refresh(p_grid); +} + +void +ggaze_grid_detach(GgazeGrid *p_grid) { + g_return_if_fail(GGAZE_IS_GRID(p_grid)); + if (p_grid->p_cancel != NULL) { + g_cancellable_cancel(p_grid->p_cancel); + g_clear_object(&p_grid->p_cancel); + } + if (p_grid->p_nav != NULL && p_grid->u_nav_handler != 0) { + g_signal_handler_disconnect(p_grid->p_nav, p_grid->u_nav_handler); + p_grid->u_nav_handler = 0; + } + p_grid->p_nav = NULL; +} + +guint +ggaze_grid_get_count(GgazeGrid *p_grid) { + g_return_val_if_fail(GGAZE_IS_GRID(p_grid), 0); + guint u_count = 0; + GtkWidget *p_child = gtk_widget_get_first_child(p_grid->p_flow); + while (p_child != NULL) { + u_count++; + p_child = gtk_widget_get_next_sibling(p_child); + } + return (u_count); +} + +guint +ggaze_grid_activate_signal(void) { + return (u_activate_signal); +}
\ No newline at end of file |
