diff --git a/README.md b/README.md index 346dd7e..1133c26 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,11 @@ rect plus a `kMaximizeRevealPx` of slack, per row) and is invisible otherwise, s decorated by controls nobody is reaching for; one click makes that row fill the plot area and the same button restores the whole arrangement; the plot's right-click menu offers the same as **Full screen this figure** / **Restore the layout**, and a layout of a single -figure carries no button (there is nothing to enlarge *to*). It is a *view* — the row set +figure carries no button (there is nothing to enlarge *to*). Beside the toggle, and with the +same show-on-approach, a **detach** button moves the figure into a window of its own (removed +from the main window's rows, which save the change); the detached window carries an **attach** +button in the same corner — its figure returns to the main window's rows and the window closes. +It is a *view* — the row set saves every row's state on the way in and re-applies it on the way out (`RowState` in `plot_host.*`), so nothing plotted moves and nothing is saved. A plot's right-click menu has **Open this plot in a new window**, which copies that figure — its cells, their width shares and diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index b779a23..cb12384 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -1422,19 +1422,28 @@ Measured output (viewport pixels; the absolute numbers are the headless window's ## `[maximize-probe]` `UDPSCOPE_PROBE_MAXIMIZE_MS`. The one-click **full-screen row** toggle, exercised through the +`RowSet` call the button's handler makes. **Do not run it with `UDPSCOPE_ENABLE_ALL_MS`**: the +enable-all probe's second leg clears the front plot two seconds in, and this probe's `before` +read then names an empty row (`row0=`) -- a probe race, not a defect. The full-screen recipe: `RowSet` call the button's handler makes (a probe cannot click an overlay button the frame places), arranged as two single-cell rows with one signal each. The two reads after it are the **corner reveal**: no pointer over the area, then a pointer position in the first row's corner (set directly -- the probe owns the state the motion controller fills). Measured output: ``` -[maximize-probe] single_buttons=0 before: rows=2 frows=2 row0=sine row1=square buttons=2; max: rows=1 frows=1 flag=1 row0=sine; back: rows=2 row0=sine row1=square flag=0 -[maximize-probe] corner: hidden_when_off=1 shown_when_near=1 other_row=0 +[maximize-probe] single_buttons=0 detach_single=0 before: rows=2 frows=2 row0=sine row1=square buttons=2 detach=2; max: rows=1 frows=1 flag=1 row0=sine; back: rows=2 row0=sine row1=square flag=0 +[maximize-probe] corner: hidden_when_off=1 shown_when_near=1 other_row=0 detach_near=1 detach_other=0 ``` - `single_buttons=` is the toggle count *before* the second row is added, while the layout is the default one figure: **0** — one figure has nothing to enlarge *to*, so it carries no button (and `buttons=` is what the same count reads once the second row exists). + `detach_single=` is the same for the **detach** buttons: **0** — detaching the only figure + would leave the main window with nothing to draw — and `detach=2` once the second row exists + (and **0** again on the `max:` line, where a maximized layout has hidden its rows). +- the corner leg's `detach_near=1 detach_other=0` read the detach buttons with the same pointer + the maximize toggle reads: both come out together at the corner, the other row's stay hidden. + The pointer sits on the detach button's own centre — its zone is the strictest of the pair. - `before:` is the arrangement as set: two plots, two figure rows, the signals on their rows, and `buttons=` one overlay toggle per figure row (the toggles are on screen whether or not the layout editor is up). diff --git a/help/C/org.udpscope.app/layout.page b/help/C/org.udpscope.app/layout.page index 6624736..0caba3e 100644 --- a/help/C/org.udpscope.app/layout.page +++ b/help/C/org.udpscope.app/layout.page @@ -77,7 +77,10 @@ nothing is saved, nothing is dropped, and the rows' traces, colours and cursors come back where they were. The plot's right-click menu offers the same as Full screen this figure (and Restore the layout while one is enlarged). A layout of a single - figure has nothing to enlarge *to*, so it carries no button.

+ figure has nothing to enlarge *to*, so it carries no button.

Beside the toggle, shown the same way, a detach button moves the figure into a + window of its own — it leaves the main window's rows; the detached window carries an + attach button in the same corner, which returns the figure to the main window and + closes the window.

A plot's right-click menu has Open this plot in a new window: a second window showing the same stream through a copy of that figure (its cells, their widths and every trace), with its own axes following the main window's time window. The overflow menu's diff --git a/src/ui.cpp b/src/ui.cpp index d2b172a..dff420f 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -583,7 +583,7 @@ void relayout_areas(App& app) { /// about the data or the display mode. `figure_row` names the main window's figure to copy -- /// its cells, their width shares and every slot's signal in its colour -- because "open this /// one big" is the reason a user reaches for a second window; -1 opens an empty scope. -void open_scope_window(App& app, int figure_row) { +void open_scope_window(App& app, int figure_row, bool detached) { if (figure_row >= app.rows->figure_row_count()) figure_row = -1; auto scope = std::make_unique(); scope->rows = std::make_unique(); @@ -621,13 +621,60 @@ void open_scope_window(App& app, int figure_row) { 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)); + scope->detached = detached; scope->window = GTK_WINDOW(gtk_window_new()); - const std::string title = figure_row >= 0 - ? "udpscope \u2014 plot " + std::to_string(figure_row + 1) - : std::string("udpscope \u2014 scope"); + std::string title = figure_row >= 0 + ? "udpscope \u2014 plot " + std::to_string(figure_row + 1) + : std::string("udpscope \u2014 scope"); + if (detached) title += " (detached)"; gtk_window_set_title(scope->window, title.c_str()); gtk_window_set_default_size(scope->window, 900, 500); - gtk_window_set_child(scope->window, scope->widget); + if (detached) { + // The attach control, and it is the main window's own reveal mechanism in miniature: an + // overlay wrapping the GL area (so a revealed button sits *under* the pointer and the motion + // event still travels the ancestor -- a leave on the GL widget would fire the moment the + // button appeared and hide it again), and one button in the top-right corner, hidden until + // the pointer nears it. Only a detached window carries one: a copy or an empty window has + // nothing to put back. + scope->attach_overlay = gtk_overlay_new(); + gtk_overlay_set_child(GTK_OVERLAY(scope->attach_overlay), scope->widget); + gtk_window_set_child(scope->window, scope->attach_overlay); + GtkEventController* motion = gtk_event_controller_motion_new(); + g_signal_connect(motion, "motion", G_CALLBACK(+[](GtkEventControllerMotion*, double x, + double y, gpointer user) { + App::ScopeWindow* scope = static_cast(user); + scope->pointer_x = x; + scope->pointer_y = y; + scope->pointer_valid = true; + }), + scope.get()); + g_signal_connect(motion, "leave", G_CALLBACK(+[](GtkEventControllerMotion*, gpointer user) { + static_cast(user)->pointer_valid = false; + }), + scope.get()); + gtk_widget_add_controller(scope->attach_overlay, GTK_EVENT_CONTROLLER(motion)); + GtkWidget* attach = gtk_button_new_from_icon_name("view-restore-symbolic"); + gtk_widget_set_tooltip_text(attach, "attach this figure back to the main window"); + gtk_widget_add_css_class(attach, "circular"); + gtk_widget_add_css_class(attach, "osd"); + gtk_widget_set_size_request(attach, kLayoutButtonPx, kLayoutButtonPx); + gtk_widget_set_visible(attach, FALSE); + g_object_set_data(G_OBJECT(attach), "udpscope-scope", scope.get()); + g_signal_connect(attach, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) { + App& a = *static_cast(user); + auto* scope = + static_cast(g_object_get_data(G_OBJECT(b), + "udpscope-scope")); + if (scope == nullptr) return; + outlive_the_rebuild(GTK_WIDGET(b)); + attach_scope_window(a, *scope); + }), + &app); + gtk_overlay_add_overlay(GTK_OVERLAY(scope->attach_overlay), attach); + scope->attach_buttons.push_back(attach); + } else { + gtk_window_set_child(scope->window, scope->widget); + } if (GtkApplication* application = gtk_window_get_application(GTK_WINDOW(app.window))) { gtk_window_set_application(scope->window, application); } @@ -1025,6 +1072,7 @@ GtkWidget* layout_overlay_button(App& app, const char* icon, const char* tooltip /// Forward declarations: the toggles' handlers are defined after the builder that wires them. void on_maximize_clicked(GtkButton* b, gpointer user); +void on_detach_clicked(GtkButton* b, gpointer user); void on_fullscreen_row(GtkButton* b, gpointer user); void rebuild_maximize_buttons(App& app); @@ -1052,6 +1100,10 @@ void rebuild_maximize_buttons(App& app) { gtk_overlay_remove_overlay(GTK_OVERLAY(app.area_overlay), b); } app.maximize_buttons.clear(); + for (GtkWidget* b : app.detach_buttons) { + gtk_overlay_remove_overlay(GTK_OVERLAY(app.area_overlay), b); + } + app.detach_buttons.clear(); app.maximize_buttons_rows = app.rows->figure_row_count(); const bool maximized = app.rows->figures_maximized(); if (!maximized && app.maximize_buttons_rows <= 1) return; @@ -1063,6 +1115,17 @@ void rebuild_maximize_buttons(App& app) { app.maximize_buttons); g_object_set_data(G_OBJECT(b), kMaximizeKey, GINT_TO_POINTER(fr + 1)); g_signal_connect(b, "clicked", G_CALLBACK(on_maximize_clicked), &app); + // The detach button sits beside it, one rule behind it: a layout of one figure detaches + // nothing (the main window would be left with nothing to draw), and a maximized one has + // hidden its own rows anyway. + if (!maximized && app.maximize_buttons_rows >= 2) { + GtkWidget* d = overlay_button(app, "send-to-symbolic", + "detach this figure to a window of its own", + app.detach_buttons); + gtk_widget_set_name(d, "udpscope-detach-button"); + g_object_set_data(G_OBJECT(d), kMaximizeKey, GINT_TO_POINTER(fr + 1)); + g_signal_connect(d, "clicked", G_CALLBACK(on_detach_clicked), &app); + } } } @@ -1114,6 +1177,38 @@ void sync_maximize_buttons(App& app) { app.area_pointer_y <= by + kLayoutButtonPx + kMaximizeRevealPx; gtk_widget_set_visible(b, revealed); } + // The detach buttons, one per figure row to the **left** of its full-screen toggle: the same + // corner, the same reveal, the row's own rect -- a pointer arriving for one of the pair shows + // both, and leaving hides both. + for (GtkWidget* b : app.detach_buttons) { + const int fr = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kMaximizeKey)) - 1; + if (fr < 0 || fr >= frows || fr >= static_cast(fl.rows.size())) { + gtk_widget_set_visible(b, FALSE); + continue; + } + const int first = app.rows->first_plot_of_row(fr); + const int n = app.rows->cells_in_row(fr); + const plot::Plot* last = n > 0 ? app.rows->row(first + n - 1).plot : nullptr; + const double right = + last != nullptr + ? static_cast(last->layout().axes.right()) / scale + : static_cast(fl.rows[static_cast(fr)].right()) / scale; + const double top = + static_cast(fl.rows[static_cast(fr)].y) / scale + kLayoutButtonPad; + gtk_widget_set_halign(b, GTK_ALIGN_START); + gtk_widget_set_valign(b, GTK_ALIGN_START); + const double dx = std::clamp(right - 2 * kLayoutButtonPx - 2 * kLayoutButtonPad, 0.0, + std::max(0.0, width - kLayoutButtonPx)); + gtk_widget_set_margin_start(b, static_cast(dx)); + const double dy = std::clamp(top, 0.0, std::max(0.0, height - kLayoutButtonPx)); + gtk_widget_set_margin_top(b, static_cast(dy)); + const bool revealed = + app.area_pointer_valid && app.area_pointer_x >= dx - kMaximizeRevealPx && + app.area_pointer_x <= dx + kLayoutButtonPx + kMaximizeRevealPx && + app.area_pointer_y >= dy - kMaximizeRevealPx && + app.area_pointer_y <= dy + kLayoutButtonPx + kMaximizeRevealPx; + gtk_widget_set_visible(b, revealed); + } } /// The row's toggle: the `kMaximizeKey` data names the figure row it sits on. @@ -1131,6 +1226,100 @@ void on_fullscreen_row(GtkButton* b, gpointer user) { if (popover != nullptr) gtk_popover_popdown(popover); } +/// The detach button, the full-screen toggle's neighbour: the figure row it names moves to a +/// window of its own. +void on_detach_clicked(GtkButton* b, gpointer user) { + App& app = *static_cast(user); + detach_figure(app, GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kMaximizeKey)) - 1); +} + +/// The attach control's reveal and placement, one call per frame from the tick: the button sits at +/// the window's top-right corner and comes out of hiding while the pointer is near it -- the +/// placed rect inflated by `kMaximizeRevealPx`, the zone and the button measured from the same +/// numbers so they cannot disagree. +void sync_scope_attach(App& app, App::ScopeWindow& scope) { + (void)app; + if (scope.attach_overlay == nullptr || scope.attach_buttons.empty() || scope.widget == nullptr) { + return; + } + const double scale = scope.area != nullptr && scope.area->device_scale() > 0.0f + ? static_cast(scope.area->device_scale()) + : 1.0; + (void)scale; + const double width = static_cast(gtk_widget_get_width(scope.widget)); + const double height = static_cast(gtk_widget_get_height(scope.widget)); + for (GtkWidget* b : scope.attach_buttons) { + const double bx = std::clamp(static_cast(width - kLayoutButtonPx - kLayoutButtonPad), + 0.0, std::max(0.0, width - kLayoutButtonPx)); + const double by = std::clamp(static_cast(kLayoutButtonPad), 0.0, + std::max(0.0, height - kLayoutButtonPx)); + gtk_widget_set_halign(b, GTK_ALIGN_START); + gtk_widget_set_valign(b, GTK_ALIGN_START); + gtk_widget_set_margin_start(b, static_cast(bx)); + gtk_widget_set_margin_top(b, static_cast(by)); + const bool revealed = + scope.pointer_valid && scope.pointer_x >= bx - kMaximizeRevealPx && + scope.pointer_x <= bx + kLayoutButtonPx + kMaximizeRevealPx && + scope.pointer_y >= by - kMaximizeRevealPx && + scope.pointer_y <= by + kLayoutButtonPx + kMaximizeRevealPx; + gtk_widget_set_visible(b, revealed); + } +} + +/// **Detach**: the figure row moves to a window of its own. Copied first (the new window opens +/// carrying exactly what was on screen), then removed from the main window's rows -- the copy and +/// the removal are one action in one call, so nothing renders in between. A detached window is the +/// one kind with an attach control; the arrangement the user had follows it there. +void detach_figure(App& app, int fr) { + if (app.rows->figures_maximized() || app.rows->figure_row_count() <= 1) return; + if (fr < 0 || fr >= app.rows->figure_row_count()) return; + const std::string label = app.rows->plot_label(app.rows->first_plot_of_row(fr)); + open_scope_window(app, fr, /*detached=*/true); + if (!app.rows->remove_row(fr)) return; + if (app.area != nullptr) app.area->set_figure(&app.rows->figure()); + rebuild_maximize_buttons(app); + rebuild_signal_panels(app); + if (app.layout_editing) rebuild_layout_editor(app); + schedule_layout_save(app); + set_notice(app, "plot " + label + " detached to its own window", 4); +} + +/// **Attach**: a detached window's figure row is appended to the main window's -- the window's +/// cells, their weights and every slot's signal in its colour -- and the window closes. Closing +/// runs its own teardown (the pages' *Plot on* groups are refreshed before the window's struct is +/// freed), so nothing here may touch `scope` once the destroy has been asked for. +void attach_scope_window(App& app, App::ScopeWindow& scope) { + if (!scope.detached || scope.rows == nullptr || scope.rows->row_count() <= 0) return; + const int at = app.rows->figure_row_count(); + if (!app.rows->insert_row(at)) { + set_notice(app, "no room for another figure row", 4); + return; + } + const int cells = scope.rows->cells_in_row(0); + for (int k = 1; k < cells; ++k) { + if (!app.rows->insert_cell(at, k)) break; + } + if (cells > 1) app.rows->set_cell_weights(at, scope.rows->cell_weights(0)); + const int first = app.rows->first_plot_of_row(at); + for (int k = 0; k < cells; ++k) { + const RowSet::Row& src = scope.rows->row(k); + for (int s = 0; s < RowSet::kSlotsPerRow; ++s) { + const std::size_t si = static_cast(s); + if (src.signal_name[si].empty()) continue; + app.rows->assign(first + k, s, src.signal_name[si], src.name[si], src.color_index[si]); + } + } + GtkWindow* window = scope.window; + if (window != nullptr) gtk_window_destroy(window); // erases the scope, refreshes the pages + app.relayout_pending = true; + if (app.area != nullptr) app.area->set_figure(&app.rows->figure()); + rebuild_maximize_buttons(app); + rebuild_signal_panels(app); + if (app.layout_editing) rebuild_layout_editor(app); + schedule_layout_save(app); + set_notice(app, "the detached figure is back on the main window", 4); +} + /// The plot menu's way into the layout editor: the same toggle the overflow menu's `Layout editor` /// entry flips, so the menu cannot drift from the button's own state -- and the place a user who is /// looking at the plot will find splitting. @@ -4255,6 +4444,7 @@ gboolean tick(gpointer user) { if (scope->rows->rebuild_if_dirty() && scope->area != nullptr) { scope->area->set_figure(&scope->rows->figure()); } + if (scope->detached) sync_scope_attach(*app, *scope); 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 6221c0c..d2a3ac1 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -221,6 +221,18 @@ struct App { /// The drop target wired onto `widget`: the handler reads the *window* off it (each window's /// target names its own scope), and the probe calls the handler with it directly. GtkDropTarget* drop = nullptr; + /// This window came from a **detach** -- the figure left the main window to live here -- so it + /// carries the attach control and can be put back. A copied or empty window is a plain second + /// view, and has no attach control. + bool detached = false; + /// The attach control: an overlay wrapping the GL widget, its button in the top-right corner + /// (revealed by pointer proximity, the same rule the main window's toggles follow), and the + /// pointer state the overlay's own motion controller keeps. + GtkWidget* attach_overlay = nullptr; + std::vector attach_buttons; + double pointer_x = 0.0; + double pointer_y = 0.0; + bool pointer_valid = false; /// 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. @@ -243,6 +255,10 @@ struct App { /// count changes (`maximize_buttons_rows` is the count they were built for). std::vector maximize_buttons; int maximize_buttons_rows = -1; + /// The **detach** buttons: one per figure row beside its full-screen toggle, same reveal, + /// same rebuild cadence -- but only while the layout holds two or more figures (detaching the + /// only figure would leave the main window with nothing to draw) and never while maximized. + std::vector detach_buttons; /// The boundary a resize drag is on, and the weights it started from: a drag is *relative* to where it /// began, so the beginning has to be kept -- reading the weights while dragging would compound. int layout_drag_row = -1; @@ -878,6 +894,7 @@ struct App { int dbg_maximize_step = 0; std::size_t dbg_maximize_single = 0; int dbg_maximize_hidden = -1; + std::size_t dbg_detach_single = 0; /// Which step of the scope-window probe runs next (`UDPSCOPE_PROBE_SCOPE_MS`). int dbg_scope_step = 0; int cursors_pane_mode = -1; @@ -2161,7 +2178,16 @@ gboolean scope_probe_step(gpointer user); /// A second top-level window showing the same stream through its own figure: `figure_row` >= 0 /// copies that figure of the main window (cells, widths, every slot's signal), -1 opens an /// empty one-row scope. The main window's tick binds, acquires and repaints it. -void open_scope_window(App& app, int figure_row); +void open_scope_window(App& app, int figure_row, bool detached = false); +/// Detaches figure row `fr` into a window of its own: the figure is copied into a new scope +/// window -- which is what makes the window *detached*, the one kind with an attach control -- +/// and removed from the main window's rows. Refused, changing nothing, for a one-figure layout +/// or a maximized one: detaching then would leave the main window with nothing to draw. +void detach_figure(App& app, int fr); +/// Puts a detached window's figure back: a figure row is appended to the main window carrying +/// the window's arrangement (cells, weights, every slot's signal in its colour) and the window +/// closes. The window must be a detached one; anything else returns without changing. +void attach_scope_window(App& app, App::ScopeWindow& scope); gboolean measurepin_probe(gpointer user); gboolean measurepin_held_wait(gpointer user); gboolean measurepin_pause_wait(gpointer user); diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index 2af228b..e7817d3 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -1651,6 +1651,7 @@ gboolean maximize_probe_step(gpointer user) { // A layout of one figure carries no full-screen toggle (there is nothing to enlarge // *to*); the button appears together with the second figure row, below. a->dbg_maximize_single = a->maximize_buttons.size(); + a->dbg_detach_single = a->detach_buttons.size(); a->rows->set_cells_per_row({1, 1}); a->rows->assign(0, 0, sine, sine, sine_color); a->rows->assign(1, 0, square, square, square_color); @@ -1684,11 +1685,13 @@ gboolean maximize_probe_step(gpointer user) { const std::string row1_back = a->rows->row(1).signal_name[0]; const bool flag_back = a->rows->figures_maximized(); std::fprintf(stderr, - "[maximize-probe] single_buttons=%zu before: rows=%d frows=%d row0=%s row1=%s " - "buttons=%zu; max: rows=%d frows=%d flag=%d row0=%s; " + "[maximize-probe] single_buttons=%zu detach_single=%zu before: rows=%d frows=%d " + "row0=%s row1=%s buttons=%zu detach=%zu; max: rows=%d frows=%d flag=%d row0=%s; " "back: rows=%d row0=%s row1=%s flag=%d\n", - a->dbg_maximize_single, rows_before, frows_before, row0_before.c_str(), - row1_before.c_str(), buttons, rows_max, frows_max, flag_max ? 1 : 0, + a->dbg_maximize_single, a->dbg_detach_single, rows_before, frows_before, + row0_before.c_str(), + row1_before.c_str(), buttons, a->detach_buttons.size(), rows_max, frows_max, + flag_max ? 1 : 0, row0_max.c_str(), rows_back, row0_back.c_str(), row1_back.c_str(), flag_back ? 1 : 0); std::fflush(stderr); @@ -1703,25 +1706,37 @@ gboolean maximize_probe_step(gpointer user) { case 2: { a->dbg_maximize_hidden = a->maximize_buttons.empty() || !gtk_widget_get_visible(a->maximize_buttons[0]) ? 1 : 0; - a->area_pointer_x = static_cast(gtk_widget_get_width(a->area_widget)) - 8.0; - a->area_pointer_y = 8.0; + // On the **detach** button's own centre -- its zone is the strictest of the corner pair, so a + // pointer there must reveal both it and the full-screen toggle beside it. + if (const plot::Plot* last = a->rows->row(0).plot) { + const double scale = + a->area != nullptr && a->area->device_scale() > 0.0f + ? static_cast(a->area->device_scale()) + : 1.0; + const double right = static_cast(last->layout().axes.right()) / scale; + a->area_pointer_x = right - 2 * kLayoutButtonPx - 2 * kLayoutButtonPad + + 0.5 * kLayoutButtonPx; + a->area_pointer_y = kLayoutButtonPad + 0.5 * kLayoutButtonPx; + } else { + a->area_pointer_x = static_cast(gtk_widget_get_width(a->area_widget)) - 8.0; + a->area_pointer_y = 8.0; + } a->area_pointer_valid = true; a->dbg_maximize_step = 3; g_timeout_add(250, maximize_probe_step, a); break; } case 3: { - const int shown = !a->maximize_buttons.empty() && - gtk_widget_get_visible(a->maximize_buttons[0]) - ? 1 - : 0; - const int other = a->maximize_buttons.size() > 1 && - gtk_widget_get_visible(a->maximize_buttons[1]) - ? 1 - : 0; + const auto visible = [](const std::vector& buttons, std::size_t i) { + return i < buttons.size() && gtk_widget_get_visible(buttons[i]) ? 1 : 0; + }; + const int shown = visible(a->maximize_buttons, 0); + const int other = visible(a->maximize_buttons, 1); + const int dshown = visible(a->detach_buttons, 0); + const int dother = visible(a->detach_buttons, 1); std::fprintf(stderr, "[maximize-probe] corner: hidden_when_off=%d shown_when_near=%d " - "other_row=%d\n", - a->dbg_maximize_hidden, shown, other); + "other_row=%d detach_near=%d detach_other=%d\n", + a->dbg_maximize_hidden, shown, other, dshown, dother); std::fflush(stderr); return G_SOURCE_REMOVE; } @@ -1824,6 +1839,34 @@ gboolean scope_probe_step(gpointer user) { std::fprintf(stderr, "[scope-probe] drop=%d slot1='%s' page_groups=%d\n", dropped ? 1 : 0, slot1.c_str(), groups); + // **Detach and attach**: a second figure row detaches -- the window is marked detached and + // carries the attach control -- and its figure comes back on the main window. + a->rows->set_cells_per_row({1, 1}); + { + std::size_t ramp = std::size_t(-1); + int color = 0; + for (std::size_t i = 0; i < a->model->signals().size(); ++i) { + if (a->model->signals()[i].name == "ramp") { + ramp = i; + color = signal_color_index(a->model->signals(), i); + } + } + if (ramp != std::size_t(-1)) a->rows->assign(1, 0, "ramp", "ramp", color); + } + a->relayout_pending = true; + detach_figure(*a, 1); + const bool detached_ok = a->scopes.size() == 3 && a->scopes[2]->detached && + a->rows->figure_row_count() == 1 && + a->scopes[2]->rows->row(0).signal_name[0] == "ramp" && + a->scopes[2]->attach_buttons.size() == 1; + attach_scope_window(*a, *a->scopes[2]); + const int frows_back = a->rows->figure_row_count(); + const bool attach_ok = a->scopes.size() == 2 && frows_back == 2 && + a->rows->row(a->rows->first_plot_of_row(1)).signal_name[0] == "ramp"; + std::fprintf(stderr, + "[scope-probe] detach=%d scopes=3 frows=1 attach=%d scopes_back=2 " + "frows_back=%d slot='ramp'\n", + detached_ok ? 1 : 0, attach_ok ? 1 : 0, frows_back); std::fflush(stderr); return G_SOURCE_REMOVE; } diff --git a/src/ui_tour.cpp b/src/ui_tour.cpp index 09d01e8..59a7f15 100644 --- a/src/ui_tour.cpp +++ b/src/ui_tour.cpp @@ -443,8 +443,8 @@ constexpr TourStep kTourSteps[] = { {"plot", nullptr, "The plot", "Every signal you have switched on is drawn here, all plots sharing one time axis -- and the " "plots of a split row share one y as well. The wheel zooms, a drag pans, and a right-click on " - "a plot opens its own menu: fit, full screen (the button also comes out in a figure's corner " - "as the pointer nears it), a second window showing that figure, and the layout editor, which " + "a plot opens its own menu: fit, full screen and detach (the corner buttons come out as the " + "pointer nears them), a second window showing that figure, and the layout editor, which " "splits a row into up to four plots.", &App::area_overlay}, {"badges", nullptr, "The badges",