diff --git a/README.md b/README.md index cea447c..2e62249 100644 --- a/README.md +++ b/README.md @@ -2909,7 +2909,7 @@ now — they are listed so they are not mistaken for anything else: unlocked, because configuring the host is what a first run is for. Each pane's heading also carries its **own switch**, the same control as the header bar's button — clicking either moves both — because a pane that is *disabled* while its feature is off is a pane whose controls you would otherwise have to leave to turn it back on. The - switcher's policy is `wide` and each page has a title. With the math + switcher's policy is `wide` and each page has a title and no icon (the switcher's own placeholder icon is hidden). With the math list folded into the signal list under a `MATH` divider whose `+` creates a channel — a derived channel already *is* a channel in the model, so the two lists were the same list drawn twice. A tab is one-of-*N*, so no toggle hides a pane any more: cursors-off **disables** its readout and leaves it in place, and diff --git a/docs/claude/invariants-gtk-and-chrome.md b/docs/claude/invariants-gtk-and-chrome.md index 9d24b0d..c594499 100644 --- a/docs/claude/invariants-gtk-and-chrome.md +++ b/docs/claude/invariants-gtk-and-chrome.md @@ -252,7 +252,12 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear session file. An icon-only version existed (policy `narrow`, empty titles, the pills named in code by `name_side_tabs()`, because `narrow` still draws a label beside an icon); it was reversed because the glyphs were not understandable. A page added with an empty title is a blank pill, and - `switcher: … pills=N labels=N pages=N` reads it as `labels` below `pills`. + `switcher: … pills=N labels=N images=0 pages=N` reads it as `labels` below `pills`. **No icon at all** + is not what "no `icon-name`" gives you: `AdwViewSwitcherButton` always builds a `GtkImage` and falls + back to `image-missing` (the `[x]` placeholder, which also squeezed the title to "Meas..."), and no + property turns it off. `keep_tabs_icon_free()` (`src/ui_source.cpp`) hides every image in the switcher, + re-hides one that is shown again, and re-runs on the stack's `items-changed` (connected *after* the + switcher's own, so the new button exists) and on the switcher's `map`; `images=0` is the reading. - **An *overlaid* sidebar covers the plot, so a drag out of it has nowhere to land.** `collapsed` in an `AdwOverlaySplitView` means the sidebar sits *on* the plot rather than beside it: the pointer is over the list, never over a drop target, and a signal can be dragged but not dropped. `row_drag_sidebar()` hides diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 8585e82..0b58c9a 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -479,9 +479,15 @@ Two lines of the self-check output are assertions in disguise: substitutes a missing-image placeholder, the button still works, and every other line of the dump still passes. Changing an icon name is a one-line change in `build_toolbar()` and this is the only check that it names something that exists. -- `switcher: policy=wide pills=4 labels=4 pages=4 size=308x34` (in the exit dump, beside `panels:`) — the +- `switcher: policy=wide pills=4 labels=4 images=0 pages=4 size=308x34` (in the exit dump, beside `panels:`) — the sidebar tabs as the switcher actually draws them, and they are **words**: `labels` counts *mapped labels - with text*, so `pills=labels=pages` says every page has a visible title. `pages` is read from the stack + with text*, so `pills=labels=pages` says every page has a visible title. **`images` must be `0`**: it + counts *mapped* `GtkImage`s inside the switcher. `AdwViewSwitcherButton` builds an icon in every tab, and + a page with a title and no `icon-name` gets `image-missing` -- the `[x]` placeholder; the tabs hide it + (`keep_tabs_icon_free()`, which also covers pages `build_source_panels()` adds). A nonzero `images` + names a tab still drawing an icon or its placeholder (`images=pages` is the unfixed state), and + `pills`/`labels` never saw it. Read it on a source with extra pages (`--source probe` under + `UDPSCOPE_PROBE_SOURCE_KINDS`) as well as on UDPS. `pages` is read from the stack (2 for UDPS, 3 for the probe source, 4 for the E-1608 with its Analog out and Digital I/O tabs). A `labels` below `pills` is a page added with an empty title; `pills=0` means nothing is mapped, which makes the rest of the line meaningless rather than a pass. `policy=wide` is read rather than assumed. diff --git a/src/ui.cpp b/src/ui.cpp index fe5c28d..632d46e 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -3060,8 +3060,11 @@ void note_panel_controls(App& app) { // pill count reads 0 and the label count means nothing, rather than passing vacuously. int pills = 0; int drawn_labels = 0; + int drawn_images = 0; const std::function count_drawn = [&](GtkWidget* w) { if (GTK_IS_TOGGLE_BUTTON(w) && gtk_widget_get_mapped(w)) ++pills; + // A *mapped* image is an icon (or its missing-icon placeholder) on a tab: tabs are words only. + if (GTK_IS_IMAGE(w) && gtk_widget_get_mapped(w)) ++drawn_images; // A *mapped* label with text: an empty label would be counted by "mapped" alone. if (GTK_IS_LABEL(w) && gtk_widget_get_mapped(w) && gtk_label_get_text(GTK_LABEL(w))[0] != '\0') { ++drawn_labels; @@ -3074,6 +3077,7 @@ void note_panel_controls(App& app) { if (app.side_switcher != nullptr) count_drawn(app.side_switcher); app.dbg_switcher_pills = pills; app.dbg_switcher_labels = drawn_labels; + app.dbg_switcher_images = drawn_images; // Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject`. app.dbg_switcher_pages = app.side_stack != nullptr @@ -5203,7 +5207,9 @@ void on_activate(GtkApplication* application, gpointer user) { G_CALLBACK(on_side_tab_changed), app); } // The source kind's own tabs (a generator, digital I/O), added before the first tick so the - // pages-versus-pills reading below counts them too. + // pages-versus-pills reading below counts them too. The tabs are words, so the switcher's icons + // are hidden first and kept hidden for the pages this call adds. + keep_tabs_icon_free(*app); build_source_panels(*app); // The left side is the events pane *and* the plot, in one row: the pane is a column of its own @@ -6240,10 +6246,10 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit app.dbg_panel_switch_trigger, app.dbg_toolbar_switch_trigger, app.dbg_panel_switch_cursors, app.dbg_toolbar_switch_cursors, app.dbg_panel_drove_toolbar); - std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d pages=%d size=%dx%d\n", + std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d images=%d pages=%d size=%dx%d\n", app.dbg_switcher_policy == 0 ? "narrow" : (app.dbg_switcher_policy == 1 ? "wide" : "?"), - app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_pages, - app.dbg_switcher_w, app.dbg_switcher_h); + app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_images, + app.dbg_switcher_pages, app.dbg_switcher_w, app.dbg_switcher_h); // The help affordance layer: what the contextual-help toggle left on the surfaces, and where the // guided tour stood when the last capture ran. Both are captured on the tick (`capture_help_state`), // so neither asks a destroyed widget anything. diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 95852f4..414f087 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -444,6 +444,9 @@ struct App { /// are words (`wide` policy, a title per page), so the label count must equal the page count. int dbg_switcher_pills = -1; int dbg_switcher_labels = -1; + /// How many `GtkImage`s the switcher has *mapped*: a tab is its title and nothing else, so this + /// must be 0 -- an icon, or the missing-icon placeholder GTK draws for a page with no `icon-name`. + int dbg_switcher_images = -1; /// How many pages the sidebar stack holds: `pills` and `labels` must both equal it. int dbg_switcher_pages = -1; /// The switcher's own size, beside it: what the tab bar costs the column. @@ -1560,6 +1563,11 @@ void config_dialog_refresh(ConfigDialog& d); void open_configurations(App& app); void save_layout_now(App& app); +/// The sidebar's tabs are words: hide the icon every `AdwViewSwitcherButton` draws (the +/// missing-icon placeholder, for a page with a title and no `icon-name`), for the buttons that exist +/// and for any page added later. Call once, before `build_source_panels()`. +void keep_tabs_icon_free(App& app); + /// Capability panels (`src/ui_source.cpp`). `build_source_panels` adds the source kind's panels as /// pages of `side_stack`, once, before the first tick. `apply_source_panels` sets every panel's /// sensitivity from `connected` -- disabled, never hidden: the tab switcher owns visibility. diff --git a/src/ui_source.cpp b/src/ui_source.cpp index d81b723..d880485 100644 --- a/src/ui_source.cpp +++ b/src/ui_source.cpp @@ -11,6 +11,48 @@ namespace udpscope { namespace ui_impl { +namespace { + +/// Hide every `GtkImage` inside `w`, and keep it hidden. `AdwViewSwitcherButton` always builds an +/// icon -- in both of its layouts -- and a page with no `icon-name` gets `image-missing`, which is +/// the placeholder the sidebar's text tabs used to draw. There is no property or policy that turns +/// the icon off, so the image is hidden (a hidden child takes no space: the label gets the pill) and +/// a `notify::visible` handler puts it back to hidden if the button ever shows it again. +void hide_tab_icons(GtkWidget* w) { + if (GTK_IS_IMAGE(w)) { + if (g_object_get_data(G_OBJECT(w), "udpscope-icon-hidden") == nullptr) { + g_object_set_data(G_OBJECT(w), "udpscope-icon-hidden", GINT_TO_POINTER(1)); + g_signal_connect(w, "notify::visible", G_CALLBACK(+[](GObject* o, GParamSpec*, gpointer) { + GtkWidget* img = GTK_WIDGET(o); + if (gtk_widget_get_visible(img)) gtk_widget_set_visible(img, FALSE); + }), + nullptr); + } + gtk_widget_set_visible(w, FALSE); + return; + } + for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr; c = gtk_widget_get_next_sibling(c)) { + hide_tab_icons(c); + } +} + +void on_tab_pages_changed(GListModel*, guint, guint, guint, gpointer sw) { + hide_tab_icons(GTK_WIDGET(sw)); +} + +} // namespace + +void keep_tabs_icon_free(App& app) { + if (app.side_switcher == nullptr || app.side_stack == nullptr) return; + hide_tab_icons(app.side_switcher); + // A page added later gets a button when the model reports it; this handler is connected *after* + // the switcher's own, so the button already exists when it runs. + g_signal_connect_after(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)), "items-changed", + G_CALLBACK(on_tab_pages_changed), app.side_switcher); + // And once more when the switcher is (re)mapped, which also covers a rebuild of the buttons. + g_signal_connect_swapped(app.side_switcher, "map", G_CALLBACK(hide_tab_icons), app.side_switcher); +} + void build_source_panels(App& app) { if (app.side_stack == nullptr || app.acq == nullptr || app.acq->source() == nullptr) return; SourceKind* kind = find_kind(app.source_kind);