| Age | Commit message (Collapse) | Author |
|
The _proceed_prev/next/first/last helpers are trivial wrappers over the
public ggaze_window navigation API and do not depend on GEGL, but were
defined only inside the #if GGAZE_HAVE_GEGL block. The always-compiled
_action_prev/next/first/last handlers reference them via _maybe_save_then,
so the gegl=disabled (minimal) lane failed with '_proceed_prev undeclared'
(regression introduced by the enhance save-dialog commit, before M8 marks).
Move the four helpers out of the GEGL guard so they are always compiled.
Verified: meson setup build-min -Dgegl=disabled -Djxl=disabled -Davif=disabled -Dheif=disabled && ninja -C build-min && meson test -C build-min (23/23 OK); default gegl=enabled lane still 24/24 OK.
|
|
Wire the navigator mark API into the window actions, grid badges, and
keyboard/pointer controls:
- Navigator: add a path-based range anchor (p_last_mark) set on `v`
toggle-on and cleared on unmark-of-anchor / clear_marks / remove /
mark_removed / rescan-prune; expose navigator_get_last_mark().
navigator_mark_removed now clears the removed file's mark (decision Q
/ task du0) and the anchor if it was it, without touching unrelated
marks, and emits "changed" whenever the removed set or marks actually
changed so grid badges and the header never go stale.
- Window: add win.mark-range (`V`: range from the last-mark anchor to the
current/grid-selected image), register it, wire Shift+V, and document
it in the shortcuts overlay. Trash/delete clear marks via mark_removed.
- Grid: middle-click on a cell selects it and dispatches win.mark
(pointer-accessible marks).
- Tests: unit test_marks_anchor_and_remove and
test_marks_anchor_pruned_on_rescan; integration test_marks.c covering
toggle, all+clear, range, persistence across view switch,
clear-on-trash, and the middle-click gesture wiring; updated
test_shortcut.c action table + count.
clang-format clean; full suite + ASan leak check green.
|
|
Add a dim 's Save enhanced copy' hint at the bottom of the enhance
panel so the save shortcut is visible right where the toggles are.
When you navigate to another image (h/l/g/G) with an unsaved enhance
preview active, a Save / Discard / Cancel alert now asks what to do:
Save exports <stem>-enhanced.<ext> with the enabled-preset chain then
moves on; Discard drops the preview and moves on; Cancel keeps the
preview and stays on the current image. _action_prev/next/first/last
route through _maybe_save_then; the s action and the dialog's Save
button share _enhance_do_save. Non-GEGL builds get a passthrough gate.
|
|
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).
|
|
Instead of a transient 'a' popup, add a persistent enhance side panel
(GtkRevealer overlaid on the left of the main content) toggled by 'a'.
It lists every preset as a button showing its hotkey (1..8) plus a
'0 Original' row, and stays open while you compare presets.
Each optimization now has a dedicated global hotkey: 1-8 apply preset
1-8 directly (win.enhance-1 .. win.enhance-8), whether the panel is
visible or not. 'a' toggles the panel; 's' still saves the enhanced
copy; the panel's '0 Original' row (and navigating away) reverts.
Adds win.enhance-1..8 actions (one _action_enhance_n handler parses the
index from the action name) and the 1-8 keybindings; the ? overlay
Enhance group documents the panel + 1-8 + s. test_shortcut full-table
updated (33 rows, 29 actions).
|
|
Replace the a=cycle behavior with a GtkPopover listing the presets as
rows (1 Auto-fix, 2 Brightness, ... 8 Denoise, 0 Original). Click a
row or press its digit to apply that preset as the live preview; Escape
or 'a' again closes. Refactored the apply logic into _apply_enhance(idx)
shared by the menu and (unchanged) win.enhance-save. The popover is
parented to the overlay and torn down on the 'closed' signal. The ?
overlay Enhance group now documents the menu.
|
|
The enhancer module existed (GEGL presets: Auto-fix, Brightness, Contrast,
Saturation, Warm, Cool, Sharpen, Denoise) but was never connected to the
app and GEGL was never initialized. Wire it up:
- app.c: gegl_init() once in GApplication::startup (GEGL-gated).
- enhancer.c: add enhancer_load(GFile) -> GeglBuffer (gegl:load) and
enhancer_buffer_to_texture(GeglBuffer) -> GdkTexture (RGBA8 ->
GdkMemoryTexture) for the live preview bridge.
- enhancer_export: pick the saver from the output extension (jpg/png/webp)
instead of always gegl:jpg-save (ju0 - was writing JPEG bytes into .png);
verify the save actually produced a non-empty newer file instead of
trusting g_file_test(EXISTS) on a pre-existing path (ku0 - false success).
- window.c: win.enhance (a) cycles the active preset and previews it on the
current image (switches to large view, loads via GEGL, applies, shows the
texture); win.enhance-save (s) exports <stem>-enhanced.<ext> with the
active preset, never overwriting the original. Preview resets on
navigation; the title shows the active preset.
- shortcuts.c: a -> win.enhance, s -> win.enhance-save; ? overlay Enhance
group.
- tests: test_enhancer gains load_and_to_texture, export_format (PNG/JPEG
signature checks), export_real_success (ku0 - parent-missing and
pre-existing-directory both return FALSE). LSAN suppressions cover
GEGL's jpg-load plugin leak. test_shortcut full-table updated (25 rows).
Synchronous apply (may briefly block on large images) and a navigate-away
dirty prompt are deliberately out of scope for v1.
|
|
_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.
|
|
The inline SHORTCUTS_UI string had mismatched closing tags (extra
</child> elements), so gtk_builder_new_from_string failed with
"Element child was closed, but the currently open element is object"
and aborted the process the moment ? was pressed. Regenerate the UI
with correct <object>/<child> nesting (validated with xmllint).
|
|
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.
|
|
ggaze_window_open always set the stack to "large" and loaded the first
image even for a directory argument, contradicting the documented
folder-to-grid behavior. A directory arg now shows the populated
thumbnail grid; a file arg still opens large on that image.
_show_texture additionally forced the stack to "large" whenever an image
finished loading, which yanked a just-opened folder back out of the grid
the moment its first image decoded. Drop the forced stack switch — the
caller already sets the right view (file-open/toggle/grid-activate set
large; directory-open sets grid), so _show_texture only needs to update
the viewer's texture.
|
|
Arrow-key navigation in the grid moved the GtkFlowBox selection but never
updated navigator.current, so Enter (and toggling to large with t) opened
the stale current image instead of the highlighted one. _on_flow_key
emitted the activate signal directly without syncing, unlike the
double-click path (_on_child_activated) which does sync.
Add ggaze_grid_sync_current(): reads the selected flowbox child and calls
navigator_set_current_file. Call it from _on_flow_key before emitting
activate, and from _action_toggle_view's grid→large branch (then load
the now-current image). Mirrors the existing double-click sync.
|
|
M6: progressive low-res preview via libjpeg-turbo.
- jpeg.c: two-phase decode (1/8 low-res via progress_cb, full via GdkPixbuf
with EXIF orientation). Feature-gated.
- loader.h/c: LoaderProgressCb + load_progressive on backend struct;
loader_load_async accepts progress params; JPEG special-cased in task thread.
- window.c: _load_progress_cb marshals partial to main thread.
- tests: test_loader_jpeg (progress + dims), test_progressive_jpeg (sample).
16/16 green, ASan clean.
|
|
M4 (task ot0): fullscreen + slideshow + info overlay.
- src/info.{c,h}: plain-C EXIF gather via libexif (camera, lens, focal,
aperture, shutter, ISO, datetime, orientation) + file info (dims, format,
size). info_format() produces the overlay text. Unit-tested.
- src/window.c: fullscreen toggle (f), slideshow (S, 3s auto-advance), info
overlay (i, GtkLabel in GtkOverlay, auto-hide 5s), Esc contextual back
(fullscreen→unfullscreen→large→grid→quit). Stack wrapped in GtkOverlay.
ggaze_window_get_stack() accessor for tests.
- src/shortcuts.c: f, S, i, Escape bindings.
- meson.build: libexif dep added.
- tests: unit test_info (EXIF extraction + orientation tag). 14/14 green,
ASan clean.
|
|
M7 (task jt0): thumbnail cache + grid view + trash.
- src/trash.{c,h}: ./Trash bin (lazy create, collision suffix -1/-2, restore_last
for u undo, permanent delete for D). Plain-C, unit-tested.
- src/thumbnail.{c,h}: freedesktop TMS cache (normal/large/x-large buckets,
md5(URI) key, Thumb::URI/MTime/Size, mtime verify). Async GTask worker with
GCancellable so the grid can cancel pending requests on dispose.
- src/gridview.{c,h}: GgazeGrid (GtkWidget wrapping GtkFlowBox): one cell per
navigator file, lazy thumbnail decode on realize, +/- resize 64-512, dim
removed cells, mark badges, Enter/double-click → 'activate' signal, cursor
sync. ggaze_grid_detach() breaks the nav ref before dispose.
- src/navigator.{c,h}: removed set (mark_removed/is_removed/unmark_removed);
_relist preserves removed items (stay dimmed even if absent from disk);
get_remaining = count - removed_count.
- src/window.c: d (trash+mark_removed+advance), D (permanent delete, confirm
dialog if >1 marked), u (restore_last+rescan), t (toggle grid/large),
+/- (viewer zoom in large / grid resize). Header counter uses remaining.
- src/shortcuts.c: d/D/u/t/+/= bindings added.
- tests: unit test_trash + test_thumbnail, integration test_grid_cull
(grid count + trash/undo + ./sample-images 613-cell grid). 13/13 green,
ASan clean.
Fixed: grid dispose UAF (detach before nav freed), g_file_get_basename leak,
test p_plain lifetime, thumbnail async-cancellable for clean dispose, test
grid-toggle avoiding async-thumbnail drain issues.
|
|
M3 (task it0): responsive + prefetch.
- src/loader/loader.{c,h}: loader_load_async/_finish wrap the sync
loader_load in a GTask worker; the task's source object is the GFile so the
finish callback can check it against navigator.current (last-write-wins).
- src/texturecache.{c,h}: bounded LRU (cap 4) of GFile->GdkTexture, hash+GQueue
O(1) get/put, entry owns the key (no double-unref), evicts LRU on overflow.
- src/window.c: async visible load (cancel-then-recreate single GCancellable);
cache hit -> show+prefetch, miss -> async load; last-write-wins guard
(g_file_equal(current, loaded)) before showing; prefetch next/prev into the
cache via a separate prefetch cancellable (cancelled each round). Finish
callbacks hold a ref on the window (released in the callback) so they never
deref a freed window; dispose cancels both cancellables.
- tests: unit test_texturecache (LRU evict/order/replace/miss), integration
test_responsive_nav (10 rapid next -> last-write-wins). M1/M2 tests updated
to pump the main loop (loads are now async) + drain_main before exit.
- meson: texturecache in libggae; GIO_USE_VFS=local in fixtures_env.
Fixed: texturecache double-unref (key ownership), async-callback UAF (window
ref in callback data + drain in tests), a test g_build_filename leak. Sub-agent
review issues addressed (dispose + cache-hit cancel in-flight loads).
|
|
M2 (task ht0): walk the directory.
- src/navigator.{c,h}: GObject (no GtkWidget) holding the folder listing —
image-MIME filter + extension fallback, hide-RAW-sidecars (decision #33),
sort name/time/size, cursor + prev/next/wrap, path-based marks (survive
re-sort, pruned on rescan/monitor, cleared on remove), nearest-fallback,
GFileMonitor debounced 250ms (decision #28), 'changed' signal. 92% gcov.
- src/shortcuts.{c,h}: GtkShortcutController (MANAGED) binding h/l/Left/Right/
g/G/o/q to win.* actions; one table all milestones add to.
- src/window.{c,h}: open file->parent+current / folder->first; h/l/g/G actions
-> nav -> load -> viewer; single GCancellable last-write-wins (g_file_equal
guard for M3 async); header title 'filename · n/total'; GtkDropTarget
(GDK_TYPE_FILE_LIST, many->first, decision Z); GtkFileDialog open.
- data/gschema: add hide-raw-sidecars key.
- tests: unit test_navigator (12 subtests), integration test_walk_folder
(temp fixtures walk + ./sample-images skip-if-absent). 8/8 green; ASan clean
(GIO_USE_VFS=local in fixtures_env).
Fixed during this milestone: a UAF (borrowed c_name used after unref), a
cancellable double-ref leak, time::modified UINT64 vs INT64, and several
test-harness leaks. Sub-agent review issues addressed (mark pruning on
rescan/monitor, remove-before-cursor test, recursive temp-dir cleanup,
callback _cb naming, connect-before-mutate).
|
|
M1 (task gt0): show one image with zoom/pan.
- src/loader/detect.{c,h}: magic-byte format sniffing (JPEG/PNG/GIF/WebP/
TIFF/ICO/JXL/AVIF/HEIF) -> GgazeFormat; pure, no I/O, unit-testable.
- src/loader/loader.{c,h}: loader_load() sniffs the header and dispatches to
the first registered backend; GgazeLoaderBackend struct; pixbuf is the
fallback (last, accepts UNKNOWN). M1 ships only the pixbuf backend.
- src/loader/backends/pixbuf.c: GdkPixbufLoader decode +
gdk_pixbuf_apply_embedded_orientation (decision #26) -> GdkTexture via
gdk_memory_texture_new (avoids the deprecated gdk_texture_new_for_pixbuf).
- src/viewer.{c,h}: GgazeViewer : GtkWidget custom widget (decision #31) —
fit/100%/in/out zoom, cursor-centered zoom, drag-to-pan with clamping,
dark background, GtkSnapshot render nodes.
- src/window.c: open -> loader_load -> viewer_set_texture -> stack 'large'.
- tests: unit test_detect (13 cases) + test_loader_pixbuf (plain/rotated-EXIF
8x4 orient6->4x8/png/rgba/missing/unsupported jxl-avif-heif/corrupt),
integration test_open_and_show (fixture + rotated + ./sample-images
skip-if-absent). 6/6 green; detect 97% / loader 91% / pixbuf 87% coverage.
- fixtures: gen.py produces plain.jpg, rot6.jpg, small.png, rgba.png.
- AGENTS.md: documents the ./sample-images optional test corpus convention.
Sub-agent review fixes: use-after-free of c_name in ggaze_window_open
(BLOCKER), gtk_stack_get_pages leak in test_window (BLOCKER), coverage gap,
dead branch, viewer measure, _prefix/_cb naming, include order, extern in
header, pan clamp, stale comments — all addressed.
|
|
M0 (task ft0): the app shell — no feature views yet (those are M1 large, M7
grid).
- src/main.c: g_application_run + --version via handle-local-options (--help
is GApplication's own); exits before activate so --version works headless.
- src/app.{c,h}: GgazeApp : AdwApplication : GtkApplication (libadwaita
styling, decision #29), single-instance (decision #32),
G_APPLICATION_HANDLES_OPEN; activate + open reuse the active window
(replace-on-new-open); ggaze_window_open remembers the GFile.
- src/window.{c,h}: GgazeWindow : GtkApplicationWindow with AdwHeaderBar and a
GtkStack (children 'grid'/'large', default 'grid'); dispose clears the file.
- data/org.buetow.ggaze.desktop (image/* MIME, validated) and
data/org.buetow.ggaze.gschema.xml (all keys + defaults, --strict clean).
- meson: libggaze static lib shared by the executable and tests; install to
bindir/applications/glib-2.0 schemas; gnome.post_install compiles schemas +
updates desktop db.
- tests: unit test_app (--version/--help/unknown via subprocess), integration
test_window (offscreen stack checks + open-titles), bootstrap. 3/3 green.
- docs/tech-stack.md: window-geometry shape corrected to (width,height,
fullscreen,maximized) to match the schema.
|