Feature/source plugins #1
No files matched your search
@@ -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-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-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. |
|
||||
@@ -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
|
||||
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**
|
||||
tab (the trigger and the channels), **each a pill with its own glyph and no text** — the
|
||||
cursors and the ruler for Measure, a sine crossing its trigger level and the channel list
|
||||
for Signals (the same picture the toolbar's trigger button carries), drawn as a
|
||||
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
|
||||
tab (the trigger and the channels), **each a text tab** (the sidebar was icon-only for a while; glyphs for "cursors and ruler" and
|
||||
"trigger and channel list" were not understandable enough, so the words won). A source kind's own tabs
|
||||
(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
|
||||
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
|
||||
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
|
||||
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
|
||||
design spec had the switcher showing *titles* and neither page an
|
||||
`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
|
||||
switcher's policy is `wide` and each page has a title. 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
|
||||
|
||||
@@ -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*
|
||||
(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.
|
||||
- **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
|
||||
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,
|
||||
measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,events,resize-rows,tab-measure,tab-signals}-symbolic.svg`
|
||||
are the seven action icons. `udpscope-math-symbolic` left with the toolbar toggle that was its last
|
||||
measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,events,resize-rows}-symbolic.svg`
|
||||
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 --
|
||||
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.
|
||||
`udpscope-tab-measure-symbolic` is two cursors beside the ruler they read against — each a cap over two
|
||||
bold dashes, beside a ruler of two bands with three marks between them and **no end caps** (a closed
|
||||
frame reads as a rectangle, and costs a fifth of the ink on two lines that say nothing) — and
|
||||
`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.
|
||||
The sidebar's **tabs have no icons any more**: two glyphs (`udpscope-tab-measure-symbolic`,
|
||||
`udpscope-tab-signals-symbolic`, each a 32x16 pair) were drawn for them and judged not understandable
|
||||
enough, so the tabs are text and both SVGs were deleted. The *trigger's* own icon was redrawn to the
|
||||
Signals glyph's picture at that time and kept it.
|
||||
- **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
|
||||
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*
|
||||
current -- a change in what the reading means, not a defect, and no functional code may depend on
|
||||
those numbers.
|
||||
- **`AdwViewSwitcherPolicy.narrow` still draws a label; an *empty title* is what makes a pill an icon.**
|
||||
Measured on libadwaita 1.10: with `narrow` and a title, the pill holds a `GtkImage` **and** a
|
||||
`GtkLabel` — 48px of "Measure" inside a 54px pill — so the policy alone does not give an icon-only tab
|
||||
bar. The sidebar's pages therefore have **no title** (their `name` is what identifies them to
|
||||
`select_side_tab()` and the session file) and the pills are *named in code* instead: the first tick
|
||||
calls `name_side_tabs()`, which walks the switcher's children — the pills are there in page order, and
|
||||
`AdwViewSwitcherButton` is not a public type, so there is no type check to make — and sets each pill's
|
||||
**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.
|
||||
- **The sidebar's tabs are text: `AdwViewSwitcherPolicy.wide`, a title on every page, no icon.** The
|
||||
built-in pages get their titles in the `.ui` and a source kind's pages from `SourcePanel::title`
|
||||
(`adw_view_stack_add_titled`); the page's *name* still identifies it to `select_side_tab()` and the
|
||||
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`.
|
||||
- **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
|
||||
|
||||
@@ -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
|
||||
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=narrow pills=2 labels=0 names=2 icon=32x32 size=308x36` (in the exit dump, beside
|
||||
`panels:`) — the two
|
||||
sidebar tabs as the switcher actually draws them. **`pills=2 labels=0`** is the claim: two pills are on
|
||||
screen and **no text is drawn** (`labels` counts *mapped labels with text*, because `narrow` keeps an
|
||||
empty label in the pill's layout and a plain mapped count would read 2 either way). `names=2` is the
|
||||
other half — both pills carry a tooltip and an accessible name set by `name_side_tabs()`, so an
|
||||
icon-only pill is not an anonymous button — and `pills=0` means nothing is mapped, which makes the
|
||||
rest of the line meaningless rather than a pass. `policy=narrow` is read rather than assumed. `icon=`
|
||||
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.
|
||||
- `switcher: policy=wide pills=4 labels=4 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
|
||||
(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.
|
||||
`size=` is the bar's own size: a text tab costs about 34 px of height, and a width of 308 shows the
|
||||
four tabs fit the 320 px column.
|
||||
- `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
|
||||
the number of ids**, and the ids are the surfaces the contextual help is on: eleven of the window
|
||||
|
||||
@@ -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 clean build emits **three** warnings and no more: two
|
||||
`-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
|
||||
the seven action icons and the two app icons that remain (a note about a generated file, which GCC
|
||||
generated GResource `.c`, whose blob is **9248** against C99's 4095-byte string-literal limit with
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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 |
@@ -20,8 +20,6 @@
|
||||
<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-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-resize-rows-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app.svg</file>
|
||||
|
||||
@@ -332,12 +332,10 @@ std::vector<SourcePanel> make_e1608_panels(DataSource& source) {
|
||||
SourcePanel ao;
|
||||
ao.id = "ao";
|
||||
ao.title = "Analog out";
|
||||
ao.icon = "audio-volume-high-symbolic";
|
||||
ao.widget = build_ao(src);
|
||||
SourcePanel dio;
|
||||
dio.id = "dio";
|
||||
dio.title = "Digital I/O";
|
||||
dio.icon = "input-dialpad-symbolic";
|
||||
dio.widget = build_dio(src);
|
||||
return {ao, dio};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ struct ConfigForm {
|
||||
struct SourcePanel {
|
||||
std::string id; ///< stack page name
|
||||
std::string title;
|
||||
std::string icon; ///< icon name for the tab, may be empty
|
||||
GtkWidget* widget = nullptr;
|
||||
};
|
||||
|
||||
|
||||
+18
-108
@@ -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
|
||||
/// 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.
|
||||
/// 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) {
|
||||
if (!app.dbg_panel_controls.empty()) return;
|
||||
const auto in_bar = [](GtkWidget* w) {
|
||||
@@ -3110,39 +3052,17 @@ void note_panel_controls(App& app) {
|
||||
icon_report(theme, "org.udpscope.app-symbolic",
|
||||
"/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
|
||||
// drawing for them. The pills are `AdwViewSwitcher`'s own buttons, built when the stack is mapped,
|
||||
// so this reads on the first tick: `pills` is the guard (0 pills would make a label count of 0
|
||||
// meaningless) and `labels` must be 0, because the switcher's policy is `narrow` -- the pages are
|
||||
// icons, not words, in a 300px column.
|
||||
// Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject`
|
||||
// (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.
|
||||
// What the switcher is drawing: the sidebar's tabs are words (`wide` policy, a title per page, no
|
||||
// icon). The pills are `AdwViewSwitcher`'s own buttons, built when the stack is mapped, so this
|
||||
// reads on the first tick. `mapped`, not `visible`: visibility is per widget, so a label whose
|
||||
// *parent* is hidden still reads visible; `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 drawn_labels = 0;
|
||||
const std::function<void(GtkWidget*)> count_drawn = [&](GtkWidget* w) {
|
||||
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
|
||||
// "mapped" alone would count the empty ones and read 2 whether the pills were icons or words.
|
||||
// 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;
|
||||
}
|
||||
@@ -3154,14 +3074,12 @@ 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;
|
||||
int named_pills = 0;
|
||||
for (GtkWidget* w = app.side_switcher != nullptr ? gtk_widget_get_first_child(app.side_switcher)
|
||||
: nullptr;
|
||||
w != nullptr; w = gtk_widget_get_next_sibling(w)) {
|
||||
const char* tip = gtk_widget_get_tooltip_text(w);
|
||||
if (tip != nullptr && tip[0] != '\0') ++named_pills;
|
||||
}
|
||||
app.dbg_switcher_names = named_pills;
|
||||
// Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject`.
|
||||
app.dbg_switcher_pages =
|
||||
app.side_stack != nullptr
|
||||
? static_cast<int>(g_list_model_get_n_items(
|
||||
G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))))
|
||||
: 0;
|
||||
if (app.side_switcher != 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) {
|
||||
App* app = static_cast<App*>(user);
|
||||
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);
|
||||
sync_source_panels(*app);
|
||||
// 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_CALLBACK(on_side_tab_changed), app);
|
||||
}
|
||||
// The source kind's own tabs (a generator, digital I/O). Before the first tick: that is when
|
||||
// `name_side_tabs()` puts the words on the pills, and it must see these pages too.
|
||||
// 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.
|
||||
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
|
||||
@@ -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_cursors, app.dbg_toolbar_switch_cursors,
|
||||
app.dbg_panel_drove_toolbar);
|
||||
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d names=%d icon=%dx%d "
|
||||
"size=%dx%d\n",
|
||||
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%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_names,
|
||||
app.dbg_switcher_icon_w, app.dbg_switcher_icon_h, app.dbg_switcher_w,
|
||||
app.dbg_switcher_h);
|
||||
app.dbg_switcher_pills, app.dbg_switcher_labels, 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.
|
||||
@@ -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).
|
||||
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] page icons: %s\n", app.dbg_page_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
|
||||
// this is where their values are read, so a wrong threshold or a window that moved under a
|
||||
|
||||
+7
-20
@@ -54,25 +54,17 @@
|
||||
child of either scroller: the one control that chooses what you are looking at must
|
||||
not be the first thing to scroll away.
|
||||
|
||||
The policy is `narrow` and the **titles are empty**: `narrow` still draws a label
|
||||
beside an icon (measured: a 48px "Measure" inside a 54px pill), so an empty title is
|
||||
what makes a pill an icon. The pages' *names* are what identify them to
|
||||
`select_side_tab()` and the session file, and the words are re-attached to the pills in
|
||||
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. -->
|
||||
The tabs are words: the policy is `wide` and every page has a title, no icon. (They
|
||||
were icon-only glyphs once; the glyphs were not understandable enough.) The pages'
|
||||
*names* are what identify them to `select_side_tab()` and the session file; a source
|
||||
kind's own tabs are added in code with their `SourcePanel::title`. -->
|
||||
<object class="GtkBox" id="side_column">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="width-request">300</property>
|
||||
<child>
|
||||
<object class="AdwViewSwitcher" id="side_switcher">
|
||||
<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-bottom">4</property>
|
||||
<property name="margin-start">6</property>
|
||||
@@ -85,10 +77,7 @@
|
||||
<child>
|
||||
<object class="AdwViewStackPage">
|
||||
<property name="name">measure</property>
|
||||
<property name="title"></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="title">Measure</property>
|
||||
<property name="child">
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
@@ -105,9 +94,7 @@
|
||||
<child>
|
||||
<object class="AdwViewStackPage">
|
||||
<property name="name">signals</property>
|
||||
<property name="title"></property>
|
||||
<!-- The trigger and the channel list it acts on. -->
|
||||
<property name="icon-name">udpscope-tab-signals-symbolic</property>
|
||||
<property name="title">Signals</property>
|
||||
<property name="child">
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
|
||||
+5
-15
@@ -440,21 +440,13 @@ struct App {
|
||||
/// works, and every other line of the dump still passes.
|
||||
std::string dbg_toolbar_icons;
|
||||
std::string dbg_app_icons;
|
||||
/// The two sidebar pages' own icons and whether the theme resolved them, read on the first tick
|
||||
/// like the two lines above.
|
||||
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.
|
||||
/// What the switcher is: how many pills it holds, and how many labels it is *drawing*. The tabs
|
||||
/// 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;
|
||||
int dbg_switcher_names = -1;
|
||||
/// The pills' icon as it is *rendered*: a 32x16 glyph in a 32px box is drawn 1:1, which is what
|
||||
/// keeps its 2px bars crisp.
|
||||
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.
|
||||
/// 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.
|
||||
int dbg_switcher_w = -1;
|
||||
int dbg_switcher_h = -1;
|
||||
/// 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.
|
||||
int dbg_arm_sensitive = -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
|
||||
/// *titlebar*, not a child of the window, so walking down from the window does not reach it.
|
||||
GtkWidget* toolbar = nullptr;
|
||||
|
||||
+2
-18
@@ -104,9 +104,8 @@ gboolean exit_probe_step3(gpointer user) {
|
||||
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_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
|
||||
// keeps its 2px bars crisp rather than blurred by a fractional scale. Read here and not on the
|
||||
// first tick, where the window has not been allocated yet and every widget still measures 0x0.
|
||||
// The switcher's size as allocated: read here and not on the first tick, where the window has not
|
||||
// been allocated yet and every widget still measures 0x0.
|
||||
if (a->side_switcher != nullptr) {
|
||||
a->dbg_switcher_w = gtk_widget_get_width(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;
|
||||
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"
|
||||
// was about. The panel's own height cannot stand in for it: it also covers the
|
||||
// heading, the hint and the box margins.
|
||||
|
||||
+3
-5
@@ -27,11 +27,9 @@ void build_source_panels(App& app) {
|
||||
GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), p.widget);
|
||||
const std::string name = "source-" + p.id;
|
||||
// An empty title on purpose, as for the built-in tabs: the pill is an icon, and
|
||||
// `name_side_tabs()` puts `title` on it as tooltip and accessible name.
|
||||
adw_view_stack_add_titled_with_icon(
|
||||
ADW_VIEW_STACK(app.side_stack), scroller, name.c_str(), "",
|
||||
p.icon.empty() ? "applications-engineering-symbolic" : p.icon.c_str());
|
||||
// The tab is its title: the sidebar's pills are words, not glyphs.
|
||||
adw_view_stack_add_titled(ADW_VIEW_STACK(app.side_stack), scroller, name.c_str(),
|
||||
p.title.c_str());
|
||||
app.source_panels.push_back({name, p.title, p.widget});
|
||||
}
|
||||
// Disconnected until the first Connected event says otherwise.
|
||||
|
||||
Reference in new issue
Block a user