/*:*
* ggaze — main window
*
* GgazeWindow : GtkApplicationWindow owns an AdwHeaderBar + a GtkStack with two
* children ("grid" placeholder until M7, "large" = the GgazeViewer). M2 adds a
* Navigator over the current folder, a single GCancellable (last-write-wins),
* keybinding->action shortcuts, and a file/folder drop target. The header
* title carries "filename · n/total". See docs/architecture.md.
*
* Copyright (c) 2026 ggaze contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*:*/
#include "window.h"
#include <adwaita.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <string.h>
#include <gtk/gtk.h>
#include "ggaze-config.h"
#include "gridview.h"
#include "info.h"
#include "loader/loader.h"
#include "navigator.h"
#include "shortcuts.h"
#include "texturecache.h"
#include "thumbnail.h"
#include "trash.h"
#include "viewer.h"
#if GGAZE_HAVE_GEGL
#include "enhancer.h"
#endif
struct _GgazeWindow {
GtkApplicationWindow parent_instance;
Navigator *p_nav; /* current folder listing (NULL until open) */
GCancellable *p_cancel; /* visible load; cancelled on each nav */
GCancellable *p_prefetch_cancel; /* prefetch round; cancelled on new round */
TextureCache *p_cache; /* bounded LRU of decoded GdkTextures */
Thumbnail *p_thumb; /* TMS thumbnail cache */
Trash *p_trash; /* ./Trash bin for the current folder */
GtkWidget *p_stack; /* GtkStack: grid / large (viewer) */
GtkWidget *p_viewer; /* GgazeViewer — the large view */
GgazeGrid *p_grid; /* the thumbnail grid (the "grid" stack child) */
int i_grid_size; /* current thumbnail size (64-512, decision T) */
GtkWidget *p_overlay; /* GtkOverlay wrapping the stack (for info label) */
GtkWidget *p_info_lbl; /* info overlay label (auto-hides) */
guint u_info_hide; /* info auto-hide timeout id (0=none) */
guint u_slideshow; /* slideshow timeout id (0=off) */
gboolean b_fullscreen;
guint u_hdr_hide; /* fullscreen header auto-hide timeout */
#if GGAZE_HAVE_GEGL
guint8 u_enhance_mask; /* bit i -> preset i enabled (layered) */
Enhancer *p_enhancer; /* GEGL preset engine (NULL w/o GEGL) */
GtkWidget *p_enhance_panel; /* GtkRevealer side panel (NULL w/o GEGL) */
GtkWidget *p_enhance_btns[8]; /* preset row buttons (for highlighting) */
#endif
GtkWidget *p_content; /* horizontal box: [enhance panel] + image area */
};
G_DEFINE_TYPE(GgazeWindow, ggaze_window, GTK_TYPE_APPLICATION_WINDOW)
/* --- forward decls ------------------------------------------------------- */
static void _load_current(GgazeWindow *p_win);
static void _prefetch(GgazeWindow *p_win);
static void _show_texture(GgazeWindow *p_win, GdkTexture *p_tex);
static void _update_header(GgazeWindow *p_win);
static void _on_grid_activate(GgazeGrid *p_grid, gpointer p_data);
static void _show_info(GgazeWindow *p_win);
static void _hide_info(GgazeWindow *p_win);
static gboolean _slideshow_tick(gpointer p_data);
#if GGAZE_HAVE_GEGL
static void _enhance_update_highlights(GgazeWindow *p_win);
static void _enhance_panel_reparent(GgazeWindow *p_win, gboolean b_overlay);
static gboolean _enhance_do_save(GgazeWindow *p_win);
#endif
#if GGAZE_HAVE_GEGL
/* Export the current image with the enabled-preset chain to
* <stem>-enhanced.<ext>. Returns TRUE on success; prints the saved name. */
static gboolean
_enhance_do_save(GgazeWindow *p_win) {
if (p_win->p_nav == NULL || p_win->p_enhancer == NULL ||
p_win->u_enhance_mask == 0) {
return (FALSE);
}
GFile *p_file = navigator_get_current(p_win->p_nav);
if (p_file == NULL) {
return (FALSE);
}
char *c_base = g_file_get_basename(p_file);
char *c_dot = strrchr(c_base, '.');
const char *c_ext = ".jpg";
if (c_dot != NULL && (g_ascii_strcasecmp(c_dot, ".jpg") == 0 ||
g_ascii_strcasecmp(c_dot, ".jpeg") == 0 ||
g_ascii_strcasecmp(c_dot, ".png") == 0 ||
g_ascii_strcasecmp(c_dot, ".webp") == 0)) {
c_ext = c_dot;
}
char *c_stem;
if (c_dot != NULL && c_ext == c_dot) {
c_stem = g_strndup(c_base, (gsize)(c_dot - c_base));
} else {
c_stem = g_strdup(c_base);
}
GFile *p_dir = g_file_get_parent(p_file);
char *c_outname = g_strdup_printf("%s-enhanced%s", c_stem, c_ext);
GFile *p_out = g_file_get_child(p_dir, c_outname);
g_free(c_outname);
g_free(c_stem);
g_free(c_base);
g_object_unref(p_dir);
GError *p_err = NULL;
GeglBuffer *p_buf = enhancer_load(p_file, &p_err);
gboolean b_ok = FALSE;
if (p_buf != NULL) {
const GPtrArray *p_presets = enhancer_get_presets(p_win->p_enhancer);
b_ok = enhancer_export_chain(p_win->p_enhancer, p_buf, p_presets,
p_win->u_enhance_mask, p_out, &p_err);
g_object_unref(p_buf);
}
char *c_saved = b_ok ? g_file_get_basename(p_out) : NULL;
g_object_unref(p_out);
if (b_ok) {
g_printerr("ggaze: saved %s\n", c_saved);
} else {
g_warning("ggaze: enhance-save failed: %s",
p_err != NULL ? p_err->message : "(no detail)");
}
g_free(c_saved);
g_clear_error(&p_err);
return (b_ok);
}
typedef struct {
GgazeWindow *p_win;
GSourceFunc fn;
gpointer data;
} _SaveCtx;
/* Alert-dialog response: 0=Cancel, 1=Discard, 2=Save. */
static void
_save_dialog_cb(GObject *p_dlg, GAsyncResult *p_res, gpointer p_data) {
_SaveCtx *p_ctx = (_SaveCtx *)p_data;
GgazeWindow *p_win = p_ctx->p_win;
GError *p_err = NULL;
gint i_btn =
gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(p_dlg),
|