feat: a signal's chip is clickable, so a trace's colour is the user's to choose
Clicking the coloured chip in the signal list opens a GtkColorDialog and the colour it returns overrides that signal's palette entry. Held by name, like every other identity here, and persisted as a session `[[color]]` table. RowSet::signal_color() becomes the one resolver: install() builds the trace through it and all three chip sites read it, because a chip exists to say which trace a row is about and an override is exactly the state in which two routes to a colour would disagree. A grey chip names no trace and so is not clickable, and the way back is `Default colour` in the signal's menu -- a setting with no undo is a trap. The gesture sits on the existing label rather than in a GtkButton: padding on this row is what made the signal list twice as tall once. It claims its sequence so a click on the chip cannot become the start of a row drag. UDPSCOPE_PROBE_COLOR_MS reads the result out of plot::Plot::trace() and out of the chip's own markup, with an override in no palette slot, so neither leg can pass by asking the resolver what it already said. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
8bc1fe8b5c
commit
4003084648
11 files changed
+406
-24
No files matched your search
@@ -22,6 +22,9 @@ the overflow menu opens the editor where the unit and the calibration live.
|
||||
• A signal's colour follows the <i>signal</i>, not the slot, so the same signal on two rows
|
||||
is one colour and the side panel's chip identifies it unambiguously. A signal on no row
|
||||
gets a grey chip rather than none, so the panel's rows keep their height.
|
||||
• <b>Click a coloured chip to change that signal's colour.</b> A grey chip names no trace, so
|
||||
it is not clickable. The colour is saved with the session; <b>Default colour</b> in the
|
||||
signal's ⋯ menu gives the palette entry back.
|
||||
|
||||
<b>Example</b>
|
||||
|
||||
|
||||
+27
-1
@@ -239,13 +239,14 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
st.window_s = 0.0;
|
||||
st.rows.clear();
|
||||
st.calibration.clear();
|
||||
st.colors.clear();
|
||||
st.enabled.clear();
|
||||
st.math.clear();
|
||||
st.measures.clear();
|
||||
|
||||
std::string line;
|
||||
// What the last `[[...]]` header opened, so keys land in the right record.
|
||||
enum class Section { Top, Plot, Calibration, Math, Measure } section = Section::Top;
|
||||
enum class Section { Top, Plot, Calibration, Color, Math, Measure } section = Section::Top;
|
||||
int plot_count = 0;
|
||||
bool plot_section_seen = false;
|
||||
while (std::getline(in, line)) {
|
||||
@@ -259,6 +260,9 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
} else if (t == "[[calibration]]") {
|
||||
section = Section::Calibration;
|
||||
st.calibration.emplace_back();
|
||||
} else if (t == "[[color]]") {
|
||||
section = Section::Color;
|
||||
st.colors.emplace_back();
|
||||
} else if (t == "[[math]]") {
|
||||
section = Section::Math;
|
||||
st.math.emplace_back();
|
||||
@@ -378,6 +382,23 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
} else if (section == Section::Color && !st.colors.empty()) {
|
||||
if (key == "name") {
|
||||
st.colors.back().name = strip_quote(value);
|
||||
} else if (key == "rgb") {
|
||||
// `"#RRGGBB"`, so the file says what it means to a reader hand-editing it -- a bare decimal
|
||||
// would be the one number in this file nobody could check by eye. Anything unparseable
|
||||
// leaves the entry at 0, and `apply_layout()` drops an entry with no name anyway.
|
||||
const std::string hex = strip_quote(value);
|
||||
const std::size_t at = hex.find('#');
|
||||
try {
|
||||
st.colors.back().rgb =
|
||||
static_cast<std::uint32_t>(std::stoul(
|
||||
at == std::string::npos ? hex : hex.substr(at + 1), nullptr, 16)) &
|
||||
0x00FFFFFFu;
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!plot_section_seen && plot_count > 0) {
|
||||
@@ -422,6 +443,11 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
||||
o << "\n[[calibration]]\nname = " << toml_escape(cal.name) << "\ngain = " << cal.gain
|
||||
<< "\noffset = " << cal.offset << "\n";
|
||||
}
|
||||
for (const auto& color : state.colors) {
|
||||
char rgb[16];
|
||||
std::snprintf(rgb, sizeof(rgb), "\"#%06X\"", color.rgb & 0x00FFFFFFu);
|
||||
o << "\n[[color]]\nname = " << toml_escape(color.name) << "\nrgb = " << rgb << "\n";
|
||||
}
|
||||
for (const MathChannel& m : state.math) {
|
||||
o << "\n[[math]]\nname = " << toml_escape(m.name) << "\nunit = " << toml_escape(m.unit)
|
||||
<< "\nenabled = " << (m.enabled ? "true" : "false") << "\noutput = " << m.output
|
||||
|
||||
@@ -32,6 +32,15 @@ struct LayoutState {
|
||||
};
|
||||
std::vector<Calibration> calibration;
|
||||
|
||||
/// The trace colours the user chose, `0xRRGGBB`. Only the *overrides*: a signal with no entry
|
||||
/// takes the positional palette entry, which is what keeps a stream's new signals coloured
|
||||
/// without a session having to name them. Saved by name like every identity here.
|
||||
struct Color {
|
||||
std::string name;
|
||||
std::uint32_t rgb = 0;
|
||||
};
|
||||
std::vector<Color> colors;
|
||||
|
||||
/// Signals the session had enabled. Enabling is what starts acquisition, so this is what makes a
|
||||
/// reopened scope look like the one that was closed -- and it is saved by *name*, like every other
|
||||
/// identity here, because a CONFIG renumbers the channels.
|
||||
|
||||
+36
-2
@@ -69,6 +69,40 @@ plot::Color RowSet::palette_color(int index) {
|
||||
|
||||
plot::Color RowSet::unplotted_color() { return plot::Color::hex(kUnplottedColor); }
|
||||
|
||||
plot::Color RowSet::signal_color(const std::string& signal, int palette_index) const {
|
||||
const auto it = signal_colors_.find(signal);
|
||||
if (it != signal_colors_.end()) return plot::Color::hex(it->second);
|
||||
return palette_color(palette_index);
|
||||
}
|
||||
|
||||
void RowSet::mark_signal_dirty(const std::string& signal) {
|
||||
for (int i = 0; i < kMaxRows; ++i) {
|
||||
const Row& r = row(i);
|
||||
for (int s = 0; s < kSlotsPerRow; ++s) {
|
||||
if (r.signal_name[static_cast<std::size_t>(s)] != signal) continue;
|
||||
row_dirty_[static_cast<std::size_t>(i)] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RowSet::set_signal_color(const std::string& signal, plot::u32 rgb) {
|
||||
if (signal.empty()) return;
|
||||
// Masked to 24 bits: the palette is `0xRRGGBB` and `Color::hex()` reads it as such, so an alpha
|
||||
// byte left in the high octet by a caller converting from a `GdkRGBA` would be silently ignored
|
||||
// by the plot and silently written to the session file.
|
||||
const plot::u32 want = rgb & 0x00FFFFFFu;
|
||||
const auto it = signal_colors_.find(signal);
|
||||
if (it != signal_colors_.end() && it->second == want) return;
|
||||
signal_colors_[signal] = want;
|
||||
mark_signal_dirty(signal);
|
||||
}
|
||||
|
||||
void RowSet::clear_signal_color(const std::string& signal) {
|
||||
if (signal_colors_.erase(signal) == 0) return;
|
||||
mark_signal_dirty(signal);
|
||||
}
|
||||
|
||||
RowSet::RowSet() { rebuild_all(); }
|
||||
|
||||
void RowSet::set_row_count(int rows) {
|
||||
@@ -130,8 +164,8 @@ void RowSet::install(Row& r, int /*row_index*/, plot::Plot& p) {
|
||||
{"", {-1.0, 1.0}, true, plot::AxisPlace::Left, false, plot::TickFormat::Si});
|
||||
for (int s = 0; s < kSlotsPerRow; ++s) {
|
||||
const std::size_t i = static_cast<std::size_t>(s);
|
||||
r.plot->add_trace(
|
||||
{&r.slots[i], palette_color(r.color_index[i]), 1.2f, r.axis_x, r.axis_y, r.name[i]});
|
||||
r.plot->add_trace({&r.slots[i], signal_color(r.signal_name[i], r.color_index[i]), 1.2f,
|
||||
r.axis_x, r.axis_y, r.name[i]});
|
||||
}
|
||||
r.cursor_a = r.plot->add_cursor(0.0, plot::Color::hex(kCursorAColor));
|
||||
r.cursor_b = r.plot->add_cursor(0.0, plot::Color::hex(kCursorBColor));
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <plot/plot/plot.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
@@ -399,7 +400,30 @@ class RowSet {
|
||||
/// names beside it shifted sideways as signals were placed.
|
||||
static plot::Color unplotted_color();
|
||||
|
||||
/// The colour a signal is drawn in: the one the user chose for it if they chose one, the
|
||||
/// positional palette entry otherwise.
|
||||
///
|
||||
/// **The one resolver**, used by `install()` for the trace and by the panels for their chips.
|
||||
/// A chip exists to say which trace on screen a panel row is about, so a chip that resolved
|
||||
/// a colour by a different route than the trace is the single failure the chip cannot
|
||||
/// survive -- and an override is exactly the state in which two routes would disagree.
|
||||
plot::Color signal_color(const std::string& signal, int palette_index) const;
|
||||
/// Gives `signal` the colour `rgb` (0xRRGGBB), by *name*: a palette entry is positional, so an
|
||||
/// override held by index would move to another signal on the next CONFIG. Marks every row that
|
||||
/// plots it dirty, because a trace's colour is fixed at `add_trace()` -- a new colour is a new
|
||||
/// figure, which is the same path `assign()` already takes.
|
||||
void set_signal_color(const std::string& signal, plot::u32 rgb);
|
||||
/// Back to the palette entry. A no-op, and no rebuild, when there was no override.
|
||||
void clear_signal_color(const std::string& signal);
|
||||
/// Every override, for the session file. Ordered by name, so a saved session's colour
|
||||
/// tables do not reshuffle between writes.
|
||||
const std::map<std::string, plot::u32>& signal_colors() const noexcept { return signal_colors_; }
|
||||
|
||||
private:
|
||||
/// Marks every row with `signal` in a slot for rebuild. Not `assign()`: nothing about what
|
||||
/// the slot *holds* changed, only the colour its trace has to be built with.
|
||||
void mark_signal_dirty(const std::string& signal);
|
||||
|
||||
/// Moves one row's *persistent* state -- what it plots, in which colours, where its
|
||||
/// cursors are -- onto another row. Deliberately not a `Row` assignment: a `Row` also
|
||||
/// carries the figure handles (`plot`, the axis and cursor ids) and the `SlotSeries`
|
||||
@@ -440,6 +464,10 @@ class RowSet {
|
||||
double right_gutter_px_ = 0.0;
|
||||
bool y_gutter_reserved_ = false;
|
||||
std::array<Row, static_cast<std::size_t>(kMaxRows)> storage_{};
|
||||
/// The colours the user chose, by signal name (see `set_signal_color()`). Sparse on purpose:
|
||||
/// an absent entry means "the palette decides", which is what keeps a new CONFIG's signals
|
||||
/// coloured without anything having to seed them.
|
||||
std::map<std::string, plot::u32> signal_colors_;
|
||||
CursorMode cursor_mode_ = CursorMode::Vertical;
|
||||
bool cursors_on_ = false;
|
||||
bool relative_x_ = false;
|
||||
|
||||
+265
-18
@@ -719,6 +719,11 @@ LayoutState collect_layout(App& app) {
|
||||
if (model.signals()[si].math || !on[si]) continue;
|
||||
st.enabled.push_back(model.signals()[si].name);
|
||||
}
|
||||
// Only the signals the user actually re-coloured: the rest take the positional palette, and
|
||||
// writing those out would freeze a colour that is meant to follow the signal list.
|
||||
for (const auto& [name, rgb] : app.rows->signal_colors()) {
|
||||
st.colors.push_back({name, rgb});
|
||||
}
|
||||
// The app's own lists are the pane's source of truth for both of these.
|
||||
st.math = app.math;
|
||||
st.measures = app.measures;
|
||||
@@ -822,6 +827,16 @@ void apply_layout(App& app, const LayoutState& st) {
|
||||
}
|
||||
}
|
||||
|
||||
// Skipped for a name the stream no longer carries, like the calibration above: an override the
|
||||
// signal list cannot show is one `collect_layout()` would write back every session for ever.
|
||||
for (const auto& color : st.colors) {
|
||||
for (std::size_t si = 0; si < signals.size(); ++si) {
|
||||
if (signals[si].name != color.name) continue;
|
||||
app.rows->set_signal_color(color.name, color.rgb);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (st.window_s > 0.0) apply_time_window(app, st.window_s);
|
||||
|
||||
// set_row_count() rebuilt the figure (its row count is fixed at construction):
|
||||
@@ -1735,14 +1750,33 @@ int signal_color_index(const std::vector<SignalInfo>& signals, std::size_t signa
|
||||
return index;
|
||||
}
|
||||
|
||||
std::string chip_markup(plot::Color c) {
|
||||
char buf[64];
|
||||
std::snprintf(buf, sizeof(buf), "<span foreground=\"#%02X%02X%02X\">\u25CF</span>",
|
||||
unsigned(c.r * 255.0f + 0.5f), unsigned(c.g * 255.0f + 0.5f),
|
||||
unsigned(c.b * 255.0f + 0.5f));
|
||||
/// A trace colour as `#RRGGBB`: what a chip is drawn with and what the session file stores. One
|
||||
/// formatter for both, so a chip and the colour it claims to name cannot be written differently --
|
||||
/// and so the probe can read a chip's own markup rather than recomputing what it should say.
|
||||
std::string color_hex(plot::Color c) {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", unsigned(c.r * 255.0f + 0.5f),
|
||||
unsigned(c.g * 255.0f + 0.5f), unsigned(c.b * 255.0f + 0.5f));
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string chip_markup(plot::Color c) {
|
||||
return "<span foreground=\"" + color_hex(c) + "\">\u25CF</span>";
|
||||
}
|
||||
|
||||
/// Whether any row's slot holds `name` -- i.e. whether the signal has a trace on screen, which is
|
||||
/// what decides between its own colour and the grey of an unplotted one, and what decides whether
|
||||
/// there is a colour for the user to change.
|
||||
bool signal_plotted(App& app, const std::string& name) {
|
||||
for (int r = 0; r < app.rows->row_count(); ++r) {
|
||||
const RowSet::Row& row = app.rows->row(r);
|
||||
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
||||
if (row.signal_name[static_cast<std::size_t>(s)] == name) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// The chip for a row of the signal list, as Pango markup: the signal's own colour when
|
||||
/// it is on a plot, grey when it is not (whether that is because it is not acquired or
|
||||
/// because it is acquired and not placed). Always a filled disc, never a blank: a blank
|
||||
@@ -1751,14 +1785,78 @@ std::string chip_markup(plot::Color c) {
|
||||
std::string signal_chip_markup(App& app, const std::vector<SignalInfo>& signals,
|
||||
std::size_t signal) {
|
||||
const std::string& name = signals[signal].name;
|
||||
for (int r = 0; r < app.rows->row_count(); ++r) {
|
||||
const RowSet::Row& row = app.rows->row(r);
|
||||
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
||||
if (row.signal_name[static_cast<std::size_t>(s)] != name) continue;
|
||||
return chip_markup(RowSet::palette_color(signal_color_index(signals, signal)));
|
||||
}
|
||||
if (!signal_plotted(app, name)) return chip_markup(RowSet::unplotted_color());
|
||||
// Through `RowSet::signal_color()`, which is also what `install()` builds the trace with: the
|
||||
// chip's whole job is to say which trace this row is about, so it may not resolve a colour by
|
||||
// its own route. A user's override is exactly the state in which two routes would disagree.
|
||||
return chip_markup(app.rows->signal_color(name, signal_color_index(signals, signal)));
|
||||
}
|
||||
|
||||
/// What an open colour chooser is choosing for. The signal's **name**, not its index: the chooser
|
||||
/// is asynchronous and a CONFIG can renumber the signal list while it is open, which is the same
|
||||
/// reason every persisted identity in this app is a name.
|
||||
struct ColorPick {
|
||||
App* app;
|
||||
std::string signal;
|
||||
};
|
||||
|
||||
/// Applies a chosen colour, or leaves everything alone if the chooser was dismissed.
|
||||
void on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user) {
|
||||
std::unique_ptr<ColorPick> pick(static_cast<ColorPick*>(user));
|
||||
GError* error = nullptr;
|
||||
GdkRGBA* picked = gtk_color_dialog_choose_rgba_finish(GTK_COLOR_DIALOG(source), res, &error);
|
||||
if (picked == nullptr) {
|
||||
// A dismissed chooser arrives here as an error (`GTK_DIALOG_ERROR_DISMISSED`, or a cancelled
|
||||
// `GError` if the window went away), and a cancel must leave the colour as it was. There is
|
||||
// nothing to report either way: the user closed a chooser.
|
||||
if (error != nullptr) g_error_free(error);
|
||||
return;
|
||||
}
|
||||
return chip_markup(RowSet::unplotted_color());
|
||||
const auto octet = [](double v) {
|
||||
return static_cast<plot::u32>(std::clamp(v, 0.0, 1.0) * 255.0 + 0.5);
|
||||
};
|
||||
const plot::u32 rgb = (octet(picked->red) << 16) | (octet(picked->green) << 8) |
|
||||
octet(picked->blue);
|
||||
gdk_rgba_free(picked);
|
||||
|
||||
App& app = *pick->app;
|
||||
app.rows->set_signal_color(pick->signal, rgb);
|
||||
// The rebuild is deferred to the frame tick, like a drop's: a trace's colour is fixed at
|
||||
// `add_trace()`, so a new colour is a new figure, and `relayout_areas()` is the one place that
|
||||
// builds one and re-points the area at it.
|
||||
app.relayout_pending = true;
|
||||
rebuild_signal_panels(app);
|
||||
schedule_layout_save(app);
|
||||
}
|
||||
|
||||
/// Opens the colour chooser for one signal's trace.
|
||||
///
|
||||
/// `GtkColorDialog`, not the `GtkColorChooserDialog` deprecated in GTK 4.10: libadwaita 1.8 already
|
||||
/// puts this build's floor at GTK 4.18, so the modern API is simply available. Alpha is off --
|
||||
/// `plot::Color` carries one but a trace is stroked opaque, so a transparency slider would offer a
|
||||
/// setting that does nothing.
|
||||
void open_signal_color(App& app, std::size_t signal) {
|
||||
std::string name;
|
||||
int palette_index = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
const std::vector<SignalInfo>& signals = app.model->signals();
|
||||
if (signal >= signals.size()) return;
|
||||
name = signals[signal].name;
|
||||
palette_index = signal_color_index(signals, signal);
|
||||
}
|
||||
if (name.empty()) return;
|
||||
const plot::Color current = app.rows->signal_color(name, palette_index);
|
||||
const GdkRGBA start{current.r, current.g, current.b, 1.0f};
|
||||
|
||||
GtkColorDialog* dialog = gtk_color_dialog_new();
|
||||
gtk_color_dialog_set_title(dialog, ("Colour for " + name).c_str());
|
||||
gtk_color_dialog_set_with_alpha(dialog, FALSE);
|
||||
gtk_color_dialog_choose_rgba(dialog, GTK_WINDOW(app.window), &start, nullptr,
|
||||
on_signal_color_chosen, new ColorPick{&app, name});
|
||||
// The async call holds its own reference for the operation's life, which is the documented
|
||||
// idiom for every `Gtk*Dialog` in GTK 4.10's async family.
|
||||
g_object_unref(dialog);
|
||||
}
|
||||
|
||||
/// One line per signal: colour chip, checkbox, name, unit, and a menu for the things
|
||||
@@ -1786,11 +1884,34 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
||||
gtk_widget_set_margin_start(box, 6);
|
||||
gtk_widget_set_margin_end(box, 2);
|
||||
|
||||
const bool plotted = signal_plotted(app, sig.name);
|
||||
GtkWidget* chip = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(chip), signal_chip_markup(app, signals, i).c_str());
|
||||
gtk_widget_set_tooltip_text(chip, app.model->signal_enabled(i)
|
||||
? "the colour this signal is plotted in"
|
||||
: "not acquired");
|
||||
gtk_widget_set_tooltip_text(
|
||||
chip, !app.model->signal_enabled(i) ? "not acquired"
|
||||
: plotted ? "the colour this signal is plotted in -- click to change it"
|
||||
: "on no plot, so there is no trace to colour");
|
||||
if (plotted) {
|
||||
// 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
|
||||
// this function's note). The chip also has to stay exactly one disc wide, or the names
|
||||
// beside it shift as signals are placed.
|
||||
gtk_widget_set_cursor_from_name(chip, "pointer");
|
||||
GtkGesture* pick = gtk_gesture_click_new();
|
||||
g_object_set_data(G_OBJECT(pick), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||
g_signal_connect(pick, "pressed",
|
||||
G_CALLBACK(+[](GtkGestureClick* g, int, double, double, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
// Claimed, so the row's `GtkDragSource` cannot take the same sequence
|
||||
// and turn a click on the chip into the start of a drag.
|
||||
gtk_gesture_set_state(GTK_GESTURE(g), GTK_EVENT_SEQUENCE_CLAIMED);
|
||||
open_signal_color(*a, static_cast<std::size_t>(GPOINTER_TO_SIZE(
|
||||
g_object_get_data(G_OBJECT(g),
|
||||
"udpscope-signal"))));
|
||||
}),
|
||||
&app);
|
||||
gtk_widget_add_controller(chip, GTK_EVENT_CONTROLLER(pick));
|
||||
}
|
||||
gtk_box_append(GTK_BOX(box), chip);
|
||||
|
||||
GtkWidget* enable = gtk_check_button_new();
|
||||
@@ -1825,6 +1946,33 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
||||
g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app);
|
||||
gtk_box_append(GTK_BOX(menu_box), info);
|
||||
|
||||
// The way back from a chosen colour. The chooser itself cannot carry a "default" button, and a
|
||||
// setting with no undo is a trap; it lives here rather than on the chip because a second
|
||||
// gesture on a one-glyph target is not something a user would find.
|
||||
GtkWidget* recolor = menu_button("Default colour", nullptr);
|
||||
gtk_widget_set_sensitive(recolor, app.rows->signal_colors().count(sig.name) != 0);
|
||||
gtk_widget_set_tooltip_text(recolor, "back to the palette colour for this signal's position");
|
||||
g_object_set_data(G_OBJECT(recolor), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||
g_signal_connect(recolor, "clicked",
|
||||
G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
const std::size_t idx = static_cast<std::size_t>(
|
||||
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(b), "udpscope-signal")));
|
||||
std::string name;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
const std::vector<SignalInfo>& sigs = a->model->signals();
|
||||
if (idx >= sigs.size()) return;
|
||||
name = sigs[idx].name;
|
||||
}
|
||||
a->rows->clear_signal_color(name);
|
||||
a->relayout_pending = true;
|
||||
rebuild_signal_panels(*a);
|
||||
schedule_layout_save(*a);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(menu_box), recolor);
|
||||
|
||||
GtkWidget* cal = menu_button("Calibrate...", nullptr);
|
||||
gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest");
|
||||
g_object_set_data(G_OBJECT(cal), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||
@@ -2365,8 +2513,9 @@ GtkWidget* measures_row(App& app, std::size_t index, const Measure& m) {
|
||||
}
|
||||
}
|
||||
gtk_label_set_markup(GTK_LABEL(sig_chip),
|
||||
chip_markup(color_index >= 0 ? RowSet::palette_color(color_index)
|
||||
: RowSet::unplotted_color())
|
||||
chip_markup(color_index >= 0
|
||||
? app.rows->signal_color(m.signal, color_index)
|
||||
: RowSet::unplotted_color())
|
||||
.c_str());
|
||||
}
|
||||
gtk_widget_set_tooltip_text(sig_chip, m.signal.c_str());
|
||||
@@ -2597,7 +2746,8 @@ void rebuild_cursors_pane(App& app) {
|
||||
// what "the pane shows a lot of empty signals that do not exist" was.
|
||||
if (row.signal_name[si].empty() || row.channel[si] < 0) continue;
|
||||
app.cursors_lines.push_back(cursors_line(
|
||||
app, row.name[si].c_str(), RowSet::palette_color(row.color_index[si]), true));
|
||||
app, row.name[si].c_str(),
|
||||
app.rows->signal_color(row.signal_name[si], row.color_index[si]), true));
|
||||
app.cursors_lines.back().row = r;
|
||||
app.cursors_lines.back().slot = s;
|
||||
}
|
||||
@@ -9185,6 +9335,100 @@ gboolean menus_probe_step(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Checks that a chosen trace colour reaches the **trace** and not merely the resolver.
|
||||
///
|
||||
/// It reads the colour back out of `plot::Plot::trace()` -- what the renderer will stroke -- and out
|
||||
/// of the signal list's chip markup, because the chip's only job is to name a trace and the two
|
||||
/// resolving a colour by separate routes is the one failure a chip cannot survive. The override is
|
||||
/// `#123456`, deliberately in no palette slot: an override that happened to equal the entry it
|
||||
/// replaced would pass against an implementation that ignored it entirely.
|
||||
gboolean color_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
|
||||
// Any slot with a *bound* trace: an unbound one has no colour on screen to check.
|
||||
int probe_row = -1;
|
||||
int probe_slot = -1;
|
||||
std::string name;
|
||||
for (int r = 0; r < a->rows->row_count() && probe_row < 0; ++r) {
|
||||
const RowSet::Row& row = a->rows->row(r);
|
||||
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
||||
const std::size_t si = static_cast<std::size_t>(s);
|
||||
if (row.signal_name[si].empty() || row.channel[si] < 0) continue;
|
||||
probe_row = r;
|
||||
probe_slot = s;
|
||||
name = row.signal_name[si];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (probe_row < 0) {
|
||||
std::fprintf(stderr, "[color] no bound trace to recolour\n");
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// The chip as the signal list actually writes it, and the trace as the renderer will read it.
|
||||
const auto chip_of = [&]() {
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
const std::vector<SignalInfo>& signals = a->model->signals();
|
||||
for (std::size_t i = 0; i < signals.size(); ++i) {
|
||||
if (signals[i].name == name) return signal_chip_markup(*a, signals, i);
|
||||
}
|
||||
return std::string();
|
||||
};
|
||||
const auto trace_of = [&]() {
|
||||
const plot::Plot* p = a->rows->row(probe_row).plot;
|
||||
if (p == nullptr) return std::string("#??????");
|
||||
return color_hex(p->trace(static_cast<plot::u32>(probe_slot)).color);
|
||||
};
|
||||
// A chip carries its colour inside `<span foreground="...">`, so "the chip says X" is the
|
||||
// substring test and not a recomputation of what it ought to say.
|
||||
const auto chip_says = [](const std::string& markup, const std::string& hex) {
|
||||
return markup.find("\"" + hex + "\"") != std::string::npos ? 1 : 0;
|
||||
};
|
||||
|
||||
const std::string before = trace_of();
|
||||
std::fprintf(stderr, "[color] '%s' at row %d slot %d: trace=%s chip_agrees=%d (want 1)\n",
|
||||
name.c_str(), probe_row, probe_slot, before.c_str(),
|
||||
chip_says(chip_of(), before));
|
||||
|
||||
constexpr plot::u32 kPick = 0x123456u;
|
||||
a->rows->set_signal_color(name, kPick);
|
||||
// The same path the frame tick takes: a trace's colour is fixed at `add_trace()`, so this is a
|
||||
// new figure, and reading the old `Row::plot` after it would read the figure that was replaced.
|
||||
relayout_areas(*a);
|
||||
const std::string after = trace_of();
|
||||
std::fprintf(stderr, "[color] override: trace=%s chip_agrees=%d (want #123456 1)\n",
|
||||
after.c_str(), chip_says(chip_of(), after));
|
||||
|
||||
// The session file, which is where a colour has to survive to be worth choosing. Through the real
|
||||
// writer and reader on a temp path, not the live session's -- a probe must not leave a colour in
|
||||
// the file a later run would inherit.
|
||||
const std::string tmp = std::string(g_get_tmp_dir()) + "/udpscope-color-probe.toml";
|
||||
LayoutState saved = collect_layout(*a);
|
||||
int round_trip = -1;
|
||||
std::size_t entries = 0;
|
||||
if (save_layout(tmp, saved)) {
|
||||
LayoutState back;
|
||||
if (load_layout(tmp, back)) {
|
||||
entries = back.colors.size();
|
||||
for (const auto& c : back.colors) {
|
||||
if (c.name == name) round_trip = static_cast<int>(c.rgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::remove(tmp.c_str());
|
||||
std::fprintf(stderr, "[color] session: entries=%zu rgb=#%06X (want 1 #123456)\n", entries,
|
||||
round_trip < 0 ? 0u : static_cast<unsigned>(round_trip));
|
||||
|
||||
a->rows->clear_signal_color(name);
|
||||
relayout_areas(*a);
|
||||
const std::string restored = trace_of();
|
||||
std::fprintf(stderr, "[color] default: trace=%s chip_agrees=%d (want %s 1)\n", restored.c_str(),
|
||||
chip_says(chip_of(), restored), before.c_str());
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Fires a representative accel from each group and reports the state it changed, then
|
||||
/// checks the GtkEditable refusal. It goes through run_app_action() for the actions and
|
||||
/// through the real handler for the refusal, because the refusal lives in the handler.
|
||||
@@ -9391,6 +9635,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), keys_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_COLOR_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), color_probe, &app);
|
||||
}
|
||||
|
||||
// Our own options were consumed by parse_args(); GTK would reject them, so only
|
||||
// the program name is handed to g_application_run().
|
||||
|
||||
Reference in new issue
Block a user