v0.0.43 - Add new-tab button and avatar to auxiliary windows: extract setup_notebook_action_widgets helper, wire new-tab button to its own notebook, sync avatar across all windows

This commit is contained in:
Laan Tungir
2026-07-17 19:01:12 -04:00
parent 12f8b45015
commit 6a8e51c812
4 changed files with 142 additions and 61 deletions
+1 -1
View File
@@ -1 +1 @@
0.0.42
0.0.43
Binary file not shown.
+139 -58
View File
@@ -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
+2 -2
View File
@@ -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 */