v0.0.29 - Tab right-click context menu with Open in New Window, Ctrl+N new window shortcut, floating X close button on sidebar, and fixed shortcut bypass for sovereign:// pages

This commit is contained in:
Laan Tungir
2026-07-16 10:37:48 -04:00
parent 0a1b117746
commit b393308b2e
7 changed files with 266 additions and 15 deletions
+1 -1
View File
@@ -1 +1 @@
0.0.28
0.0.29
+35 -9
View File
@@ -281,20 +281,42 @@ void on_menu_agent(GtkMenuItem *item, gpointer data) {
* sovereign://settings page (key-capture UI) and persisted to SQLite.
*/
/* Made non-static so tab_manager.c can connect it to each webview. */
/* Made non-static so tab_manager.c can connect it to each webview.
*
* This handler is connected to two places:
* 1. Each webview's "key-press-event" (in tab_manager.c tab_create())
* 2. The main window's "key-press-event" (in main.c setup)
*
* For the webview connection, we bypass shortcut interception when the
* active page is a sovereign:// internal page — this lets the page's JS
* capture arbitrary key combos (e.g. the settings page's shortcut capture
* UI needs to see Ctrl+key events that would otherwise be intercepted).
*
* For the window connection, we always process shortcuts so that
* browser-level shortcuts (Ctrl+T, Ctrl+N, etc.) work even on
* sovereign:// pages. The window handler is a fallback that fires after
* the webview handler (event propagation: child → parent → window). */
gboolean on_key_press(GtkWidget *widget, GdkEventKey *event,
gpointer data) {
(void)widget;
(void)data;
/* Check if this handler is connected to a webview (not the window).
* WEBKIT_IS_WEB_VIEW is true for webview connections, false for the
* window connection. */
gboolean from_webview = WEBKIT_IS_WEB_VIEW(widget);
/* When the active tab is showing a sovereign:// internal page (e.g.
* the settings page's keyboard-shortcut capture), don't intercept
* any key events — let them pass through to the web page's JS so it
* can capture arbitrary key combos (including Ctrl+key). */
tab_info_t *active = tab_manager_get_active();
if (active && active->current_url[0] != '\0' &&
strncmp(active->current_url, "sovereign://", 12) == 0) {
return FALSE;
* the settings page's keyboard-shortcut capture), bypass shortcut
* interception on the webview connection only — let the event pass
* through to the web page's JS so it can capture arbitrary key combos.
* The window-level connection still processes shortcuts so Ctrl+T,
* Ctrl+N, etc. work on internal pages. */
if (from_webview) {
tab_info_t *active = tab_manager_get_active();
if (active && active->current_url[0] != '\0' &&
strncmp(active->current_url, "sovereign://", 12) == 0) {
return FALSE;
}
}
int action = shortcuts_lookup(event);
@@ -306,6 +328,10 @@ gboolean on_key_press(GtkWidget *widget, GdkEventKey *event,
tab_manager_new_tab(NULL);
return TRUE;
case SHORTCUT_NEW_WINDOW:
tab_manager_new_window_blank();
return TRUE;
case SHORTCUT_CLOSE_TAB:
tab_manager_close_active();
return TRUE;
+4
View File
@@ -26,6 +26,10 @@ static const shortcut_meta_t g_registry[SHORTCUT_COUNT] = {
"new_tab", "New tab",
"Open a new tab", "<Control>t"
},
[SHORTCUT_NEW_WINDOW] = {
"new_window", "New window",
"Open a new browser window", "<Control>n"
},
[SHORTCUT_CLOSE_TAB] = {
"close_tab", "Close tab",
"Close the active tab", "<Control>w"
+1
View File
@@ -28,6 +28,7 @@ extern "C" {
typedef enum {
SHORTCUT_NEW_TAB = 0,
SHORTCUT_NEW_WINDOW,
SHORTCUT_CLOSE_TAB,
SHORTCUT_FOCUS_URL,
SHORTCUT_NEXT_TAB,
+210 -3
View File
@@ -145,6 +145,9 @@ static void on_tab_close_clicked_proxy_new(GtkMenuItem *item, gpointer d
static void on_avatar_clicked(GtkButton *btn, gpointer data);
static GtkWidget *tab_manager_new_window(const char *url,
WebKitWebView *related_view);
static gboolean on_notebook_button_press(GtkWidget *widget,
GdkEventButton *event,
gpointer user_data);
static gboolean on_window_focus_in(GtkWidget *widget,
GdkEventFocus *event,
gpointer user_data);
@@ -562,6 +565,117 @@ static void on_tab_close_clicked(GtkButton *btn, gpointer user_data) {
}
}
/* Notebook-level button-press handler. Catches right-clicks on tab
* labels that GtkNotebook's internal handling would otherwise consume
* before the tab label's own button-press-event handler fires. We find
* which tab was clicked by checking the tab label's allocation against
* the click coordinates, then show the same context menu as
* on_tab_label_button_press. */
static gboolean on_notebook_button_press(GtkWidget *widget,
GdkEventButton *event,
gpointer user_data) {
(void)user_data;
if (widget == NULL) return FALSE;
GtkNotebook *nb = GTK_NOTEBOOK(widget);
if (event->button != 3 && event->button != 2) return FALSE;
gint n_pages = gtk_notebook_get_n_pages(nb);
for (gint i = 0; i < n_pages; i++) {
GtkWidget *page = gtk_notebook_get_nth_page(nb, i);
if (page == NULL) continue;
GtkWidget *label = gtk_notebook_get_tab_label(nb, page);
if (label == NULL) continue;
GtkAllocation alloc;
gtk_widget_get_allocation(label, &alloc);
/* Convert click coords to the label's coordinate space. The
* event coords are relative to the notebook's bin window. */
gint x = (gint)event->x;
gint y = (gint)event->y;
/* Get the label's allocation position relative to the notebook. */
gint lx = alloc.x;
gint ly = alloc.y;
if (x >= lx && x < lx + alloc.width &&
y >= ly && y < ly + alloc.height) {
/* Found the clicked tab. Look up the tab_info_t. */
if (i < g_tab_count && g_tabs[i] != NULL) {
/* Reuse the existing handler by synthesizing a call. */
tab_info_t *tab = g_tabs[i];
int index = tab_array_find(tab);
if (index < 0) return FALSE;
if (event->button == 2) {
const browser_settings_t *s = settings_get();
if (s->middle_click_close) {
tab_manager_close_tab(index);
return TRUE;
}
return FALSE;
}
/* Right-click: show context menu (same as
* on_tab_label_button_press). */
GtkWidget *menu = gtk_menu_new();
GtkWidget *item_new = gtk_menu_item_new_with_label("New Tab");
g_signal_connect(item_new, "activate",
G_CALLBACK(on_tab_close_clicked_proxy_new), NULL);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_new);
GtkWidget *item_close = gtk_menu_item_new_with_label("Close Tab");
g_signal_connect_swapped(item_close, "activate",
G_CALLBACK(tab_manager_close_tab),
GINT_TO_POINTER(index));
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_close);
GtkWidget *item_close_others =
gtk_menu_item_new_with_label("Close Other Tabs");
g_signal_connect_swapped(item_close_others, "activate",
G_CALLBACK(tab_manager_close_others),
GINT_TO_POINTER(index));
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_close_others);
GtkWidget *item_close_right =
gtk_menu_item_new_with_label("Close Tabs to the Right");
g_signal_connect_swapped(item_close_right, "activate",
G_CALLBACK(tab_manager_close_to_right),
GINT_TO_POINTER(index));
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_close_right);
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
gtk_separator_menu_item_new());
GtkWidget *item_new_win =
gtk_menu_item_new_with_label("Open in New Window");
g_signal_connect_swapped(item_new_win, "activate",
G_CALLBACK(tab_manager_open_in_new_window),
GINT_TO_POINTER(index));
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_new_win);
GtkWidget *item_dup = gtk_menu_item_new_with_label("Duplicate Tab");
g_signal_connect_swapped(item_dup, "activate",
G_CALLBACK(tab_manager_duplicate),
GINT_TO_POINTER(index));
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_dup);
GtkWidget *item_reload = gtk_menu_item_new_with_label("Reload Tab");
g_signal_connect_swapped(item_reload, "activate",
G_CALLBACK(tab_manager_reload),
GINT_TO_POINTER(index));
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_reload);
gtk_widget_show_all(menu);
gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
return TRUE;
}
}
}
return FALSE;
}
static gboolean on_tab_label_button_press(GtkWidget *widget,
GdkEventButton *event,
gpointer user_data) {
@@ -612,6 +726,13 @@ static gboolean on_tab_label_button_press(GtkWidget *widget,
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
gtk_separator_menu_item_new());
GtkWidget *item_new_win =
gtk_menu_item_new_with_label("Open in New Window");
g_signal_connect_swapped(item_new_win, "activate",
G_CALLBACK(tab_manager_open_in_new_window),
GINT_TO_POINTER(index));
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_new_win);
GtkWidget *item_dup = gtk_menu_item_new_with_label("Duplicate Tab");
g_signal_connect_swapped(item_dup, "activate",
G_CALLBACK(tab_manager_duplicate),
@@ -987,13 +1108,33 @@ static GtkWidget *build_tab_label(tab_info_t *tab) {
gtk_widget_set_tooltip_text(close_btn, "Close tab");
g_signal_connect(close_btn, "clicked",
G_CALLBACK(on_tab_close_clicked), tab);
/* Connect button-press so right-click on the close button
* also triggers the tab context menu. */
gtk_widget_add_events(close_btn, GDK_BUTTON_PRESS_MASK);
g_signal_connect(close_btn, "button-press-event",
G_CALLBACK(on_tab_label_button_press), tab);
gtk_box_pack_start(GTK_BOX(box), close_btn, FALSE, FALSE, 0);
}
/* Connect button-press for middle-click and right-click on the label. */
/* Connect button-press for middle-click and right-click on the label.
* We add the event mask and connect the handler to the box AND all
* child widgets (favicon, label, close button), because child widgets
* intercept button-press events before they bubble up to the parent.
* Without this, right-clicking on the label or close button wouldn't
* show the context menu. */
gtk_widget_add_events(box, GDK_BUTTON_PRESS_MASK);
g_signal_connect(box, "button-press-event",
G_CALLBACK(on_tab_label_button_press), tab);
/* Also connect child widgets so right-clicks on them trigger the
* context menu. The handler checks event->button and only acts on
* right-click (button 3) and middle-click (button 2), so normal
* left-clicks on the close button still work. */
gtk_widget_add_events(tab->favicon, GDK_BUTTON_PRESS_MASK);
g_signal_connect(tab->favicon, "button-press-event",
G_CALLBACK(on_tab_label_button_press), tab);
gtk_widget_add_events(tab->title_label, GDK_BUTTON_PRESS_MASK);
g_signal_connect(tab->title_label, "button-press-event",
G_CALLBACK(on_tab_label_button_press), tab);
gtk_widget_show_all(box);
return box;
@@ -2372,6 +2513,15 @@ void tab_manager_init(GtkContainer *parent,
gtk_notebook_set_show_border(GTK_NOTEBOOK(g_notebook), FALSE);
gtk_notebook_set_show_tabs(GTK_NOTEBOOK(g_notebook), TRUE);
/* Connect button-press on the notebook to handle right-click context
* menus on tabs. This is more reliable than connecting to individual
* tab label child widgets, because GtkNotebook intercepts button
* presses on tabs for tab switching before they reach the label's
* children. */
gtk_widget_add_events(g_notebook, GDK_BUTTON_PRESS_MASK);
g_signal_connect(g_notebook, "button-press-event",
G_CALLBACK(on_notebook_button_press), NULL);
/* New-tab button as an action widget at the end of the tab strip. */
GtkWidget *new_btn = gtk_button_new();
gtk_button_set_relief(GTK_BUTTON(new_btn), GTK_RELIEF_NONE);
@@ -2686,6 +2836,23 @@ void tab_manager_duplicate(int index) {
tab_manager_new_tab(tab->current_url);
}
void tab_manager_open_in_new_window(int index) {
if (index < 0 || index >= g_tab_count) return;
tab_info_t *tab = g_tabs[index];
if (tab == NULL) return;
/* Open the tab's current URL in a new window. Pass the active
* webview as the related view so the new window shares the
* WebProcess. */
tab_manager_new_window(tab->current_url, tab->webview);
}
void tab_manager_new_window_blank(void) {
/* Open a new window with the default new-tab URL. Pass the active
* webview as the related view for WebProcess sharing. */
tab_info_t *tab = tab_manager_get_active();
tab_manager_new_window(NULL, tab ? tab->webview : NULL);
}
void tab_manager_reload(int index) {
if (index < 0 || index >= g_tab_count) return;
tab_info_t *tab = g_tabs[index];
@@ -2727,6 +2894,19 @@ static window_state_t *window_state_for_notebook(GtkWidget *notebook) {
* scheme works. It is packed into the sidebar container (the left pane
* of the window-level GtkPaned). Called the first time the sidebar is
* shown for a window. */
/* Close button callback for the sidebar floating X button. */
static void on_sidebar_close_clicked(GtkButton *btn, gpointer user_data) {
(void)btn;
window_state_t *ws = (window_state_t *)user_data;
if (ws == NULL) return;
if (ws->sidebar_visible) {
gtk_paned_set_position(GTK_PANED(ws->paned), 0);
gtk_widget_hide(ws->sidebar_container);
ws->sidebar_visible = FALSE;
}
}
static void sidebar_create_webview(window_state_t *ws) {
if (ws == NULL || ws->sidebar_webview != NULL) return;
if (g_ctx == NULL) return;
@@ -2753,9 +2933,36 @@ static void sidebar_create_webview(window_state_t *ws) {
gtk_widget_set_vexpand(GTK_WIDGET(ws->sidebar_webview), TRUE);
gtk_widget_set_hexpand(GTK_WIDGET(ws->sidebar_webview), TRUE);
/* Pack into the sidebar container. */
/* Use a GtkOverlay to float a close (X) button in the top-right
* corner over the webview, without a separate header bar. This
* saves vertical space — the X sits on the same row as the chat
* page's tab bar (Chat / Conversations / Skills). */
GtkWidget *overlay = gtk_overlay_new();
gtk_widget_set_vexpand(overlay, TRUE);
gtk_widget_set_hexpand(overlay, TRUE);
gtk_container_add(GTK_CONTAINER(overlay),
GTK_WIDGET(ws->sidebar_webview));
/* Floating close button — top-right corner. */
GtkWidget *close_btn = gtk_button_new();
gtk_button_set_relief(GTK_BUTTON(close_btn), GTK_RELIEF_NONE);
gtk_button_set_image(GTK_BUTTON(close_btn),
gtk_image_new_from_icon_name("window-close-symbolic",
GTK_ICON_SIZE_MENU));
gtk_widget_set_tooltip_text(close_btn, "Close sidebar");
gtk_widget_set_halign(close_btn, GTK_ALIGN_END);
gtk_widget_set_valign(close_btn, GTK_ALIGN_START);
gtk_widget_set_margin_top(close_btn, 2);
gtk_widget_set_margin_end(close_btn, 2);
g_signal_connect(close_btn, "clicked",
G_CALLBACK(on_sidebar_close_clicked), ws);
gtk_overlay_add_overlay(GTK_OVERLAY(overlay), close_btn);
/* Pack the overlay into the sidebar container. */
gtk_box_pack_start(GTK_BOX(ws->sidebar_container),
GTK_WIDGET(ws->sidebar_webview), TRUE, TRUE, 0);
overlay, TRUE, TRUE, 0);
/* Load the chat page. */
webkit_web_view_load_uri(ws->sidebar_webview, AGENT_CHAT_URL_STR);
+13
View File
@@ -51,6 +51,19 @@ void tab_manager_init(GtkContainer *parent,
*/
int tab_manager_new_tab(const char *url);
/*
* Open the tab at the given index's URL in a new browser window.
* Creates a new top-level GtkWindow with its own notebook and a single
* tab loading the same URL. No-op if the index is out of range.
*/
void tab_manager_open_in_new_window(int index);
/*
* Open a new browser window with a blank tab (or the default new-tab
* URL). Used by the Ctrl+N shortcut.
*/
void tab_manager_new_window_blank(void);
/*
* Close the tab at the given index. If it's the last tab, the window
* destroy handler will fire (caller is responsible for quitting).
+2 -2
View File
@@ -11,9 +11,9 @@
#ifndef SOVEREIGN_BROWSER_VERSION_H
#define SOVEREIGN_BROWSER_VERSION_H
#define SB_VERSION "v0.0.28"
#define SB_VERSION "v0.0.29"
#define SB_VERSION_MAJOR 0
#define SB_VERSION_MINOR 0
#define SB_VERSION_PATCH 28
#define SB_VERSION_PATCH 29
#endif /* SOVEREIGN_BROWSER_VERSION_H */