From 992b09357a1d16eaabd754c9009fe30d9ed1961d Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Wed, 29 Jul 2026 10:30:34 -0400 Subject: [PATCH] =?UTF-8?q?v0.0.61=20-=20Release:=20fix=20Save=20Image=20c?= =?UTF-8?q?ontext=20menu=20=E2=80=94=20add=20download-started=20handler=20?= =?UTF-8?q?with=20file=20chooser=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- src/version.h | 4 +- src/web_context.c | 149 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index b4ae2bd..72189de 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.60 +0.0.61 diff --git a/src/version.h b/src/version.h index 540337a..d6135fb 100644 --- a/src/version.h +++ b/src/version.h @@ -11,9 +11,9 @@ #ifndef SOVEREIGN_BROWSER_VERSION_H #define SOVEREIGN_BROWSER_VERSION_H -#define SB_VERSION "v0.0.60" +#define SB_VERSION "v0.0.61" #define SB_VERSION_MAJOR 0 #define SB_VERSION_MINOR 0 -#define SB_VERSION_PATCH 60 +#define SB_VERSION_PATCH 61 #endif /* SOVEREIGN_BROWSER_VERSION_H */ diff --git a/src/web_context.c b/src/web_context.c index 4ca3cb2..d2d5062 100644 --- a/src/web_context.c +++ b/src/web_context.c @@ -9,6 +9,7 @@ #include "profile.h" #include +#include #include #include @@ -18,6 +19,148 @@ * wants the per-user context. */ static WebKitWebContext *g_ctx = NULL; +/* ── Download handling ─────────────────────────────────────────────── * + * WebKitGTK fires the "download-started" signal on the WebKitWebContext + * whenever the user triggers a download — most notably the "Save Image" + * context-menu item on an . In the webkit2gtk-4.1 API the download + * does NOT auto-prompt for a destination: the application must handle the + * download's "decide-destination" signal (which supplies a suggested + * filename), call webkit_download_set_destination(), and return TRUE. + * If nobody handles "decide-destination", WebKit writes the file to + * G_USER_DIRECTORY_DOWNLOAD using the suggested name — but on many + * minimal/containerized setups that directory resolves to a path the + * user never sees, so "Save Image" appears to do nothing. + * + * Our flow: + * download-started (context) → take a ref, wire finished/failed + * decide-destination (download) → show a GtkFileChooserDialog pre-filled + * with the suggested filename, set the + * chosen destination, return TRUE + * finished / failed → unref the download + * + * Since 2.40, returning TRUE from "decide-destination" without + * immediately calling webkit_download_set_destination() is allowed: the + * download pauses until the destination is set or the download is + * cancelled. We use that to run a modal file chooser inside the handler. + */ + +/* Find a reasonable parent GtkWindow for the file chooser. We prefer the + * currently-active (focused) top-level window; if none is focused we fall + * back to the first visible top-level. web_context.c deliberately does + * not depend on tab_manager.c's g_active_window, so this stays self- + * contained. Returns NULL if no usable window is found. */ +static GtkWindow *pick_dialog_parent(void) { + GList *toplevels = gtk_window_list_toplevels(); + GtkWindow *active = NULL; + GtkWindow *first_visible = NULL; + + for (GList *l = toplevels; l != NULL; l = l->next) { + GtkWindow *w = GTK_WINDOW(l->data); + if (!gtk_widget_get_visible(GTK_WIDGET(w))) continue; + if (first_visible == NULL) first_visible = w; + if (gtk_window_is_active(w)) { active = w; break; } + } + + GtkWindow *result = active ? active : first_visible; + if (result != NULL) g_object_ref_sink(G_OBJECT(result)); + g_list_free(toplevels); + return result; /* caller unrefs */ +} + +/* Reduce a possibly-full suggested path to its final path segment so the + * save dialog never starts in an unexpected directory. Returns a newly + * allocated string. */ +static char *basename_of(const char *name) { + if (name == NULL || name[0] == '\0') return NULL; + const char *slash = strrchr(name, '/'); + return g_strdup(slash ? slash + 1 : name); +} + +static void on_download_finished(WebKitDownload *dl, gpointer user) { + (void)user; + const char *dest = webkit_download_get_destination(dl); + g_print("[download] Finished: %s\n", dest ? dest : "(no destination)"); + g_object_unref(dl); +} + +static void on_download_failed(WebKitDownload *dl, GError *err, gpointer user) { + (void)user; + g_printerr("[download] Failed: %s\n", err ? err->message : "(unknown)"); + g_object_unref(dl); +} + +/* "decide-destination" handler. WebKit passes the suggested filename + * (derived from the response Content-Disposition / URL). We show a save + * dialog, set the destination, and return TRUE. Returning TRUE without + * setting a destination is explicitly supported since 2.40 and pauses + * the download until we set one (or cancel). */ +static gboolean on_download_decide_destination(WebKitDownload *dl, + const char *suggested, + gpointer user) { + (void)user; + + char *default_name = basename_of(suggested); + if (default_name == NULL || default_name[0] == '\0') { + g_free(default_name); + default_name = g_strdup("download"); + } + + GtkWindow *parent = pick_dialog_parent(); + + GtkWidget *dialog = gtk_file_chooser_dialog_new( + "Save File", parent, GTK_FILE_CHOOSER_ACTION_SAVE, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Save", GTK_RESPONSE_ACCEPT, + NULL); + if (parent) g_object_unref(parent); + + GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog); + gtk_file_chooser_set_current_name(chooser, default_name); + gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE); + + gint resp = gtk_dialog_run(GTK_DIALOG(dialog)); + char *path = (resp == GTK_RESPONSE_ACCEPT) + ? gtk_file_chooser_get_filename(chooser) + : NULL; + gtk_widget_destroy(dialog); + g_free(default_name); + + if (path == NULL) { + /* User cancelled — abort the download. */ + webkit_download_cancel(dl); + return TRUE; /* we handled it (by cancelling) */ + } + + char *uri = g_filename_to_uri(path, NULL, NULL); + g_free(path); + if (uri == NULL) { + webkit_download_cancel(dl); + return TRUE; + } + + webkit_download_set_destination(dl, uri); + g_free(uri); + return TRUE; /* stop other handlers; destination is now set */ +} + +static void on_download_started(WebKitWebContext *ctx, + WebKitDownload *dl, + gpointer user_data) { + (void)ctx; + (void)user_data; + + /* Hold a ref for the lifetime of the download so our finished/failed + * handlers can safely access it; we unref there. */ + g_object_ref(dl); + + g_signal_connect(dl, "decide-destination", + G_CALLBACK(on_download_decide_destination), NULL); + g_signal_connect(dl, "finished", + G_CALLBACK(on_download_finished), NULL); + g_signal_connect(dl, "failed", + G_CALLBACK(on_download_failed), NULL); +} + /* ── Security / TLS / favicon configuration ────────────────────────── * * Applied to every context we build (per-user and ephemeral). Mirrors * the configuration that main.c used to apply to the default context. @@ -67,6 +210,12 @@ static void configure_context(WebKitWebContext *ctx) { g_get_tmp_dir()); } webkit_web_context_set_favicon_database_directory(ctx, fav_dir); + + /* Handle downloads — most importantly the "Save Image" context-menu + * action. Without this handler WebKitGTK cancels the download (and + * the user sees nothing happen). See on_download_started() above. */ + g_signal_connect(ctx, "download-started", + G_CALLBACK(on_download_started), NULL); } WebKitWebContext *web_context_build_for_pubkey(const char *pubkey_hex) {