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:
+35
-9
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user