scope windows: their own layout editor, and the same shortcuts
A detached window is a view with an arrangement of its own -- and there was no way to change it:
the layout editor was the main window's, and a second window's rows could not be added or removed.
Every scope window now carries its own editor: `L` (or the grid toggle in its corner, revealed by
the same proximity rule as the attach button) shows the main editor's `+` / `×` / Done controls,
and they act on **that window's** row set (`set_scope_layout_editing()`, every button tagged with
its window), so a window holds as many figures as the user gives it and the two editors cannot
move each other's rows. Nothing a scope window arranges is written to the main window's session:
the windows themselves are not persisted.
The keyboard: each scope window gets a `GtkEventControllerKey` running the application's one
accelerator table -- `L` acts on the window the key arrived in, every other action reaches the
application through `run_app_action()`. The plot's own keys ('f', the arrows, Escape) already
worked there, through its plot controller.
The window struct now always builds its overlay (the corner controls' host); the attach button
stays detached-only, and both corner toggles step aside while the editor is up (Done is the way
out).
Probe: `[scope-probe] layout buttons=3 rows=1->2` -- the toggle builds 2*rows+1 buttons and a
boundary `+` adds a row. `L` in a real scope window verified by OCR ("Done" appears, the row's
`×` at its left). Docs: README, the Mallard layout page, the probe readings, and the layout
invariant (a view's arrangement is the view's).
This commit is contained in:
1 parent
ecdbf4f0e1
commit
0c9333858c
6 files changed
+315
-25
No files matched your search
@@ -145,9 +145,11 @@ every frame, and pumps its render — so the two windows never disagree about th
|
||||
mode or the span. **A signal badge dragged onto a scope window's plot lands there** — the same
|
||||
drop the main plot takes, into that window's own row set, on the cell the pointer is over — and
|
||||
the signal page's *Plot on* buttons list **every window**: one group per window (named, `In
|
||||
"plot 2"`), one button per plot of it. A right-click on a scope window's plot opens **its own
|
||||
menu** — y axis (auto or a fixed range), *Fit X axis*, *Fit all*, remove a trace — acting on that
|
||||
window's rows (the x fit resets the **shared** time span, so every view follows); a detached
|
||||
"plot 2"`), one button per plot of it. A scope window also **carries its own layout editor**: `L` (or the grid button in its corner,
|
||||
revealed like the others) shows the same `+`/`×`/Done controls the main window's editor has, and
|
||||
they change *that* window's rows — so a second window can hold several figures as easily as the
|
||||
main one. A right-click on a scope window's plot opens **its own menu** — y axis (auto or a fixed
|
||||
range), *Fit X axis*, *Fit all*, remove a trace — acting on that window's rows (the x fit resets the **shared** time span, so every view follows); a detached
|
||||
window's menu carries **Attach back to the main window** beside them. A window that cannot paint
|
||||
says so on stderr and in the status bar after two seconds, naming its **GSK renderer**: a blank
|
||||
plot is indistinguishable from a context that failed, and the GTK **Vulkan** renderer is known to
|
||||
|
||||
@@ -380,3 +380,12 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
figure row carries **no** toggle (and no menu entry): there is nothing to enlarge *to*, and a
|
||||
visible no-op is worse than none. The button reappears while a row *is* maximized, because
|
||||
there it is the way back.
|
||||
|
||||
- **A view's arrangement is the view's, editor included.** A scope window holds its own figure
|
||||
rows and edits them with its own `+`/`×`/Done set (`set_scope_layout_editing()`,
|
||||
`rebuild_scope_layout_editor()`, `sync_scope_layout_editor()`): the buttons are tagged with the
|
||||
window and the handlers act on *that* window's row set, so the two editors cannot move each
|
||||
other's figures. `L` acts on the window the key arrived in (a `GtkEventControllerKey` per
|
||||
window; every other accelerator reaches the application through `run_app_action()`), and a
|
||||
pinned layout change is **not** written to the main window's session — the windows themselves
|
||||
are not persisted.
|
||||
@@ -1500,6 +1500,10 @@ a defect in the copy. Measured output:
|
||||
is the defect this exists for: `pin_event_window()` takes **spans**, and passing it a range's
|
||||
signed `min` turned the window inside out, so a scope in Held or Paused mode drew no trace and no
|
||||
x labels at all.
|
||||
- `layout buttons=3 rows=1->2` is the window's **own layout editor**: the toggle builds
|
||||
`2 × rows + 1` buttons (a `×` per row, a `+` per boundary, Done is not counted) for one figure,
|
||||
and emitting the first boundary `+` adds a row. A wrong count is a button set built for the main
|
||||
window's rows; `rows` unchanged is a handler writing the wrong row set.
|
||||
- `rclick=1` is the scope window's **own right-click menu** being wired (one click gesture with
|
||||
button 3 on its GL area, beside the drop target): the menu's actions act on that window's row
|
||||
set.
|
||||
|
||||
+264
-18
@@ -645,16 +645,15 @@ void open_scope_window(App& app, int figure_row, bool detached) {
|
||||
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);
|
||||
// The overlay holds every corner control this window has: the attach button (detached windows
|
||||
// only), the layout toggle, and the layout editor's own buttons. It is **always** created, not
|
||||
// only for detached windows, because a scope is a view with its own arrangement and every one of
|
||||
// them can be rearranged. The motion controller lives on the overlay, so a revealed button sits
|
||||
// *under* the pointer and the event still travels the ancestor -- a leave on the GL widget would
|
||||
// fire the moment the button appeared and hide it again.
|
||||
scope->overlay = gtk_overlay_new();
|
||||
gtk_overlay_set_child(GTK_OVERLAY(scope->overlay), scope->widget);
|
||||
gtk_window_set_child(scope->window, scope->overlay);
|
||||
GtkEventController* motion = gtk_event_controller_motion_new();
|
||||
g_signal_connect(motion, "motion", G_CALLBACK(+[](GtkEventControllerMotion*, double x,
|
||||
double y, gpointer user) {
|
||||
@@ -668,7 +667,9 @@ void open_scope_window(App& app, int figure_row, bool detached) {
|
||||
static_cast<App::ScopeWindow*>(user)->pointer_valid = false;
|
||||
}),
|
||||
scope.get());
|
||||
gtk_widget_add_controller(scope->attach_overlay, GTK_EVENT_CONTROLLER(motion));
|
||||
gtk_widget_add_controller(scope->overlay, GTK_EVENT_CONTROLLER(motion));
|
||||
if (detached) {
|
||||
// Only a detached window has something to put back.
|
||||
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");
|
||||
@@ -686,11 +687,34 @@ void open_scope_window(App& app, int figure_row, bool detached) {
|
||||
attach_scope_window(a, *scope);
|
||||
}),
|
||||
&app);
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(scope->attach_overlay), attach);
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(scope->overlay), attach);
|
||||
scope->attach_buttons.push_back(attach);
|
||||
} else {
|
||||
gtk_window_set_child(scope->window, scope->widget);
|
||||
}
|
||||
// The layout toggle: the editor's door, the same one the main window's overflow menu carries,
|
||||
// revealed by the same proximity rule.
|
||||
scope->layout_toggle = gtk_button_new_from_icon_name("view-grid-symbolic");
|
||||
gtk_widget_set_tooltip_text(scope->layout_toggle,
|
||||
"layout editor: add and remove figure rows in this window");
|
||||
gtk_widget_add_css_class(scope->layout_toggle, "circular");
|
||||
gtk_widget_add_css_class(scope->layout_toggle, "osd");
|
||||
gtk_widget_set_size_request(scope->layout_toggle, kLayoutButtonPx, kLayoutButtonPx);
|
||||
gtk_widget_set_visible(scope->layout_toggle, FALSE);
|
||||
g_object_set_data(G_OBJECT(scope->layout_toggle), "udpscope-scope", scope.get());
|
||||
g_signal_connect(scope->layout_toggle, "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;
|
||||
set_scope_layout_editing(a, *scope, !scope->layout_editing);
|
||||
}),
|
||||
&app);
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(scope->overlay), scope->layout_toggle);
|
||||
// The window's keyboard: the same accelerators the main window's keys carry. `L` acts on *this*
|
||||
// window's layout; every other action reaches the application (see `on_scope_key()`).
|
||||
GtkEventController* keys = gtk_event_controller_key_new();
|
||||
g_signal_connect(keys, "key-pressed", G_CALLBACK(on_scope_key), &app);
|
||||
gtk_widget_add_controller(GTK_WIDGET(scope->window), GTK_EVENT_CONTROLLER(keys));
|
||||
if (GtkApplication* application = gtk_window_get_application(GTK_WINDOW(app.window))) {
|
||||
gtk_window_set_application(scope->window, application);
|
||||
}
|
||||
@@ -1255,9 +1279,7 @@ void on_detach_clicked(GtkButton* b, gpointer user) {
|
||||
/// 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;
|
||||
}
|
||||
if (scope.overlay == nullptr || 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;
|
||||
@@ -1280,6 +1302,227 @@ void sync_scope_attach(App& app, App::ScopeWindow& scope) {
|
||||
scope.pointer_y <= by + kLayoutButtonPx + kMaximizeRevealPx;
|
||||
gtk_widget_set_visible(b, revealed);
|
||||
}
|
||||
if (scope.layout_toggle != nullptr) {
|
||||
// The layout toggle sits at the corner, to the left of the attach button when there is one.
|
||||
const double bx = std::clamp(static_cast<double>(width - kLayoutButtonPx - kLayoutButtonPad -
|
||||
(scope.attach_buttons.empty()
|
||||
? 0
|
||||
: 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(scope.layout_toggle, GTK_ALIGN_START);
|
||||
gtk_widget_set_valign(scope.layout_toggle, GTK_ALIGN_START);
|
||||
gtk_widget_set_margin_start(scope.layout_toggle, static_cast<int>(bx));
|
||||
gtk_widget_set_margin_top(scope.layout_toggle, static_cast<int>(by));
|
||||
const bool revealed =
|
||||
!scope.layout_editing && 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(scope.layout_toggle, revealed);
|
||||
}
|
||||
// The attach button steps aside while the editor is up: Done is the way out of that mode.
|
||||
for (GtkWidget* b : scope.attach_buttons) {
|
||||
if (scope.layout_editing) gtk_widget_set_visible(b, FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
// --- the scope windows' own layout editor -----------------------------------------------------
|
||||
//
|
||||
// A scope window is a view with its own arrangement, and its arrangement is the user's: it holds
|
||||
// as many figure rows as it is given and edits them with the same controls the main window's
|
||||
// editor carries (`+` on a boundary, `×` on a row, Done to leave). The buttons are the window's
|
||||
// own -- the main editor's target is the main window -- so the two editors cannot move each
|
||||
// other's rows.
|
||||
|
||||
namespace {
|
||||
/// Data keys of a scope editor's buttons: the figure row a `×` removes, the boundary a `+` splits.
|
||||
constexpr const char* kScopeLayoutRowKey = "udpscope-scope-layout-row";
|
||||
constexpr const char* kScopeLayoutBoundaryKey = "udpscope-scope-layout-boundary";
|
||||
|
||||
App::ScopeWindow* scope_layout_scope(GtkWidget* w) {
|
||||
return static_cast<App::ScopeWindow*>(g_object_get_data(G_OBJECT(w), "udpscope-scope"));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void rebuild_scope_layout_editor(App& app, App::ScopeWindow& scope);
|
||||
void set_scope_layout_editing(App& app, App::ScopeWindow& scope, bool on);
|
||||
|
||||
void clear_scope_layout_editor(App::ScopeWindow& scope) {
|
||||
if (scope.overlay != nullptr) {
|
||||
for (GtkWidget* b : scope.layout_buttons) {
|
||||
gtk_overlay_remove_overlay(GTK_OVERLAY(scope.overlay), b);
|
||||
}
|
||||
if (scope.layout_done != nullptr) {
|
||||
gtk_overlay_remove_overlay(GTK_OVERLAY(scope.overlay), scope.layout_done);
|
||||
}
|
||||
}
|
||||
scope.layout_buttons.clear();
|
||||
scope.layout_done = nullptr;
|
||||
scope.layout_buttons_rows = -1;
|
||||
}
|
||||
|
||||
/// One of the scope editor's buttons: the main editor's look (`circular`, `osd`, the button size)
|
||||
/// on this window's overlay.
|
||||
GtkWidget* scope_layout_button(App::ScopeWindow& scope, const char* icon, const char* tip) {
|
||||
GtkWidget* b = gtk_button_new_from_icon_name(icon);
|
||||
gtk_widget_set_tooltip_text(b, tip);
|
||||
gtk_widget_add_css_class(b, "circular");
|
||||
gtk_widget_add_css_class(b, "osd");
|
||||
gtk_widget_set_size_request(b, kLayoutButtonPx, kLayoutButtonPx);
|
||||
g_object_set_data(G_OBJECT(b), "udpscope-scope", &scope);
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(scope.overlay), b);
|
||||
scope.layout_buttons.push_back(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
void on_scope_layout_row_remove(GtkButton* b, gpointer user) {
|
||||
App& app = *static_cast<App*>(user);
|
||||
App::ScopeWindow* scope = scope_layout_scope(GTK_WIDGET(b));
|
||||
if (scope == nullptr || scope->rows == nullptr) return;
|
||||
const int fr = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kScopeLayoutRowKey)) - 1;
|
||||
outlive_the_rebuild(GTK_WIDGET(b));
|
||||
if (!scope->rows->remove_row(fr)) {
|
||||
set_notice(app, "a window keeps at least one figure", 4);
|
||||
return;
|
||||
}
|
||||
if (scope->area != nullptr) scope->area->set_figure(&scope->rows->figure());
|
||||
rebuild_scope_layout_editor(app, *scope);
|
||||
if (scope->widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(scope->widget));
|
||||
}
|
||||
|
||||
void on_scope_layout_row_insert(GtkButton* b, gpointer user) {
|
||||
App& app = *static_cast<App*>(user);
|
||||
App::ScopeWindow* scope = scope_layout_scope(GTK_WIDGET(b));
|
||||
if (scope == nullptr || scope->rows == nullptr) return;
|
||||
const int at = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kScopeLayoutBoundaryKey));
|
||||
outlive_the_rebuild(GTK_WIDGET(b));
|
||||
if (!scope->rows->insert_row(at)) {
|
||||
set_notice(app, "this window holds " + std::to_string(RowSet::kMaxRows) + " figures already", 4);
|
||||
return;
|
||||
}
|
||||
if (scope->area != nullptr) scope->area->set_figure(&scope->rows->figure());
|
||||
rebuild_scope_layout_editor(app, *scope);
|
||||
if (scope->widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(scope->widget));
|
||||
}
|
||||
|
||||
void on_scope_layout_done(GtkButton* b, gpointer user) {
|
||||
App& app = *static_cast<App*>(user);
|
||||
App::ScopeWindow* scope = scope_layout_scope(GTK_WIDGET(b));
|
||||
if (scope == nullptr) return;
|
||||
set_scope_layout_editing(app, *scope, false);
|
||||
}
|
||||
|
||||
void rebuild_scope_layout_editor(App& app, App::ScopeWindow& scope) {
|
||||
clear_scope_layout_editor(scope);
|
||||
if (!scope.layout_editing || scope.rows == nullptr || scope.overlay == nullptr) return;
|
||||
const int frows = scope.rows->figure_row_count();
|
||||
scope.layout_buttons_rows = frows;
|
||||
for (int fr = 0; fr < frows; ++fr) {
|
||||
GtkWidget* rm = scope_layout_button(scope, "window-close-symbolic", "remove this figure");
|
||||
g_object_set_data(G_OBJECT(rm), kScopeLayoutRowKey, GINT_TO_POINTER(fr + 1));
|
||||
g_signal_connect(rm, "clicked", G_CALLBACK(on_scope_layout_row_remove), &app);
|
||||
}
|
||||
for (int at = 0; at <= frows; ++at) {
|
||||
GtkWidget* add = scope_layout_button(scope, "list-add-symbolic", "add a figure here");
|
||||
g_object_set_data(G_OBJECT(add), kScopeLayoutBoundaryKey, GINT_TO_POINTER(at));
|
||||
g_signal_connect(add, "clicked", G_CALLBACK(on_scope_layout_row_insert), &app);
|
||||
}
|
||||
scope.layout_done = gtk_button_new_with_label("Done");
|
||||
gtk_widget_set_tooltip_text(scope.layout_done, "leave the layout editor");
|
||||
gtk_widget_add_css_class(scope.layout_done, "osd");
|
||||
gtk_widget_set_size_request(scope.layout_done, -1, kLayoutButtonPx);
|
||||
g_object_set_data(G_OBJECT(scope.layout_done), "udpscope-scope", &scope);
|
||||
g_signal_connect(scope.layout_done, "clicked", G_CALLBACK(on_scope_layout_done), &app);
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(scope.overlay), scope.layout_done);
|
||||
}
|
||||
|
||||
/// Restates where the editor's buttons sit, from the rects the last frame was laid out with: the
|
||||
/// `×` inside its row's left edge, the `+` on its boundary, Done in the corner.
|
||||
void sync_scope_layout_editor(App& app, App::ScopeWindow& scope) {
|
||||
if (!scope.layout_editing || scope.rows == nullptr || scope.area == nullptr ||
|
||||
scope.widget == nullptr) {
|
||||
return;
|
||||
}
|
||||
const int frows = scope.rows->figure_row_count();
|
||||
if (frows != scope.layout_buttons_rows) rebuild_scope_layout_editor(app, scope);
|
||||
const plot::FigureLayout& fl = scope.rows->figure().layout();
|
||||
if (static_cast<int>(fl.rows.size()) != frows) return;
|
||||
const double scale =
|
||||
scope.area->device_scale() > 0.0f ? static_cast<double>(scope.area->device_scale()) : 1.0;
|
||||
const double height = static_cast<double>(gtk_widget_get_height(scope.widget));
|
||||
const double width = static_cast<double>(gtk_widget_get_width(scope.widget));
|
||||
if (!(height > 0.0) || !(width > 0.0)) return;
|
||||
const double half = kLayoutButtonPx * 0.5;
|
||||
for (GtkWidget* b : scope.layout_buttons) {
|
||||
const int fr = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kScopeLayoutRowKey)) - 1;
|
||||
const int at = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kScopeLayoutBoundaryKey));
|
||||
gtk_widget_set_halign(b, GTK_ALIGN_START);
|
||||
gtk_widget_set_valign(b, GTK_ALIGN_START);
|
||||
if (fr >= 0) {
|
||||
if (fr >= frows) continue;
|
||||
const plot::Rect& row = fl.rows[static_cast<std::size_t>(fr)];
|
||||
gtk_widget_set_margin_top(
|
||||
b, static_cast<int>(std::clamp(static_cast<double>(row.y) / scale + kLayoutButtonPad, 0.0,
|
||||
height - kLayoutButtonPx) + 0.5));
|
||||
// Inside the axes, not over the y labels: the row's first cell's plot knows where they end.
|
||||
const plot::Plot* p = scope.rows->row(scope.rows->first_plot_of_row(fr)).plot;
|
||||
const double left =
|
||||
(p != nullptr ? static_cast<double>(p->layout().axes.x) / scale
|
||||
: static_cast<double>(row.x) / scale) + kLayoutButtonPad;
|
||||
gtk_widget_set_margin_start(b, static_cast<int>(std::clamp(left, 0.0, width - kLayoutButtonPx) + 0.5));
|
||||
} else if (at >= 0) {
|
||||
const double edge = at < frows
|
||||
? static_cast<double>(fl.rows[static_cast<std::size_t>(at)].y) / scale
|
||||
: (static_cast<double>(fl.rows.back().y) +
|
||||
static_cast<double>(fl.rows.back().h)) / scale;
|
||||
gtk_widget_set_margin_top(
|
||||
b, static_cast<int>(std::clamp(edge - half, 0.0, height - kLayoutButtonPx) + 0.5));
|
||||
gtk_widget_set_margin_start(b, static_cast<int>(kLayoutButtonPad));
|
||||
}
|
||||
}
|
||||
if (scope.layout_done != nullptr) {
|
||||
gtk_widget_set_halign(scope.layout_done, GTK_ALIGN_END);
|
||||
gtk_widget_set_valign(scope.layout_done, GTK_ALIGN_START);
|
||||
gtk_widget_set_margin_end(scope.layout_done, kLayoutButtonPad);
|
||||
gtk_widget_set_margin_top(scope.layout_done, kLayoutButtonPad);
|
||||
}
|
||||
}
|
||||
|
||||
void set_scope_layout_editing(App& app, App::ScopeWindow& scope, bool on) {
|
||||
if (scope.layout_editing == on) return;
|
||||
scope.layout_editing = on;
|
||||
if (on) {
|
||||
rebuild_scope_layout_editor(app, scope);
|
||||
} else {
|
||||
clear_scope_layout_editor(scope);
|
||||
}
|
||||
sync_scope_attach(app, scope);
|
||||
if (scope.widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(scope.widget));
|
||||
}
|
||||
|
||||
gboolean on_scope_key(GtkEventControllerKey* controller, guint keyval, guint, GdkModifierType state,
|
||||
gpointer user) {
|
||||
App& app = *static_cast<App*>(user);
|
||||
const Accel* accel = find_accel(keyval, static_cast<unsigned>(state));
|
||||
if (accel == nullptr) return FALSE;
|
||||
if (accel->action == AppAction::LayoutEditor) {
|
||||
// The window the key arrived in, not the main one: `L` is the layout editor of the view you
|
||||
// are looking at.
|
||||
GtkWidget* widget = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(controller));
|
||||
GtkWidget* window = widget != nullptr ? gtk_widget_get_ancestor(widget, GTK_TYPE_WINDOW) : nullptr;
|
||||
for (const std::unique_ptr<App::ScopeWindow>& scope : app.scopes) {
|
||||
if (GTK_WIDGET(scope->window) != window) continue;
|
||||
set_scope_layout_editing(app, *scope, !scope->layout_editing);
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
// Everything else is the application's, exactly as the main window's keys take it.
|
||||
run_app_action(app, accel->action);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/// **Detach**: the figure row moves to a window of its own. Copied first (the new window opens
|
||||
@@ -4650,7 +4893,10 @@ 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->detached || scope->layout_toggle != nullptr) sync_scope_attach(*app, *scope);
|
||||
// The window's own editor: buttons restated from the rects this frame was laid out with.
|
||||
// Its own arrangement is its own -- nothing here saves the main window's session.
|
||||
if (scope->layout_editing) sync_scope_layout_editor(*app, *scope);
|
||||
// **A window that cannot paint says so.** A blank plot is indistinguishable from a
|
||||
// context that failed or an area that never got a frame, and the report ("nothing is
|
||||
// drawn, no axes") is exactly what this turns into a sentence: the window's own title
|
||||
|
||||
+19
-4
@@ -225,10 +225,18 @@ struct App {
|
||||
/// 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;
|
||||
/// The overlay wrapping the GL widget: the corner controls (attach, the layout toggle, the
|
||||
/// layout editor's own buttons) all live on it. Its motion controller keeps the pointer state
|
||||
/// they are revealed by.
|
||||
GtkWidget* overlay = nullptr;
|
||||
/// Layout editing in this window: its own editor and toggle. A scope is a view with its own
|
||||
/// arrangement, so it holds as many figure rows as it is given and edits them with the same
|
||||
/// `+` / `×` / Done controls the main window's editor has -- its own, never the main window's.
|
||||
bool layout_editing = false;
|
||||
GtkWidget* layout_toggle = nullptr;
|
||||
std::vector<GtkWidget*> layout_buttons;
|
||||
GtkWidget* layout_done = nullptr;
|
||||
int layout_buttons_rows = -1;
|
||||
std::vector<GtkWidget*> attach_buttons;
|
||||
double pointer_x = 0.0;
|
||||
double pointer_y = 0.0;
|
||||
@@ -2077,6 +2085,13 @@ gboolean on_scope_drop_channel(GtkDropTarget*, const GValue* value, double x, do
|
||||
/// drop target both end here). False, with a notice, when the signal cannot be acquired or the plot
|
||||
/// holds four signals already.
|
||||
bool assign_signal_to_scope(App& app, App::ScopeWindow& scope, std::size_t signal, int row);
|
||||
/// Runs one keyboard accelerator. Scope windows carry the same shortcuts as the main window: the
|
||||
/// view-level ones (`L` -- the layout editor) act on the window the key arrived in; the rest reach
|
||||
/// the application through `run_app_action()`.
|
||||
gboolean on_scope_key(GtkEventControllerKey* controller, guint keyval, guint keycode,
|
||||
GdkModifierType state, gpointer user);
|
||||
/// Turns a scope window's own layout editor on or off, building or taking down its buttons.
|
||||
void set_scope_layout_editing(App& app, App::ScopeWindow& scope, bool on);
|
||||
/// The scope window's own right-click menu: the same row menu's actions, aimed at **that
|
||||
/// window's** row set.
|
||||
void on_scope_right_click(GtkGestureClick* gesture, int n_press, double x, double y,
|
||||
|
||||
@@ -1960,6 +1960,20 @@ gboolean scope_probe_step(gpointer user) {
|
||||
scope_pin.max, scope_x.min, scope_x.max);
|
||||
std::fflush(stderr);
|
||||
a->rows->set_time_window(2.0); // unpin: leave the window as the run found it
|
||||
// **The scope window's own layout editor**: the toggle builds its buttons and a `+` adds a
|
||||
// figure row -- the arrangement of a view is the view's.
|
||||
set_scope_layout_editing(*a, *a->scopes[1], true);
|
||||
const std::size_t layout_buttons = a->scopes[1]->layout_buttons.size();
|
||||
const int rows_before = a->scopes[1]->rows->row_count();
|
||||
if (layout_buttons > static_cast<std::size_t>(rows_before)) {
|
||||
g_signal_emit_by_name(a->scopes[1]->layout_buttons[static_cast<std::size_t>(rows_before)],
|
||||
"clicked");
|
||||
}
|
||||
const int rows_after = a->scopes[1]->rows->row_count();
|
||||
set_scope_layout_editing(*a, *a->scopes[1], false);
|
||||
std::fprintf(stderr, "[scope-probe] layout buttons=%zu rows=%d->%d\n", layout_buttons,
|
||||
rows_before, rows_after);
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
default:
|
||||
|
||||
Reference in new issue
Block a user