Gated measurements #2
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
|
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
|
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
|
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
|
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
|
`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
|
**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]`
|
## `[maximize-probe]`
|
||||||
|
|
||||||
`UDPSCOPE_PROBE_MAXIMIZE_MS`. The one-click **full-screen row** toggle, exercised through the
|
`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
|
`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
|
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
|
**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:
|
(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] 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
|
[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
|
- `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
|
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).
|
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,
|
- `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
|
and `buttons=` one overlay toggle per figure row (the toggles are on screen whether or not
|
||||||
the layout editor is up).
|
the layout editor is up).
|
||||||
|
|||||||
@@ -77,7 +77,10 @@
|
|||||||
nothing is saved, nothing is dropped, and the rows' traces, colours and cursors come back
|
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
|
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</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
|
<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
|
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
|
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 --
|
/// 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
|
/// 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.
|
/// 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;
|
if (figure_row >= app.rows->figure_row_count()) figure_row = -1;
|
||||||
auto scope = std::make_unique<App::ScopeWindow>();
|
auto scope = std::make_unique<App::ScopeWindow>();
|
||||||
scope->rows = std::make_unique<RowSet>();
|
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_object_set_data(G_OBJECT(scope->drop), "udpscope-scope", scope.get());
|
||||||
g_signal_connect(scope->drop, "drop", G_CALLBACK(on_scope_drop_channel), &app);
|
g_signal_connect(scope->drop, "drop", G_CALLBACK(on_scope_drop_channel), &app);
|
||||||
gtk_widget_add_controller(scope->widget, GTK_EVENT_CONTROLLER(scope->drop));
|
gtk_widget_add_controller(scope->widget, GTK_EVENT_CONTROLLER(scope->drop));
|
||||||
|
scope->detached = detached;
|
||||||
scope->window = GTK_WINDOW(gtk_window_new());
|
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)
|
? "udpscope \u2014 plot " + std::to_string(figure_row + 1)
|
||||||
: std::string("udpscope \u2014 scope");
|
: std::string("udpscope \u2014 scope");
|
||||||
|
if (detached) title += " (detached)";
|
||||||
gtk_window_set_title(scope->window, title.c_str());
|
gtk_window_set_title(scope->window, title.c_str());
|
||||||
gtk_window_set_default_size(scope->window, 900, 500);
|
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);
|
gtk_window_set_child(scope->window, scope->widget);
|
||||||
|
}
|
||||||
if (GtkApplication* application = gtk_window_get_application(GTK_WINDOW(app.window))) {
|
if (GtkApplication* application = gtk_window_get_application(GTK_WINDOW(app.window))) {
|
||||||
gtk_window_set_application(scope->window, application);
|
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.
|
/// Forward declarations: the toggles' handlers are defined after the builder that wires them.
|
||||||
void on_maximize_clicked(GtkButton* b, gpointer user);
|
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 on_fullscreen_row(GtkButton* b, gpointer user);
|
||||||
void rebuild_maximize_buttons(App& app);
|
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);
|
gtk_overlay_remove_overlay(GTK_OVERLAY(app.area_overlay), b);
|
||||||
}
|
}
|
||||||
app.maximize_buttons.clear();
|
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();
|
app.maximize_buttons_rows = app.rows->figure_row_count();
|
||||||
const bool maximized = app.rows->figures_maximized();
|
const bool maximized = app.rows->figures_maximized();
|
||||||
if (!maximized && app.maximize_buttons_rows <= 1) return;
|
if (!maximized && app.maximize_buttons_rows <= 1) return;
|
||||||
@@ -1063,6 +1115,17 @@ void rebuild_maximize_buttons(App& app) {
|
|||||||
app.maximize_buttons);
|
app.maximize_buttons);
|
||||||
g_object_set_data(G_OBJECT(b), kMaximizeKey, GINT_TO_POINTER(fr + 1));
|
g_object_set_data(G_OBJECT(b), kMaximizeKey, GINT_TO_POINTER(fr + 1));
|
||||||
g_signal_connect(b, "clicked", G_CALLBACK(on_maximize_clicked), &app);
|
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;
|
app.area_pointer_y <= by + kLayoutButtonPx + kMaximizeRevealPx;
|
||||||
gtk_widget_set_visible(b, revealed);
|
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.
|
/// 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);
|
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`
|
/// 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
|
/// 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.
|
/// looking at the plot will find splitting.
|
||||||
@@ -4255,6 +4444,7 @@ gboolean tick(gpointer user) {
|
|||||||
if (scope->rows->rebuild_if_dirty() && scope->area != nullptr) {
|
if (scope->rows->rebuild_if_dirty() && scope->area != nullptr) {
|
||||||
scope->area->set_figure(&scope->rows->figure());
|
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));
|
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
|
/// 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.
|
/// target names its own scope), and the probe calls the handler with it directly.
|
||||||
GtkDropTarget* drop = nullptr;
|
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
|
/// 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
|
/// outlives the widget, and sharing the main window's placeholder would hand two areas
|
||||||
/// one plot the moment the figure mode is ever left.
|
/// 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).
|
/// count changes (`maximize_buttons_rows` is the count they were built for).
|
||||||
std::vector<GtkWidget*> maximize_buttons;
|
std::vector<GtkWidget*> maximize_buttons;
|
||||||
int maximize_buttons_rows = -1;
|
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
|
/// 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.
|
/// began, so the beginning has to be kept -- reading the weights while dragging would compound.
|
||||||
int layout_drag_row = -1;
|
int layout_drag_row = -1;
|
||||||
@@ -878,6 +894,7 @@ struct App {
|
|||||||
int dbg_maximize_step = 0;
|
int dbg_maximize_step = 0;
|
||||||
std::size_t dbg_maximize_single = 0;
|
std::size_t dbg_maximize_single = 0;
|
||||||
int dbg_maximize_hidden = -1;
|
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`).
|
/// Which step of the scope-window probe runs next (`UDPSCOPE_PROBE_SCOPE_MS`).
|
||||||
int dbg_scope_step = 0;
|
int dbg_scope_step = 0;
|
||||||
int cursors_pane_mode = -1;
|
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
|
/// 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
|
/// 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.
|
/// 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_probe(gpointer user);
|
||||||
gboolean measurepin_held_wait(gpointer user);
|
gboolean measurepin_held_wait(gpointer user);
|
||||||
gboolean measurepin_pause_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
|
// 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.
|
// *to*); the button appears together with the second figure row, below.
|
||||||
a->dbg_maximize_single = a->maximize_buttons.size();
|
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->set_cells_per_row({1, 1});
|
||||||
a->rows->assign(0, 0, sine, sine, sine_color);
|
a->rows->assign(0, 0, sine, sine, sine_color);
|
||||||
a->rows->assign(1, 0, square, square, square_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 std::string row1_back = a->rows->row(1).signal_name[0];
|
||||||
const bool flag_back = a->rows->figures_maximized();
|
const bool flag_back = a->rows->figures_maximized();
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[maximize-probe] single_buttons=%zu before: rows=%d frows=%d row0=%s row1=%s "
|
"[maximize-probe] single_buttons=%zu detach_single=%zu before: rows=%d frows=%d "
|
||||||
"buttons=%zu; max: rows=%d frows=%d flag=%d row0=%s; "
|
"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",
|
"back: rows=%d row0=%s row1=%s flag=%d\n",
|
||||||
a->dbg_maximize_single, rows_before, frows_before, row0_before.c_str(),
|
a->dbg_maximize_single, a->dbg_detach_single, rows_before, frows_before,
|
||||||
row1_before.c_str(), buttons, rows_max, frows_max, flag_max ? 1 : 0,
|
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(),
|
row0_max.c_str(), rows_back, row0_back.c_str(), row1_back.c_str(),
|
||||||
flag_back ? 1 : 0);
|
flag_back ? 1 : 0);
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
@@ -1703,25 +1706,37 @@ gboolean maximize_probe_step(gpointer user) {
|
|||||||
case 2: {
|
case 2: {
|
||||||
a->dbg_maximize_hidden =
|
a->dbg_maximize_hidden =
|
||||||
a->maximize_buttons.empty() || !gtk_widget_get_visible(a->maximize_buttons[0]) ? 1 : 0;
|
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_x = static_cast<double>(gtk_widget_get_width(a->area_widget)) - 8.0;
|
||||||
a->area_pointer_y = 8.0;
|
a->area_pointer_y = 8.0;
|
||||||
|
}
|
||||||
a->area_pointer_valid = true;
|
a->area_pointer_valid = true;
|
||||||
a->dbg_maximize_step = 3;
|
a->dbg_maximize_step = 3;
|
||||||
g_timeout_add(250, maximize_probe_step, a);
|
g_timeout_add(250, maximize_probe_step, a);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 3: {
|
case 3: {
|
||||||
const int shown = !a->maximize_buttons.empty() &&
|
const auto visible = [](const std::vector<GtkWidget*>& buttons, std::size_t i) {
|
||||||
gtk_widget_get_visible(a->maximize_buttons[0])
|
return i < buttons.size() && gtk_widget_get_visible(buttons[i]) ? 1 : 0;
|
||||||
? 1
|
};
|
||||||
: 0;
|
const int shown = visible(a->maximize_buttons, 0);
|
||||||
const int other = a->maximize_buttons.size() > 1 &&
|
const int other = visible(a->maximize_buttons, 1);
|
||||||
gtk_widget_get_visible(a->maximize_buttons[1])
|
const int dshown = visible(a->detach_buttons, 0);
|
||||||
? 1
|
const int dother = visible(a->detach_buttons, 1);
|
||||||
: 0;
|
|
||||||
std::fprintf(stderr, "[maximize-probe] corner: hidden_when_off=%d shown_when_near=%d "
|
std::fprintf(stderr, "[maximize-probe] corner: hidden_when_off=%d shown_when_near=%d "
|
||||||
"other_row=%d\n",
|
"other_row=%d detach_near=%d detach_other=%d\n",
|
||||||
a->dbg_maximize_hidden, shown, other);
|
a->dbg_maximize_hidden, shown, other, dshown, dother);
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
@@ -1824,6 +1839,34 @@ gboolean scope_probe_step(gpointer user) {
|
|||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[scope-probe] drop=%d slot1='%s' page_groups=%d\n",
|
"[scope-probe] drop=%d slot1='%s' page_groups=%d\n",
|
||||||
dropped ? 1 : 0, slot1.c_str(), groups);
|
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);
|
std::fflush(stderr);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -443,8 +443,8 @@ constexpr TourStep kTourSteps[] = {
|
|||||||
{"plot", nullptr, "The plot",
|
{"plot", nullptr, "The plot",
|
||||||
"Every signal you have switched on is drawn here, all plots sharing one time axis -- and the "
|
"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 "
|
"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 "
|
"a plot opens its own menu: fit, full screen and detach (the corner buttons come out as the "
|
||||||
"as the pointer nears it), a second window showing that figure, and the layout editor, which "
|
"pointer nears them), a second window showing that figure, and the layout editor, which "
|
||||||
"splits a row into up to four plots.",
|
"splits a row into up to four plots.",
|
||||||
&App::area_overlay},
|
&App::area_overlay},
|
||||||
{"badges", nullptr, "The badges",
|
{"badges", nullptr, "The badges",
|
||||||
|
|||||||
Reference in new issue
Block a user