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:
Binary file not shown.
+139
-58
@@ -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
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user