Added Ctrl+F search bar and Downloads

This commit is contained in:
tsukasa 2026-08-13 16:22:36 +08:00
parent 837734ab3c
commit 11800a9a0c
2 changed files with 235 additions and 5 deletions

View file

@ -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]);

232
page.c
View file

@ -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);