From da7b5eb72c9fc0c313babacadb6a4ae4fc46d406 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Thu, 30 Jul 2026 19:10:31 -0400 Subject: [PATCH] v0.0.63 - Release: toggle menu bars keyboard shortcut (Ctrl+Shift+M, per-window) --- VERSION | 2 +- plans/toggle-menu-bars.md | 177 ++++++++++++++++++++++++++++++++++++++ src/main.c | 4 + src/shortcuts.c | 5 ++ src/shortcuts.h | 1 + src/tab_manager.c | 38 ++++++++ src/tab_manager.h | 10 +++ src/version.h | 4 +- 8 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 plans/toggle-menu-bars.md diff --git a/VERSION b/VERSION index 7eb3665..8d056f1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.62 +0.0.63 diff --git a/plans/toggle-menu-bars.md b/plans/toggle-menu-bars.md new file mode 100644 index 0000000..8a90f00 --- /dev/null +++ b/plans/toggle-menu-bars.md @@ -0,0 +1,177 @@ +# Plan: Keyboard Shortcut to Toggle All Menu Bars (per-window) + +## Goal + +Add a configurable keyboard shortcut that shows/hides all the browser's +"menu bars" — the per-tab toolbars, bookmark bars, and the tab strip — +**independently per window**. Pressing the shortcut in one window hides +that window's chrome only; other windows are unaffected. This mirrors how +the agent sidebar already works (per-window `sidebar_visible` on +[`window_state_t`](src/tab_manager.c:87)). + +In sovereign_browser there is no traditional `GtkMenuBar`; each tab's chrome +is two widgets packed at the top of the tab page +([`tab->page`](src/tab_manager.c:2966)): + +1. **`#main-toolbar`** — the horizontal GtkBox holding the hamburger menu + button, refresh/stop, back, forward, URL entry, and bookmark button + (built in [`tab_create()`](src/tab_manager.c:2973)). +2. **`#bookmark-bar`** — the bookmarks toolbar below the URL toolbar + ([`tab->bookmark_bar`](src/tab_manager.c:3089)). + +Plus the tab strip itself, which is the `GtkNotebook` header, controlled +via `gtk_notebook_set_show_tabs()`. + +Hiding all three gives the webview the entire window (a focus / kiosk +mode). Pressing the shortcut again restores them. + +## Design + +### 1. New shortcut action + +Add `SHORTCUT_TOGGLE_TOOLBARS` to the enum in [`shortcuts.h`](src/shortcuts.h:29) +(before `SHORTCUT_COUNT`) and a matching entry in the `g_registry` table in +[`shortcuts.c`](src/shortcuts.c:24): + +| Field | Value | +|---------|----------------------------------------------------| +| id | `toggle_toolbars` | +| label | `Toggle menu bars` | +| desc | `Show or hide the tab strip, toolbars, and bookmark bars in the active window` | +| dflt | `m` | + +Default `m` ("m" for menu) is free and mnemonic. It is a +browser-level shortcut (not consumed by WebKit for web content), so it +reaches the window-level `on_key_press` handler reliably. + +Because the settings page enumerates actions via +[`sovereign://settings/config`](src/nostr_bridge.c:2994) (which loops over +`SHORTCUT_COUNT` calling [`shortcuts_meta()`](src/shortcuts.c:232) / +[`shortcuts_get()`](src/shortcuts.c:202)), the new action appears +automatically in the Keyboard Shortcuts section of +[`www/settings.html`](www/settings.html:74) with no JS changes. NIP-78 sync +also picks it up for free via [`shortcuts_serialize()`](src/shortcuts.c:247). + +### 2. Track toolbar widgets on each tab + +The `#main-toolbar` GtkBox is currently a local variable in +[`tab_create()`](src/tab_manager.c:2973) and is not stored on +[`tab_info_t`](src/tab_manager.h:23). Add a field: + +```c +GtkWidget *toolbar; /* #main-toolbar (hamburger + nav + URL) */ +``` + +to `tab_info_t` in [`tab_manager.h`](src/tab_manager.h:23), and assign +`tab->toolbar = toolbar;` in [`tab_create()`](src/tab_manager.c:2973). +`tab->bookmark_bar` already exists. + +### 3. Per-window visibility flag + +Add a field to [`window_state_t`](src/tab_manager.c:87) in +[`tab_manager.c`](src/tab_manager.c): + +```c +gboolean toolbars_visible; /* per-window menu-bar visibility */ +``` + +Initialize it to `TRUE` for `g_main_window` (in +[`tab_manager_init`](src/tab_manager.c:3598), near where +`sidebar_visible = FALSE` is set at line 3643) and for each aux window +created in [`tab_manager_new_window`](src/tab_manager.c:1262) (near line +1376 where `sidebar_visible = FALSE` is set). + +### 4. Toggle function (active window only) + +Add a public function in [`tab_manager.c`](src/tab_manager.c): + +```c +void tab_manager_toggle_toolbars(void); +``` + +Implementation — mirrors [`tab_manager_toggle_sidebar()`](src/tab_manager.c:4143): + +1. `window_state_t *ws = get_active_window_state();` — resolves the focused + window (main or aux), exactly like the sidebar toggle does. +2. Flip `ws->toolbars_visible`. +3. `gtk_notebook_set_show_tabs(GTK_NOTEBOOK(ws->notebook), ws->toolbars_visible);` + — hides/shows the tab strip for this window's notebook only. +4. Iterate `g_tabs[0..g_tab_count)` and, for each tab whose **page belongs + to this window's notebook** (use [`tab_find_notebook(tab->page)`](src/tab_manager.c:2660) + and compare to `ws->notebook`), call + `gtk_widget_set_visible(tab->toolbar, ws->toolbars_visible)` and + `gtk_widget_set_visible(tab->bookmark_bar, ws->toolbars_visible)`. + +This affects only the active window's tabs and tab strip; other windows +keep their own `toolbars_visible` state. + +### 5. Respect hidden state for new tabs + +In [`tab_create()`](src/tab_manager.c:2973), after building `toolbar` and +`tab->bookmark_bar`, look up the notebook the tab is being added to (the +target notebook — `g_active_notebook` / `get_effective_notebook()` at tab +creation time, or `tab_find_notebook(tab->page)` after packing) and its +`window_state_t` via [`window_state_for_notebook()`](src/tab_manager.c:4051), +then apply that window's flag: + +```c +window_state_t *ws = window_state_for_notebook(); +gboolean vis = ws ? ws->toolbars_visible : TRUE; +gtk_widget_set_visible(toolbar, vis); +gtk_widget_set_visible(tab->bookmark_bar, vis); +``` + +So a tab opened in a hidden-chrome window is also hidden, while a tab +opened in a visible-chrome window stays visible. The tab strip visibility +is per-notebook via `gtk_notebook_set_show_tabs()`, so no extra work is +needed there. + +### 6. Dispatch the shortcut + +Add a case to the switch in [`on_key_press`](src/main.c:611) in +[`main.c`](src/main.c): + +```c +case SHORTCUT_TOGGLE_TOOLBARS: + tab_manager_toggle_toolbars(); + return TRUE; +``` + +The key-press handler is window-level, so `get_active_window_state()` +inside the toggle resolves to the window that had focus when the key was +pressed — giving the per-window behavior. + +### 7. Expose in tab_manager.h + +Add the prototype for `tab_manager_toggle_toolbars(void)` near the other +toggle prototypes ([`tab_manager_toggle_inspector`](src/tab_manager.h), +[`tab_manager_toggle_sidebar`](src/tab_manager.h)). + +## Files touched + +| File | Change | +|---------------------|---------------------------------------------------------------| +| [`src/shortcuts.h`](src/shortcuts.h:29) | Add `SHORTCUT_TOGGLE_TOOLBARS` to enum | +| [`src/shortcuts.c`](src/shortcuts.c:24) | Add registry entry (id/label/desc/dflt) | +| [`src/tab_manager.h`](src/tab_manager.h:23) | Add `GtkWidget *toolbar` field + `tab_manager_toggle_toolbars` prototype | +| [`src/tab_manager.c`](src/tab_manager.c:87) | Add `toolbars_visible` to `window_state_t`; init to TRUE for main + aux windows | +| [`src/tab_manager.c`](src/tab_manager.c:2973) | Store `tab->toolbar`; apply window's `toolbars_visible` on creation; implement `tab_manager_toggle_toolbars()` (active window only: tab strip + that window's tabs' toolbars/bookmark bars) | +| [`src/main.c`](src/main.c:611) | Add `case SHORTCUT_TOGGLE_TOOLBARS` dispatch | + +No changes needed in `www/` (settings page auto-discovers the action) or in +`settings_sync.c` (NIP-78 sync already serializes all actions). + +## Verification + +1. `make` — builds clean. +2. `./browser.sh restart --login-method generate --url https://example.com` +3. Press `Ctrl+Shift+M` → in the focused window, toolbars + bookmark bar + + tab strip disappear; the webview fills the entire window. Press again → + they reappear. +4. Open a second window (`Ctrl+N`). Hide chrome in window A with + `Ctrl+Shift+M`; window B's chrome stays visible. Hide B independently. +5. With chrome hidden in a window, `Ctrl+T` opens a new tab in that window + whose toolbars are also hidden; the tab strip stays hidden too. +6. Open `sovereign://settings` → Keyboard Shortcuts section lists + "Toggle menu bars" with `Ctrl+Shift+M`; rebind it and confirm the new + binding works. diff --git a/src/main.c b/src/main.c index fc08f11..8172fb4 100644 --- a/src/main.c +++ b/src/main.c @@ -627,6 +627,10 @@ gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, tab_manager_toggle_sidebar(); return TRUE; + case SHORTCUT_TOGGLE_TOOLBARS: + tab_manager_toggle_toolbars(); + return TRUE; + case SHORTCUT_ZOOM_IN: tab_manager_zoom_in(); return TRUE; diff --git a/src/shortcuts.c b/src/shortcuts.c index 0db96a1..e54e194 100644 --- a/src/shortcuts.c +++ b/src/shortcuts.c @@ -100,6 +100,11 @@ static const shortcut_meta_t g_registry[SHORTCUT_COUNT] = { "toggle_sidebar", "Toggle agent sidebar", "Show or hide the agent chat sidebar", "a" }, + [SHORTCUT_TOGGLE_TOOLBARS] = { + "toggle_toolbars", "Toggle menu bars", + "Show or hide the tab strip, toolbars, and bookmark bars in the active window", + "m" + }, [SHORTCUT_ZOOM_IN] = { "zoom_in", "Zoom in", "Increase the zoom level of the active tab", "plus" diff --git a/src/shortcuts.h b/src/shortcuts.h index 6299b97..aa20587 100644 --- a/src/shortcuts.h +++ b/src/shortcuts.h @@ -46,6 +46,7 @@ typedef enum { SHORTCUT_TOGGLE_FULLSCREEN, SHORTCUT_TOGGLE_INSPECTOR, SHORTCUT_TOGGLE_SIDEBAR, + SHORTCUT_TOGGLE_TOOLBARS, SHORTCUT_ZOOM_IN, SHORTCUT_ZOOM_OUT, SHORTCUT_ZOOM_RESET, diff --git a/src/tab_manager.c b/src/tab_manager.c index de20b48..34ebb6d 100644 --- a/src/tab_manager.c +++ b/src/tab_manager.c @@ -91,6 +91,8 @@ typedef struct { GtkWidget *sidebar_container; /* GtkBox for sidebar webview */ WebKitWebView *sidebar_webview; /* lazily created on first toggle */ gboolean sidebar_visible; + gboolean toolbars_visible; /* per-window menu-bar (tab strip + + * toolbars + bookmark bars) visibility */ } window_state_t; /* ── Static state ─────────────────────────────────────────────────── */ @@ -1374,6 +1376,7 @@ static GtkWidget *tab_manager_new_window(const char *url, ws.sidebar_container = sidebar_container; ws.sidebar_webview = NULL; ws.sidebar_visible = FALSE; + ws.toolbars_visible = TRUE; g_array_append_val(g_aux_windows, ws); /* This new window is now the active window. */ @@ -2976,6 +2979,7 @@ static tab_info_t *tab_create(const char *url) { gtk_widget_set_margin_bottom(toolbar, 4); gtk_widget_set_margin_start(toolbar, 4); gtk_widget_set_margin_end(toolbar, 4); + tab->toolbar = toolbar; gtk_box_pack_start(GTK_BOX(tab->page), toolbar, FALSE, FALSE, 0); tab->hamburger = build_hamburger_menu(tab); @@ -3095,6 +3099,17 @@ static tab_info_t *tab_create(const char *url) { gtk_box_pack_start(GTK_BOX(tab->page), tab->bookmark_bar, FALSE, FALSE, 0); bookmark_bar_refresh(tab); + /* Apply the active window's menu-bar visibility so a tab opened in a + * hidden-chrome window is also hidden. The tab strip itself is + * per-notebook (gtk_notebook_set_show_tabs), already set by the + * toggle; here we only sync the per-tab toolbar + bookmark bar. */ + { + window_state_t *ws = get_active_window_state(); + gboolean vis = ws ? ws->toolbars_visible : TRUE; + gtk_widget_set_visible(tab->toolbar, vis); + gtk_widget_set_visible(tab->bookmark_bar, vis); + } + /* Ensure the webview expands vertically to fill the available space. * Without this, WebKitGTK may only allocate 1px of height on some * display servers, resulting in a blank page. */ @@ -3641,6 +3656,7 @@ void tab_manager_init(GtkContainer *parent, g_main_window.notebook = g_notebook; g_main_window.sidebar_webview = NULL; g_main_window.sidebar_visible = FALSE; + g_main_window.toolbars_visible = TRUE; /* The main window/notebook are the default active window/notebook * for MCP get_active_webview() and keyboard shortcuts (zoom, etc.). @@ -4164,6 +4180,28 @@ void tab_manager_toggle_sidebar(void) { } } +void tab_manager_toggle_toolbars(void) { + window_state_t *ws = get_active_window_state(); + if (ws == NULL || ws->notebook == NULL) return; + + ws->toolbars_visible = !ws->toolbars_visible; + gboolean vis = ws->toolbars_visible; + + /* Tab strip (GtkNotebook header) — per-notebook. */ + gtk_notebook_set_show_tabs(GTK_NOTEBOOK(ws->notebook), vis); + + /* Per-tab toolbar + bookmark bar, but only for tabs that belong to + * this window's notebook (aux windows have their own notebook). */ + for (int i = 0; i < g_tab_count; i++) { + tab_info_t *tab = g_tabs[i]; + if (tab == NULL || tab->page == NULL) continue; + GtkWidget *nb = tab_find_notebook(tab->page); + if (nb != ws->notebook) continue; + if (tab->toolbar) gtk_widget_set_visible(tab->toolbar, vis); + if (tab->bookmark_bar) gtk_widget_set_visible(tab->bookmark_bar, vis); + } +} + WebKitWebView *tab_manager_get_main_webview(void) { tab_info_t *tab = tab_manager_get_active(); if (tab == NULL) return NULL; diff --git a/src/tab_manager.h b/src/tab_manager.h index 31ff2d9..04d0aec 100644 --- a/src/tab_manager.h +++ b/src/tab_manager.h @@ -29,6 +29,7 @@ typedef struct { GtkWidget *title_label; /* child of tab_label */ GtkWidget *favicon; /* favicon image in tab_label */ GtkWidget *refresh_btn; /* toolbar reload/stop button (per-tab) */ + GtkWidget *toolbar; /* #main-toolbar (hamburger + nav + URL) */ GtkWidget *bookmark_bar; /* bookmarks toolbar (below URL toolbar) */ gboolean refresh_shows_stop; /* cached visual state, avoids redundant updates */ char current_url[TAB_URL_MAX]; @@ -228,6 +229,15 @@ void tab_manager_zoom_reset(void); */ void tab_manager_toggle_sidebar(void); +/* + * Toggle visibility of all "menu bars" in the active window: the tab strip + * (GtkNotebook header), each tab's #main-toolbar, and each tab's bookmark + * bar. Per-window — only the focused window is affected, mirroring + * tab_manager_toggle_sidebar(). New tabs opened in a hidden-chrome window + * inherit the hidden state. + */ +void tab_manager_toggle_toolbars(void); + /* * Returns the main webview (the web page) of the active tab, never the * sidebar webview. This is used by agent tools so they always operate diff --git a/src/version.h b/src/version.h index 086f59b..14050b2 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.62" +#define SB_VERSION "v0.0.63" #define SB_VERSION_MAJOR 0 #define SB_VERSION_MINOR 0 -#define SB_VERSION_PATCH 62 +#define SB_VERSION_PATCH 63 #endif /* SOVEREIGN_BROWSER_VERSION_H */