layout: detach a figure to its own window, and attach it back
Beside each figure row's full-screen toggle -- same corner, same show-on-approach reveal, same one-figure/never-maximized rules -- a **detach** button moves the figure into a window of its own: copied into a new scope window (titled "detached") and removed from the main window's rows, which then save the change. The detached window is the one scope window with an **attach** control: an overlay wrapping its GL area, a button in the top-right corner revealed by the same pointer-proximity rule from the overlay's own motion controller, and one click appends the figure back to the main window's rows (cells, weights, every slot's signal in its colour) and closes the window. `detach_figure()` / `attach_scope_window()`; the reveal and placement share the maximize buttons' geometry and pointer state, per row. A detached window's rows participate everywhere a scope window's do (drops, the signal page's *Plot on* groups), which the previous commit gave them. Probe: `[maximize-probe]` now counts the detach buttons (`detach_single=0` for one figure, `detach=2` for two, absent while maximized) and reads their corner reveal (`detach_near=1 detach_other=0`, the pointer parked on the detach button's own centre -- the strictest zone of the pair). Real-pointer smoke: hover reveals the pair, clicking detach opens the window, clicking attach closes it with the figure back. Docs: README, the Mallard layout page, the probe readings, the layout invariant, and the tour's plot stop.
This commit is contained in:
1 parent
51b4fad386
commit
5ed75eb470
7 files changed
+298
-23
No files matched your search
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 <gui>Full screen this
|
||||
figure</gui> (and <gui>Restore the layout</gui> while one is enlarged). A layout of a single
|
||||
figure has nothing to enlarge *to*, so it carries no button.</p></item>
|
||||
figure has nothing to enlarge *to*, so it carries no button.</p></item> Beside the toggle, shown the same way, a <gui>detach</gui> button moves the figure into a
|
||||
window of its own — it leaves the main window's rows; the detached window carries an
|
||||
<gui>attach</gui> button in the same corner, which returns the figure to the main window and
|
||||
closes the window.
|
||||
<item><p>A plot's right-click menu has <gui>Open this plot in a new window</gui>: 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
|
||||
|
||||
+192
-2
@@ -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<App::ScopeWindow>();
|
||||
scope->rows = std::make_unique<RowSet>();
|
||||
@@ -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
|
||||
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);
|
||||
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<App::ScopeWindow*>(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<App::ScopeWindow*>(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<App*>(user);
|
||||
auto* scope =
|
||||
static_cast<App::ScopeWindow*>(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<int>(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<double>(last->layout().axes.right()) / scale
|
||||
: static_cast<double>(fl.rows[static_cast<std::size_t>(fr)].right()) / scale;
|
||||
const double top =
|
||||
static_cast<double>(fl.rows[static_cast<std::size_t>(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<int>(dx));
|
||||
const double dy = std::clamp(top, 0.0, std::max(0.0, height - kLayoutButtonPx));
|
||||
gtk_widget_set_margin_top(b, static_cast<int>(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<App*>(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<double>(scope.area->device_scale())
|
||||
: 1.0;
|
||||
(void)scale;
|
||||
const double width = static_cast<double>(gtk_widget_get_width(scope.widget));
|
||||
const double height = static_cast<double>(gtk_widget_get_height(scope.widget));
|
||||
for (GtkWidget* b : scope.attach_buttons) {
|
||||
const double bx = std::clamp(static_cast<double>(width - kLayoutButtonPx - kLayoutButtonPad),
|
||||
0.0, std::max(0.0, width - kLayoutButtonPx));
|
||||
const double by = std::clamp(static_cast<double>(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<int>(bx));
|
||||
gtk_widget_set_margin_top(b, static_cast<int>(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<std::size_t>(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));
|
||||
}
|
||||
}
|
||||
|
||||
+27
-1
@@ -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<GtkWidget*> 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<GtkWidget*> 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<GtkWidget*> 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);
|
||||
|
||||
+57
-14
@@ -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;
|
||||
// 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<double>(a->area->device_scale())
|
||||
: 1.0;
|
||||
const double right = static_cast<double>(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<double>(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<GtkWidget*>& 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;
|
||||
}
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user