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:
Martino FerrariandClaude Opus 4.6 committed 2026-10-07 08:22:32 +02:00
1 parent 8bc1fe8b5c
commit 4003084648
11 files changed
+406 -24

No files matched your search

+27 -1
View File
@@ -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