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

+9
View File
@@ -38,6 +38,15 @@
<property name="css-classes">flat</property>
</object>
</child>
<child>
<!-- The pane's own switch, the same one the header bar carries: a feature whose pane is
disabled while it is off needs its control *in* the pane, or turning it back on means
finding the toolbar button that greyed it out. `body` is what goes insensitive, not the
panel, so this stays live. -->
<object class="GtkSwitch" id="cursors_switch">
<property name="valign">center</property>
</object>
</child>
</object>
</child>
<child>
+4 -8
View File
@@ -2,8 +2,10 @@
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The trigger panel's frame: the heading, one labelled row per setting, the two buttons that run it, and
the search block.
The trigger panel's frame: one labelled row per setting, the two buttons that run it, and the search
block. Its *heading* is not here: the heading row carries the switch that turns trigger mode on and
off, and this panel's body is what goes insensitive when it is off, so the heading has to be its
sibling rather than a child of it (`build_trigger_panel()` returns the two together).
Every row here is a label and a place for a control, and the control itself is built in code. A
dropdown's model and a spin button's range are the *application's* (the condition list, the level
@@ -24,12 +26,6 @@
<property name="margin-bottom">8</property>
<property name="margin-start">8</property>
<property name="margin-end">8</property>
<child>
<object class="GtkLabel" id="trigger_heading">
<property name="label" translatable="yes">Trigger</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkBox" id="signal_row">
<property name="orientation">horizontal</property>