summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-22 09:55:50 +0300
committerPaul Buetow <paul@buetow.org>2026-07-22 09:55:50 +0300
commit00f9ad7f18349781a679e7b3281d7e8f84ebb5b1 (patch)
tree7d38c655a3b0e48bc0d25952db679a2903dc3ff3 /src
parentd39915e214b66423e5edff788d81199c9db0b045 (diff)
enhance: layered (composable) filter toggles, highlighted rows, side panel next to image
Filters are now independent toggles that compose: the active set is a bitmask (u_enhance_mask) and the enabled GEGL ops are chained in array order (enhancer_apply_chain / enhancer_export_chain). Press 1-8 to toggle any preset on/off at any time; 0 / the Original row clears all; the live preview re-applies the whole stack each time, so you can add/remove filters in any order. The window title lists the enabled presets. The side panel now sits NEXT TO the image (a horizontal content box: [panel] + image area) instead of over it; in fullscreen it reparents to an overlay over the image so fullscreen stays full. Enabled rows are highlighted (.ggaze-enhance-on: accent background, white bold). enhancer.c: extract _op_for_builtin, add enhancer_apply_chain and enhancer_export_chain (single-preset apply/export kept for the tests); _save_buffer factored out (ju0/ku0 fixes unchanged). New test /enhancer/apply_chain (compose + empty-mask rejection).
Diffstat (limited to 'src')
-rw-r--r--src/enhancer.c191
-rw-r--r--src/enhancer.h12
-rw-r--r--src/window.c250
3 files changed, 316 insertions, 137 deletions
diff --git a/src/enhancer.c b/src/enhancer.c
index aea40f9..26e4ca3 100644
--- a/src/enhancer.c
+++ b/src/enhancer.c
@@ -67,40 +67,53 @@ enhancer_get_presets(Enhancer *e) {
return e ? e->p_presets : NULL;
}
+/* Create the GEGL op node for a built-in preset (in p_graph), or NULL if the
+ * name is unknown. */
+static GeglNode *
+_op_for_builtin(GeglNode *p_graph, const char *c_name) {
+ if (g_str_equal(c_name, "Auto-fix")) {
+ return (gegl_node_new_child(p_graph, "operation", "gegl:stretch-contrast",
+ NULL));
+ }
+ if (g_str_equal(c_name, "Brightness")) {
+ return (gegl_node_new_child(p_graph, "operation", "gegl:exposure",
+ "exposure", 0.5, NULL));
+ }
+ if (g_str_equal(c_name, "Contrast")) {
+ return (gegl_node_new_child(p_graph, "operation",
+ "gegl:brightness-contrast", "contrast", 1.3,
+ NULL));
+ }
+ if (g_str_equal(c_name, "Saturation")) {
+ return (gegl_node_new_child(p_graph, "operation", "gegl:saturation",
+ "scale", 1.4, NULL));
+ }
+ if (g_str_equal(c_name, "Warm")) {
+ return (
+ gegl_node_new_child(p_graph, "operation", "gegl:color-enhance", NULL));
+ }
+ if (g_str_equal(c_name, "Cool")) {
+ return (gegl_node_new_child(p_graph, "operation", "gegl:exposure",
+ "exposure", -0.3, NULL));
+ }
+ if (g_str_equal(c_name, "Sharpen")) {
+ return (
+ gegl_node_new_child(p_graph, "operation", "gegl:unsharp-mask", NULL));
+ }
+ if (g_str_equal(c_name, "Denoise")) {
+ return (gegl_node_new_child(p_graph, "operation", "gegl:noise-reduction",
+ NULL));
+ }
+ return (NULL);
+}
+
/* Build and apply a GEGL graph for a built-in preset. */
static GeglBuffer *
_apply_builtin(GeglBuffer *p_in, const char *c_name, GError **p_err) {
GeglNode *p_graph = gegl_node_new();
GeglNode *p_src = gegl_node_new_child(
p_graph, "operation", "gegl:buffer-source", "buffer", p_in, NULL);
- GeglNode *p_op = NULL;
-
- if (g_str_equal(c_name, "Auto-fix")) {
- p_op = gegl_node_new_child(p_graph, "operation", "gegl:stretch-contrast",
- NULL);
- } else if (g_str_equal(c_name, "Brightness")) {
- p_op = gegl_node_new_child(p_graph, "operation", "gegl:exposure",
- "exposure", 0.5, NULL);
- } else if (g_str_equal(c_name, "Contrast")) {
- p_op =
- gegl_node_new_child(p_graph, "operation", "gegl:brightness-contrast",
- "contrast", 1.3, NULL);
- } else if (g_str_equal(c_name, "Saturation")) {
- p_op = gegl_node_new_child(p_graph, "operation", "gegl:saturation",
- "scale", 1.4, NULL);
- } else if (g_str_equal(c_name, "Warm")) {
- p_op =
- gegl_node_new_child(p_graph, "operation", "gegl:color-enhance", NULL);
- } else if (g_str_equal(c_name, "Cool")) {
- p_op = gegl_node_new_child(p_graph, "operation", "gegl:exposure",
- "exposure", -0.3, NULL);
- } else if (g_str_equal(c_name, "Sharpen")) {
- p_op =
- gegl_node_new_child(p_graph, "operation", "gegl:unsharp-mask", NULL);
- } else if (g_str_equal(c_name, "Denoise")) {
- p_op = gegl_node_new_child(p_graph, "operation", "gegl:noise-reduction",
- NULL);
- }
+ GeglNode *p_op = _op_for_builtin(p_graph, c_name);
if (p_op == NULL) {
g_object_unref(p_graph);
@@ -121,6 +134,59 @@ _apply_builtin(GeglBuffer *p_in, const char *c_name, GError **p_err) {
return p_out;
}
+/* Apply a chain of the enabled built-in presets (bit i of u_mask -> preset i)
+ * in array order, composing them. Returns a new buffer, or NULL if no preset
+ * is enabled or any op is unavailable. */
+GeglBuffer *
+enhancer_apply_chain(Enhancer *e, GeglBuffer *p_in, const GPtrArray *p_presets,
+ guint8 u_mask, GError **p_err) {
+ (void)e;
+ g_return_val_if_fail(p_in != NULL, NULL);
+ g_return_val_if_fail(p_presets != NULL, NULL);
+ GeglNode *p_graph = gegl_node_new();
+ GeglNode *p_src = gegl_node_new_child(
+ p_graph, "operation", "gegl:buffer-source", "buffer", p_in, NULL);
+ GeglNode *p_prev = p_src;
+ gboolean b_any = FALSE;
+ for (guint i = 0; i < p_presets->len && i < 8; i++) {
+ if ((u_mask & (guint8)(1u << i)) == 0) {
+ continue;
+ }
+ const EnhancerPreset *p_pr = g_ptr_array_index((GPtrArray *)p_presets, i);
+ if (!p_pr->i_builtin) {
+ g_object_unref(p_graph);
+ g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+ "enhancer: user graph presets not chainable");
+ return NULL;
+ }
+ GeglNode *p_op = _op_for_builtin(p_graph, p_pr->c_name);
+ if (p_op == NULL) {
+ g_object_unref(p_graph);
+ g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "enhancer: unknown preset '%s'", p_pr->c_name);
+ return NULL;
+ }
+ gegl_node_link(p_prev, p_op);
+ p_prev = p_op;
+ b_any = TRUE;
+ }
+ if (!b_any) {
+ g_object_unref(p_graph);
+ g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "enhancer: no preset enabled");
+ return NULL;
+ }
+ GeglRectangle st_rect = {0, 0, gegl_buffer_get_width(p_in),
+ gegl_buffer_get_height(p_in)};
+ GeglBuffer *p_out = gegl_buffer_new(&st_rect, babl_format("RGBA float"));
+ GeglNode *p_sink = gegl_node_new_child(
+ p_graph, "operation", "gegl:buffer-sink", "buffer", &p_out, NULL);
+ gegl_node_link(p_prev, p_sink);
+ gegl_node_process(p_sink);
+ g_object_unref(p_graph);
+ return p_out;
+}
+
GeglBuffer *
enhancer_apply(Enhancer *e, GeglBuffer *p_in, const EnhancerPreset *p_preset,
GError **p_err) {
@@ -171,20 +237,18 @@ _saver_for_ext(GFile *p_out) {
return c_op;
}
-gboolean
-enhancer_export(Enhancer *e, GeglBuffer *p_in, const EnhancerPreset *p_preset,
- GFile *p_out, GError **p_err) {
- (void)e;
- g_return_val_if_fail(p_in != NULL, FALSE);
- g_return_val_if_fail(p_out != NULL, FALSE);
-
+/* Save p_buf to p_out with the format chosen by the output extension.
+ * ju0: pick the saver by extension (jpg q95 / png / webp). ku0: verify the
+ * save actually produced a non-empty, newer file instead of trusting a
+ * pre-existing path. Returns TRUE on a real write. */
+static gboolean
+_save_buffer(GeglBuffer *p_buf, GFile *p_out, GError **p_err) {
char *c_path = g_file_get_path(p_out);
if (c_path == NULL) {
g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED,
"enhancer: non-local export path");
return FALSE;
}
-
const char *c_op = _saver_for_ext(p_out);
if (c_op == NULL) {
g_free(c_path);
@@ -192,21 +256,10 @@ enhancer_export(Enhancer *e, GeglBuffer *p_in, const EnhancerPreset *p_preset,
"enhancer: unsupported export extension");
return FALSE;
}
-
- /* Apply the preset first. */
- GeglBuffer *p_buf = enhancer_apply(NULL, p_in, p_preset, p_err);
- if (p_buf == NULL) {
- g_free(c_path);
- return FALSE;
- }
-
- /* ku0: capture the save's real success. Record the output's pre-state so
- * a pre-existing file can't masquerade as a successful save. */
- GStatBuf st_before;
- gboolean b_existed = (g_stat(c_path, &st_before) == 0);
-
- GeglNode *p_graph = gegl_node_new();
- GeglNode *p_src = gegl_node_new_child(
+ GStatBuf st_before;
+ gboolean b_existed = (g_stat(c_path, &st_before) == 0);
+ GeglNode *p_graph = gegl_node_new();
+ GeglNode *p_src = gegl_node_new_child(
p_graph, "operation", "gegl:buffer-source", "buffer", p_buf, NULL);
GeglNode *p_save;
if (g_str_equal(c_op, "gegl:jpg-save")) {
@@ -219,9 +272,6 @@ enhancer_export(Enhancer *e, GeglBuffer *p_in, const EnhancerPreset *p_preset,
gegl_node_link(p_src, p_save);
gegl_node_process(p_save);
g_object_unref(p_graph);
- g_object_unref(p_buf);
-
- /* Verify the save actually produced a non-empty file newer than before. */
GStatBuf st_after;
gboolean b_ok = FALSE;
if (g_stat(c_path, &st_after) == 0 && st_after.st_size > 0) {
@@ -239,6 +289,39 @@ enhancer_export(Enhancer *e, GeglBuffer *p_in, const EnhancerPreset *p_preset,
return TRUE;
}
+gboolean
+enhancer_export(Enhancer *e, GeglBuffer *p_in, const EnhancerPreset *p_preset,
+ GFile *p_out, GError **p_err) {
+ (void)e;
+ g_return_val_if_fail(p_in != NULL, FALSE);
+ g_return_val_if_fail(p_out != NULL, FALSE);
+ GeglBuffer *p_buf = enhancer_apply(NULL, p_in, p_preset, p_err);
+ if (p_buf == NULL) {
+ return FALSE;
+ }
+ gboolean b_ok = _save_buffer(p_buf, p_out, p_err);
+ g_object_unref(p_buf);
+ return b_ok;
+}
+
+/* Export p_in with the enabled-preset chain (u_mask) composed, to p_out. */
+gboolean
+enhancer_export_chain(Enhancer *e, GeglBuffer *p_in, const GPtrArray *p_presets,
+ guint8 u_mask, GFile *p_out, GError **p_err) {
+ (void)e;
+ g_return_val_if_fail(p_in != NULL, FALSE);
+ g_return_val_if_fail(p_out != NULL, FALSE);
+ g_return_val_if_fail(p_presets != NULL, FALSE);
+ GeglBuffer *p_buf =
+ enhancer_apply_chain(NULL, p_in, p_presets, u_mask, p_err);
+ if (p_buf == NULL) {
+ return FALSE;
+ }
+ gboolean b_ok = _save_buffer(p_buf, p_out, p_err);
+ g_object_unref(p_buf);
+ return b_ok;
+}
+
#if GGAZE_HAVE_GEGL
GeglBuffer *
diff --git a/src/enhancer.h b/src/enhancer.h
index f466d21..b3f3c4d 100644
--- a/src/enhancer.h
+++ b/src/enhancer.h
@@ -28,6 +28,13 @@ const GPtrArray *enhancer_get_presets(Enhancer *p_e);
GeglBuffer *enhancer_apply(Enhancer *p_e, GeglBuffer *p_in,
const EnhancerPreset *p_preset, GError **p_err);
+/* Apply a chain of the enabled built-in presets (bit i of u_mask -> preset i)
+ * in array order, composing them. Returns a new buffer, or NULL if none are
+ * enabled / on error. */
+GeglBuffer *enhancer_apply_chain(Enhancer *p_e, GeglBuffer *p_in,
+ const GPtrArray *p_presets, guint8 u_mask,
+ GError **p_err);
+
/* Export the enhanced buffer to a file. The saver is chosen from p_out's
* extension: .jpg/.jpeg -> gegl:jpg-save (quality 95), .png -> gegl:png-save,
* .webp -> gegl:webp-save (if available). Other extensions fail with
@@ -37,6 +44,11 @@ gboolean enhancer_export(Enhancer *p_e, GeglBuffer *p_in,
const EnhancerPreset *p_preset, GFile *p_out,
GError **p_err);
+/* Export p_in with the enabled-preset chain (u_mask) composed, to p_out. */
+gboolean enhancer_export_chain(Enhancer *p_e, GeglBuffer *p_in,
+ const GPtrArray *p_presets, guint8 u_mask,
+ GFile *p_out, GError **p_err);
+
#if GGAZE_HAVE_GEGL
/* Load a file into a GeglBuffer via the gegl:load op. Returns a new buffer
* (caller unrefs) or NULL with p_err set. */
diff --git a/src/window.c b/src/window.c
index 8c2a49a..529711b 100644
--- a/src/window.c
+++ b/src/window.c
@@ -52,10 +52,12 @@ struct _GgazeWindow {
gboolean b_fullscreen;
guint u_hdr_hide; /* fullscreen header auto-hide timeout */
#if GGAZE_HAVE_GEGL
- int i_enhance_idx; /* active preset index (-1 = original) */
- Enhancer *p_enhancer; /* GEGL preset engine (NULL w/o GEGL) */
- GtkWidget *p_enhance_panel; /* GtkRevealer side panel (NULL w/o 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)
@@ -69,6 +71,10 @@ 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);
+#endif
/* --- actions ------------------------------------------------------------- */
@@ -463,8 +469,8 @@ static const char *SHORTCUTS_UI =
" <child>\n"
" <object class=\"GtkShortcutsShortcut\">\n"
" <property name=\"accelerator\">1 2 3 4 5 6 7 8</property>\n"
- " <property name=\"title\">Apply enhance preset "
- "1-8</property>\n"
+ " <property name=\"title\">Toggle enhance preset 1-8 "
+ "(layered); 0 = Original</property>\n"
" </object>\n"
" </child>\n"
" <child>\n"
@@ -565,9 +571,15 @@ _action_fullscreen(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;
+#if GGAZE_HAVE_GEGL
+ _enhance_panel_reparent(p_win, FALSE); /* back to sidebar next to image */
+#endif
} else {
gtk_window_fullscreen(GTK_WINDOW(p_win));
p_win->b_fullscreen = TRUE;
+#if GGAZE_HAVE_GEGL
+ _enhance_panel_reparent(p_win, TRUE); /* overlay over the image */
+#endif
}
}
@@ -618,12 +630,27 @@ _action_back(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
}
#if GGAZE_HAVE_GEGL
-/* Apply preset i_idx to the current image as a live preview (i_idx = -1 =>
- * restore the original). Switches to large view so the result shows. On any
- * failure resets to the original. Synchronous - may briefly block the UI on
- * large images (async apply is a later milestone). */
+/* Update each preset button's "ggaze-enhance-on" highlight from the mask. */
+static void
+_enhance_update_highlights(GgazeWindow *p_win) {
+ for (guint i = 0; i < G_N_ELEMENTS(p_win->p_enhance_btns); i++) {
+ GtkWidget *p_btn = p_win->p_enhance_btns[i];
+ if (p_btn == NULL) {
+ continue;
+ }
+ if ((p_win->u_enhance_mask & (guint8)(1u << i)) != 0) {
+ gtk_widget_add_css_class(p_btn, "ggaze-enhance-on");
+ } else {
+ gtk_widget_remove_css_class(p_btn, "ggaze-enhance-on");
+ }
+ }
+}
+
+/* Apply the enabled-preset chain (u_enhance_mask) to the current image as a
+ * live preview. An empty mask restores the original. Switches to large view
+ * so the result shows. Synchronous - may briefly block on large images. */
static void
-_apply_enhance(GgazeWindow *p_win, gint i_idx) {
+_apply_enhance_mask(GgazeWindow *p_win) {
if (p_win->p_nav == NULL || p_win->p_enhancer == NULL) {
return;
}
@@ -632,34 +659,25 @@ _apply_enhance(GgazeWindow *p_win, gint i_idx) {
if (g_strcmp0(c_cur, "large") != 0) {
gtk_stack_set_visible_child_name(GTK_STACK(p_win->p_stack), "large");
}
- p_win->i_enhance_idx = i_idx;
- if (i_idx < 0) {
+ if (p_win->u_enhance_mask == 0) {
_load_current(p_win); /* restore original (texturecache is fast) */
_update_header(p_win);
return;
}
GFile *p_file = navigator_get_current(p_win->p_nav);
if (p_file == NULL) {
- p_win->i_enhance_idx = -1;
- _update_header(p_win);
- return;
- }
- const GPtrArray *p_presets = enhancer_get_presets(p_win->p_enhancer);
- const EnhancerPreset *p_preset =
- (p_presets != NULL && (guint)i_idx < p_presets->len)
- ? g_ptr_array_index((GPtrArray *)p_presets, (guint)i_idx)
- : NULL;
- if (p_preset == NULL) {
- p_win->i_enhance_idx = -1;
+ p_win->u_enhance_mask = 0;
+ _enhance_update_highlights(p_win);
_update_header(p_win);
return;
}
- GError *p_err = NULL;
- GeglBuffer *p_buf = enhancer_load(p_file, &p_err);
- GdkTexture *p_tex = NULL;
+ const GPtrArray *p_presets = enhancer_get_presets(p_win->p_enhancer);
+ GError *p_err = NULL;
+ GeglBuffer *p_buf = enhancer_load(p_file, &p_err);
+ GdkTexture *p_tex = NULL;
if (p_buf != NULL) {
- GeglBuffer *p_enh =
- enhancer_apply(p_win->p_enhancer, p_buf, p_preset, &p_err);
+ GeglBuffer *p_enh = enhancer_apply_chain(
+ p_win->p_enhancer, p_buf, p_presets, p_win->u_enhance_mask, &p_err);
if (p_enh != NULL) {
p_tex = enhancer_buffer_to_texture(p_enh, &p_err);
g_object_unref(p_enh);
@@ -670,7 +688,8 @@ _apply_enhance(GgazeWindow *p_win, gint i_idx) {
g_warning("ggaze: enhance failed: %s",
p_err != NULL ? p_err->message : "(no detail)");
g_clear_error(&p_err);
- p_win->i_enhance_idx = -1;
+ p_win->u_enhance_mask = 0;
+ _enhance_update_highlights(p_win);
_load_current(p_win);
} else {
_show_texture(p_win, p_tex);
@@ -679,21 +698,26 @@ _apply_enhance(GgazeWindow *p_win, gint i_idx) {
_update_header(p_win);
}
-/* Apply the preset attached to the clicked button (qdata "idx"). The side
- * panel stays open - it's a persistent panel, not a dismissable popup. */
+/* Clicked row: idx 0..7 toggles that preset's bit; idx -1 (Original) clears
+ * the mask. Then refresh highlights + re-apply the (possibly empty) chain. */
static void
-_enhance_row_apply(GgazeWindow *p_win, GtkWidget *p_btn) {
+_enhance_row_toggle(GgazeWindow *p_win, GtkWidget *p_btn) {
gint i_idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(p_btn), "idx"));
- _apply_enhance(p_win, i_idx);
+ if (i_idx < 0) {
+ p_win->u_enhance_mask = 0; /* Original */
+ } else if (i_idx < (gint)G_N_ELEMENTS(p_win->p_enhance_btns)) {
+ p_win->u_enhance_mask ^= (guint8)(1u << i_idx);
+ }
+ _enhance_update_highlights(p_win);
+ _apply_enhance_mask(p_win);
}
/* Build the enhance side panel: a GtkRevealer (slide right) holding one
- * button per preset plus an "Original" row, overlaid on the left of the main
- * content. Hidden until 'a' toggles it on. Each row's label shows its hotkey
- * (1..N, 0). */
+ * button per preset plus an "Original" row, placed in the content box next to
+ * the image (reparented to the overlay in fullscreen). Hidden until 'a'. */
static void
_build_enhance_panel(GgazeWindow *p_win) {
- if (p_win->p_enhancer == NULL) {
+ if (p_win->p_enhancer == NULL || p_win->p_content == NULL) {
return;
}
const GPtrArray *p_presets = enhancer_get_presets(p_win->p_enhancer);
@@ -702,27 +726,28 @@ _build_enhance_panel(GgazeWindow *p_win) {
gtk_widget_set_margin_end(p_box, 8);
gtk_widget_set_margin_top(p_box, 8);
gtk_widget_set_margin_bottom(p_box, 8);
- gtk_widget_add_css_class(p_box, "osd");
- if (p_presets != NULL) {
- for (guint i = 0; i < p_presets->len && i < 9; i++) {
- const EnhancerPreset *p_pr =
- g_ptr_array_index((GPtrArray *)p_presets, i);
- char *c_lbl = g_strdup_printf("%u %s", i + 1, p_pr->c_name);
- GtkWidget *p_btn = gtk_button_new_with_label(c_lbl);
- gtk_widget_set_size_request(p_btn, 150, -1);
- gtk_widget_set_halign(p_btn, GTK_ALIGN_START);
- g_object_set_data(G_OBJECT(p_btn), "idx", GINT_TO_POINTER((gint)i));
- g_signal_connect_swapped(p_btn, "clicked",
- G_CALLBACK(_enhance_row_apply), p_win);
- gtk_box_append(GTK_BOX(p_box), p_btn);
- g_free(c_lbl);
- }
+ for (guint i = 0; i < G_N_ELEMENTS(p_win->p_enhance_btns); i++) {
+ const EnhancerPreset *p_pr =
+ (p_presets != NULL && i < p_presets->len)
+ ? g_ptr_array_index((GPtrArray *)p_presets, i)
+ : NULL;
+ char *c_lbl =
+ g_strdup_printf("%u %s", i + 1, p_pr != NULL ? p_pr->c_name : "-");
+ GtkWidget *p_btn = gtk_button_new_with_label(c_lbl);
+ gtk_widget_set_size_request(p_btn, 160, -1);
+ gtk_widget_set_halign(p_btn, GTK_ALIGN_START);
+ g_object_set_data(G_OBJECT(p_btn), "idx", GINT_TO_POINTER((gint)i));
+ g_signal_connect_swapped(p_btn, "clicked",
+ G_CALLBACK(_enhance_row_toggle), p_win);
+ gtk_box_append(GTK_BOX(p_box), p_btn);
+ p_win->p_enhance_btns[i] = p_btn;
+ g_free(c_lbl);
}
GtkWidget *p_btn0 = gtk_button_new_with_label("0 Original");
- gtk_widget_set_size_request(p_btn0, 150, -1);
+ gtk_widget_set_size_request(p_btn0, 160, -1);
gtk_widget_set_halign(p_btn0, GTK_ALIGN_START);
g_object_set_data(G_OBJECT(p_btn0), "idx", GINT_TO_POINTER(-1));
- g_signal_connect_swapped(p_btn0, "clicked", G_CALLBACK(_enhance_row_apply),
+ g_signal_connect_swapped(p_btn0, "clicked", G_CALLBACK(_enhance_row_toggle),
p_win);
gtk_box_append(GTK_BOX(p_box), p_btn0);
@@ -730,14 +755,46 @@ _build_enhance_panel(GgazeWindow *p_win) {
gtk_revealer_set_transition_type(GTK_REVEALER(p_rev),
GTK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT);
gtk_revealer_set_child(GTK_REVEALER(p_rev), p_box);
- gtk_widget_set_halign(p_rev, GTK_ALIGN_START);
- gtk_widget_set_valign(p_rev, GTK_ALIGN_START);
- gtk_widget_set_margin_top(p_rev, 48);
gtk_revealer_set_reveal_child(GTK_REVEALER(p_rev), FALSE);
- gtk_overlay_add_overlay(GTK_OVERLAY(p_win->p_overlay), p_rev);
+ /* Sidebar (normal mode): sits in the content box next to the image. */
+ gtk_widget_set_hexpand(p_rev, FALSE);
+ gtk_widget_set_vexpand(p_rev, TRUE);
+ gtk_box_prepend(GTK_BOX(p_win->p_content), p_rev);
p_win->p_enhance_panel = p_rev;
}
+/* Move the enhance panel between the sidebar (next to the image) and an
+ * overlay over the image (used in fullscreen, where there's no side room). */
+static void
+_enhance_panel_reparent(GgazeWindow *p_win, gboolean b_overlay) {
+ GtkWidget *p_rev = p_win->p_enhance_panel;
+ if (p_rev == NULL) {
+ return;
+ }
+ GtkWidget *p_parent = gtk_widget_get_parent(p_rev);
+ if (b_overlay && p_parent != p_win->p_overlay) {
+ g_object_ref(p_rev);
+ if (p_parent == p_win->p_content) {
+ gtk_box_remove(GTK_BOX(p_win->p_content), p_rev);
+ }
+ gtk_overlay_add_overlay(GTK_OVERLAY(p_win->p_overlay), p_rev);
+ gtk_widget_set_halign(p_rev, GTK_ALIGN_START);
+ gtk_widget_set_valign(p_rev, GTK_ALIGN_START);
+ gtk_widget_set_margin_top(p_rev, 48);
+ g_object_unref(p_rev);
+ } else if (!b_overlay && p_parent != p_win->p_content) {
+ g_object_ref(p_rev);
+ if (p_parent == p_win->p_overlay) {
+ gtk_overlay_remove_overlay(GTK_OVERLAY(p_win->p_overlay), p_rev);
+ }
+ gtk_widget_set_halign(p_rev, GTK_ALIGN_FILL);
+ gtk_widget_set_valign(p_rev, GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top(p_rev, 0);
+ gtk_box_prepend(GTK_BOX(p_win->p_content), p_rev);
+ g_object_unref(p_rev);
+ }
+}
+
/* win.enhance (key 'a'): toggle the enhance side panel on/off. */
static void
_action_enhance(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
@@ -752,8 +809,7 @@ _action_enhance(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
gtk_revealer_set_reveal_child(GTK_REVEALER(p_win->p_enhance_panel), !b_vis);
}
-/* win.enhance-N (keys 1-8): apply preset N directly (the panel need not be
- * visible). */
+/* win.enhance-N (keys 1-8): toggle preset N on/off (layered), then re-apply. */
static void
_action_enhance_n(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
(void)p_v;
@@ -764,14 +820,18 @@ _action_enhance_n(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
}
gint i_idx =
(gint)g_ascii_strtoll(c_name + strlen("enhance-"), NULL, 10) - 1;
- if (i_idx >= 0) {
- _apply_enhance(p_win, i_idx);
+ if (p_win->p_enhancer == NULL || i_idx < 0 ||
+ i_idx >= (gint)G_N_ELEMENTS(p_win->p_enhance_btns)) {
+ return;
}
+ p_win->u_enhance_mask ^= (guint8)(1u << i_idx);
+ _enhance_update_highlights(p_win);
+ _apply_enhance_mask(p_win);
}
-/* win.enhance-save (key 's'): export the current image with the active preset
- * to <stem>-enhanced.<ext>. Never overwrites the original. No-op (with a
- * warning) when no preview is active. */
+/* win.enhance-save (key 's'): export the current image with the enabled-preset
+ * chain to <stem>-enhanced.<ext>. Never overwrites the original. No-op (with
+ * a warning) when no preset is enabled. */
static void
_action_enhance_save(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
(void)p_a;
@@ -780,8 +840,8 @@ _action_enhance_save(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
if (p_win->p_nav == NULL || p_win->p_enhancer == NULL) {
return;
}
- if (p_win->i_enhance_idx < 0) {
- g_warning("ggaze: nothing to save (no enhance preview active)");
+ if (p_win->u_enhance_mask == 0) {
+ g_warning("ggaze: nothing to save (no enhance preset enabled)");
return;
}
GFile *p_file = navigator_get_current(p_win->p_nav);
@@ -817,10 +877,9 @@ _action_enhance_save(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
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);
- const EnhancerPreset *p_preset =
- g_ptr_array_index((GPtrArray *)p_presets, (guint)p_win->i_enhance_idx);
- b_ok = enhancer_export(p_win->p_enhancer, p_buf, p_preset, p_out, &p_err);
+ 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;
@@ -965,8 +1024,9 @@ nav_changed_cb(Navigator *p_nav, gpointer p_data) {
GgazeWindow *p_win = GGAZE_WINDOW(p_data);
#if GGAZE_HAVE_GEGL
/* New image starts fresh: drop any enhanced preview so we don't show a
- * stale enhanced texture for a different file. */
- p_win->i_enhance_idx = -1;
+ * stale enhanced texture for a different file, and clear the highlights. */
+ p_win->u_enhance_mask = 0;
+ _enhance_update_highlights(p_win);
#endif
_load_current(p_win);
}
@@ -1160,19 +1220,30 @@ _update_header(GgazeWindow *p_win) {
}
}
#if GGAZE_HAVE_GEGL
- /* Append the active enhance preset name when a preview is showing. */
- if (p_win->i_enhance_idx >= 0 && p_win->p_enhancer != NULL &&
+ /* Append the enabled enhance preset names (comma-joined) when layered. */
+ if (p_win->u_enhance_mask != 0 && p_win->p_enhancer != NULL &&
c_title != NULL) {
const GPtrArray *p_presets = enhancer_get_presets(p_win->p_enhancer);
- if (p_presets != NULL && (guint)p_win->i_enhance_idx < p_presets->len) {
- const EnhancerPreset *p_preset = g_ptr_array_index(
- (GPtrArray *)p_presets, (guint)p_win->i_enhance_idx);
- if (p_preset->c_name != NULL) {
+ if (p_presets != NULL) {
+ GString *p_str = g_string_new(NULL);
+ for (guint i = 0; i < p_presets->len && i < 8; i++) {
+ if ((p_win->u_enhance_mask & (guint8)(1u << i)) == 0) {
+ continue;
+ }
+ const EnhancerPreset *p_pr =
+ g_ptr_array_index((GPtrArray *)p_presets, i);
+ if (p_str->len > 0) {
+ g_string_append_c(p_str, ',');
+ }
+ g_string_append(p_str, p_pr->c_name);
+ }
+ if (p_str->len > 0) {
char *c_tmp =
- g_strdup_printf("%s \u00b7 %s", c_title, p_preset->c_name);
+ g_strdup_printf("%s \u00b7 %s", c_title, p_str->str);
g_free(c_title);
c_title = c_tmp;
}
+ g_string_free(p_str, TRUE);
}
}
#endif
@@ -1244,6 +1315,12 @@ _ensure_css(void) {
" border: 2px solid #3584e4;\n"
" border-radius: 4px;\n"
" background-color: rgba(53, 132, 228, 0.15);\n"
+ "}\n"
+ "/* enabled enhance preset row highlight. */\n"
+ ".ggaze-enhance-on {\n"
+ " background-color: #3584e4;\n"
+ " color: #ffffff;\n"
+ " font-weight: bold;\n"
"}\n");
GdkDisplay *p_disp = gdk_display_get_default();
if (p_disp != NULL) {
@@ -1265,8 +1342,8 @@ ggaze_window_init(GgazeWindow *p_win) {
p_win->p_grid = NULL; /* created on open */
p_win->i_grid_size = 128;
#if GGAZE_HAVE_GEGL
- p_win->i_enhance_idx = -1; /* start on the original */
- p_win->p_enhancer = enhancer_new();
+ p_win->u_enhance_mask = 0; /* start on the original */
+ p_win->p_enhancer = enhancer_new();
#endif
/* Header bar (libadwaita, decision #29). */
@@ -1287,7 +1364,14 @@ ggaze_window_init(GgazeWindow *p_win) {
gtk_widget_set_margin_top(p_win->p_info_lbl, 12);
gtk_widget_set_visible(p_win->p_info_lbl, FALSE);
gtk_overlay_add_overlay(GTK_OVERLAY(p_win->p_overlay), p_win->p_info_lbl);
- gtk_window_set_child(GTK_WINDOW(p_win), p_win->p_overlay);
+ /* Layout: a horizontal box with the image area (overlay) taking the space,
+ * and the enhance panel slotted in next to it (or overlaid in fullscreen).
+ */
+ p_win->p_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_hexpand(p_win->p_overlay, TRUE);
+ gtk_widget_set_vexpand(p_win->p_overlay, TRUE);
+ gtk_box_append(GTK_BOX(p_win->p_content), p_win->p_overlay);
+ gtk_window_set_child(GTK_WINDOW(p_win), p_win->p_content);
#if GGAZE_HAVE_GEGL
_build_enhance_panel(p_win);
#endif