working on todo.md features
This commit is contained in:
1 parent
210d124814
commit
8f54841c2c
110 files changed
+15932
-6576
No files matched your search
@@ -0,0 +1,431 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
//
|
||||
// The signal list: one row per signal with its colour chip and acquire tick, the panel around it,\n// and the drop that puts a signal on a row.
|
||||
|
||||
#include "ui_app.hpp"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace udpscope {
|
||||
namespace ui_impl {
|
||||
|
||||
void on_signal_switch(GObject* obj, GParamSpec*, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (app->updating_panels) return;
|
||||
const std::size_t index = static_cast<std::size_t>(
|
||||
GPOINTER_TO_SIZE(g_object_get_data(obj, "udpscope-signal")));
|
||||
app->model->set_signal_enabled(index, gtk_check_button_get_active(GTK_CHECK_BUTTON(obj)));
|
||||
// The enable state is part of the session (a signal can be on only as a math channel's source).
|
||||
schedule_layout_save(*app);
|
||||
}
|
||||
|
||||
/// The palette entry a signal owns: its position among the *plottable* signals, the
|
||||
/// time base skipped. It is a property of the signal and not of the row/slot a drop
|
||||
/// lands in, so the same signal is drawn in the same colour on every plot it appears on
|
||||
/// -- which is what lets one chip in this list name every one of those traces.
|
||||
///
|
||||
/// Positional, like the channel list, so a new CONFIG re-colours; that is fine because a
|
||||
/// CONFIG drops the assignments and the history with them. A name hash would survive a
|
||||
/// CONFIG but collide inside one, and two traces of the same colour on one row is the
|
||||
/// failure this exists to prevent.
|
||||
int signal_color_index(const std::vector<SignalInfo>& signals, std::size_t signal) {
|
||||
// Counted over *signals*, not channels: a group's model holds each signal once per event, and
|
||||
// counting the copies would spread one signal's colour across the palette and give its second layer a
|
||||
// colour of its own.
|
||||
int index = 0;
|
||||
std::vector<std::string> seen;
|
||||
for (std::size_t i = 0; i < signals.size() && i < signal; ++i) {
|
||||
if (ScopeModel::is_time_signal(signals, i)) continue;
|
||||
const std::string base = layer_base_name(signals[i].name);
|
||||
if (std::find(seen.begin(), seen.end(), base) != seen.end()) continue;
|
||||
seen.push_back(base);
|
||||
++index;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// cell is narrower than a disc, so the names beside it used to shift sideways as
|
||||
/// signals were placed, and "no trace" read as "no chip here" rather than as a state.
|
||||
std::string signal_chip_markup(App& app, const std::vector<SignalInfo>& signals,
|
||||
std::size_t signal) {
|
||||
const std::string& name = signals[signal].name;
|
||||
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)));
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/// One line per signal: colour chip, checkbox, name, unit, and a menu for the things
|
||||
/// you touch once (information, calibration). A `GtkSwitch` and two always-visible
|
||||
/// buttons made every row about twice this tall, which is what turned a four-signal
|
||||
/// stream into a panel you have to scroll.
|
||||
void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
||||
app.updating_panels = true;
|
||||
GtkWidget* list = app.signal_list;
|
||||
GtkWidget* child = gtk_widget_get_first_child(list);
|
||||
while (child != nullptr) {
|
||||
GtkWidget* next = gtk_widget_get_next_sibling(child);
|
||||
gtk_list_box_remove(GTK_LIST_BOX(list), child);
|
||||
child = next;
|
||||
}
|
||||
|
||||
// One line per *signal*, not per channel. A group's model holds each signal once per event; a list
|
||||
// that showed both would name one "sine (A)" and the other "sine (B)" as if they were different
|
||||
// things, and give them different colours. What the row toggles, colours and drags is the signal.
|
||||
std::vector<std::size_t> rows;
|
||||
for (std::size_t i = 0; i < signals.size(); ++i) {
|
||||
if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not plottable
|
||||
const std::string base = layer_base_name(signals[i].name);
|
||||
bool seen = false;
|
||||
for (std::size_t j = 0; j < i && !seen; ++j) {
|
||||
seen = !ScopeModel::is_time_signal(signals, j) &&
|
||||
layer_base_name(signals[j].name) == base;
|
||||
}
|
||||
if (!seen) rows.push_back(i);
|
||||
}
|
||||
|
||||
for (std::size_t i : rows) {
|
||||
const SignalInfo& sig = signals[i];
|
||||
GtkWidget* row = gtk_list_box_row_new();
|
||||
gtk_widget_add_css_class(row, "udpscope-compact");
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
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) ? "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();
|
||||
gtk_check_button_set_active(GTK_CHECK_BUTTON(enable), app.model->signal_enabled(i));
|
||||
gtk_widget_set_tooltip_text(enable, "acquire this signal");
|
||||
// A file view has nothing to acquire: an event's or a file's samples are already stored, and
|
||||
// "acquire this signal" is a stream's question. Hidden rather than disabled -- a greyed tick invites
|
||||
// the user to look for the reason, and the reason is that this mode has no acquisition at all.
|
||||
gtk_widget_set_visible(enable, app.waveform_name.empty());
|
||||
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);
|
||||
gtk_box_append(GTK_BOX(box), enable);
|
||||
|
||||
GtkWidget* name = gtk_label_new(sig.name.c_str());
|
||||
gtk_widget_set_hexpand(name, true);
|
||||
gtk_widget_set_halign(name, GTK_ALIGN_START);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END);
|
||||
gtk_box_append(GTK_BOX(box), name);
|
||||
|
||||
if (!sig.unit.empty()) {
|
||||
GtkWidget* unit = gtk_label_new(sig.unit.c_str());
|
||||
gtk_widget_add_css_class(unit, "dim-label");
|
||||
gtk_box_append(GTK_BOX(box), unit);
|
||||
}
|
||||
|
||||
// The overflow: information and calibration are set once per session, so they
|
||||
// cost a click here instead of a button on every row forever.
|
||||
GtkWidget* menu_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_set_margin_top(menu_box, 4);
|
||||
gtk_widget_set_margin_bottom(menu_box, 4);
|
||||
gtk_widget_set_margin_start(menu_box, 4);
|
||||
gtk_widget_set_margin_end(menu_box, 4);
|
||||
|
||||
GtkWidget* info = menu_button("Information", nullptr);
|
||||
g_object_set_data(G_OBJECT(info), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||
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));
|
||||
g_signal_connect(cal, "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")));
|
||||
open_calibration(*a, GTK_WINDOW(a->window), idx);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(menu_box), cal);
|
||||
|
||||
GtkWidget* popover = gtk_popover_new();
|
||||
gtk_widget_add_css_class(popover, "udpscope-menu");
|
||||
gtk_popover_set_child(GTK_POPOVER(popover), menu_box);
|
||||
GtkWidget* more = gtk_menu_button_new();
|
||||
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(more), "view-more-symbolic");
|
||||
gtk_menu_button_set_popover(GTK_MENU_BUTTON(more), popover);
|
||||
gtk_widget_add_css_class(more, "flat");
|
||||
gtk_widget_set_tooltip_text(more, "information and calibration");
|
||||
gtk_box_append(GTK_BOX(box), more);
|
||||
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
|
||||
|
||||
DragContext* ctx = new DragContext{&app, i};
|
||||
GtkDragSource* source = gtk_drag_source_new();
|
||||
gtk_drag_source_set_actions(source, GDK_ACTION_COPY);
|
||||
g_signal_connect_data(source, "prepare", G_CALLBACK(on_drag_prepare), ctx,
|
||||
[](gpointer data, GClosure*) { delete static_cast<DragContext*>(data); },
|
||||
G_CONNECT_DEFAULT);
|
||||
gtk_widget_add_controller(row, GTK_EVENT_CONTROLLER(source));
|
||||
|
||||
gtk_list_box_append(GTK_LIST_BOX(list), row);
|
||||
}
|
||||
app.updating_panels = false;
|
||||
}
|
||||
|
||||
void on_signals_toggled(GtkToggleButton* button, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
gtk_revealer_set_reveal_child(GTK_REVEALER(app->signal_revealer),
|
||||
gtk_toggle_button_get_active(button));
|
||||
}
|
||||
|
||||
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y,
|
||||
gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (!G_VALUE_HOLDS_STRING(value)) return FALSE;
|
||||
// Where the drop landed decides the row: one area covers them all.
|
||||
const int row = row_at(*app, x, y);
|
||||
|
||||
const char* text = g_value_get_string(value);
|
||||
const std::size_t signal = static_cast<std::size_t>(std::atoll(text ? text : "0"));
|
||||
if (signal >= app->model->signals().size()) return FALSE;
|
||||
|
||||
const auto notice = [&](const std::string& what) {
|
||||
app->notice = what;
|
||||
app->notice_until_us = g_get_monotonic_time() + 4 * G_TIME_SPAN_SECOND;
|
||||
std::fprintf(stderr, "[udpscope] drop: %s\n", what.c_str());
|
||||
std::fflush(stderr);
|
||||
};
|
||||
|
||||
// The dropped signal's channels are the ones the model built for it; take the
|
||||
// first, or the first element if the signal is a vector.
|
||||
const std::vector<ChannelInfo>& channels = app->model->channels();
|
||||
int channel = -1;
|
||||
std::string name;
|
||||
for (std::size_t i = 0; i < channels.size(); ++i) {
|
||||
if (channels[i].signal == signal) {
|
||||
channel = static_cast<int>(i);
|
||||
name = channels[i].name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (channel < 0) {
|
||||
// The signal is not enabled: enable it, then take the channel that appears.
|
||||
app->model->set_signal_enabled(signal, true);
|
||||
const std::vector<ChannelInfo>& again = app->model->channels();
|
||||
for (std::size_t i = 0; i < again.size(); ++i) {
|
||||
if (again[i].signal == signal) {
|
||||
channel = static_cast<int>(i);
|
||||
name = again[i].name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (channel < 0) {
|
||||
notice("cannot acquire '" + app->model->signals()[signal].name +
|
||||
"' (multi-element signals are not acquired)");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
int slot = -1;
|
||||
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
||||
if (!app->rows->slot_bound(row, s)) {
|
||||
slot = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot < 0) {
|
||||
notice("plot " + std::to_string(row + 1) + " already holds four signals");
|
||||
return FALSE;
|
||||
}
|
||||
// The slot remembers the *signal* (and element), never the channel index: the
|
||||
// channel list is renumbered whenever a signal is enabled or disabled.
|
||||
const std::vector<SignalInfo>& signals = app->model->signals();
|
||||
const SignalInfo& sig = signals[signal];
|
||||
app->rows->assign(row, slot, sig.name, name, signal_color_index(signals, signal));
|
||||
schedule_layout_save(*app);
|
||||
app->relayout_pending = true;
|
||||
rebuild_signal_panels(*app); // the row's colour chip now has a trace to name
|
||||
cancel_drag_on(*app, row);
|
||||
notice("plotting '" + name + "' on plot " + std::to_string(row + 1));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void rebuild_signal_panels(App& app) {
|
||||
// Snapshot the model's lists under the lock: the acquisition thread replaces
|
||||
// them on every CONFIG, and the panel build holds these references across many
|
||||
// GTK calls (a use-after-free of the name strings otherwise).
|
||||
std::vector<SignalInfo> signals;
|
||||
bool have_signals = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
signals = app.model->signals();
|
||||
have_signals = !signals.empty();
|
||||
}
|
||||
rebuild_signal_list(app, signals);
|
||||
rebuild_trigger_signals(app);
|
||||
refresh_window_limits(app);
|
||||
|
||||
// Nothing to configure before a CONFIG arrives: hide the whole column and its
|
||||
// toolbar buttons rather than showing two empty panels. The first CONFIG
|
||||
// reveals the signal panel (the trigger panel stays behind its own button).
|
||||
update_side_visibility(app);
|
||||
if (have_signals && !app.signals_panel_initialised) {
|
||||
app.signals_panel_initialised = true;
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.signal_toggle), TRUE);
|
||||
}
|
||||
if (!have_signals) {
|
||||
app.signals_panel_initialised = false;
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.signal_toggle), FALSE);
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), FALSE);
|
||||
}
|
||||
app.signal_generation = app.model->generation();
|
||||
}
|
||||
|
||||
GtkWidget* build_signal_panel(App& app) {
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
gtk_widget_set_margin_top(box, 8);
|
||||
gtk_widget_set_margin_bottom(box, 8);
|
||||
gtk_widget_set_margin_start(box, 8);
|
||||
gtk_widget_set_margin_end(box, 8);
|
||||
|
||||
GtkWidget* heading = gtk_label_new("Signals");
|
||||
gtk_widget_set_halign(heading, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(box), heading);
|
||||
|
||||
GtkWidget* hint = gtk_label_new("drag a signal onto a plot");
|
||||
gtk_widget_set_halign(hint, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(hint, "dim-label");
|
||||
gtk_box_append(GTK_BOX(box), hint);
|
||||
|
||||
app.signal_list = gtk_list_box_new();
|
||||
gtk_list_box_set_selection_mode(GTK_LIST_BOX(app.signal_list), GTK_SELECTION_NONE);
|
||||
// Width only. A height floor here made the list occupy 120 px whatever it held,
|
||||
// which hid the compact rows' height behind it and wasted the space on an empty
|
||||
// stream; the scroller above it already caps how tall it can grow.
|
||||
gtk_widget_set_size_request(app.signal_list, 260, -1);
|
||||
gtk_box_append(GTK_BOX(box), app.signal_list);
|
||||
return box;
|
||||
}
|
||||
|
||||
/// The pane's availability as one reading: the history button on the bar, the pane revealed, and the
|
||||
/// switch that decides half of it. Both the button and the pane follow trigger mode *and* recording,
|
||||
/// so every step that changes either prints all three.
|
||||
/// Whether the signal list's first row still carries its acquire tick. A file view has nothing to
|
||||
/// acquire -- the samples are stored -- so the tick goes; in live mode it is the control that decides
|
||||
/// what is acquired at all.
|
||||
int signal_tick_visible(App& app) {
|
||||
GtkWidget* row =
|
||||
app.signal_list != nullptr ? gtk_widget_get_first_child(app.signal_list) : nullptr;
|
||||
if (row == nullptr) return -1;
|
||||
for (GtkWidget* box = gtk_widget_get_first_child(row); box != nullptr;
|
||||
box = gtk_widget_get_next_sibling(box)) {
|
||||
for (GtkWidget* w = gtk_widget_get_first_child(box); w != nullptr;
|
||||
w = gtk_widget_get_next_sibling(w)) {
|
||||
if (GTK_IS_CHECK_BUTTON(w)) return gtk_widget_get_visible(w) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace ui_impl
|
||||
} // namespace udpscope
|
||||
Reference in new issue
Block a user