|
|
|
@@ -621,6 +621,12 @@ void open_scope_window(App& app, int figure_row, bool detached) {
|
|
|
|
|
g_object_set_data(G_OBJECT(scope->drop), "udpscope-scope", scope.get());
|
|
|
|
|
g_signal_connect(scope->drop, "drop", G_CALLBACK(on_scope_drop_channel), &app);
|
|
|
|
|
gtk_widget_add_controller(scope->widget, GTK_EVENT_CONTROLLER(scope->drop));
|
|
|
|
|
// The window's own right-click menu: its y scales, its traces, its removals -- the same
|
|
|
|
|
// actions the main window's plot menu carries, aimed at **this window's** row set.
|
|
|
|
|
GtkGesture* right = gtk_gesture_click_new();
|
|
|
|
|
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(right), 3);
|
|
|
|
|
g_signal_connect(right, "pressed", G_CALLBACK(on_scope_right_click), &app);
|
|
|
|
|
gtk_widget_add_controller(scope->widget, GTK_EVENT_CONTROLLER(right));
|
|
|
|
|
scope->detached = detached;
|
|
|
|
|
scope->window = GTK_WINDOW(gtk_window_new());
|
|
|
|
|
std::string title = figure_row >= 0
|
|
|
|
@@ -3338,8 +3344,9 @@ void on_y_auto_clicked(GtkButton* button, gpointer user) {
|
|
|
|
|
|
|
|
|
|
void on_fixed_range_apply(GtkButton*, gpointer user) {
|
|
|
|
|
FixedRangeContext* ctx = static_cast<FixedRangeContext*>(user);
|
|
|
|
|
ctx->app->rows->set_y_range(ctx->row, gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->lo)),
|
|
|
|
|
gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->hi)));
|
|
|
|
|
RowSet& rows = ctx->rows != nullptr ? *ctx->rows : *ctx->app->rows;
|
|
|
|
|
rows.set_y_range(ctx->row, gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->lo)),
|
|
|
|
|
gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->hi)));
|
|
|
|
|
gtk_window_destroy(GTK_WINDOW(ctx->window));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -3354,7 +3361,7 @@ void on_y_fixed_clicked(GtkButton* button, gpointer user) {
|
|
|
|
|
gtk_box_append(GTK_BOX(box), labeled_box("min", lo));
|
|
|
|
|
gtk_box_append(GTK_BOX(box), labeled_box("max", hi));
|
|
|
|
|
|
|
|
|
|
auto* ctx = new FixedRangeContext{app, row, lo, hi, window};
|
|
|
|
|
auto* ctx = new FixedRangeContext{app, nullptr, row, lo, hi, window};
|
|
|
|
|
GtkWidget* cancel = gtk_button_new_with_label("Cancel");
|
|
|
|
|
g_signal_connect_swapped(cancel, "clicked", G_CALLBACK(gtk_window_destroy), window);
|
|
|
|
|
GtkWidget* apply = gtk_button_new_with_label("Apply");
|
|
|
|
@@ -3374,9 +3381,9 @@ void on_y_fixed_clicked(GtkButton* button, gpointer user) {
|
|
|
|
|
|
|
|
|
|
/// Frees one trace slot of a row. All it takes is the free: the row is rebuilt with a series that is
|
|
|
|
|
/// nothing, so it draws nothing and leaves the legend.
|
|
|
|
|
void remove_trace(App& app, int row, int slot) {
|
|
|
|
|
if (row < 0 || row >= app.rows->row_count()) return;
|
|
|
|
|
app.rows->assign(row, slot, std::string(), std::string(), 0);
|
|
|
|
|
void remove_trace(RowSet& rows, int row, int slot) {
|
|
|
|
|
if (row < 0 || row >= rows.row_count()) return;
|
|
|
|
|
rows.assign(row, slot, std::string(), std::string(), 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Disables every math channel that (transitively) reads `name`, and answers with their names.
|
|
|
|
@@ -3429,7 +3436,7 @@ int remove_signal_from_plots(App& app, const std::string& name) {
|
|
|
|
|
// The slot remembers the *signal* (and its element), never the channel index -- the channel list is
|
|
|
|
|
// renumbered on every enable change, which is what this is reacting to.
|
|
|
|
|
if (app.rows->row(row).signal_name[static_cast<std::size_t>(slot)] != name) continue;
|
|
|
|
|
remove_trace(app, row, slot);
|
|
|
|
|
remove_trace(*app.rows, row, slot);
|
|
|
|
|
++removed;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -3448,14 +3455,14 @@ void on_remove_clicked(GtkButton* button, gpointer user) {
|
|
|
|
|
int removed = 0;
|
|
|
|
|
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
|
|
|
|
if (r.channel[static_cast<std::size_t>(s)] < 0) continue;
|
|
|
|
|
remove_trace(app, target->row, s);
|
|
|
|
|
remove_trace(*target->app->rows, target->row, s);
|
|
|
|
|
++removed;
|
|
|
|
|
}
|
|
|
|
|
what = "removed " + std::to_string(removed) + (removed == 1 ? " trace" : " traces") +
|
|
|
|
|
" from plot " + app.rows->plot_label(target->row);
|
|
|
|
|
} else {
|
|
|
|
|
const std::string name = r.name[static_cast<std::size_t>(target->slot)];
|
|
|
|
|
remove_trace(app, target->row, target->slot);
|
|
|
|
|
remove_trace(*target->app->rows, target->row, target->slot);
|
|
|
|
|
what = "removed '" + name + "' from plot " + app.rows->plot_label(target->row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -3656,6 +3663,163 @@ void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer
|
|
|
|
|
open_row_menu(*app, row, x, y, widget);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Scope-menu helpers: the rows carry their window, their plot and their slot, the popover to
|
|
|
|
|
/// dismiss, and the handlers are free functions -- a menu row's handler is a plain callback, not a
|
|
|
|
|
/// lambda, whose commas would split the `G_CALLBACK` macro into arguments.
|
|
|
|
|
App::ScopeWindow* scope_menu_scope(GtkButton* b) {
|
|
|
|
|
return static_cast<App::ScopeWindow*>(g_object_get_data(G_OBJECT(b), "udpscope-scope"));
|
|
|
|
|
}
|
|
|
|
|
int scope_menu_row(GtkButton* b) {
|
|
|
|
|
return GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), "udpscope-row"));
|
|
|
|
|
}
|
|
|
|
|
void scope_menu_dismiss(GtkButton* b) {
|
|
|
|
|
GtkPopover* p = GTK_POPOVER(g_object_get_data(G_OBJECT(b), "udpscope-popover"));
|
|
|
|
|
if (p != nullptr) gtk_popover_popdown(p);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void on_scope_y_auto_clicked(GtkButton* b, gpointer) {
|
|
|
|
|
App::ScopeWindow* scope = scope_menu_scope(b);
|
|
|
|
|
if (scope == nullptr || scope->rows == nullptr) return;
|
|
|
|
|
scope->rows->set_y_auto(scope_menu_row(b), true);
|
|
|
|
|
scope_menu_dismiss(b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The main menu's fixed-range dialog, aimed at **this window's** rows: the dialog's Apply
|
|
|
|
|
/// writes the row set the context names, and the context carries the scope's.
|
|
|
|
|
void on_scope_y_fixed_clicked(GtkButton* b, gpointer user) {
|
|
|
|
|
App* a = static_cast<App*>(user);
|
|
|
|
|
App::ScopeWindow* scope = scope_menu_scope(b);
|
|
|
|
|
const int row = scope_menu_row(b);
|
|
|
|
|
if (scope == nullptr || scope->rows == nullptr || scope->rows->row(row).plot == nullptr) return;
|
|
|
|
|
GtkWidget* window = dialog_window(a->window, "Fixed Y range");
|
|
|
|
|
GtkWidget* box = gtk_window_get_child(GTK_WINDOW(window));
|
|
|
|
|
const plot::Range cur = scope->rows->row(row).plot->y_range(0);
|
|
|
|
|
GtkWidget* lo = spin(-1.0e12, 1.0e12, 0.1, 6, cur.min);
|
|
|
|
|
GtkWidget* hi = spin(-1.0e12, 1.0e12, 0.1, 6, cur.max);
|
|
|
|
|
gtk_box_append(GTK_BOX(box), labeled_box("min", lo));
|
|
|
|
|
gtk_box_append(GTK_BOX(box), labeled_box("max", hi));
|
|
|
|
|
auto* ctx = new FixedRangeContext{a, scope->rows.get(), row, lo, hi, window};
|
|
|
|
|
GtkWidget* cancel = gtk_button_new_with_label("Cancel");
|
|
|
|
|
g_signal_connect_swapped(cancel, "clicked", G_CALLBACK(gtk_window_destroy), window);
|
|
|
|
|
GtkWidget* apply = gtk_button_new_with_label("Apply");
|
|
|
|
|
g_object_set_data_full(G_OBJECT(apply), "udpscope-ctx", ctx,
|
|
|
|
|
[](gpointer p) { delete static_cast<FixedRangeContext*>(p); });
|
|
|
|
|
g_signal_connect(apply, "clicked", G_CALLBACK(on_fixed_range_apply), ctx);
|
|
|
|
|
dialog_add_button(window, cancel);
|
|
|
|
|
dialog_add_button(window, apply);
|
|
|
|
|
gtk_window_present(GTK_WINDOW(window));
|
|
|
|
|
scope_menu_dismiss(b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The y fit of a scope window: the window's x follows the main window's span, so the fit is the
|
|
|
|
|
/// y axes alone -- `fit_all()`'s x reset would be undone by the next frame's mirror.
|
|
|
|
|
void on_scope_fit_clicked(GtkButton* b, gpointer) {
|
|
|
|
|
App::ScopeWindow* scope = scope_menu_scope(b);
|
|
|
|
|
if (scope == nullptr || scope->rows == nullptr) return;
|
|
|
|
|
scope->rows->set_y_auto(scope_menu_row(b), true);
|
|
|
|
|
scope_menu_dismiss(b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void on_scope_remove_clicked(GtkButton* b, gpointer) {
|
|
|
|
|
App::ScopeWindow* scope = scope_menu_scope(b);
|
|
|
|
|
if (scope == nullptr || scope->rows == nullptr) return;
|
|
|
|
|
remove_trace(*scope->rows, scope_menu_row(b),
|
|
|
|
|
GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), "udpscope-slot")));
|
|
|
|
|
scope_menu_dismiss(b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void on_scope_attach_menu_clicked(GtkButton* b, gpointer user) {
|
|
|
|
|
App& a = *static_cast<App*>(user);
|
|
|
|
|
App::ScopeWindow* scope = scope_menu_scope(b);
|
|
|
|
|
if (scope == nullptr) return;
|
|
|
|
|
outlive_the_rebuild(GTK_WIDGET(b));
|
|
|
|
|
attach_scope_window(a, *scope);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A scope window's own right-click menu. A scope window is a *view with its own arrangement*:
|
|
|
|
|
/// its y scales, its traces and its removals are its own, so the menu acts on **that window's**
|
|
|
|
|
/// row set -- the same rows the drop target and the signal page's *Plot on* groups write. What it
|
|
|
|
|
/// deliberately does not carry is an x fit: the window follows the main window's time span, so an
|
|
|
|
|
/// x fit here would be undone by the next frame.
|
|
|
|
|
void on_scope_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user) {
|
|
|
|
|
App* app = static_cast<App*>(user);
|
|
|
|
|
GtkWidget* widget = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture));
|
|
|
|
|
App::ScopeWindow* scope = nullptr;
|
|
|
|
|
for (const std::unique_ptr<App::ScopeWindow>& s : app->scopes) {
|
|
|
|
|
if (s->widget == widget) scope = s.get();
|
|
|
|
|
}
|
|
|
|
|
if (scope == nullptr || scope->rows == nullptr || scope->rows->row_count() <= 0) return;
|
|
|
|
|
const int row = std::clamp(static_cast<int>(scope->area->row_at(x, y)), 0,
|
|
|
|
|
scope->rows->row_count() - 1);
|
|
|
|
|
gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_CLAIMED);
|
|
|
|
|
if (plot::input::Controller* c = scope->area->row_controller(static_cast<plot::u32>(row))) {
|
|
|
|
|
c->on_key(plot::input::KeyEvent{plot::input::kKeyEscape, 0, true});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GtkWidget* popover = gtk_popover_new();
|
|
|
|
|
gtk_widget_add_css_class(popover, "udpscope-menu");
|
|
|
|
|
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
|
|
|
|
gtk_widget_set_margin_top(box, 6);
|
|
|
|
|
gtk_widget_set_margin_bottom(box, 6);
|
|
|
|
|
gtk_widget_set_margin_start(box, 6);
|
|
|
|
|
gtk_widget_set_margin_end(box, 6);
|
|
|
|
|
const auto row_btn = [&](const char* label) {
|
|
|
|
|
GtkWidget* b = gtk_button_new_with_label(label);
|
|
|
|
|
gtk_widget_add_css_class(b, "flat");
|
|
|
|
|
gtk_widget_set_halign(b, GTK_ALIGN_FILL);
|
|
|
|
|
g_object_set_data(G_OBJECT(b), "udpscope-popover", popover);
|
|
|
|
|
g_object_set_data(G_OBJECT(b), "udpscope-scope", scope);
|
|
|
|
|
g_object_set_data(G_OBJECT(b), "udpscope-row", GINT_TO_POINTER(row));
|
|
|
|
|
gtk_box_append(GTK_BOX(box), b);
|
|
|
|
|
return b;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
char markup[64];
|
|
|
|
|
std::snprintf(markup, sizeof(markup), "<b>Plot %s</b>", scope->rows->plot_label(row).c_str());
|
|
|
|
|
GtkWidget* heading = gtk_label_new(nullptr);
|
|
|
|
|
gtk_label_set_markup(GTK_LABEL(heading), markup);
|
|
|
|
|
gtk_widget_set_halign(heading, GTK_ALIGN_START);
|
|
|
|
|
gtk_box_append(GTK_BOX(box), heading);
|
|
|
|
|
|
|
|
|
|
GtkWidget* y_auto = row_btn("Y axis: auto scale");
|
|
|
|
|
g_signal_connect(y_auto, "clicked", G_CALLBACK(on_scope_y_auto_clicked), user);
|
|
|
|
|
GtkWidget* y_fixed = row_btn("Y axis: fixed range...");
|
|
|
|
|
g_signal_connect(y_fixed, "clicked", G_CALLBACK(on_scope_y_fixed_clicked), user);
|
|
|
|
|
GtkWidget* fit = row_btn("Fit Y axis");
|
|
|
|
|
g_signal_connect(fit, "clicked", G_CALLBACK(on_scope_fit_clicked), user);
|
|
|
|
|
|
|
|
|
|
// One removal per trace on the plot, flat rather than as a second view: a scope window holds
|
|
|
|
|
// at most its own figure's traces, and the menu is small.
|
|
|
|
|
bool traces = false;
|
|
|
|
|
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
|
|
|
|
const std::size_t si = static_cast<std::size_t>(s);
|
|
|
|
|
const std::string name = scope->rows->row(row).signal_name[si];
|
|
|
|
|
if (name.empty()) continue;
|
|
|
|
|
traces = true;
|
|
|
|
|
GtkWidget* rm = row_btn(("Remove '" + name + "'").c_str());
|
|
|
|
|
g_object_set_data(G_OBJECT(rm), "udpscope-slot", GINT_TO_POINTER(s));
|
|
|
|
|
g_signal_connect(rm, "clicked", G_CALLBACK(on_scope_remove_clicked), user);
|
|
|
|
|
}
|
|
|
|
|
if (!traces) {
|
|
|
|
|
GtkWidget* none = gtk_label_new("no traces on this plot");
|
|
|
|
|
gtk_widget_add_css_class(none, "dim-label");
|
|
|
|
|
gtk_widget_set_halign(none, GTK_ALIGN_START);
|
|
|
|
|
gtk_box_append(GTK_BOX(box), none);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (scope->detached) {
|
|
|
|
|
GtkWidget* back = row_btn("Attach back to the main window");
|
|
|
|
|
g_signal_connect(back, "clicked", G_CALLBACK(on_scope_attach_menu_clicked), user);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gtk_popover_set_child(GTK_POPOVER(popover), box);
|
|
|
|
|
GdkRectangle rect{static_cast<int>(x), static_cast<int>(y), 1, 1};
|
|
|
|
|
gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect);
|
|
|
|
|
gtk_widget_set_parent(popover, widget);
|
|
|
|
|
gtk_popover_popup(GTK_POPOVER(popover));
|
|
|
|
|
g_signal_connect(popover, "closed", G_CALLBACK(on_popover_closed), nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The availability of everything that does nothing until the stream has signals: the dock and the two
|
|
|
|
|
/// slide-over pages that were once in it (Cursors, Measurements). "Disable, do not hide": a page may be
|
|
|
|
|
/// opened by its key before the first CONFIG, and shows its controls insensitive until then. The signal
|
|
|
|
|