/* * tab_manager.c — multi-tab management for sovereign_browser * * Each tab is a GtkBox page containing a per-tab toolbar (hamburger menu + * URL entry) and a WebKitWebView. All webviews share the same * WebKitWebContext so the sovereign:// bridge and security settings apply * to every tab automatically. */ #include "tab_manager.h" #include "settings.h" #include "nostr_inject.h" #include "history.h" #include "version.h" #include #include /* ── External callbacks defined in main.c ─────────────────────────── * * These handle identity-related menu actions that need access to the * global app_state_t (signer, pubkey, method). main.c owns that state. */ /* Proxy functions defined in main.c that wrap the app_state_t-aware * callbacks so they match GTK signal handler signatures. */ extern void app_menu_switch_identity_proxy(GtkMenuItem *item, gpointer data); extern void app_menu_lock_session_proxy(GtkMenuItem *item, gpointer data); extern void app_menu_logout_proxy(GtkMenuItem *item, gpointer data); extern void app_menu_security_strip_proxy(GtkCheckMenuItem *item, gpointer data); extern void app_menu_fips_proxy(GtkMenuItem *item, gpointer data); extern void app_menu_nostr_sign_proxy(GtkMenuItem *item, gpointer data); extern void app_menu_about_proxy(GtkMenuItem *item, gpointer data); extern void on_menu_settings(GtkMenuItem *item, gpointer data); /* ── Static state ─────────────────────────────────────────────────── */ static GtkWidget *g_notebook = NULL; static WebKitWebContext *g_ctx = NULL; static GtkWindow *g_window = 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; static int g_tab_cap = 0; /* ── Forward declarations ─────────────────────────────────────────── */ static tab_info_t *tab_create(const char *url); static GtkWidget *build_tab_label(tab_info_t *tab); 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); /* ── URL helper (same logic as main.c's normalize_url) ────────────── */ static char *normalize_url(const char *input) { if (input == NULL || input[0] == '\0') { return NULL; } if (strstr(input, "://") != NULL) { return g_strdup(input); } return g_strdup_printf("https://%s", input); } /* ── Tab label widget ─────────────────────────────────────────────── */ static void on_tab_close_clicked(GtkButton *btn, gpointer user_data) { tab_info_t *tab = (tab_info_t *)user_data; (void)btn; int index = gtk_notebook_page_num(GTK_NOTEBOOK(g_notebook), tab->page); if (index >= 0) { tab_manager_close_tab(index); } } static gboolean on_tab_label_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data) { (void)widget; tab_info_t *tab = (tab_info_t *)user_data; int index = gtk_notebook_page_num(GTK_NOTEBOOK(g_notebook), tab->page); if (index < 0) return FALSE; const browser_settings_t *s = settings_get(); /* Middle-click to close. */ if (event->button == 2 && s->middle_click_close) { tab_manager_close_tab(index); return TRUE; } /* Right-click context menu. */ if (event->button == 3) { 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_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; } /* Proxy callback for "New Tab" in the context menu — just calls new_tab. */ static void on_tab_close_clicked_proxy_new(GtkMenuItem *item, gpointer data) { (void)item; (void)data; tab_manager_new_tab(NULL); } static GtkWidget *build_tab_label(tab_info_t *tab) { GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); /* Favicon placeholder icon. */ GtkWidget *icon = gtk_image_new_from_icon_name("text-html", GTK_ICON_SIZE_MENU); gtk_box_pack_start(GTK_BOX(box), icon, FALSE, FALSE, 0); /* Title label. */ tab->title_label = gtk_label_new("New Tab"); gtk_label_set_ellipsize(GTK_LABEL(tab->title_label), PANGO_ELLIPSIZE_END); gtk_label_set_max_width_chars(GTK_LABEL(tab->title_label), 20); gtk_box_pack_start(GTK_BOX(box), tab->title_label, TRUE, TRUE, 0); /* Close button. */ const browser_settings_t *s = settings_get(); if (s->show_tab_close_buttons) { 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 tab"); g_signal_connect(close_btn, "clicked", G_CALLBACK(on_tab_close_clicked), 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. */ gtk_widget_add_events(box, GDK_BUTTON_PRESS_MASK); g_signal_connect(box, "button-press-event", G_CALLBACK(on_tab_label_button_press), tab); gtk_widget_show_all(box); return box; } /* ── Per-tab signal handlers ──────────────────────────────────────── */ static void on_url_activate(GtkEntry *entry, gpointer user_data) { tab_info_t *tab = (tab_info_t *)user_data; const char *text = gtk_entry_get_text(entry); char *url = normalize_url(text); if (url != NULL) { webkit_web_view_load_uri(tab->webview, url); g_free(url); } } static void on_load_changed(WebKitWebView *webview, WebKitLoadEvent load_event, gpointer user_data) { tab_info_t *tab = (tab_info_t *)user_data; if (load_event == WEBKIT_LOAD_COMMITTED) { const gchar *uri = webkit_web_view_get_uri(webview); if (uri != NULL) { gtk_entry_set_text(GTK_ENTRY(tab->url_entry), uri); snprintf(tab->current_url, sizeof(tab->current_url), "%s", uri); } } else if (load_event == WEBKIT_LOAD_FINISHED) { const gchar *title = webkit_web_view_get_title(webview); const gchar *uri = webkit_web_view_get_uri(webview); g_print("[loaded] %s -- title: %s\n", uri ? uri : "(null)", (title && title[0]) ? title : "(none)"); /* Update tab title. */ if (title && title[0]) { int index = gtk_notebook_page_num(GTK_NOTEBOOK(g_notebook), tab->page); if (index >= 0) { tab_manager_set_title(index, title); } } /* Add to history. */ if (uri != NULL && uri[0] != '\0') { history_add(uri); } } } static gboolean on_load_failed(WebKitWebView *webview, WebKitLoadEvent load_event, gchar *failing_uri, GError *error, gpointer data) { (void)webview; (void)load_event; (void)data; g_print("[failed] %s -- %s\n", failing_uri ? failing_uri : "(null)", error ? error->message : "(unknown)"); return FALSE; } /* ── Hamburger menu callbacks (webview-specific) ──────────────────── */ static void on_menu_reload(GtkMenuItem *item, gpointer data) { (void)item; (void)data; tab_info_t *tab = tab_manager_get_active(); if (tab && tab->webview) { webkit_web_view_reload_bypass_cache(tab->webview); } } static void on_menu_stop(GtkMenuItem *item, gpointer data) { (void)item; (void)data; tab_info_t *tab = tab_manager_get_active(); if (tab && tab->webview) { webkit_web_view_stop_loading(tab->webview); } } static void on_menu_inspector(GtkMenuItem *item, gpointer data) { (void)item; (void)data; tab_info_t *tab = tab_manager_get_active(); if (tab && tab->webview) { WebKitWebInspector *insp = webkit_web_view_get_inspector(tab->webview); if (insp) webkit_web_inspector_show(insp); } } static void on_menu_open_file(GtkMenuItem *item, gpointer data) { (void)item; (void)data; if (g_window == NULL) return; GtkWidget *dialog = gtk_file_chooser_dialog_new( "Open File", g_window, GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL); GtkFileFilter *filter_html = gtk_file_filter_new(); gtk_file_filter_set_name(filter_html, "HTML files"); gtk_file_filter_add_pattern(filter_html, "*.html"); gtk_file_filter_add_pattern(filter_html, "*.htm"); gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter_html); GtkFileFilter *filter_all = gtk_file_filter_new(); gtk_file_filter_set_name(filter_all, "All files"); gtk_file_filter_add_pattern(filter_all, "*"); gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter_all); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); char *uri = g_strdup_printf("file://%s", filename); tab_info_t *tab = tab_manager_get_active(); if (tab && tab->webview) { webkit_web_view_load_uri(tab->webview, uri); } g_free(uri); g_free(filename); } gtk_widget_destroy(dialog); } static void on_menu_history_item(GtkMenuItem *item, gpointer data) { (void)data; tab_info_t *tab = tab_manager_get_active(); if (tab == NULL || tab->webview == NULL || item == NULL) return; const char *url = gtk_menu_item_get_label(item); if (url && url[0]) { char *normalized = normalize_url(url); if (normalized) { webkit_web_view_load_uri(tab->webview, normalized); g_free(normalized); } } } static void on_menu_history_clear(GtkMenuItem *item, gpointer data) { (void)item; (void)data; history_clear(); g_print("[history] cleared\n"); } static void on_history_menu_show(GtkWidget *menu, gpointer user_data) { (void)user_data; /* Remove all existing items. */ GList *children = gtk_container_get_children(GTK_CONTAINER(menu)); for (GList *l = children; l != NULL; l = l->next) { gtk_widget_destroy(GTK_WIDGET(l->data)); } g_list_free(children); /* Rebuild with current history. */ int hcount = history_count(); if (hcount == 0) { GtkWidget *empty = gtk_menu_item_new_with_label("(no recent pages)"); gtk_widget_set_sensitive(empty, FALSE); gtk_menu_shell_append(GTK_MENU_SHELL(menu), empty); } else { int show = hcount < 20 ? hcount : 20; for (int i = 0; i < show; i++) { const char *url = history_get(i); if (url == NULL) break; char label[80]; if (strlen(url) > 75) { snprintf(label, sizeof(label), "%.72s...", url); } else { snprintf(label, sizeof(label), "%s", url); } GtkWidget *hitem = gtk_menu_item_new_with_label(label); gtk_widget_set_tooltip_text(hitem, url); g_signal_connect(hitem, "activate", G_CALLBACK(on_menu_history_item), NULL); gtk_menu_shell_append(GTK_MENU_SHELL(menu), hitem); } gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); GtkWidget *clear_item = gtk_menu_item_new_with_label("Clear Recents"); g_signal_connect(clear_item, "activate", G_CALLBACK(on_menu_history_clear), NULL); gtk_menu_shell_append(GTK_MENU_SHELL(menu), clear_item); } gtk_widget_show_all(menu); } /* ── Hamburger menu builder ───────────────────────────────────────── */ static GtkWidget *build_hamburger_menu(tab_info_t *tab) { (void)tab; GtkWidget *menu = gtk_menu_new(); /* Navigation group. */ GtkWidget *item_open = gtk_menu_item_new_with_label("Open File…"); GtkWidget *item_reload = gtk_menu_item_new_with_label("Reload"); GtkWidget *item_stop = gtk_menu_item_new_with_label("Stop"); g_signal_connect(item_open, "activate", G_CALLBACK(on_menu_open_file), NULL); g_signal_connect(item_reload, "activate", G_CALLBACK(on_menu_reload), NULL); g_signal_connect(item_stop, "activate", G_CALLBACK(on_menu_stop), NULL); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_open); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_reload); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_stop); gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); /* Recents submenu. */ GtkWidget *history_menu = gtk_menu_new(); g_signal_connect(history_menu, "show", G_CALLBACK(on_history_menu_show), NULL); GtkWidget *item_history = gtk_menu_item_new_with_label("Recents"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_history), history_menu); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_history); gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); /* Identity group — delegated to main.c (app_state_t). */ GtkWidget *item_switch = gtk_menu_item_new_with_label("Switch Identity…"); GtkWidget *item_lock = gtk_menu_item_new_with_label("Lock Session"); GtkWidget *item_logout = gtk_menu_item_new_with_label("Logout"); g_signal_connect(item_switch, "activate", G_CALLBACK(app_menu_switch_identity_proxy), g_window); g_signal_connect(item_lock, "activate", G_CALLBACK(app_menu_lock_session_proxy), NULL); g_signal_connect(item_logout, "activate", G_CALLBACK(app_menu_logout_proxy), NULL); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_switch); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_lock); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_logout); gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); /* Roadmap group. */ GtkWidget *item_security = gtk_check_menu_item_new_with_label("Security strip (SOP/CORS/certs)"); gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item_security), FALSE); g_signal_connect(item_security, "toggled", G_CALLBACK(app_menu_security_strip_proxy), NULL); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_security); GtkWidget *item_fips = gtk_menu_item_new_with_label("FIPS URI scheme…"); GtkWidget *item_nostr = gtk_menu_item_new_with_label("Nostr signing status"); g_signal_connect(item_fips, "activate", G_CALLBACK(app_menu_fips_proxy), NULL); g_signal_connect(item_nostr, "activate", G_CALLBACK(app_menu_nostr_sign_proxy), NULL); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_fips); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_nostr); gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); GtkWidget *item_inspector = gtk_menu_item_new_with_label("Toggle Inspector"); g_signal_connect(item_inspector, "activate", G_CALLBACK(on_menu_inspector), NULL); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_inspector); GtkWidget *item_settings = gtk_menu_item_new_with_label("Settings…"); g_signal_connect(item_settings, "activate", G_CALLBACK(on_menu_settings), g_window); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_settings); GtkWidget *item_about = gtk_menu_item_new_with_label("About"); g_signal_connect(item_about, "activate", G_CALLBACK(app_menu_about_proxy), g_window); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_about); gtk_widget_show_all(menu); GtkWidget *button = gtk_menu_button_new(); gtk_menu_button_set_popup(GTK_MENU_BUTTON(button), menu); gtk_button_set_image( GTK_BUTTON(button), gtk_image_new_from_icon_name("open-menu-symbolic", GTK_ICON_SIZE_BUTTON)); gtk_widget_set_tooltip_text(button, "Menu"); return button; } /* ── Tab array management ─────────────────────────────────────────── */ static int tab_array_add(tab_info_t *tab) { if (g_tab_count >= g_tab_cap) { int new_cap = g_tab_cap == 0 ? 8 : g_tab_cap * 2; tab_info_t **new_arr = g_realloc(g_tabs, new_cap * sizeof(tab_info_t *)); if (new_arr == NULL) return -1; g_tabs = new_arr; g_tab_cap = new_cap; } g_tabs[g_tab_count] = tab; return g_tab_count++; } static void tab_array_remove(int index) { if (index < 0 || index >= g_tab_count) return; /* Free the tab_info_t. */ tab_info_t *tab = g_tabs[index]; if (tab) { /* The webview and widgets are destroyed by GtkNotebook when the * page is removed. We just free the struct. */ g_free(tab); } /* Shift remaining entries down. */ for (int i = index; i < g_tab_count - 1; i++) { g_tabs[i] = g_tabs[i + 1]; } g_tab_count--; } /* ── Tab creation ─────────────────────────────────────────────────── */ static tab_info_t *tab_create(const char *url) { const browser_settings_t *s = settings_get(); tab_info_t *tab = g_new0(tab_info_t, 1); if (tab == NULL) return NULL; /* Determine the URL to load. */ const char *load_url = url; char *default_url = NULL; if (load_url == NULL || load_url[0] == '\0') { load_url = s->new_tab_url; } default_url = normalize_url(load_url); if (default_url == NULL) { default_url = g_strdup(load_url); } snprintf(tab->current_url, sizeof(tab->current_url), "%s", default_url); snprintf(tab->title, sizeof(tab->title), "Loading…"); /* Create the webview from the shared context. */ tab->webview = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(g_ctx)); /* Enable developer extras + security settings (same as original main.c). */ WebKitSettings *settings = webkit_web_view_get_settings(tab->webview); 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(tab->webview); /* Build the per-tab page: toolbar on top, webview below. */ tab->page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); GtkWidget *toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); gtk_widget_set_margin_top(toolbar, 4); gtk_widget_set_margin_bottom(toolbar, 4); gtk_widget_set_margin_start(toolbar, 4); gtk_widget_set_margin_end(toolbar, 4); gtk_box_pack_start(GTK_BOX(tab->page), toolbar, FALSE, FALSE, 0); tab->hamburger = build_hamburger_menu(tab); gtk_box_pack_start(GTK_BOX(toolbar), tab->hamburger, FALSE, FALSE, 0); tab->url_entry = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(tab->url_entry), default_url); gtk_box_pack_start(GTK_BOX(toolbar), tab->url_entry, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(tab->page), GTK_WIDGET(tab->webview), TRUE, TRUE, 0); /* Build the tab label. */ tab->tab_label = build_tab_label(tab); /* Wire signals. */ g_signal_connect(tab->url_entry, "activate", G_CALLBACK(on_url_activate), tab); g_signal_connect(tab->webview, "load-changed", G_CALLBACK(on_load_changed), tab); g_signal_connect(tab->webview, "load-failed", G_CALLBACK(on_load_failed), NULL); /* Load the URL. */ webkit_web_view_load_uri(tab->webview, default_url); g_free(default_url); return tab; } /* ── New tab button ───────────────────────────────────────────────── */ static void on_new_tab_clicked(GtkButton *btn, gpointer data) { (void)btn; (void)data; tab_manager_new_tab(NULL); } /* ── Public API ───────────────────────────────────────────────────── */ void tab_manager_init(GtkContainer *parent, WebKitWebContext *ctx, GtkWindow *window) { g_ctx = ctx; g_window = window; g_notebook = gtk_notebook_new(); gtk_notebook_set_scrollable(GTK_NOTEBOOK(g_notebook), TRUE); gtk_notebook_set_show_border(GTK_NOTEBOOK(g_notebook), FALSE); /* 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); gtk_button_set_image(GTK_BUTTON(new_btn), gtk_image_new_from_icon_name("tab-new-symbolic", GTK_ICON_SIZE_BUTTON)); gtk_widget_set_tooltip_text(new_btn, "New tab (Ctrl+T)"); g_signal_connect(new_btn, "clicked", G_CALLBACK(on_new_tab_clicked), NULL); gtk_widget_show_all(new_btn); gtk_notebook_set_action_widget(GTK_NOTEBOOK(g_notebook), new_btn, GTK_PACK_END); gtk_container_add(parent, g_notebook); tab_manager_apply_settings(); } void tab_manager_apply_settings(void) { if (g_notebook == NULL) return; const browser_settings_t *s = settings_get(); gtk_notebook_set_tab_pos(GTK_NOTEBOOK(g_notebook), s->tab_bar_position); /* Apply drag reorder to all existing tabs. */ for (int i = 0; i < g_tab_count; i++) { if (g_tabs[i] && g_tabs[i]->page) { gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(g_notebook), g_tabs[i]->page, s->tab_drag_reorder); } } } int tab_manager_new_tab(const char *url) { const browser_settings_t *s = settings_get(); if (g_tab_count >= s->max_tabs) { g_print("[tabs] Maximum tab count (%d) reached\n", s->max_tabs); return -1; } tab_info_t *tab = tab_create(url); if (tab == NULL) return -1; int index = tab_array_add(tab); if (index < 0) { g_free(tab); return -1; } /* Add to notebook. */ int page_num = gtk_notebook_append_page(GTK_NOTEBOOK(g_notebook), tab->page, tab->tab_label); gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(g_notebook), tab->page, s->tab_drag_reorder); /* Switch to the new tab. */ gtk_notebook_set_current_page(GTK_NOTEBOOK(g_notebook), page_num); /* Show everything. */ gtk_widget_show_all(tab->page); gtk_widget_show_all(tab->tab_label); g_print("[tabs] Created tab %d, total: %d\n", page_num, g_tab_count); return page_num; } void tab_manager_close_tab(int index) { if (index < 0 || index >= g_tab_count) return; tab_info_t *tab = g_tabs[index]; if (tab == NULL) return; /* Remove from notebook (this destroys the page widget). */ gtk_notebook_remove_page(GTK_NOTEBOOK(g_notebook), index); /* Remove from our array. */ tab_array_remove(index); g_print("[tabs] Closed tab %d, remaining: %d\n", index, g_tab_count); /* If no tabs left, quit the app. */ if (g_tab_count == 0) { g_print("[tabs] Last tab closed, exiting.\n"); if (g_window) { gtk_window_close(g_window); } } } void tab_manager_close_active(void) { int index = tab_manager_get_active_index(); if (index >= 0) { tab_manager_close_tab(index); } } int tab_manager_get_active_index(void) { if (g_notebook == NULL) return -1; return gtk_notebook_get_current_page(GTK_NOTEBOOK(g_notebook)); } void tab_manager_switch_to(int index) { if (g_notebook == NULL) return; if (index < 0 || index >= g_tab_count) return; gtk_notebook_set_current_page(GTK_NOTEBOOK(g_notebook), index); } tab_info_t *tab_manager_get_active(void) { int index = tab_manager_get_active_index(); if (index < 0 || index >= g_tab_count) return NULL; return g_tabs[index]; } tab_info_t *tab_manager_get(int index) { if (index < 0 || index >= g_tab_count) return NULL; return g_tabs[index]; } int tab_manager_count(void) { return g_tab_count; } void tab_manager_set_title(int index, const char *title) { if (index < 0 || index >= g_tab_count) return; tab_info_t *tab = g_tabs[index]; if (tab == NULL || tab->title_label == NULL) return; snprintf(tab->title, sizeof(tab->title), "%s", (title && title[0]) ? title : "(Untitled)"); gtk_label_set_text(GTK_LABEL(tab->title_label), tab->title); } const char *tab_manager_get_url(int index) { if (index < 0 || index >= g_tab_count) return NULL; tab_info_t *tab = g_tabs[index]; if (tab == NULL) return NULL; return tab->current_url; } void tab_manager_next(void) { if (g_notebook == NULL || g_tab_count == 0) return; int cur = gtk_notebook_get_current_page(GTK_NOTEBOOK(g_notebook)); int next = (cur + 1) % g_tab_count; gtk_notebook_set_current_page(GTK_NOTEBOOK(g_notebook), next); } void tab_manager_prev(void) { if (g_notebook == NULL || g_tab_count == 0) return; int cur = gtk_notebook_get_current_page(GTK_NOTEBOOK(g_notebook)); int prev = (cur - 1 + g_tab_count) % g_tab_count; gtk_notebook_set_current_page(GTK_NOTEBOOK(g_notebook), prev); } void tab_manager_close_others(int index) { if (index < 0 || index >= g_tab_count) return; /* Close tabs to the right first (indices shift as we close). */ tab_manager_close_to_right(index); /* Close tabs to the left (close from the start so indices stay valid). */ while (index > 0) { tab_manager_close_tab(0); index--; } } void tab_manager_close_to_right(int index) { if (index < 0 || index >= g_tab_count) return; /* Close from the end so indices don't shift. */ while (g_tab_count > index + 1) { tab_manager_close_tab(g_tab_count - 1); } } void tab_manager_duplicate(int index) { if (index < 0 || index >= g_tab_count) return; tab_info_t *tab = g_tabs[index]; if (tab == NULL) return; tab_manager_new_tab(tab->current_url); } void tab_manager_reload(int index) { if (index < 0 || index >= g_tab_count) return; tab_info_t *tab = g_tabs[index]; if (tab && tab->webview) { webkit_web_view_reload_bypass_cache(tab->webview); } }