Merge feature/text-tabs into feature/source-plugins

This commit is contained in:
Martino Ferrari committed 2026-11-02 10:03:28 +01:00
commit eb4e59ac50
15 files changed
+65 -291

No files matched your search

+1 -1
View File
@@ -61,4 +61,4 @@ The detail lives in `docs/claude/`. Read the files that touch the area you are e
| [invariants-measurements.md](docs/claude/invariants-measurements.md) | Why the pane is refreshed on a cadence, why neither pass may read its window raw, how a crossing is counted with hysteresis, and which mean over a split is which. | | [invariants-measurements.md](docs/claude/invariants-measurements.md) | Why the pane is refreshed on a cadence, why neither pass may read its window raw, how a crossing is counted with hysteresis, and which mean over a split is which. |
| [invariants-math-channels.md](docs/claude/invariants-math-channels.md) | A math channel *is* a channel; the engine runs on the producer thread, so a regression there costs packets and not frames; the node editor's own rules. | | [invariants-math-channels.md](docs/claude/invariants-math-channels.md) | A math channel *is* a channel; the engine runs on the producer thread, so a regression there costs packets and not frames; the node editor's own rules. |
| [invariants-session-and-export.md](docs/claude/invariants-session-and-export.md) | What a session file persists and in what order, when a save is refused, and when an export is collected. | | [invariants-session-and-export.md](docs/claude/invariants-session-and-export.md) | What a session file persists and in what order, when a save is refused, and when an export is collected. |
| [invariants-gtk-and-chrome.md](docs/claude/invariants-gtk-and-chrome.md) | The GTK signal-signature and dialog-lifetime traps this app has already fallen into, the status bar's two tiers, the six bundled icons and their authoring rules, and how the Mallard manual is opened. | | [invariants-gtk-and-chrome.md](docs/claude/invariants-gtk-and-chrome.md) | The GTK signal-signature and dialog-lifetime traps this app has already fallen into, the status bar's two tiers, the bundled icons and their authoring rules, and how the Mallard manual is opened. |
+4 -8
View File
@@ -2886,11 +2886,9 @@ now — they are listed so they are not mistaken for anything else:
signal list — in one scrolling column; with four of them open in a 320 px sidebar every signal list — in one scrolling column; with four of them open in a 320 px sidebar every
section was short of room, and finding one meant scrolling past the others. The built section was short of room, and finding one meant scrolling past the others. The built
sidebar is a **Measure** tab (the cursors readout and the measurements) and a **Signals** sidebar is a **Measure** tab (the cursors readout and the measurements) and a **Signals**
tab (the trigger and the channels), **each a pill with its own glyph and no text** — the tab (the trigger and the channels), **each a text tab** (the sidebar was icon-only for a while; glyphs for "cursors and ruler" and
cursors and the ruler for Measure, a sine crossing its trigger level and the channel list "trigger and channel list" were not understandable enough, so the words won). A source kind's own tabs
for Signals (the same picture the toolbar's trigger button carries), drawn as a (the E-1608's **Analog out** and **Digital I/O**) sit beside them as text too. Both panes also have a **locked** mode, which is what a returning user gets: the
32x16 pair so both motifs are legible at a glance — with the words kept as each pill's tooltip and
accessible name. Both panes also have a **locked** mode, which is what a returning user gets: the
trigger pane shows one line saying what it is set to, the signal list shows only the signals that are trigger pane shows one line saying what it is set to, the signal list shows only the signals that are
on, and neither offers a control that changes the configuration — each heading has an `[edit]` button on, and neither offers a control that changes the configuration — each heading has an `[edit]` button
to switch to the full pane, and back. Dragging a signal onto a plot still works while locked — filling to switch to the full pane, and back. Dragging a signal onto a plot still works while locked — filling
@@ -2901,9 +2899,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 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 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 feature is off is a pane whose controls you would otherwise have to leave to turn it back on. The
design spec had the switcher showing *titles* and neither page an switcher's policy is `wide` and each page has a title. With the math
`icon-name`; `narrow` alone does not do it (libadwaita still draws a label beside the icon,
measured), so the pages carry an empty title and the pills are named in code. 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 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 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 toggle hides a pane any more: cursors-off **disables** its readout and leaves it in place, and
+15 -36
View File
@@ -51,38 +51,20 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
how the first version of that line earned them. What stays in the menu is what acts on the *data* how the first version of that line earned them. What stays in the menu is what acts on the *data*
(Fit, Export, Configurations) and the one editor that is not a pane (the layout editor): the signal list (Fit, Export, Configurations) and the one editor that is not a pane (the layout editor): the signal list
is a *tab* of the sidebar, so there is nothing left to reveal or hide about it. The Arm/Disarm button in the trigger panel still has text. is a *tab* of the sidebar, so there is nothing left to reveal or hide about it. The Arm/Disarm button in the trigger panel still has text.
- **Seven of this app's icons are its own, and they are a `GResource` compiled into the - **Five of this app's icons are its own, and they are a `GResource` compiled into the
binary.** No icon theme has a glyph for an oscilloscope trigger, a pair of cursors or a binary.** No icon theme has a glyph for an oscilloscope trigger, a pair of cursors or a
measurement ruler, and the stock names those buttons used to carry said something else measurement ruler, and the stock names those buttons used to carry said something else
entirely: a *record dot* for the trigger and three different **magnifiers** for cursors, entirely: a *record dot* for the trigger and three different **magnifiers** for cursors,
measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,events,resize-rows,tab-measure,tab-signals}-symbolic.svg` measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,events,resize-rows}-symbolic.svg`
are the seven action icons. `udpscope-math-symbolic` left with the toolbar toggle that was its last are the five action icons. `udpscope-math-symbolic` left with the toolbar toggle that was its last
user; `udpscope-measure-symbolic` **stayed**, because the Preferences window's Plot page carries it -- user; `udpscope-measure-symbolic` **stayed**, because the Preferences window's Plot page carries it --
an icon with a live user is not an orphan, whatever the toggle that introduced it did -- and it is an icon with a live user is not an orphan, whatever the toggle that introduced it did -- and it is
the Preferences window's icon, not the tab's. the Preferences window's icon.
The two **tabs** got glyphs of their own, and each is a *pair* rather than one of the icons above. The sidebar's **tabs have no icons any more**: two glyphs (`udpscope-tab-measure-symbolic`,
`udpscope-tab-measure-symbolic` is two cursors beside the ruler they read against — each a cap over two `udpscope-tab-signals-symbolic`, each a 32x16 pair) were drawn for them and judged not understandable
bold dashes, beside a ruler of two bands with three marks between them and **no end caps** (a closed enough, so the tabs are text and both SVGs were deleted. The *trigger's* own icon was redrawn to the
frame reads as a rectangle, and costs a fifth of the ink on two lines that say nothing) — and Signals glyph's picture at that time and kept it.
`udpscope-tab-signals-symbolic` is a sine crossing its dashed level, beside two bold channel rows. They
are not reuses, because a glyph the toolbar already carries means two things the moment a tab shows it
too; instead the *trigger's* own icon (`udpscope-trigger-symbolic`) was redrawn to the Signals pill's
picture, so the pill and the button that turns the trigger on are recognisably about the same thing.
The wave is a **full period and not a hump**: a crest and a trough are what make it read as a wave
rather than as a hill, and the level's dashes survive between the crossings. (A hump was tried and
reverted — it named the "above the level" half of a trigger and lost the crossings, which is the half
that matters.)
Both are **32x16** composites drawn **bold** — one thick band, two thick dashes, few features — because
at this size a thin wave or a 1px tick reads as *texture* rather than as a shape, which is what "not
clear" looks like. Wider than tall on purpose: a 16px square cell carries one motif, not a pair. And
**GDK paints an icon into a square box whatever the document's aspect** (measured:
`gtk_icon_theme_lookup_icon()` at 32 reports `32x32`, and librsvg fits it to `32x16`), so the pair is
letterboxed rather than stretched — and the pill's image is given `pixel_size` 32 in `name_side_tabs()`,
which draws the artwork at the size it is written, so its bars stay crisp. Every glyph here is
**inset two pixels**, because art flush with the cell edge is what a button's padding or a crop takes a
pixel off, and a fractional scale is what would blur it, which is why nothing is drawn at 24 or 48. The
two tab glyphs measure 29% and 30% ink, inside the range the icons above them set.
- **A shortcut table row's `accel` is GTK accelerator syntax, and a pointer gesture has - **A shortcut table row's `accel` is GTK accelerator syntax, and a pointer gesture has
none.** `src/shortcuts.cpp`'s strings are handed to `AdwShortcutsItem`, which parses them none.** `src/shortcuts.cpp`'s strings are handed to `AdwShortcutsItem`, which parses them
with `gtk_accelerator_parse()` — so they are `"space"`, `"<Control>e"`, `"<Control>slash"`, with `gtk_accelerator_parse()` — so they are `"space"`, `"<Control>e"`, `"<Control>slash"`,
@@ -264,16 +246,13 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
allocation read in the sidebar is a debug capture, and each reads 0 on the page that is *not* allocation read in the sidebar is a debug capture, and each reads 0 on the page that is *not*
current -- a change in what the reading means, not a defect, and no functional code may depend on current -- a change in what the reading means, not a defect, and no functional code may depend on
those numbers. those numbers.
- **`AdwViewSwitcherPolicy.narrow` still draws a label; an *empty title* is what makes a pill an icon.** - **The sidebar's tabs are text: `AdwViewSwitcherPolicy.wide`, a title on every page, no icon.** The
Measured on libadwaita 1.10: with `narrow` and a title, the pill holds a `GtkImage` **and** a built-in pages get their titles in the `.ui` and a source kind's pages from `SourcePanel::title`
`GtkLabel` — 48px of "Measure" inside a 54px pill — so the policy alone does not give an icon-only tab (`adw_view_stack_add_titled`); the page's *name* still identifies it to `select_side_tab()` and the
bar. The sidebar's pages therefore have **no title** (their `name` is what identifies them to session file. An icon-only version existed (policy `narrow`, empty titles, the pills named in code by
`select_side_tab()` and the session file) and the pills are *named in code* instead: the first tick `name_side_tabs()`, because `narrow` still draws a label beside an icon); it was reversed because the
calls `name_side_tabs()`, which walks the switcher's children — the pills are there in page order, and glyphs were not understandable. A page added with an empty title is a blank pill, and
`AdwViewSwitcherButton` is not a public type, so there is no type check to make — and sets each pill's `switcher: … pills=N labels=N pages=N` reads it as `labels` below `pills`.
**tooltip and accessible name** from the page's name, then hides the empty label that would otherwise
leave the icon off-centre. An icon-only pill is still a named control for a screen reader, and
`switcher: … pills=2 labels=0 names=2` is the reading for all three facts at once.
- **An *overlaid* sidebar covers the plot, so a drag out of it has nowhere to land.** `collapsed` in an - **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 `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 list, never over a drop target, and a signal can be dragged but not dropped. `row_drag_sidebar()` hides
+8 -21
View File
@@ -479,27 +479,14 @@ 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 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 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. is the only check that it names something that exists.
- `switcher: policy=narrow pills=2 labels=0 names=2 icon=32x32 size=308x36` (in the exit dump, beside - `switcher: policy=wide pills=4 labels=4 pages=4 size=308x34` (in the exit dump, beside `panels:`) — the
`panels:`) — the two sidebar tabs as the switcher actually draws them, and they are **words**: `labels` counts *mapped labels
sidebar tabs as the switcher actually draws them. **`pills=2 labels=0`** is the claim: two pills are on with text*, so `pills=labels=pages` says every page has a visible title. `pages` is read from the stack
screen and **no text is drawn** (`labels` counts *mapped labels with text*, because `narrow` keeps an (2 for UDPS, 3 for the probe source, 4 for the E-1608 with its Analog out and Digital I/O tabs). A
empty label in the pill's layout and a plain mapped count would read 2 either way). `names=2` is the `labels` below `pills` is a page added with an empty title; `pills=0` means nothing is mapped, which
other half — both pills carry a tooltip and an accessible name set by `name_side_tabs()`, so an makes the rest of the line meaningless rather than a pass. `policy=wide` is read rather than assumed.
icon-only pill is not an anonymous button — and `pills=0` means nothing is mapped, which makes the `size=` is the bar's own size: a text tab costs about 34 px of height, and a width of 308 shows the
rest of the line meaningless rather than a pass. `policy=narrow` is read rather than assumed. `icon=` four tabs fit the 320 px column.
is the box the pills' `GtkImage` is *rendered* in, which proves the 32px override stuck: the tab
glyphs are 32x16 documents, and GDK paints an icon into a square box whatever the document's aspect,
so the artwork is drawn 32x16 at 1:1 inside that 32x32 box — letterboxed, not stretched, which is what
keeps its 2px bars crisp. It reads `0x0` on the first tick, where nothing has been allocated yet, and
is therefore read by the exit probe with the rest of the sidebar's geometry. `size=` is the bar's own
size, and it is the answer to "what did the wider glyphs cost": `36` is what it was with 16px icons,
because the pill's fixed height takes a 32px box without growing — the glyph gets twice the width for
no vertical room at all.
- `page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)` (in the exit
dump, beside `toolbar icons:`) — each tab's own glyph and `gtk_icon_theme_has_icon()`'s verdict, read
on the first tick for the same reason the toolbar's are. **Both must read `(ok)`**: the page titles are
empty on purpose, so an icon that does not resolve leaves a *blank* pill, and a blank pill looks
exactly like a page with nothing in it.
- `help layer: contextual=on hints=12 ids={events plot status cursors measures trigger trigger_advanced events_record trigger_search signals signals_config window} icon=dialog-information-symbolic(ok) hint=12x12; tour=idle steps=5 step=0 auto=1 requested=0` (in the exit - `help layer: contextual=on hints=12 ids={events plot status cursors measures trigger trigger_advanced events_record trigger_search signals signals_config window} icon=dialog-information-symbolic(ok) hint=12x12; tour=idle steps=5 step=0 auto=1 requested=0` (in the exit
dump, beside `switcher:`) — the help affordance layer as the last tick found it. **`hints=` must equal dump, beside `switcher:`) — the help affordance layer as the last tick found it. **`hints=` must equal
the number of ids**, and the ids are the surfaces the contextual help is on: eleven of the window the number of ids**, and the ids are the surfaces the contextual help is on: eleven of the window
+2 -2
View File
@@ -17,8 +17,8 @@ Two baselines every run is read against. The UDPS runs must be **unchanged by th
a UDPS self-check, number-normalised (`sed 's/[0-9.]\+/N/g'`), diffs empty against one taken before it. a UDPS self-check, number-normalised (`sed 's/[0-9.]\+/N/g'`), diffs empty against one taken before it.
A clean build emits **three** warnings and no more: two A clean build emits **three** warnings and no more: two
`-Wmissing-field-initializers` on `MathNode::source` and one `-Woverlength-strings` on the icons' `-Wmissing-field-initializers` on `MathNode::source` and one `-Woverlength-strings` on the icons'
generated GResource `.c`, whose blob is **12352** against C99's 4095-byte string-literal limit with generated GResource `.c`, whose blob is **9248** against C99's 4095-byte string-literal limit with
the seven action icons and the two app icons that remain (a note about a generated file, which GCC the five action icons and the two app icons that remain (a note about a generated file, which GCC
itself supports). The *fourth* was the help pages' generated `.c` (**516044** bytes); the manual is itself supports). The *fourth* was the help pages' generated `.c` (**516044** bytes); the manual is
now a Mallard set installed for Yelp rather than a GResource compiled into the binary, so that now a Mallard set installed for Yelp rather than a GResource compiled into the binary, so that
bundle — and its warning — is gone, and the baseline moved from four to **three**. Re-measure by bundle — and its warning — is gone, and the baseline moved from four to **three**. Re-measure by
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!-- The Measure tab: two cursors beside the ruler they read against. Each cursor is a cap over two
bold dashes — the mark the toolbar's cursors button carries, at a weight that survives this size.
The ruler is two bands with three marks between them and *no end caps*: a closed frame reads as a
rectangle, and it costs a fifth of the ink on two lines that say nothing. Both motifs are centred
on the same line and share one weight, and everything is inset two pixels. -->
<svg height="16px" width="32px" viewBox="0 0 32 16" xmlns="http://www.w3.org/2000/svg">
<path fill="#2e3436" d="M2 2 H7 V5 H2 Z
M3 6 H6 V10 H3 Z
M3 11 H6 V14 H3 Z
M9 2 H14 V5 H9 Z
M10 6 H13 V10 H10 Z
M10 11 H13 V14 H10 Z
M17 4 H30 V6 H17 Z
M17 10 H30 V12 H17 Z
M18 6 H20 V10 H18 Z
M22 6 H24 V10 H22 Z
M26 6 H28 V10 H26 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!-- The Signals tab: the toolbar's trigger picture — a sine crossing its dashed level — beside the
channel list. Sharing that picture is the point: the pill and the button that turns the trigger on
should be recognisably about the same thing. A full period and not a hump: the crest and the trough
are what make it read as a wave rather than as a hill, and the level's dashes survive between the
crossings. Two channel rows on the right, drawn bold like the wave, and everything inset two
pixels, because art flush with the cell edge is what a button or a crop clips. -->
<svg height="16px" width="32px" viewBox="0 0 32 16" xmlns="http://www.w3.org/2000/svg">
<path fill="#2e3436" d="M2 6 H3 V10 H2 Z
M3 4 H4 V8 H3 Z
M4 3 H5 V7 H4 Z
M5 2 H6 V6 H5 Z
M6 3 H7 V7 H6 Z
M7 4 H8 V8 H7 Z
M8 6 H9 V10 H8 Z
M9 8 H10 V12 H9 Z
M10 9 H11 V13 H10 Z
M11 10 H12 V14 H11 Z
M12 9 H13 V13 H12 Z
M13 8 H14 V12 H13 Z
M14 6 H15 V10 H14 Z
M2 7 H5 V9 H2 Z
M7 7 H10 V9 H7 Z
M12 7 H15 V9 H12 Z
M18 3 H23 V8 H18 Z
M24 4 H30 V7 H24 Z
M18 10 H23 V15 H18 Z
M24 11 H30 V14 H24 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-2
View File
@@ -20,8 +20,6 @@
<file preprocess="xml-stripblanks">scalable/actions/udpscope-trigger-symbolic.svg</file> <file preprocess="xml-stripblanks">scalable/actions/udpscope-trigger-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-cursors-symbolic.svg</file> <file preprocess="xml-stripblanks">scalable/actions/udpscope-cursors-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-measure-symbolic.svg</file> <file preprocess="xml-stripblanks">scalable/actions/udpscope-measure-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-tab-measure-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-tab-signals-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-events-symbolic.svg</file> <file preprocess="xml-stripblanks">scalable/actions/udpscope-events-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-resize-rows-symbolic.svg</file> <file preprocess="xml-stripblanks">scalable/actions/udpscope-resize-rows-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app.svg</file> <file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app.svg</file>
-2
View File
@@ -332,12 +332,10 @@ std::vector<SourcePanel> make_e1608_panels(DataSource& source) {
SourcePanel ao; SourcePanel ao;
ao.id = "ao"; ao.id = "ao";
ao.title = "Analog out"; ao.title = "Analog out";
ao.icon = "audio-volume-high-symbolic";
ao.widget = build_ao(src); ao.widget = build_ao(src);
SourcePanel dio; SourcePanel dio;
dio.id = "dio"; dio.id = "dio";
dio.title = "Digital I/O"; dio.title = "Digital I/O";
dio.icon = "input-dialpad-symbolic";
dio.widget = build_dio(src); dio.widget = build_dio(src);
return {ao, dio}; return {ao, dio};
} }
-1
View File
@@ -38,7 +38,6 @@ struct ConfigForm {
struct SourcePanel { struct SourcePanel {
std::string id; ///< stack page name std::string id; ///< stack page name
std::string title; std::string title;
std::string icon; ///< icon name for the tab, may be empty
GtkWidget* widget = nullptr; GtkWidget* widget = nullptr;
}; };
+18 -108
View File
@@ -2975,64 +2975,6 @@ std::string icon_report(GtkIconTheme* theme, const char* name, const char* resou
/// and Reset is inside the cursors pane. Recorded on the first tick -- the widgets exist and the /// and Reset is inside the cursors pane. Recorded on the first tick -- the widgets exist and the
/// window is alive, neither of which is true when the dump runs -- so the dump can print a fact /// window is alive, neither of which is true when the dump runs -- so the dump can print a fact
/// instead of asking GTK about a destroyed widget. /// instead of asking GTK about a destroyed widget.
/// The words the pills do not draw. The pages' `title` is empty on purpose (see the `.ui`), so the
/// human name of a tab lives here and is put on the pill — its tooltip and its accessible name — by
/// `name_side_tabs()`. That keeps an icon-only pill a *named* control for a screen reader.
static const char* side_tab_title(const App& app, const char* name) {
if (name != nullptr && std::strcmp(name, "measure") == 0) return "Measure";
if (name != nullptr && std::strcmp(name, "signals") == 0) return "Signals";
if (name != nullptr) {
for (const SourcePanelPage& p : app.source_panels) {
if (p.name == name) return p.title.c_str();
}
}
return "";
}
/// Names the switcher's pills and takes their empty labels out of the layout: an empty `GtkLabel`
/// still asks for its own width, which leaves the icon off-centre in a pill meant to be one glyph.
/// Called from the first tick, because the pills are `AdwViewSwitcher`'s own and exist only once the
/// widget has been mapped.
void name_side_tabs(App& app) {
if (app.side_tabs_named || app.side_switcher == nullptr) return;
GListModel* pages = app.side_stack != nullptr
? G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))
: nullptr;
// The switcher's children *are* its pills, in page order, and `AdwViewSwitcherButton` is not a
// public type (there is no `ADW_IS_VIEW_SWITCHER_BUTTON`), so the two lists are walked together by
// index rather than checked by type.
GtkWidget* pill = gtk_widget_get_first_child(app.side_switcher);
for (guint i = 0; pages != nullptr && pill != nullptr && i < g_list_model_get_n_items(pages);
pill = gtk_widget_get_next_sibling(pill), ++i) {
AdwViewStackPage* page = ADW_VIEW_STACK_PAGE(g_list_model_get_item(pages, i));
const char* title = side_tab_title(app, adw_view_stack_page_get_name(page));
g_object_unref(page);
if (title[0] == '\0') continue;
gtk_widget_set_tooltip_text(pill, title);
gtk_accessible_update_property(GTK_ACCESSIBLE(pill), GTK_ACCESSIBLE_PROPERTY_LABEL, title, -1);
const std::function<void(GtkWidget*)> hide_labels = [&](GtkWidget* w) {
if (GTK_IS_LABEL(w)) gtk_widget_set_visible(w, FALSE);
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
hide_labels(c);
}
};
hide_labels(pill);
// The tabs' glyphs are 32x16 documents, and the pill has the width for them: drawn at 32 they are
// 1:1, so both motifs sit side by side at the size they were authored at and the 2px bars stay
// crisp. A 16px box would scale the pair down to a smear, and a fractional scale would blur it.
const std::function<void(GtkWidget*)> size_images = [&](GtkWidget* w) {
if (GTK_IS_IMAGE(w)) gtk_image_set_pixel_size(GTK_IMAGE(w), 32);
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
size_images(c);
}
};
size_images(pill);
app.side_tabs_named = true;
}
}
void note_panel_controls(App& app) { void note_panel_controls(App& app) {
if (!app.dbg_panel_controls.empty()) return; if (!app.dbg_panel_controls.empty()) return;
const auto in_bar = [](GtkWidget* w) { const auto in_bar = [](GtkWidget* w) {
@@ -3110,39 +3052,17 @@ void note_panel_controls(App& app) {
icon_report(theme, "org.udpscope.app-symbolic", icon_report(theme, "org.udpscope.app-symbolic",
"/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg"); "/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg");
// The two sidebar pages' own icons, named and resolved the same way, and what the switcher is // What the switcher is drawing: the sidebar's tabs are words (`wide` policy, a title per page, no
// drawing for them. The pills are `AdwViewSwitcher`'s own buttons, built when the stack is mapped, // icon). The pills are `AdwViewSwitcher`'s own buttons, built when the stack is mapped, so this
// so this reads on the first tick: `pills` is the guard (0 pills would make a label count of 0 // reads on the first tick. `mapped`, not `visible`: visibility is per widget, so a label whose
// meaningless) and `labels` must be 0, because the switcher's policy is `narrow` -- the pages are // *parent* is hidden still reads visible; `mapped` requires every ancestor, which is what "on
// icons, not words, in a 300px column. // screen" means -- and it is why the pill count is read the same way: if nothing is mapped the
// Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject` // pill count reads 0 and the label count means nothing, rather than passing vacuously.
// (only its `child` is a widget), so `gtk_widget_get_first_child()` on the stack yields the pages'
// scrolled windows and this line read empty the first time it was written.
GListModel* pages = app.side_stack != nullptr
? G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))
: nullptr;
for (guint i = 0; pages != nullptr && i < g_list_model_get_n_items(pages); ++i) {
AdwViewStackPage* page = ADW_VIEW_STACK_PAGE(g_list_model_get_item(pages, i));
const char* page_name = adw_view_stack_page_get_name(page);
const char* icon = adw_view_stack_page_get_icon_name(page);
app.dbg_page_icons += std::string(app.dbg_page_icons.empty() ? "" : " ") +
(page_name != nullptr ? page_name : "?") + "=" +
(icon != nullptr ? icon : "-") +
(icon != nullptr && gtk_icon_theme_has_icon(theme, icon) ? "(ok)"
: "(MISSING)");
g_object_unref(page);
}
// `mapped`, not `visible`: visibility is per widget, so a label whose *parent* is hidden still
// reads visible, and in the narrow policy that is exactly the case. `mapped` requires every
// ancestor, which is what "on screen" means -- and it is why the pill count is read the same way:
// if nothing is mapped the pill count reads 0 and the label count means nothing, rather than
// passing vacuously.
int pills = 0; int pills = 0;
int drawn_labels = 0; int drawn_labels = 0;
const std::function<void(GtkWidget*)> count_drawn = [&](GtkWidget* w) { const std::function<void(GtkWidget*)> count_drawn = [&](GtkWidget* w) {
if (GTK_IS_TOGGLE_BUTTON(w) && gtk_widget_get_mapped(w)) ++pills; if (GTK_IS_TOGGLE_BUTTON(w) && gtk_widget_get_mapped(w)) ++pills;
// A *mapped* label with text: libadwaita's narrow pill keeps an empty label in the layout, so // A *mapped* label with text: an empty label would be counted by "mapped" alone.
// "mapped" alone would count the empty ones and read 2 whether the pills were icons or words.
if (GTK_IS_LABEL(w) && gtk_widget_get_mapped(w) && gtk_label_get_text(GTK_LABEL(w))[0] != '\0') { if (GTK_IS_LABEL(w) && gtk_widget_get_mapped(w) && gtk_label_get_text(GTK_LABEL(w))[0] != '\0') {
++drawn_labels; ++drawn_labels;
} }
@@ -3154,14 +3074,12 @@ void note_panel_controls(App& app) {
if (app.side_switcher != nullptr) count_drawn(app.side_switcher); if (app.side_switcher != nullptr) count_drawn(app.side_switcher);
app.dbg_switcher_pills = pills; app.dbg_switcher_pills = pills;
app.dbg_switcher_labels = drawn_labels; app.dbg_switcher_labels = drawn_labels;
int named_pills = 0; // Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject`.
for (GtkWidget* w = app.side_switcher != nullptr ? gtk_widget_get_first_child(app.side_switcher) app.dbg_switcher_pages =
: nullptr; app.side_stack != nullptr
w != nullptr; w = gtk_widget_get_next_sibling(w)) { ? static_cast<int>(g_list_model_get_n_items(
const char* tip = gtk_widget_get_tooltip_text(w); G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))))
if (tip != nullptr && tip[0] != '\0') ++named_pills; : 0;
}
app.dbg_switcher_names = named_pills;
if (app.side_switcher != nullptr) { if (app.side_switcher != nullptr) {
g_object_get(G_OBJECT(app.side_switcher), "policy", &app.dbg_switcher_policy, nullptr); g_object_get(G_OBJECT(app.side_switcher), "policy", &app.dbg_switcher_policy, nullptr);
} }
@@ -3456,11 +3374,6 @@ void apply_density(App& app, DisplayMode display, bool live_display) {
gboolean tick(gpointer user) { gboolean tick(gpointer user) {
App* app = static_cast<App*>(user); App* app = static_cast<App*>(user);
if (app->window == nullptr) return G_SOURCE_REMOVE; if (app->window == nullptr) return G_SOURCE_REMOVE;
// Before the reading, and not after it: the pills are `AdwViewSwitcher`'s own widgets and exist
// only once the switcher is mapped, so this is the first frame that can name them -- and their
// page titles are deliberately empty, so a frame that read before this ran would read pills with
// no text and no name either.
name_side_tabs(*app);
note_panel_controls(*app); note_panel_controls(*app);
sync_source_panels(*app); sync_source_panels(*app);
// The tour, re-measured every frame: the target's allocation is 0 on the frame it is first shown // The tour, re-measured every frame: the target's allocation is 0 on the frame it is first shown
@@ -5289,8 +5202,8 @@ void on_activate(GtkApplication* application, gpointer user) {
g_signal_connect(app->side_stack, "notify::visible-child-name", g_signal_connect(app->side_stack, "notify::visible-child-name",
G_CALLBACK(on_side_tab_changed), app); G_CALLBACK(on_side_tab_changed), app);
} }
// The source kind's own tabs (a generator, digital I/O). Before the first tick: that is when // The source kind's own tabs (a generator, digital I/O), added before the first tick so the
// `name_side_tabs()` puts the words on the pills, and it must see these pages too. // pages-versus-pills reading below counts them too.
build_source_panels(*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 // The left side is the events pane *and* the plot, in one row: the pane is a column of its own
@@ -6324,12 +6237,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_trigger, app.dbg_toolbar_switch_trigger,
app.dbg_panel_switch_cursors, app.dbg_toolbar_switch_cursors, app.dbg_panel_switch_cursors, app.dbg_toolbar_switch_cursors,
app.dbg_panel_drove_toolbar); app.dbg_panel_drove_toolbar);
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d names=%d icon=%dx%d " std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d pages=%d size=%dx%d\n",
"size=%dx%d\n",
app.dbg_switcher_policy == 0 ? "narrow" : (app.dbg_switcher_policy == 1 ? "wide" : "?"), app.dbg_switcher_policy == 0 ? "narrow" : (app.dbg_switcher_policy == 1 ? "wide" : "?"),
app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_names, app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_pages,
app.dbg_switcher_icon_w, app.dbg_switcher_icon_h, app.dbg_switcher_w, app.dbg_switcher_w, app.dbg_switcher_h);
app.dbg_switcher_h);
// The help affordance layer: what the contextual-help toggle left on the surfaces, and where the // 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`), // guided tour stood when the last capture ran. Both are captured on the tick (`capture_help_state`),
// so neither asks a destroyed widget anything. // so neither asks a destroyed widget anything.
@@ -6463,7 +6374,6 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
// critical (which is how the first version of this line earned four of them). // critical (which is how the first version of this line earned four of them).
std::printf("[udpscope] panel controls: %s\n", app.dbg_panel_controls.c_str()); std::printf("[udpscope] panel controls: %s\n", app.dbg_panel_controls.c_str());
std::printf("[udpscope] toolbar icons: %s\n", app.dbg_toolbar_icons.c_str()); std::printf("[udpscope] toolbar icons: %s\n", app.dbg_toolbar_icons.c_str());
std::printf("[udpscope] page icons: %s\n", app.dbg_page_icons.c_str());
std::printf("[udpscope] app icons: %s\n", app.dbg_app_icons.c_str()); std::printf("[udpscope] app icons: %s\n", app.dbg_app_icons.c_str());
// The measurements pane, with the numbers the engine computed: a probe adds measurements and // The measurements pane, with the numbers the engine computed: a probe adds measurements and
// this is where their values are read, so a wrong threshold or a window that moved under a // this is where their values are read, so a wrong threshold or a window that moved under a
+7 -20
View File
@@ -54,25 +54,17 @@
child of either scroller: the one control that chooses what you are looking at must child of either scroller: the one control that chooses what you are looking at must
not be the first thing to scroll away. not be the first thing to scroll away.
The policy is `narrow` and the **titles are empty**: `narrow` still draws a label The tabs are words: the policy is `wide` and every page has a title, no icon. (They
beside an icon (measured: a 48px "Measure" inside a 54px pill), so an empty title is were icon-only glyphs once; the glyphs were not understandable enough.) The pages'
what makes a pill an icon. The pages' *names* are what identify them to *names* are what identify them to `select_side_tab()` and the session file; a source
`select_side_tab()` and the session file, and the words are re-attached to the pills in kind's own tabs are added in code with their `SourcePanel::title`. -->
code — tooltip and accessible name — by `name_side_tabs()` on the first tick, so an
icon-only pill is still a named thing rather than an anonymous button.
Each page's icon is a glyph of its own, and each one is a *pair*: the Measure tab is
`udpscope-tab-measure-symbolic` (the cursors and the ruler they read against), and the
Signals tab is `udpscope-tab-signals-symbolic` (the trigger's edge and the channel list
below it). They are drawn for the tabs rather than reusing the icons the toolbar and the
Preferences window already carry: a glyph the toolbar uses for one thing means two things
the moment a tab shows it too. -->
<object class="GtkBox" id="side_column"> <object class="GtkBox" id="side_column">
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<property name="width-request">300</property> <property name="width-request">300</property>
<child> <child>
<object class="AdwViewSwitcher" id="side_switcher"> <object class="AdwViewSwitcher" id="side_switcher">
<property name="stack">side_stack</property> <property name="stack">side_stack</property>
<property name="policy">narrow</property> <property name="policy">wide</property>
<property name="margin-top">4</property> <property name="margin-top">4</property>
<property name="margin-bottom">4</property> <property name="margin-bottom">4</property>
<property name="margin-start">6</property> <property name="margin-start">6</property>
@@ -85,10 +77,7 @@
<child> <child>
<object class="AdwViewStackPage"> <object class="AdwViewStackPage">
<property name="name">measure</property> <property name="name">measure</property>
<property name="title"></property> <property name="title">Measure</property>
<!-- The cursors and the ruler: what the pane brackets, and what it
reads the bracketed window with. -->
<property name="icon-name">udpscope-tab-measure-symbolic</property>
<property name="child"> <property name="child">
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property> <property name="hscrollbar-policy">never</property>
@@ -105,9 +94,7 @@
<child> <child>
<object class="AdwViewStackPage"> <object class="AdwViewStackPage">
<property name="name">signals</property> <property name="name">signals</property>
<property name="title"></property> <property name="title">Signals</property>
<!-- The trigger and the channel list it acts on. -->
<property name="icon-name">udpscope-tab-signals-symbolic</property>
<property name="child"> <property name="child">
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property> <property name="hscrollbar-policy">never</property>
+5 -15
View File
@@ -440,21 +440,13 @@ struct App {
/// works, and every other line of the dump still passes. /// works, and every other line of the dump still passes.
std::string dbg_toolbar_icons; std::string dbg_toolbar_icons;
std::string dbg_app_icons; std::string dbg_app_icons;
/// The two sidebar pages' own icons and whether the theme resolved them, read on the first tick /// What the switcher is: how many pills it holds, and how many labels it is *drawing*. The tabs
/// like the two lines above. /// are words (`wide` policy, a title per page), so the label count must equal the page count.
std::string dbg_page_icons;
/// What the switcher is: how many pills it holds, and how many labels it is *drawing*. The label
/// count is the one that says the pages are icons rather than words: `narrow` hides them, and the
/// titles stay set because they are the pages' names for a screen reader and for the tooltip.
int dbg_switcher_pills = -1; int dbg_switcher_pills = -1;
int dbg_switcher_labels = -1; int dbg_switcher_labels = -1;
int dbg_switcher_names = -1; /// How many pages the sidebar stack holds: `pills` and `labels` must both equal it.
/// The pills' icon as it is *rendered*: a 32x16 glyph in a 32px box is drawn 1:1, which is what int dbg_switcher_pages = -1;
/// keeps its 2px bars crisp. /// The switcher's own size, beside it: what the tab bar costs the column.
int dbg_switcher_icon_w = -1;
int dbg_switcher_icon_h = -1;
/// The switcher's own size, beside it: the pills carry 32px icon boxes now, so the tab bar's height
/// is what it costs.
int dbg_switcher_w = -1; int dbg_switcher_w = -1;
int dbg_switcher_h = -1; int dbg_switcher_h = -1;
/// The panes' own switches and the header bar's buttons, read together by the exit probe: the two /// The panes' own switches and the header bar's buttons, read together by the exit probe: the two
@@ -473,8 +465,6 @@ struct App {
/// thing in the body that *is* disabled with the mode. /// thing in the body that *is* disabled with the mode.
int dbg_arm_sensitive = -1; int dbg_arm_sensitive = -1;
int dbg_switcher_policy = -1; int dbg_switcher_policy = -1;
/// Whether `name_side_tabs()` has named the pills: it can only run once the switcher is mapped.
bool side_tabs_named = false;
/// The header bar, so the dump can report the order the controls are in: a header bar is a /// The header bar, so the dump can report the order the controls are in: a header bar is a
/// *titlebar*, not a child of the window, so walking down from the window does not reach it. /// *titlebar*, not a child of the window, so walking down from the window does not reach it.
GtkWidget* toolbar = nullptr; GtkWidget* toolbar = nullptr;
+2 -18
View File
@@ -104,9 +104,8 @@ gboolean exit_probe_step3(gpointer user) {
a->dbg_signal_h = gtk_widget_get_height(a->signal_panel); a->dbg_signal_h = gtk_widget_get_height(a->signal_panel);
a->dbg_trigger_w = gtk_widget_get_width(a->trigger_panel); a->dbg_trigger_w = gtk_widget_get_width(a->trigger_panel);
a->dbg_trigger_h = gtk_widget_get_height(a->trigger_panel); a->dbg_trigger_h = gtk_widget_get_height(a->trigger_panel);
// The switcher's icon as it is *rendered*: a 32x16 glyph in a 32px box draws 1:1, which is what // The switcher's size as allocated: read here and not on the first tick, where the window has not
// keeps its 2px bars crisp rather than blurred by a fractional scale. Read here and not on the // been allocated yet and every widget still measures 0x0.
// first tick, where the window has not been allocated yet and every widget still measures 0x0.
if (a->side_switcher != nullptr) { if (a->side_switcher != nullptr) {
a->dbg_switcher_w = gtk_widget_get_width(a->side_switcher); a->dbg_switcher_w = gtk_widget_get_width(a->side_switcher);
a->dbg_switcher_h = gtk_widget_get_height(a->side_switcher); a->dbg_switcher_h = gtk_widget_get_height(a->side_switcher);
@@ -180,21 +179,6 @@ gboolean exit_probe_step3(gpointer user) {
? 1 : 0; ? 1 : 0;
a->dbg_panel_drove_toolbar = a->dbg_toolbar_switch_cursors; a->dbg_panel_drove_toolbar = a->dbg_toolbar_switch_cursors;
} }
for (GtkWidget* pill = a->side_switcher != nullptr ? gtk_widget_get_first_child(a->side_switcher)
: nullptr;
pill != nullptr; pill = gtk_widget_get_next_sibling(pill)) {
const std::function<void(GtkWidget*)> measure_icon = [&](GtkWidget* node) {
if (GTK_IS_IMAGE(node) && gtk_widget_get_mapped(node)) {
a->dbg_switcher_icon_w = std::max(a->dbg_switcher_icon_w, gtk_widget_get_width(node));
a->dbg_switcher_icon_h = std::max(a->dbg_switcher_icon_h, gtk_widget_get_height(node));
}
for (GtkWidget* c = gtk_widget_get_first_child(node); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
measure_icon(c);
}
};
measure_icon(pill);
}
// The height of one signal row, which is the thing "the channel toggle is too tall" // The height of one signal row, which is the thing "the channel toggle is too tall"
// was about. The panel's own height cannot stand in for it: it also covers the // was about. The panel's own height cannot stand in for it: it also covers the
// heading, the hint and the box margins. // heading, the hint and the box margins.
+3 -5
View File
@@ -27,11 +27,9 @@ void build_source_panels(App& app) {
GTK_POLICY_AUTOMATIC); GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), p.widget); gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), p.widget);
const std::string name = "source-" + p.id; const std::string name = "source-" + p.id;
// An empty title on purpose, as for the built-in tabs: the pill is an icon, and // The tab is its title: the sidebar's pills are words, not glyphs.
// `name_side_tabs()` puts `title` on it as tooltip and accessible name. adw_view_stack_add_titled(ADW_VIEW_STACK(app.side_stack), scroller, name.c_str(),
adw_view_stack_add_titled_with_icon( p.title.c_str());
ADW_VIEW_STACK(app.side_stack), scroller, name.c_str(), "",
p.icon.empty() ? "applications-engineering-symbolic" : p.icon.c_str());
app.source_panels.push_back({name, p.title, p.widget}); app.source_panels.push_back({name, p.title, p.widget});
} }
// Disconnected until the first Connected event says otherwise. // Disconnected until the first Connected event says otherwise.