From 11800a9a0c5510311dab393111e7dce9789a1d0b Mon Sep 17 00:00:00 2001 From: tsukasa Date: Thu, 13 Aug 2026 16:22:36 +0800 Subject: [PATCH] Added Ctrl+F search bar and Downloads --- config.h | 8 ++ page.c | 232 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 235 insertions(+), 5 deletions(-) diff --git a/config.h b/config.h index 11a951e..6678bcf 100644 --- a/config.h +++ b/config.h @@ -47,6 +47,9 @@ typedef enum { ACTION_ESCAPE, ACTION_SCROLL_UP, ACTION_SCROLL_DOWN, + ACTION_FIND, + ACTION_FIND_NEXT, + ACTION_FIND_PREV, } PageAction; typedef struct { @@ -76,6 +79,11 @@ static const PageKeybind KEYBINDINGS[] = { { GDK_KEY_L, GDK_CONTROL_MASK | GDK_SHIFT_MASK, ACTION_FORWARD }, { GDK_KEY_J, GDK_CONTROL_MASK | GDK_SHIFT_MASK, ACTION_SCROLL_DOWN }, { GDK_KEY_K, GDK_CONTROL_MASK | GDK_SHIFT_MASK, ACTION_SCROLL_UP }, + { GDK_KEY_f, GDK_CONTROL_MASK, ACTION_FIND }, + { GDK_KEY_F3, 0, ACTION_FIND_NEXT }, + { GDK_KEY_F3, GDK_SHIFT_MASK, ACTION_FIND_PREV }, + { GDK_KEY_g, GDK_CONTROL_MASK, ACTION_FIND_NEXT }, + { GDK_KEY_G, GDK_CONTROL_MASK | GDK_SHIFT_MASK, ACTION_FIND_PREV }, }; static const size_t N_KEYBINDINGS = sizeof(KEYBINDINGS) / sizeof(KEYBINDINGS[0]); diff --git a/page.c b/page.c index 1a613ea..7ea5a10 100644 --- a/page.c +++ b/page.c @@ -14,6 +14,10 @@ typedef struct { #endif GtkWidget *web_view; GtkWidget *progress; + GtkWidget *find_bar; + GtkWidget *find_entry; + GtkWidget *find_count_label; + gboolean find_open; } PageApp; static void update_title(PageApp *app) { @@ -124,6 +128,165 @@ static void scroll_by(PageApp *app, int delta) { g_free(script); } +#define FIND_OPTIONS (WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE | WEBKIT_FIND_OPTIONS_WRAP_AROUND) +#define FIND_MAX_MATCHES 1000 + +static WebKitFindController *find_controller(PageApp *app) { + return webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(app->web_view)); +} + +static void find_show(PageApp *app) { + app->find_open = TRUE; + gtk_widget_set_visible(app->find_bar, TRUE); + gtk_revealer_set_reveal_child(GTK_REVEALER(app->find_bar), TRUE); + gtk_widget_grab_focus(app->find_entry); + const char *text = gtk_editable_get_text(GTK_EDITABLE(app->find_entry)); + if (text && *text) + gtk_editable_select_region(GTK_EDITABLE(app->find_entry), 0, -1); +} + +static void find_hide(PageApp *app) { + app->find_open = FALSE; + webkit_find_controller_search_finish(find_controller(app)); + gtk_revealer_set_reveal_child(GTK_REVEALER(app->find_bar), FALSE); + gtk_label_set_text(GTK_LABEL(app->find_count_label), ""); + gtk_widget_grab_focus(app->web_view); +} + +static gboolean find_bar_visible(PageApp *app) { + return gtk_revealer_get_reveal_child(GTK_REVEALER(app->find_bar)); +} + +static void find_run(PageApp *app, WebKitFindOptions extra_opts) { + const char *text = gtk_editable_get_text(GTK_EDITABLE(app->find_entry)); + if (!text || !*text) { + webkit_find_controller_search_finish(find_controller(app)); + gtk_label_set_text(GTK_LABEL(app->find_count_label), ""); + return; + } + webkit_find_controller_search(find_controller(app), text, + FIND_OPTIONS | extra_opts, FIND_MAX_MATCHES); +} + +static void on_find_entry_changed(GtkEditable *entry, gpointer user_data) { + (void)entry; + find_run((PageApp *)user_data, 0); +} + +static void on_find_entry_activate(GtkEntry *entry, gpointer user_data) { + (void)entry; + find_run((PageApp *)user_data, WEBKIT_FIND_OPTIONS_NONE); +} + +static void on_find_next_clicked(GtkButton *btn, gpointer user_data) { + (void)btn; + webkit_find_controller_search_next(find_controller((PageApp *)user_data)); +} + +static void on_find_prev_clicked(GtkButton *btn, gpointer user_data) { + (void)btn; + webkit_find_controller_search_previous(find_controller((PageApp *)user_data)); +} + +static void on_find_close_clicked(GtkButton *btn, gpointer user_data) { + (void)btn; + find_hide((PageApp *)user_data); +} + +static void on_found_text(WebKitFindController *ctrl, guint match_count, gpointer user_data) { + (void)ctrl; + PageApp *app = user_data; + if (!app->find_open) + return; + gchar *label = match_count == G_MAXUINT + ? g_strdup_printf("%d+ matches", FIND_MAX_MATCHES) + : g_strdup_printf("%u match%s", match_count, match_count == 1 ? "" : "es"); + gtk_label_set_text(GTK_LABEL(app->find_count_label), label); + g_free(label); +} + +static void on_failed_to_find_text(WebKitFindController *ctrl, gpointer user_data) { + (void)ctrl; + PageApp *app = user_data; + if (!app->find_open) + return; + gtk_label_set_text(GTK_LABEL(app->find_count_label), "no matches"); +} + +/* downloads and such */ + +static gchar *unique_destination_path(const char *dir, const char *filename) { + gchar *path = g_build_filename(dir, filename, NULL); + if (!g_file_test(path, G_FILE_TEST_EXISTS)) + return path; + + gchar *base = g_strdup(filename); + gchar *ext = strrchr(base, '.'); + gchar *stem = (ext && ext != base) ? g_strndup(base, ext - base) : g_strdup(base); + const char *suffix = (ext && ext != base) ? ext : ""; + + gchar *candidate = NULL; + int n; + for (n = 1; n <= 999; n++) { + g_free(candidate); + candidate = g_strdup_printf("%s (%d)%s", stem, n, suffix); + g_free(path); + path = g_build_filename(dir, candidate, NULL); + if (!g_file_test(path, G_FILE_TEST_EXISTS)) + break; + } + if (n > 999) { + g_free(path); + gchar *unique_suffix = g_strdup_printf("-%" G_GINT64_FORMAT "%s", + g_get_real_time(), suffix); + gchar *unique_name = g_strconcat(stem, unique_suffix, NULL); + path = g_build_filename(dir, unique_name, NULL); + g_free(unique_suffix); + g_free(unique_name); + } + g_free(candidate); + g_free(base); + g_free(stem); + return path; +} + +static gboolean on_decide_destination(WebKitDownload *download, const gchar *suggested_filename, + gpointer user_data) { + (void)user_data; + + const char *xdg_downloads = g_get_user_special_dir(G_USER_DIRECTORY_DOWNLOAD); + gchar *dir_owned = xdg_downloads ? NULL : g_build_filename(g_get_home_dir(), "Downloads", NULL); + const char *dir = xdg_downloads ? xdg_downloads : dir_owned; + g_mkdir_with_parents(dir, 0700); + + const char *raw = (suggested_filename && *suggested_filename) ? suggested_filename : "download"; + gchar *safe_name = g_path_get_basename(raw); + if (!*safe_name || g_strcmp0(safe_name, ".") == 0 || g_strcmp0(safe_name, "/") == 0) { + g_free(safe_name); + safe_name = g_strdup("download"); + } + + gchar *dest_path = unique_destination_path(dir, safe_name); + + if (g_path_is_absolute(dest_path)) + webkit_download_set_destination(download, dest_path); + else + webkit_download_cancel(download); + + g_free(safe_name); + g_free(dest_path); + g_free(dir_owned); + return TRUE; +} + +static gboolean on_download_started(WebKitNetworkSession *session, WebKitDownload *download, + gpointer user_data) { + (void)session; + (void)user_data; + g_signal_connect(download, "decide-destination", G_CALLBACK(on_decide_destination), NULL); + return TRUE; +} + /* run the behaviour bound to a PageAction */ static void dispatch_action(PageApp *app, PageAction action) { switch (action) { @@ -160,24 +323,40 @@ static void dispatch_action(PageApp *app, PageAction action) { case ACTION_FULLSCREEN: toggle_fullscreen(app); break; - case ACTION_ESCAPE: + case ACTION_ESCAPE: { + gboolean did_something = FALSE; + if (find_bar_visible(app)) { + find_hide(app); + did_something = TRUE; + } #if ENABLE_URL_BAR if (gtk_widget_has_focus(app->url_entry)) { update_url_bar(app); gtk_widget_grab_focus(app->web_view); - } else { - webkit_web_view_stop_loading(WEBKIT_WEB_VIEW(app->web_view)); + did_something = TRUE; } -#else - webkit_web_view_stop_loading(WEBKIT_WEB_VIEW(app->web_view)); #endif + if (!did_something) + webkit_web_view_stop_loading(WEBKIT_WEB_VIEW(app->web_view)); break; + } case ACTION_SCROLL_UP: scroll_by(app, -SCROLL_STEP); break; case ACTION_SCROLL_DOWN: scroll_by(app, SCROLL_STEP); break; + case ACTION_FIND: + find_show(app); + break; + case ACTION_FIND_NEXT: + if (find_bar_visible(app)) + webkit_find_controller_search_next(find_controller(app)); + break; + case ACTION_FIND_PREV: + if (find_bar_visible(app)) + webkit_find_controller_search_previous(find_controller(app)); + break; } } @@ -263,8 +442,47 @@ static void activate(GtkApplication *gtk_app, gpointer user_data) { gtk_widget_set_visible(app->progress, FALSE); gtk_box_append(GTK_BOX(box), app->progress); + GtkWidget *find_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_margin_start(find_row, 6); + gtk_widget_set_margin_end(find_row, 6); + gtk_widget_set_margin_top(find_row, 4); + gtk_widget_set_margin_bottom(find_row, 4); + + app->find_entry = gtk_entry_new(); + gtk_entry_set_placeholder_text(GTK_ENTRY(app->find_entry), "Find in page\u2026"); + gtk_widget_set_hexpand(app->find_entry, TRUE); + + app->find_count_label = gtk_label_new(""); + gtk_widget_set_size_request(app->find_count_label, 90, -1); + + GtkWidget *find_prev_btn = gtk_button_new_from_icon_name("go-up-symbolic"); + GtkWidget *find_next_btn = gtk_button_new_from_icon_name("go-down-symbolic"); + GtkWidget *find_close_btn = gtk_button_new_from_icon_name("window-close-symbolic"); + gtk_widget_set_tooltip_text(find_prev_btn, "Previous match (Shift+F3)"); + gtk_widget_set_tooltip_text(find_next_btn, "Next match (F3)"); + + gtk_box_append(GTK_BOX(find_row), app->find_entry); + gtk_box_append(GTK_BOX(find_row), app->find_count_label); + gtk_box_append(GTK_BOX(find_row), find_prev_btn); + gtk_box_append(GTK_BOX(find_row), find_next_btn); + gtk_box_append(GTK_BOX(find_row), find_close_btn); + + app->find_bar = gtk_revealer_new(); + gtk_revealer_set_transition_type(GTK_REVEALER(app->find_bar), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); + gtk_revealer_set_child(GTK_REVEALER(app->find_bar), find_row); + gtk_revealer_set_reveal_child(GTK_REVEALER(app->find_bar), FALSE); + gtk_widget_set_visible(app->find_bar, FALSE); + gtk_box_append(GTK_BOX(box), app->find_bar); + + g_signal_connect(app->find_entry, "changed", G_CALLBACK(on_find_entry_changed), app); + g_signal_connect(app->find_entry, "activate", G_CALLBACK(on_find_entry_activate), app); + g_signal_connect(find_prev_btn, "clicked", G_CALLBACK(on_find_prev_clicked), app); + g_signal_connect(find_next_btn, "clicked", G_CALLBACK(on_find_next_clicked), app); + g_signal_connect(find_close_btn, "clicked", G_CALLBACK(on_find_close_clicked), app); + /* cookies/cache/history/logins/whatever persist under DATA_DIR */ WebKitNetworkSession *session = build_network_session(); + g_signal_connect(session, "download-started", G_CALLBACK(on_download_started), app); app->web_view = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW, "network-session", session, NULL)); @@ -281,6 +499,10 @@ static void activate(GtkApplication *gtk_app, gpointer user_data) { G_CALLBACK(on_progress_changed), app); g_signal_connect(app->web_view, "notify::title", G_CALLBACK(on_title_changed), app); + WebKitFindController *fctrl = webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(app->web_view)); + g_signal_connect(fctrl, "found-text", G_CALLBACK(on_found_text), app); + g_signal_connect(fctrl, "failed-to-find-text", G_CALLBACK(on_failed_to_find_text), app); + gtk_box_append(GTK_BOX(box), app->web_view); gtk_window_set_child(GTK_WINDOW(app->window), box);