diff --git a/README.md b/README.md index 65a520b..d1b3df7 100644 --- a/README.md +++ b/README.md @@ -146,8 +146,11 @@ mode or the span. **A signal badge dragged onto a scope window's plot lands ther drop the main plot takes, into that window's own row set, on the cell the pointer is over — and the signal page's *Plot on* buttons list **every window**: one group per window (named, `In "plot 2"`), one button per plot of it. A right-click on a scope window's plot opens **its own -menu** — y axis (auto or a fixed range), fit, remove a trace — acting on that window's rows; a -detached window's menu carries **Attach back to the main window** beside them. +menu** — y axis (auto or a fixed range), *Fit X axis*, *Fit all*, remove a trace — acting on that +window's rows (the x fit resets the **shared** time span, so every view follows); a detached +window's menu carries **Attach back to the main window** beside them. A window that cannot paint +says so in its own title after two seconds (and on stderr): a blank plot is indistinguishable from +a context that failed, and the sentence names which. mode or the span. **The window.** The plot is the window. Under it sits a fixed-height **badge bar** (36 px). Its left diff --git a/src/ui.cpp b/src/ui.cpp index c0308a5..d615d0b 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -3711,15 +3711,38 @@ void on_scope_y_fixed_clicked(GtkButton* b, gpointer user) { 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) { +/// The y fit of a scope window: the y axes alone. +void on_scope_fit_y_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 x fit of a scope window is the main window's: there is **one** time span for every view, +/// and the scope mirrors it -- so the fit resets the shared x range and the next frame mirrors +/// the result back, in this window and in every other. +void on_scope_fit_x_clicked(GtkButton* b, gpointer user) { + App& app = *static_cast(user); + app.rows->fit_x(); + app.notice = "x back to the " + format_window(app.rows->time_window()) + " window"; + app.notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND; + scope_menu_dismiss(b); +} + +/// Both fits: this window's y, the shared x. +void on_scope_fit_all_clicked(GtkButton* b, gpointer user) { + App& app = *static_cast(user); + App::ScopeWindow* scope = scope_menu_scope(b); + if (scope == nullptr || scope->rows == nullptr) return; + scope->rows->set_y_auto(scope_menu_row(b), true); + app.rows->fit_x(); + app.notice = "y fitted to the data, x back to the " + + format_window(app.rows->time_window()) + " window"; + app.notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND; + 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; @@ -3785,8 +3808,10 @@ void on_scope_right_click(GtkGestureClick* gesture, int, double x, double y, gpo 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); + GtkWidget* fit_x = row_btn("Fit X axis"); + g_signal_connect(fit_x, "clicked", G_CALLBACK(on_scope_fit_x_clicked), user); + GtkWidget* fit = row_btn("Fit all"); + g_signal_connect(fit, "clicked", G_CALLBACK(on_scope_fit_all_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. @@ -4610,6 +4635,31 @@ gboolean tick(gpointer user) { // scope->area->set_figure(&scope->rows->figure()); // } if (scope->detached) sync_scope_attach(*app, *scope); + // **A window that cannot paint says so.** A blank plot is indistinguishable from a + // context that failed or an area that never got a frame, and the report ("nothing is + // drawn, no axes") is exactly what this turns into a sentence: the window's own title + // carries the reason, and stderr carries it with the window's name. + if (scope->area != nullptr && scope->paint_checks >= 0 && scope->paint_checks < 120) { + ++scope->paint_checks; + if (scope->paint_checks == 120) { + const plot::Status st = scope->area->init_status(); + const plot::u64 painted = scope->area->frames_painted(); + if (!st || painted == 0) { + const std::string why = + st ? std::string("the plot painted no frame in two seconds") + : std::string("GL context failed: ") + st.message; + std::fprintf(stderr, "[scope] window '%s' is not drawing: %s\n", + scope->name.c_str(), why.c_str()); + std::fflush(stderr); + if (scope->window != nullptr) { + const char* current = gtk_window_get_title(scope->window); + gtk_window_set_title(scope->window, + ((current != nullptr ? std::string(current) + " — " : "") + why) + .c_str()); + } + } + } + } if (scope->widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(scope->widget)); } } diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 7d67ad0..560e9e6 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -233,6 +233,10 @@ struct App { double pointer_x = 0.0; double pointer_y = 0.0; bool pointer_valid = false; + /// Frames this window's area was watched for its first paint: a window whose GL context failed + /// or that never paints says so in its own title (and on stderr) instead of staying a blank + /// rectangle nobody can diagnose. + int paint_checks = 0; /// The area's initial plot, owned here: `PlotArea::create()` wants a `Plot&` that /// outlives the widget, and sharing the main window's placeholder would hand two areas /// one plot the moment the figure mode is ever left.