v0.0.18 - Add real new window support for target=_blank and Open in New Window — uses webkit_web_view_new_with_related_view() to share WebProcess, window focus tracking for MCP active webview

This commit is contained in:
Laan Tungir
2026-07-13 06:09:41 -04:00
parent d032cef0ab
commit a722a7cc67
3 changed files with 214 additions and 17 deletions
+1 -1
View File
@@ -1 +1 @@
0.0.17
0.0.18
+211 -14
View File
@@ -61,6 +61,12 @@ static GtkWidget *g_notebook = NULL;
static WebKitWebContext *g_ctx = NULL;
static GtkWindow *g_window = NULL;
/* Active window/notebook for MCP get_active_webview().
* When a new window gets focus, these are updated so MCP tools operate
* on the focused window's webview. Defaults to the main window/notebook. */
static GtkWindow *g_active_window = NULL;
static GtkWidget *g_active_notebook = NULL;
/* Dynamic array of tab_info_t pointers, indexed by notebook page number. */
static tab_info_t **g_tabs = NULL;
static int g_tab_count = 0;
@@ -74,6 +80,28 @@ static GtkWidget *build_hamburger_menu(tab_info_t *tab);
static char *normalize_url(const char *input);
static void on_tab_close_clicked_proxy_new(GtkMenuItem *item, gpointer data);
static void on_avatar_clicked(GtkButton *btn, gpointer data);
static GtkWidget *tab_manager_new_window(const char *url,
WebKitWebView *related_view);
static gboolean on_window_focus_in(GtkWidget *widget,
GdkEventFocus *event,
gpointer user_data);
static void on_aux_window_destroy(GtkWidget *widget,
gpointer user_data);
/* Forward declarations for signal handlers used by tab_manager_new_window
* (which is defined before these handlers in the file). */
static void on_load_changed(WebKitWebView *webview,
WebKitLoadEvent load_event,
gpointer user_data);
static gboolean on_load_failed(WebKitWebView *webview,
WebKitLoadEvent load_event,
gchar *failing_uri,
GError *error,
gpointer data);
static gboolean on_webview_context_menu(WebKitWebView *webview,
WebKitContextMenu *context_menu,
GdkEvent *event,
WebKitHitTestResult *hit_test,
gpointer user_data);
/* ── URL bar completion (search dropdown) ─────────────────────────── *
* Each tab's URL entry has a GtkEntryCompletion backed by a GtkListStore.
@@ -650,30 +678,188 @@ static gboolean on_decide_policy(WebKitWebView *webview,
static GtkWidget *on_create_webview(WebKitWebView *webview,
WebKitNavigationAction *action,
gpointer user_data) {
(void)webview;
(void)user_data;
WebKitURIRequest *request = webkit_navigation_action_get_request(action);
const char *uri = webkit_uri_request_get_uri(request);
/* When a page requests a new window (target="_blank"), WebKit fires
* the "create" signal. We intercept it by opening the URI in a new
* tab ourselves, then return NULL to tell WebKit we handled it.
/* When a page requests a new window (target="_blank", window.open()),
* WebKit fires the "create" signal. We create a real GtkWindow with
* its own webview (created with webkit_web_view_new_with_related_view
* so it shares the parent's WebProcess) and return that webview.
*
* Returning a webview here causes a crash in WebKit's
* std::optional<WindowFeatures> assertion because the returned
* webview doesn't have the expected window features set up.
* By returning NULL and handling the navigation ourselves, we
* avoid the crash entirely. */
* Using webkit_web_view_new_with_related_view() shares the WebProcess
* and the related view's window features, which avoids the previous
* std::optional<WindowFeatures> assertion crash that occurred when
* returning a webview created with webkit_web_view_new_with_context().
*
* If new window creation fails, fall back to opening a tab. */
if (uri && uri[0]) {
GtkWidget *new_wv = tab_manager_new_window(uri, webview);
if (new_wv != NULL) {
return new_wv;
}
/* Fallback: open in a new tab in the main window. */
tab_manager_new_tab(uri);
}
/* Return NULL to indicate we handled the creation ourselves.
* WebKit will not proceed with the navigation in a new view. */
return NULL;
}
/* ── New window creation (real GtkWindow for target="_blank") ──────── *
* Creates a new top-level GtkWindow with its own GtkNotebook and a
* single webview. The webview is created with
* webkit_web_view_new_with_related_view(related_view) when a parent
* webview is available, so it shares the parent's WebProcess and
* window features — this avoids the std::optional<WindowFeatures>
* assertion crash that occurred with webkit_web_view_new_with_context().
*
* The new window's webview gets the same setup as a regular tab:
* WebKitSettings, nostr_inject_setup, key-press-event, decide-policy,
* create, load-changed, etc. The window does NOT quit the app when
* closed — only the main window (in main.c) does that.
*
* Returns the new WebKitWebView widget, or NULL on failure.
*/
/* focus-in-event handler for auxiliary windows: updates the global
* active window/notebook pointers so MCP get_active_webview() resolves
* to the focused window's webview. */
static gboolean on_window_focus_in(GtkWidget *widget,
GdkEventFocus *event,
gpointer user_data) {
(void)event;
GtkWidget *notebook = GTK_WIDGET(user_data);
if (widget == NULL || notebook == NULL) return FALSE;
g_active_window = GTK_WINDOW(widget);
g_active_notebook = notebook;
g_print("[windows] Active window changed to %p (notebook %p)\n",
(void *)g_active_window, (void *)g_active_notebook);
return FALSE;
}
/* destroy handler for auxiliary windows: if this was the active window,
* fall back to the main window/notebook so MCP keeps working. Does NOT
* quit the app — only the main window's destroy handler does that. */
static void on_aux_window_destroy(GtkWidget *widget, gpointer user_data) {
(void)user_data;
if (g_active_window == GTK_WINDOW(widget)) {
g_active_window = g_window;
g_active_notebook = g_notebook;
g_print("[windows] Active window closed, reverting to main window\n");
}
g_print("[windows] Auxiliary window destroyed: %p\n", (void *)widget);
}
static GtkWidget *tab_manager_new_window(const char *url,
WebKitWebView *related_view) {
if (g_ctx == NULL) return NULL;
/* Create the top-level window. */
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(window), "sovereign browser");
gtk_window_set_default_size(GTK_WINDOW(window), 1024, 768);
/* Create a notebook for this window (so future tabs could be added;
* for now it holds a single page). */
GtkWidget *notebook = gtk_notebook_new();
gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook), FALSE);
gtk_notebook_set_show_border(GTK_NOTEBOOK(notebook), FALSE);
gtk_notebook_set_show_tabs(GTK_NOTEBOOK(notebook), TRUE);
gtk_container_add(GTK_CONTAINER(window), notebook);
/* Create the webview. Prefer webkit_web_view_new_with_related_view
* so the new webview shares the parent's WebProcess and window
* features (avoids the WindowFeatures assertion crash). Fall back
* to the shared context if no related view is provided. */
WebKitWebView *wv = NULL;
if (related_view != NULL && WEBKIT_IS_WEB_VIEW(related_view)) {
wv = WEBKIT_WEB_VIEW(webkit_web_view_new_with_related_view(related_view));
} else {
wv = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(g_ctx));
}
if (wv == NULL) {
gtk_widget_destroy(window);
return NULL;
}
/* Apply the same WebKitSettings as regular tabs. */
WebKitSettings *settings = webkit_web_view_get_settings(wv);
webkit_settings_set_enable_developer_extras(settings, TRUE);
webkit_settings_set_enable_javascript(settings, TRUE);
webkit_settings_set_javascript_can_open_windows_automatically(settings, TRUE);
webkit_settings_set_allow_file_access_from_file_urls(settings, TRUE);
webkit_settings_set_allow_universal_access_from_file_urls(settings, TRUE);
webkit_settings_set_allow_modal_dialogs(settings, TRUE);
/* Inject window.nostr into this webview. */
nostr_inject_setup(wv);
/* Connect the key-press handler (browser shortcuts). */
g_signal_connect(G_OBJECT(wv), "key-press-event",
G_CALLBACK(on_key_press), NULL);
/* Ensure the webview expands to fill the window. */
gtk_widget_set_vexpand(GTK_WIDGET(wv), TRUE);
gtk_widget_set_hexpand(GTK_WIDGET(wv), TRUE);
/* Build a minimal tab label for the notebook page. */
GtkWidget *label_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
GtkWidget *title_lbl = gtk_label_new("Loading…");
gtk_label_set_ellipsize(GTK_LABEL(title_lbl), PANGO_ELLIPSIZE_END);
gtk_label_set_max_width_chars(GTK_LABEL(title_lbl), 20);
gtk_widget_set_hexpand(title_lbl, TRUE);
gtk_box_pack_start(GTK_BOX(label_box), title_lbl, TRUE, TRUE, 0);
/* Add the webview as a notebook page. */
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), GTK_WIDGET(wv),
label_box);
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(notebook), GTK_WIDGET(wv),
TRUE);
/* Wire the same webview signals as tab_create(). We pass NULL as
* user_data for handlers that expect a tab_info_t* — those handlers
* only dereference user_data for UI updates (URL bar, tab title,
* favicon) that don't apply to a standalone window webview. The
* navigation-relevant handlers (decide-policy, create, load-failed)
* don't need the tab pointer. */
g_signal_connect(wv, "load-changed",
G_CALLBACK(on_load_changed), NULL);
g_signal_connect(wv, "load-failed",
G_CALLBACK(on_load_failed), NULL);
g_signal_connect(wv, "decide-policy",
G_CALLBACK(on_decide_policy), NULL);
g_signal_connect(wv, "create",
G_CALLBACK(on_create_webview), NULL);
g_signal_connect(wv, "context-menu",
G_CALLBACK(on_webview_context_menu), NULL);
/* Window lifecycle: focus-in updates the active window/notebook
* pointers; destroy reverts to the main window but does NOT quit. */
g_signal_connect(window, "focus-in-event",
G_CALLBACK(on_window_focus_in), notebook);
g_signal_connect(window, "destroy",
G_CALLBACK(on_aux_window_destroy), NULL);
/* Load the URL. */
if (url && url[0]) {
webkit_web_view_load_uri(wv, url);
}
/* Show everything and present the window. */
gtk_widget_show_all(window);
gtk_window_present(GTK_WINDOW(window));
/* This new window is now the active window. */
g_active_window = GTK_WINDOW(window);
g_active_notebook = notebook;
g_print("[windows] Created new window %p for %s\n",
(void *)window, url ? url : "(none)");
return GTK_WIDGET(wv);
}
static GtkWidget *build_tab_label(tab_info_t *tab) {
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
@@ -829,7 +1015,11 @@ static void on_load_changed(WebKitWebView *webview,
gpointer user_data) {
tab_info_t *tab = (tab_info_t *)user_data;
/* tab is NULL for webviews in auxiliary (new) windows, which don't
* have a tab_info_t. Skip the tab-UI updates (URL bar, favicon, tab
* title) but still record history on load-finished below. */
if (load_event == WEBKIT_LOAD_COMMITTED) {
if (tab == NULL) return;
const gchar *uri = webkit_web_view_get_uri(webview);
if (uri != NULL) {
/* Clear the favicon — the new page's favicon (if any) will
@@ -873,8 +1063,8 @@ static void on_load_changed(WebKitWebView *webview,
uri ? uri : "(null)",
(title && title[0]) ? title : "(none)");
/* Update tab title. */
if (title && title[0]) {
/* Update tab title (only for tabs in the main notebook). */
if (tab != NULL && title && title[0]) {
int index = gtk_notebook_page_num(GTK_NOTEBOOK(g_notebook),
tab->page);
if (index >= 0) {
@@ -882,7 +1072,8 @@ static void on_load_changed(WebKitWebView *webview,
}
}
/* Add to history (with title for the Recents submenu tooltip). */
/* Add to history (with title for the Recents submenu tooltip).
* This applies to both main-window tabs and auxiliary windows. */
if (uri != NULL && uri[0] != '\0') {
history_add_titled(uri, (title && title[0]) ? title : NULL);
}
@@ -2025,6 +2216,12 @@ void tab_manager_init(GtkContainer *parent,
gtk_container_add(parent, g_notebook);
/* The main window/notebook are the default active window/notebook
* for MCP get_active_webview(). Updated when auxiliary windows
* gain focus (see on_window_focus_in). */
g_active_window = window;
g_active_notebook = g_notebook;
tab_manager_apply_settings();
}
+2 -2
View File
@@ -11,9 +11,9 @@
#ifndef SOVEREIGN_BROWSER_VERSION_H
#define SOVEREIGN_BROWSER_VERSION_H
#define SB_VERSION "v0.0.17"
#define SB_VERSION "v0.0.18"
#define SB_VERSION_MAJOR 0
#define SB_VERSION_MINOR 0
#define SB_VERSION_PATCH 17
#define SB_VERSION_PATCH 18
#endif /* SOVEREIGN_BROWSER_VERSION_H */