fix: row titles mean their mnemonics instead of drawing the underscore

Fifteen display strings in the two preference forms were written as GTK mnemonics -- `_Directory`,
`_Server`, `Multicast _group`, `_Theme` and the rest -- but the property that makes GTK read them was
never set, so GTK drew the underscore literally: `_Directory` in the row, not a D with a line under it.

Two switches are involved, FALSE by default in each case. `GtkLabel`/`GtkButton` take `use-underline`;
the rows take `AdwPreferencesRow:use-underline`, "whether an embedded underline in the title indicates a
mnemonic". The two buttons in the source form already set it (so their mnemonics worked and showed no
underscore); the thirteen rows did not. All thirteen now do.

The row classes that carry the property are not all related -- AdwEntryRow has its own, while AdwSpinRow,
AdwComboRow and AdwSwitchRow get it through AdwActionRow -- so the probe reads it by name on a GObject
(g_object_class_find_property, then g_object_get) rather than casting to one row type; a cast to a
`GtkWidget*` where the type is only forward-declared does not compile at all.

`_Cancel` and `_Connect` both claimed C in the same form, so Alt+C could only reach one of them: that
marker was unreachable, which is worse than absent because it is in the source to be trusted. `_Connect`
becomes `Conn_ect`, freeing C for Cancel.

[prefs-probe] reads the count (4/4) on the real widgets; check_ui validates the property against the GIR
for every object in every .ui file, inherited properties included.
This commit is contained in:
Martino Ferrari committed 2026-10-12 15:10:22 +02:00
1 parent 2d9f327da7
commit f00d161900
8 files changed
+78 -1

No files matched your search

+18
View File
@@ -280,3 +280,21 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
re-enter a dead window. GTK does not promise to take a transient down with its parent, and a window 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 whose handlers point into a returned stack frame is how this project already earned one
use-after-free. 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 _events`
and 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`/`GtkButton` take `use-underline`, and the rows take
**`AdwPreferencesRow:use-underline`** ("whether an embedded underline in the title indicates a mnemonic").
The row classes that carry it are not all related -- `AdwEntryRow` has its own, while
`AdwSpinRow`/`AdwComboRow`/`AdwSwitchRow` get it through `AdwActionRow` -- so `row_underlines()` asks by
name on a `GObject` (`g_object_class_find_property`, then `g_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-ui` is 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.** `_Cancel` and `_Connect` both claimed C in
one form, so `Alt+C` could 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_ect` freed
it. Rows on different pages of a preferences dialog may share a letter: an unshown page cannot be
activated.
+13
View File
@@ -1143,3 +1143,16 @@ copy and never writes is exactly the failure this has to catch.
default path back into the field and presses *Apply*, and the file must end up with **no** default path back into the field and presses *Apply*, and the file must end up with **no**
`event_cache_dir` at all. A path in the file is a user's choice, and typing the default is not one -- `event_cache_dir` at all. A path in the file is a user's choice, and typing the default is not one --
pinning it there would stop the setting following the cache directory it is named after. pinning it there would stop the setting following the cache directory it is named after.
## `mnemonics=` in the two preference probes
```
[prefs-probe] mnemonics=4/4
[sources] form mnemonics=8/8
```
How many of the dialog's rows have `use-underline` set -- the property that makes a `_` in a row's title a
mnemonic. It is FALSE on `AdwPreferencesRow` by default, which is why these titles were drawn with a
literal underscore; anything below the full count is that bug back. Read by property name rather than
through one row type, for the reason in `invariants-gtk-and-chrome.md`. The form's line appears only when
`UDPSCOPE_PROBE_NEWHOST` drives the form.
+7
View File
@@ -642,6 +642,13 @@ gboolean pick_probe(gpointer user) {
gtk_editable_set_text(GTK_EDITABLE(c.form_note), "made by the new-host probe"); gtk_editable_set_text(GTK_EDITABLE(c.form_note), "made by the new-host probe");
adw_spin_row_set_value(c.form_buffer, 32); adw_spin_row_set_value(c.form_buffer, 32);
adw_switch_row_set_active(c.form_relative, TRUE); adw_switch_row_set_active(c.form_relative, TRUE);
// 8/8: the form's row titles are mnemonics (`_Server`, `_Port`, `Multicast _group`, ...). The two
// buttons already asked for it; the rows did not, and drew their underscores.
std::fprintf(stderr, "[sources] form mnemonics=%d/8\n",
(row_underlines(G_OBJECT(c.form_server)) + row_underlines(G_OBJECT(c.form_port)) +
row_underlines(G_OBJECT(c.form_group)) + row_underlines(G_OBJECT(c.form_iface)) +
row_underlines(G_OBJECT(c.form_name)) + row_underlines(G_OBJECT(c.form_note)) +
row_underlines(G_OBJECT(c.form_buffer)) + row_underlines(G_OBJECT(c.form_relative))));
std::fprintf(stderr, std::fprintf(stderr,
"[sources] form: name='%s' note='%s' buffer=%.6g relative=%d " "[sources] form: name='%s' note='%s' buffer=%.6g relative=%d "
"connect_sensitive=%d\n", "connect_sensitive=%d\n",
+5
View File
@@ -32,12 +32,14 @@
<child> <child>
<object class="AdwEntryRow" id="dir_row"> <object class="AdwEntryRow" id="dir_row">
<property name="title" translatable="yes">_Directory</property> <property name="title" translatable="yes">_Directory</property>
<property name="use-underline">True</property>
<property name="tooltip-text" translatable="yes">One directory in which every source gets a folder of its own, never one directory between two sources</property> <property name="tooltip-text" translatable="yes">One directory in which every source gets a folder of its own, never one directory between two sources</property>
</object> </object>
</child> </child>
<child> <child>
<object class="AdwSpinRow" id="keep_row"> <object class="AdwSpinRow" id="keep_row">
<property name="title" translatable="yes">Keep _events</property> <property name="title" translatable="yes">Keep _events</property>
<property name="use-underline">True</property>
<property name="subtitle" translatable="yes">Per source; 0 keeps every event, which is a decision about the disk</property> <property name="subtitle" translatable="yes">Per source; 0 keeps every event, which is a decision about the disk</property>
<property name="adjustment"> <property name="adjustment">
<object class="GtkAdjustment"> <object class="GtkAdjustment">
@@ -62,12 +64,14 @@
<child> <child>
<object class="AdwSwitchRow" id="relative_row"> <object class="AdwSwitchRow" id="relative_row">
<property name="title" translatable="yes">_Relative time</property> <property name="title" translatable="yes">_Relative time</property>
<property name="use-underline">True</property>
<property name="subtitle" translatable="yes">Take the plot's time from the sample rate and the packet counter rather than from the stream's own clock, so arrival and clock jitter cannot move a sample; absolute time is still kept for export. Read at start-up, because the buffers are planned before any source is known.</property> <property name="subtitle" translatable="yes">Take the plot's time from the sample rate and the packet counter rather than from the stream's own clock, so arrival and clock jitter cannot move a sample; absolute time is still kept for export. Read at start-up, because the buffers are planned before any source is known.</property>
</object> </object>
</child> </child>
<child> <child>
<object class="AdwComboRow" id="theme_row"> <object class="AdwComboRow" id="theme_row">
<property name="title" translatable="yes">_Theme</property> <property name="title" translatable="yes">_Theme</property>
<property name="use-underline">True</property>
<property name="subtitle" translatable="yes">What the interface looks like; auto follows the system</property> <property name="subtitle" translatable="yes">What the interface looks like; auto follows the system</property>
</object> </object>
</child> </child>
@@ -84,6 +88,7 @@
<child> <child>
<object class="AdwButtonRow" id="reset_row"> <object class="AdwButtonRow" id="reset_row">
<property name="title" translatable="yes">_Reset to defaults</property> <property name="title" translatable="yes">_Reset to defaults</property>
<property name="use-underline">True</property>
<property name="start-icon-name">view-refresh-symbolic</property> <property name="start-icon-name">view-refresh-symbolic</property>
</object> </object>
</child> </child>
+9 -1
View File
@@ -38,7 +38,7 @@
</child> </child>
<child type="end"> <child type="end">
<object class="GtkButton" id="form_apply"> <object class="GtkButton" id="form_apply">
<property name="label" translatable="yes">_Connect</property> <property name="label" translatable="yes">Conn_ect</property>
<property name="use-underline">1</property> <property name="use-underline">1</property>
<property name="receives-default">1</property> <property name="receives-default">1</property>
<property name="css-classes">suggested-action</property> <property name="css-classes">suggested-action</property>
@@ -59,11 +59,13 @@
<child> <child>
<object class="AdwEntryRow" id="server_row"> <object class="AdwEntryRow" id="server_row">
<property name="title" translatable="yes">_Server</property> <property name="title" translatable="yes">_Server</property>
<property name="use-underline">True</property>
</object> </object>
</child> </child>
<child> <child>
<object class="AdwSpinRow" id="port_row"> <object class="AdwSpinRow" id="port_row">
<property name="title" translatable="yes">_Port</property> <property name="title" translatable="yes">_Port</property>
<property name="use-underline">True</property>
<property name="adjustment"> <property name="adjustment">
<object class="GtkAdjustment"> <object class="GtkAdjustment">
<property name="lower">1</property> <property name="lower">1</property>
@@ -77,11 +79,13 @@
<child> <child>
<object class="AdwEntryRow" id="group_row"> <object class="AdwEntryRow" id="group_row">
<property name="title" translatable="yes">Multicast _group</property> <property name="title" translatable="yes">Multicast _group</property>
<property name="use-underline">True</property>
</object> </object>
</child> </child>
<child> <child>
<object class="AdwEntryRow" id="iface_row"> <object class="AdwEntryRow" id="iface_row">
<property name="title" translatable="yes">_Interface</property> <property name="title" translatable="yes">_Interface</property>
<property name="use-underline">True</property>
</object> </object>
</child> </child>
</object> </object>
@@ -93,16 +97,19 @@
<child> <child>
<object class="AdwEntryRow" id="name_row"> <object class="AdwEntryRow" id="name_row">
<property name="title" translatable="yes">_Name</property> <property name="title" translatable="yes">_Name</property>
<property name="use-underline">True</property>
</object> </object>
</child> </child>
<child> <child>
<object class="AdwEntryRow" id="note_row"> <object class="AdwEntryRow" id="note_row">
<property name="title" translatable="yes">N_ote</property> <property name="title" translatable="yes">N_ote</property>
<property name="use-underline">True</property>
</object> </object>
</child> </child>
<child> <child>
<object class="AdwSpinRow" id="buffer_row"> <object class="AdwSpinRow" id="buffer_row">
<property name="title" translatable="yes">_Buffer MB</property> <property name="title" translatable="yes">_Buffer MB</property>
<property name="use-underline">True</property>
<property name="subtitle" translatable="yes">sample buffer per source, applied when this source is opened</property> <property name="subtitle" translatable="yes">sample buffer per source, applied when this source is opened</property>
<property name="adjustment"> <property name="adjustment">
<object class="GtkAdjustment"> <object class="GtkAdjustment">
@@ -117,6 +124,7 @@
<child> <child>
<object class="AdwSwitchRow" id="form_relative_row"> <object class="AdwSwitchRow" id="form_relative_row">
<property name="title" translatable="yes">_Relative time</property> <property name="title" translatable="yes">_Relative time</property>
<property name="use-underline">True</property>
<property name="subtitle" translatable="yes">plot time from the packet counter and the declared rate, instead of the stream's own time signal</property> <property name="subtitle" translatable="yes">plot time from the packet counter and the declared rate, instead of the stream's own time signal</property>
</object> </object>
</child> </child>
+3
View File
@@ -13,6 +13,7 @@
#pragma once #pragma once
#include <gtk4/gtk_plot_area.hpp> #include <gtk4/gtk_plot_area.hpp>
#include "acquisition.hpp" #include "acquisition.hpp"
@@ -33,6 +34,8 @@
#include "status_text.hpp" #include "status_text.hpp"
#include "waveform.hpp" #include "waveform.hpp"
#include <adwaita.h> #include <adwaita.h>
#include <gtk/gtk.h> #include <gtk/gtk.h>
+17
View File
@@ -29,6 +29,23 @@
#include "settings.hpp" #include "settings.hpp"
/// Whether a row asks for `use-underline` -- the property (default FALSE) that makes a `_` in a row's title
/// a mnemonic. Without it GTK draws the underscore as a literal character, which is what `_Directory` and the
/// other row titles did.
///
/// Asked for by *name* on a plain `GObject`: the row classes that carry the property are not all related
/// (`AdwEntryRow`'s `use-underline` is its own, `AdwSpinRow`/`AdwComboRow`/`AdwSwitchRow`'s come through
/// `AdwActionRow`), so a common getter or a cast to one row type would miss half of them -- and passing a
/// row as a `GtkWidget*` where the type is only forward-declared does not even compile. A class without the
/// property answers false rather than crashing.
inline bool row_underlines(GObject* row) {
if (row == nullptr) return false;
if (g_object_class_find_property(G_OBJECT_GET_CLASS(row), "use-underline") == nullptr) return false;
gboolean on = FALSE;
g_object_get(row, "use-underline", &on, nullptr);
return on != FALSE;
}
namespace udpscope { namespace udpscope {
namespace ui_impl { namespace ui_impl {
+6
View File
@@ -1696,6 +1696,12 @@ gboolean prefs_probe(gpointer user) {
// the `.ui` -- the one mistake neither the build nor `GtkBuilder` sees is a missing id. // the `.ui` -- the one mistake neither the build nor `GtkBuilder` sees is a missing id.
const char* shown = const char* shown =
a.prefs.dir_row != nullptr ? gtk_editable_get_text(GTK_EDITABLE(a.prefs.dir_row)) : "-"; a.prefs.dir_row != nullptr ? gtk_editable_get_text(GTK_EDITABLE(a.prefs.dir_row)) : "-";
// 4/4: every row's title carries a mnemonic (`_Directory`, `Keep _events`, ...), so a row that does not
// ask for `use-underline` draws the underscore instead of underlining the letter.
std::fprintf(stderr, "[prefs-probe] mnemonics=%d/4\n",
(row_underlines(G_OBJECT(a.prefs.dir_row)) + row_underlines(G_OBJECT(a.prefs.keep_row)) +
row_underlines(G_OBJECT(a.prefs.relative_row)) +
row_underlines(G_OBJECT(a.prefs.theme_row))));
std::fprintf(stderr, "[prefs-probe] opened=%d rows=%d dir=%s store=%s\n", std::fprintf(stderr, "[prefs-probe] opened=%d rows=%d dir=%s store=%s\n",
a.prefs.dialog != nullptr ? 1 : 0, a.prefs.dir_row != nullptr ? 1 : 0, a.prefs.dialog != nullptr ? 1 : 0, a.prefs.dir_row != nullptr ? 1 : 0,
a.prefs.dir_row != nullptr ? shown : "-", a.prefs.dir_row != nullptr ? shown : "-",