The bold pass had turned the trigger into half a period — a signal rising above its level — and that lost the thing the glyph is for: the crossings. A crest and a trough read as a wave; a hump reads as a hill, and a trigger that shows no crossing names the "above the level" half of an event. The full-period sine with the dashed level through it is restored, exactly as it was, and the Signals pill carries the same wave again so the pill and the toolbar button stay one picture. The Measure glyph keeps the bold redesign, which is the part that was better: two cursors (a cap over two 4px dashes) beside a ruler of two bands with three 2x4 marks and no end caps. The Signals pill also keeps its two bold channel rows. Verified: `toolbar icons: trigger=udpscope-trigger-symbolic(ok)`; `page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)`; `switcher: policy=narrow pills=2 labels=0 names=2 icon=32x32 size=308x36`; `[side-tab-probe] … advanced=4`; the Preferences dialog opened in the same run with no "not present"; `make test-ui` 14 files; zero `Gtk-CRITICAL`. The icons' blob is 12352 bytes, still over the C99 limit, so the clean-build warning count stays four.
37 KiB
Invariants: GTK traps, the status bar, the icons and the help
The GTK signal-signature and dialog-lifetime traps this app has already fallen into, plus the status bar's two tiers, the bundled icons and the help pages.
Part of CLAUDE.md. These are rules that cost a defect to learn; read the ones touching the area you are editing before you edit it.
-
The status line's per-second fields are measured once a second, not per frame.
App::stored_rate/declared_rateand the label's tooltip are refreshed on the same tick as the channel dump; the status line only formats what is already there. The badge's markup is likewise rewritten on a change of phase (twice a second), never on all sixty frames. -
A
notify::<prop>handler takes three parameters, avalue-changedhandler two.GObject::notifypasses(GObject*, GParamSpec*, gpointer)whileGtkSpinButton::value-changedpasses(GtkSpinButton*, gpointer), so one callback cannot serve both: on thenotify::connection theGParamSpec*lands in theuser_dataslot and the handler dereferences it as itsApp*.G_CALLBACKis a cast, so nothing warns. That is why the trigger panel's spin buttons and its dropdowns have separate thin entry points (on_trigger_field_changed,on_trigger_drop_changed) onto onepublish_trigger_fields(): changing the edge or Normal/Single dropdown used to segfault outright.UDPSCOPE_PROBE_FIELDS_MSis the guard. -
MeasureDialogandMathDialogare brace-initialised positionally insrc/ui.cpp, so a field added in the middle of either silently shifts every argument after it: add at the end of the widget group and read the initialiser in the same commit. A row array widened by a new control is the same trap from the other side -- arows[N]that was not grown shows a control the sync never touches, and the compiler says so as an out-of-bounds warning onrows. -
A control that cannot affect the current type is hidden, not disabled: the trigger's band row is hidden for
dropout, the measurement's for every non-crossing type, and the node editor's median row for every op butde-noise. A greyed row is a question the user can see and cannot answer. -
GtkDropDown'snotify::selectedarrives after theset_selected()that caused it returns, so theupdating_panelsflag — set around a programmatic write and cleared when it returns — does not suppress it. Where the write is part of a readout the handler reads back, that is a feedback loop:sync_window_display()writes a value rounded for reading into the window entry and picks the unit for it, and the late notify hadon_window_unit_changed()re-parse that rounded text, turning a 500 s request into 499.98 s. The guard isApp::window_sync_unit— the unit we last synced — and a selection equal to it is ours, since GTK only notifies on a change. Any new control that both writes and reads aGtkDropDownneeds the same shape; the flag alone is not enough. -
The header bar's buttons are icon-only, so headless probes must reach them through the
Apppointers (trigger_toggle,pause_button, …), not viafind_labelled_button(). The pane switches are toolbar buttons too (cursors_toggle,measures_toggle,math_toggle-- packed withheader_bar_pack_end(), where the first packed is the rightmost, and the bar is an ancestor rather than the parent because packing parents into an internal box), and the cursors pane owns its own Reset button rather than the overflow menu owning one for it. The dump printspanel controls: … ; toolbar, left to right: …to say where they are and in what order. The widget tree's child order is the visual order left to right, so that line reads as the reverse of the pack order -- it is how an arrangement gets checked with no screenshot, and it is what finally settled the pack-order rule above (two comments inbuild_toolbar()disagreed with each other and one of them with reality): it is computed on the first tick, because the dump runs after the window is destroyed and asking GTK about a widget then is four criticals, which is 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
GResourcecompiled 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.svgare the seven action icons.udpscope-math-symbolicleft with the toolbar toggle that was its last user;udpscope-measure-symbolicstayed, 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 two tabs got glyphs of their own, and each is a pair rather than one of the icons above.
udpscope-tab-measure-symbolicis 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) — andudpscope-tab-signals-symbolicis 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 reports32x32, and librsvg fits it to32x16), so the pair is letterboxed rather than stretched — and the pill's image is givenpixel_size32 inname_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
accelis GTK accelerator syntax, and a pointer gesture has none.src/shortcuts.cpp's strings are handed toAdwShortcutsItem, which parses them withgtk_accelerator_parse()— so they are"space","<Control>e","<Control>slash","plus minus"(a space-separated pair is the "either of these" form the label draws as+ / -), never the prose"Ctrl+E"or"+ / -". Neither GTK's syntax nor libadwaita's knows a wheel or a drag:"Wheel"costs aFailed to parsewarning and draws an empty cell. Those six rows therefore carryaccel = ""and put the prose insubtitle, which is the one field that is free text. A row added with a parse error is otherwise silent — the overview still opens and the row is simply blank — so read the probe run's stderr forFailed to parseafter touching either table. -
The status bar is two tiers, and the dump reads the detail one.
format_glance()writes the bar andformat_detail()writes both the popover's prose andApp::status_detail, which is whatexit_probe_step3/exit_probe_step2copy intodbg_mode_live/dbg_mode_triggered. Those two used to read the label, and the label no longer carriespackets,gaps,plot 1:or the buffer figures — so agtk_label_get_text()there would silently reduce every documentedmode …:assertion to four fields. The glance line's state field is one word from a fixed set (connecting,live,paused,armed,capturing,event held,disarmed) for the layout reason above; a word wider than the widest already there, or a field whose width tracks the stream, breaks the row the plot is laid out under. The set may grow —connectingwas added for the no-CONFIG state — but only on that condition, and the check is the dump'smax_glance_chars.connectingis derived fromhave_configalone and sits ahead of every other branch, including the display mode's: with no signal list, every other word claims something that does not exist yet. Themodeprose branches the same way for the same reason, which is what keeps the two tiers from disagreeing. -
The ⓘ popover's widgets are built once and only their text is set. It is visible when the tick updates it, so a rebuild is a per-frame relayout of a widget on screen.
update_status_popover()only ever callsgtk_label_set_text(), andpopover_rebuildsin the dump is the assertion. -
The About dialog, the help window and the shortcut overview are opened by
show_about(),show_help()andshow_shortcuts(), and each closes with the matching call. There is exactly oneGtkWindowamong the three — the paged help window. It closes withgtk_window_close(), which emitsclose-requestso the handler that nulls the file-scope pointer runs.gtk_window_destroy()does not emit that signal and must not be used — the math dialog fell into exactly this trap, and the pointer then points at a dead widget until the next attempt to open the same window. The other two — the About dialog (anAdwAboutDialog) and the shortcut overview (anAdwShortcutsDialog) — areAdwDialogs and notGtkWindows: they close withadw_dialog_close()and clear on::closed, there being no::close-requeston anAdwDialog. Both share oneon_dialog_closed()handler keyed by a pointer to the pointer it must null (&g_about,&g_shortcuts), which is what keeps one handler honest for two dialogs. OneGtkWidget*(orAdwDialog*) per window, nulled in its own close handler, is the shape that prevents the double-open defect across all three. -
build_info.cppis generated, never committed.cmake/write_build_info.cmakeruns at configure time (soadd_executablecan name the file) and again on every build throughudpscope_build_info(so the commit and the timestamp belong to the binary that is running).gitabsent leaveskCommitat"unknown"and the build succeeds. The About dialog and the[help]probe both readkVersion,kCommitandkTimestampfrom it;settings=in the probe readsconfig_dir(), which is the path the session file is written to. -
The keyboard zoom's modifiers are not the wheel's.
+/-with Ctrl restricts to x and with Alt to y, while the wheel keeps upstream's Shift = x / Ctrl = y convention. The two cannot agree: Shift is unusable on the keyboard path becausekey_from()folds'+'and'='ontokKeyZoomIn—+is Shift+=on common layouts, so a bare zoom-in already arrives withMod::Shiftset. Ctrl+Alt together zooms neither axis, which needs no special case. The guards aredeps/plotter's ownctrl_plus_zooms_only_xandalt_plus_zooms_only_ytests, plusUDPSCOPE_PROBE_ZOOMKEYS_MS. -
A help page is one
.txt, one<file>line in the GResource manifest, and onekPagesrow. The bodies are Pango markup in a second GResource (--c-name=udpscope_help, so its generated symbols cannot collide with the icons'), validated withpango_parse_markup()at load; a page that fails either lookup or parse renders a one-line stand-in and is excluded fromparsed.UDPSCOPE_PROBE_HELP_MS'sparsedmust equalpages. A page may also carry screenshots (HelpPage::images, a null-terminated list of resources). They areGtkPictures with a height request: one createdcan_shrinkasks for no height at all inside a box, so without the request it is allocated nothing and the page renders its text with a silent gap where its screenshots should be — which is what the first version did. -
A
GtkPanedhands its start child at least its minimum unless the position says otherwise. Putting the events pane in a paned is what makes its width the user's to drag -- but with the position left alone,shrink-start-childor not, the pane's minimum is reserved as an empty column while it is closed, and the plot pays for a pane nobody opened. The position is the only knob that says "no room": store the width when it is closed (readinggtk_paned_get_position()before zeroing it) and set 0, restoring it when the pane opens.gtk_paned_set_shrink_start_child(TRUE)is still needed, or the minimum would clamp even a zero position. -
An unrevealed
GtkRevealermeasures 0x0 and is still given the leftover width.reveal_child = FALSEmakes both the revealer's minimum and its natural size zero -- and its box then hands it everything the expanding sibling beside it did not claim, so a collapsed pane reserved a 310-pixel empty column between the window edge and a plot drawn 630 pixels wide in a 1270-pixel window. Every vertical revealer in the side panel hides this, because there the leftover goes down instead of across.gtk_widget_set_visible()is what a layout honours, so anything that must not take room has to be hidden and not merely unrevealed; the revealer keeps the slide while the pane is open. The allocated width is how this was found -- the revealer's own measure said 0. The sidebar is a tab stack now, and has noGtkRevealerleft in it: anAdwViewSwitcherabove anAdwViewStack, each page aGtkScrolledWindowof its own (the two pages are different heights, so one shared scroller would leave a switch scrolled where the new page never asked to be). The reason is the one this trap teaches: five sections competing for one 320 px column left every one of them short of room. A feature's on/off is no longer a visibility at all -- the trigger's and the cursors' toggles set their panel's sensitivity, so the panel stays where it is and the settings you are about to switch back on do not vanish while you look for them. The trap itself does not go away with the widget: a stack's hidden page measures 0x0 too and is still given the leftover width, so ask about state, not about size carries over unchanged. Every 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.narrowstill draws a label; an empty title is what makes a pill an icon. Measured on libadwaita 1.10: withnarrowand a title, the pill holds aGtkImageand aGtkLabel— 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 (theirnameis what identifies them toselect_side_tab()and the session file) and the pills are named in code instead: the first tick callsname_side_tabs(), which walks the switcher's children — the pills are there in page order, andAdwViewSwitcherButtonis 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, andswitcher: … pills=2 labels=0 names=2is the reading for all three facts at once. -
A list box outlives the children of the pane it sits in, and the teardown order is not yours to choose. Tearing the window down finalizes the events pane's buttons, then the list box, and the list's own
selected-rows-changedthen reaches a handler holding the freed pointers: fourGtk-CRITICALs fromgtk_widget_set_sensitiveand a read of memory nobody owns.close-requestis not the hook --UDPSCOPE_EXIT_AFTER_MSends the self-check withgtk_window_destroy(), which does not emit it. The hook that does work isApp::window: every teardown path clears it before the widgets go (the exit probe says so in a comment: "stop the tick before the widgets go"), so a handler that can fire during teardown checks that first, rather than asking GTK whether a pointer is still a widget. -
A
GtkPicture's natural size is its content, so a size request on it is a floor, not a cap. A 480x160 texture in a row of a list claims the row's whole width and squeezes the text beside it out of the row -- the box gives a non-expanding child its natural size and grows it further when the expanding child has nothing left to take. Give the picture a frame whose size request equals the scaled texture's size (natural == minimum, which is what stops the box growing it), and scale the texture once at decode: it is also what keeps a per-row cache at 12 KB per picture rather than 300. -
A pointer to a widget inside a list survives the list being rebuilt.
events_thumbnamed a row's picture acrossrefresh_events_pane(), which removes every row first: the next reader of that pointer got aGtk-CRITICALfromgtk_picture_get_paintableon freed memory (the four criticals read as "the pane is empty"). A rebuild has to null what it is about to destroy, not only rebuild it. -
gtk_picture_set_filename()can render nothing, silently. It hands the file to the image loader's sandboxed, asynchronous decoder, which on this machine fails without a message: the picture keeps a paint-able and takes an allocation, so a probe that checksgtk_picture_get_paintable()reads success while the pane shows an empty rectangle (the event pane's preview did exactly that).gdk_texture_new_from_filename()decodes the same file on this thread andgtk_picture_set_paintable()hands over the result -- and a NULL texture at least says so. A picture that must be on screen is also a picture with a height request:can_shrinkalone asks for no height at all in a box. -
A cache that is the only source of a widget's content must be created by the thing that reads it. The event pane's thumbnails come from an
EventThumbCachehung off the list box withg_object_set_data_full, andevent_preview_texture()answers out of that cache or not at all. Nothing ever created it — so for as long as the pane existed, every row came up without a picture, which is what "event screenshot are unusable and sometime missing" was. A missing cache there is not a slow path, it is a blank one;event_thumb_cache()therefore creates one on first use. Owned by the list, because nothing else has the pane's lifetime: textures of a list that no longer exists are so much held memory, and the destroy notify is where they are unref'd. -
Hover magnification is a tooltip, not a popover of ours. A row's thumbnail is 138 px wide and the stored preview is 480: the small one says which event this is, and the zoom says what is in it. GTK's own tooltip (
gtk_widget_set_has_tooltip+query-tooltip+gtk_tooltip_set_custom) is shown, placed, dismissed and destroyed by GTK, and takes no pointer grab. A popover per row would be a parented widget to unparent on every rebuild of the pane — one defect below — and a grab to lose, for a thing with no interaction in it. Two details:can_shrinkmust be FALSE on the tooltip's picture or the tooltip is given the smallest size its content allows (the opposite of the point), and the path handed to the handler has to be owned by the closure (g_signal_connect_datawith a destroy func), since the record it came from is a loop variable. -
Unparenting a popover inside its own
closedemission is a use-after-free. Dismissals go throughgtk_popover_popdown(), which emitsclosed; a handler that unparents the popover there drops the reference the parent held, the widget dies, and GTK's own popdown path then runs on freed memory —gtk_popover_get_autohide: assertion 'GTK_IS_POPOVER (popover)' failed, on every menu entry that dismisses its popover, which is every entry there is.on_popover_closed()therefore defers the unparent by one idle and holds a reference across it (g_idle_add_full(..., g_object_ref(popover), g_object_unref)), so the popover outlives the emission that dismissed it. -
find_accel()masks the modifiers before comparing. Lock, Num-Lock and the pointer buttons all arrive inGdkModifierType, so an unmasked compare matches no table row at all once Caps Lock is on. -
A UI file is data, and a mistake in it is a runtime abort, not a build failure.
.uifiles live insrc/ui/and are compiled into the binary by the third GResource (udpscope-ui.gresource.xml,prefix=/org/udpscope/ui), loaded withgtk_builder_new_from_resource().preprocess="xml-stripblanks"catches a malformed file at build time -- which is why it is on every file in every bundle -- and that includes an XML rule easy to break by hand: a comment may not contain a double hyphen, so prose written the way every C++ comment in this project is written (-- like this --) fails the build with a caret under the offending character. Use an em dash in a UI file.What it cannot catch is a file that is valid XML but wrong for GTK, and that is what
cmake --build build --target test-uiis for: it round-trips every file throughblueprint-compiler, which type-checks properties and child types against the GIR -- for every file, rather than for the windows a probe happens to open (see build-and-run.md). Without that check,GtkBuilderreports such a file with aGtk-ERROR, which aborts the process by default, so a window built from bad markup takes the application down rather than failing to appear.Two more paid for: a
GIconproperty takes an object, not a name (<property name="icon">foo</property>is read as a reference to another object and fails with "Object with ID foo not found" -- the icon is a nestedGThemedIcon); and a.uibig enough to exceed 4095 characters needs-Wno-overlength-stringson its generated source, because glib-compile-resources writes each file as one C string literal and the warning fires on generated data. -
Deprecated widgets may be the right widget, as long as no deprecated function is called. The shortcut overview is a
GtkShortcutsWindow(gtk/deprecated/, deprecated in 4.20) because libadwaita's successor,AdwShortcutsDialog, cannot draw a mouse gesture:AdwShortcutsItemhas no icon property at all, so its shortcut cell can only be an accelerator, and its cell reads "No Shortcut" when there is none.GtkShortcutsShortcuthas one generic gesture type whoseiconis drawn. Loading the window from a.uiand presenting it as aGtkWindownames no deprecated function, so the build stays at its four warnings. When such a window closes, its builder is replaced on the next opening, not kept: the window belongs to the builder's object list and a destroyedGtkWindowcannot be presented again. -
An
AdwDialogis not aGtkWindow, and not every toplevel is a widget either. The preferences are anAdwDialog, which is aGObjectthat a window presents, so it cannot parent aGtkFileDialog: the form keeps the window it was opened with (PrefsForm::parent, the caller's) and hands that to the picker. Two things follow, and each cost a run. In its floating presentation mode anAdwDialogadds a native surface togtk_window_get_toplevels(), so that list holds things that are neitherGtkWindows nor widgets --GTK_WINDOW()on one is a segfault insidegtk_window_close(), andg_object_unref()on one is aGLib-GObject-CRITICAL. Anything walking it must testGTK_IS_WIDGET()andGTK_IS_WINDOW()first, and a probe should not walk it at all where a cancellable will do. -
A folder chooser outlives the dialog that opened it unless something cancels it. The picker's answer writes a row, and the row is gone once the dialog closes.
PrefsForm::browse_cancelis cancelled by theclosedhandler and replaced by the next chooser: one open picker at a time, and none that can call back into a cleared form. It is also how a probe takes the cancel path without a mouse. -
An
AdwApplicationWindowrefuses a titlebar, and refuses it by aborting.gtk_window_set_titlebar()on one is a fatalAdwaita-ERROR(is not supported for AdwApplicationWindow), not a warning that leaves the window merely ugly. The main window is therefore aGtkApplicationWindow-- an Adw window would want its header as anAdwToolbarView's top bar instead -- andbuild_toolbar()'sGtkHeaderBarstays the window's own titlebar, which is also what gives the window its controls: aGtkHeaderBardraws them only as a titlebar. -
libadwaita's rows do not share one property set, and a
.uiis where that shows. AnAdwEntryRowis anAdwPreferencesRow: it has atitleand nosubtitle.AdwSpinRowandAdwSwitchRowareAdwActionRows and have both. A hint about an entry -- "empty = unicast" -- therefore belongs in the group'sdescription, which is the better place for it anyway: a placeholder vanishes as soon as there is a value, and a description does not. And anAdwDialoghas nocontentproperty: its child is a plain<child>, where anAdwToolbarViewdoes takecontent. Both mistakes are aGtk-ERRORat load, so both abort the process, and both are caught by the probe that opens every window rather than by anything at build time. -
A dialog reachable from two windows cannot own its state in either. The preferences open from the main window's menu and from the source chooser, which exists before
Appdoes (main()chooses a source, then builds the model and the UI). The widgets therefore live in aPrefsFormthe caller owns —App::prefs,Chooser::prefs— and what a change means is a callback, because it means different things on the two sides. Both are plain members of their owners, on the same stack frame, so nothing can outlive the frame that built it. -
A transient window is destroyed before its parent.
chooser_finish()destroys the preferences window before the chooser, and nulls the pointer before the destroy soclose-requestcannot re-enter a dead window. GTK does not promise to take a transient down with its parent, and a window whose handlers point into a returned stack frame is how this project already earned one use-after-free. -
A
_in a label is a mnemonic only on a widget that was asked for one. GTK reads the underscore as "the next letter is this widget's accelerator, and draw it underlined", so wherever the widget has not been asked, GTK draws the underscore literally -- which is what_Directory,_Server,Keep _eventsand eleven more titles did in the two preference forms, for as long as they existed. Two different switches, FALSE by default in both cases:GtkLabel/GtkButtontakeuse-underline, and the rows takeAdwPreferencesRow:use-underline("whether an embedded underline in the title indicates a mnemonic"). The row classes that carry it are not all related --AdwEntryRowhas its own, whileAdwSpinRow/AdwComboRow/AdwSwitchRowget it throughAdwActionRow-- sorow_underlines()asks by name on aGObject(g_object_class_find_property, theng_object_get) instead of casting to one row type: casting a row whose type is only forward-declared in that translation unit does not even compile.test-uiis what catches the property being set on a class that does not have it -- it validates every property against the GIR, inherited ones included. -
A mnemonic is unique per window, and the first match wins.
_Canceland_Connectboth claimed C in one form, soAlt+Ccould only ever reach one of them and the other's marker was unreachable -- worse than no mnemonic, because the underscore is there in the source for a reader to trust.Conn_ectfreed it. Rows on different pages of a preferences dialog may share a letter: an unshown page cannot be activated. -
A
GtkExpander's child is not in the widget-parent chain.gtk_widget_get_parent()on the widget an expander holds answers NULL, before and after the window is presented, whilegtk_expander_get_child()returns it -- measured all three ways (the.ui'schildproperty, a codegtk_expander_set_child(), a code-made expander). So a probe that walks parents to prove a widget is inside a panel reads 0 for everything behind an expander, on a panel that is entirely correct; the walk has to start atgtk_expander_get_child(). That mistake cost a false regression, a "the property does not attach it" conclusion drawn from it, and a pointless second root in the file before the throwaway program settled it. Related:gtk_expander_set_child()asserts that its child has no parent yet, so a block the.uihas already put in the panel cannot then be handed to an expander -- the file'schildproperty is the right way, and it works. -
--is not allowed inside an XML comment, and a.uicomment is XML.<!-- ... -- ... -->is a parse error:xml-stripblanksfails the build, andmake test-uiprintsFAIL <file>with no message under it, because blueprint-compiler's decompiler aborts on the malformed XML before it can report anything -- the bareFAILline is that mistake and nothing else. The house style for a dash in prose is the em dash, which is why no other.uiinsrc/ui/contains a double hyphen. It has cost two slices a debugging round for one character. -
A
.uifile instantiates its objects once, so anything whose count is data cannot live in one. The layout editor's overlay buttons are one per row, plus one per boundary between them: aGtkBuilderwould hand back the first and nothing for the rest, and there is no "prototype the code copies" in the format. They are therefore code, and what they share -- the icon, the two CSS classes that make a small round on-screen-display control, and the size -- is one function (layout_overlay_button()) rather than three copies. The same reason keeps the math editor's palette and parameter rows in code: their count follows the model, not the file..uifiles are for the frame, and the frame is what has no count. -
A code-added child of a header bar lands after the file's, so going first means using a slot.
gtk_header_bar_pack_start()appends to the start group and there is no prepend. The time-window control is code -- its menu's rows come from the buffer's limits -- and belongs at the bar's head, soheader-bar.uideclares an emptywindow_control_slotbox as its first start child and the code appends the control into it. Packing the control straight onto the bar would have put it to the right of everything the file packed, and[udpscope] header:(window_button_w=130 bar_w=756) is the reading that shows such a move. -
<child type="end">is added leftwards-from-the-right, in document order. A header bar's end group reads the reverse of the file:header-bar.uideclares menu, sidebar, math, measures, cursors and the screen shows cursors, measures, math, sidebar, menu. The three comments this replaced each had it wrong once, which is why the order is the file's now and a byte-for-byte diff of the probe is the check. -
A widget can end up in the wrong container, and only a structural read notices.
math_bodywas written as a<child>of the window instead of the box above it when the math editor's frame moved into a.ui. AGtkWindowtakes exactly one default<child>, so nothing failed: the body -- with the canvas and the parameter form inside it -- was left parentless,visiblebut nevermapped, size0x0, drawing nothing. The symptom was "the editor opens and the canvas is blank", with the window sized around its remaining children.check_uipasses such a file;xml-stripblankspasses it; so does every probe that reads values off the widgets inside. What catches it is a reading that walks up from the widget (in_dialog=1) or asks whether it was ever given an allocation (mapped=1,canvas=420x836). When a frame moves into a file, the probe that proves it has to read the frame's structure, not only the numbers it holds. -
A popover's autohide is only as good as its grab.
GtkPopoverdismisses itself when it loses the pointer grab -- and the grab is the thing that can be missing: a window manager moving it, a nested popover (aGtkDropDown's menu, which the time-window row has) taking it, and every headless run, where there is no manager to hand one out and the probes have to turn autohide off before a popover will stay up to be measured. The symptom is "sometimes it is awkward to close": a click anywhere else leaves the bubble up.on_window_popover_press()states the rule outright instead -- any press that reaches the window's own surface closes the window popover. That needs no hit test beyond the button, because a popover is its own surface and a click inside it never arrives; the button is left to its own click, whose toggle is what closes it.[windowpop]reads both halves, with autohide off so it is testing the rule and not the grab. -
A notice shares the status line with the glance, so it has to look like a message. The status bar renders
app.notice + " | " + glancein one label, which is exactly where a reader skimming the routine text ("live · 9.97 kS/s · 2 s window") will not see it: a refused drop that said "plot 1 holds four signals already" was read as no feedback at all. While a notice is fresh the label carries the stock.warningclass, so a message looks like a message and the class goes when the notice does. Two corollaries worth knowing: the line composes on its own cadence, so a notice can appear a beat after it was set (do not read the label in the same tick a handler ran), and a refusal should say what to do about it -- "drop it on another plot, or add a row with the layout editor (L)" rather than the bare statement of the limit. -
The status line cannot cover the gap before the window exists, so a window does. Between the chooser closing and
gtk_window_present()there is no main loop at all: the chooser destroyed its own window, thenmain()allocates the sample buffer, waits for the first CONFIG or reads a recording, andon_activate()builds every panel -- at a few gigabytes of buffer that is the ten seconds a user reported as "the dialog disappeared and nothing happened".loading_show()/loading_hide()(insource_dialog.cpp, beside the chooser they continue) keep anAdwWindowon screen across it, raised inmain()before the model is constructed and taken down inon_activate()after the present -- either side is a frame of empty screen. Three rules it exists under:- It must be painted before the caller blocks.
gtk_window_present()only queues the frame, and nothing after it runs a loop, soloading_show()ends in a nestedGMainLoopquit by a timeout (the chooser's own idiom). Iterating pending events is not enough: a window needs a frame-clock cycle, and the frame clock only ticks while a loop runs. - It says what the wait is, in words. The spinner is frozen for the whole of it -- no loop -- and a
progress bar would be a lie for the same reason, so the text carries the information: the address
being connected to or the file being opened, and under it the buffer size, which is the number that
explains the duration. It is
deletable=0with no title buttons: a close button that cannot be serviced until the work is over is worse than none. - Only the path that asked gets one.
main()raises it only when the chooser actually ran, which is why every documented probe (all of them pass--server) is on exactly the path it was before -- and a--serverrun never had a window to lose, so the post-present notice is the right place for it.loading_hide()is a no-op when none is up, sorun_ui()needs no notion of which path it is on.
- It must be painted before the caller blocks.