ui: one badge per enabled signal, an N signals overflow, and the order kept in memory
This commit is contained in:
1 parent
ad5e30f338
commit
f27925da59
6 files changed
+213
-3
No files matched your search
@@ -2652,6 +2652,20 @@ the slide-over closed and the page was dropped by `signal_pages_sync()`. `pruned
|
||||
channel removed) and the slide-over was still on `trigger`, both by `slideover_page()` and by the stack's visible child; `0` is `signal_pages_sync()` parking the stack on the
|
||||
signals list while the title and the badge marker still name `trigger`; `-1` when there is no `trigger` page or no math channel could be added.
|
||||
|
||||
## `[sigbadges]`
|
||||
|
||||
```
|
||||
[sigbadges] enabled=4 badges=4 overflow=0 overflow_text=- plus_visible=1 dot_plotted=4 bar_h=36 bar_h_fixed=1 bar_min_w=336 opens=1 plus_opens=1
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_SIGNAL_BADGES_MS` reads the signal badges. `badges` must equal `enabled` (the enabled, listed signals) while `enabled` is at most 6
|
||||
(`kMaxSignalBadges`); beyond that `badges=0 overflow=1 overflow_text=<N> signals plus_visible=0` (the "N signals" badge replaces the badges and the "+"). `dot_plotted` is the number of
|
||||
badges whose signal is on a plot (its dot is then the trace colour, not the unplotted grey). `bar_h` is the bar's allocated height and `bar_h_fixed=1` that neither it nor its natural
|
||||
height moved while one signal was switched off and on again (`rebuild_signal_panels()` each time), and that the badge count followed; `0` is a bar whose height depends on its content.
|
||||
`bar_min_w` is only a record: the scroller must keep it the same however many signals there are (it reads 336 with the mock's 4 and with the overflow forced on). `opens=1`: clicking the
|
||||
first signal badge opens `signal:<name>` with the badge marked open and a second click closes it (`-1` when overflowed, there being no signal badges); `plus_opens=1` the same for the "+"
|
||||
(the overflow badge when overflowed) and the `signals` page.
|
||||
|
||||
## `[badges]`
|
||||
|
||||
```
|
||||
|
||||
@@ -451,6 +451,10 @@ precedent, not an exception.
|
||||
per-signal `signal:<name>` slide-over pages. **Against a config that has already seen the tour** (a first run's
|
||||
auto-tour closes the slide-over at about 1.5 s, which is when this probe opens its page). One `[signalpage]` line;
|
||||
see [probe-readings.md](probe-readings.md).
|
||||
- `UDPSCOPE_PROBE_SIGNAL_BADGES_MS=1500` with `UDPSCOPE_EXIT_AFTER_MS=5000` and `UDPSCOPE_ENABLE_ALL_MS=500` -- the
|
||||
signal badges in the badge bar. **Against a config that has already seen the tour**, as above (the probe clicks
|
||||
the badges, and the auto-tour would close the page). One `[sigbadges]` line; see
|
||||
[probe-readings.md](probe-readings.md).
|
||||
- `UDPSCOPE_PROBE_BADGES_MS=2000` with `UDPSCOPE_EXIT_AFTER_MS=5000` and `UDPSCOPE_ENABLE_ALL_MS=500` -- the badge
|
||||
bar under the plot. One `[badges]` line, which must read `table_bad=0 fixed=1 open_marked=1 page_open=1
|
||||
closed_unmarked=1`: `table_bad` counts the rows of the 16-input `trigger_badge_state()` table the probe
|
||||
|
||||
+7
-1
@@ -4673,7 +4673,10 @@ void run_app_action(App& app, AppAction action) {
|
||||
toggle(app.cursors_toggle);
|
||||
break;
|
||||
case AppAction::ShowMeasure: dock_show_section(app, "measure"); break;
|
||||
case AppAction::ShowSignals: slideover_toggle(app, "signals"); break;
|
||||
case AppAction::ShowSignals:
|
||||
// The `+` badge is insensitive without signals; so is its shortcut.
|
||||
if (!app.model->signals().empty()) slideover_toggle(app, "signals");
|
||||
break;
|
||||
case AppAction::LayoutEditor:
|
||||
toggle(app.layout_toggle);
|
||||
break;
|
||||
@@ -5887,6 +5890,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_BADGES_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), badges_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_SIGNAL_BADGES_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), signal_badges_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_SIGNAL_PAGES_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), signal_page_probe, &app);
|
||||
}
|
||||
|
||||
@@ -149,7 +149,11 @@ struct Badge {
|
||||
GtkWidget* name = nullptr;
|
||||
GtkWidget* state = nullptr;
|
||||
std::string page;
|
||||
std::string signal_name; ///< a signal badge: the signal it stands for; empty for the others
|
||||
std::size_t signal = 0; ///< a signal badge: its index in `ScopeModel::signals()` when it was built
|
||||
};
|
||||
/// The most signal badges the bar shows; beyond that one "N signals" badge stands for them all.
|
||||
inline constexpr std::size_t kMaxSignalBadges = 6;
|
||||
/// The badge bar under the plot (src/ui_badges.cpp).
|
||||
struct BadgeBar {
|
||||
GtkWidget* bar = nullptr; ///< main-window.ui `badge_bar`, fixed height
|
||||
@@ -157,6 +161,11 @@ struct BadgeBar {
|
||||
GtkWidget* system_slot = nullptr; ///< right: Trigger, Source
|
||||
Badge trigger;
|
||||
Badge source; ///< opens the first source page; a read-out when the source has none
|
||||
GtkWidget* signals_row = nullptr; ///< the box inside the scroller: signal badges, then the "+"
|
||||
/// One per shown enabled signal; empty when overflowed. `unique_ptr`: each button's "clicked" handler holds
|
||||
/// a pointer to its `Badge`, which must not move when the vector does.
|
||||
std::vector<std::unique_ptr<Badge>> signal_badges;
|
||||
Badge overflow; ///< the "N signals" badge, used only beyond `kMaxSignalBadges`
|
||||
Badge plus; ///< the "+" in the signals slot: opens the `signals` page
|
||||
};
|
||||
|
||||
@@ -174,6 +183,8 @@ struct App {
|
||||
BadgeBar badges;
|
||||
GtkWidget* badge_bar = nullptr;
|
||||
GtkWidget* badge_signals = nullptr;
|
||||
/// Signal names in the order the user left the badges (memory only; names not in it sort last).
|
||||
std::vector<std::string> badge_order;
|
||||
GtkWidget* badge_system = nullptr;
|
||||
/// The one GL area, drawing the `RowSet`'s figure: every row is a row of that
|
||||
/// figure, so there is one widget, one context and one `::render` whatever the row
|
||||
@@ -1952,6 +1963,9 @@ const char* trigger_badge_state(bool enabled, bool armed, bool pending, bool has
|
||||
void build_badge_bar(App& app); // after build_slideover, the toolbar (it reads trigger_toggle) and build_source_panels (the Source badge's page)
|
||||
void sync_badge_bar(App& app); // per tick and on every slide-over open/close; only writes what changed
|
||||
gboolean badges_probe(gpointer user); // UDPSCOPE_PROBE_BADGES_MS
|
||||
/// Rebuilds the signal badges (one per enabled signal, or the "N signals" overflow) from the model.
|
||||
void sync_signal_badges(App& app);
|
||||
gboolean signal_badges_probe(gpointer user); // UDPSCOPE_PROBE_SIGNAL_BADGES_MS
|
||||
gboolean source_kinds_probe_report(gpointer user);
|
||||
gboolean source_kinds_probe(gpointer user);
|
||||
/// MCC E-1608 end to end against `tools/mock_e1608.py`: the stream, the calibration, the panels and
|
||||
|
||||
+173
-2
@@ -11,6 +11,11 @@
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace udpscope {
|
||||
namespace ui_impl {
|
||||
@@ -49,6 +54,9 @@ void on_badge_clicked(GtkButton*, gpointer user) {
|
||||
slideover_close(*app);
|
||||
return;
|
||||
}
|
||||
// A signal's page is made on first use (and kept in step by `signal_pages_sync`). Nothing here rebuilds the
|
||||
// badges (the page's fill and the slide-over open/close only restyle them), so the badge outlives this call.
|
||||
if (b->page.rfind("signal:", 0) == 0 && !signal_page_ensure(*app, b->signal_name)) return;
|
||||
slideover_toggle(*app, b->page);
|
||||
}
|
||||
|
||||
@@ -70,6 +78,28 @@ void make_badge(App& app, Badge& b, GtkWidget* parent, const char* name, const s
|
||||
gtk_box_append(GTK_BOX(parent), b.button);
|
||||
}
|
||||
|
||||
/// A signal's badge: its colour dot (grey when it has no trace) and its name. Words and a dot, no icon.
|
||||
void make_signal_badge(App& app, Badge& b, const std::vector<SignalInfo>& signals, std::size_t index) {
|
||||
b.signal = index;
|
||||
b.signal_name = signals[index].name;
|
||||
b.page = "signal:" + b.signal_name;
|
||||
b.button = gtk_button_new();
|
||||
gtk_widget_add_css_class(b.button, "flat");
|
||||
gtk_widget_add_css_class(b.button, "udpscope-badge");
|
||||
GtkWidget* row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
GtkWidget* dot = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(dot), signal_chip_markup(app, signals, index).c_str());
|
||||
b.name = gtk_label_new(b.signal_name.c_str());
|
||||
gtk_label_set_ellipsize(GTK_LABEL(b.name), PANGO_ELLIPSIZE_END);
|
||||
gtk_label_set_max_width_chars(GTK_LABEL(b.name), 14);
|
||||
gtk_box_append(GTK_BOX(row), dot);
|
||||
gtk_box_append(GTK_BOX(row), b.name);
|
||||
gtk_button_set_child(GTK_BUTTON(b.button), row);
|
||||
gtk_widget_set_tooltip_text(b.button, b.signal_name.c_str());
|
||||
g_object_set_data(G_OBJECT(b.button), "udpscope-app", &app);
|
||||
g_signal_connect(b.button, "clicked", G_CALLBACK(on_badge_clicked), &b);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void build_badge_bar(App& app) {
|
||||
@@ -86,9 +116,67 @@ void build_badge_bar(App& app) {
|
||||
gtk_widget_set_can_target(bb.source.button, FALSE);
|
||||
gtk_widget_set_focusable(bb.source.button, FALSE);
|
||||
}
|
||||
// The full signal list. Insensitive until the stream has declared signals ("disable, do not hide").
|
||||
make_badge(app, bb.plus, bb.signals_slot, "+", "signals");
|
||||
// The signal badges sit in a scroller: a bar whose minimum width grew with the number of signals would
|
||||
// widen the window (layout must not depend on content). EXTERNAL scrolls by wheel/touch with no bar.
|
||||
GtkWidget* scroller = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), GTK_POLICY_EXTERNAL, GTK_POLICY_NEVER);
|
||||
gtk_widget_set_hexpand(scroller, TRUE);
|
||||
bb.signals_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), bb.signals_row);
|
||||
gtk_box_append(GTK_BOX(bb.signals_slot), scroller);
|
||||
// The full signal list. Insensitive until the stream has declared signals ("disable, do not hide"). It
|
||||
// follows the signal badges, which are inserted before it. It has no state, so its empty label is hidden
|
||||
// rather than leaving a dead gap in the pill.
|
||||
make_badge(app, bb.plus, bb.signals_row, "+", "signals");
|
||||
gtk_widget_set_visible(bb.plus.state, FALSE);
|
||||
gtk_widget_set_tooltip_text(bb.plus.button, "all signals: switch more on, add a math channel");
|
||||
sync_signal_badges(app);
|
||||
}
|
||||
|
||||
void sync_signal_badges(App& app) {
|
||||
BadgeBar& bb = app.badges;
|
||||
if (bb.signals_row == nullptr || bb.plus.button == nullptr) return;
|
||||
std::vector<SignalInfo> signals;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
signals = app.model->signals();
|
||||
}
|
||||
// The enabled signals, in the user's order: names in `badge_order` first, the rest in model order.
|
||||
std::vector<std::size_t> shown;
|
||||
for (std::size_t i : listed_signals(signals)) {
|
||||
if (app.model->signal_enabled(i)) shown.push_back(i);
|
||||
}
|
||||
const auto rank = [&](std::size_t i) {
|
||||
const auto it = std::find(app.badge_order.begin(), app.badge_order.end(), signals[i].name);
|
||||
return static_cast<std::size_t>(it - app.badge_order.begin()); // == size() when unknown: sorts last
|
||||
};
|
||||
std::stable_sort(shown.begin(), shown.end(), [&](std::size_t a, std::size_t b) { return rank(a) < rank(b); });
|
||||
|
||||
// Clear: every signal badge, and the overflow if it was there. The "+" is kept. The badges' buttons die
|
||||
// with the removal (their handlers with them), so the `Badge` records they point to can go too.
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(bb.signals_row); c != nullptr;) {
|
||||
GtkWidget* next = gtk_widget_get_next_sibling(c);
|
||||
if (c != bb.plus.button) gtk_box_remove(GTK_BOX(bb.signals_row), c);
|
||||
c = next;
|
||||
}
|
||||
bb.signal_badges.clear();
|
||||
bb.overflow = Badge{};
|
||||
|
||||
const bool overflowed = shown.size() > kMaxSignalBadges;
|
||||
if (overflowed) {
|
||||
make_badge(app, bb.overflow, bb.signals_row, "", "signals");
|
||||
gtk_widget_set_visible(bb.overflow.state, FALSE);
|
||||
gtk_label_set_text(GTK_LABEL(bb.overflow.name), (std::to_string(shown.size()) + " signals").c_str());
|
||||
} else {
|
||||
for (std::size_t i : shown) {
|
||||
bb.signal_badges.push_back(std::make_unique<Badge>());
|
||||
make_signal_badge(app, *bb.signal_badges.back(), signals, i);
|
||||
gtk_box_insert_child_after(GTK_BOX(bb.signals_row), bb.signal_badges.back()->button,
|
||||
gtk_widget_get_prev_sibling(bb.plus.button));
|
||||
}
|
||||
}
|
||||
// The overflow badge stands for the "+" (it opens the same page); the "+" is shown only when it is not.
|
||||
gtk_widget_set_visible(bb.plus.button, !overflowed);
|
||||
sync_badge_bar(app);
|
||||
}
|
||||
|
||||
@@ -107,6 +195,13 @@ void sync_badge_bar(App& app) {
|
||||
gtk_widget_set_sensitive(bb.plus.button, !app.model->signals().empty());
|
||||
set_open(bb.plus.button, slideover_page(app) == "signals");
|
||||
}
|
||||
if (bb.overflow.button != nullptr) {
|
||||
gtk_widget_set_sensitive(bb.overflow.button, !app.model->signals().empty());
|
||||
set_open(bb.overflow.button, slideover_page(app) == bb.overflow.page);
|
||||
}
|
||||
for (const std::unique_ptr<Badge>& sb : bb.signal_badges) {
|
||||
set_open(sb->button, slideover_page(app) == sb->page);
|
||||
}
|
||||
if (bb.source.button == nullptr) return;
|
||||
// The source's name and link state. A file has neither a link nor a kind worth naming.
|
||||
const bool file_mode = app.replaying || !app.model->config().open_waveform.empty();
|
||||
@@ -192,5 +287,81 @@ gboolean badges_probe(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean signal_badges_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
BadgeBar& bb = a->badges;
|
||||
const auto bar_height = [&] {
|
||||
int min_h = 0, nat_h = 0;
|
||||
gtk_widget_measure(bb.bar, GTK_ORIENTATION_VERTICAL, -1, &min_h, &nat_h, nullptr, nullptr);
|
||||
return std::make_pair(gtk_widget_get_height(bb.bar), nat_h);
|
||||
};
|
||||
const auto before = bar_height();
|
||||
int bar_min_w = 0;
|
||||
gtk_widget_measure(bb.bar, GTK_ORIENTATION_HORIZONTAL, -1, &bar_min_w, nullptr, nullptr, nullptr);
|
||||
|
||||
// The enabled, listed signals the bar must stand for.
|
||||
std::vector<SignalInfo> signals;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
signals = a->model->signals();
|
||||
}
|
||||
std::size_t enabled = 0;
|
||||
std::size_t toggle = signals.size();
|
||||
for (std::size_t i : listed_signals(signals)) {
|
||||
if (!a->model->signal_enabled(i)) continue;
|
||||
++enabled;
|
||||
if (toggle == signals.size()) toggle = i;
|
||||
}
|
||||
const bool overflowed = bb.overflow.button != nullptr;
|
||||
int dot_plotted = 0;
|
||||
for (const std::unique_ptr<Badge>& sb : bb.signal_badges) {
|
||||
if (signal_plotted(*a, sb->signal_name)) ++dot_plotted;
|
||||
}
|
||||
// A copy: the toggling below rebuilds the badges, and the label with them.
|
||||
const std::string overflow_text = overflowed ? gtk_label_get_text(GTK_LABEL(bb.overflow.name)) : "-";
|
||||
const int plus_visible = gtk_widget_get_visible(bb.plus.button) != FALSE ? 1 : 0;
|
||||
const std::size_t badges = bb.signal_badges.size();
|
||||
|
||||
// One signal off and on again: the bar's height does not move, and the badges come back.
|
||||
int bar_h_fixed = -1;
|
||||
if (toggle != signals.size()) {
|
||||
a->model->set_signal_enabled(toggle, false);
|
||||
rebuild_signal_panels(*a);
|
||||
const auto off = bar_height();
|
||||
const std::size_t badges_off = bb.signal_badges.size();
|
||||
a->model->set_signal_enabled(toggle, true);
|
||||
rebuild_signal_panels(*a);
|
||||
const auto on = bar_height();
|
||||
bar_h_fixed = before == off && before == on && (overflowed || badges_off + 1 == badges || badges == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
// A signal badge opens its page and the second click closes it; the "+" (the overflow when overflowed)
|
||||
// does the same with the list.
|
||||
int opens = -1;
|
||||
if (!bb.signal_badges.empty()) {
|
||||
Badge& first = *bb.signal_badges.front();
|
||||
const std::string want = "signal:" + first.signal_name;
|
||||
g_signal_emit_by_name(first.button, "clicked");
|
||||
const bool open1 = slideover_page(*a) == want &&
|
||||
gtk_widget_has_css_class(first.button, "udpscope-badge-open") != FALSE;
|
||||
g_signal_emit_by_name(first.button, "clicked");
|
||||
const bool closed = slideover_page(*a).empty() &&
|
||||
gtk_widget_has_css_class(first.button, "udpscope-badge-open") == FALSE;
|
||||
opens = open1 && closed ? 1 : 0;
|
||||
}
|
||||
Badge& list_badge = bb.overflow.button != nullptr ? bb.overflow : bb.plus;
|
||||
g_signal_emit_by_name(list_badge.button, "clicked");
|
||||
const bool plus1 = slideover_page(*a) == "signals";
|
||||
g_signal_emit_by_name(list_badge.button, "clicked");
|
||||
const int plus_opens = plus1 && slideover_page(*a).empty() ? 1 : 0;
|
||||
|
||||
std::fprintf(stderr,
|
||||
"[sigbadges] enabled=%zu badges=%zu overflow=%d overflow_text=%s plus_visible=%d dot_plotted=%d "
|
||||
"bar_h=%d bar_h_fixed=%d bar_min_w=%d opens=%d plus_opens=%d\n",
|
||||
enabled, badges, overflowed ? 1 : 0, overflow_text.c_str(), plus_visible, dot_plotted, before.first,
|
||||
bar_h_fixed, bar_min_w, opens, plus_opens);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
} // namespace ui_impl
|
||||
} // namespace udpscope
|
||||
@@ -602,6 +602,7 @@ void rebuild_signal_panels(App& app) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), FALSE);
|
||||
}
|
||||
signal_pages_sync(app);
|
||||
sync_signal_badges(app);
|
||||
app.signal_generation = app.model->generation();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user