ui: each pane carries its own switch, the same one the header bar has

The trigger pane and the cursors pane now have a `GtkSwitch` in their heading, and it is *one control in
two places* with the header bar's button: both run the same action (`run_app_action()`), and
`sync_panel_switches()` re-reads each pair from the toolbar's state after any change, so clicking either
end moves both. A pane whose feature is off is disabled, which is exactly the state you need the switch
in, so it belongs *in* the pane rather than only in the toolbar.

Three things it needed:

- **The sensitive target moved to the pane's body.** A control inside an insensitive parent cannot be
  clicked, so a switch that greyed out with its own pane could never turn it back on. `on_trigger_toggled`
  and `on_cursors_toggled` now disable `trigger_body` / `cursors_body`, and so does the startup default;
  the headings, with their switches, stay live.
- **The trigger's heading moved out of `trigger-panel.ui`** and is built in `build_trigger_panel()`,
  which returns the heading row and the panel together. It has to be a sibling of the body for the switch
  to stay usable, and keeping it in the file would have meant the file owning a widget the code must
  arrange. The file's own panel is unchanged otherwise, so the `[trigger panel:]` counts still read.
- **A guard on the sync.** Setting a `GtkSwitch` emits `notify::active` for a programmatic write just as
  it does for a click, so without `App::panel_switch_sync` the program's own write would come back as a
  click and flip the feature again.

Readings, both directions in one line:

    trigger panel sensitive before the probe toggled it: 0; … ; pane switches live while their panes are off=1
    panel switches: trigger=1/1 cursors=1/1 drove_toolbar=1  (pane switch / header bar button)

`pane switches live …=1` is the design's point and is read before anything turns either pane on; `0` would
be a switch drawn, greyed and useless. The trigger pair is moved from the *toolbar* end and the cursors
pair from the *pane* end, so the line carries both halves of the sync, and `drove_toolbar=1` is the pane's
own switch having turned the cursors on with the button following. `trigger panel sensitive …=0` is the
body, not the panel.

Verified: the two lines above; `[side-tab-probe] … advanced=4`; `make test-ui` 14 files; zero
`Gtk-CRITICAL` and no missing icon. Docs: the new reading in `probe-readings.md` with what a `0` means,
the invariants bullet for the one-control-in-two-places rule and its two traps, the sidebar bullet's
sensitive target, and the README note on why the switch is in the pane.
This commit is contained in:
Martino Ferrari committed 2026-10-14 14:13:49 +02:00
1 parent d9287ea826
commit cc8335c514
10 files changed
+201 -26

No files matched your search

+12 -2
View File
@@ -96,6 +96,12 @@ GtkWidget* build_cursors_panel(App& app) {
app.cursors_mode_drop = GTK_WIDGET(gtk_builder_get_object(ui, "mode_drop"));
app.cursors_reset = GTK_WIDGET(gtk_builder_get_object(ui, "reset"));
app.cursors_body = GTK_WIDGET(gtk_builder_get_object(ui, "body"));
app.cursors_switch = GTK_WIDGET(gtk_builder_get_object(ui, "cursors_switch"));
if (app.cursors_switch != nullptr) {
gtk_widget_set_tooltip_text(app.cursors_switch,
"the cursors, the same switch the header bar carries");
g_signal_connect(app.cursors_switch, "notify::active", G_CALLBACK(on_panel_switch_toggled), &app);
}
if (box == nullptr || app.cursors_mode_drop == nullptr || app.cursors_reset == nullptr ||
app.cursors_body == nullptr) {
// A missing id is a mistake in the `.ui`. The panel comes up empty rather than taking the window
@@ -129,15 +135,19 @@ void on_cursors_toggled(GtkToggleButton* button, gpointer user) {
const bool on = gtk_toggle_button_get_active(button);
app->rows->set_cursors_visible(on);
// Disabled, not hidden, and the same for the reset beside it: with the cursors off there is
// nothing to reset, but the button must stay where the user will look for it.
// nothing to reset, but the button must stay where the user will look for it. The *body* goes
// insensitive and not the panel, because the panel's header carries the switch that turns the
// cursors back on -- and a control inside an insensitive parent cannot be clicked.
gtk_widget_set_sensitive(app->cursors_reset, on);
if (app->cursors_panel != nullptr) gtk_widget_set_sensitive(app->cursors_panel, on);
if (app->cursors_body != nullptr) gtk_widget_set_sensitive(app->cursors_body, on);
if (!on) update_cursors_pane(*app);
if (on) {
app->rows->reset_cursors();
rebuild_cursors_pane(*app);
update_cursors_pane(*app);
}
// The pane's own switch shows the same state, whichever of the two was clicked.
sync_panel_switches(*app);
}
void on_cursors_reset(GtkButton*, gpointer user) {