feat: locked panes for a returning user, and the trigger is persisted
Two panes, two modes. **Locked** 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 (the Adwaita `emblem-system-symbolic`,
swapping to `emblem-ok-symbolic` in configuration mode) that flips its pane. A **first run** — no session
file for that source — never reaches `apply_layout()` and comes up in configuration mode, because
configuring the host is what a first run is for. The mode is a mode and not a setting, so it is
deliberately not persisted.
The locked signal list drops the rows of every signal that is off, which is the whole of "show active
signals": the list already filters on nothing, so the lock is one `continue`. With the rows go the acquire
tick, the overflow menu (Information, Calibrate…, Delete, rename) and the `MATH` divider's `+` — and the
colour chip stops being clickable, because choosing a colour is configuration too. Dragging a signal onto
a plot stays: that is which view shows what, not what the scope is set up to do.
**The trigger is persisted, settings and state both** — the settings *even while it is off*, which is the
point: a trigger configured once and switched off is still the trigger you come back to. `LayoutState`
gains fifteen flat `trigger_*` keys; the channel is stored by *name* (every rebuild renumbers the
channels, the model's own rule) and the two enumerations as **the words the panel's dropdowns show**,
because an index ties the file to a declaration order — the lesson `math_op_at()` already carries. The
words are shared with the panel now (`trigger_type_text()` and friends build the dropdown arrays), so the
two cannot drift.
Two ordering traps, both found by running it:
- **The channel name resolves twice over.** `rebuild_trigger_signals()` picks up
`App::pending_trigger_channel` when the stream arrives *after* the load, and `apply_trigger_layout()`
resolves it itself when the channels are already there — without the second path the name stayed
pending past the one rebuild that would have used it, and the trigger silently sat on channel 0
(`summary='pulse width · sine …'` where the session said `square`).
- **The trigger's state is applied by pressing the control, not by setting a flag**: the toggle is set
*outside* the `updating_panels` window `apply_layout()` uses, because a toggle set inside it emits
`toggled` into a handler that returns at once and nothing would happen — no arming, no events button,
no sensitivity.
Verified, in both modes and both directions:
panes: trigger=locked signals=locked body_visible=0 summary='window · square · 0.3…0.6 · 20 % pre'
panes: trigger=config signals=config body_visible=1 summary=''
signals panel: labels=2 list=1 rows=7 signals=2 math=4 divider=1
The locked trigger pane is 42 px tall against 580 unlocked. A hand-written session round-trips: every
value it named came back in the panel's own controls, `trigger=1/1` (the state arrived and the pane's
switch followed it), and the file the app wrote back carried all fifteen keys with the untouched ones at
their defaults. `[side-tab-probe] … advanced=4` on its documented recipe; `make test-ui` 14 files; zero
`Gtk-CRITICAL` and no missing icon.
Docs: the `panes:` reading with what each field means, the corrected `labels=` note (the panel's frame is
counted above the list now, because the heading sits in a heading row), the invariants for both the
persistence and the lock, the README's deviation note, and the removal of its claim that the trigger has
never been persisted — it is now, and the locked view is built from exactly that.
This commit is contained in:
1 parent
cc8335c514
commit
121abbefae
12 files changed
+574
-28
No files matched your search
@@ -1111,8 +1111,11 @@ assertion, being a stream stamp rebased by whichever rebuild last dropped histor
|
|||||||
`probe-readings.md`.
|
`probe-readings.md`.
|
||||||
|
|
||||||
A shape's own rows are the only ones shown; the rest are hidden, so the panel never offers a
|
A shape's own rows are the only ones shown; the rest are hidden, so the panel never offers a
|
||||||
width to a window trigger. The condition is **not** persisted in the session file — the session
|
width to a window trigger. The condition **is** persisted in the session file, together with trigger
|
||||||
has never persisted the trigger — so a restart comes up on an edge trigger.
|
mode itself: the settings are written even while the trigger is off, because a trigger set up once and
|
||||||
|
switched off is still the trigger you come back to. The signal it names is stored by *name* — every
|
||||||
|
rebuild renumbers the channels — and the shapes by the words the panel's dropdowns show, so a
|
||||||
|
reordered enumeration cannot reinterpret a file.
|
||||||
|
|
||||||
### Searching the buffer
|
### Searching the buffer
|
||||||
|
|
||||||
@@ -2397,7 +2400,11 @@ now — they are listed so they are not mistaken for anything else:
|
|||||||
cursors and the ruler for Measure, a sine crossing its trigger level and the channel list
|
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
|
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
|
32x16 pair so both motifs are legible at a glance — with the words kept as each pill's tooltip and
|
||||||
accessible name. Each pane's heading also carries its **own switch**, the same control as the
|
accessible name. 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. A **first run** (no session file for that source) comes up
|
||||||
|
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
|
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
|
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
|
design spec had the switcher showing *titles* and neither page an
|
||||||
|
|||||||
@@ -4,6 +4,21 @@ What a session persists and in what order, when a save is refused, and when an e
|
|||||||
|
|
||||||
Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to learn; read the ones touching the area you are editing before you edit it.
|
Part of [CLAUDE.md](../../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 trigger is persisted, settings and state both.** Its condition, its channel (by *name*), its
|
||||||
|
pre-trigger share, hold-off and mode, and whether trigger mode was on — written even while it is off,
|
||||||
|
because a trigger configured once and switched off is still the trigger you come back to. The two
|
||||||
|
enumerations are stored as the *words the panel's dropdowns show*, not as their indexes: an index ties
|
||||||
|
the file to a declaration order, which is the lesson `math_op_at()` carries, and the words are shared
|
||||||
|
with the panel so the two cannot drift. The channel resolves when the acquired channels exist
|
||||||
|
(`rebuild_trigger_signals()` picks up `App::pending_trigger_channel`, or `apply_trigger_layout()`
|
||||||
|
resolves it itself when they are already there) — a name that resolved to nothing would silently move
|
||||||
|
the trigger to channel 0.
|
||||||
|
- **A session that loaded is what locks the two panes.** Locked is the returning user's mode: the
|
||||||
|
trigger pane shows one compact line, the signal list only the signals that are on, and neither offers
|
||||||
|
a control that changes the configuration. A *first run* — no session file for that source — never
|
||||||
|
reaches `apply_layout()` and stays in configuration mode, which is what a first run is for. The mode
|
||||||
|
is a mode and not a setting: it is deliberately not persisted, so a scope reopened tomorrow starts
|
||||||
|
where a returning user expects to start.
|
||||||
- **The session is the layout.** `LayoutState` persists what makes a reopened scope look like the
|
- **The session is the layout.** `LayoutState` persists what makes a reopened scope look like the
|
||||||
one that was closed: the window, the rows and their slot signals, the calibration, **the enabled
|
one that was closed: the window, the rows and their slot signals, the calibration, **the enabled
|
||||||
signals**, **the chosen trace colours** (`[[color]]`, `name` + `rgb = "#RRGGBB"` — only the
|
signals**, **the chosen trace colours** (`[[color]]`, `name` + `rgb = "#RRGGBB"` — only the
|
||||||
|
|||||||
@@ -1139,6 +1139,29 @@ library measures and draws the rect in tests of its own.
|
|||||||
defect this reading exists for, and it was invisible from the frame alone: the file view pinning its
|
defect this reading exists for, and it was invisible from the frame alone: the file view pinning its
|
||||||
axis from the ring each frame is what swallowed the gesture.
|
axis from the ring each frame is what swallowed the gesture.
|
||||||
|
|
||||||
|
## `panes:` — the locked and configuration modes
|
||||||
|
|
||||||
|
```
|
||||||
|
[udpscope] panes: trigger=locked signals=locked body_visible=0 summary='window · square · 0.25…0.5 · 20 % pre'
|
||||||
|
[udpscope] panes: trigger=config signals=config body_visible=1 summary=''
|
||||||
|
```
|
||||||
|
|
||||||
|
Both panes have two modes. **Locked** is what a returning user gets — the trigger pane shows the one line
|
||||||
|
in `summary`, the signal list shows only the signals that are on, and neither offers a control that
|
||||||
|
changes the configuration — and **config** is the full pane, which is what a *first run* gets, because a
|
||||||
|
session file that loaded is what "we have been here before" means. Each heading's `[edit]` button flips
|
||||||
|
its pane.
|
||||||
|
|
||||||
|
- **`summary`** is the locked trigger pane's whole content, so it is read rather than assumed: empty here
|
||||||
|
is a locked pane showing nothing at all. It quotes the shape, the channel, the figures that shape
|
||||||
|
actually reads (a level, two levels, a width plus its comparison, or a timeout) and the pre-trigger
|
||||||
|
share — never a figure the shape ignores.
|
||||||
|
- **`body_visible=0`** is the settings being off screen, which is what makes the pane 42 px tall instead
|
||||||
|
of 580 in `panels:`.
|
||||||
|
- **The locked signal list drops the rows of the signals that are off**, so `rows` counts what is on:
|
||||||
|
`signals panel: … rows=3 signals=2 divider=1` is two on signals and the `MATH` divider, not four
|
||||||
|
signals. Its `+` goes with them — a `+` on a locked list opens the editor the lock exists to keep shut.
|
||||||
|
|
||||||
## `panel switches:` and the trigger panel's default sensitivity
|
## `panel switches:` and the trigger panel's default sensitivity
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -1330,7 +1353,8 @@ The signals panel's frame read from the window, and in the same line the *shape*
|
|||||||
the heading and the hint the `.ui` gives it, the list the rows go in, how many rows that list holds
|
the heading and the hint the `.ui` gives it, the list the rows go in, how many rows that list holds
|
||||||
(`rows`), how many of them are streamed signals (`signals`), how many math channels the app has (`math`),
|
(`rows`), how many of them are streamed signals (`signals`), how many math channels the app has (`math`),
|
||||||
and `divider`, which is 1 when `rows == signals + 1 + math` and 0 otherwise. `labels=1` is a `.ui` that lost
|
and `divider`, which is 1 when `rows == signals + 1 + math` and 0 otherwise. `labels=1` is a `.ui` that lost
|
||||||
one of the two labels, `list=0` one that lost the list -- both of which otherwise show as a panel quietly
|
one of the two labels — counted over the panel's subtree *above* the list, because the rows' own name
|
||||||
|
and unit labels are the code's and the heading sits in a heading row beside the pane's `[edit]` button — `list=0` one that lost the list -- both of which otherwise show as a panel quietly
|
||||||
missing a piece -- and **`divider=0` is the merged list that lost its `+`**, the one control that can create
|
missing a piece -- and **`divider=0` is the merged list that lost its `+`**, the one control that can create
|
||||||
a math channel. The first line above is a run with no math channels at all: the divider is there anyway,
|
a math channel. The first line above is a run with no math channels at all: the divider is there anyway,
|
||||||
because a `+` that only appeared once a channel existed could never create the first one. `panels: …
|
because a `+` that only appeared once a channel existed could never create the first one. `panels: …
|
||||||
|
|||||||
@@ -476,6 +476,48 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
|||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// One `trigger_*` key, by its name after the prefix. Text for the two enumerations and for the
|
||||||
|
/// signal, numbers for the rest; `value` arrives unquoted.
|
||||||
|
const auto trigger_key = [](LayoutState::Trigger& t, const std::string& k, const std::string& v) {
|
||||||
|
const auto num = [&](double& out) {
|
||||||
|
try {
|
||||||
|
out = std::stod(v);
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (k == "enabled") {
|
||||||
|
t.enabled = v == "true";
|
||||||
|
} else if (k == "signal") {
|
||||||
|
t.channel = v;
|
||||||
|
} else if (k == "type") {
|
||||||
|
t.type = v;
|
||||||
|
} else if (k == "edge") {
|
||||||
|
t.edge = v;
|
||||||
|
} else if (k == "cmp") {
|
||||||
|
t.cmp = v;
|
||||||
|
} else if (k == "mode") {
|
||||||
|
t.mode = v;
|
||||||
|
} else if (k == "level") {
|
||||||
|
num(t.level);
|
||||||
|
t.level_given = true;
|
||||||
|
} else if (k == "level2") {
|
||||||
|
num(t.level2);
|
||||||
|
} else if (k == "width") {
|
||||||
|
num(t.width_s);
|
||||||
|
} else if (k == "width2") {
|
||||||
|
num(t.width2_s);
|
||||||
|
} else if (k == "timeout") {
|
||||||
|
num(t.timeout_s);
|
||||||
|
} else if (k == "pre") {
|
||||||
|
num(t.pretrigger_pct);
|
||||||
|
} else if (k == "holdoff") {
|
||||||
|
num(t.holdoff_s);
|
||||||
|
} else if (k == "band") {
|
||||||
|
num(t.noise_band);
|
||||||
|
} else if (k == "hf") {
|
||||||
|
num(t.hf_fc);
|
||||||
|
}
|
||||||
|
};
|
||||||
if (section == Section::Top) {
|
if (section == Section::Top) {
|
||||||
if (key == "window_s") {
|
if (key == "window_s") {
|
||||||
try {
|
try {
|
||||||
@@ -498,6 +540,8 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
|||||||
st.measure_stats = value == "true";
|
st.measure_stats = value == "true";
|
||||||
} else if (key == "side_tab") {
|
} else if (key == "side_tab") {
|
||||||
st.side_tab = strip_quote(value);
|
st.side_tab = strip_quote(value);
|
||||||
|
} else if (key.rfind("trigger_", 0) == 0) {
|
||||||
|
trigger_key(st.trigger, key.substr(8), strip_quote(value));
|
||||||
} else if (key == "multicast") {
|
} else if (key == "multicast") {
|
||||||
st.multicast = strip_quote(value);
|
st.multicast = strip_quote(value);
|
||||||
} else if (key == "interface") {
|
} else if (key == "interface") {
|
||||||
@@ -648,6 +692,24 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
|||||||
}
|
}
|
||||||
o << "measure_stats = " << (state.measure_stats ? "true" : "false") << "\n";
|
o << "measure_stats = " << (state.measure_stats ? "true" : "false") << "\n";
|
||||||
o << "side_tab = \"" << state.side_tab << "\"\n";
|
o << "side_tab = \"" << state.side_tab << "\"\n";
|
||||||
|
// The trigger, settings and state both. Written even when it is off: that is the point of it.
|
||||||
|
o << "trigger_enabled = " << (state.trigger.enabled ? "true" : "false") << "\n";
|
||||||
|
if (!state.trigger.channel.empty()) {
|
||||||
|
o << "trigger_signal = " << toml_escape(state.trigger.channel) << "\n";
|
||||||
|
}
|
||||||
|
o << "trigger_type = " << toml_escape(state.trigger.type) << "\n";
|
||||||
|
o << "trigger_edge = " << toml_escape(state.trigger.edge) << "\n";
|
||||||
|
o << "trigger_cmp = " << toml_escape(state.trigger.cmp) << "\n";
|
||||||
|
o << "trigger_mode = " << toml_escape(state.trigger.mode) << "\n";
|
||||||
|
o << "trigger_level = " << state.trigger.level << "\n";
|
||||||
|
o << "trigger_level2 = " << state.trigger.level2 << "\n";
|
||||||
|
o << "trigger_width = " << state.trigger.width_s << "\n";
|
||||||
|
o << "trigger_width2 = " << state.trigger.width2_s << "\n";
|
||||||
|
o << "trigger_timeout = " << state.trigger.timeout_s << "\n";
|
||||||
|
o << "trigger_pre = " << state.trigger.pretrigger_pct << "\n";
|
||||||
|
o << "trigger_holdoff = " << state.trigger.holdoff_s << "\n";
|
||||||
|
o << "trigger_band = " << state.trigger.noise_band << "\n";
|
||||||
|
o << "trigger_hf = " << state.trigger.hf_fc << "\n";
|
||||||
if (!state.comment.empty()) o << "comment = " << toml_escape(state.comment) << "\n";
|
if (!state.comment.empty()) o << "comment = " << toml_escape(state.comment) << "\n";
|
||||||
if (state.buffer_mb > 0.0) o << "buffer_mb = " << state.buffer_mb << "\n";
|
if (state.buffer_mb > 0.0) o << "buffer_mb = " << state.buffer_mb << "\n";
|
||||||
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
|
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
|
||||||
|
|||||||
@@ -73,6 +73,36 @@ struct LayoutState {
|
|||||||
/// The measurements pane, by signal *name* and with the type and options a row was built with.
|
/// The measurements pane, by signal *name* and with the type and options a row was built with.
|
||||||
std::vector<Measure> measures;
|
std::vector<Measure> measures;
|
||||||
|
|
||||||
|
/// The trigger: its settings *and* whether trigger mode was on. Both, and the settings even while
|
||||||
|
/// it is off, because a trigger set up once and switched off is still the trigger you come back to
|
||||||
|
/// -- and the pane's locked view is built from exactly this.
|
||||||
|
///
|
||||||
|
/// The signal and the two enumerations are stored as *text*: the signal by name, because every
|
||||||
|
/// rebuild renumbers the channels (the model's own rule), and the shapes by the words the panel's
|
||||||
|
/// dropdowns show, because an index would tie this file to a declaration order -- which the math
|
||||||
|
/// channel's op index already taught this store once.
|
||||||
|
struct Trigger {
|
||||||
|
bool enabled = false;
|
||||||
|
std::string channel; ///< the acquired channel's name; empty is "none chosen"
|
||||||
|
std::string type = "edge"; ///< edge | pulse width | window | dropout
|
||||||
|
std::string edge = "rising"; ///< rising | falling | both
|
||||||
|
std::string cmp = "greater"; ///< pulse width: `<` | `>` | within | outside
|
||||||
|
std::string mode = "normal"; ///< normal | single
|
||||||
|
double level = 0.0;
|
||||||
|
double level2 = 0.0; ///< window: the band's other edge
|
||||||
|
double width_s = 1.0e-3;
|
||||||
|
double width2_s = 2.0e-3;
|
||||||
|
double timeout_s = 1.0e-3;
|
||||||
|
double pretrigger_pct = 50.0;
|
||||||
|
double holdoff_s = 0.010;
|
||||||
|
double noise_band = 0.0;
|
||||||
|
double hf_fc = 0.0;
|
||||||
|
/// Whether the file said anything about the level. A session that did keeps it; one that did not
|
||||||
|
/// leaves the panel's own "follow the channel's range" default alone.
|
||||||
|
bool level_given = false;
|
||||||
|
};
|
||||||
|
Trigger trigger;
|
||||||
|
|
||||||
/// Free text the user attached to this configuration: what it is for, what to look at first.
|
/// Free text the user attached to this configuration: what it is for, what to look at first.
|
||||||
/// A configuration is a *named* session, and the name alone rarely says what makes it different
|
/// A configuration is a *named* session, and the name alone rarely says what makes it different
|
||||||
/// from its neighbour.
|
/// from its neighbour.
|
||||||
|
|||||||
+64
-5
@@ -982,6 +982,18 @@ void rebuild_trigger_signals(App& app) {
|
|||||||
g_object_unref(list);
|
g_object_unref(list);
|
||||||
|
|
||||||
TriggerConfig& tc = app.model->trigger();
|
TriggerConfig& tc = app.model->trigger();
|
||||||
|
// A session stores the trigger's channel by *name*: resolved here, where the acquired channels
|
||||||
|
// exist. The pending name is cleared either way, so a name this stream does not have falls through
|
||||||
|
// to the default below rather than being retried on every rebuild.
|
||||||
|
if (!app.pending_trigger_channel.empty()) {
|
||||||
|
for (std::size_t c = 0; c < channels.size(); ++c) {
|
||||||
|
if (channels[c].name == app.pending_trigger_channel) {
|
||||||
|
tc.channel = static_cast<int>(c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.pending_trigger_channel.clear();
|
||||||
|
}
|
||||||
if (tc.channel < 0 && !channels.empty()) tc.channel = 0;
|
if (tc.channel < 0 && !channels.empty()) tc.channel = 0;
|
||||||
const int sel = tc.channel;
|
const int sel = tc.channel;
|
||||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_signal_drop),
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_signal_drop),
|
||||||
@@ -4388,6 +4400,39 @@ void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user) {
|
|||||||
sync_panel_switches(*app);
|
sync_panel_switches(*app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Both panes' modes, applied together: the trigger's body against its summary line, the two [edit]
|
||||||
|
/// buttons against what they would do, and the signal list rebuilt because the lock decides what a row
|
||||||
|
/// carries. Idempotent, so a caller only flips a flag and calls this.
|
||||||
|
void apply_pane_lock(App& app) {
|
||||||
|
if (app.trigger_body != nullptr) gtk_widget_set_visible(app.trigger_body, !app.trigger_locked);
|
||||||
|
if (app.trigger_summary != nullptr) {
|
||||||
|
gtk_widget_set_visible(app.trigger_summary, app.trigger_locked);
|
||||||
|
}
|
||||||
|
const auto edit_button = [](GtkWidget* button, bool locked, const char* what) {
|
||||||
|
if (button == nullptr) return;
|
||||||
|
gtk_button_set_icon_name(GTK_BUTTON(button),
|
||||||
|
locked ? "emblem-system-symbolic" : "emblem-ok-symbolic");
|
||||||
|
const std::string tip = std::string(locked ? "configure " : "keep this configuration of ") + what;
|
||||||
|
gtk_widget_set_tooltip_text(button, tip.c_str());
|
||||||
|
};
|
||||||
|
edit_button(app.trigger_edit, app.trigger_locked, "the trigger");
|
||||||
|
edit_button(app.signals_edit, app.signals_locked, "the signals");
|
||||||
|
sync_trigger_summary(app);
|
||||||
|
// The list, because the lock is what decides whether a row carries a tick, a menu and the `+`.
|
||||||
|
if (app.signal_list != nullptr) rebuild_signal_panels(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Either pane's [edit] button: one handler, because the two differ only in which flag they flip.
|
||||||
|
void on_pane_edit_clicked(GtkButton* button, gpointer user) {
|
||||||
|
App* app = static_cast<App*>(user);
|
||||||
|
if (GTK_WIDGET(button) == app->trigger_edit) {
|
||||||
|
app->trigger_locked = !app->trigger_locked;
|
||||||
|
} else if (GTK_WIDGET(button) == app->signals_edit) {
|
||||||
|
app->signals_locked = !app->signals_locked;
|
||||||
|
}
|
||||||
|
apply_pane_lock(*app);
|
||||||
|
}
|
||||||
|
|
||||||
void run_app_action(App& app, AppAction action) {
|
void run_app_action(App& app, AppAction action) {
|
||||||
const auto toggle = [](GtkWidget* button) {
|
const auto toggle = [](GtkWidget* button) {
|
||||||
if (button == nullptr) return;
|
if (button == nullptr) return;
|
||||||
@@ -5677,13 +5722,23 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
// list the file gives it. A `.ui` that lost one of them reads here -- `labels=1` or `list=0` -- rather
|
// list the file gives it. A `.ui` that lost one of them reads here -- `labels=1` or `list=0` -- rather
|
||||||
// than showing as a panel quietly missing its hint.
|
// than showing as a panel quietly missing its hint.
|
||||||
GtkWidget* panel = app.signal_list != nullptr ? gtk_widget_get_parent(app.signal_list) : nullptr;
|
GtkWidget* panel = app.signal_list != nullptr ? gtk_widget_get_parent(app.signal_list) : nullptr;
|
||||||
|
// Counted over the panel's *subtree*, not its direct children: the heading lives in a heading row
|
||||||
|
// beside the pane's [edit] button, and a count of children would read 1 whatever the file lost.
|
||||||
int labels = 0;
|
int labels = 0;
|
||||||
int lists = 0;
|
int lists = 0;
|
||||||
for (GtkWidget* c = panel != nullptr ? gtk_widget_get_first_child(panel) : nullptr; c != nullptr;
|
const std::function<void(GtkWidget*)> count = [&](GtkWidget* w) {
|
||||||
c = gtk_widget_get_next_sibling(c)) {
|
// The list stops the walk: its rows are the code's, and this reading is about the file's frame.
|
||||||
if (GTK_IS_LABEL(c)) ++labels;
|
if (GTK_IS_LIST_BOX(w)) {
|
||||||
if (GTK_IS_LIST_BOX(c)) ++lists;
|
++lists;
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
if (GTK_IS_LABEL(w)) ++labels;
|
||||||
|
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
|
||||||
|
c = gtk_widget_get_next_sibling(c)) {
|
||||||
|
count(c);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (panel != nullptr) count(panel);
|
||||||
// The list's shape, beside the frame: `rows` is what the list holds, `signals` the streamed
|
// The list's shape, beside the frame: `rows` is what the list holds, `signals` the streamed
|
||||||
// ones, and the `MATH` divider plus the math channels account for the rest -- so
|
// ones, and the `MATH` divider plus the math channels account for the rest -- so
|
||||||
// `rows == signals + 1 + math` is the invariant, and `divider=0` is a list that lost its `+`.
|
// `rows == signals + 1 + math` is the invariant, and `divider=0` is a list that lost its `+`.
|
||||||
@@ -5720,6 +5775,10 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
app.dbg_trigger_sensitive, app.dbg_switcher_visible,
|
app.dbg_trigger_sensitive, app.dbg_switcher_visible,
|
||||||
rows.cursors_visible() ? 1 : 0, rows.time_window(),
|
rows.cursors_visible() ? 1 : 0, rows.time_window(),
|
||||||
app.window_limit_s);
|
app.window_limit_s);
|
||||||
|
std::printf("[udpscope] panes: trigger=%s signals=%s body_visible=%d summary='%s'\n",
|
||||||
|
app.dbg_trigger_locked ? "locked" : "config",
|
||||||
|
app.dbg_signals_locked ? "locked" : "config", app.dbg_trigger_body_visible,
|
||||||
|
app.dbg_trigger_summary.c_str());
|
||||||
std::printf("[udpscope] panel switches: trigger=%d/%d cursors=%d/%d drove_toolbar=%d "
|
std::printf("[udpscope] panel switches: trigger=%d/%d cursors=%d/%d drove_toolbar=%d "
|
||||||
"(pane switch / header bar button; `drove_toolbar` is the pane's own switch having "
|
"(pane switch / header bar button; `drove_toolbar` is the pane's own switch having "
|
||||||
"moved the button)\n",
|
"moved the button)\n",
|
||||||
|
|||||||
+21
-3
@@ -21,9 +21,27 @@
|
|||||||
<property name="margin-start">8</property>
|
<property name="margin-start">8</property>
|
||||||
<property name="margin-end">8</property>
|
<property name="margin-end">8</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="signals_heading">
|
<!-- The heading row, with the button that switches this pane between its two modes: locked, where
|
||||||
<property name="label" translatable="yes">Signals</property>
|
the list shows the signals that are on and nothing else, and configuration, which is the list
|
||||||
<property name="halign">start</property>
|
with every control on it. It is the code that swaps the icon and the tooltip, because the mode
|
||||||
|
is the code's. -->
|
||||||
|
<object class="GtkBox" id="signals_head">
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="spacing">8</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="signals_heading">
|
||||||
|
<property name="label" translatable="yes">Signals</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="hexpand">1</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="signals_edit">
|
||||||
|
<property name="icon-name">emblem-system-symbolic</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="css-classes">flat</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
|||||||
+40
-1
@@ -258,6 +258,26 @@ struct App {
|
|||||||
/// True while `sync_panel_switches()` is writing the switches: setting a `GtkSwitch` emits its own
|
/// True while `sync_panel_switches()` is writing the switches: setting a `GtkSwitch` emits its own
|
||||||
/// `notify::active`, and without this the program's own write would come back as a click.
|
/// `notify::active`, and without this the program's own write would come back as a click.
|
||||||
bool panel_switch_sync = false;
|
bool panel_switch_sync = false;
|
||||||
|
/// The channel a session's trigger names, until `rebuild_trigger_signals()` can resolve it: the
|
||||||
|
/// file stores a *name* and the trigger needs an index into the acquired channels, which do not
|
||||||
|
/// exist until the stream has been described. Empty means "nothing pending".
|
||||||
|
std::string pending_trigger_channel;
|
||||||
|
/// The two panes' modes. *Locked* is the one a returning user gets: the trigger pane shows a line
|
||||||
|
/// that says what it is set to, the signal list shows only the signals that are on, and neither
|
||||||
|
/// offers a control that changes the configuration. A first run -- no session file for this source
|
||||||
|
/// -- starts in configuration mode, because that is what a first run is for. The flags last for the
|
||||||
|
/// session: a lock is a mode, not a setting, so it is not persisted.
|
||||||
|
bool trigger_locked = false;
|
||||||
|
bool signals_locked = false;
|
||||||
|
GtkWidget* trigger_summary = nullptr;
|
||||||
|
GtkWidget* trigger_edit = nullptr;
|
||||||
|
GtkWidget* signals_edit = nullptr;
|
||||||
|
int dbg_trigger_locked = -1;
|
||||||
|
int dbg_signals_locked = -1;
|
||||||
|
int dbg_trigger_body_visible = -1;
|
||||||
|
/// The compact line the locked trigger pane shows. It is the pane's whole content in that mode, so it
|
||||||
|
/// is read rather than assumed: empty here is a locked pane showing nothing at all.
|
||||||
|
std::string dbg_trigger_summary;
|
||||||
/// The cursors pane: listing what the instruments read -- T0/T1/dT always, per row an Y1/Y2/dY
|
/// The cursors pane: listing what the instruments read -- T0/T1/dT always, per row an Y1/Y2/dY
|
||||||
/// where rulers are up, and in `mix` mode the value each cursor crosses each trace at.
|
/// where rulers are up, and in `mix` mode the value each cursor crosses each trace at.
|
||||||
GtkWidget* cursors_mode_drop = nullptr;
|
GtkWidget* cursors_mode_drop = nullptr;
|
||||||
@@ -1279,7 +1299,7 @@ void on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user);
|
|||||||
void open_signal_color(App& app, std::size_t signal);
|
void open_signal_color(App& app, std::size_t signal);
|
||||||
void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals);
|
void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals);
|
||||||
/// The `MATH` divider row in the one channel list, and the `+` it carries.
|
/// The `MATH` divider row in the one channel list, and the `+` it carries.
|
||||||
GtkWidget* make_math_divider(App& app);
|
GtkWidget* make_math_divider(App& app, bool with_add);
|
||||||
void on_math_add_clicked(GtkButton* button, gpointer user);
|
void on_math_add_clicked(GtkButton* button, gpointer user);
|
||||||
/// The index in `App::math` of the channel a synthetic signal is named after, or `-1`.
|
/// The index in `App::math` of the channel a synthetic signal is named after, or `-1`.
|
||||||
int math_index_for_signal(App& app, const std::string& name);
|
int math_index_for_signal(App& app, const std::string& name);
|
||||||
@@ -1413,6 +1433,25 @@ void select_side_tab(App& app, const char* name);
|
|||||||
/// Reads each pane's own switch back off the header bar's button, and the other way round: the two are
|
/// Reads each pane's own switch back off the header bar's button, and the other way round: the two are
|
||||||
/// one control in two places, and `panel_switch_sync` keeps the write from looking like a click.
|
/// one control in two places, and `panel_switch_sync` keeps the write from looking like a click.
|
||||||
void sync_panel_switches(App& app);
|
void sync_panel_switches(App& app);
|
||||||
|
/// The trigger's four enumerations as words: the panel's dropdown labels and the session file's
|
||||||
|
/// vocabulary, in one place so the two cannot drift.
|
||||||
|
const char* trigger_type_text(TriggerType t);
|
||||||
|
TriggerType trigger_type_from_text(const std::string& s);
|
||||||
|
const char* trigger_edge_text(TriggerEdge e);
|
||||||
|
TriggerEdge trigger_edge_from_text(const std::string& s);
|
||||||
|
const char* width_cmp_text(WidthCmp c);
|
||||||
|
WidthCmp width_cmp_from_text(const std::string& s);
|
||||||
|
const char* trigger_mode_text(TriggerMode m);
|
||||||
|
TriggerMode trigger_mode_from_text(const std::string& s);
|
||||||
|
/// The panel's controls from the model, and the session's trigger into both.
|
||||||
|
void write_trigger_widgets(App& app);
|
||||||
|
/// The compact line the trigger pane shows while it is locked, and the lock itself: the panes'
|
||||||
|
/// bodies, their [edit] buttons and the signal list's rows all follow from `trigger_locked` and
|
||||||
|
/// `signals_locked`.
|
||||||
|
void sync_trigger_summary(App& app);
|
||||||
|
void apply_pane_lock(App& app);
|
||||||
|
void on_pane_edit_clicked(GtkButton* button, gpointer user);
|
||||||
|
void apply_trigger_layout(App& app, const LayoutState::Trigger& t);
|
||||||
void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user);
|
void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user);
|
||||||
void update_side_visibility(App& app);
|
void update_side_visibility(App& app);
|
||||||
void on_sidebar_toggled(GtkToggleButton* button, gpointer user);
|
void on_sidebar_toggled(GtkToggleButton* button, gpointer user);
|
||||||
|
|||||||
@@ -59,6 +59,30 @@ LayoutState collect_layout(App& app) {
|
|||||||
st.measures = app.measures;
|
st.measures = app.measures;
|
||||||
st.measure_stats = app.measure_stats_on;
|
st.measure_stats = app.measure_stats_on;
|
||||||
st.side_tab = app.side_tab;
|
st.side_tab = app.side_tab;
|
||||||
|
// The trigger, its settings *and* whether it was on. The channel goes by name, like every identity
|
||||||
|
// in this file: every rebuild renumbers the channels.
|
||||||
|
{
|
||||||
|
const TriggerConfig& tc = app.model->trigger();
|
||||||
|
const std::vector<ChannelInfo>& channels = app.model->channels();
|
||||||
|
st.trigger.enabled = tc.enabled;
|
||||||
|
if (tc.channel >= 0 && tc.channel < static_cast<int>(channels.size())) {
|
||||||
|
st.trigger.channel = channels[static_cast<std::size_t>(tc.channel)].name;
|
||||||
|
}
|
||||||
|
st.trigger.type = trigger_type_text(tc.condition.type);
|
||||||
|
st.trigger.edge = trigger_edge_text(tc.condition.edge);
|
||||||
|
st.trigger.cmp = width_cmp_text(tc.condition.cmp);
|
||||||
|
st.trigger.mode = trigger_mode_text(tc.mode);
|
||||||
|
st.trigger.level = tc.condition.level;
|
||||||
|
st.trigger.level2 = tc.condition.level2;
|
||||||
|
st.trigger.width_s = tc.condition.width_s;
|
||||||
|
st.trigger.width2_s = tc.condition.width2_s;
|
||||||
|
st.trigger.timeout_s = tc.condition.timeout_s;
|
||||||
|
st.trigger.pretrigger_pct = tc.pretrigger_pct;
|
||||||
|
st.trigger.holdoff_s = tc.holdoff_s;
|
||||||
|
st.trigger.noise_band = tc.condition.noise_band;
|
||||||
|
st.trigger.hf_fc = tc.condition.hf_fc;
|
||||||
|
st.trigger.level_given = true; // written means given, so a load keeps it
|
||||||
|
}
|
||||||
// The connection is the session's too: the file's name carries only host and port.
|
// The connection is the session's too: the file's name carries only host and port.
|
||||||
st.multicast = app.source_multicast;
|
st.multicast = app.source_multicast;
|
||||||
st.interface_addr = app.source_interface;
|
st.interface_addr = app.source_interface;
|
||||||
@@ -289,6 +313,56 @@ void schedule_layout_save(App& app) {
|
|||||||
/// Applies a loaded layout: rows, slot assignments, calibration, window. Names
|
/// Applies a loaded layout: rows, slot assignments, calibration, window. Names
|
||||||
/// that no longer exist in the stream are skipped rather than freeing their slot
|
/// that no longer exist in the stream are skipped rather than freeing their slot
|
||||||
/// (a stream that renamed a signal must not blank the user's layout).
|
/// (a stream that renamed a signal must not blank the user's layout).
|
||||||
|
/// The session's trigger into the model and the panel, called from `apply_layout()`. The *channel* it
|
||||||
|
/// names is resolved where the channels exist -- `rebuild_trigger_signals()` picks up
|
||||||
|
/// `App::pending_trigger_channel` -- because at this point the stream may not have described its
|
||||||
|
/// signals yet, and a name that resolves to nothing would silently move the trigger to channel 0.
|
||||||
|
void apply_trigger_layout(App& app, const LayoutState::Trigger& t) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||||
|
TriggerConfig& tc = app.model->trigger();
|
||||||
|
tc.condition.type = trigger_type_from_text(t.type);
|
||||||
|
tc.condition.edge = trigger_edge_from_text(t.edge);
|
||||||
|
tc.condition.cmp = width_cmp_from_text(t.cmp);
|
||||||
|
tc.mode = trigger_mode_from_text(t.mode);
|
||||||
|
tc.condition.level = t.level;
|
||||||
|
tc.condition.level2 = t.level2;
|
||||||
|
tc.condition.width_s = t.width_s;
|
||||||
|
tc.condition.width2_s = t.width2_s;
|
||||||
|
tc.condition.timeout_s = t.timeout_s;
|
||||||
|
tc.condition.noise_band = t.noise_band;
|
||||||
|
tc.condition.hf_fc = t.hf_fc;
|
||||||
|
tc.pretrigger_pct = t.pretrigger_pct;
|
||||||
|
tc.holdoff_s = t.holdoff_s;
|
||||||
|
}
|
||||||
|
// A file that carried a level keeps it: the panel's own default follows the channel's range, and
|
||||||
|
// `trigger_level_touched` is what says the user has a level of their own.
|
||||||
|
if (t.level_given) app.trigger_level_touched = true;
|
||||||
|
app.pending_trigger_channel = t.channel;
|
||||||
|
{
|
||||||
|
// If the stream has already described its channels, resolve the name *now*: the signals rebuild that
|
||||||
|
// would otherwise do it has already been and gone, so a pending name would leave the trigger on
|
||||||
|
// channel 0 -- which is a trigger on whichever signal happens to be first.
|
||||||
|
const std::vector<ChannelInfo>& channels = app.model->channels();
|
||||||
|
for (std::size_t c = 0; c < channels.size(); ++c) {
|
||||||
|
if (channels[c].name != app.pending_trigger_channel) continue;
|
||||||
|
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||||
|
app.model->trigger().channel = static_cast<int>(c);
|
||||||
|
app.pending_trigger_channel.clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write_trigger_widgets(app);
|
||||||
|
// The state is applied by *pressing the same control a user would*: the handler is what arms, tells
|
||||||
|
// the events button, greys the body and syncs the pane's own switch. Deliberately outside the
|
||||||
|
// `updating_panels` window `apply_layout()` uses for the panels, because a toggle set inside it
|
||||||
|
// would emit `toggled` into a handler that returns at once and nothing would happen.
|
||||||
|
if (app.trigger_toggle != nullptr) {
|
||||||
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), t.enabled);
|
||||||
|
}
|
||||||
|
sync_panel_switches(app);
|
||||||
|
}
|
||||||
|
|
||||||
void apply_layout(App& app, const LayoutState& st) {
|
void apply_layout(App& app, const LayoutState& st) {
|
||||||
ScopeModel& model = *app.model;
|
ScopeModel& model = *app.model;
|
||||||
const auto& signals = model.signals();
|
const auto& signals = model.signals();
|
||||||
@@ -410,6 +484,17 @@ void apply_layout(App& app, const LayoutState& st) {
|
|||||||
if (app.area != nullptr) app.area->set_figure(&app.rows->figure());
|
if (app.area != nullptr) app.area->set_figure(&app.rows->figure());
|
||||||
app.relayout_pending = true;
|
app.relayout_pending = true;
|
||||||
rebuild_signal_panels(app);
|
rebuild_signal_panels(app);
|
||||||
|
// The trigger last: its channel names an *acquired* channel, so it is resolved once the enables
|
||||||
|
// above have built the channel list.
|
||||||
|
apply_trigger_layout(app, st.trigger);
|
||||||
|
|
||||||
|
// A session that loaded is a user who has been here before, and that is what the lock means: the two
|
||||||
|
// panes come up showing what they are set to rather than showing every control. A first run -- no
|
||||||
|
// file for this source -- never reaches this function and stays in configuration mode, which is what
|
||||||
|
// a first run is for.
|
||||||
|
app.trigger_locked = true;
|
||||||
|
app.signals_locked = true;
|
||||||
|
apply_pane_lock(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
void on_layout_row_remove(GtkButton* button, gpointer user) {
|
void on_layout_row_remove(GtkButton* button, gpointer user) {
|
||||||
|
|||||||
@@ -2322,6 +2322,13 @@ gboolean exit_probe_step1(gpointer user) {
|
|||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
// Record the panel states before the toggles are exercised.
|
// Record the panel states before the toggles are exercised.
|
||||||
a->dbg_trigger_sensitive_default = gtk_widget_get_sensitive(a->trigger_body) ? 1 : 0;
|
a->dbg_trigger_sensitive_default = gtk_widget_get_sensitive(a->trigger_body) ? 1 : 0;
|
||||||
|
a->dbg_trigger_locked = a->trigger_locked ? 1 : 0;
|
||||||
|
a->dbg_signals_locked = a->signals_locked ? 1 : 0;
|
||||||
|
a->dbg_trigger_body_visible =
|
||||||
|
a->trigger_body != nullptr && gtk_widget_get_visible(a->trigger_body) ? 1 : 0;
|
||||||
|
if (a->trigger_summary != nullptr) {
|
||||||
|
a->dbg_trigger_summary = gtk_label_get_text(GTK_LABEL(a->trigger_summary));
|
||||||
|
}
|
||||||
a->dbg_panel_switch_live =
|
a->dbg_panel_switch_live =
|
||||||
a->trigger_switch != nullptr && gtk_widget_get_sensitive(a->trigger_switch) &&
|
a->trigger_switch != nullptr && gtk_widget_get_sensitive(a->trigger_switch) &&
|
||||||
a->cursors_switch != nullptr && gtk_widget_get_sensitive(a->cursors_switch)
|
a->cursors_switch != nullptr && gtk_widget_get_sensitive(a->cursors_switch)
|
||||||
|
|||||||
+29
-11
@@ -166,7 +166,7 @@ int math_index_for_signal(App& app, const std::string& name) {
|
|||||||
/// The `MATH` boundary in the one channel list, carrying the `+` that creates a channel. Not
|
/// The `MATH` boundary in the one channel list, carrying the `+` that creates a channel. Not
|
||||||
/// selectable and not activatable: it is a heading, and a heading that highlights under the
|
/// selectable and not activatable: it is a heading, and a heading that highlights under the
|
||||||
/// pointer reads as a row you can plot.
|
/// pointer reads as a row you can plot.
|
||||||
GtkWidget* make_math_divider(App& app) {
|
GtkWidget* make_math_divider(App& app, bool with_add) {
|
||||||
GtkWidget* row = gtk_list_box_row_new();
|
GtkWidget* row = gtk_list_box_row_new();
|
||||||
gtk_list_box_row_set_selectable(GTK_LIST_BOX_ROW(row), FALSE);
|
gtk_list_box_row_set_selectable(GTK_LIST_BOX_ROW(row), FALSE);
|
||||||
gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(row), FALSE);
|
gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(row), FALSE);
|
||||||
@@ -180,11 +180,16 @@ GtkWidget* make_math_divider(App& app) {
|
|||||||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||||
gtk_widget_set_hexpand(label, TRUE);
|
gtk_widget_set_hexpand(label, TRUE);
|
||||||
gtk_box_append(GTK_BOX(box), label);
|
gtk_box_append(GTK_BOX(box), label);
|
||||||
GtkWidget* add = gtk_button_new_from_icon_name("list-add-symbolic");
|
// Locked: the heading stays, because the list it divides is the one being read, but the `+` that
|
||||||
gtk_widget_add_css_class(add, "flat");
|
// creates a channel does not. A `+` on a locked list is a button that opens the editor the lock
|
||||||
gtk_widget_set_tooltip_text(add, "add a math channel");
|
// exists to keep shut.
|
||||||
g_signal_connect(add, "clicked", G_CALLBACK(on_math_add_clicked), &app);
|
if (with_add) {
|
||||||
gtk_box_append(GTK_BOX(box), add);
|
GtkWidget* add = gtk_button_new_from_icon_name("list-add-symbolic");
|
||||||
|
gtk_widget_add_css_class(add, "flat");
|
||||||
|
gtk_widget_set_tooltip_text(add, "add a math channel");
|
||||||
|
g_signal_connect(add, "clicked", G_CALLBACK(on_math_add_clicked), &app);
|
||||||
|
gtk_box_append(GTK_BOX(box), add);
|
||||||
|
}
|
||||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
|
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
@@ -278,6 +283,9 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
|||||||
std::size_t plain = 0;
|
std::size_t plain = 0;
|
||||||
for (std::size_t i = 0; i < signals.size(); ++i) {
|
for (std::size_t i = 0; i < signals.size(); ++i) {
|
||||||
if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not plottable
|
if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not plottable
|
||||||
|
// Locked: a row per signal that is *on*, which is the list a user reads rather than the list they
|
||||||
|
// configure. Turning one on is what the pane's [edit] button is for.
|
||||||
|
if (app.signals_locked && !app.model->signal_enabled(i)) continue;
|
||||||
const std::string base = layer_base_name(signals[i].name);
|
const std::string base = layer_base_name(signals[i].name);
|
||||||
bool seen = false;
|
bool seen = false;
|
||||||
for (std::size_t j = 0; j < i && !seen; ++j) {
|
for (std::size_t j = 0; j < i && !seen; ++j) {
|
||||||
@@ -293,7 +301,7 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
|||||||
// `+` that creates the first one.
|
// `+` that creates the first one.
|
||||||
if (sig.math && !math_divider_done) {
|
if (sig.math && !math_divider_done) {
|
||||||
math_divider_done = true;
|
math_divider_done = true;
|
||||||
gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app));
|
gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app, !app.signals_locked));
|
||||||
}
|
}
|
||||||
if (!sig.math) ++plain;
|
if (!sig.math) ++plain;
|
||||||
GtkWidget* row = gtk_list_box_row_new();
|
GtkWidget* row = gtk_list_box_row_new();
|
||||||
@@ -309,7 +317,7 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
|||||||
chip, !app.model->signal_enabled(i) ? "not acquired"
|
chip, !app.model->signal_enabled(i) ? "not acquired"
|
||||||
: plotted ? "the colour this signal is plotted in -- click to change it"
|
: plotted ? "the colour this signal is plotted in -- click to change it"
|
||||||
: "on no plot, so there is no trace to colour");
|
: "on no plot, so there is no trace to colour");
|
||||||
if (plotted) {
|
if (plotted && !app.signals_locked) {
|
||||||
// A gesture on the label rather than a `GtkButton` around it: a button brings its own
|
// A gesture on the label rather than a `GtkButton` around it: a button brings its own
|
||||||
// padding, and padding on this row is what made the list twice as tall once already (see
|
// padding, and padding on this row is what made the list twice as tall once already (see
|
||||||
// this function's note). The chip also has to stay exactly one disc wide, or the names
|
// this function's note). The chip also has to stay exactly one disc wide, or the names
|
||||||
@@ -343,7 +351,9 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
|||||||
gtk_widget_set_visible(enable, sig.math || app.waveform_name.empty());
|
gtk_widget_set_visible(enable, sig.math || app.waveform_name.empty());
|
||||||
g_object_set_data(G_OBJECT(enable), "udpscope-signal", GSIZE_TO_POINTER(i));
|
g_object_set_data(G_OBJECT(enable), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||||
g_signal_connect(enable, "notify::active", G_CALLBACK(on_signal_switch), &app);
|
g_signal_connect(enable, "notify::active", G_CALLBACK(on_signal_switch), &app);
|
||||||
gtk_box_append(GTK_BOX(box), enable);
|
// Locked: the switch decides what is acquired, so it is a configuration control and it goes with
|
||||||
|
// the rest of them.
|
||||||
|
if (!app.signals_locked) gtk_box_append(GTK_BOX(box), enable);
|
||||||
|
|
||||||
GtkWidget* name = gtk_label_new(sig.name.c_str());
|
GtkWidget* name = gtk_label_new(sig.name.c_str());
|
||||||
gtk_widget_set_hexpand(name, true);
|
gtk_widget_set_hexpand(name, true);
|
||||||
@@ -435,7 +445,9 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
|||||||
gtk_menu_button_set_popover(GTK_MENU_BUTTON(more), popover);
|
gtk_menu_button_set_popover(GTK_MENU_BUTTON(more), popover);
|
||||||
gtk_widget_add_css_class(more, "flat");
|
gtk_widget_add_css_class(more, "flat");
|
||||||
gtk_widget_set_tooltip_text(more, "information and calibration");
|
gtk_widget_set_tooltip_text(more, "information and calibration");
|
||||||
gtk_box_append(GTK_BOX(box), more);
|
// Locked: the menu holds calibration, deletion and a rename, which are exactly what this mode is
|
||||||
|
// not for. The popover is left unattached, so nothing can reach it.
|
||||||
|
if (!app.signals_locked) gtk_box_append(GTK_BOX(box), more);
|
||||||
|
|
||||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
|
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
|
||||||
|
|
||||||
@@ -451,7 +463,9 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
|||||||
}
|
}
|
||||||
// Appended when there are no math channels at all: the `+` is the only way to create the first
|
// Appended when there are no math channels at all: the `+` is the only way to create the first
|
||||||
// one, so it cannot be conditional on one already existing.
|
// one, so it cannot be conditional on one already existing.
|
||||||
if (!math_divider_done) gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app));
|
if (!math_divider_done) {
|
||||||
|
gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app, !app.signals_locked));
|
||||||
|
}
|
||||||
app.dbg_signal_plain = plain;
|
app.dbg_signal_plain = plain;
|
||||||
app.dbg_signal_rows = 0;
|
app.dbg_signal_rows = 0;
|
||||||
for (GtkWidget* c = gtk_widget_get_first_child(list); c != nullptr;
|
for (GtkWidget* c = gtk_widget_get_first_child(list); c != nullptr;
|
||||||
@@ -567,6 +581,10 @@ GtkWidget* build_signal_panel(App& app) {
|
|||||||
GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/signals-panel.ui");
|
GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/signals-panel.ui");
|
||||||
GtkWidget* box = GTK_WIDGET(gtk_builder_get_object(ui, "signal_panel"));
|
GtkWidget* box = GTK_WIDGET(gtk_builder_get_object(ui, "signal_panel"));
|
||||||
app.signal_list = GTK_WIDGET(gtk_builder_get_object(ui, "signal_list"));
|
app.signal_list = GTK_WIDGET(gtk_builder_get_object(ui, "signal_list"));
|
||||||
|
app.signals_edit = GTK_WIDGET(gtk_builder_get_object(ui, "signals_edit"));
|
||||||
|
if (app.signals_edit != nullptr) {
|
||||||
|
g_signal_connect(app.signals_edit, "clicked", G_CALLBACK(on_pane_edit_clicked), &app);
|
||||||
|
}
|
||||||
if (box == nullptr || app.signal_list == nullptr) {
|
if (box == nullptr || app.signal_list == nullptr) {
|
||||||
// A missing id is a mistake in the `.ui`: the panel comes up empty rather than taking the window with
|
// A missing id is a mistake in the `.ui`: the panel comes up empty rather than taking the window with
|
||||||
// it, and `[udpscope] signals panel:` reads the difference.
|
// it, and `[udpscope] signals panel:` reads the difference.
|
||||||
|
|||||||
+186
-4
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
#include "ui_app.hpp"
|
#include "ui_app.hpp"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -40,6 +42,164 @@ void on_arm_clicked(GtkButton* button, gpointer user) {
|
|||||||
|
|
||||||
/// The trigger type dropdown's order, as a table rather than a cast: the dropdown lists
|
/// The trigger type dropdown's order, as a table rather than a cast: the dropdown lists
|
||||||
/// only the conditions that are implemented, so its index is not `TriggerType`'s value.
|
/// only the conditions that are implemented, so its index is not `TriggerType`'s value.
|
||||||
|
/// The trigger's four enumerations as words, and back. These are the panel's dropdown labels *and*
|
||||||
|
/// what the session file stores: an index would tie the file to a declaration order, which is the
|
||||||
|
/// lesson `math_op_at()` already carries, and two vocabularies (one for the UI, one for the file)
|
||||||
|
/// drift apart the moment one of them gains a member.
|
||||||
|
const char* trigger_type_text(TriggerType t) {
|
||||||
|
switch (t) {
|
||||||
|
case TriggerType::Edge: return "edge";
|
||||||
|
case TriggerType::PulseWidth: return "pulse width";
|
||||||
|
case TriggerType::Window: return "window";
|
||||||
|
case TriggerType::Dropout: return "dropout";
|
||||||
|
}
|
||||||
|
return "edge";
|
||||||
|
}
|
||||||
|
TriggerType trigger_type_from_text(const std::string& s) {
|
||||||
|
if (s == "pulse width") return TriggerType::PulseWidth;
|
||||||
|
if (s == "window") return TriggerType::Window;
|
||||||
|
if (s == "dropout") return TriggerType::Dropout;
|
||||||
|
return TriggerType::Edge;
|
||||||
|
}
|
||||||
|
const char* trigger_edge_text(TriggerEdge e) {
|
||||||
|
switch (e) {
|
||||||
|
case TriggerEdge::Rising: return "rising";
|
||||||
|
case TriggerEdge::Falling: return "falling";
|
||||||
|
case TriggerEdge::Both: return "both";
|
||||||
|
}
|
||||||
|
return "rising";
|
||||||
|
}
|
||||||
|
TriggerEdge trigger_edge_from_text(const std::string& s) {
|
||||||
|
if (s == "falling") return TriggerEdge::Falling;
|
||||||
|
if (s == "both") return TriggerEdge::Both;
|
||||||
|
return TriggerEdge::Rising;
|
||||||
|
}
|
||||||
|
const char* width_cmp_text(WidthCmp c) {
|
||||||
|
switch (c) {
|
||||||
|
case WidthCmp::Less: return "<";
|
||||||
|
case WidthCmp::Greater: return ">";
|
||||||
|
case WidthCmp::Within: return "within";
|
||||||
|
case WidthCmp::Outside: return "outside";
|
||||||
|
}
|
||||||
|
return ">";
|
||||||
|
}
|
||||||
|
WidthCmp width_cmp_from_text(const std::string& s) {
|
||||||
|
if (s == "<") return WidthCmp::Less;
|
||||||
|
if (s == "within") return WidthCmp::Within;
|
||||||
|
if (s == "outside") return WidthCmp::Outside;
|
||||||
|
return WidthCmp::Greater;
|
||||||
|
}
|
||||||
|
const char* trigger_mode_text(TriggerMode m) {
|
||||||
|
return m == TriggerMode::Single ? "single" : "normal";
|
||||||
|
}
|
||||||
|
TriggerMode trigger_mode_from_text(const std::string& s) {
|
||||||
|
return s == "single" ? TriggerMode::Single : TriggerMode::Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The dropdown's row for a piece of text, or 0. Asked of the widget's own model, so the mapping from
|
||||||
|
/// a stored word to a selection is the list the user sees and not a second table to keep in step.
|
||||||
|
static guint dropdown_index_of(GtkWidget* drop, const char* text) {
|
||||||
|
GListModel* model = drop != nullptr ? gtk_drop_down_get_model(GTK_DROP_DOWN(drop)) : nullptr;
|
||||||
|
const guint n = model != nullptr ? g_list_model_get_n_items(model) : 0;
|
||||||
|
for (guint i = 0; i < n; ++i) {
|
||||||
|
GtkStringObject* item = GTK_STRING_OBJECT(g_list_model_get_item(model, i));
|
||||||
|
const bool hit = item != nullptr && std::strcmp(gtk_string_object_get_string(item), text) == 0;
|
||||||
|
if (item != nullptr) g_object_unref(item);
|
||||||
|
if (hit) return i;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The panel's controls from the model: what a session load has just put in the model, so the pane
|
||||||
|
/// shows the trigger it is about to run. Guarded, because a control's own handler publishes what it
|
||||||
|
/// holds and this is the other direction.
|
||||||
|
void write_trigger_widgets(App& app) {
|
||||||
|
if (app.trigger_level_spin == nullptr) return;
|
||||||
|
TriggerConfig tc;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||||
|
tc = app.model->trigger();
|
||||||
|
}
|
||||||
|
const bool was = app.updating_panels;
|
||||||
|
app.updating_panels = true;
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_level_spin), tc.condition.level);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_pre_spin), tc.pretrigger_pct);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_holdoff_spin), tc.holdoff_s);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_width_spin), tc.condition.width_s);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_width2_spin), tc.condition.width2_s);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_level2_spin), tc.condition.level2);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_timeout_spin), tc.condition.timeout_s);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_band_spin), tc.condition.noise_band);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_hf_spin), tc.condition.hf_fc);
|
||||||
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_type_drop),
|
||||||
|
dropdown_index_of(app.trigger_type_drop,
|
||||||
|
trigger_type_text(tc.condition.type)));
|
||||||
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_edge_drop),
|
||||||
|
dropdown_index_of(app.trigger_edge_drop,
|
||||||
|
trigger_edge_text(tc.condition.edge)));
|
||||||
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_cmp_drop),
|
||||||
|
dropdown_index_of(app.trigger_cmp_drop,
|
||||||
|
width_cmp_text(tc.condition.cmp)));
|
||||||
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_mode_drop),
|
||||||
|
dropdown_index_of(app.trigger_mode_drop,
|
||||||
|
trigger_mode_text(tc.mode)));
|
||||||
|
app.updating_panels = was;
|
||||||
|
sync_trigger_rows(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The compact line: what the pane is set to, in the words the panel's own controls use. Read from the
|
||||||
|
/// model rather than from the widgets, because the widgets are not on screen in this mode and are not
|
||||||
|
/// rebuilt when a session loads.
|
||||||
|
void sync_trigger_summary(App& app) {
|
||||||
|
if (app.trigger_summary == nullptr) return;
|
||||||
|
TriggerConfig tc;
|
||||||
|
std::string channel;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||||
|
tc = app.model->trigger();
|
||||||
|
const std::vector<ChannelInfo>& channels = app.model->channels();
|
||||||
|
if (tc.channel >= 0 && tc.channel < static_cast<int>(channels.size())) {
|
||||||
|
channel = channels[static_cast<std::size_t>(tc.channel)].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char buf[192];
|
||||||
|
std::string text = trigger_type_text(tc.condition.type);
|
||||||
|
text += " · ";
|
||||||
|
text += channel.empty() ? "no channel" : channel;
|
||||||
|
switch (tc.condition.type) {
|
||||||
|
case TriggerType::PulseWidth:
|
||||||
|
std::snprintf(buf, sizeof(buf), " · %s %.6g ms", width_cmp_text(tc.condition.cmp),
|
||||||
|
tc.condition.width_s * 1000.0);
|
||||||
|
text += buf;
|
||||||
|
break;
|
||||||
|
case TriggerType::Window:
|
||||||
|
std::snprintf(buf, sizeof(buf), " · %.6g…%.6g", tc.condition.level, tc.condition.level2);
|
||||||
|
text += buf;
|
||||||
|
break;
|
||||||
|
case TriggerType::Dropout:
|
||||||
|
std::snprintf(buf, sizeof(buf), " · %.6g ms", tc.condition.timeout_s * 1000.0);
|
||||||
|
text += buf;
|
||||||
|
break;
|
||||||
|
case TriggerType::Edge:
|
||||||
|
std::snprintf(buf, sizeof(buf), " · level %.6g", tc.condition.level);
|
||||||
|
text += buf;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!(tc.condition.noise_band > 0.0) || tc.condition.type == TriggerType::Dropout) {
|
||||||
|
// A band is not offered for a dropout, so it is not quoted for one either.
|
||||||
|
} else {
|
||||||
|
std::snprintf(buf, sizeof(buf), " · band %.6g", tc.condition.noise_band);
|
||||||
|
text += buf;
|
||||||
|
}
|
||||||
|
if (tc.condition.hf_fc > 0.0) {
|
||||||
|
std::snprintf(buf, sizeof(buf), " · HF %.6g Hz", tc.condition.hf_fc);
|
||||||
|
text += buf;
|
||||||
|
}
|
||||||
|
std::snprintf(buf, sizeof(buf), " · %.6g %% pre", tc.pretrigger_pct);
|
||||||
|
text += buf;
|
||||||
|
gtk_label_set_text(GTK_LABEL(app.trigger_summary), text.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
TriggerType trigger_type_at(guint i) {
|
TriggerType trigger_type_at(guint i) {
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -248,17 +408,24 @@ GtkWidget* build_trigger_panel(App& app) {
|
|||||||
G_CALLBACK(on_trigger_signal_changed), &app);
|
G_CALLBACK(on_trigger_signal_changed), &app);
|
||||||
row_for(ui, "signal_row", app.trigger_signal_drop);
|
row_for(ui, "signal_row", app.trigger_signal_drop);
|
||||||
|
|
||||||
const char* types[] = {"edge", "pulse width", "window", "dropout", nullptr};
|
const char* types[] = {trigger_type_text(TriggerType::Edge),
|
||||||
|
trigger_type_text(TriggerType::PulseWidth),
|
||||||
|
trigger_type_text(TriggerType::Window),
|
||||||
|
trigger_type_text(TriggerType::Dropout), nullptr};
|
||||||
app.trigger_type_drop = gtk_drop_down_new_from_strings(types);
|
app.trigger_type_drop = gtk_drop_down_new_from_strings(types);
|
||||||
gtk_widget_set_tooltip_text(app.trigger_type_drop,
|
gtk_widget_set_tooltip_text(app.trigger_type_drop,
|
||||||
"what counts as an event; the fields below change to suit it");
|
"what counts as an event; the fields below change to suit it");
|
||||||
row_for(ui, "condition_row", app.trigger_type_drop);
|
row_for(ui, "condition_row", app.trigger_type_drop);
|
||||||
|
|
||||||
const char* edges[] = {"rising", "falling", "both", nullptr};
|
const char* edges[] = {trigger_edge_text(TriggerEdge::Rising),
|
||||||
|
trigger_edge_text(TriggerEdge::Falling),
|
||||||
|
trigger_edge_text(TriggerEdge::Both), nullptr};
|
||||||
app.trigger_edge_drop = gtk_drop_down_new_from_strings(edges);
|
app.trigger_edge_drop = gtk_drop_down_new_from_strings(edges);
|
||||||
row_for(ui, "edge_row", app.trigger_edge_drop);
|
row_for(ui, "edge_row", app.trigger_edge_drop);
|
||||||
|
|
||||||
const char* cmps[] = {"<", ">", "within", "outside", nullptr};
|
const char* cmps[] = {width_cmp_text(WidthCmp::Less), width_cmp_text(WidthCmp::Greater),
|
||||||
|
width_cmp_text(WidthCmp::Within), width_cmp_text(WidthCmp::Outside),
|
||||||
|
nullptr};
|
||||||
app.trigger_cmp_drop = gtk_drop_down_new_from_strings(cmps);
|
app.trigger_cmp_drop = gtk_drop_down_new_from_strings(cmps);
|
||||||
gtk_widget_set_tooltip_text(app.trigger_cmp_drop,
|
gtk_widget_set_tooltip_text(app.trigger_cmp_drop,
|
||||||
"how the measured pulse width is compared: '<' is the glitch "
|
"how the measured pulse width is compared: '<' is the glitch "
|
||||||
@@ -307,7 +474,8 @@ GtkWidget* build_trigger_panel(App& app) {
|
|||||||
app.trigger_hf_spin = spin(0.0, 1.0e7, 1.0, 3, 0.0);
|
app.trigger_hf_spin = spin(0.0, 1.0e7, 1.0, 3, 0.0);
|
||||||
row_for(ui, "hf_row", app.trigger_hf_spin);
|
row_for(ui, "hf_row", app.trigger_hf_spin);
|
||||||
|
|
||||||
const char* modes[] = {"normal", "single", nullptr};
|
const char* modes[] = {trigger_mode_text(TriggerMode::Normal),
|
||||||
|
trigger_mode_text(TriggerMode::Single), nullptr};
|
||||||
app.trigger_mode_drop = gtk_drop_down_new_from_strings(modes);
|
app.trigger_mode_drop = gtk_drop_down_new_from_strings(modes);
|
||||||
row_for(ui, "mode_row", app.trigger_mode_drop);
|
row_for(ui, "mode_row", app.trigger_mode_drop);
|
||||||
|
|
||||||
@@ -440,6 +608,20 @@ GtkWidget* build_trigger_panel(App& app) {
|
|||||||
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
||||||
gtk_widget_set_hexpand(title, TRUE);
|
gtk_widget_set_hexpand(title, TRUE);
|
||||||
gtk_box_append(GTK_BOX(head), title);
|
gtk_box_append(GTK_BOX(head), title);
|
||||||
|
// What the pane is set to, for the mode where its settings are not on screen.
|
||||||
|
app.trigger_summary = gtk_label_new("");
|
||||||
|
gtk_widget_add_css_class(app.trigger_summary, "dim-label");
|
||||||
|
gtk_label_set_ellipsize(GTK_LABEL(app.trigger_summary), PANGO_ELLIPSIZE_END);
|
||||||
|
gtk_widget_set_halign(app.trigger_summary, GTK_ALIGN_START);
|
||||||
|
gtk_widget_set_hexpand(app.trigger_summary, TRUE);
|
||||||
|
gtk_widget_set_visible(app.trigger_summary, FALSE);
|
||||||
|
gtk_box_append(GTK_BOX(head), app.trigger_summary);
|
||||||
|
app.trigger_edit = gtk_button_new_from_icon_name("emblem-system-symbolic");
|
||||||
|
gtk_widget_add_css_class(app.trigger_edit, "flat");
|
||||||
|
gtk_widget_set_valign(app.trigger_edit, GTK_ALIGN_CENTER);
|
||||||
|
gtk_widget_set_tooltip_text(app.trigger_edit, "configure the trigger");
|
||||||
|
g_signal_connect(app.trigger_edit, "clicked", G_CALLBACK(on_pane_edit_clicked), &app);
|
||||||
|
gtk_box_append(GTK_BOX(head), app.trigger_edit);
|
||||||
app.trigger_switch = gtk_switch_new();
|
app.trigger_switch = gtk_switch_new();
|
||||||
gtk_widget_set_valign(app.trigger_switch, GTK_ALIGN_CENTER);
|
gtk_widget_set_valign(app.trigger_switch, GTK_ALIGN_CENTER);
|
||||||
gtk_widget_set_tooltip_text(app.trigger_switch,
|
gtk_widget_set_tooltip_text(app.trigger_switch,
|
||||||
|
|||||||
Reference in new issue
Block a user