diff --git a/VERSION b/VERSION index 54a0022..5c49468 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.42 +0.0.43 diff --git a/sovereign_browser-0.0.42.tar.gz b/sovereign_browser-0.0.42.tar.gz new file mode 100644 index 0000000..ab7640d Binary files /dev/null and b/sovereign_browser-0.0.42.tar.gz differ diff --git a/src/tab_manager.c b/src/tab_manager.c index 1ebe726..ce02f75 100644 --- a/src/tab_manager.c +++ b/src/tab_manager.c @@ -152,6 +152,10 @@ static GtkWidget *tab_manager_new_window(const char *url, static gboolean on_notebook_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data); +static void on_new_tab_clicked(GtkButton *btn, gpointer data); +static void setup_notebook_action_widgets(GtkWidget *notebook, + gboolean is_main); +static void track_avatar_image(GtkWidget *image); static gboolean on_window_focus_in(GtkWidget *widget, GdkEventFocus *event, gpointer user_data); @@ -1180,6 +1184,13 @@ static GtkWidget *tab_manager_new_window(const char *url, gtk_notebook_set_show_border(GTK_NOTEBOOK(notebook), FALSE); gtk_notebook_set_show_tabs(GTK_NOTEBOOK(notebook), TRUE); + /* New-tab button + avatar as notebook action widgets, same as the + * main window. The new-tab button is wired to THIS notebook so tabs + * open in this window, not the main window. is_main=FALSE so a + * separate avatar image is created and tracked (the global g_avatar + * stays pointing at the main window's image). */ + setup_notebook_action_widgets(notebook, FALSE); + /* Build a window-level GtkPaned: left = sidebar container, right = * notebook. The sidebar is per-window (not per-tab), so it persists * across tab switches within this window. Hidden by default. */ @@ -2715,6 +2726,50 @@ static tab_info_t *tab_create(const char *url) { static GtkWidget *g_avatar = NULL; +/* List of all avatar GtkImage widgets across all windows. When the + * avatar picture is downloaded, every image in this list is updated so + * auxiliary windows show the same avatar as the main window. The main + * window's g_avatar is also kept in this list. */ +static GSList *g_avatar_images = NULL; + +/* Track all avatar images so they can all be updated when the picture + * arrives. Adds the image to the global list. */ +static void track_avatar_image(GtkWidget *image) { + if (image == NULL) return; + g_avatar_images = g_slist_prepend(g_avatar_images, image); + /* Sink a ref so the image stays alive for the list even if the + * button is destroyed before the list is cleaned up. The list is + * never freed during the app lifetime (avatars persist for the + * whole session), so this is a small intentional leak that avoids + * dangling pointers in the download callback. */ + g_object_ref_sink(G_OBJECT(image)); +} + +/* Update every tracked avatar image with the given pixbuf. Used by the + * avatar download idle callback so all windows' avatars stay in sync. */ +static void set_all_avatar_pixbufs(GdkPixbuf *pixbuf) { + for (GSList *l = g_avatar_images; l != NULL; l = l->next) { + GtkWidget *img = GTK_WIDGET(l->data); + if (img && GTK_IS_IMAGE(img) && pixbuf) { + /* Each image needs its own pixbuf reference. */ + GdkPixbuf *copy = g_object_ref(pixbuf); + gtk_image_set_from_pixbuf(GTK_IMAGE(img), copy); + g_object_unref(copy); + } + } +} + +/* Reset every tracked avatar image to the default icon. */ +static void set_all_avatar_icons(const char *icon_name) { + for (GSList *l = g_avatar_images; l != NULL; l = l->next) { + GtkWidget *img = GTK_WIDGET(l->data); + if (img && GTK_IS_IMAGE(img)) { + gtk_image_set_from_icon_name(GTK_IMAGE(img), icon_name, + GTK_ICON_SIZE_BUTTON); + } + } +} + /* Scale a pixbuf to fill the given size, center-cropping to preserve * aspect ratio (like CSS object-fit: cover), then round the corners * to match the button's border-radius. Returns a new pixbuf (caller @@ -2783,11 +2838,12 @@ typedef struct { int size; } avatar_fetch_t; -/* Idle callback to set the avatar pixbuf on the widget. */ +/* Idle callback to set the avatar pixbuf on every tracked avatar image + * (main window + all auxiliary windows) so they all stay in sync. */ static gboolean avatar_set_pixbuf_idle(gpointer data) { GdkPixbuf *pixbuf = (GdkPixbuf *)data; - if (g_avatar && pixbuf) { - gtk_image_set_from_pixbuf(GTK_IMAGE(g_avatar), pixbuf); + if (pixbuf) { + set_all_avatar_pixbufs(pixbuf); g_object_unref(pixbuf); } return G_SOURCE_REMOVE; @@ -2841,20 +2897,14 @@ static gpointer avatar_fetch_thread_v2(gpointer data) { * Called from main.c after login. */ void tab_manager_set_avatar(const char *pubkey_hex) { if (g_avatar == NULL || pubkey_hex == NULL || pubkey_hex[0] == '\0') { - if (g_avatar) { - gtk_image_set_from_icon_name(GTK_IMAGE(g_avatar), - "avatar-default-symbolic", - GTK_ICON_SIZE_BUTTON); - } + set_all_avatar_icons("avatar-default-symbolic"); return; } /* Query the kind 0 profile from SQLite. */ cJSON *kind0 = db_get_latest_event(pubkey_hex, 0); if (kind0 == NULL) { - gtk_image_set_from_icon_name(GTK_IMAGE(g_avatar), - "avatar-default-symbolic", - GTK_ICON_SIZE_BUTTON); + set_all_avatar_icons("avatar-default-symbolic"); return; } @@ -2873,9 +2923,7 @@ void tab_manager_set_avatar(const char *pubkey_hex) { cJSON_Delete(kind0); if (picture == NULL) { - gtk_image_set_from_icon_name(GTK_IMAGE(g_avatar), - "avatar-default-symbolic", - GTK_ICON_SIZE_BUTTON); + set_all_avatar_icons("avatar-default-symbolic"); return; } @@ -2907,8 +2955,81 @@ static void on_avatar_clicked(GtkButton *btn, gpointer data) { static void on_new_tab_clicked(GtkButton *btn, gpointer data) { (void)btn; - (void)data; - tab_manager_new_tab(NULL); + GtkWidget *nb = GTK_WIDGET(data); + /* If a specific notebook was passed (the button sits in an auxiliary + * window's tab strip), direct the new tab into that notebook by + * setting g_target_notebook for the duration of the call. Otherwise + * fall back to the active window's notebook. */ + if (nb != NULL) { + g_target_notebook = nb; + tab_manager_new_tab(NULL); + g_target_notebook = NULL; + } else { + tab_manager_new_tab(NULL); + } +} + +/* ── Notebook action-widget setup ─────────────────────────────────── * + * Adds the new-tab button (right end) and avatar button (left end) as + * GtkNotebook action widgets, plus the right-click context-menu handler. + * Used by both the main window's notebook (tab_manager_init) and each + * auxiliary window's notebook (tab_manager_new_window) so every window + * has a working new-tab button that opens tabs in THAT window. + * + * notebook: the GtkNotebook to attach the action widgets to + * is_main: TRUE for the main window — the avatar image is stored in + * the global g_avatar (so tab_manager_set_avatar can find it + * by legacy reference). For auxiliary windows, a separate + * image is created and tracked in g_avatar_images so it + * still gets updated when the picture arrives. + */ +static void setup_notebook_action_widgets(GtkWidget *notebook, gboolean is_main) { + g_return_if_fail(notebook != NULL && GTK_IS_NOTEBOOK(notebook)); + + /* Right-click / middle-click context menu on the tab strip. */ + gtk_widget_add_events(notebook, GDK_BUTTON_PRESS_MASK); + g_signal_connect(notebook, "button-press-event", + G_CALLBACK(on_notebook_button_press), NULL); + + /* New-tab button at the end of the tab strip. Pass the notebook as + * user_data so the button opens the tab in THIS notebook, not just + * whatever window happens to be active. */ + 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), notebook); + gtk_widget_show_all(new_btn); + gtk_notebook_set_action_widget(GTK_NOTEBOOK(notebook), new_btn, + GTK_PACK_END); + + /* User avatar at the start (far left) of the tab strip. The main + * window's image is stored in g_avatar for backward compatibility; + * auxiliary windows get their own image, tracked in g_avatar_images + * so all windows' avatars update together. */ + GtkWidget *avatar_img = gtk_image_new_from_icon_name( + "avatar-default-symbolic", GTK_ICON_SIZE_BUTTON); + if (is_main) { + g_avatar = avatar_img; + } + track_avatar_image(avatar_img); + + GtkWidget *avatar_btn = gtk_button_new(); + gtk_button_set_relief(GTK_BUTTON(avatar_btn), GTK_RELIEF_NORMAL); + gtk_button_set_image(GTK_BUTTON(avatar_btn), avatar_img); + gtk_widget_set_tooltip_text(avatar_btn, "Your profile"); + gtk_widget_set_valign(avatar_btn, GTK_ALIGN_CENTER); + gtk_widget_set_margin_start(avatar_btn, 4); + gtk_widget_set_name(avatar_btn, "avatar-btn"); + gtk_widget_set_size_request(avatar_btn, 28, 28); /* fixed square, matches hamburger */ + g_signal_connect(avatar_btn, "clicked", + G_CALLBACK(on_avatar_clicked), NULL); + gtk_widget_show_all(avatar_btn); + gtk_notebook_set_action_widget(GTK_NOTEBOOK(notebook), avatar_btn, + GTK_PACK_START); } /* ── Public API ───────────────────────────────────────────────────── */ @@ -2926,48 +3047,8 @@ 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); - 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); - - /* User avatar as an action widget at the start (far left) of the - * tab strip. Uses a GtkButton with GTK_RELIEF_NORMAL to match the - * hamburger menu button's visible border. margin_start matches the - * toolbar's left margin (4px) so the avatar's left edge aligns - * horizontally with the hamburger button's left edge. - * The inner GtkImage (g_avatar) is updated via tab_manager_set_avatar(). */ - g_avatar = gtk_image_new_from_icon_name("avatar-default-symbolic", - GTK_ICON_SIZE_BUTTON); - GtkWidget *avatar_btn = gtk_button_new(); - gtk_button_set_relief(GTK_BUTTON(avatar_btn), GTK_RELIEF_NORMAL); - gtk_button_set_image(GTK_BUTTON(avatar_btn), g_avatar); - gtk_widget_set_tooltip_text(avatar_btn, "Your profile"); - gtk_widget_set_valign(avatar_btn, GTK_ALIGN_CENTER); - gtk_widget_set_margin_start(avatar_btn, 4); - gtk_widget_set_name(avatar_btn, "avatar-btn"); - gtk_widget_set_size_request(avatar_btn, 28, 28); /* fixed square, matches hamburger */ - g_signal_connect(avatar_btn, "clicked", - G_CALLBACK(on_avatar_clicked), NULL); - gtk_widget_show_all(avatar_btn); - gtk_notebook_set_action_widget(GTK_NOTEBOOK(g_notebook), avatar_btn, - GTK_PACK_START); + /* New-tab button + avatar as notebook action widgets. */ + setup_notebook_action_widgets(g_notebook, TRUE); /* CSS: remove internal padding from both buttons so their contents * fill edge-to-edge. Force both to a fixed square size (28x28) and diff --git a/src/version.h b/src/version.h index 4906195..1f71bd9 100644 --- a/src/version.h +++ b/src/version.h @@ -11,9 +11,9 @@ #ifndef SOVEREIGN_BROWSER_VERSION_H #define SOVEREIGN_BROWSER_VERSION_H -#define SB_VERSION "v0.0.42" +#define SB_VERSION "v0.0.43" #define SB_VERSION_MAJOR 0 #define SB_VERSION_MINOR 0 -#define SB_VERSION_PATCH 42 +#define SB_VERSION_PATCH 43 #endif /* SOVEREIGN_BROWSER_VERSION_H */